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 ankiDeck string ankiModel string } type ( errMsg error definitionMsg string wordAddedMsg string ) func initialModel(c *http.Client, db *sql.DB, ankiDeck, ankiModel string) 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, ankiDeck: ankiDeck, ankiModel: ankiModel, } } 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.ankiDeck, m.ankiModel, 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, "