Skip to content

Commit a18d3a8

Browse files
authored
Merge pull request #3 from elbachir-one/main
Made some fixes and added version flag
2 parents d1efc27 + 592bd9c commit a18d3a8

File tree

6 files changed

+97
-56
lines changed

6 files changed

+97
-56
lines changed

.goreleaser.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ builds:
2121
goarch:
2222
- amd64
2323
- arm64
24+
ldflags:
25+
- -s -w -X github.com/ashish0kumar/typtea/cmd.version={{.Version}}
2426

2527
archives:
2628
- id: default
@@ -50,4 +52,4 @@ changelog:
5052

5153
release:
5254
draft: false
53-
prerelease: auto
55+
prerelease: auto

cmd/root.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,33 @@ import (
77
"github.com/spf13/cobra"
88
)
99

10+
var (
11+
version = "dev" // fallback to dev
12+
showVersion bool
13+
)
14+
1015
// rootCmd represents the base command when called without any subcommands
1116
var rootCmd = &cobra.Command{
1217
Use: "typtea",
1318
Short: "A minimal typing speed test in your terminal",
1419
Long: `A terminal-based typing speed test application.
15-
Supports multiple programming languages like Python, JavaScript, Go, and more.`,
20+
Supports multiple programming languages like Python, JavaScript, Go, and more.`,
1621
Example: ` typtea start --lang python
17-
typtea start --duration 30 --lang javascript
18-
typtea start --list-langs`,
22+
typtea start --duration 30 --lang javascript
23+
typtea start --list-langs`,
24+
Run: func(cmd *cobra.Command, args []string) {
25+
// Show help if no subcommands or flags are provided
26+
cmd.Help()
27+
},
28+
}
29+
30+
// versionCmd prints the current version of typtea
31+
var versionCmd = &cobra.Command{
32+
Use: "version",
33+
Short: "Show the version of typtea",
34+
Run: func(cmd *cobra.Command, args []string) {
35+
fmt.Println("typtea version", version)
36+
},
1937
}
2038

2139
// Execute adds all child commands to the root command and sets flags appropriately
@@ -26,8 +44,22 @@ func Execute() {
2644
}
2745
}
2846

29-
// init function initializes the root command and adds subcommands
47+
// init function initializes the root command and adds subcommands and flags
3048
func init() {
3149
rootCmd.CompletionOptions.DisableDefaultCmd = true // Disable default completion command
50+
51+
// Add --version flag with shorthand -v
52+
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "Show the version and exit")
53+
54+
// Add your subcommands
3255
rootCmd.AddCommand(startCmd)
56+
rootCmd.AddCommand(versionCmd)
57+
58+
// Check for version flag early and exit if set
59+
cobra.OnInitialize(func() {
60+
if showVersion {
61+
fmt.Println("typtea version", version)
62+
os.Exit(0)
63+
}
64+
})
3365
}

cmd/start.go

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ package cmd
22

