From f12fe12fc58e64cf657f1248cd1a034b2703ddf8 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 14:54:42 +1100 Subject: [PATCH 1/8] refactor: move starter package --- cmd/tetrigo/subcommands.go | 2 +- internal/tui/{models => }/starter/model.go | 0 internal/tui/{models => }/starter/styles.go | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename internal/tui/{models => }/starter/model.go (100%) rename internal/tui/{models => }/starter/styles.go (100%) diff --git a/cmd/tetrigo/subcommands.go b/cmd/tetrigo/subcommands.go index c6ea8fa..35de244 100644 --- a/cmd/tetrigo/subcommands.go +++ b/cmd/tetrigo/subcommands.go @@ -2,11 +2,11 @@ package main import ( "fmt" + "github.com/Broderick-Westrope/tetrigo/internal/tui/starter" "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/data" "github.com/Broderick-Westrope/tetrigo/internal/tui" - "github.com/Broderick-Westrope/tetrigo/internal/tui/models/starter" tea "github.com/charmbracelet/bubbletea" ) diff --git a/internal/tui/models/starter/model.go b/internal/tui/starter/model.go similarity index 100% rename from internal/tui/models/starter/model.go rename to internal/tui/starter/model.go diff --git a/internal/tui/models/starter/styles.go b/internal/tui/starter/styles.go similarity index 100% rename from internal/tui/models/starter/styles.go rename to internal/tui/starter/styles.go From bc38ef3ad991a7597c0cdad8cf0dac6d361997ab Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:22:14 +1100 Subject: [PATCH 2/8] refactor: move models to views package --- go.mod | 9 +- go.sum | 6 +- internal/tui/errors.go | 7 + internal/tui/models/game/keymap.go | 76 ----- internal/tui/overlay.go | 207 -------------- internal/tui/starter/model.go | 19 +- internal/tui/utils.go | 261 ++++++++++++++++++ internal/tui/views/game_keys.go | 62 +++++ .../game/styles.go => views/game_styles.go} | 8 +- .../keymap.go => views/leaderboard_keys.go} | 12 +- .../model.go => views/leaderboard_model.go} | 26 +- .../menu/keymap.go => views/menu_keys.go} | 12 +- .../menu/model.go => views/menu_model.go} | 34 +-- .../menu/styles.go => views/menu_styles.go} | 8 +- .../single/model.go => views/single_model.go} | 62 +++-- 15 files changed, 431 insertions(+), 378 deletions(-) create mode 100644 internal/tui/errors.go delete mode 100644 internal/tui/models/game/keymap.go delete mode 100644 internal/tui/overlay.go create mode 100644 internal/tui/utils.go create mode 100644 internal/tui/views/game_keys.go rename internal/tui/{models/game/styles.go => views/game_styles.go} (95%) rename internal/tui/{models/leaderboard/keymap.go => views/leaderboard_keys.go} (77%) rename internal/tui/{models/leaderboard/model.go => views/leaderboard_model.go} (78%) rename internal/tui/{models/menu/keymap.go => views/menu_keys.go} (82%) rename internal/tui/{models/menu/model.go => views/menu_model.go} (87%) rename internal/tui/{models/menu/styles.go => views/menu_styles.go} (73%) rename internal/tui/{models/single/model.go => views/single_model.go} (86%) diff --git a/go.mod b/go.mod index e3ab64b..018a091 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,9 @@ require ( github.com/charmbracelet/bubbles v0.18.0 github.com/charmbracelet/bubbletea v0.25.0 github.com/charmbracelet/lipgloss v0.9.1 + github.com/charmbracelet/x/ansi v0.4.5 github.com/mattn/go-runewidth v0.0.16 github.com/mattn/go-sqlite3 v1.14.22 - github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b - github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.15.2 github.com/stretchr/testify v1.9.0 ) @@ -24,9 +22,12 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-localereader v0.0.1 // indirect + github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.4.6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect golang.org/x/sync v0.1.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/term v0.6.0 // indirect diff --git a/go.sum b/go.sum index a93d3d8..6d6b65f 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg= github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I= +github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSexTdRM= +github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -45,8 +47,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.6 h1:Sovz9sDSwbOz9tgUy8JpT+KgCkPYJEN/oYzlJiYTNLg= -github.com/rivo/uniseg v0.4.6/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/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= diff --git a/internal/tui/errors.go b/internal/tui/errors.go new file mode 100644 index 0000000..34bc9c4 --- /dev/null +++ b/internal/tui/errors.go @@ -0,0 +1,7 @@ +package tui + +import "errors" + +var ( + ErrInvalidTypeAssertion = errors.New("invalid type assertion") +) diff --git a/internal/tui/models/game/keymap.go b/internal/tui/models/game/keymap.go deleted file mode 100644 index 0fa280f..0000000 --- a/internal/tui/models/game/keymap.go +++ /dev/null @@ -1,76 +0,0 @@ -package game - -import ( - "github.com/Broderick-Westrope/tetrigo/internal/config" - "github.com/charmbracelet/bubbles/key" -) - -type KeyMap struct { - ForceQuit key.Binding - Exit key.Binding - Help key.Binding - Left key.Binding - Right key.Binding - Clockwise key.Binding - CounterClockwise key.Binding - SoftDrop key.Binding - HardDrop key.Binding - Hold key.Binding -} - -func constructKeyBinding(keys []string, desc string) key.Binding { - buildHelpKeys := func(keys []string) string { - helpKeys := "" - for _, key := range keys { - if key == " " { - key = "space" - } - helpKeys += key + ", " - } - return helpKeys[:len(helpKeys)-2] - } - - return key.NewBinding(key.WithKeys(keys...), key.WithHelp(buildHelpKeys(keys), desc)) -} - -func ConstructKeyMap(keys *config.Keys) *KeyMap { - return &KeyMap{ - ForceQuit: constructKeyBinding(keys.ForceQuit, "force quit"), - Exit: constructKeyBinding(keys.Exit, "exit"), - Help: constructKeyBinding(keys.Help, "help"), - Left: constructKeyBinding(keys.Left, "move left"), - Right: constructKeyBinding(keys.Right, "move right"), - Clockwise: constructKeyBinding(keys.RotateClockwise, "rotate clockwise"), - CounterClockwise: constructKeyBinding(keys.RotateCounterClockwise, "rotate counter-clockwise"), - SoftDrop: constructKeyBinding(keys.Down, "toggle soft drop"), - HardDrop: constructKeyBinding(keys.Up, "hard drop"), - Hold: constructKeyBinding(keys.Submit, "hold"), - } -} - -func (k *KeyMap) ShortHelp() []key.Binding { - return []key.Binding{ - k.Exit, - k.Help, - } -} - -func (k *KeyMap) FullHelp() [][]key.Binding { - return [][]key.Binding{ - { - k.Exit, - k.Help, - k.Left, - }, - { - k.Right, - k.Clockwise, - k.CounterClockwise, - }, - { - k.SoftDrop, - k.HardDrop, - k.Hold, - }, - } -} diff --git a/internal/tui/overlay.go b/internal/tui/overlay.go deleted file mode 100644 index a3ddeb0..0000000 --- a/internal/tui/overlay.go +++ /dev/null @@ -1,207 +0,0 @@ -package tui - -import ( - "bytes" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/mattn/go-runewidth" - "github.com/muesli/ansi" - "github.com/muesli/reflow/truncate" - "github.com/muesli/termenv" -) - -const ( - pausedMsg = ` ____ __ - / __ \____ ___ __________ ____/ / - / /_/ / __ ^/ / / / ___/ _ \/ __ / -/ ____/ /_/ / /_/ (__ ) __/ /_/ / -/_/ \__,_/\__,_/____/\___/\__,_/ -Press PAUSE to continue or HOLD to exit.` - - gameOverMsg = ` ______ ____ - / ____/___ _____ ___ ___ / __ \_ _____ _____ - / / __/ __ ^/ __ ^__ \/ _ \ / / / / | / / _ \/ ___/ -/ /_/ / /_/ / / / / / / __/ / /_/ /| |/ / __/ / -\____/\__,_/_/ /_/ /_/\___/ \____/ |___/\___/_/ - - Press EXIT or HOLD to continue.` -) - -// Most of this code is borrowed from -// https://github.com/charmbracelet/lipgloss/pull/102 -// as well as the lipgloss library. - -func OverlayPausedMessage(bg string) string { - return placeOverlayCenter(pausedMsg, bg) -} - -func OverlayGameOverMessage(bg string) string { - return placeOverlayCenter(gameOverMsg, bg) -} - -// Split a string into lines, additionally returning the size of the widest -// line. -func getLines(s string) ([]string, int) { - lines := strings.Split(s, "\n") - - var widest int - for _, l := range lines { - w := ansi.PrintableRuneWidth(l) - if widest < w { - widest = w - } - } - - return lines, widest -} - -func placeOverlayCenter(fg, bg string, opts ...WhitespaceOption) string { - x := lipgloss.Width(bg) / 2 - y := lipgloss.Height(bg) / 2 - return placeOverlay(x, y, fg, bg, opts...) -} - -// placeOverlay places fg on top of bg. -func placeOverlay(x, y int, fg, bg string, opts ...WhitespaceOption) string { - fgLines, fgWidth := getLines(fg) - bgLines, bgWidth := getLines(bg) - bgHeight := len(bgLines) - fgHeight := len(fgLines) - - if fgWidth >= bgWidth && fgHeight >= bgHeight { - // FIXME: return fg or bg? - return fg - } - // TODO: allow placement outside of the bg box? - x = clamp(x, 0, bgWidth-fgWidth) - y = clamp(y, 0, bgHeight-fgHeight) - - ws := &whitespace{} - for _, opt := range opts { - opt(ws) - } - - var b strings.Builder - for i, bgLine := range bgLines { - if i > 0 { - b.WriteByte('\n') - } - if i < y || i >= y+fgHeight { - b.WriteString(bgLine) - continue - } - - pos := 0 - if x > 0 { - left := truncate.String(bgLine, uint(x)) - pos = ansi.PrintableRuneWidth(left) - b.WriteString(left) - if pos < x { - b.WriteString(ws.render(x - pos)) - pos = x - } - } - - fgLine := fgLines[i-y] - b.WriteString(fgLine) - pos += ansi.PrintableRuneWidth(fgLine) - - right := cutLeft(bgLine, pos) - bgWidth = ansi.PrintableRuneWidth(bgLine) - rightWidth := ansi.PrintableRuneWidth(right) - if rightWidth <= bgWidth-pos { - b.WriteString(ws.render(bgWidth - rightWidth - pos)) - } - - b.WriteString(right) - } - - return b.String() -} - -// cutLeft cuts printable characters from the left. -// This function is heavily based on muesli's ansi and truncate packages. -func cutLeft(s string, cutWidth int) string { - var ( - pos int - isAnsi bool - ab bytes.Buffer - b bytes.Buffer - ) - for _, c := range s { - var w int - if c == ansi.Marker || isAnsi { - isAnsi = true - ab.WriteRune(c) - if ansi.IsTerminator(c) { - isAnsi = false - if bytes.HasSuffix(ab.Bytes(), []byte("[0m")) { - ab.Reset() - } - } - } else { - w = runewidth.RuneWidth(c) - } - - if pos < cutWidth { - pos += w - continue - } - - if b.Len() == 0 { - if ab.Len() > 0 { - b.Write(ab.Bytes()) - } - if pos-cutWidth > 1 { - b.WriteByte(' ') - continue - } - } - b.WriteRune(c) - pos += w - } - return b.String() -} - -func clamp(v, lower, upper int) int { - return min(max(v, lower), upper) -} - -type whitespace struct { - style termenv.Style - chars string -} - -// Render whitespaces. -func (w whitespace) render(width int) string { - if w.chars == "" { - w.chars = " " - } - - r := []rune(w.chars) - j := 0 - b := strings.Builder{} - - // Cycle through runes and print them into the whitespace. - for i := 0; i < width; { - b.WriteRune(r[j]) - j++ - if j >= len(r) { - j = 0 - } - i += ansi.PrintableRuneWidth(string(r[j])) - } - - // Fill any extra gaps white spaces. This might be necessary if any runes - // are more than one cell wide, which could leave a one-rune gap. - short := width - ansi.PrintableRuneWidth(b.String()) - if short > 0 { - b.WriteString(strings.Repeat(" ", short)) - } - - return w.style.Styled(b.String()) -} - -// WhitespaceOption sets a styling rule for rendering whitespace. -type WhitespaceOption func(*whitespace) diff --git a/internal/tui/starter/model.go b/internal/tui/starter/model.go index 0c61ebc..4d9d682 100644 --- a/internal/tui/starter/model.go +++ b/internal/tui/starter/model.go @@ -3,20 +3,17 @@ package starter import ( "database/sql" "errors" + "github.com/Broderick-Westrope/tetrigo/internal/tui/views" "reflect" "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/tui" - "github.com/Broderick-Westrope/tetrigo/internal/tui/models/leaderboard" - "github.com/Broderick-Westrope/tetrigo/internal/tui/models/menu" - "github.com/Broderick-Westrope/tetrigo/internal/tui/models/single" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) var ( - ErrInvalidSwitchModeInput = errors.New("invalid SwitchModeInput") - ErrInvalidSwitchMode = errors.New("invalid SwitchMode") + ErrInvalidSwitchMode = errors.New("invalid SwitchMode") ) type Input struct { @@ -104,15 +101,15 @@ func (m *Model) setChild(mode tui.Mode, switchIn tui.SwitchModeInput) error { case tui.ModeMenu: menuIn, ok := switchIn.(*tui.MenuInput) if !ok { - return ErrInvalidSwitchModeInput + return tui.ErrInvalidTypeAssertion } - m.child = menu.NewModel(menuIn) + m.child = views.NewMenuModel(menuIn) case tui.ModeMarathon, tui.ModeSprint, tui.ModeUltra: singleIn, ok := switchIn.(*tui.SingleInput) if !ok { - return ErrInvalidSwitchModeInput + return tui.ErrInvalidTypeAssertion } - child, err := single.NewModel(singleIn, m.cfg) + child, err := views.NewSingleModel(singleIn, m.cfg) if err != nil { return err } @@ -120,9 +117,9 @@ func (m *Model) setChild(mode tui.Mode, switchIn tui.SwitchModeInput) error { case tui.ModeLeaderboard: leaderboardIn, ok := switchIn.(*tui.LeaderboardInput) if !ok { - return ErrInvalidSwitchModeInput + return tui.ErrInvalidTypeAssertion } - child, err := leaderboard.NewModel(leaderboardIn, m.db) + child, err := views.NewLeaderboardModel(leaderboardIn, m.db) if err != nil { return err } diff --git a/internal/tui/utils.go b/internal/tui/utils.go new file mode 100644 index 0000000..470783a --- /dev/null +++ b/internal/tui/utils.go @@ -0,0 +1,261 @@ +package tui + +import ( + "errors" + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/x/ansi" + "github.com/mattn/go-runewidth" + "regexp" + "strings" + "unicode" +) + +const ( + PausedMessage = ` ____ __ + / __ \____ ___ __________ ____/ / + / /_/ / __ ^/ / / / ___/ _ \/ __ / +/ ____/ /_/ / /_/ (__ ) __/ /_/ / +/_/ \__,_/\__,_/____/\___/\__,_/ +Press PAUSE to continue or HOLD to exit.` + + GameOverMessage = ` ______ ____ + / ____/___ _____ ___ ___ / __ \_ _____ _____ + / / __/ __ ^/ __ ^__ \/ _ \ / / / / | / / _ \/ ___/ +/ /_/ / /_/ / / / / / / __/ / /_/ /| |/ / __/ / +\____/\__,_/_/ /_/ /_/\___/ \____/ |___/\___/_/ + + Press EXIT or HOLD to continue.` +) + +func ConstructKeyBinding(keys []string, desc string) key.Binding { + buildHelpKeys := func(keys []string) string { + helpKeys := "" + for _, key := range keys { + if key == " " { + key = "space" + } + helpKeys += key + ", " + } + return helpKeys[:len(helpKeys)-2] + } + + return key.NewBinding(key.WithKeys(keys...), key.WithHelp(buildHelpKeys(keys), desc)) +} + +// Window overlay (CREDIT: https://gist.github.com/ras0q/9bf5d81544b22302393f61206892e2cd) ------------------------------------ + +// OverlayCenter writes the overlay string onto the background string such that the middle of the +// overlay string will be at the middle of the overlay will be at the middle of the background. +func OverlayCenter(bg string, overlay string, ignoreMarginWhitespace bool) (string, error) { + row := (lipgloss.Height(bg) - lipgloss.Height(overlay)) / 2 + row = max(0, row) + col := (lipgloss.Width(bg) - lipgloss.Width(overlay)) / 2 + col = max(0, col) + return Overlay(bg, overlay, row, col, ignoreMarginWhitespace) +} + +// Overlay writes the overlay string onto the background string at the specified row and column. +// In this case, the row and column are zero indexed. +func Overlay(bg, overlay string, row, col int, ignoreMarginWhitespace bool) (string, error) { + bgLines := strings.Split(bg, "\n") + overlayLines := strings.Split(overlay, "\n") + + for i, overlayLine := range overlayLines { + targetRow := i + row + + // Ensure the target row exists in the background lines + for len(bgLines) <= targetRow { + bgLines = append(bgLines, "") + } + + bgLine := bgLines[targetRow] + bgLineWidth := ansi.StringWidth(bgLine) + + if bgLineWidth < col { + bgLine += strings.Repeat(" ", col-bgLineWidth) // Add padding + } + + // Handle ignoreMarginWhitespace + if ignoreMarginWhitespace { + // Process the overlay line to preserve leading and trailing whitespace + overlayLine = removeMarginWhitespace(bgLine, overlayLine, col) + } + + bgLeft := ansi.Truncate(bgLine, col, "") + bgRight, err := truncateLeft(bgLine, col+ansi.StringWidth(overlayLine)) + if err != nil { + return "", err + } + + bgLines[targetRow] = bgLeft + overlayLine + bgRight + } + + return strings.Join(bgLines, "\n"), nil +} + +// removeMarginWhitespace preserves the background where the overlay line has leading or trailing whitespace. +// This is done by detecting those empty cells in the overlay string and replacing them with the corresponding background cells. +// +//nolint:gocognit +func removeMarginWhitespace(bgLine, overlayLine string, col int) string { + var result strings.Builder + + // Variables to track ANSI escape sequences + inAnsi := false + ansiSeq := strings.Builder{} + + // Strip ANSI codes to analyze whitespace + overlayStripped := ansi.Strip(overlayLine) + overlayRunes := []rune(overlayStripped) + + // Find first and last non-whitespace positions + firstNonWhitespacePos := -1 + lastNonWhitespacePos := -1 + visualPos := 0 + overlayVisualWidths := make([]int, len(overlayRunes)) + + for i, r := range overlayRunes { + runeWidth := runewidth.RuneWidth(r) + overlayVisualWidths[i] = runeWidth + if !unicode.IsSpace(r) { + if firstNonWhitespacePos == -1 { + firstNonWhitespacePos = visualPos + } + lastNonWhitespacePos = visualPos + runeWidth - 1 // inclusive + } + visualPos += runeWidth + } + + // If all characters are whitespace + if firstNonWhitespacePos == -1 { + firstNonWhitespacePos = 0 + lastNonWhitespacePos = -1 + } + + // Now, process the overlayLine, keeping track of visual positions + visualPos = 0 + runeReader := strings.NewReader(overlayLine) + + for { + r, _, err := runeReader.ReadRune() + if err != nil { + break + } + + if r == '\x1b' { + // Start of ANSI escape sequence + inAnsi = true + ansiSeq.WriteRune(r) + continue + } + + if inAnsi { + ansiSeq.WriteRune(r) + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + // End of ANSI escape sequence + inAnsi = false + result.WriteString(ansiSeq.String()) + ansiSeq.Reset() + } + continue + } + + runeWidth := runewidth.RuneWidth(r) + + // Determine if current position is leading whitespace or trailing whitespace + var isLeadingWhitespace, isTrailingWhitespace bool + + if visualPos < firstNonWhitespacePos { + isLeadingWhitespace = true + } else if visualPos > lastNonWhitespacePos { + isTrailingWhitespace = true + } + + if unicode.IsSpace(r) && (isLeadingWhitespace || isTrailingWhitespace) { + // Preserve background character + for k := range runeWidth { + bgChar := getBgCharAt(bgLine, col+visualPos+k) + result.WriteString(bgChar) + } + } else { + // Include character from overlay (could be a non-whitespace or whitespace character in between) + result.WriteRune(r) + } + + visualPos += runeWidth + } + + return result.String() +} + +// getBgCharAt returns the character from the background line at the specified visual index. +func getBgCharAt(bgLine string, visualIndex int) string { + var result strings.Builder + displayWidth := 0 + inAnsi := false + ansiSeq := strings.Builder{} + + runeReader := strings.NewReader(bgLine) + for { + r, _, err := runeReader.ReadRune() + if err != nil { + break + } + + if r == '\x1b' { + // Start of ANSI escape sequence + inAnsi = true + ansiSeq.WriteRune(r) + continue + } + + if inAnsi { + ansiSeq.WriteRune(r) + if (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') { + // End of ANSI escape sequence + inAnsi = false + result.WriteString(ansiSeq.String()) + ansiSeq.Reset() + } + continue + } + + charWidth := runewidth.RuneWidth(r) + if displayWidth+charWidth > visualIndex { + // We have reached the desired index + result.WriteRune(r) + break + } + + displayWidth += charWidth + } + + // If no character found at the position, return a space + if result.Len() == 0 { + return " " + } + + return result.String() +} + +// truncateLeft removes characters from the beginning of a line, considering ANSI escape codes. +func truncateLeft(line string, padding int) (string, error) { + if strings.Contains(line, "\n") { + return "", errors.New("line must not contain newline") + } + + wrapped := strings.Split(ansi.Hardwrap(line, padding, true), "\n") + if len(wrapped) == 1 { + return "", nil + } + + var ansiStyle string + // Regular expression to match ANSI escape codes. + ansiStyles := regexp.MustCompile(`\x1b[[\d;]*m`).FindAllString(wrapped[0], -1) + if len(ansiStyles) > 0 { + ansiStyle = ansiStyles[len(ansiStyles)-1] + } + + return ansiStyle + strings.Join(wrapped[1:], ""), nil +} diff --git a/internal/tui/views/game_keys.go b/internal/tui/views/game_keys.go new file mode 100644 index 0000000..3a317c3 --- /dev/null +++ b/internal/tui/views/game_keys.go @@ -0,0 +1,62 @@ +package views + +import ( + "github.com/Broderick-Westrope/tetrigo/internal/config" + "github.com/Broderick-Westrope/tetrigo/internal/tui" + "github.com/charmbracelet/bubbles/key" +) + +type GameKeyMap struct { + ForceQuit key.Binding + Exit key.Binding + Help key.Binding + Left key.Binding + Right key.Binding + Clockwise key.Binding + CounterClockwise key.Binding + SoftDrop key.Binding + HardDrop key.Binding + Hold key.Binding +} + +func ConstructGameKeyMap(keys *config.Keys) *GameKeyMap { + return &GameKeyMap{ + ForceQuit: tui.ConstructKeyBinding(keys.ForceQuit, "force quit"), + Exit: tui.ConstructKeyBinding(keys.Exit, "exit"), + Help: tui.ConstructKeyBinding(keys.Help, "help"), + Left: tui.ConstructKeyBinding(keys.Left, "move left"), + Right: tui.ConstructKeyBinding(keys.Right, "move right"), + Clockwise: tui.ConstructKeyBinding(keys.RotateClockwise, "rotate clockwise"), + CounterClockwise: tui.ConstructKeyBinding(keys.RotateCounterClockwise, "rotate counter-clockwise"), + SoftDrop: tui.ConstructKeyBinding(keys.Down, "toggle soft drop"), + HardDrop: tui.ConstructKeyBinding(keys.Up, "hard drop"), + Hold: tui.ConstructKeyBinding(keys.Submit, "hold"), + } +} + +func (k *GameKeyMap) ShortHelp() []key.Binding { + return []key.Binding{ + k.Exit, + k.Help, + } +} + +func (k *GameKeyMap) FullHelp() [][]key.Binding { + return [][]key.Binding{ + { + k.Exit, + k.Help, + k.Left, + }, + { + k.Right, + k.Clockwise, + k.CounterClockwise, + }, + { + k.SoftDrop, + k.HardDrop, + k.Hold, + }, + } +} diff --git a/internal/tui/models/game/styles.go b/internal/tui/views/game_styles.go similarity index 95% rename from internal/tui/models/game/styles.go rename to internal/tui/views/game_styles.go index e15801d..1abf7f8 100644 --- a/internal/tui/models/game/styles.go +++ b/internal/tui/views/game_styles.go @@ -1,11 +1,11 @@ -package game +package views import ( "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/charmbracelet/lipgloss" ) -type Styles struct { +type GameStyles struct { Playfield lipgloss.Style EmptyCell lipgloss.Style TetriminoCellStyles map[byte]lipgloss.Style @@ -29,8 +29,8 @@ type cellCharacters struct { Tetriminos string } -func CreateStyles(theme *config.Theme) *Styles { - s := Styles{ +func CreateGameStyles(theme *config.Theme) *GameStyles { + s := GameStyles{ Playfield: lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).Padding(0), EmptyCell: lipgloss.NewStyle().Foreground(lipgloss.Color(theme.Colours.EmptyCell)), TetriminoCellStyles: map[byte]lipgloss.Style{ diff --git a/internal/tui/models/leaderboard/keymap.go b/internal/tui/views/leaderboard_keys.go similarity index 77% rename from internal/tui/models/leaderboard/keymap.go rename to internal/tui/views/leaderboard_keys.go index 71e3815..84f603a 100644 --- a/internal/tui/models/leaderboard/keymap.go +++ b/internal/tui/views/leaderboard_keys.go @@ -1,10 +1,10 @@ -package leaderboard +package views import ( "github.com/charmbracelet/bubbles/key" ) -type keyMap struct { +type leaderboardKeyMap struct { Exit key.Binding Help key.Binding Left key.Binding @@ -13,8 +13,8 @@ type keyMap struct { Down key.Binding } -func defaultKeyMap() *keyMap { - return &keyMap{ +func defaultLeaderboardKeyMap() *leaderboardKeyMap { + return &leaderboardKeyMap{ Exit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("escape", "exit")), Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), Left: key.NewBinding(key.WithKeys("left"), key.WithHelp("left arrow", "move left")), @@ -24,14 +24,14 @@ func defaultKeyMap() *keyMap { } } -func (k *keyMap) ShortHelp() []key.Binding { +func (k *leaderboardKeyMap) ShortHelp() []key.Binding { return []key.Binding{ k.Exit, k.Help, } } -func (k *keyMap) FullHelp() [][]key.Binding { +func (k *leaderboardKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ { k.Exit, diff --git a/internal/tui/models/leaderboard/model.go b/internal/tui/views/leaderboard_model.go similarity index 78% rename from internal/tui/models/leaderboard/model.go rename to internal/tui/views/leaderboard_model.go index 1ccb32a..7251da7 100644 --- a/internal/tui/models/leaderboard/model.go +++ b/internal/tui/views/leaderboard_model.go @@ -1,4 +1,4 @@ -package leaderboard +package views import ( "database/sql" @@ -13,15 +13,15 @@ import ( "github.com/charmbracelet/lipgloss" ) -type Model struct { - keys *keyMap +type LeaderboardModel struct { + keys *leaderboardKeyMap help help.Model repo *data.LeaderboardRepository table table.Model } -func NewModel(in *tui.LeaderboardInput, db *sql.DB) (Model, error) { +func NewLeaderboardModel(in *tui.LeaderboardInput, db *sql.DB) (LeaderboardModel, error) { repo := data.NewLeaderboardRepository(db) var err error @@ -33,28 +33,28 @@ func NewModel(in *tui.LeaderboardInput, db *sql.DB) (Model, error) { newEntryID, err = repo.Save(in.NewEntry) if err != nil { - return Model{}, err + return LeaderboardModel{}, err } } scores, err := repo.All(in.GameMode) if err != nil { - return Model{}, err + return LeaderboardModel{}, err } - return Model{ - keys: defaultKeyMap(), + return LeaderboardModel{ + keys: defaultLeaderboardKeyMap(), help: help.New(), repo: repo, - table: getLeaderboardTable(scores, newEntryID), + table: buildLeaderboardTable(scores, newEntryID), }, nil } -func (m Model) Init() tea.Cmd { +func (m LeaderboardModel) Init() tea.Cmd { return nil } -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m LeaderboardModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(msg, m.keys.Exit): @@ -69,11 +69,11 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m Model) View() string { +func (m LeaderboardModel) View() string { return m.table.View() + "\n" + m.help.View(m.keys) } -func getLeaderboardTable(scores []data.Score, focusID int) table.Model { +func buildLeaderboardTable(scores []data.Score, focusID int) table.Model { cols := []table.Column{ {Title: "Rank", Width: 4}, {Title: "Name", Width: 10}, diff --git a/internal/tui/models/menu/keymap.go b/internal/tui/views/menu_keys.go similarity index 82% rename from internal/tui/models/menu/keymap.go rename to internal/tui/views/menu_keys.go index 0506ce0..f40d469 100644 --- a/internal/tui/models/menu/keymap.go +++ b/internal/tui/views/menu_keys.go @@ -1,10 +1,10 @@ -package menu +package views import ( "github.com/charmbracelet/bubbles/key" ) -type keyMap struct { +type menuKeyMap struct { Exit key.Binding Help key.Binding Left key.Binding @@ -14,8 +14,8 @@ type keyMap struct { Start key.Binding } -func defaultKeyMap() *keyMap { - return &keyMap{ +func defaultMenuKeyMap() *menuKeyMap { + return &menuKeyMap{ Exit: key.NewBinding(key.WithKeys("esc"), key.WithHelp("escape", "exit")), Help: key.NewBinding(key.WithKeys("?"), key.WithHelp("?", "help")), Left: key.NewBinding(key.WithKeys("left"), key.WithHelp("left arrow", "move left")), @@ -26,7 +26,7 @@ func defaultKeyMap() *keyMap { } } -func (k *keyMap) ShortHelp() []key.Binding { +func (k *menuKeyMap) ShortHelp() []key.Binding { return []key.Binding{ k.Start, k.Exit, @@ -34,7 +34,7 @@ func (k *keyMap) ShortHelp() []key.Binding { } } -func (k *keyMap) FullHelp() [][]key.Binding { +func (k *menuKeyMap) FullHelp() [][]key.Binding { return [][]key.Binding{ { k.Exit, diff --git a/internal/tui/models/menu/model.go b/internal/tui/views/menu_model.go similarity index 87% rename from internal/tui/models/menu/model.go rename to internal/tui/views/menu_model.go index 5eecd81..0b781d0 100644 --- a/internal/tui/models/menu/model.go +++ b/internal/tui/views/menu_model.go @@ -1,4 +1,4 @@ -package menu +package views import ( "fmt" @@ -20,24 +20,24 @@ const titleStr = ` /_/ /_____/ /_/ /_/ |_/___/\____/\____/ ` -var _ tea.Model = Model{} +var _ tea.Model = MenuModel{} -type Model struct { - items []item +type MenuModel struct { + items []menuItem selected int - keys *keyMap - styles *styles + keys *menuKeyMap + styles *menuStyles help help.Model } -type item struct { +type menuItem struct { label string model tea.Model hideLabel bool } -func NewModel(_ *tui.MenuInput) *Model { +func NewMenuModel(_ *tui.MenuInput) *MenuModel { nameInput := textinput.NewModel("Enter your name", 20, 20) modePicker := hpicker.NewModel([]hpicker.KeyValuePair{ {Key: "Marathon", Value: tui.ModeMarathon}, @@ -46,25 +46,25 @@ func NewModel(_ *tui.MenuInput) *Model { }) levelPicker := hpicker.NewModel(nil, hpicker.WithRange(1, 15)) - return &Model{ - items: []item{ + return &MenuModel{ + items: []menuItem{ {label: "Name", model: nameInput, hideLabel: true}, {label: "Mode", model: modePicker}, {label: "Starting Level", model: levelPicker}, }, selected: 0, - keys: defaultKeyMap(), - styles: defaultStyles(), + keys: defaultMenuKeyMap(), + styles: defaultMenuStyles(), help: help.New(), } } -func (m Model) Init() tea.Cmd { +func (m MenuModel) Init() tea.Cmd { return nil } -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m MenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(msg, m.keys.Exit): @@ -97,7 +97,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m Model) View() string { +func (m MenuModel) View() string { items := make([]string, len(m.items)) for i := range m.items { items[i] = m.renderItem(i) + "\n" @@ -115,7 +115,7 @@ func (m Model) View() string { return output } -func (m Model) renderItem(index int) string { +func (m MenuModel) renderItem(index int) string { i := m.items[index] output := i.model.View() if !i.hideLabel { @@ -131,7 +131,7 @@ func (m Model) renderItem(index int) string { return m.styles.settingUnselected.Render(output) } -func (m Model) startGame() (tea.Cmd, error) { +func (m MenuModel) startGame() (tea.Cmd, error) { var level int var mode tui.Mode var playerName string diff --git a/internal/tui/models/menu/styles.go b/internal/tui/views/menu_styles.go similarity index 73% rename from internal/tui/models/menu/styles.go rename to internal/tui/views/menu_styles.go index 84696b4..1970d89 100644 --- a/internal/tui/models/menu/styles.go +++ b/internal/tui/views/menu_styles.go @@ -1,14 +1,14 @@ -package menu +package views import "github.com/charmbracelet/lipgloss" -type styles struct { +type menuStyles struct { settingSelected lipgloss.Style settingUnselected lipgloss.Style } -func defaultStyles() *styles { - s := styles{ +func defaultMenuStyles() *menuStyles { + s := menuStyles{ settingSelected: lipgloss.NewStyle().Padding(0, 2), } s.settingUnselected = s.settingSelected.Copy().Foreground(lipgloss.Color("241")) diff --git a/internal/tui/models/single/model.go b/internal/tui/views/single_model.go similarity index 86% rename from internal/tui/models/single/model.go rename to internal/tui/views/single_model.go index 9ef593f..d3b8986 100644 --- a/internal/tui/models/single/model.go +++ b/internal/tui/views/single_model.go @@ -1,4 +1,4 @@ -package single +package views import ( "fmt" @@ -8,7 +8,6 @@ import ( "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/data" "github.com/Broderick-Westrope/tetrigo/internal/tui" - game2 "github.com/Broderick-Westrope/tetrigo/internal/tui/models/game" "github.com/Broderick-Westrope/tetrigo/pkg/tetris" "github.com/Broderick-Westrope/tetrigo/pkg/tetris/modes/single" "github.com/charmbracelet/bubbles/help" @@ -23,9 +22,9 @@ const ( timerUpdateInterval = time.Millisecond * 13 ) -var _ tea.Model = &Model{} +var _ tea.Model = &SingleModel{} -type Model struct { +type SingleModel struct { playerName string game *single.Game nextQueueLength int @@ -36,19 +35,19 @@ type Model struct { gameTimer timer.Model gameStopwatch stopwatch.Model - styles *game2.Styles + styles *GameStyles help help.Model - keys *game2.KeyMap + keys *GameKeyMap isPaused bool } -func NewModel(in *tui.SingleInput, cfg *config.Config) (*Model, error) { +func NewSingleModel(in *tui.SingleInput, cfg *config.Config) (*SingleModel, error) { // Setup initial model - m := &Model{ + m := &SingleModel{ playerName: in.PlayerName, - styles: game2.CreateStyles(cfg.Theme), + styles: CreateGameStyles(cfg.Theme), help: help.New(), - keys: game2.ConstructKeyMap(cfg.Keys), + keys: ConstructGameKeyMap(cfg.Keys), isPaused: false, nextQueueLength: cfg.NextQueueLength, mode: in.Mode, @@ -105,7 +104,7 @@ func NewModel(in *tui.SingleInput, cfg *config.Config) (*Model, error) { return m, nil } -func (m *Model) Init() tea.Cmd { +func (m *SingleModel) Init() tea.Cmd { var cmd tea.Cmd switch m.useTimer { case true: @@ -117,7 +116,7 @@ func (m *Model) Init() tea.Cmd { return tea.Batch(m.fallStopwatch.Init(), cmd) } -func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *SingleModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd @@ -159,7 +158,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *Model) dependenciesUpdate(msg tea.Msg) (*Model, tea.Cmd) { +func (m *SingleModel) dependenciesUpdate(msg tea.Msg) (*SingleModel, tea.Cmd) { var cmd tea.Cmd var cmds []tea.Cmd @@ -177,7 +176,7 @@ func (m *Model) dependenciesUpdate(msg tea.Msg) (*Model, tea.Cmd) { return m, tea.Batch(cmds...) } -func (m *Model) gameOverUpdate(msg tea.Msg) (*Model, tea.Cmd) { +func (m *SingleModel) gameOverUpdate(msg tea.Msg) (*SingleModel, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { if key.Matches(msg, m.keys.Exit, m.keys.Hold) { modeStr := m.mode.String() @@ -198,7 +197,7 @@ func (m *Model) gameOverUpdate(msg tea.Msg) (*Model, tea.Cmd) { return m, nil } -func (m *Model) pausedUpdate(msg tea.Msg) (*Model, tea.Cmd) { +func (m *SingleModel) pausedUpdate(msg tea.Msg) (*SingleModel, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(msg, m.keys.Exit): @@ -211,7 +210,7 @@ func (m *Model) pausedUpdate(msg tea.Msg) (*Model, tea.Cmd) { return m, nil } -func (m *Model) playingUpdate(msg tea.Msg) (*Model, tea.Cmd) { +func (m *SingleModel) playingUpdate(msg tea.Msg) (*SingleModel, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: return m.playingKeyMsgUpdate(msg) @@ -239,7 +238,7 @@ func (m *Model) playingUpdate(msg tea.Msg) (*Model, tea.Cmd) { return m, nil } -func (m *Model) playingKeyMsgUpdate(msg tea.KeyMsg) (*Model, tea.Cmd) { +func (m *SingleModel) playingKeyMsgUpdate(msg tea.KeyMsg) (*SingleModel, tea.Cmd) { switch { case key.Matches(msg, m.keys.Left): m.game.MoveLeft() @@ -292,17 +291,24 @@ func (m *Model) playingKeyMsgUpdate(msg tea.KeyMsg) (*Model, tea.Cmd) { return m, nil } -func (m *Model) View() string { +func (m *SingleModel) View() string { var output = lipgloss.JoinHorizontal(lipgloss.Top, lipgloss.JoinVertical(lipgloss.Right, m.holdView(), m.informationView()), m.matrixView(), m.bagView(), ) + var err error if m.game.IsGameOver() { - output = tui.OverlayGameOverMessage(output) + output, err = tui.OverlayCenter(output, tui.GameOverMessage, true) + if err != nil { + return "** FAILED TO OVERLAY GAME OVER MESSAGE **" + output + } } else if m.isPaused { - output = tui.OverlayPausedMessage(output) + output, err = tui.OverlayCenter(output, tui.PausedMessage, true) + if err != nil { + return "** FAILED TO OVERLAY PAUSED MESSAGE **" + output + } } output = lipgloss.JoinVertical(lipgloss.Left, output, m.help.View(m.keys)) @@ -310,7 +316,7 @@ func (m *Model) View() string { return output } -func (m *Model) matrixView() string { +func (m *SingleModel) matrixView() string { matrix, err := m.game.GetVisibleMatrix() if err != nil { panic(err) @@ -336,7 +342,7 @@ func (m *Model) matrixView() string { ) } -func (m *Model) informationView() string { +func (m *SingleModel) informationView() string { width := m.styles.Information.GetWidth() var header string @@ -384,14 +390,14 @@ func (m *Model) informationView() string { return m.styles.Information.Render(lipgloss.JoinVertical(lipgloss.Left, header, output)) } -func (m *Model) holdView() string { +func (m *SingleModel) holdView() string { label := m.styles.Hold.Label.Render("Hold:") item := m.styles.Hold.Item.Render(m.renderTetrimino(m.game.GetHoldTetrimino(), 1)) output := lipgloss.JoinVertical(lipgloss.Top, label, item) return m.styles.Hold.View.Render(output) } -func (m *Model) bagView() string { +func (m *SingleModel) bagView() string { output := "Next:\n" for i, t := range m.game.GetBagTetriminos() { if i >= m.nextQueueLength { @@ -402,7 +408,7 @@ func (m *Model) bagView() string { return m.styles.Bag.Render(output) } -func (m *Model) renderTetrimino(t *tetris.Tetrimino, background byte) string { +func (m *SingleModel) renderTetrimino(t *tetris.Tetrimino, background byte) string { var output string for row := range t.Minos { for col := range t.Minos[row] { @@ -417,7 +423,7 @@ func (m *Model) renderTetrimino(t *tetris.Tetrimino, background byte) string { return output } -func (m *Model) renderCell(cell byte) string { +func (m *SingleModel) renderCell(cell byte) string { switch cell { case 0: return m.styles.EmptyCell.Render(m.styles.CellChar.Empty) @@ -434,7 +440,7 @@ func (m *Model) renderCell(cell byte) string { return "??" } -func (m *Model) triggerGameOver() tea.Cmd { +func (m *SingleModel) triggerGameOver() tea.Cmd { m.game.EndGame() m.isPaused = false @@ -448,7 +454,7 @@ func (m *Model) triggerGameOver() tea.Cmd { return tea.Batch(cmds...) } -func (m *Model) togglePause() tea.Cmd { +func (m *SingleModel) togglePause() tea.Cmd { m.isPaused = !m.isPaused return tea.Batch( m.fallStopwatch.Toggle(), From c50e1f4dcd0ee75af869741c835bee94956b1e5b Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:31:22 +1100 Subject: [PATCH 3/8] refactor: merge input and mode files --- internal/tui/input.go | 65 ------------------------------------------- internal/tui/mode.go | 53 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 65 deletions(-) diff --git a/internal/tui/input.go b/internal/tui/input.go index 607e3d7..c874d24 100644 --- a/internal/tui/input.go +++ b/internal/tui/input.go @@ -1,67 +1,2 @@ package tui -import "github.com/Broderick-Westrope/tetrigo/internal/data" - -type MarathonInput struct { - Level int - PlayerName string -} - -func NewMarathonInput(level int, playerName string) *MarathonInput { - return &MarathonInput{ - Level: level, - PlayerName: playerName, - } -} - -func (in *MarathonInput) isSwitchModeInput() {} - -type SingleInput struct { - Mode Mode - Level int - PlayerName string -} - -func NewSingleInput(mode Mode, level int, playerName string) *SingleInput { - return &SingleInput{ - Mode: mode, - Level: level, - PlayerName: playerName, - } -} - -func (in *SingleInput) isSwitchModeInput() {} - -type MenuInput struct { -} - -func NewMenuInput() *MenuInput { - return &MenuInput{} -} - -func (in *MenuInput) isSwitchModeInput() {} - -type LeaderboardInput struct { - GameMode string - NewEntry *data.Score -} - -func NewLeaderboardInput(gameMode string, opts ...func(input *LeaderboardInput)) *LeaderboardInput { - in := &LeaderboardInput{ - GameMode: gameMode, - } - - for _, opt := range opts { - opt(in) - } - - return in -} - -func (in *LeaderboardInput) isSwitchModeInput() {} - -func WithNewEntry(entry *data.Score) func(input *LeaderboardInput) { - return func(in *LeaderboardInput) { - in.NewEntry = entry - } -} diff --git a/internal/tui/mode.go b/internal/tui/mode.go index c03e484..d3dc98f 100644 --- a/internal/tui/mode.go +++ b/internal/tui/mode.go @@ -1,6 +1,7 @@ package tui import ( + "github.com/Broderick-Westrope/tetrigo/internal/data" tea "github.com/charmbracelet/bubbletea" ) @@ -43,3 +44,55 @@ var modeToStrMap = map[Mode]string{ func (m Mode) String() string { return modeToStrMap[m] } + +// SwitchModeInput values -------------------------------------------------- + +type SingleInput struct { + Mode Mode + Level int + PlayerName string +} + +func NewSingleInput(mode Mode, level int, playerName string) *SingleInput { + return &SingleInput{ + Mode: mode, + Level: level, + PlayerName: playerName, + } +} + +func (in *SingleInput) isSwitchModeInput() {} + +type MenuInput struct { +} + +func NewMenuInput() *MenuInput { + return &MenuInput{} +} + +func (in *MenuInput) isSwitchModeInput() {} + +type LeaderboardInput struct { + GameMode string + NewEntry *data.Score +} + +func NewLeaderboardInput(gameMode string, opts ...func(input *LeaderboardInput)) *LeaderboardInput { + in := &LeaderboardInput{ + GameMode: gameMode, + } + + for _, opt := range opts { + opt(in) + } + + return in +} + +func (in *LeaderboardInput) isSwitchModeInput() {} + +func WithNewEntry(entry *data.Score) func(input *LeaderboardInput) { + return func(in *LeaderboardInput) { + in.NewEntry = entry + } +} From da24a468ed632e73aa0b7468a7640643e698cac6 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:36:39 +1100 Subject: [PATCH 4/8] refactor: rename components types --- .../hpicker/{model.go => hpicker.go} | 38 +++++++++---------- .../hpicker/{keymap.go => hpicker_keys.go} | 8 ++-- .../hpicker/{styles.go => hpicker_styles.go} | 6 +-- .../tui/components/textinput/textinput.go | 12 +++--- .../{leaderboard_model.go => leaderboard.go} | 0 internal/tui/views/{menu_model.go => menu.go} | 12 +++--- .../tui/views/{single_model.go => single.go} | 0 7 files changed, 38 insertions(+), 38 deletions(-) rename internal/tui/components/hpicker/{model.go => hpicker.go} (72%) rename internal/tui/components/hpicker/{keymap.go => hpicker_keys.go} (62%) rename internal/tui/components/hpicker/{styles.go => hpicker_styles.go} (86%) rename internal/tui/views/{leaderboard_model.go => leaderboard.go} (100%) rename internal/tui/views/{menu_model.go => menu.go} (91%) rename internal/tui/views/{single_model.go => single.go} (100%) diff --git a/internal/tui/components/hpicker/model.go b/internal/tui/components/hpicker/hpicker.go similarity index 72% rename from internal/tui/components/hpicker/model.go rename to internal/tui/components/hpicker/hpicker.go index b663ec9..063fa97 100644 --- a/internal/tui/components/hpicker/model.go +++ b/internal/tui/components/hpicker/hpicker.go @@ -8,17 +8,17 @@ import ( "github.com/charmbracelet/lipgloss" ) -var _ tea.Model = &Model{} +var _ tea.Model = &HPickerModel{} -// Model is the model for the horizontal picker component. -type Model struct { +// HPickerModel is the model for the horizontal picker component. +type HPickerModel struct { // cursor is the index of the currently selected option. selected int // options is a list of the possible options for this component. options []KeyValuePair // keymap encodes the keybindings recognized by the component. - keymap *KeyMap - styles Styles + keymap *hPickerKeyMap + styles hPickerStyles } type KeyValuePair struct { @@ -26,13 +26,13 @@ type KeyValuePair struct { Value any } -type Option func(*Model) +type Option func(*HPickerModel) -func NewModel(options []KeyValuePair, opts ...Option) *Model { - m := &Model{ +func NewHPickerModel(options []KeyValuePair, opts ...Option) *HPickerModel { + m := &HPickerModel{ options: options, - keymap: defaultKeyMap(), - styles: DefaultStyles(), + keymap: defaultHPickerKeyMap(), + styles: defaultHPickerStyles(), } for _, opt := range opts { @@ -43,7 +43,7 @@ func NewModel(options []KeyValuePair, opts ...Option) *Model { } func WithRange(minValue, maxValue int) Option { - return func(m *Model) { + return func(m *HPickerModel) { m.options = make([]KeyValuePair, (maxValue-minValue)+1) for i := minValue - 1; i < maxValue; i++ { m.options[i].Key = strconv.Itoa(i + 1) @@ -52,12 +52,12 @@ func WithRange(minValue, maxValue int) Option { } } -func (m *Model) Init() tea.Cmd { +func (m *HPickerModel) Init() tea.Cmd { return nil } // Update is the Tea update function which binds keystrokes to pagination. -func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m *HPickerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { if msg, ok := msg.(tea.KeyMsg); ok { switch { case key.Matches(msg, m.keymap.Next): @@ -71,7 +71,7 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { } // View renders the cursor to a string. -func (m *Model) View() string { +func (m *HPickerModel) View() string { var prev lipgloss.Style if m.isFirst() { prev = m.styles.PrevDisabledStyle @@ -95,7 +95,7 @@ func (m *Model) View() string { // Prev is a helper function for navigating one option backward. // It will not go page beyond the first option (i.e. option 0). -func (m *Model) Prev() { +func (m *HPickerModel) Prev() { if !m.isFirst() { m.selected-- } @@ -103,20 +103,20 @@ func (m *Model) Prev() { // Next is a helper function for navigating one option forward. // It will not go beyond the last option (i.e. len(options) - 1). -func (m *Model) Next() { +func (m *HPickerModel) Next() { if !m.isLast() { m.selected++ } } -func (m *Model) isFirst() bool { +func (m *HPickerModel) isFirst() bool { return m.selected == 0 } -func (m *Model) isLast() bool { +func (m *HPickerModel) isLast() bool { return m.selected == len(m.options)-1 } -func (m *Model) GetSelection() KeyValuePair { +func (m *HPickerModel) GetSelection() KeyValuePair { return m.options[m.selected] } diff --git a/internal/tui/components/hpicker/keymap.go b/internal/tui/components/hpicker/hpicker_keys.go similarity index 62% rename from internal/tui/components/hpicker/keymap.go rename to internal/tui/components/hpicker/hpicker_keys.go index 156ae72..678d652 100644 --- a/internal/tui/components/hpicker/keymap.go +++ b/internal/tui/components/hpicker/hpicker_keys.go @@ -4,14 +4,14 @@ import ( "github.com/charmbracelet/bubbles/key" ) -// KeyMap is the key bindings for different actions within the component. -type KeyMap struct { +// hPickerKeyMap is the key bindings for different actions within the component. +type hPickerKeyMap struct { Prev key.Binding Next key.Binding } -func defaultKeyMap() *KeyMap { - return &KeyMap{ +func defaultHPickerKeyMap() *hPickerKeyMap { + return &hPickerKeyMap{ Prev: key.NewBinding( key.WithKeys("left"), key.WithHelp("<-", "move left"), diff --git a/internal/tui/components/hpicker/styles.go b/internal/tui/components/hpicker/hpicker_styles.go similarity index 86% rename from internal/tui/components/hpicker/styles.go rename to internal/tui/components/hpicker/hpicker_styles.go index 7ba47e4..95d24af 100644 --- a/internal/tui/components/hpicker/styles.go +++ b/internal/tui/components/hpicker/hpicker_styles.go @@ -2,7 +2,7 @@ package hpicker import "github.com/charmbracelet/lipgloss" -type Styles struct { +type hPickerStyles struct { NextIndicator string NextStyle lipgloss.Style NextDisabledStyle lipgloss.Style @@ -14,8 +14,8 @@ type Styles struct { SelectionStyle lipgloss.Style } -func DefaultStyles() Styles { - return Styles{ +func defaultHPickerStyles() hPickerStyles { + return hPickerStyles{ NextIndicator: " >", NextStyle: lipgloss.NewStyle(), NextDisabledStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("241")), diff --git a/internal/tui/components/textinput/textinput.go b/internal/tui/components/textinput/textinput.go index 5e6f4c1..3d0ea8a 100644 --- a/internal/tui/components/textinput/textinput.go +++ b/internal/tui/components/textinput/textinput.go @@ -5,32 +5,32 @@ import ( tea "github.com/charmbracelet/bubbletea" ) -type Model struct { +type TextInputModel struct { Child textinput.Model } -func NewModel(placeholder string, width int, charLimit int) Model { +func NewTextInputModel(placeholder string, width int, charLimit int) TextInputModel { c := textinput.New() c.Placeholder = placeholder c.Width = width c.CharLimit = charLimit c.Focus() - return Model{ + return TextInputModel{ Child: c, } } -func (m Model) Init() tea.Cmd { +func (m TextInputModel) Init() tea.Cmd { return nil } -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m TextInputModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { var cmd tea.Cmd m.Child, cmd = m.Child.Update(msg) return m, cmd } -func (m Model) View() string { +func (m TextInputModel) View() string { return m.Child.View() } diff --git a/internal/tui/views/leaderboard_model.go b/internal/tui/views/leaderboard.go similarity index 100% rename from internal/tui/views/leaderboard_model.go rename to internal/tui/views/leaderboard.go diff --git a/internal/tui/views/menu_model.go b/internal/tui/views/menu.go similarity index 91% rename from internal/tui/views/menu_model.go rename to internal/tui/views/menu.go index 0b781d0..d206c66 100644 --- a/internal/tui/views/menu_model.go +++ b/internal/tui/views/menu.go @@ -38,13 +38,13 @@ type menuItem struct { } func NewMenuModel(_ *tui.MenuInput) *MenuModel { - nameInput := textinput.NewModel("Enter your name", 20, 20) - modePicker := hpicker.NewModel([]hpicker.KeyValuePair{ + nameInput := textinput.NewTextInputModel("Enter your name", 20, 20) + modePicker := hpicker.NewHPickerModel([]hpicker.KeyValuePair{ {Key: "Marathon", Value: tui.ModeMarathon}, {Key: "Sprint (40 Lines)", Value: tui.ModeSprint}, {Key: "Ultra (Time Trial)", Value: tui.ModeUltra}, }) - levelPicker := hpicker.NewModel(nil, hpicker.WithRange(1, 15)) + levelPicker := hpicker.NewHPickerModel(nil, hpicker.WithRange(1, 15)) return &MenuModel{ items: []menuItem{ @@ -142,7 +142,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { for _, i := range m.items { switch i.label { case "Starting Level": - picker, ok := i.model.(*hpicker.Model) + picker, ok := i.model.(*hpicker.HPickerModel) if !ok { return nil, errInvalidModel } @@ -151,7 +151,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { return nil, errInvalidValue } case "Mode": - picker, ok := i.model.(*hpicker.Model) + picker, ok := i.model.(*hpicker.HPickerModel) if !ok { return nil, errInvalidModel } @@ -160,7 +160,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { return nil, errInvalidValue } case "Name": - playerName = i.model.(textinput.Model).Child.Value() + playerName = i.model.(textinput.TextInputModel).Child.Value() default: return nil, fmt.Errorf("invalid item label: %q", i.label) } diff --git a/internal/tui/views/single_model.go b/internal/tui/views/single.go similarity index 100% rename from internal/tui/views/single_model.go rename to internal/tui/views/single.go From 9dbd85c6f0f3fe8650efbe976a57a42986b44cc1 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:37:14 +1100 Subject: [PATCH 5/8] refactor: move types to components package --- internal/tui/components/{hpicker => }/hpicker.go | 2 +- internal/tui/components/{hpicker => }/hpicker_keys.go | 2 +- .../tui/components/{hpicker => }/hpicker_styles.go | 2 +- internal/tui/components/{textinput => }/textinput.go | 2 +- internal/tui/views/menu.go | 10 +++++----- 5 files changed, 9 insertions(+), 9 deletions(-) rename internal/tui/components/{hpicker => }/hpicker.go (99%) rename internal/tui/components/{hpicker => }/hpicker_keys.go (96%) rename internal/tui/components/{hpicker => }/hpicker_styles.go (97%) rename internal/tui/components/{textinput => }/textinput.go (97%) diff --git a/internal/tui/components/hpicker/hpicker.go b/internal/tui/components/hpicker.go similarity index 99% rename from internal/tui/components/hpicker/hpicker.go rename to internal/tui/components/hpicker.go index 063fa97..117a370 100644 --- a/internal/tui/components/hpicker/hpicker.go +++ b/internal/tui/components/hpicker.go @@ -1,4 +1,4 @@ -package hpicker +package components import ( "strconv" diff --git a/internal/tui/components/hpicker/hpicker_keys.go b/internal/tui/components/hpicker_keys.go similarity index 96% rename from internal/tui/components/hpicker/hpicker_keys.go rename to internal/tui/components/hpicker_keys.go index 678d652..8027ca2 100644 --- a/internal/tui/components/hpicker/hpicker_keys.go +++ b/internal/tui/components/hpicker_keys.go @@ -1,4 +1,4 @@ -package hpicker +package components import ( "github.com/charmbracelet/bubbles/key" diff --git a/internal/tui/components/hpicker/hpicker_styles.go b/internal/tui/components/hpicker_styles.go similarity index 97% rename from internal/tui/components/hpicker/hpicker_styles.go rename to internal/tui/components/hpicker_styles.go index 95d24af..54bb8cb 100644 --- a/internal/tui/components/hpicker/hpicker_styles.go +++ b/internal/tui/components/hpicker_styles.go @@ -1,4 +1,4 @@ -package hpicker +package components import "github.com/charmbracelet/lipgloss" diff --git a/internal/tui/components/textinput/textinput.go b/internal/tui/components/textinput.go similarity index 97% rename from internal/tui/components/textinput/textinput.go rename to internal/tui/components/textinput.go index 3d0ea8a..6fc6783 100644 --- a/internal/tui/components/textinput/textinput.go +++ b/internal/tui/components/textinput.go @@ -1,4 +1,4 @@ -package textinput +package components import ( "github.com/charmbracelet/bubbles/textinput" diff --git a/internal/tui/views/menu.go b/internal/tui/views/menu.go index d206c66..b7c4dda 100644 --- a/internal/tui/views/menu.go +++ b/internal/tui/views/menu.go @@ -2,9 +2,9 @@ package views import ( "fmt" + "github.com/Broderick-Westrope/tetrigo/internal/tui/components" "github.com/Broderick-Westrope/tetrigo/internal/tui" - "github.com/Broderick-Westrope/tetrigo/internal/tui/components/hpicker" "github.com/Broderick-Westrope/tetrigo/internal/tui/components/textinput" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" @@ -39,12 +39,12 @@ type menuItem struct { func NewMenuModel(_ *tui.MenuInput) *MenuModel { nameInput := textinput.NewTextInputModel("Enter your name", 20, 20) - modePicker := hpicker.NewHPickerModel([]hpicker.KeyValuePair{ + modePicker := components.NewHPickerModel([]components.KeyValuePair{ {Key: "Marathon", Value: tui.ModeMarathon}, {Key: "Sprint (40 Lines)", Value: tui.ModeSprint}, {Key: "Ultra (Time Trial)", Value: tui.ModeUltra}, }) - levelPicker := hpicker.NewHPickerModel(nil, hpicker.WithRange(1, 15)) + levelPicker := components.NewHPickerModel(nil, components.WithRange(1, 15)) return &MenuModel{ items: []menuItem{ @@ -142,7 +142,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { for _, i := range m.items { switch i.label { case "Starting Level": - picker, ok := i.model.(*hpicker.HPickerModel) + picker, ok := i.model.(*components.HPickerModel) if !ok { return nil, errInvalidModel } @@ -151,7 +151,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { return nil, errInvalidValue } case "Mode": - picker, ok := i.model.(*hpicker.HPickerModel) + picker, ok := i.model.(*components.HPickerModel) if !ok { return nil, errInvalidModel } From 7a0bd726985b42e7f8ca17bb454b47748a040382 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:38:27 +1100 Subject: [PATCH 6/8] refactor: move game files to components package --- internal/tui/{views => components}/game_keys.go | 2 +- internal/tui/{views => components}/game_styles.go | 2 +- internal/tui/input.go | 2 -- internal/tui/views/single.go | 9 +++++---- 4 files changed, 7 insertions(+), 8 deletions(-) rename internal/tui/{views => components}/game_keys.go (98%) rename internal/tui/{views => components}/game_styles.go (99%) delete mode 100644 internal/tui/input.go diff --git a/internal/tui/views/game_keys.go b/internal/tui/components/game_keys.go similarity index 98% rename from internal/tui/views/game_keys.go rename to internal/tui/components/game_keys.go index 3a317c3..79daee3 100644 --- a/internal/tui/views/game_keys.go +++ b/internal/tui/components/game_keys.go @@ -1,4 +1,4 @@ -package views +package components import ( "github.com/Broderick-Westrope/tetrigo/internal/config" diff --git a/internal/tui/views/game_styles.go b/internal/tui/components/game_styles.go similarity index 99% rename from internal/tui/views/game_styles.go rename to internal/tui/components/game_styles.go index 1abf7f8..45fdc40 100644 --- a/internal/tui/views/game_styles.go +++ b/internal/tui/components/game_styles.go @@ -1,4 +1,4 @@ -package views +package components import ( "github.com/Broderick-Westrope/tetrigo/internal/config" diff --git a/internal/tui/input.go b/internal/tui/input.go deleted file mode 100644 index c874d24..0000000 --- a/internal/tui/input.go +++ /dev/null @@ -1,2 +0,0 @@ -package tui - diff --git a/internal/tui/views/single.go b/internal/tui/views/single.go index d3b8986..136e990 100644 --- a/internal/tui/views/single.go +++ b/internal/tui/views/single.go @@ -2,6 +2,7 @@ package views import ( "fmt" + "github.com/Broderick-Westrope/tetrigo/internal/tui/components" "strconv" "time" @@ -35,9 +36,9 @@ type SingleModel struct { gameTimer timer.Model gameStopwatch stopwatch.Model - styles *GameStyles + styles *components.GameStyles help help.Model - keys *GameKeyMap + keys *components.GameKeyMap isPaused bool } @@ -45,9 +46,9 @@ func NewSingleModel(in *tui.SingleInput, cfg *config.Config) (*SingleModel, erro // Setup initial model m := &SingleModel{ playerName: in.PlayerName, - styles: CreateGameStyles(cfg.Theme), + styles: components.CreateGameStyles(cfg.Theme), help: help.New(), - keys: ConstructGameKeyMap(cfg.Keys), + keys: components.ConstructGameKeyMap(cfg.Keys), isPaused: false, nextQueueLength: cfg.NextQueueLength, mode: in.Mode, From 6ef91c4cf2256b108246165bb31a3f5840a4ae80 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 16:53:35 +1100 Subject: [PATCH 7/8] fix: build errors --- internal/tui/views/menu.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/tui/views/menu.go b/internal/tui/views/menu.go index b7c4dda..2648704 100644 --- a/internal/tui/views/menu.go +++ b/internal/tui/views/menu.go @@ -5,7 +5,6 @@ import ( "github.com/Broderick-Westrope/tetrigo/internal/tui/components" "github.com/Broderick-Westrope/tetrigo/internal/tui" - "github.com/Broderick-Westrope/tetrigo/internal/tui/components/textinput" "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -38,7 +37,7 @@ type menuItem struct { } func NewMenuModel(_ *tui.MenuInput) *MenuModel { - nameInput := textinput.NewTextInputModel("Enter your name", 20, 20) + nameInput := components.NewTextInputModel("Enter your name", 20, 20) modePicker := components.NewHPickerModel([]components.KeyValuePair{ {Key: "Marathon", Value: tui.ModeMarathon}, {Key: "Sprint (40 Lines)", Value: tui.ModeSprint}, @@ -160,7 +159,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { return nil, errInvalidValue } case "Name": - playerName = i.model.(textinput.TextInputModel).Child.Value() + playerName = i.model.(components.TextInputModel).Child.Value() default: return nil, fmt.Errorf("invalid item label: %q", i.label) } From 97a4b0f1d38c12872daaa199e213df81c1471a73 Mon Sep 17 00:00:00 2001 From: Broderick Westrope Date: Thu, 14 Nov 2024 17:00:43 +1100 Subject: [PATCH 8/8] chore: fix lint errors --- cmd/tetrigo/subcommands.go | 5 +++-- internal/tui/starter/model.go | 2 +- internal/tui/utils.go | 8 +++++--- internal/tui/views/menu.go | 12 ++++++++++-- internal/tui/views/single.go | 3 ++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/cmd/tetrigo/subcommands.go b/cmd/tetrigo/subcommands.go index 35de244..1e5778c 100644 --- a/cmd/tetrigo/subcommands.go +++ b/cmd/tetrigo/subcommands.go @@ -2,12 +2,13 @@ package main import ( "fmt" - "github.com/Broderick-Westrope/tetrigo/internal/tui/starter" + + tea "github.com/charmbracelet/bubbletea" "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/data" "github.com/Broderick-Westrope/tetrigo/internal/tui" - tea "github.com/charmbracelet/bubbletea" + "github.com/Broderick-Westrope/tetrigo/internal/tui/starter" ) type MenuCmd struct{} diff --git a/internal/tui/starter/model.go b/internal/tui/starter/model.go index 4d9d682..a997249 100644 --- a/internal/tui/starter/model.go +++ b/internal/tui/starter/model.go @@ -3,11 +3,11 @@ package starter import ( "database/sql" "errors" - "github.com/Broderick-Westrope/tetrigo/internal/tui/views" "reflect" "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/tui" + "github.com/Broderick-Westrope/tetrigo/internal/tui/views" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" ) diff --git a/internal/tui/utils.go b/internal/tui/utils.go index 470783a..bd4a0d9 100644 --- a/internal/tui/utils.go +++ b/internal/tui/utils.go @@ -1,11 +1,12 @@ package tui import ( - "errors" "github.com/charmbracelet/bubbles/key" "github.com/charmbracelet/lipgloss" "github.com/charmbracelet/x/ansi" "github.com/mattn/go-runewidth" + + "errors" "regexp" "strings" "unicode" @@ -43,7 +44,7 @@ func ConstructKeyBinding(keys []string, desc string) key.Binding { return key.NewBinding(key.WithKeys(keys...), key.WithHelp(buildHelpKeys(keys), desc)) } -// Window overlay (CREDIT: https://gist.github.com/ras0q/9bf5d81544b22302393f61206892e2cd) ------------------------------------ +// Window overlay (CREDIT: https://gist.github.com/ras0q/9bf5d81544b22302393f61206892e2cd) // OverlayCenter writes the overlay string onto the background string such that the middle of the // overlay string will be at the middle of the overlay will be at the middle of the background. @@ -95,7 +96,8 @@ func Overlay(bg, overlay string, row, col int, ignoreMarginWhitespace bool) (str } // removeMarginWhitespace preserves the background where the overlay line has leading or trailing whitespace. -// This is done by detecting those empty cells in the overlay string and replacing them with the corresponding background cells. +// This is done by detecting those empty cells in the overlay string and +// replacing them with the corresponding background cells. // //nolint:gocognit func removeMarginWhitespace(bgLine, overlayLine string, col int) string { diff --git a/internal/tui/views/menu.go b/internal/tui/views/menu.go index 2648704..3d6612d 100644 --- a/internal/tui/views/menu.go +++ b/internal/tui/views/menu.go @@ -2,9 +2,10 @@ package views import ( "fmt" - "github.com/Broderick-Westrope/tetrigo/internal/tui/components" "github.com/Broderick-Westrope/tetrigo/internal/tui" + "github.com/Broderick-Westrope/tetrigo/internal/tui/components" + "github.com/charmbracelet/bubbles/help" "github.com/charmbracelet/bubbles/key" tea "github.com/charmbracelet/bubbletea" @@ -149,6 +150,7 @@ func (m MenuModel) startGame() (tea.Cmd, error) { if !ok { return nil, errInvalidValue } + case "Mode": picker, ok := i.model.(*components.HPickerModel) if !ok { @@ -158,8 +160,14 @@ func (m MenuModel) startGame() (tea.Cmd, error) { if !ok { return nil, errInvalidValue } + case "Name": - playerName = i.model.(components.TextInputModel).Child.Value() + input, ok := i.model.(components.TextInputModel) + if !ok { + return nil, errInvalidModel + } + playerName = input.Child.Value() + default: return nil, fmt.Errorf("invalid item label: %q", i.label) } diff --git a/internal/tui/views/single.go b/internal/tui/views/single.go index 136e990..ff7444c 100644 --- a/internal/tui/views/single.go +++ b/internal/tui/views/single.go @@ -2,10 +2,11 @@ package views import ( "fmt" - "github.com/Broderick-Westrope/tetrigo/internal/tui/components" "strconv" "time" + "github.com/Broderick-Westrope/tetrigo/internal/tui/components" + "github.com/Broderick-Westrope/tetrigo/internal/config" "github.com/Broderick-Westrope/tetrigo/internal/data" "github.com/Broderick-Westrope/tetrigo/internal/tui"