From 10f8e8c5ea5d3dd191d7e51682efc237d34cded4 Mon Sep 17 00:00:00 2001 From: David Schlachter Date: Thu, 8 Jan 2026 10:44:50 -0500 Subject: Move all the UI stuff to a new file --- ui.go | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 ui.go (limited to 'ui.go') diff --git a/ui.go b/ui.go new file mode 100644 index 0000000..966243b --- /dev/null +++ b/ui.go @@ -0,0 +1,160 @@ +package main + +import ( + "database/sql" + "errors" + "fmt" + "net/http" + "regexp" + "strings" + + "github.com/charmbracelet/bubbles/textinput" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/microcosm-cc/bluemonday" + "github.com/muesli/reflow/wordwrap" +) + +type model struct { + wordInput textinput.Model + err error + currentWord string + currentDefinition string + db *sql.DB + c *http.Client + wordAddStatus string + p bluemonday.Policy + vp viewport.Model +} + +type ( + errMsg error + definitionMsg string + wordAddedMsg string +) + +func initialModel(c *http.Client, db *sql.DB) model { + ti := textinput.New() + ti.Placeholder = "" + ti.Focus() + ti.CharLimit = 156 + ti.Width = 36 + vp := viewport.New(80, 30) + + return model{ + wordInput: ti, + err: nil, + db: db, + c: c, + wordAddStatus: "", + p: *bluemonday.StrictPolicy(), + vp: vp, + } +} + +func (m model) Init() tea.Cmd { + return textinput.Blink +} + +func lookupWord(db *sql.DB, word string) tea.Cmd { + return func() tea.Msg { + var definition string + row := db.QueryRow(`select definition from words where word = ? limit 1`, word) + err := row.Scan(&definition) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return definitionMsg("") + } + return errMsg(fmt.Errorf("looking up '%s': %s", word, err)) + } + return definitionMsg(definition) + } +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + var textPickerLongerCmds []tea.Cmd + + switch msg := msg.(type) { + case definitionMsg: + m.currentDefinition = string(msg) + m.err = nil + m.vp.SetContent(formatDefinitionForDisplay(m.p, m.currentDefinition, m.vp.Width)) + return m, nil + case wordAddedMsg: + m.wordAddStatus = fmt.Sprintf("✅ Added '%s' to Anki", string(msg)) + m.currentWord = "" + m.currentDefinition = "" + m.wordInput.SetValue("") + m.vp.SetContent("") + m.err = nil + case tea.WindowSizeMsg: + // headerHeight is the height of everything above the definition window. + headerHeight := 11 + m.vp.Width = msg.Width + m.vp.Height = msg.Height - headerHeight + m.vp.SetContent(formatDefinitionForDisplay(m.p, m.currentDefinition, m.vp.Width)) + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC: + return m, tea.Quit + case tea.KeyEsc: + m.currentWord = "" + m.currentDefinition = "" + m.wordInput.SetValue("") + m.vp.SetContent("") + case tea.KeyEnter: + return m, addCard(m.c, m.currentWord, m.currentDefinition) + } + + case errMsg: + m.err = msg + return m, nil + } + + if m.wordInput.Value() != m.currentWord { + m.currentWord = m.wordInput.Value() + textPickerLongerCmds = append(textPickerLongerCmds, lookupWord(m.db, m.currentWord)) + } + + var vpCmd tea.Cmd + m.vp, vpCmd = m.vp.Update(msg) + textPickerLongerCmds = append(textPickerLongerCmds, vpCmd) + + m.wordInput, cmd = m.wordInput.Update(msg) + textPickerLongerCmds = append(textPickerLongerCmds, cmd) + return m, tea.Batch(textPickerLongerCmds...) +} + +var whitespaceTrimmerRe = regexp.MustCompile(`^[ \t]*$`) + +func (m model) View() string { + return fmt.Sprintf( + "\x1b[1;30;42mLook up a word:\x1b[0m\n\n%s\n\n\x1b[1;30;42mStatus:\x1b[0m %s\n\n%s\n\n%s\n%s", + m.wordInput.View(), + formatStatus(m.err, m.wordAddStatus), + "(ctrl-c to quit, esc to clear, enter to add to Anki)", + "\x1b[1;30;42mCurrent definition:\x1b[0m\n", + m.vp.View(), + ) + "\n" +} + +func formatDefinitionForDisplay(policy bluemonday.Policy, definition string, maxWidth int) string { + str := strings.ReplaceAll(definition, "
  • ", "
  • - ") + str = strings.ReplaceAll(str, "\t
  • \x1b[0m") + str = policy.Sanitize(str) + str = strings.ReplaceAll(str, "\t- ", "\x1b[0;33;49m•\x1b[0m ") + + width := min(maxWidth, 80) + + return wordwrap.String(str, width) +} + +func formatStatus(lastError error, lastSuccess string) string { + if lastError == nil { + return lastSuccess + } + return fmt.Sprintf("\x1b[0;31;49m%s\x1b[0m", lastError.Error()) +} -- cgit v1.2.3