summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--main.go8
-rw-r--r--setup.go234
-rw-r--r--ui.go55
3 files changed, 154 insertions, 143 deletions
diff --git a/main.go b/main.go
index f173107..696a101 100644
--- a/main.go
+++ b/main.go
@@ -41,16 +41,14 @@ func main() {
log.Fatalf("Failed to create or open dictionary at '%s': %s", *dict, err)
}
defer db.Close()
-
- err = setupDatabase(*rawDict, db)
- if err != nil {
- log.Fatalf("Failed to set up database: %s", err)
+ if err = setupTables(db); err != nil {
+ log.Fatalf("Failed to create database tables in dictionary '%s': %s", *dict, err)
}
c := http.DefaultClient
c.Timeout = 5 * time.Second
- p := tea.NewProgram(initialModel(c, db, *apiURL, *deckName, *modelName, *initialWord))
+ p := tea.NewProgram(initialModel(c, db, *apiURL, *deckName, *modelName, *rawDict, *initialWord))
if _, err := p.Run(); err != nil {
log.Fatalf("Unexpected error encountered while running program: %s", err)
}
diff --git a/setup.go b/setup.go
index 23c8bbe..1a69d45 100644
--- a/setup.go
+++ b/setup.go
@@ -5,43 +5,42 @@ import (
"database/sql"
"fmt"
"html/template"
- "log"
"os"
"strings"
+ tea "github.com/charmbracelet/bubbletea"
"github.com/goccy/go-json"
)
-func setupDatabase(rawDictionary string, db *sql.DB) error {
+func setupTables(db *sql.DB) error {
_, err := db.Exec("create table IF NOT EXISTS words (word text not null, definition text);")
if err != nil {
return fmt.Errorf("creating table: %s", err)
}
- row := db.QueryRow(`SELECT count(*) as count from words`)
- var count int
- err = row.Scan(&count)
- if err != nil {
- return fmt.Errorf("counting rows: %s", err)
- }
-
- // Only populate the database if it is empty.
- if count > 0 {
- return nil
- }
-
// Faster import performance.
_, err = db.Exec("PRAGMA synchronous = OFF;")
if err != nil {
return fmt.Errorf("setting risky writes: %s", err)
}
- if err = populateDictionary(rawDictionary, db); err != nil {
- return fmt.Errorf("failed to prepare dictionary: %s", err)
- }
return nil
}
+func isDatabaseEmpty(db *sql.DB) tea.Cmd {
+ return func() tea.Msg {
+ row := db.QueryRow(`SELECT count(*) as count from words`)
+ var count int
+ err := row.Scan(&count)
+ if err != nil {
+ return errMsg(fmt.Errorf("counting rows: %s", err))
+ }
+
+ // Only populate the database if it is empty.
+ return isDBEmptyMsg(count == 0)
+ }
+}
+
type rawDictionaryEntry struct {
Word string `json:"word"`
LangCode string `json:"lang_code"`
@@ -79,131 +78,126 @@ type SenseForDictionaryEntry struct {
Example string
}
-func populateDictionary(rawDictionary string, db *sql.DB) error {
- log.Printf("preparing sqlite database from raw dictionary data...")
-
- // Set up the template
- tmpl, err := template.New("entry").Parse(
- `<p>{{ .Word }} {{ .Sound }} <i>{{ .POS }} {{ .Gender }}</i></p>
+func populateDictionary(rawDictionary string, db *sql.DB) tea.Cmd {
+ return func() tea.Msg {
+ // Set up the template
+ tmpl, err := template.New("entry").Parse(
+ `<p>{{ .Word }} {{ .Sound }} <i>{{ .POS }} {{ .Gender }}</i></p>
<ol>{{ range .Senses}}
<li class=sense>{{ .Sense }}<br>
{{ if .Example }}<ul><li><i>{{ .Example }}</i></li></ul></li>{{ end }}
{{ end }}</ol>
{{ if .Etymology }}<p><i>Étymologie: {{ .Etymology }}</i>{{ end }}`)
- if err != nil {
- panic(err)
- }
+ if err != nil {
+ panic(err)
+ }
- tx, err := db.Begin()
- if err != nil {
- return fmt.Errorf("starting transaction: %w", err)
- }
+ tx, err := db.Begin()
+ if err != nil {
+ return errMsg(fmt.Errorf("starting transaction: %w", err))
+ }
- // Set up a prepared statement
- stmt, err := tx.Prepare("insert into words(word, definition) values(?, ?)")
- if err != nil {
- return fmt.Errorf("preparing statement: %w", err)
- }
- defer stmt.Close()
+ // Set up a prepared statement
+ stmt, err := tx.Prepare("insert into words(word, definition) values(?, ?)")
+ if err != nil {
+ return errMsg(fmt.Errorf("preparing statement: %w", err))
+ }
+ defer stmt.Close()
- file, err := os.Open(rawDictionary)
- if err != nil {
- return fmt.Errorf("opening: %w", err)
- }
- defer file.Close()
+ file, err := os.Open(rawDictionary)
+ if err != nil {
+ return errMsg(fmt.Errorf("opening: %w", err))
+ }
+ defer file.Close()
- var wordsAdded int
- scanner := bufio.NewScanner(file)
+ var wordsAdded int
+ scanner := bufio.NewScanner(file)
- maxCapacity := 2_000_000
- buf := make([]byte, maxCapacity)
- scanner.Buffer(buf, maxCapacity)
+ maxCapacity := 2_000_000
+ buf := make([]byte, maxCapacity)
+ scanner.Buffer(buf, maxCapacity)
- for scanner.Scan() {
- var result rawDictionaryEntry
- json.Unmarshal([]byte(scanner.Text()), &result)
- if result.LangCode != "fr" {
- continue
- }
+ for scanner.Scan() {
+ var result rawDictionaryEntry
+ json.Unmarshal([]byte(scanner.Text()), &result)
+ if result.LangCode != "fr" {
+ continue
+ }
- // Clean up the word. Replace apostrophes (common in phrases) with
- // single quotes (more likely to be typed by a user).
- result.Word = strings.ReplaceAll(result.Word, `’`, `'`)
+ // Clean up the word. Replace apostrophes (common in phrases) with
+ // single quotes (more likely to be typed by a user).
+ result.Word = strings.ReplaceAll(result.Word, `’`, `'`)
- // Create the definition text.
- entry := templateReadyDictionaryEntry{
- Word: result.Word,
- POS: strings.ToLower(result.POS),
- }
- if len(result.Etymology) > 0 {
- entry.Etymology = result.Etymology[0]
- }
- if len(result.Sounds) > 0 {
- entry.Sound = result.Sounds[0].IPA
- }
+ // Create the definition text.
+ entry := templateReadyDictionaryEntry{
+ Word: result.Word,
+ POS: strings.ToLower(result.POS),
+ }
+ if len(result.Etymology) > 0 {
+ entry.Etymology = result.Etymology[0]
+ }
+ if len(result.Sounds) > 0 {
+ entry.Sound = result.Sounds[0].IPA
+ }
- var genders, numbers []string
- for _, r := range result.Tags {
- switch r {
- case "masculine":
- genders = append(genders, "masculin")
- case "feminine":
- genders = append(genders, "féminin")
- case "plural":
- numbers = append(numbers, "pluriel")
- case "singular":
- numbers = append(numbers, "singulier")
+ var genders, numbers []string
+ for _, r := range result.Tags {
+ switch r {
+ case "masculine":
+ genders = append(genders, "masculin")
+ case "feminine":
+ genders = append(genders, "féminin")
+ case "plural":
+ numbers = append(numbers, "pluriel")
+ case "singular":
+ numbers = append(numbers, "singulier")
+ }
}
- }
- entry.Gender = strings.Join(
- []string{
- strings.Join(genders, " / "),
- strings.Join(numbers, " et "),
- },
- " ",
- )
-
- for _, s := range result.Senses {
- var example string
- if len(s.Examples) > 0 {
- example = s.Examples[0].Text
+ entry.Gender = strings.Join(
+ []string{
+ strings.Join(genders, " / "),
+ strings.Join(numbers, " et "),
+ },
+ " ",
+ )
+
+ for _, s := range result.Senses {
+ var example string
+ if len(s.Examples) > 0 {
+ example = s.Examples[0].Text
+ }
+ sense := strings.Join(s.Glosses, "; ")
+ entry.Senses = append(entry.Senses, SenseForDictionaryEntry{Sense: sense, Example: example})
}
- sense := strings.Join(s.Glosses, "; ")
- entry.Senses = append(entry.Senses, SenseForDictionaryEntry{Sense: sense, Example: example})
- }
- formattedDefinition := strings.Builder{}
- err := tmpl.Execute(&formattedDefinition, entry)
- if err != nil {
- return fmt.Errorf("failed to render: %w", err)
- }
+ formattedDefinition := strings.Builder{}
+ err := tmpl.Execute(&formattedDefinition, entry)
+ if err != nil {
+ return errMsg(fmt.Errorf("failed to render: %w", err))
+ }
- // Insert the entry
- _, err = stmt.Exec(entry.Word, formattedDefinition.String())
- if err != nil {
- return fmt.Errorf("inserting '%s': %w", entry.Word, err)
- }
+ // Insert the entry
+ _, err = stmt.Exec(entry.Word, formattedDefinition.String())
+ if err != nil {
+ return errMsg(fmt.Errorf("inserting '%s': %w", entry.Word, err))
+ }
- wordsAdded++
- if wordsAdded%100_000 == 0 && wordsAdded > 1 {
- log.Printf("processed %d lines (most recent word was '%s')", wordsAdded, entry.Word)
+ wordsAdded++
+
+ }
+ if err := scanner.Err(); err != nil {
+ return errMsg(fmt.Errorf("scanning: %w", err))
}
- }
- if err := scanner.Err(); err != nil {
- return fmt.Errorf("scanning: %w", err)
- }
+ if err := tx.Commit(); err != nil {
+ return errMsg(fmt.Errorf("committing: %w", err))
+ }
- if err := tx.Commit(); err != nil {
- return fmt.Errorf("committing: %w", err)
- }
+ _, err = db.Exec("create index wordindex on words(word);")
+ if err != nil {
+ return errMsg(fmt.Errorf("creating index: %s", err))
+ }
- _, err = db.Exec("create index wordindex on words(word);")
- if err != nil {
- return fmt.Errorf("creating index: %s", err)
+ return isDBEmptyMsg(false)
}
-
- log.Printf("prepared %d dictionary entries", wordsAdded)
-
- return nil
}
diff --git a/ui.go b/ui.go
index eb8d077..5e68171 100644
--- a/ui.go
+++ b/ui.go
@@ -23,9 +23,12 @@ type model struct {
wordInput textinput.Model
definitionViewport viewport.Model
- ankiDeck string
- ankiModel string
- apiURL string
+ ankiDeck string
+ ankiModel string
+ apiURL string
+ rawDictionaryPath string
+
+ dictionaryReady bool
currentWord string
currentDefinition string
@@ -37,9 +40,10 @@ type (
errMsg error
definitionMsg string
wordAddedMsg string
+ isDBEmptyMsg bool
)
-func initialModel(c *http.Client, db *sql.DB, apiURL, ankiDeck, ankiModel, firstWord string) model {
+func initialModel(c *http.Client, db *sql.DB, apiURL, ankiDeck, ankiModel, rawDictionary, firstWord string) model {
input := textinput.New()
input.Placeholder = ""
input.Focus()
@@ -70,9 +74,10 @@ func initialModel(c *http.Client, db *sql.DB, apiURL, ankiDeck, ankiModel, first
wordInput: input,
definitionViewport: textbox,
- ankiDeck: ankiDeck,
- ankiModel: ankiModel,
- apiURL: apiURL,
+ ankiDeck: ankiDeck,
+ ankiModel: ankiModel,
+ apiURL: apiURL,
+ rawDictionaryPath: rawDictionary,
currentWord: firstWord,
}
@@ -80,9 +85,7 @@ func initialModel(c *http.Client, db *sql.DB, apiURL, ankiDeck, ankiModel, first
func (m model) Init() tea.Cmd {
cmds := []tea.Cmd{textinput.Blink}
- if m.currentWord != "" {
- cmds = append(cmds, lookupWord(m.db, m.currentWord))
- }
+ cmds = append(cmds, isDatabaseEmpty(m.db))
return tea.Batch(cmds...)
}
@@ -92,6 +95,16 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var textPickerLongerCmds []tea.Cmd
switch msg := msg.(type) {
+ case isDBEmptyMsg:
+ if bool(msg) {
+ // set up the database
+ return m, populateDictionary(m.rawDictionaryPath, m.db)
+ } else {
+ m.dictionaryReady = true
+ if m.currentWord != "" {
+ textPickerLongerCmds = append(textPickerLongerCmds, lookupWord(m.db, m.currentWord))
+ }
+ }
case definitionMsg:
m.currentDefinition = string(msg)
m.err = nil
@@ -144,14 +157,20 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
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.statusString),
- "(Ctrl-C to quit, Esc to clear, Enter to add to Anki, PgUp/Down to scroll)",
- "\x1b[1;30;42mCurrent definition:\x1b[0m\n",
- m.definitionViewport.View(),
- ) + "\n"
+ if m.dictionaryReady {
+ 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.statusString),
+ "(Ctrl-C to quit, Esc to clear, Enter to add to Anki, PgUp/Down to scroll)",
+ "\x1b[1;30;42mCurrent definition:\x1b[0m\n",
+ m.definitionViewport.View(),
+ ) + "\n"
+ }
+ if m.err != nil {
+ return fmt.Sprintf("Failed to load dictionary! Error:\n\n%s\n\nExit with Control-C.\n", m.err)
+ }
+ return fmt.Sprintln("Preparing dictionary...")
}
func formatDefinitionForDisplay(policy bluemonday.Policy, definition string, maxWidth int) string {