summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--main.go155
-rw-r--r--ui.go160
3 files changed, 163 insertions, 156 deletions
diff --git a/.gitignore b/.gitignore
index f34bcf3..dbf363f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
raw-wiktextract-data.jsonl
-raw-wiktextract-data.sqlite3
-raw-wiktextract-data.sqlite3-journal
+dictionary.sqlite3
+dictionary.sqlite3-journal \ No newline at end of file
diff --git a/main.go b/main.go
index aa716e5..e73b06f 100644
--- a/main.go
+++ b/main.go
@@ -3,176 +3,23 @@
package main
import (
- "database/sql"
- "errors"
- "fmt"
"log"
"net/http"
- "regexp"
- "strings"
"time"
- "github.com/charmbracelet/bubbles/textinput"
- "github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
_ "github.com/mattn/go-sqlite3"
- "github.com/microcosm-cc/bluemonday"
- "github.com/muesli/reflow/wordwrap"
)
const (
rawDictionary = "/home/david/work/french-wiktionary-flashcards/raw-wiktextract-data.jsonl"
- dictionary = "/home/david/work/french-wiktionary-flashcards/raw-wiktextract-data.sqlite3"
+ dictionary = "dictionary.sqlite3"
apiURL = "http://localhost:8765"
deckName = "Français"
modelName = "Basic-830ae"
)
-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())
-}
-
func main() {
db, err := setupDatabase()
if err != nil {
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())
+}