summaryrefslogtreecommitdiff
path: root/ui.go
diff options
context:
space:
mode:
Diffstat (limited to 'ui.go')
-rw-r--r--ui.go160
1 files changed, 160 insertions, 0 deletions
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, "<li class=sense>", "<li class=sense>- ")
+ str = strings.ReplaceAll(str, "\t<ul><li><i>", "\n\t<ul><li><i>\x1b[3;39;49m")
+ str = strings.ReplaceAll(str, "</i></li></ul></li>", "</i></li></ul></li>\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())
+}