From 1e36d1ba1ba9659ffd01e06e93ffee670f842ff8 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Giovanini" Date: Mon, 15 Apr 2024 22:17:54 +0200 Subject: feat: Add initial go implementation At this point this code still classified as playground code. --- Makefile | 15 +++++ app.go | 160 ++++++++++++++++++++++++++++++++++++++++++++++++++ db.go | 201 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 19 ++++++ go.sum | 52 +++++++++++++++++ main.go | 25 ++++++++ 6 files changed, 472 insertions(+) create mode 100644 Makefile create mode 100644 app.go create mode 100644 db.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1fa3b49 --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +BIN?=bin/dict + +buid: ext + go build -v --tags "fts5" -o $(BIN) . + +run: ext + go run -v --tags "fts5" . + +import: ext + go run -v --tags "fts5" . import + +ext: + gcc -shared -o ext/libsqlite3ext.so -fPIC ext/spellfix.c + +.PHONY: ext diff --git a/app.go b/app.go new file mode 100644 index 0000000..b35c049 --- /dev/null +++ b/app.go @@ -0,0 +1,160 @@ +package main + +import ( + "bufio" + "bytes" + "context" + "fmt" + "io" + "log/slog" + "math" + "os" + "strings" + + "github.com/rivo/tview" +) + +const ( + memory = ":memory:" +) + +func run(ctx context.Context, name string) error { + db, err := Open(memory) + if err != nil { + return err + } + + err = db.Restore(ctx, name) + if err != nil { + return err + } + + list := tview.NewList() + + input := tview.NewInputField(). + SetLabel("S:"). + SetChangedFunc(func(v string) { + list.Clear() + + words, err := db.SelectDict(ctx, v, 100) + if err != nil { + return + } + + for _, w := range words { + list.AddItem(w.Word, w.Line, 0, nil) + } + }). + SetAutocompleteFunc(func(v string) []string { + if len(v) == 0 { + return []string{} + } + + vs, err := db.SelectSpell(ctx, v) + if err != nil { + slog.Error("Error select spelling", "error", err) + return []string{} + } + + return vs + }) + + grid := tview.NewGrid(). + SetRows(1, 0, 3). + AddItem(input, 0, 0, 1, 3, 0, 0, false). + AddItem(list, 1, 0, 1, 3, 0, 0, false) + + err = tview.NewApplication(). + SetRoot(grid, true). + SetFocus(input). + Run() + + return err +} + +func importDict(ctx context.Context, name string) error { + db, err := Open(memory) + if err != nil { + return err + } + err = db.Migrate(ctx) + if err != nil { + return err + } + + file, err := os.Open("dict.txt") + if err != nil { + return err + } + defer file.Close() + + count := 0 + total, err := LineCounter(file) + if err != nil { + return err + } + + _, err = file.Seek(0, 0) + if err != nil { + return err + } + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "#") || scanner.Text() == "" { + continue + } + + if err := db.InsertLine(ctx, scanner.Text()); err != nil { + return err + } + count++ + + if (count % 1234) == 0 { + fmt.Print("\033[G\033[K") // move the cursor left and clear the line + per := math.Ceil((float64(count) / float64(total)) * 100.0) + fmt.Printf("%d/%d (%.0f%%)", count, total, per) + } + } + + fmt.Printf("Consolidating") + err = db.Consolidade(ctx) + if err != nil { + return err + } + + err = db.Backup(ctx, name) + if err != nil { + return err + } + return nil +} + +func LineCounter(r io.Reader) (int, error) { + var count int + const lineBreak = '\n' + + buf := make([]byte, bufio.MaxScanTokenSize) + + for { + bufferSize, err := r.Read(buf) + if err != nil && err != io.EOF { + return 0, err + } + + var buffPosition int + for { + i := bytes.IndexByte(buf[buffPosition:], lineBreak) + if i == -1 || bufferSize == buffPosition { + break + } + buffPosition += i + 1 + count++ + } + if err == io.EOF { + break + } + } + + return count, nil +} diff --git a/db.go b/db.go new file mode 100644 index 0000000..b105414 --- /dev/null +++ b/db.go @@ -0,0 +1,201 @@ +package main + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/mattn/go-sqlite3" +) + +type ( + DB struct { + db *sql.DB + source string // for backup + } + + Word struct { + Word string + Line string + } +) + +func Open(filename string) (*DB, error) { + sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{ + ConnectHook: func(conn *sqlite3.SQLiteConn) error { + return conn.LoadExtension("ext/libsqlite3ext", "sqlite3_spellfix_init") + }, + }) + + db, err := sql.Open("sqlite3_with_extensions", filename) + if err != nil { + return nil, err + } + + return &DB{ + db: db, + source: filename, + }, nil +} + +func (d *DB) Migrate(ctx context.Context) error { + _, err := d.db.ExecContext( + ctx, + `CREATE VIRTUAL TABLE IF NOT EXISTS words USING fts5 (word, line); + CREATE VIRTUAL TABLE IF NOT EXISTS words_terms USING fts4aux(words); + CREATE VIRTUAL TABLE IF NOT EXISTS spell USING spellfix1; + `, + ) + return err +} + +func (d *DB) SelectDict(ctx context.Context, query string, limit int) ([]*Word, error) { + rows, err := d.db.QueryContext( + ctx, + `SELECT + word, line + FROM words + WHERE word MATCH ? + ORDER BY rank;`, + query, limit, + ) + if err != nil { + return nil, err + } + + words := make([]*Word, 0) + for rows.Next() { + w := Word{} + err := rows.Scan(&w.Word, &w.Line) + if err != nil { + return nil, err + } + words = append(words, &w) + } + + return words, err + +} + +func (d *DB) SelectSpell(ctx context.Context, query string) ([]string, error) { + rows, err := d.db.QueryContext( + ctx, + `SELECT + word + FROM spell + WHERE word MATCH ?;`, + query, + ) + if err != nil { + return nil, err + } + + words := make([]string, 0) + for rows.Next() { + w := "" + err := rows.Scan(&w) + if err != nil { + return nil, err + } + words = append(words, w) + } + + return words, err + +} + +func (d *DB) InsertLine(ctx context.Context, line string) error { + p := strings.SplitN(line, "\t", 2) + + _, err := d.db.ExecContext( + ctx, + `INSERT INTO words (WORD, LINE) VALUES(?, ?);`, + p[0], strings.ReplaceAll(p[1], "\t", " "), + ) + if err != nil { + return err + } + return err +} + +func (d *DB) Consolidade(ctx context.Context) error { + _, err := d.db.ExecContext( + ctx, + `INSERT INTO spell(word,rank) + SELECT term, documents FROM words_terms WHERE col='*'`, + ) + if err != nil { + return err + } + return err +} + +func (d *DB) Backup(ctx context.Context, name string) error { + destDb, err := sql.Open("sqlite3_with_extensions", name) + if err != nil { + return err + } + defer destDb.Close() + + return Copy(ctx, d.db, destDb) +} + +func (d *DB) Restore(ctx context.Context, name string) error { + srcDb, err := sql.Open("sqlite3_with_extensions", name) + if err != nil { + return err + } + defer srcDb.Close() + + return Copy(ctx, srcDb, d.db) +} + +func Copy(ctx context.Context, srcDb *sql.DB, destDb *sql.DB) error { + destConn, err := destDb.Conn(ctx) + if err != nil { + return err + } + defer destConn.Close() + + srcConn, err := srcDb.Conn(ctx) + if err != nil { + return err + } + defer srcConn.Close() + + return destConn.Raw(func(destConn interface{}) error { + return srcConn.Raw(func(srcConn interface{}) error { + destSQLiteConn, ok := destConn.(*sqlite3.SQLiteConn) + if !ok { + return fmt.Errorf("can't convert destination connection to SQLiteConn") + } + + srcSQLiteConn, ok := srcConn.(*sqlite3.SQLiteConn) + if !ok { + return fmt.Errorf("can't convert source connection to SQLiteConn") + } + + b, err := destSQLiteConn.Backup("main", srcSQLiteConn, "main") + if err != nil { + return fmt.Errorf("error initializing SQLite backup: %w", err) + } + + done, err := b.Step(-1) + if !done { + return fmt.Errorf("step of -1, but not done") + } + if err != nil { + return fmt.Errorf("error in stepping backup: %w", err) + } + + err = b.Finish() + if err != nil { + return fmt.Errorf("error finishing backup: %w", err) + } + + return err + }) + }) + +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..40fd8f1 --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.gabrielgio.me/dict + +go 1.21.9 + +require ( + github.com/gdamore/tcell/v2 v2.7.4 + github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95 +) + +require ( + github.com/gdamore/encoding v1.0.0 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/term v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4d93204 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= +github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95 h1:dPivHKc1ZAicSlawH/eAmGPSCfOuCYRQLl+Eq1eRKNU= +github.com/rivo/tview v0.0.0-20240413115534-b0d41c484b95/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/main.go b/main.go new file mode 100644 index 0000000..5ba494f --- /dev/null +++ b/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "context" + "log/slog" + "os" +) + +func main() { + ctx := context.Background() + + if len(os.Args) > 1 && os.Args[1] == "import" { + err := importDict(ctx, "main.dict") + if err != nil { + slog.Error("Error importing", "error", err) + return + } + } else { + err := run(ctx, "main.dict") + if err != nil { + slog.Error("Error running", "error", err) + return + } + } +} -- cgit v1.2.3