33
import (
44
"fmt"
5-
"log"
6-
"os"
75
"strings"
86

97
"github.com/ashish0kumar/typtea/internal/game"
@@ -31,24 +29,22 @@ var startCmd = &cobra.Command{
3129
RunE: runTypingTest,
3230
}
3331

34-
// init function initializes the start command and its flags
3532
func init() {
36-
startCmd.Flags().IntVarP(&duration, "duration", "d", 30, "Test duration in seconds")
33+
startCmd.Flags().IntVarP(&duration, "duration", "d", 30, "Test duration in seconds (10-300)")
3734
startCmd.Flags().StringVarP(&language, "lang", "l", "en", "Language for typing test")
3835
startCmd.Flags().BoolVar(&listLangs, "list-langs", false, "List all available languages")
3936
}
4037

41-
// runTypingTest is the main function that runs the typing test
38+
// runTypingTest runs the typing test or lists languages if requested
4239
func runTypingTest(cmd *cobra.Command, args []string) error {
43-
4440
// Initialize the language manager
4541
langManager := game.NewLanguageManager()
4642

47-
// If --list-langs is set, print available languages and exit
43+
// If --list-langs flag is set, print available languages and exit
4844
if listLangs {
49-
fmt.Println("Available languages:")
45+
cmd.Println("Available languages:")
5046
for _, lang := range langManager.GetAvailableLanguages() {
51-
fmt.Printf(" %s\n", lang)
47+
cmd.Printf(" %s\n", lang)
5248
}
5349
return nil
5450
}
@@ -58,25 +54,24 @@ func runTypingTest(cmd *cobra.Command, args []string) error {
5854
return fmt.Errorf("duration must be between 10 and 300 seconds (e.g., --duration 60)")
5955
}
6056

61-
// Validate language
57+
// Validate language availability
6258
if !langManager.IsLanguageAvailable(language) {
6359
available := langManager.GetAvailableLanguages()
64-
fmt.Fprintf(os.Stderr, "Error: Language '%s' not available.\n", language)
65-
fmt.Fprintf(os.Stderr, "Available languages: %s\n", strings.Join(available, ", "))
60+
cmd.PrintErrf("Error: Language '%s' not available.\n", language)
61+
cmd.PrintErrf("Available languages: %s\n", strings.Join(available, ", "))
6662
return fmt.Errorf("invalid language: %s", language)
6763
}
6864

6965
// Create a new typing test model
7066
model, err := tui.NewModel(duration, language)
7167
if err != nil {
72-
fmt.Fprintf(os.Stderr, "Error creating typing test: %v\n", err)
73-
os.Exit(1)
68+
return fmt.Errorf("error creating typing test: %w", err)
7469
}
7570

76-
// Start the TUI program
71+
// Start the TUI program with alternate screen
7772
p := tea.NewProgram(model, tea.WithAltScreen())
7873
if _, err := p.Run(); err != nil {
79-
log.Fatal(err)
74+
return fmt.Errorf("error running TUI program: %w", err)
8075
}
8176

8277
return nil

internal/game/languages.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,17 @@ func NewLanguageManager() *LanguageManager {
2929
lm := &LanguageManager{
3030
loadedLanguages: make(map[string][]string),
3131
}
32-
lm.scanAvailableLanguages()
32+
if err := lm.scanAvailableLanguages(); err != nil {
33+
fmt.Printf("Warning: failed to scan available languages: %v\n", err)
34+
}
3335
return lm
3436
}
3537

3638
// scanAvailableLanguages scans the embedded filesystem for available language files
37-
func (lm *LanguageManager) scanAvailableLanguages() {
39+
func (lm *LanguageManager) scanAvailableLanguages() error {
3840
entries, err := fs.ReadDir(embeddedLanguages, "data")
3941
if err != nil {
40-
return
42+
return err
4143
}
4244

4345
for _, entry := range entries {
@@ -46,10 +48,13 @@ func (lm *LanguageManager) scanAvailableLanguages() {
4648
lm.availableLanguages = append(lm.availableLanguages, lang)
4749
}
4850
}
51+
return nil
4952
}
5053

5154
// LoadLanguage loads the specified language from embedded files and caches it
5255
func (lm *LanguageManager) LoadLanguage(langCode string) ([]string, error) {
56+
langCode = strings.ToLower(langCode)
57+
5358
// Check if already loaded
5459
if words, exists := lm.loadedLanguages[langCode]; exists {
5560
return words, nil
@@ -78,13 +83,16 @@ func (lm *LanguageManager) LoadLanguage(langCode string) ([]string, error) {
7883
return langData.Words, nil
7984
}
8085

81-
// GetAvailableLanguages returns a list of all available language codes
86+
// GetAvailableLanguages returns a copy of all available language codes
8287
func (lm *LanguageManager) GetAvailableLanguages() []string {
83-
return lm.availableLanguages
88+
cpy := make([]string, len(lm.availableLanguages))
89+
copy(cpy, lm.availableLanguages)
90+
return cpy
8491
}
8592

8693
// IsLanguageAvailable checks if a language is available in the manager
8794
func (lm *LanguageManager) IsLanguageAvailable(langCode string) bool {
95+
langCode = strings.ToLower(langCode)
8896
for _, lang := range lm.availableLanguages {
8997
if lang == langCode {
9098
return true

internal/game/typing.go

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,15 @@ func NewTypingGame(duration int) *TypingGame {
4444
LinesPerView: 3,
4545
CharsPerLine: 50,
4646
}
47-
4847
game.generateDisplayLines()
4948
return game
5049
}
5150

51+
// Reset reinitializes the game to a fresh state
52+
func (g *TypingGame) Reset() {
53+
*g = *NewTypingGame(g.Duration)
54+
}
55+
5256
// generateDisplayLines creates the initial display lines based on the words available
5357
func (g *TypingGame) generateDisplayLines() {
5458
lines := make([]string, 0, g.LinesPerView)
@@ -109,32 +113,36 @@ func (g *TypingGame) Start() {
109113
}
110114
}
111115

112-
// Reset resets the game state to allow for a new session
116+
// AddCharacter handles user input and updates game state
113117
func (g *TypingGame) AddCharacter(char rune) {
114118
if !g.IsStarted {
115119
g.Start()
116120
}
117121

118122
if g.IsFinished || g.IsTimeUp() {
123+
g.IsFinished = true
119124
return
120125
}
121126

122127
g.UserInput += string(char)
123-
displayText := strings.Join(g.DisplayLines, " ")
128+
displayText := []rune(strings.Join(g.DisplayLines, " "))
124129

125130
// Check if the character is within bounds
126131
if g.CurrentPos < len(displayText) && g.CurrentPos >= 0 {
127-
if rune(displayText[g.CurrentPos]) != char {
132+
if displayText[g.CurrentPos] != char {
128133
g.Errors[g.GlobalPos] = true
129134
g.TotalErrorsMade++
130135
}
131136
g.CurrentPos++
132137
g.GlobalPos++
133138

134139
// Check if the first line is completed
135-
if g.CurrentPos >= len(g.DisplayLines[0])+1 { // +1 for space
140+
if g.CurrentPos >= len([]rune(g.DisplayLines[0])) {
136141
g.shiftLines()
137142
}
143+
} else {
144+
// End of available text
145+
g.IsFinished = true
138146
}
139147
}
140148

@@ -161,6 +169,7 @@ func (g *TypingGame) RemoveCharacter() {
161169
g.CurrentPos--
162170
g.GlobalPos--
163171

172+
// Remove error mark if previously added
164173
delete(g.Errors, g.GlobalPos)
165174
}
166175
}
@@ -189,19 +198,16 @@ func (g *TypingGame) GetStats() TypingStats {
189198
uncorrectedErrors := len(g.Errors)
190199

191200
// Calculate Net WPM (Gross WPM - uncorrected errors per minute)
192-
netWPM := grossWPM
193-
if minutes > 0 {
194-
errorRate := float64(uncorrectedErrors) / minutes
195-
netWPM = grossWPM - errorRate
196-
}
201+
errorRate := float64(uncorrectedErrors) / minutes
202+
netWPM := grossWPM - errorRate
197203

198204
// Ensure Net WPM doesn't go below 0
199205
if netWPM < 0 {
200206
netWPM = 0
201207
}
202208

203209
// Calculate accuracy (correct characters / total characters typed * 100)
204-
correctChars := g.GlobalPos - g.TotalErrorsMade // You need to add this field
210+
correctChars := g.GlobalPos - g.TotalErrorsMade
205211
accuracy := 0.0
206212
if g.GlobalPos > 0 {
207213
accuracy = float64(correctChars) / float64(g.GlobalPos) * 100
@@ -217,7 +223,7 @@ func (g *TypingGame) GetStats() TypingStats {
217223
Accuracy: accuracy,
218224
CharactersTyped: g.GlobalPos,
219225
CorrectChars: correctChars,
220-
TotalChars: len(g.GetDisplayText()),
226+
TotalChars: len([]rune(g.GetDisplayText())),
221227
TimeElapsed: elapsed,
222228
IsComplete: g.IsFinished,
223229
UncorrectedErrors: uncorrectedErrors,

internal/game/words.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package game
22

33
import (
44
"math/rand"
5+
"sort"
56
"strings"
67
"time"
78
)
@@ -12,9 +13,13 @@ var weights []int
1213
var cumulativeWeights []int
1314
var currentLanguageCode string
1415

15-
// init initializes the language manager and sets the default language to en
16+
// init initializes the language manager and sets the default language to "en"
1617
func init() {
1718
languageManager = NewLanguageManager()
19+
err := SetLanguage("en")
20+
if err != nil {
21+
panic("failed to initialize default language: " + err.Error())
22+
}
1823
}
1924

2025
// SetLanguage sets the current language for the game and loads the corresponding words
@@ -50,7 +55,7 @@ func calculateWeights() {
5055
weights[i] = len(currentLanguageWords) - i
5156
}
5257

53-
// Calculate cumulative weights
58+
// Calculate cumulative weights for binary search
5459
cumulativeWeights = make([]int, len(weights))
5560
cumSum := 0
5661
for i, w := range weights {
@@ -65,25 +70,18 @@ func findWordIndex(r int) int {
6570
return 0
6671
}
6772

68-
left, right := 0, len(cumulativeWeights)-1
69-
70-
for left < right {
71-
mid := (left + right) / 2
72-
if cumulativeWeights[mid] < r {
73-
left = mid + 1
74-
} else {
75-
right = mid
76-
}
77-
}
78-
79-
return left
73+
return sort.Search(len(cumulativeWeights), func(i int) bool {
74+
return cumulativeWeights[i] >= r
75+
})
8076
}
8177

8278
// GenerateWords generates a slice of words based on the current language and the specified count
8379
func GenerateWords(count int) []string {
8480
if len(currentLanguageWords) == 0 {
8581
// Fallback to English
86-
SetLanguage("en")
82+
if err := SetLanguage("en"); err != nil {
83+
panic("failed to load fallback language: " + err.Error())
84+
}
8785
}
8886

8987
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
@@ -93,16 +91,16 @@ func GenerateWords(count int) []string {
9391
if currentLanguageCode == "en" && len(cumulativeWeights) > 0 {
9492
maxWeight := cumulativeWeights[len(cumulativeWeights)-1]
9593

96-
for i := 0; i < count; i++ {
97-
r := rand.Intn(maxWeight) + 1
94+
for i := range words {
95+
r := rng.Intn(maxWeight) + 1 // random in range [1, maxWeight]
9896
idx := findWordIndex(r)
9997
words[i] = currentLanguageWords[idx]
10098
}
10199
return words
102100
}
103101

104102
// For all other languages, use simple random selection
105-
for i := 0; i < count; i++ {
103+
for i := range words {
106104
words[i] = currentLanguageWords[rng.Intn(len(currentLanguageWords))]
107105
}
108106

0 commit comments

Comments
 (0)