dict @ 57c782546739fde08138b00e2d0b3ba5f18fb676

ref: Better organize the files
  1diff --git a/.gitignore b/.gitignore
  2index e660fd93d3196215552065b1e63bf6a2f393ed86..50e293ee933b72f8e433a00c303a16efcf3ebb6c 100644
  3--- a/.gitignore
  4+++ b/.gitignore
  5@@ -1 +1,5 @@
  6 bin/
  7+ext/*.so
  8+
  9+dict.txt
 10+main.dict
 11diff --git a/Makefile b/Makefile
 12index 1fa3b49e5580558081b5a0de4a643f51f1e0abb2..eb08518ffbba68cbace6e59867f37074cb02adac 100644
 13--- a/Makefile
 14+++ b/Makefile
 15@@ -1,13 +1,17 @@
 16 BIN?=bin/dict
 17+GO_BUILD=go build -v --tags "fts5"
 18+GO_RUN=go run -v --tags "fts5"
 19+
 20 
 21 buid: ext
 22-	go build -v --tags "fts5" -o $(BIN) .
 23+	$(GO_BUILD) -o $(BIN) ./cmd/dict/main.go
 24 
 25 run: ext
 26-	go run -v --tags "fts5" .
 27+	$(GO_RUN) ./cmd/dict/main.go
 28+		
 29 
 30 import: ext
 31-	go run -v --tags "fts5" . import
 32+	$(GO_RUN) ./cmd/dict/main.go import
 33 
 34 ext:
 35 	gcc -shared -o ext/libsqlite3ext.so -fPIC ext/spellfix.c
 36diff --git a/app.go b/app.go
 37deleted file mode 100644
 38index b35c04960ab10fc00fb26bae25639792287a634a..0000000000000000000000000000000000000000
 39--- a/app.go
 40+++ /dev/null
 41@@ -1,160 +0,0 @@
 42-package main
 43-
 44-import (
 45-	"bufio"
 46-	"bytes"
 47-	"context"
 48-	"fmt"
 49-	"io"
 50-	"log/slog"
 51-	"math"
 52-	"os"
 53-	"strings"
 54-
 55-	"github.com/rivo/tview"
 56-)
 57-
 58-const (
 59-	memory = ":memory:"
 60-)
 61-
 62-func run(ctx context.Context, name string) error {
 63-	db, err := Open(memory)
 64-	if err != nil {
 65-		return err
 66-	}
 67-
 68-	err = db.Restore(ctx, name)
 69-	if err != nil {
 70-		return err
 71-	}
 72-
 73-	list := tview.NewList()
 74-
 75-	input := tview.NewInputField().
 76-		SetLabel("S:").
 77-		SetChangedFunc(func(v string) {
 78-			list.Clear()
 79-
 80-			words, err := db.SelectDict(ctx, v, 100)
 81-			if err != nil {
 82-				return
 83-			}
 84-
 85-			for _, w := range words {
 86-				list.AddItem(w.Word, w.Line, 0, nil)
 87-			}
 88-		}).
 89-		SetAutocompleteFunc(func(v string) []string {
 90-			if len(v) == 0 {
 91-				return []string{}
 92-			}
 93-
 94-			vs, err := db.SelectSpell(ctx, v)
 95-			if err != nil {
 96-				slog.Error("Error select spelling", "error", err)
 97-				return []string{}
 98-			}
 99-
100-			return vs
101-		})
102-
103-	grid := tview.NewGrid().
104-		SetRows(1, 0, 3).
105-		AddItem(input, 0, 0, 1, 3, 0, 0, false).
106-		AddItem(list, 1, 0, 1, 3, 0, 0, false)
107-
108-	err = tview.NewApplication().
109-		SetRoot(grid, true).
110-		SetFocus(input).
111-		Run()
112-
113-	return err
114-}
115-
116-func importDict(ctx context.Context, name string) error {
117-	db, err := Open(memory)
118-	if err != nil {
119-		return err
120-	}
121-	err = db.Migrate(ctx)
122-	if err != nil {
123-		return err
124-	}
125-
126-	file, err := os.Open("dict.txt")
127-	if err != nil {
128-		return err
129-	}
130-	defer file.Close()
131-
132-	count := 0
133-	total, err := LineCounter(file)
134-	if err != nil {
135-		return err
136-	}
137-
138-	_, err = file.Seek(0, 0)
139-	if err != nil {
140-		return err
141-	}
142-
143-	scanner := bufio.NewScanner(file)
144-	for scanner.Scan() {
145-		if strings.HasPrefix(scanner.Text(), "#") || scanner.Text() == "" {
146-			continue
147-		}
148-
149-		if err := db.InsertLine(ctx, scanner.Text()); err != nil {
150-			return err
151-		}
152-		count++
153-
154-		if (count % 1234) == 0 {
155-			fmt.Print("\033[G\033[K") // move the cursor left and clear the line
156-			per := math.Ceil((float64(count) / float64(total)) * 100.0)
157-			fmt.Printf("%d/%d (%.0f%%)", count, total, per)
158-		}
159-	}
160-
161-	fmt.Printf("Consolidating")
162-	err = db.Consolidade(ctx)
163-	if err != nil {
164-		return err
165-	}
166-
167-	err = db.Backup(ctx, name)
168-	if err != nil {
169-		return err
170-	}
171-	return nil
172-}
173-
174-func LineCounter(r io.Reader) (int, error) {
175-	var count int
176-	const lineBreak = '\n'
177-
178-	buf := make([]byte, bufio.MaxScanTokenSize)
179-
180-	for {
181-		bufferSize, err := r.Read(buf)
182-		if err != nil && err != io.EOF {
183-			return 0, err
184-		}
185-
186-		var buffPosition int
187-		for {
188-			i := bytes.IndexByte(buf[buffPosition:], lineBreak)
189-			if i == -1 || bufferSize == buffPosition {
190-				break
191-			}
192-			buffPosition += i + 1
193-			count++
194-		}
195-		if err == io.EOF {
196-			break
197-		}
198-	}
199-
200-	return count, nil
201-}
202diff --git a/cmd/dict/main.go b/cmd/dict/main.go
203new file mode 100644
204index 0000000000000000000000000000000000000000..09e9412599a4f1d86d1348570f7abec66e36fd6c
205--- /dev/null
206+++ b/cmd/dict/main.go
207@@ -0,0 +1,27 @@
208+package main
209+
210+import (
211+	"log/slog"
212+	"os"
213+
214+	"github.com/urfave/cli/v2"
215+
216+	"git.gabrielgio.me/dict/cmd/importer"
217+	"git.gabrielgio.me/dict/cmd/ui"
218+)
219+
220+func main() {
221+	app := &cli.App{
222+		Name:  "dict",
223+		Usage: "interactive dictionary",
224+		Commands: []*cli.Command{
225+			importer.ImportCommand,
226+			ui.UICommand,
227+		},
228+	}
229+
230+	if err := app.Run(os.Args); err != nil {
231+		slog.Error("Error running application", "error", err)
232+		os.Exit(1)
233+	}
234+}
235diff --git a/cmd/importer/importer.go b/cmd/importer/importer.go
236new file mode 100644
237index 0000000000000000000000000000000000000000..18a7a7bcde0a0d70d582ae258a3e98c28b89a1a0
238--- /dev/null
239+++ b/cmd/importer/importer.go
240@@ -0,0 +1,131 @@
241+package importer
242+
243+import (
244+	"bufio"
245+	"bytes"
246+	"context"
247+	"fmt"
248+	"io"
249+	"math"
250+	"os"
251+	"strings"
252+
253+	"github.com/urfave/cli/v2"
254+
255+	"git.gabrielgio.me/dict/db"
256+)
257+
258+var ImportCommand = &cli.Command{
259+	Name:  "import",
260+	Usage: "convert dict.cc dictionary into a queryable sqlite format.",
261+	Flags: []cli.Flag{
262+		&cli.StringFlag{
263+			Name:  "output",
264+			Value: "main.dict",
265+			Usage: "Dictionary database location",
266+		},
267+		&cli.StringFlag{
268+			Name:  "input",
269+			Value: "dict.txt",
270+			Usage: "Dict.cc txt dictionary file",
271+		},
272+	},
273+	Action: func(cCtx *cli.Context) error {
274+		input := cCtx.String("input")
275+		output := cCtx.String("output")
276+		return Import(context.Background(), input, output)
277+	},
278+}
279+
280+func Import(ctx context.Context, txtInput, sqliteOutput string) error {
281+	db, err := db.Open(":memory:")
282+	if err != nil {
283+		return err
284+	}
285+	err = db.Migrate(ctx)
286+	if err != nil {
287+		return err
288+	}
289+
290+	file, err := os.Open(txtInput)
291+	if err != nil {
292+		return err
293+	}
294+	defer file.Close()
295+
296+	count := 0
297+	total, err := lineCounter(file)
298+	if err != nil {
299+		return err
300+	}
301+
302+	_, err = file.Seek(0, 0)
303+	if err != nil {
304+		return err
305+	}
306+
307+	scanner := bufio.NewScanner(file)
308+	for scanner.Scan() {
309+		if strings.HasPrefix(scanner.Text(), "#") || scanner.Text() == "" {
310+			continue
311+		}
312+
313+		var (
314+			p    = strings.SplitN(scanner.Text(), "\t", 2)
315+			word = p[0]
316+			line = strings.ReplaceAll(p[1], "\t", " ")
317+		)
318+
319+		if err := db.InsertLine(ctx, word, line); err != nil {
320+			return err
321+		}
322+		count++
323+
324+		if (count % 1234) == 0 {
325+			fmt.Print("\033[G\033[K") // move the cursor left and clear the line
326+			per := math.Ceil((float64(count) / float64(total)) * 100.0)
327+			fmt.Printf("%d/%d (%.0f%%)", count, total, per)
328+		}
329+	}
330+
331+	fmt.Printf("Consolidating")
332+	err = db.Consolidade(ctx)
333+	if err != nil {
334+		return err
335+	}
336+
337+	err = db.Backup(ctx, sqliteOutput)
338+	if err != nil {
339+		return err
340+	}
341+	return nil
342+}
343+
344+func lineCounter(r io.Reader) (int, error) {
345+	var count int
346+	const lineBreak = '\n'
347+
348+	buf := make([]byte, bufio.MaxScanTokenSize)
349+
350+	for {
351+		bufferSize, err := r.Read(buf)
352+		if err != nil && err != io.EOF {
353+			return 0, err
354+		}
355+
356+		var buffPosition int
357+		for {
358+			i := bytes.IndexByte(buf[buffPosition:], lineBreak)
359+			if i == -1 || bufferSize == buffPosition {
360+				break
361+			}
362+			buffPosition += i + 1
363+			count++
364+		}
365+		if err == io.EOF {
366+			break
367+		}
368+	}
369+
370+	return count, nil
371+}
372diff --git a/cmd/ui/ui.go b/cmd/ui/ui.go
373new file mode 100644
374index 0000000000000000000000000000000000000000..82c0bc5fed899b86af453dc08e1d3b2909ac9501
375--- /dev/null
376+++ b/cmd/ui/ui.go
377@@ -0,0 +1,105 @@
378+package ui
379+
380+import (
381+	"context"
382+	"fmt"
383+	"log/slog"
384+
385+	"github.com/gdamore/tcell/v2"
386+	"github.com/rivo/tview"
387+	"github.com/urfave/cli/v2"
388+
389+	"git.gabrielgio.me/dict/db"
390+)
391+
392+const (
393+	memory = ":memory:"
394+)
395+
396+var UICommand = &cli.Command{
397+	Name:  "ui",
398+	Usage: "interactive dictionary",
399+	Flags: []cli.Flag{
400+		&cli.StringFlag{
401+			Name:  "filename",
402+			Value: "main.dict",
403+			Usage: "Dictionary database location",
404+		},
405+	},
406+	Action: func(cCtx *cli.Context) error {
407+		name := cCtx.String("lang")
408+		return Run(context.Background(), name)
409+	},
410+}
411+
412+func Run(ctx context.Context, name string) error {
413+	db, err := db.Open(memory)
414+	if err != nil {
415+		return err
416+	}
417+
418+	err = db.Restore(ctx, name)
419+	if err != nil {
420+		return err
421+	}
422+
423+	textView := tview.NewTextView().
424+		SetDynamicColors(true).
425+		SetRegions(true)
426+
427+	input := tview.NewInputField().
428+		SetChangedFunc(func(v string) {
429+			textView.Clear()
430+
431+			words, err := db.SelectDict(ctx, v, 100)
432+			if err != nil {
433+				return
434+			}
435+
436+			lastWord := ""
437+			for _, w := range words {
438+
439+				if lastWord == w.Word {
440+					fmt.Fprintf(textView, "%s\n", w.Line)
441+				} else if lastWord == "" {
442+					fmt.Fprintf(textView, "[bold]%s[normal]\n", w.Word)
443+					fmt.Fprintf(textView, "%s\n", w.Line)
444+				} else {
445+					fmt.Fprintf(textView, "\n[bold]%s[normal]\n", w.Word)
446+					fmt.Fprintf(textView, "%s\n", w.Line)
447+				}
448+
449+				lastWord = w.Word
450+			}
451+		}).
452+		SetAutocompleteFunc(func(v string) []string {
453+			if len(v) == 0 {
454+				return []string{}
455+			}
456+
457+			vs, err := db.SelectSpell(ctx, v)
458+			if err != nil {
459+				slog.Error("Error select spelling", "error", err)
460+				return []string{}
461+			}
462+
463+			return vs
464+		})
465+
466+	input.SetDoneFunc(func(key tcell.Key) {
467+		textView.Clear()
468+		input.SetText("")
469+	})
470+
471+	grid := tview.NewGrid().
472+		SetRows(1, 0, 3).
473+		AddItem(input, 0, 0, 1, 3, 0, 0, false).
474+		AddItem(textView, 1, 0, 1, 3, 0, 0, false)
475+
476+	err = tview.NewApplication().
477+		SetRoot(grid, true).
478+		SetFocus(input).
479+		Run()
480+
481+	return err
482+}
483diff --git a/db.go b/db/db.go
484rename from db.go
485rename to db/db.go
486index b1054149ad69947f6a063c4da2f8edc1d4d6488b..746c30d2b2886716386f8794a339201479da9e24 100644
487--- a/db.go
488+++ b/db/db.go
489@@ -1,10 +1,9 @@
490-package main
491+package db
492 
493 import (
494 	"context"
495 	"database/sql"
496 	"fmt"
497-	"strings"
498 
499 	"github.com/mattn/go-sqlite3"
500 )
501@@ -105,13 +104,11 @@ 	return words, err
502 
503 }
504 
505-func (d *DB) InsertLine(ctx context.Context, line string) error {
506-	p := strings.SplitN(line, "\t", 2)
507-
508+func (d *DB) InsertLine(ctx context.Context, word, line string) error {
509 	_, err := d.db.ExecContext(
510 		ctx,
511 		`INSERT INTO words (WORD, LINE) VALUES(?, ?);`,
512-		p[0], strings.ReplaceAll(p[1], "\t", " "),
513+		word, line,
514 	)
515 	if err != nil {
516 		return err
517diff --git a/go.mod b/go.mod
518index 40fd8f1fe390b3d2865bc05cff72219e0314e9c0..9a4b5a42c380087f14f0c6c31450efc591ded796 100644
519--- a/go.mod
520+++ b/go.mod
521@@ -4,15 +4,19 @@ go 1.21.9
522 
523 require (
524 	github.com/gdamore/tcell/v2 v2.7.4
525+	github.com/mattn/go-sqlite3 v1.14.22
526 	github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95
527+	github.com/urfave/cli/v2 v2.27.1
528 )
529 
530 require (
531+	github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
532 	github.com/gdamore/encoding v1.0.0 // indirect
533 	github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
534 	github.com/mattn/go-runewidth v0.0.15 // indirect
535-	github.com/mattn/go-sqlite3 v1.14.22 // indirect
536 	github.com/rivo/uniseg v0.4.7 // indirect
537+	github.com/russross/blackfriday/v2 v2.1.0 // indirect
538+	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
539 	golang.org/x/sys v0.17.0 // indirect
540 	golang.org/x/term v0.17.0 // indirect
541 	golang.org/x/text v0.14.0 // indirect
542diff --git a/go.sum b/go.sum
543index 4d932044f552566f0dfdc92c4ba5abcf11388355..ab0b469885be727fd18edf59bb543c4b0a9898fe 100644
544--- a/go.sum
545+++ b/go.sum
546@@ -1,3 +1,5 @@
547+github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
548+github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
549 github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
550 github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
551 github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
552@@ -14,6 +16,12 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
553 github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
554 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
555 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
556+github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
557+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
558+github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
559+github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
560+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
561+github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
562 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
563 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
564 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
565diff --git a/main.go b/main.go
566deleted file mode 100644
567index 5ba494f6c47bf52e05cef13df7ee8f12add9c9c8..0000000000000000000000000000000000000000
568--- a/main.go
569+++ /dev/null
570@@ -1,25 +0,0 @@
571-package main
572-
573-import (
574-	"context"
575-	"log/slog"
576-	"os"
577-)
578-
579-func main() {
580-	ctx := context.Background()
581-
582-	if len(os.Args) > 1 && os.Args[1] == "import" {
583-		err := importDict(ctx, "main.dict")
584-		if err != nil {
585-			slog.Error("Error importing", "error", err)
586-			return
587-		}
588-	} else {
589-		err := run(ctx, "main.dict")
590-		if err != nil {
591-			slog.Error("Error running", "error", err)
592-			return
593-		}
594-	}
595-}