Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
)

require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
Expand All @@ -28,6 +29,7 @@ require (
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sahilm/fuzzy v0.1.1 // 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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
Expand All @@ -21,6 +23,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
Expand All @@ -39,6 +43,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=
github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
Expand Down
232 changes: 232 additions & 0 deletions ui/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ package ui
import (
"encoding/json"
"fmt"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"os"
"path/filepath"
)
Expand Down Expand Up @@ -158,3 +161,232 @@ func InitSettings() {
ApplySettings()
}
}

type SettingsItem struct {
title string
options []string
details string
selected int
expanded bool
key string
}

func (i SettingsItem) Title() string {
arrow := "→"
if i.expanded {
arrow = "↓"
}
return fmt.Sprintf("%s %s", arrow, i.title)
}

func (i SettingsItem) Description() string {
return fmt.Sprintf("%s: %s", i.options[i.selected], i.details)
}

func (i SettingsItem) FilterValue() string { return i.title }

type SettingsModel struct {
list list.Model
height, width int
settings UserSettings
}

func createSettingsItems(settings UserSettings) []list.Item {
themeOptions := []string{"default", "dark", "light"}
themeSelected := 0
for i, opt := range themeOptions {
if opt == settings.ThemeName {
themeSelected = i
break
}
}

cursorOptions := []string{"block", "underline"}
cursorSelected := 0
for i, opt := range cursorOptions {
if opt == settings.CursorType {
cursorSelected = i
break
}
}

gameModeOptions := []string{GameModeNormal, GameModeSimple}
gameModeSelected := 0
for i, opt := range gameModeOptions {
if opt == settings.GameMode {
gameModeSelected = i
break
}
}

textLengthOptions := []string{TextLengthShort, TextLengthMedium, TextLengthLong, TextLengthVeryLong}
textLengthSelected := 0
for i, opt := range textLengthOptions {
if opt == settings.TextLength {
textLengthSelected = i
break
}
}

refreshRateOptions := []string{"5", "10", "15", "20", "30"}
refreshRateSelected := 0
for i, opt := range refreshRateOptions {
if opt == fmt.Sprintf("%d", settings.RefreshRate) {
refreshRateSelected = i
break
}
}

return []list.Item{
&SettingsItem{
title: "Theme",
options: themeOptions,
details: "Select your preferred theme",
selected: themeSelected,
key: "theme",
},
&SettingsItem{
title: "Cursor Type",
options: cursorOptions,
details: "Choose cursor appearance",
selected: cursorSelected,
key: "cursor",
},
&SettingsItem{
title: "Game Mode",
options: gameModeOptions,
details: "Select game difficulty mode",
selected: gameModeSelected,
key: "game_mode",
},
&SettingsItem{
title: "Text Length",
options: textLengthOptions,
details: "Choose text length for typing",
selected: textLengthSelected,
key: "text_length",
},
&SettingsItem{
title: "Refresh Rate",
options: refreshRateOptions,
details: "Set UI refresh rate (FPS)",
selected: refreshRateSelected,
key: "refresh_rate",
},
}
}

func initialSettingsModel() SettingsModel {
settings := CurrentSettings
items := createSettingsItems(settings)

l := list.New(items, list.NewDefaultDelegate(), 0, 0)
l.SetShowHelp(false)
l.SetFilteringEnabled(false)
l.SetShowStatusBar(false)
l.Title = "Settings"
l.Styles.Title = SettingsTitleStyle

return SettingsModel{
list: l,
settings: settings,
}
}

func (m SettingsModel) Init() tea.Cmd { return nil }

func (m SettingsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
m.width, m.height = msg.Width, msg.Height
m.list.SetSize(m.width/3, m.height/2)

case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "enter":
if i, ok := m.list.SelectedItem().(*SettingsItem); ok {
if !i.expanded {
i.expanded = true
} else {
i.selected = (i.selected + 1) % len(i.options)
switch i.key {
case "theme":
m.settings.ThemeName = i.options[i.selected]
case "cursor":
m.settings.CursorType = i.options[i.selected]
case "game_mode":
m.settings.GameMode = i.options[i.selected]
case "text_length":
m.settings.TextLength = i.options[i.selected]
case "refresh_rate":
fmt.Sscanf(i.options[i.selected], "%d", &m.settings.RefreshRate)
}
UpdateSettings(m.settings)
}
}
case "esc":
if i, ok := m.list.SelectedItem().(*SettingsItem); ok {
i.expanded = false
}
}
}

var cmd tea.Cmd
m.list, cmd = m.list.Update(msg)
return m, cmd
}

func (m SettingsModel) View() string {
if m.width == 0 {
return "Loading..."
}

listView := SettingsListStyle.Render(m.list.View())

details := "Select an item to view details"
if i, ok := m.list.SelectedItem().(*SettingsItem); ok {
if i.expanded {
details = fmt.Sprintf("%s\n\nCurrent: %s\n\nOptions:\n",
i.details,
i.options[i.selected],
)
for idx, opt := range i.options {
bullet := "•"
if idx == i.selected {
bullet = ">"
}
details += fmt.Sprintf("%s %s\n", bullet, opt)
}
} else {
details = i.details
}
}

detailsView := SettingsDetailsStyle.Render(details)

content := lipgloss.Place(
m.width,
m.height-1,
lipgloss.Center,
lipgloss.Center,
lipgloss.JoinHorizontal(lipgloss.Top, listView, detailsView),
)

help := lipgloss.PlaceHorizontal(
m.width,
lipgloss.Center,
SettingsHelpStyle.Render("↑/↓: Navigate • Enter: Select • Esc: Back • q: Quit"),
)

return lipgloss.JoinVertical(lipgloss.Bottom, content, help)
}

func ShowSettings() error {
p := tea.NewProgram(initialSettingsModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
return fmt.Errorf("error running settings program: %w", err)
}
return nil
}
24 changes: 21 additions & 3 deletions ui/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,23 @@ func UpdateStyles() {
Foreground(GetColor("cursor_underline")).
Underline(true)

SettingsListStyle = lipgloss.NewStyle().
Width(MaxWidth/3 - 4).
MarginLeft(2).
MarginRight(2)

SettingsDetailsStyle = lipgloss.NewStyle().
Width(MaxWidth / 2).
MarginLeft(2)

SettingsTitleStyle = lipgloss.NewStyle().
Background(lipgloss.Color("62")).
Foreground(lipgloss.Color("0")).
Padding(0, 1)

SettingsHelpStyle = lipgloss.NewStyle().
Foreground(lipgloss.Color("241"))

EndGameTitleStyle = lipgloss.NewStyle().
Foreground(GetColor("text_correct")).
Bold(true).
Expand Down Expand Up @@ -109,9 +126,10 @@ var DimStyle lipgloss.Style
var TextContainerStyle lipgloss.Style
var BlockCursorStyle lipgloss.Style
var UnderlineCursorStyle lipgloss.Style

// endgame screen styles
// .
var SettingsListStyle lipgloss.Style
var SettingsDetailsStyle lipgloss.Style
var SettingsTitleStyle lipgloss.Style
var SettingsHelpStyle lipgloss.Style
var EndGameTitleStyle lipgloss.Style
var EndGameStatsBoxStyle lipgloss.Style
var EndGameWpmStyle lipgloss.Style
Expand Down
Loading