diff options
| author | Gabriel A. Giovanini <mail@gabrielgio.me> | 2024-04-15 22:17:54 +0200 | 
|---|---|---|
| committer | Gabriel A. Giovanini <mail@gabrielgio.me> | 2024-04-15 22:17:54 +0200 | 
| commit | 1e36d1ba1ba9659ffd01e06e93ffee670f842ff8 (patch) | |
| tree | d9c9d1ac019107c8427aff60bf1537fea1687cc6 | |
| parent | 6dd0c4747aa57227b5898fc639e3f2b643ce013c (diff) | |
| download | dict-1e36d1ba1ba9659ffd01e06e93ffee670f842ff8.tar.gz dict-1e36d1ba1ba9659ffd01e06e93ffee670f842ff8.tar.bz2 dict-1e36d1ba1ba9659ffd01e06e93ffee670f842ff8.zip | |
feat: Add initial go implementation
At this point this code still classified as playground code.
| -rw-r--r-- | Makefile | 15 | ||||
| -rw-r--r-- | app.go | 160 | ||||
| -rw-r--r-- | db.go | 201 | ||||
| -rw-r--r-- | go.mod | 19 | ||||
| -rw-r--r-- | go.sum | 52 | ||||
| -rw-r--r-- | main.go | 25 | 
6 files changed, 472 insertions, 0 deletions
| 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 @@ -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 +} @@ -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 +		}) +	}) + +} @@ -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 +) @@ -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= @@ -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 +		} +	} +} | 
