dict @ 1e36d1ba1ba9659ffd01e06e93ffee670f842ff8

feat: Add initial go implementation

At this point this code still classified as playground code.
  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+}