mirror of
https://github.com/anotherhadi/default-creds-tui.git
synced 2026-10-05 10:58:25 +02:00
+372
@@ -0,0 +1,372 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/list"
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/anotherhadi/ilovetui/app"
|
||||
"github.com/anotherhadi/ilovetui/bubbles"
|
||||
"github.com/anotherhadi/ilovetui/helpbar"
|
||||
"github.com/anotherhadi/ilovetui/notification"
|
||||
"github.com/anotherhadi/ilovetui/style"
|
||||
|
||||
"github.com/anotherhadi/default-creds-tui/internal/config"
|
||||
"github.com/anotherhadi/default-creds-tui/internal/creds"
|
||||
)
|
||||
|
||||
const (
|
||||
modeLocal = creds.Local
|
||||
modeOnline = creds.Online
|
||||
)
|
||||
|
||||
type (
|
||||
localLoadedMsg struct {
|
||||
entries []creds.Entry
|
||||
err error
|
||||
}
|
||||
statsMsg struct {
|
||||
manufacturers, credentials int
|
||||
err error
|
||||
}
|
||||
onlineResultsMsg struct {
|
||||
gen int
|
||||
entries []creds.Entry
|
||||
total int
|
||||
err error
|
||||
}
|
||||
searchTickMsg struct{ gen int }
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
mode creds.Mode
|
||||
apiURL string
|
||||
printOnEnter bool
|
||||
printEntry *creds.Entry
|
||||
|
||||
list list.Model
|
||||
help helpbar.Model
|
||||
searchInput textinput.Model
|
||||
searching bool
|
||||
|
||||
allLocal []creds.Entry
|
||||
statsLoaded bool
|
||||
statsManufacturers, statsCredentials int
|
||||
|
||||
query string
|
||||
totalResults int
|
||||
loading bool
|
||||
searchGen int
|
||||
|
||||
width, height int
|
||||
|
||||
ModalOpen bool
|
||||
}
|
||||
|
||||
func New(mode creds.Mode, apiURL string, printOnEnter bool) Model {
|
||||
l := bubbles.NewList(nil, 0, 0)
|
||||
l.SetDelegate(entryDelegate{})
|
||||
l.SetShowHelp(false)
|
||||
l.SetFilteringEnabled(false)
|
||||
l.SetShowStatusBar(false)
|
||||
l.SetShowTitle(false)
|
||||
l.DisableQuitKeybindings()
|
||||
|
||||
gk := config.Global.Keybindings.Global
|
||||
l.KeyMap.CursorUp = gk.Up
|
||||
l.KeyMap.CursorDown = gk.Down
|
||||
l.KeyMap.GoToStart = gk.GotoTop
|
||||
l.KeyMap.GoToEnd = gk.GotoBottom
|
||||
|
||||
h := helpbar.New(helpbar.WithToggle(gk.Help))
|
||||
|
||||
si := bubbles.NewTextInput()
|
||||
if style.S.NerdFonts {
|
||||
si.Prompt = searchIcon + " "
|
||||
}
|
||||
si.Placeholder = "search a product, manufacturer, tag…"
|
||||
|
||||
focus := config.Global.App.FocusSearchOnLaunch
|
||||
if focus {
|
||||
si.Focus()
|
||||
}
|
||||
|
||||
return Model{
|
||||
mode: mode,
|
||||
apiURL: apiURL,
|
||||
printOnEnter: printOnEnter,
|
||||
list: l,
|
||||
help: h,
|
||||
searchInput: si,
|
||||
searching: focus,
|
||||
}
|
||||
}
|
||||
|
||||
func (m Model) PrintEntry() (creds.Entry, bool) {
|
||||
if m.printEntry == nil {
|
||||
return creds.Entry{}, false
|
||||
}
|
||||
return *m.printEntry, true
|
||||
}
|
||||
|
||||
func (m Model) printSelectedAndQuit() (Model, tea.Cmd) {
|
||||
e, ok := m.selectedEntry()
|
||||
if !ok {
|
||||
return m, nil
|
||||
}
|
||||
m.printEntry = &e
|
||||
return m, app.Quit()
|
||||
}
|
||||
|
||||
func (m Model) Init() tea.Cmd {
|
||||
cmds := []tea.Cmd{app.SetTitle("Default Creds TUI")}
|
||||
switch m.mode {
|
||||
case modeLocal:
|
||||
cmds = append(cmds, loadLocalCmd)
|
||||
default:
|
||||
cmds = append(cmds, fetchStatsCmd(m.apiURL))
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
|
||||
case tea.WindowSizeMsg:
|
||||
m.width, m.height = msg.Width, msg.Height
|
||||
m.help.SetWidth(msg.Width)
|
||||
m.searchInput.SetWidth(max(msg.Width-6, 10))
|
||||
m.resizeList()
|
||||
return m, nil
|
||||
|
||||
case localLoadedMsg:
|
||||
if msg.err != nil {
|
||||
return m, errorToast(msg.err)
|
||||
}
|
||||
m.allLocal = msg.entries
|
||||
m.statsManufacturers, m.statsCredentials = creds.LocalStats(msg.entries)
|
||||
m.statsLoaded = true
|
||||
cmd := m.runSearch()
|
||||
return m, cmd
|
||||
|
||||
case statsMsg:
|
||||
if msg.err != nil {
|
||||
return m, nil
|
||||
}
|
||||
m.statsManufacturers, m.statsCredentials = msg.manufacturers, msg.credentials
|
||||
m.statsLoaded = true
|
||||
return m, nil
|
||||
|
||||
case searchTickMsg:
|
||||
if msg.gen != m.searchGen {
|
||||
return m, nil
|
||||
}
|
||||
return m, fetchOnlineCmd(m.apiURL, m.query, msg.gen)
|
||||
|
||||
case onlineResultsMsg:
|
||||
if msg.gen != m.searchGen {
|
||||
return m, nil
|
||||
}
|
||||
m.loading = false
|
||||
if msg.err != nil {
|
||||
return m, errorToast(msg.err)
|
||||
}
|
||||
m.totalResults = msg.total
|
||||
cmd := m.setEntries(msg.entries)
|
||||
return m, cmd
|
||||
|
||||
case tea.KeyPressMsg:
|
||||
if m.ModalOpen {
|
||||
return m, nil
|
||||
}
|
||||
if m.searching {
|
||||
return m.updateSearch(msg)
|
||||
}
|
||||
return m.updateList(msg)
|
||||
|
||||
case tea.MouseWheelMsg:
|
||||
if m.ModalOpen {
|
||||
return m, nil
|
||||
}
|
||||
switch msg.Button {
|
||||
case tea.MouseWheelUp:
|
||||
m.list.CursorUp()
|
||||
case tea.MouseWheelDown:
|
||||
m.list.CursorDown()
|
||||
}
|
||||
return m, nil
|
||||
|
||||
case tea.MouseClickMsg:
|
||||
if m.ModalOpen || msg.Mouse().Button != tea.MouseLeft {
|
||||
return m, nil
|
||||
}
|
||||
return m.updateMouseClick(msg)
|
||||
}
|
||||
|
||||
if m.ModalOpen {
|
||||
return m, nil
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) updateList(msg tea.KeyPressMsg) (Model, tea.Cmd) {
|
||||
gk := config.Global.Keybindings.Global
|
||||
sk := config.Global.Keybindings.Search
|
||||
rk := config.Global.Keybindings.Results
|
||||
|
||||
switch {
|
||||
case key.Matches(msg, gk.Quit):
|
||||
return m, app.Quit()
|
||||
case key.Matches(msg, gk.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
m.resizeList()
|
||||
return m, nil
|
||||
case key.Matches(msg, sk.Focus):
|
||||
m.searching = true
|
||||
return m, m.searchInput.Focus()
|
||||
case key.Matches(msg, gk.Escape):
|
||||
return m, app.Quit()
|
||||
case key.Matches(msg, rk.Refresh):
|
||||
cmd := m.runSearch()
|
||||
return m, cmd
|
||||
case key.Matches(msg, rk.CopyUser):
|
||||
return m, m.copySelected(fieldUser)
|
||||
case key.Matches(msg, rk.CopyPass):
|
||||
return m, m.copySelected(fieldPass)
|
||||
case key.Matches(msg, rk.Enter):
|
||||
if !m.printOnEnter {
|
||||
return m, m.copySelected(fieldPass)
|
||||
}
|
||||
return m.printSelectedAndQuit()
|
||||
case key.Matches(msg, rk.PrintAndQuit):
|
||||
return m.printSelectedAndQuit()
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.list, cmd = m.list.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m Model) View() string {
|
||||
header := m.renderHeader()
|
||||
helpView := strings.TrimRight(m.help.View(m.helpBindings()...), "\n")
|
||||
|
||||
var body string
|
||||
if strings.TrimSpace(m.query) == "" {
|
||||
body = m.renderDefaultView(m.bodyHeight())
|
||||
} else {
|
||||
body = strings.TrimRight(m.renderResultsBody(), "\n")
|
||||
}
|
||||
|
||||
return strings.Join([]string{header, body, helpView}, "\n")
|
||||
}
|
||||
|
||||
func (m Model) renderResultsBody() string {
|
||||
if m.loading {
|
||||
return lipgloss.Place(m.width, max(m.bodyHeight(), 1), lipgloss.Center, lipgloss.Center,
|
||||
mutedStyle.Render("searching…"))
|
||||
}
|
||||
if len(m.list.Items()) == 0 {
|
||||
innerWidth := max(m.width-4, 10)
|
||||
wrapped := lipgloss.NewStyle().Width(innerWidth).Align(lipgloss.Center)
|
||||
text := strings.Join([]string{
|
||||
wrapped.Foreground(style.S.Muted).Render("no results for \"" + m.query + "\""),
|
||||
"",
|
||||
wrapped.Foreground(style.S.Subtle).Render("you can contribute new credentials at github.com/anotherhadi/default-creds"),
|
||||
}, "\n")
|
||||
return lipgloss.Place(m.width, max(m.bodyHeight(), 1), lipgloss.Center, lipgloss.Center, text)
|
||||
}
|
||||
lines := []string{m.renderResultsCountLine(), m.list.View()}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func (m Model) renderResultsCountLine() string {
|
||||
shown := len(m.list.Items())
|
||||
text := ""
|
||||
switch {
|
||||
case m.mode == modeOnline && m.totalResults > shown:
|
||||
text = fmt.Sprintf("showing %d of %d results, refine your search", shown, m.totalResults)
|
||||
case shown == 1:
|
||||
text = "1 result"
|
||||
default:
|
||||
text = fmt.Sprintf("%d results", shown)
|
||||
}
|
||||
return mutedStyle.Render(" " + text)
|
||||
}
|
||||
|
||||
func (m Model) helpBindings() []key.Binding {
|
||||
pages := config.Help(config.Global.Keybindings.Global, config.Global.Keybindings.Search, config.Global.Keybindings.Results)
|
||||
if m.help.ShowAll {
|
||||
var all []key.Binding
|
||||
for _, col := range pages.FullHelp() {
|
||||
all = append(all, col...)
|
||||
}
|
||||
return all
|
||||
}
|
||||
return pages.ShortHelp()
|
||||
}
|
||||
|
||||
func (m Model) headerHeight() int {
|
||||
return lipgloss.Height(m.renderHeader())
|
||||
}
|
||||
|
||||
func (m Model) bodyHeight() int {
|
||||
helpH := m.help.Height(m.helpBindings()...)
|
||||
h := m.height - m.headerHeight() - helpH
|
||||
if strings.TrimSpace(m.query) != "" && len(m.list.Items()) > 0 {
|
||||
h--
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func (m *Model) resizeList() {
|
||||
m.list.SetSize(m.width, m.bodyHeight())
|
||||
m.list.SetSize(m.width, m.bodyHeight())
|
||||
}
|
||||
|
||||
func (m *Model) setEntries(entries []creds.Entry) tea.Cmd {
|
||||
items := make([]list.Item, len(entries))
|
||||
for i, e := range entries {
|
||||
items[i] = e
|
||||
}
|
||||
cmd := m.list.SetItems(items)
|
||||
m.resizeList()
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (m Model) selectedEntry() (creds.Entry, bool) {
|
||||
if item := m.list.SelectedItem(); item != nil {
|
||||
return item.(creds.Entry), true
|
||||
}
|
||||
return creds.Entry{}, false
|
||||
}
|
||||
|
||||
func loadLocalCmd() tea.Msg {
|
||||
entries, err := creds.LoadLocal(creds.LocalDataDir())
|
||||
return localLoadedMsg{entries: entries, err: err}
|
||||
}
|
||||
|
||||
func fetchStatsCmd(apiURL string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
manufacturers, credentials, err := creds.FetchStats(apiURL)
|
||||
return statsMsg{manufacturers: manufacturers, credentials: credentials, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchOnlineCmd(apiURL, query string, gen int) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
entries, total, err := creds.FetchOnline(apiURL, query, 50)
|
||||
return onlineResultsMsg{gen: gen, entries: entries, total: total, err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func errorToast(err error) tea.Cmd {
|
||||
return notification.Show("Error", err.Error(), notification.Error, notification.WithID("action-error"))
|
||||
}
|
||||
Reference in New Issue
Block a user