summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--add.go106
-rw-r--r--go.mod28
-rw-r--r--go.sum55
-rw-r--r--main.go135
-rw-r--r--setup.go15
5 files changed, 252 insertions, 87 deletions
diff --git a/add.go b/add.go
index 40c45ee..2b358fe 100644
--- a/add.go
+++ b/add.go
@@ -6,54 +6,86 @@ import (
"fmt"
"io"
"net/http"
+
+ tea "github.com/charmbracelet/bubbletea"
)
const apiVersion = 6
-func addCard(c *http.Client, front, back string) error {
- noteRequest := addNote{
- Action: "addNote",
- Version: apiVersion,
- Params: addNoteParams{
- Note: note{
- DeckName: deckName,
- ModelName: modelName,
- Fields: fields{
- Front: front,
- Back: back,
- },
- Options: options{
- AllowDuplicate: false,
- DuplicateScope: "deck",
+type addNote struct {
+ Action string `json:"action"`
+ Version int `json:"version"`
+ Params addNoteParams `json:"params"`
+}
+
+type addNoteParams struct {
+ Note note `json:"note"`
+}
+
+type note struct {
+ DeckName string `json:"deckName"`
+ ModelName string `json:"modelName"`
+ // Fields will not be trivial to generalize
+ Fields fields `json:"fields"`
+ Options options `json:"options"`
+}
+
+type fields struct {
+ Front string `json:"Front"`
+ Back string `json:"Back"`
+}
+
+type options struct {
+ AllowDuplicate bool `json:"allowDuplicate"`
+ DuplicateScope string `json:"duplicateScope"`
+}
+
+func addCard(c *http.Client, front, back string) tea.Cmd {
+ return func() tea.Msg {
+ noteRequest := addNote{
+ Action: "addNote",
+ Version: apiVersion,
+ Params: addNoteParams{
+ Note: note{
+ DeckName: deckName,
+ ModelName: modelName,
+ Fields: fields{
+ Front: front,
+ Back: back,
+ },
+ Options: options{
+ AllowDuplicate: false,
+ DuplicateScope: "deck",
+ },
},
},
- },
- }
+ }
- jsonBytes, err := json.Marshal(noteRequest)
- if err != nil {
- return fmt.Errorf("marshaling JSON: %s", err)
- }
+ jsonBytes, err := json.Marshal(noteRequest)
+ if err != nil {
+ return errMsg(fmt.Errorf("marshaling JSON: %s", err))
+ }
- req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBytes))
- req.Header.Set("Content-Type", "application/json")
+ req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonBytes))
+ req.Header.Set("Content-Type", "application/json")
- resp, err := c.Do(req)
- if err != nil {
- return fmt.Errorf("making request: %s", err)
- }
- defer resp.Body.Close()
+ resp, err := c.Do(req)
+ if err != nil {
+ return errMsg(fmt.Errorf("making request: %s", err))
+ }
+ defer resp.Body.Close()
- body, _ := io.ReadAll(resp.Body)
+ body, _ := io.ReadAll(resp.Body)
- var jsonResp struct {
- Error string `json:"error"`
- }
+ var jsonResp struct {
+ Error string `json:"error"`
+ }
- json.Unmarshal(body, &jsonResp)
- if jsonResp.Error != "" {
- return fmt.Errorf("creating card: %s", jsonResp.Error)
- }
+ json.Unmarshal(body, &jsonResp)
+ if jsonResp.Error != "" {
+ return errMsg(fmt.Errorf("creating card: %s", jsonResp.Error))
+ }
- return nil
+ return wordAddedMsg(front)
+ }
}
diff --git a/go.mod b/go.mod
index 61d6704..3bf4820 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,34 @@ module davidschlachter.com/french-wiktionary-flashcards
go 1.24.1
require (
+ github.com/charmbracelet/bubbles v0.21.0
+ github.com/charmbracelet/bubbletea v1.3.10
github.com/goccy/go-json v0.10.5
github.com/mattn/go-sqlite3 v1.14.33
+ github.com/microcosm-cc/bluemonday v1.0.27
+)
+
+require (
+ github.com/atotto/clipboard v0.1.4 // indirect
+ github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
+ github.com/aymerick/douceur v0.2.0 // indirect
+ github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
+ github.com/charmbracelet/lipgloss v1.1.0 // indirect
+ github.com/charmbracelet/x/ansi v0.10.1 // indirect
+ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
+ github.com/charmbracelet/x/term v0.2.1 // indirect
+ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
+ github.com/gorilla/css v1.0.1 // indirect
+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/mattn/go-localereader v0.0.1 // indirect
+ github.com/mattn/go-runewidth v0.0.16 // indirect
+ github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
+ github.com/muesli/cancelreader v0.2.2 // indirect
+ github.com/muesli/termenv v0.16.0 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
+ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+ golang.org/x/net v0.26.0 // indirect
+ golang.org/x/sys v0.36.0 // indirect
+ golang.org/x/text v0.16.0 // indirect
)
diff --git a/go.sum b/go.sum
index 720c2c7..2c916fa 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,59 @@
+github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
+github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
+github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
+github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
+github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
+github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
+github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
+github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
+github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
+github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
+github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
+github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
+github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
+github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
+github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
+github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
+github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0=
github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
+github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
+github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
+github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
+github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
+github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
+github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
+golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
+golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ=
+golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
+golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
+golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
diff --git a/main.go b/main.go
index ea0f382..235be18 100644
--- a/main.go
+++ b/main.go
@@ -3,12 +3,16 @@
package main
import (
+ "database/sql"
+ "fmt"
"log"
"net/http"
- "os"
"time"
+ "github.com/charmbracelet/bubbles/textinput"
+ tea "github.com/charmbracelet/bubbletea"
_ "github.com/mattn/go-sqlite3"
+ "github.com/microcosm-cc/bluemonday"
)
const (
@@ -20,32 +24,99 @@ const (
modelName = "Basic-830ae"
)
-type addNote struct {
- Action string `json:"action"`
- Version int `json:"version"`
- Params addNoteParams `json:"params"`
+type model struct {
+ wordInput textinput.Model
+ err error
+ currentWord string
+ currentDefinition string
+ db *sql.DB
+ c *http.Client
+ wordAddStatus string
+ p bluemonday.Policy
}
-type addNoteParams struct {
- Note note `json:"note"`
+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
+
+ return model{
+ wordInput: ti,
+ err: nil,
+ db: db,
+ c: c,
+ wordAddStatus: "(Press 'Enter' to add this word and its definition to Anki)",
+ p: *bluemonday.StrictPolicy(),
+ }
}
-type note struct {
- DeckName string `json:"deckName"`
- ModelName string `json:"modelName"`
- // Fields will not be trivial to generalize
- Fields fields `json:"fields"`
- Options options `json:"options"`
+func (m model) Init() tea.Cmd {
+ return textinput.Blink
}
-type fields struct {
- Front string `json:"Front"`
- Back string `json:"Back"`
+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 {
+ return errMsg(fmt.Errorf("looking up '%s': %s", word, err))
+ }
+ return definitionMsg(definition)
+ }
}
-type options struct {
- AllowDuplicate bool `json:"allowDuplicate"`
- DuplicateScope string `json:"duplicateScope"`
+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)
+ return m, nil
+ case wordAddedMsg:
+ m.wordAddStatus = fmt.Sprintf("✅ Added '%s' to Anki", string(msg))
+ case tea.KeyMsg:
+ switch msg.Type {
+ case tea.KeyCtrlC, tea.KeyEsc:
+ return m, tea.Quit
+ 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))
+ }
+
+ m.wordInput, cmd = m.wordInput.Update(msg)
+ textPickerLongerCmds = append(textPickerLongerCmds, cmd)
+ return m, tea.Batch(textPickerLongerCmds...)
+}
+
+func (m model) View() string {
+ return fmt.Sprintf(
+ "Look up a word:\n\n%s\n\n%s\n%s\n\n%s\n%s",
+ m.wordInput.View(),
+ m.wordAddStatus,
+ "(esc to quit)",
+ "Current definition:\n",
+ m.p.Sanitize(m.currentDefinition),
+ ) + "\n"
}
func main() {
@@ -55,31 +126,11 @@ func main() {
}
defer db.Close()
- // We're going to start this app very simply! The first iteration will take
- // a word as its first command-line argument. We will search for the word in
- // the dictionary and create a new Anki card using the first exact match.
- //
- // In the future, we may make it easy for the user to edit cards (flag?),
- // and possibly implement a TUI to choose definitions more interactively as
- // well (e.g. search with partial matches).
- //
- // I would also like to make more things less hard-coded.
- if len(os.Args) < 2 {
- log.Fatalf("no word was provided")
- }
- word := os.Args[1]
-
- var definition string
- row := db.QueryRow(`select definition from words where word = ? limit 1`, word)
- err = row.Scan(&definition)
- if err != nil {
- log.Fatalf("looking up '%s': %s", word, err)
- }
-
c := http.DefaultClient
c.Timeout = 5 * time.Second
- if err := addCard(c, word, definition); err != nil {
- log.Fatalf("creating card: %s", err)
+ p := tea.NewProgram(initialModel(c, db))
+ if _, err := p.Run(); err != nil {
+ log.Fatal(err)
}
}
diff --git a/setup.go b/setup.go
index 83827c5..12a805c 100644
--- a/setup.go
+++ b/setup.go
@@ -88,15 +88,14 @@ func populateDictionary(db *sql.DB) error {
log.Printf("preparing list of dictionary words...")
// Set up the template
- tmpl, err := template.New("entry").Parse(`<p>{{ .Word }} {{ .Sound }} <i>{{ .POS }} {{ .Gender }}</i></p>
- <ol>
- {{ range .Senses}}
- <li>{{ .Sense }}<br>
- {{ if .Example }}
- <ul><li><i>{{ .Example }}</i></li></ul></li>
- {{ end }}
+ tmpl, err := template.New("entry").Parse(
+ `<p>{{ .Word }} {{ .Sound }} <i>{{ .POS }} {{ .Gender }}</i></p>
+<ol>{{ range .Senses}}
+ <li>{{ .Sense }}<br>
+ {{ if .Example }}
+ <ul><li><i>{{ .Example }}</i></li></ul></li>
{{ end }}
- </ol>`)
+{{ end }}</ol>`)
if err != nil {
panic(err)
}