1diff --git a/Makefile b/Makefile
2new file mode 100644
3index 0000000000000000000000000000000000000000..1fa3b49e5580558081b5a0de4a643f51f1e0abb2
4--- /dev/null
5+++ b/Makefile
6@@ -0,0 +1,15 @@
7+BIN?=bin/dict
8+
9+buid: ext
10+ go build -v --tags "fts5" -o $(BIN) .
11+
12+run: ext
13+ go run -v --tags "fts5" .
14+
15+import: ext
16+ go run -v --tags "fts5" . import
17+
18+ext:
19+ gcc -shared -o ext/libsqlite3ext.so -fPIC ext/spellfix.c
20+
21+.PHONY: ext
22diff --git a/app.go b/app.go
23new file mode 100644
24index 0000000000000000000000000000000000000000..b35c04960ab10fc00fb26bae25639792287a634a
25--- /dev/null
26+++ b/app.go
27@@ -0,0 +1,160 @@
28+package main
29+
30+import (
31+ "bufio"
32+ "bytes"
33+ "context"
34+ "fmt"
35+ "io"
36+ "log/slog"
37+ "math"
38+ "os"
39+ "strings"
40+
41+ "github.com/rivo/tview"
42+)
43+
44+const (
45+ memory = ":memory:"
46+)
47+
48+func run(ctx context.Context, name string) error {
49+ db, err := Open(memory)
50+ if err != nil {
51+ return err
52+ }
53+
54+ err = db.Restore(ctx, name)
55+ if err != nil {
56+ return err
57+ }
58+
59+ list := tview.NewList()
60+
61+ input := tview.NewInputField().
62+ SetLabel("S:").
63+ SetChangedFunc(func(v string) {
64+ list.Clear()
65+
66+ words, err := db.SelectDict(ctx, v, 100)
67+ if err != nil {
68+ return
69+ }
70+
71+ for _, w := range words {
72+ list.AddItem(w.Word, w.Line, 0, nil)
73+ }
74+ }).
75+ SetAutocompleteFunc(func(v string) []string {
76+ if len(v) == 0 {
77+ return []string{}
78+ }
79+
80+ vs, err := db.SelectSpell(ctx, v)
81+ if err != nil {
82+ slog.Error("Error select spelling", "error", err)
83+ return []string{}
84+ }
85+
86+ return vs
87+ })
88+
89+ grid := tview.NewGrid().
90+ SetRows(1, 0, 3).
91+ AddItem(input, 0, 0, 1, 3, 0, 0, false).
92+ AddItem(list, 1, 0, 1, 3, 0, 0, false)
93+
94+ err = tview.NewApplication().
95+ SetRoot(grid, true).
96+ SetFocus(input).
97+ Run()
98+
99+ return err
100+}
101+
102+func importDict(ctx context.Context, name string) error {
103+ db, err := Open(memory)
104+ if err != nil {
105+ return err
106+ }
107+ err = db.Migrate(ctx)
108+ if err != nil {
109+ return err
110+ }
111+
112+ file, err := os.Open("dict.txt")
113+ if err != nil {
114+ return err
115+ }
116+ defer file.Close()
117+
118+ count := 0
119+ total, err := LineCounter(file)
120+ if err != nil {
121+ return err
122+ }
123+
124+ _, err = file.Seek(0, 0)
125+ if err != nil {
126+ return err
127+ }
128+
129+ scanner := bufio.NewScanner(file)
130+ for scanner.Scan() {
131+ if strings.HasPrefix(scanner.Text(), "#") || scanner.Text() == "" {
132+ continue
133+ }
134+
135+ if err := db.InsertLine(ctx, scanner.Text()); err != nil {
136+ return err
137+ }
138+ count++
139+
140+ if (count % 1234) == 0 {
141+ fmt.Print("\033[G\033[K") // move the cursor left and clear the line
142+ per := math.Ceil((float64(count) / float64(total)) * 100.0)
143+ fmt.Printf("%d/%d (%.0f%%)", count, total, per)
144+ }
145+ }
146+
147+ fmt.Printf("Consolidating")
148+ err = db.Consolidade(ctx)
149+ if err != nil {
150+ return err
151+ }
152+
153+ err = db.Backup(ctx, name)
154+ if err != nil {
155+ return err
156+ }
157+ return nil
158+}
159+
160+func LineCounter(r io.Reader) (int, error) {
161+ var count int
162+ const lineBreak = '\n'
163+
164+ buf := make([]byte, bufio.MaxScanTokenSize)
165+
166+ for {
167+ bufferSize, err := r.Read(buf)
168+ if err != nil && err != io.EOF {
169+ return 0, err
170+ }
171+
172+ var buffPosition int
173+ for {
174+ i := bytes.IndexByte(buf[buffPosition:], lineBreak)
175+ if i == -1 || bufferSize == buffPosition {
176+ break
177+ }
178+ buffPosition += i + 1
179+ count++
180+ }
181+ if err == io.EOF {
182+ break
183+ }
184+ }
185+
186+ return count, nil
187+}
188diff --git a/db.go b/db.go
189new file mode 100644
190index 0000000000000000000000000000000000000000..b1054149ad69947f6a063c4da2f8edc1d4d6488b
191--- /dev/null
192+++ b/db.go
193@@ -0,0 +1,201 @@
194+package main
195+
196+import (
197+ "context"
198+ "database/sql"
199+ "fmt"
200+ "strings"
201+
202+ "github.com/mattn/go-sqlite3"
203+)
204+
205+type (
206+ DB struct {
207+ db *sql.DB
208+ source string // for backup
209+ }
210+
211+ Word struct {
212+ Word string
213+ Line string
214+ }
215+)
216+
217+func Open(filename string) (*DB, error) {
218+ sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{
219+ ConnectHook: func(conn *sqlite3.SQLiteConn) error {
220+ return conn.LoadExtension("ext/libsqlite3ext", "sqlite3_spellfix_init")
221+ },
222+ })
223+
224+ db, err := sql.Open("sqlite3_with_extensions", filename)
225+ if err != nil {
226+ return nil, err
227+ }
228+
229+ return &DB{
230+ db: db,
231+ source: filename,
232+ }, nil
233+}
234+
235+func (d *DB) Migrate(ctx context.Context) error {
236+ _, err := d.db.ExecContext(
237+ ctx,
238+ `CREATE VIRTUAL TABLE IF NOT EXISTS words USING fts5 (word, line);
239+ CREATE VIRTUAL TABLE IF NOT EXISTS words_terms USING fts4aux(words);
240+ CREATE VIRTUAL TABLE IF NOT EXISTS spell USING spellfix1;
241+ `,
242+ )
243+ return err
244+}
245+
246+func (d *DB) SelectDict(ctx context.Context, query string, limit int) ([]*Word, error) {
247+ rows, err := d.db.QueryContext(
248+ ctx,
249+ `SELECT
250+ word, line
251+ FROM words
252+ WHERE word MATCH ?
253+ ORDER BY rank;`,
254+ query, limit,
255+ )
256+ if err != nil {
257+ return nil, err
258+ }
259+
260+ words := make([]*Word, 0)
261+ for rows.Next() {
262+ w := Word{}
263+ err := rows.Scan(&w.Word, &w.Line)
264+ if err != nil {
265+ return nil, err
266+ }
267+ words = append(words, &w)
268+ }
269+
270+ return words, err
271+
272+}
273+
274+func (d *DB) SelectSpell(ctx context.Context, query string) ([]string, error) {
275+ rows, err := d.db.QueryContext(
276+ ctx,
277+ `SELECT
278+ word
279+ FROM spell
280+ WHERE word MATCH ?;`,
281+ query,
282+ )
283+ if err != nil {
284+ return nil, err
285+ }
286+
287+ words := make([]string, 0)
288+ for rows.Next() {
289+ w := ""
290+ err := rows.Scan(&w)
291+ if err != nil {
292+ return nil, err
293+ }
294+ words = append(words, w)
295+ }
296+
297+ return words, err
298+
299+}
300+
301+func (d *DB) InsertLine(ctx context.Context, line string) error {
302+ p := strings.SplitN(line, "\t", 2)
303+
304+ _, err := d.db.ExecContext(
305+ ctx,
306+ `INSERT INTO words (WORD, LINE) VALUES(?, ?);`,
307+ p[0], strings.ReplaceAll(p[1], "\t", " "),
308+ )
309+ if err != nil {
310+ return err
311+ }
312+ return err
313+}
314+
315+func (d *DB) Consolidade(ctx context.Context) error {
316+ _, err := d.db.ExecContext(
317+ ctx,
318+ `INSERT INTO spell(word,rank)
319+ SELECT term, documents FROM words_terms WHERE col='*'`,
320+ )
321+ if err != nil {
322+ return err
323+ }
324+ return err
325+}
326+
327+func (d *DB) Backup(ctx context.Context, name string) error {
328+ destDb, err := sql.Open("sqlite3_with_extensions", name)
329+ if err != nil {
330+ return err
331+ }
332+ defer destDb.Close()
333+
334+ return Copy(ctx, d.db, destDb)
335+}
336+
337+func (d *DB) Restore(ctx context.Context, name string) error {
338+ srcDb, err := sql.Open("sqlite3_with_extensions", name)
339+ if err != nil {
340+ return err
341+ }
342+ defer srcDb.Close()
343+
344+ return Copy(ctx, srcDb, d.db)
345+}
346+
347+func Copy(ctx context.Context, srcDb *sql.DB, destDb *sql.DB) error {
348+ destConn, err := destDb.Conn(ctx)
349+ if err != nil {
350+ return err
351+ }
352+ defer destConn.Close()
353+
354+ srcConn, err := srcDb.Conn(ctx)
355+ if err != nil {
356+ return err
357+ }
358+ defer srcConn.Close()
359+
360+ return destConn.Raw(func(destConn interface{}) error {
361+ return srcConn.Raw(func(srcConn interface{}) error {
362+ destSQLiteConn, ok := destConn.(*sqlite3.SQLiteConn)
363+ if !ok {
364+ return fmt.Errorf("can't convert destination connection to SQLiteConn")
365+ }
366+
367+ srcSQLiteConn, ok := srcConn.(*sqlite3.SQLiteConn)
368+ if !ok {
369+ return fmt.Errorf("can't convert source connection to SQLiteConn")
370+ }
371+
372+ b, err := destSQLiteConn.Backup("main", srcSQLiteConn, "main")
373+ if err != nil {
374+ return fmt.Errorf("error initializing SQLite backup: %w", err)
375+ }
376+
377+ done, err := b.Step(-1)
378+ if !done {
379+ return fmt.Errorf("step of -1, but not done")
380+ }
381+ if err != nil {
382+ return fmt.Errorf("error in stepping backup: %w", err)
383+ }
384+
385+ err = b.Finish()
386+ if err != nil {
387+ return fmt.Errorf("error finishing backup: %w", err)
388+ }
389+
390+ return err
391+ })
392+ })
393+
394+}
395diff --git a/go.mod b/go.mod
396new file mode 100644
397index 0000000000000000000000000000000000000000..40fd8f1fe390b3d2865bc05cff72219e0314e9c0
398--- /dev/null
399+++ b/go.mod
400@@ -0,0 +1,19 @@
401+module git.gabrielgio.me/dict
402+
403+go 1.21.9
404+
405+require (
406+ github.com/gdamore/tcell/v2 v2.7.4
407+ github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95
408+)
409+
410+require (
411+ github.com/gdamore/encoding v1.0.0 // indirect
412+ github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
413+ github.com/mattn/go-runewidth v0.0.15 // indirect
414+ github.com/mattn/go-sqlite3 v1.14.22 // indirect
415+ github.com/rivo/uniseg v0.4.7 // indirect
416+ golang.org/x/sys v0.17.0 // indirect
417+ golang.org/x/term v0.17.0 // indirect
418+ golang.org/x/text v0.14.0 // indirect
419+)
420diff --git a/go.sum b/go.sum
421new file mode 100644
422index 0000000000000000000000000000000000000000..4d932044f552566f0dfdc92c4ba5abcf11388355
423--- /dev/null
424+++ b/go.sum
425@@ -0,0 +1,52 @@
426+github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
427+github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
428+github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
429+github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
430+github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
431+github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
432+github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
433+github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
434+github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
435+github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
436+github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95 h1:dPivHKc1ZAicSlawH/eAmGPSCfOuCYRQLl+Eq1eRKNU=
437+github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
438+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
439+github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
440+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
441+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
442+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
443+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
444+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
445+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
446+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
447+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
448+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
449+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
450+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
451+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
452+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
453+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
454+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
455+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
456+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
457+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
458+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
459+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
460+golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
461+golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
462+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
463+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
464+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
465+golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U=
466+golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
467+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
468+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
469+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
470+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
471+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
472+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
473+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
474+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
475+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
476+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
477+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
478diff --git a/main.go b/main.go
479new file mode 100644
480index 0000000000000000000000000000000000000000..5ba494f6c47bf52e05cef13df7ee8f12add9c9c8
481--- /dev/null
482+++ b/main.go
483@@ -0,0 +1,25 @@
484+package main
485+
486+import (
487+ "context"
488+ "log/slog"
489+ "os"
490+)
491+
492+func main() {
493+ ctx := context.Background()
494+
495+ if len(os.Args) > 1 && os.Args[1] == "import" {
496+ err := importDict(ctx, "main.dict")
497+ if err != nil {
498+ slog.Error("Error importing", "error", err)
499+ return
500+ }
501+ } else {
502+ err := run(ctx, "main.dict")
503+ if err != nil {
504+ slog.Error("Error running", "error", err)
505+ return
506+ }
507+ }
508+}