diff --git a/go.mod b/go.mod index e4892d2..7fde692 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,10 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/glebarez/go-sqlite v1.22.0 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -23,10 +26,15 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/sync v0.13.0 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.3.8 // indirect + modernc.org/libc v1.37.6 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index fe62e91..d54ad75 100644 --- a/go.sum +++ b/go.sum @@ -13,8 +13,14 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= +github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -31,6 +37,8 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= @@ -53,3 +61,11 @@ golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +modernc.org/libc v1.37.6 h1:orZH3c5wmhIQFTXF+Nt+eeauyd+ZIt2BX6ARe+kD+aw= +modernc.org/libc v1.37.6/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ= +modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0= diff --git a/internal/database/database.go b/internal/database/database.go new file mode 100644 index 0000000..e7d040a --- /dev/null +++ b/internal/database/database.go @@ -0,0 +1,114 @@ +package database + +import ( + "os" + "path/filepath" + "database/sql" + "sync" + "errors" + _ "github.com/glebarez/go-sqlite" + "time" +) + +var ( + db *sql.DB + once sync.Once +) + +type Stats struct { + WPM float64 + Accuracy float64 + AddedAt time.Time +} + +// ConnectToDatabase initializes the connection if it hasn't been initialized +func connectToDatabase() (*sql.DB, error) { + var err error + once.Do(func() { + path, e := getDBPath() + if e != nil { + err = e + return + } + db, err = sql.Open("sqlite", path) + if err != nil { + return + } + + _, err = createTable(db) + }) + + return db, err +} + +func getDBPath() (string, error) { + configDir, err := os.UserConfigDir() + if err != nil { + return "", err + } + + appDir := filepath.Join(configDir, "typtea") + // Create directory if it doesn't exist + if err := os.MkdirAll(appDir, 0700); err != nil { + return "", err + } + + return filepath.Join(appDir, "typtea.db"), nil +} + +// CreateTable ensures the stats table exists +func createTable(db *sql.DB) (sql.Result, error) { + sqlStmt := `CREATE TABLE IF NOT EXISTS stats ( + id INTEGER PRIMARY KEY, + wpm REAL NOT NULL, + accuracy REAL NOT NULL, + added_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP + );` + return db.Exec(sqlStmt) +} + +// Insert adds a new stats entry +func InsertStats(stats *Stats) (int64, error) { + db, err := connectToDatabase() + if err != nil { + return 0, err + } + + sqlStmt := `INSERT INTO stats (wpm, accuracy) VALUES (?, ?);` + result, err := db.Exec(sqlStmt, stats.WPM, stats.Accuracy) + if err != nil { + return 0, err + } + + return result.LastInsertId() +} + +type MaxStatsData struct { + MaxWPM float64 + MaxAccuracy float64 +} + +func MaxStats() (*MaxStatsData, error) { + db, err := connectToDatabase() + if err != nil { + return nil, err + } + + sqlStmt := `SELECT MAX(wpm), MAX(accuracy) FROM stats;` + var maxWPM sql.NullFloat64 + var maxAccuracy sql.NullFloat64 + + err = db.QueryRow(sqlStmt).Scan(&maxWPM, &maxAccuracy) + if err != nil { + return nil, err + } + + if !maxWPM.Valid || !maxAccuracy.Valid { + return nil, errors.New("no records found") + } + + return &MaxStatsData{ + MaxWPM: maxWPM.Float64, + MaxAccuracy: maxAccuracy.Float64, + }, nil +} diff --git a/internal/game/typing.go b/internal/game/typing.go index c2e6c8c..04eff34 100644 --- a/internal/game/typing.go +++ b/internal/game/typing.go @@ -3,12 +3,16 @@ package game import ( "strings" "time" + "fmt" + "github.com/ashish0kumar/typtea/internal/database" ) // TypingStats holds the statistics for a game session type TypingStats struct { WPM float64 + BestWPM float64 Accuracy float64 + BestAccuracy float64 CharactersTyped int CorrectChars int TotalChars int @@ -218,9 +222,22 @@ func (g *TypingGame) GetStats() TypingStats { accuracy = 0 } + if _, err := database.InsertStats(&database.Stats{WPM: netWPM, Accuracy: accuracy}); err != nil { + fmt.Println("Insert failed:", err) + } + + maxStats, err := database.MaxStats() + if err != nil { + fmt.Println(err) + maxStats = &database.MaxStatsData{MaxWPM: 0, MaxAccuracy: 0} + } + + return TypingStats{ WPM: netWPM, + BestWPM: maxStats.MaxWPM, Accuracy: accuracy, + BestAccuracy: maxStats.MaxAccuracy, CharactersTyped: g.GlobalPos, CorrectChars: correctChars, TotalChars: len([]rune(g.GetDisplayText())), diff --git a/internal/tui/view.go b/internal/tui/view.go index f53221c..38ce2d2 100644 --- a/internal/tui/view.go +++ b/internal/tui/view.go @@ -137,12 +137,24 @@ func (m Model) renderResults() string { boldStyle.Render(fmt.Sprintf("%.0f%%", stats.Accuracy)), ) + bestAccSection := lipgloss.JoinVertical( + lipgloss.Center, + mutedStyle.Render("best acc"), + boldStyle.Render(fmt.Sprintf("%.0f%%", stats.BestAccuracy)), + ) + wpmSection := lipgloss.JoinVertical( lipgloss.Right, mutedStyle.Render("wpm"), boldStyle.Render(fmt.Sprintf("%.0f", stats.WPM)), ) + bestWpmSection := lipgloss.JoinVertical( + lipgloss.Center, + mutedStyle.Render("best wpm"), + boldStyle.Render(fmt.Sprintf("%.0f", stats.BestWPM)), + ) + timeSection := lipgloss.JoinVertical( lipgloss.Right, mutedStyle.Render("time"), @@ -160,8 +172,12 @@ func (m Model) renderResults() string { lipgloss.Top, accSection, strings.Repeat(" ", statGap), + bestAccSection, + strings.Repeat(" ", statGap), wpmSection, strings.Repeat(" ", statGap), + bestWpmSection, + strings.Repeat(" ", statGap), timeSection, strings.Repeat(" ", statGap), languageSection,