diff options
| -rw-r--r-- | README.md | 38 | ||||
| -rw-r--r-- | add.go | 2 | ||||
| -rw-r--r-- | main.go | 42 | ||||
| -rw-r--r-- | setup.go | 27 | ||||
| -rw-r--r-- | ui.go | 8 |
5 files changed, 85 insertions, 32 deletions
@@ -1,10 +1,42 @@ +# README + +This program provides a fast local dictionary, with the option to add any word & its definition to Anki. ## Getting started -You need a copy of https://kaikki.org/frwiktionary/raw-wiktextract-data.jsonl.gz +You need a copy of https://kaikki.org/frwiktionary/raw-wiktextract-data.jsonl.gz. Uncompress it to raw-wiktextract-data.jsonl. This is a structured dump of French wiktionary. + +To be able to add Anki cards, you need to install the `anki-connect` extension. You can find instructions to do so here: https://git.sr.ht/~foosoft/anki-connect When you're running this app, you'll also need to have Anki open (with the `anki-connect` plugin installed) in order to add new cards. + +Also from Anki, take note of the name of the Deck where you want to add new cards, and the `Type` of these cards (appears in the upper-left of the 'add new cards' dialog in Anki). + +(Note: currently this application assumes that your card type has two fields: "Front" and "Back". Eventually, this may be configurable. If the fields are called something else in your card type, you'll currently have to update the json tags for the `fields` struct in `add.go`.) + +With all this done, you can now run the app! The first time you run it, the app will build a SQLite database of words and definitions from the Wiktionary data, which will take a minute or two. From the root of this repository, you can use a command like this to start the app: + +``` +go run . -rawDictionary=raw-wiktextract-data.jsonl -deck="Français" -model="Basic-830ae" +``` + +## Usage + +``` +Usage of french-wiktionary-flashcards: + -deck string + Name of the deck where new Anki cards will be created. + -dictionary string + Path to the parsed dictionary data. This will be generated + from rawDictionary. (default "dictionary.sqlite3") + -model string + Name of the card type ('model') for new Anki cards. + -rawDictionary string + Path to the raw wiktionary data. You can get this by + downloading and unzipping + https://kaikki.org/frwiktionary/raw-wiktextract-data.jsonl.gz + (for French). (default "raw-wiktextract-data.jsonl") + +``` # TODO -- no hardcoded paths, or Anki info -- better setup instructions (intall plugin, set deck & card, properties, etc) - clear the screen after we finish the initial import, probably?
\ No newline at end of file @@ -41,7 +41,7 @@ type options struct { DuplicateScope string `json:"duplicateScope"` } -func addCard(c *http.Client, front, back string) tea.Cmd { +func addCard(c *http.Client, deckName, modelName, front, back string) tea.Cmd { return func() tea.Msg { if back == "" { return errMsg(errors.New("definition is blank")) @@ -3,6 +3,8 @@ package main import ( + "database/sql" + "flag" "log" "net/http" "time" @@ -12,26 +14,46 @@ import ( ) const ( - rawDictionary = "/home/david/work/french-wiktionary-flashcards/raw-wiktextract-data.jsonl" - dictionary = "dictionary.sqlite3" - - apiURL = "http://localhost:8765" - deckName = "Français" - modelName = "Basic-830ae" + apiURL = "http://localhost:8765" ) func main() { - db, err := setupDatabase() + rawDict := flag.String("rawDictionary", "raw-wiktextract-data.jsonl", "Path to the raw wiktionary data. You can get this by downloading and unzipping https://kaikki.org/frwiktionary/raw-wiktextract-data.jsonl.gz (for French).") + dict := flag.String("dictionary", "dictionary.sqlite3", "Path to the parsed dictionary data. This will be generated from rawDictionary.") + deckName := flag.String("deck", "", "Name of the deck where new Anki cards will be created.") + modelName := flag.String("model", "", "Name of the card type ('model') for new Anki cards.") + + flag.Parse() + + if dict == nil { + log.Fatal("The -dictionary flag cannot be an empty string. (If not provided, it defaults to dictionary.sqlite3.)") + } + if rawDict == nil { + log.Fatal("The -rawDictionary flag cannot be an empty string. (If not provided, it defaults to aw-wiktextract-data.jsonl.)") + } + if deckName == nil { + log.Fatal("The -deck flag must be provided (name of the deck where Anki cards will be created).") + } + if modelName == nil { + log.Fatal("The -model flag must be provided. This is the name of the card type ('model') for any Anki cards created by this program. This appears under 'Type' on the dialog for creating new cards in Anki.") + } + + db, err := sql.Open("sqlite3", *dict) if err != nil { - log.Fatalf("setting up database: %s", err) + 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) + } + c := http.DefaultClient c.Timeout = 5 * time.Second - p := tea.NewProgram(initialModel(c, db)) + p := tea.NewProgram(initialModel(c, db, *deckName, *modelName)) if _, err := p.Run(); err != nil { - log.Fatal(err) + log.Fatalf("Unexpected error encountered while running program: %s", err) } } @@ -12,39 +12,34 @@ import ( "github.com/goccy/go-json" ) -func setupDatabase() (*sql.DB, error) { - db, err := sql.Open("sqlite3", dictionary) +func setupDatabase(rawDictionary string, db *sql.DB) error { + _, err := db.Exec("create table IF NOT EXISTS words (word text not null, definition text);") if err != nil { - return nil, fmt.Errorf("opening DB '%s': %s", dictionary, err) - } - - _, err = db.Exec("create table IF NOT EXISTS words (word text not null, definition text);") - if err != nil { - return nil, fmt.Errorf("creating table: %s", err) + 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 nil, fmt.Errorf("counting rows: %s", err) + return fmt.Errorf("counting rows: %s", err) } // Only populate the database if it is empty. if count > 0 { - return db, nil + return nil } // Faster import performance. _, err = db.Exec("PRAGMA synchronous = OFF;") if err != nil { - return nil, fmt.Errorf("setting risky writes: %s", err) + return fmt.Errorf("setting risky writes: %s", err) } - if err = populateDictionary(db); err != nil { - return nil, fmt.Errorf("failed to prepare dictionary: %s", err) + if err = populateDictionary(rawDictionary, db); err != nil { + return fmt.Errorf("failed to prepare dictionary: %s", err) } - return db, nil + return nil } type rawDictionaryEntry struct { @@ -84,8 +79,8 @@ type SenseForDictionaryEntry struct { Example string } -func populateDictionary(db *sql.DB) error { - log.Printf("preparing list of dictionary words...") +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( @@ -25,6 +25,8 @@ type model struct { wordAddStatus string p bluemonday.Policy vp viewport.Model + ankiDeck string + ankiModel string } type ( @@ -33,7 +35,7 @@ type ( wordAddedMsg string ) -func initialModel(c *http.Client, db *sql.DB) model { +func initialModel(c *http.Client, db *sql.DB, ankiDeck, ankiModel string) model { ti := textinput.New() ti.Placeholder = "" ti.Focus() @@ -49,6 +51,8 @@ func initialModel(c *http.Client, db *sql.DB) model { wordAddStatus: "", p: *bluemonday.StrictPolicy(), vp: vp, + ankiDeck: ankiDeck, + ankiModel: ankiModel, } } @@ -105,7 +109,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.wordInput.SetValue("") m.vp.SetContent("") case tea.KeyEnter: - return m, addCard(m.c, m.currentWord, m.currentDefinition) + return m, addCard(m.c, m.ankiDeck, m.ankiModel, m.currentWord, m.currentDefinition) } case errMsg: |
