commit d71b71f545fa14502a7d728e8d40425604476f95 Author: qowevisa Date: Sat Feb 15 19:06:22 2025 +0200 Init state diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3f2668d --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +tipitypy +*.log +bin/* +*.db + +_test* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2d50ba7 --- /dev/null +++ b/Makefile @@ -0,0 +1,10 @@ +def: scrapper tipitypy + @ + +scrapper: + go build -o ./bin/$@ ./cmd/$@ + +tipitypy: + go build + +.PHONY: tipitypy diff --git a/cmd/scrapper/main.go b/cmd/scrapper/main.go new file mode 100644 index 0000000..fff4637 --- /dev/null +++ b/cmd/scrapper/main.go @@ -0,0 +1,59 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "tipitypy/db" +) + +func AddToDbNWords(n int64) (int, error) { + url := fmt.Sprintf("https://random-word-api.herokuapp.com/word?number=%d", n) + resp, err := http.Get(url) + if err != nil { + return 0, fmt.Errorf("http.Get: %w", err) + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, fmt.Errorf("io.ReadAll: %w", err) + } + var words []string + err = json.Unmarshal(body, &words) + if err != nil { + return 0, fmt.Errorf("json.Unmarshal: %w", err) + } + dbc := db.Connect() + successes := 0 + for _, wordValue := range words { + word := &db.Word{Value: wordValue} + if err := dbc.Create(word).Error; err != nil { + continue + } + successes++ + } + return successes, nil +} + +func main() { + if len(os.Args) > 1 { + if val, err := strconv.ParseInt(os.Args[1], 10, 64); err == nil { + res, err := AddToDbNWords(val) + if err != nil { + fmt.Printf("ERROR: %v", err) + os.Exit(1) + } + fmt.Printf("Successfully added %d words to database!\n", res) + os.Exit(0) + } + } + res, err := AddToDbNWords(10) + if err != nil { + fmt.Printf("ERROR: %v", err) + os.Exit(1) + } + fmt.Printf("Successfully added %d words to database!\n", res) +} diff --git a/colorizer/colors.go b/colorizer/colors.go new file mode 100644 index 0000000..6682120 --- /dev/null +++ b/colorizer/colors.go @@ -0,0 +1,49 @@ +package colorizer + +type colors struct{} + +var Colors colors + +func (c colors) Reset() string { + return "\033[38;5;7m" +} + +func (c colors) Red() string { + return "\033[38;5;1m" +} + +func (c colors) Green() string { + return "\033[38;5;2m" +} + +func (c colors) Yellow() string { + return "\033[38;5;3m" +} + +func (c colors) Blue() string { + return "\033[38;5;4m" +} + +func (c colors) Purple() string { + return "\033[38;5;5m" +} + +func (c colors) Cyan() string { + return "\033[38;5;6m" +} + +func (c colors) Red2() string { + return "\033[38;5;9m" +} + +func (c colors) Green2() string { + return "\033[38;5;10m" +} + +func (c colors) Yellow2() string { + return "\033[38;5;11m" +} + +func (c colors) Blue2() string { + return "\033[38;5;12m" +} diff --git a/colorizer/easer.go b/colorizer/easer.go new file mode 100644 index 0000000..978fcd5 --- /dev/null +++ b/colorizer/easer.go @@ -0,0 +1,63 @@ +package colorizer + +import ( + "fmt" + "log" +) + +type RuneHandler func(rune) (bool, string) +type Handler func([]rune, string) RuneHandler + +func General(accept []rune, color string) RuneHandler { + return func(r rune) (bool, string) { + for _, t := range accept { + if t == r { + return true, color + } + } + return false, "" + } +} + +// Left +var LeftLittleFinger = General([]rune{'z', 'a', 'q', '1', '2'}, Colors.Red()) +var LeftRingFinger = General([]rune{'x', 's', 'w', '3'}, Colors.Green()) +var LeftMiddleFinger = General([]rune{'c', 'd', 'e', '4'}, Colors.Yellow()) +var LeftIndexFinger = General([]rune{'v', 'f', 'r', '5', 'b', 'g', 't', '6'}, Colors.Blue()) +var LeftThumpFinger = General([]rune{}, "") + +// Right +var RightThumpFinger = General([]rune{}, "") +var RightIndexFinger = General([]rune{'n', 'h', 'y', '7', 'm', 'j', 'u', '8'}, Colors.Blue2()) +var RightMiddleFinger = General([]rune{',', 'k', 'i', '9'}, Colors.Yellow2()) +var RightRingFinger = General([]rune{'.', 'l', 'o', '0'}, Colors.Green2()) +var RightLittleFinger = General([]rune{'/', ';', 'p', '-', '\\', '\'', '[', ']', '='}, Colors.Red2()) + +var handlers = [8]RuneHandler{ + LeftLittleFinger, + LeftRingFinger, + LeftMiddleFinger, + LeftIndexFinger, + RightIndexFinger, + RightMiddleFinger, + RightRingFinger, + RightLittleFinger, +} + +func Accept(r rune) string { + var buffer string + var wasReallyAccpeted bool + for i, h := range handlers { + accepted, color := h(r) + if accepted { + log.Printf("%sRune %c was accepted by handler with %d id%s", color, r, i, Colors.Reset()) + buffer += fmt.Sprintf("%s%c", color, r) + wasReallyAccpeted = accepted + break + } + } + if !wasReallyAccpeted { + buffer += fmt.Sprintf("%s%c", Colors.Reset(), r) + } + return buffer +} diff --git a/colorizer/reader.go b/colorizer/reader.go new file mode 100644 index 0000000..998a76a --- /dev/null +++ b/colorizer/reader.go @@ -0,0 +1,16 @@ +package colorizer + +import ( + "fmt" + "log" +) + +func PrintColorized(s string) { + var buffer string + for _, r := range s { + log.Printf("Trying to colorize %c", r) + buffer += Accept(r) + } + buffer += Colors.Reset() + fmt.Println(buffer) +} diff --git a/db/db.go b/db/db.go new file mode 100644 index 0000000..3d8f6c8 --- /dev/null +++ b/db/db.go @@ -0,0 +1,48 @@ +package db + +import ( + "log" + "os" + "sync" + "time" + + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var ( + udb *gorm.DB + conMu sync.Mutex +) + +func Connect() *gorm.DB { + conMu.Lock() + defer conMu.Unlock() + if udb != nil { + return udb + } + logFile, err := os.OpenFile("db.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + newLogger := logger.New( + log.New(logFile, "\r\n", log.LstdFlags), + logger.Config{ + SlowThreshold: time.Second, + LogLevel: logger.Error, + IgnoreRecordNotFoundError: true, + ParameterizedQueries: true, + Colorful: false, + }, + ) + gormDB, err := gorm.Open(sqlite.Open("tipitypy.db"), &gorm.Config{ + Logger: newLogger, + }) + if err != nil { + log.Panic(err) + } + newUDB := gormDB + gormDB.AutoMigrate(&Word{}) + return newUDB +} diff --git a/db/word.go b/db/word.go new file mode 100644 index 0000000..efa5c43 --- /dev/null +++ b/db/word.go @@ -0,0 +1,34 @@ +package db + +import ( + "errors" + + "gorm.io/gorm" +) + +type Word struct { + gorm.Model + Value string +} + +var ( + ErrCardValueEmpty = errors.New("the 'Value' field for 'Word' cannot be empty") + ErrCardValueNotUnique = errors.New("the 'Value' field for 'Word' have to be unique for user") +) + +func (c *Word) BeforeSave(tx *gorm.DB) error { + if c.Value == "" { + return ErrCardValueEmpty + } + + var dup Word + if err := tx.Find(&dup, Word{Value: c.Value}).Error; err != nil { + return err + } + + if c.ID != dup.ID && dup.ID != 0 { + return ErrCardValueNotUnique + } + + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7c566d3 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module tipitypy + +go 1.21.6 + +require ( + golang.org/x/term v0.17.0 + gorm.io/driver/sqlite v1.5.7 + gorm.io/gorm v1.25.12 +) + +require ( + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/mattn/go-sqlite3 v1.14.22 // indirect + golang.org/x/sys 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..d77a877 --- /dev/null +++ b/go.sum @@ -0,0 +1,16 @@ +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +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= +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.17.0 h1:mkTF7LCd6WGJNL3K1Ad7kwxNfYAW6a8a8QqtMblp/4U= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= +gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= +gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= +gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..dbf470f --- /dev/null +++ b/main.go @@ -0,0 +1,91 @@ +package main + +import ( + "bufio" + "fmt" + "log" + "os" + "time" + "tipitypy/colorizer" + "tipitypy/reader" + + "golang.org/x/term" +) + +func finish(start time.Time) { + past := time.Now().Sub(start) + fmt.Printf("\r\n%s", colorizer.Colors.Reset()) + fmt.Printf("Correct: %d ; False: %d ; Skipped: %d\r\n", globalStat.Correct, globalStat.False, globalStat.Skipped) + fmt.Println(past) + fmt.Print("\r\n") + fmt.Printf("CPM: %.2f ;; WPM: %.2f\r\n", + float64(globalStat.Correct)/past.Minutes(), + float64(globalStat.Words+1)/past.Minutes()) +} + +func main() { + logFile, err := os.OpenFile("ttt.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) + if err != nil { + panic(err) + } + defer logFile.Close() + log.SetOutput(logFile) + source, err := reader.GetSourceLine() + if err != nil { + panic(err) + } + log.Print("Start of tipitypy") + colorizer.PrintColorized(source) + fmt.Printf("\n") + // + // Put the terminal in raw mode to read characters as they are typed + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + if err != nil { + panic(err) + } + defer term.Restore(int(os.Stdin.Fd()), oldState) // Restore terminal state at the end + // + p := []rune(source) + i := 0 + // + startTime := time.Now() + defer finish(startTime) + reader := bufio.NewReader(os.Stdin) + for { + r, _, err := reader.ReadRune() + if err != nil { + panic(err) + } + log.Printf("Read %c ; %d as rune", r, r) + + // CTRL + C + if r == 3 { + break + } + // CTRL + D + if r == 4 { + // TODO: Delete + } + // CTRL + S + if r == 19 { + globalStat.Skipped++ + fmt.Printf("%s", colorizer.Accept(p[i])) + i++ + continue + } + // Check + if p[i] == r { + if r == ' ' { + globalStat.Words++ + } + globalStat.Correct++ + fmt.Printf("%s", colorizer.Accept(r)) + i++ + if i == len(p) { + break + } + } else { + globalStat.False++ + } + } +} diff --git a/reader/db_reader.go b/reader/db_reader.go new file mode 100644 index 0000000..5a594db --- /dev/null +++ b/reader/db_reader.go @@ -0,0 +1,17 @@ +package reader + +import ( + "fmt" + "strings" + "tipitypy/db" +) + +func GetSourceLine() (string, error) { + dbc := db.Connect() + var res []string + err := dbc.Raw(`SELECT value FROM words WHERE deleted_at IS NULL ORDER BY RANDOM() LIMIT 10;`).Scan(&res).Error + if err != nil { + return "", fmt.Errorf("dbc.Raw: %w", err) + } + return strings.Join(res, " "), nil +} diff --git a/stat.go b/stat.go new file mode 100644 index 0000000..8feea6f --- /dev/null +++ b/stat.go @@ -0,0 +1,10 @@ +package main + +type Stat struct { + Skipped uint + Correct uint + False uint + Words uint +} + +var globalStat Stat