feat: add navigation and window size handlers with corresponding tests
This commit is contained in:
@@ -0,0 +1,41 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type downNavigationHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDownNavigationHandler() KeyMsgHandler {
|
||||||
|
return downNavigationHandler{
|
||||||
|
keys: []string{"down", "j"},
|
||||||
|
description: "Move down",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downNavigationHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downNavigationHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downNavigationHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h downNavigationHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
s.MoveDown()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewDownNavigationHandler(t *testing.T) {
|
||||||
|
h := handlers.NewDownNavigationHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Move down" {
|
||||||
|
t.Errorf("Expected description 'Move down', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
|
||||||
|
if !slices.Contains(keys, "down") || !slices.Contains(keys, "j") {
|
||||||
|
t.Errorf("Expected keys ['down', 'j'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownNavigationHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewDownNavigationHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches 'down' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "down",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches 'j' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "j",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'down' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "down",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'j' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "j",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "up",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if tt.key == "down" {
|
||||||
|
msg = tea.KeyMsg{Type: tea.KeyDown}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownNavigationHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewDownNavigationHandler()
|
||||||
|
|
||||||
|
fullTiers := make([]core.Tier, 6)
|
||||||
|
fullTiers[0] = core.Tier{Name: "S", Items: []core.Item{"1", "2", "3"}}
|
||||||
|
fullTiers[1] = core.Tier{Name: "A", Items: []core.Item{"4"}}
|
||||||
|
|
||||||
|
s := state.New(
|
||||||
|
state.WithTiers(fullTiers),
|
||||||
|
state.WithCursor(0, 2), // Start at the end of Row 0
|
||||||
|
state.WithMode(state.NavigationMode),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyDown}
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.CursorRow() != 1 {
|
||||||
|
t.Errorf("Expected cursor to move to Row 1, got %d", s.CursorRow())
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.CursorCol() != 0 {
|
||||||
|
t.Errorf("Expected cursor column to clamp to 0, got %d", s.CursorCol())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected Handle to return nil, got a command")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewHelpHandler(t *testing.T) {
|
||||||
|
h := handlers.NewHelpHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Toggle help menu" {
|
||||||
|
t.Errorf("Expected description 'Toggle help menu', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
|
||||||
|
if !slices.Contains(keys, "?") {
|
||||||
|
t.Errorf("Expected keys ['?'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewHelpHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches '?' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "?",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches '?' in Help Mode",
|
||||||
|
mode: state.HelpMode,
|
||||||
|
key: "?",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores '?' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "?",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "q",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHelpHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewHelpHandler()
|
||||||
|
|
||||||
|
t.Run("Toggles from Navigation to Help", func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(state.NavigationMode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}
|
||||||
|
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.Mode() != state.HelpMode {
|
||||||
|
t.Errorf("Expected mode to switch to HelpMode, got %v", s.Mode())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected nil command, got non-nil")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Toggles from Help to Navigation", func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(state.HelpMode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("?")}
|
||||||
|
|
||||||
|
h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
t.Errorf("Expected mode to switch to NavigationMode, got %v", s.Mode())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type helpHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHelpHandler() KeyMsgHandler {
|
||||||
|
return helpHandler{
|
||||||
|
keys: []string{"?"},
|
||||||
|
description: "Toggle help menu",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h helpHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h helpHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h helpHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
return s.Mode() == state.NavigationMode && slices.Contains(h.keys, msg.String()) ||
|
||||||
|
s.Mode() == state.HelpMode && slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h helpHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
s.ToggleHelp()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type KeyMsgHandler interface {
|
||||||
|
KeysHandled() []string
|
||||||
|
Description() string
|
||||||
|
Match(s state.State, msg tea.KeyMsg) bool
|
||||||
|
Handle(s state.State, msg tea.KeyMsg) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllKeyHandlers(saver core.Saver) []KeyMsgHandler {
|
||||||
|
return []KeyMsgHandler{
|
||||||
|
NewTextInputHandler(),
|
||||||
|
NewQuitHandler(),
|
||||||
|
NewUpNavigationHandler(),
|
||||||
|
NewDownNavigationHandler(),
|
||||||
|
NewLeftNavigationHandler(),
|
||||||
|
NewRightNavigationHandler(),
|
||||||
|
NewDeleteItemHandler(),
|
||||||
|
NewAddItemHandler(),
|
||||||
|
NewUpdateItemHandler(),
|
||||||
|
NewSelectItemHandler(),
|
||||||
|
NewLoadHandler(saver),
|
||||||
|
NewSaveHandler(saver),
|
||||||
|
NewHelpHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type leftNavigationHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLeftNavigationHandler() KeyMsgHandler {
|
||||||
|
return leftNavigationHandler{
|
||||||
|
keys: []string{"left", "h"},
|
||||||
|
description: "Move left",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h leftNavigationHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h leftNavigationHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h leftNavigationHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h leftNavigationHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
s.MoveLeft()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewLeftNavigationHandler(t *testing.T) {
|
||||||
|
h := handlers.NewLeftNavigationHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Move left" {
|
||||||
|
t.Errorf("Expected description 'Move left', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
|
||||||
|
if !slices.Contains(keys, "left") || !slices.Contains(keys, "h") {
|
||||||
|
t.Errorf("Expected keys ['left', 'h'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeftNavigationHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewLeftNavigationHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches 'left' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "left",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches 'h' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "h",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'left' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "left",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'h' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "h",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "right",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if tt.key == "left" {
|
||||||
|
msg = tea.KeyMsg{Type: tea.KeyLeft}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLeftNavigationHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewLeftNavigationHandler()
|
||||||
|
|
||||||
|
s := state.New(
|
||||||
|
state.WithCursor(0, 1),
|
||||||
|
state.WithMode(state.NavigationMode),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyLeft}
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.CursorCol() != 0 {
|
||||||
|
t.Errorf("Expected cursor to move to Col 0, got %d", s.CursorCol())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected Handle to return nil, got a command")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockSaver struct {
|
||||||
|
shouldError bool
|
||||||
|
dataToReturn core.SaveState
|
||||||
|
savedData core.SaveState
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSaver) Save(filename string, state core.SaveState) error {
|
||||||
|
if m.shouldError {
|
||||||
|
return errors.New("mock save error")
|
||||||
|
}
|
||||||
|
|
||||||
|
m.savedData = state
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockSaver) Load(filename string) (core.SaveState, error) {
|
||||||
|
if m.shouldError {
|
||||||
|
return core.SaveState{}, errors.New("mock load error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.dataToReturn, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type quitHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewQuitHandler() KeyMsgHandler {
|
||||||
|
return quitHandler{
|
||||||
|
keys: []string{"ctrl+c", "q"},
|
||||||
|
description: "Quit",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h quitHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h quitHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h quitHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h quitHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
return tea.Quit
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewQuitHandler(t *testing.T) {
|
||||||
|
h := handlers.NewQuitHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Quit" {
|
||||||
|
t.Errorf("Expected description 'Quit', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
|
||||||
|
if !slices.Contains(keys, "q") || !slices.Contains(keys, "ctrl+c") {
|
||||||
|
t.Errorf("Expected keys ['q', 'ctrl+c'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuitHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewQuitHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches 'q' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "q",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches 'ctrl+c' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "ctrl+c",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'q' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "q",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'ctrl+c' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "ctrl+c",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "x",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if tt.key == "ctrl+c" {
|
||||||
|
msg = tea.KeyMsg{Type: tea.KeyCtrlC}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQuitHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewQuitHandler()
|
||||||
|
s := state.New(state.WithMode(state.NavigationMode))
|
||||||
|
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("q")}
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if cmd == nil {
|
||||||
|
t.Fatal("Expected Handle to return a command, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdMsg := cmd()
|
||||||
|
|
||||||
|
if _, ok := cmdMsg.(tea.QuitMsg); !ok {
|
||||||
|
t.Errorf("Expected command to produce tea.QuitMsg, got %T", cmdMsg)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rightNavigationHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRightNavigationHandler() KeyMsgHandler {
|
||||||
|
return rightNavigationHandler{
|
||||||
|
keys: []string{"right", "l"},
|
||||||
|
description: "Move right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rightNavigationHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rightNavigationHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rightNavigationHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h rightNavigationHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
s.MoveRight()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/core"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRightNavigationHandler(t *testing.T) {
|
||||||
|
h := handlers.NewRightNavigationHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Move right" {
|
||||||
|
t.Errorf("Expected description 'Move right', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
|
||||||
|
if !slices.Contains(keys, "right") || !slices.Contains(keys, "l") {
|
||||||
|
t.Errorf("Expected keys ['right', 'l'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRightNavigationHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewRightNavigationHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches 'right' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "right",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches 'l' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "l",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'right' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "right",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'l' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "l",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "left",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if tt.key == "right" {
|
||||||
|
msg = tea.KeyMsg{Type: tea.KeyRight}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRightNavigationHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewRightNavigationHandler()
|
||||||
|
|
||||||
|
tiers := make([]core.Tier, 6)
|
||||||
|
tiers[0] = core.Tier{Name: "S", Items: []core.Item{"A", "B"}}
|
||||||
|
|
||||||
|
s := state.New(
|
||||||
|
state.WithTiers(tiers),
|
||||||
|
state.WithCursor(0, 0),
|
||||||
|
state.WithMode(state.NavigationMode),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRight}
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.CursorCol() != 1 {
|
||||||
|
t.Errorf("Expected cursor to move to Col 1, got %d", s.CursorCol())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected Handle to return nil, got a command")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type trackWindowSizeHandler struct{}
|
||||||
|
|
||||||
|
func NewTrackWindowSizeHandler() WinSizeMsgHandler {
|
||||||
|
return trackWindowSizeHandler{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h trackWindowSizeHandler) Match(s state.State, msg tea.WindowSizeMsg) bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h trackWindowSizeHandler) Handle(s state.State, msg tea.WindowSizeMsg) tea.Cmd {
|
||||||
|
s.SetWindowSize(msg.Width, msg.Height)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewTrackWindowSizeHandler(t *testing.T) {
|
||||||
|
h := handlers.NewTrackWindowSizeHandler()
|
||||||
|
|
||||||
|
if h == nil {
|
||||||
|
t.Error("Expected NewTrackWindowSizeHandler to return a handler, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackWindowSizeHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewTrackWindowSizeHandler()
|
||||||
|
s := state.New()
|
||||||
|
|
||||||
|
msg := tea.WindowSizeMsg{Width: 100, Height: 100}
|
||||||
|
|
||||||
|
if !h.Match(s, msg) {
|
||||||
|
t.Error("Expected Match to return true for WindowSizeMsg")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackWindowSizeHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewTrackWindowSizeHandler()
|
||||||
|
s := state.New()
|
||||||
|
|
||||||
|
expectedWidth := 1024
|
||||||
|
expectedHeight := 768
|
||||||
|
msg := tea.WindowSizeMsg{Width: expectedWidth, Height: expectedHeight}
|
||||||
|
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.WindowWidth() != expectedWidth {
|
||||||
|
t.Errorf("Expected WindowWidth to be %d, got %d", expectedWidth, s.WindowWidth())
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.WindowHeight() != expectedHeight {
|
||||||
|
t.Errorf("Expected WindowHeight to be %d, got %d", expectedHeight, s.WindowHeight())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected Handle to return nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type upNavigationHandler struct {
|
||||||
|
keys []string
|
||||||
|
description string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewUpNavigationHandler() KeyMsgHandler {
|
||||||
|
return upNavigationHandler{
|
||||||
|
keys: []string{"up", "k"},
|
||||||
|
description: "Move up",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h upNavigationHandler) KeysHandled() []string {
|
||||||
|
return h.keys
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h upNavigationHandler) Description() string {
|
||||||
|
return h.description
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h upNavigationHandler) Match(s state.State, msg tea.KeyMsg) bool {
|
||||||
|
if s.Mode() != state.NavigationMode {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return slices.Contains(h.keys, msg.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h upNavigationHandler) Handle(s state.State, msg tea.KeyMsg) tea.Cmd {
|
||||||
|
s.MoveUp()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package handlers_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/handlers"
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewUpNavigationHandler(t *testing.T) {
|
||||||
|
h := handlers.NewUpNavigationHandler()
|
||||||
|
|
||||||
|
if h.Description() != "Move up" {
|
||||||
|
t.Errorf("Expected description 'Move up', got %q", h.Description())
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := h.KeysHandled()
|
||||||
|
if !slices.Contains(keys, "up") || !slices.Contains(keys, "k") {
|
||||||
|
t.Errorf("Expected keys ['up', 'k'], got %v", keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpNavigationHandler_Match(t *testing.T) {
|
||||||
|
h := handlers.NewUpNavigationHandler()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode state.Mode
|
||||||
|
key string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Matches 'up' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "up",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Matches 'k' in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "k",
|
||||||
|
expected: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'up' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "up",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores 'k' in Input Mode",
|
||||||
|
mode: state.InputMode,
|
||||||
|
key: "k",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ignores other keys in Navigation Mode",
|
||||||
|
mode: state.NavigationMode,
|
||||||
|
key: "down",
|
||||||
|
expected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
s := state.New(state.WithMode(tt.mode))
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(tt.key)}
|
||||||
|
|
||||||
|
if tt.key == "up" {
|
||||||
|
msg = tea.KeyMsg{Type: tea.KeyUp}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := h.Match(s, msg); got != tt.expected {
|
||||||
|
t.Errorf("Match() = %v, want %v", got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpNavigationHandler_Handle(t *testing.T) {
|
||||||
|
h := handlers.NewUpNavigationHandler()
|
||||||
|
|
||||||
|
s := state.New(
|
||||||
|
state.WithCursor(1, 0),
|
||||||
|
state.WithMode(state.NavigationMode),
|
||||||
|
)
|
||||||
|
|
||||||
|
msg := tea.KeyMsg{Type: tea.KeyUp}
|
||||||
|
cmd := h.Handle(s, msg)
|
||||||
|
|
||||||
|
if s.CursorRow() != 0 {
|
||||||
|
t.Errorf("Expected cursor to move to Row 0, got %d", s.CursorRow())
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd != nil {
|
||||||
|
t.Error("Expected Handle to return nil, got a command")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/StevanFreeborn/term-tier/internal/state"
|
||||||
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WinSizeMsgHandler interface {
|
||||||
|
Match(s state.State, msg tea.WindowSizeMsg) bool
|
||||||
|
Handle(s state.State, msg tea.WindowSizeMsg) tea.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllWinSizeHandlers() []WinSizeMsgHandler {
|
||||||
|
return []WinSizeMsgHandler{
|
||||||
|
NewTrackWindowSizeHandler(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user