diff options
| author | David Schlachter <t480-debian-git@schlachter.ca> | 2026-01-12 00:21:47 -0500 |
|---|---|---|
| committer | David Schlachter <t480-debian-git@schlachter.ca> | 2026-01-12 00:21:47 -0500 |
| commit | 414bb721a642670b73ee269ecfc50ffced4aa1f3 (patch) | |
| tree | 9c763c750bd8741b9d199c06dc226ea141c89115 | |
| parent | c7e2672b0083eb81ed4d494e1f90b30e0a86c7d9 (diff) | |
Nicer progress bar for first run
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | setup.go | 42 | ||||
| -rw-r--r-- | ui.go | 22 |
5 files changed, 59 insertions, 9 deletions
@@ -88,7 +88,6 @@ Usage of french-wiktionary-flashcards: and I would want to display them in the target language). - allow setting the language for initial processing, so that we could support languages other than French -- general code cleanup & organization - italicise part-of-speech in the TUI - maybe we could create the Anki and the TUI definitions at the same time during initial parsing? Then we'd have all the information required to do @@ -16,6 +16,7 @@ require ( 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/harmonica v0.2.0 // 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 @@ -10,6 +10,8 @@ github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlv 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/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= 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= @@ -2,9 +2,11 @@ package main import ( "bufio" + "bytes" "database/sql" "fmt" "html/template" + "io" "os" "strings" @@ -89,6 +91,7 @@ type dictionaryPopulator struct { tx *sql.Tx stmt *sql.Stmt tmpl *template.Template + fh *os.File scanner *bufio.Scanner totalLines int @@ -122,15 +125,25 @@ func setupPopulator(dp *dictionaryPopulator) tea.Cmd { return errMsg(fmt.Errorf("preparing statement: %w", err)) } - file, err := os.Open(dp.rawDictionaryPath) + dp.fh, err = os.Open(dp.rawDictionaryPath) if err != nil { return errMsg(fmt.Errorf("opening: %w", err)) } - dp.scanner = bufio.NewScanner(file) + // Figure out how many lines the file has, for reporting import + // progress. + lines, err := lineCounter(dp.fh) + if err != nil { + return errMsg(fmt.Errorf("reading lines from file: %w", err)) + } + dp.totalLines = lines - maxCapacity := 2_000_000 + // We've just read through the whole file, reset the read position to + // the beginning because we're about to set up a scanner on it. + dp.fh.Seek(0, 0) + dp.scanner = bufio.NewScanner(dp.fh) + maxCapacity := 2_000_000 buf := make([]byte, maxCapacity) dp.scanner.Buffer(buf, maxCapacity) @@ -138,6 +151,25 @@ func setupPopulator(dp *dictionaryPopulator) tea.Cmd { } } +func lineCounter(r io.Reader) (int, error) { + buf := make([]byte, 64*1024) + count := 0 + lineSep := []byte{'\n'} + + for { + c, err := r.Read(buf) + count += bytes.Count(buf[:c], lineSep) + + switch { + case err == io.EOF: + return count, nil + + case err != nil: + return count, err + } + } +} + func populateDictionary(dp *dictionaryPopulator) tea.Cmd { return func() tea.Msg { for dp.scanner.Scan() { @@ -227,6 +259,10 @@ func populateDictionary(dp *dictionaryPopulator) tea.Cmd { return errMsg(fmt.Errorf("creating index: %s", err)) } + // Clean up resources + dp.stmt.Close() + dp.fh.Close() + return isDictionaryEmptyMsg(false) // We're done! } } @@ -8,6 +8,7 @@ import ( "strings" "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/progress" "github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" @@ -23,6 +24,7 @@ type model struct { wordInput textinput.Model definitionViewport viewport.Model + importProgress progress.Model ankiDeck string ankiModel string @@ -34,6 +36,7 @@ type model struct { currentWord string currentDefinition string statusString string + importFraction float64 err error } @@ -82,6 +85,7 @@ func initialModel(c *http.Client, db *sql.DB, apiURL, ankiDeck, ankiModel, rawDi wordInput: input, definitionViewport: textbox, + importProgress: progress.New(progress.WithDefaultGradient()), ankiDeck: ankiDeck, ankiModel: ankiModel, @@ -115,7 +119,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } } case populatingDictionaryMsg: - m.statusString = fmt.Sprintf("Currently on line %d...", msg.currentLine) + m.importFraction = float64(msg.currentLine) / float64(msg.totalLines) return m, populateDictionary((*dictionaryPopulator)(msg)) case definitionMsg: m.currentDefinition = string(msg) @@ -130,12 +134,17 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { m.definitionViewport.SetContent("") m.err = nil case tea.WindowSizeMsg: - // headerHeight is the height of everything above the definition window. - headerHeight := 11 + m.importProgress.Width = msg.Width m.wordInput.Width = msg.Width m.definitionViewport.Width = msg.Width + + // headerHeight is the height of everything above the definition window. + headerHeight := 11 + m.definitionViewport.Height = msg.Height - headerHeight - m.definitionViewport.SetContent(formatDefinitionForDisplay(m.p, m.currentDefinition, m.definitionViewport.Width)) + m.definitionViewport.SetContent( + formatDefinitionForDisplay(m.p, m.currentDefinition, m.definitionViewport.Width), + ) case tea.KeyMsg: switch msg.Type { case tea.KeyCtrlC, tea.KeyCtrlD: @@ -182,7 +191,10 @@ func (m model) View() string { 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.Sprintf("Preparing dictionary...\n\n%s\n\n", m.statusString) + if m.importFraction == 0 { + return "\nWe'll start preparing the dictionary in a few seconds...\n\n\n\n" + } + return fmt.Sprintf("\nPreparing dictionary...\n\n%s\n\n", m.importProgress.ViewAs(m.importFraction)) } func formatDefinitionForDisplay(policy bluemonday.Policy, definition string, maxWidth int) string { |
