diff --git a/internal/handlers/downNavigationHandler.go b/internal/handlers/downNavigationHandler.go new file mode 100644 index 0000000..6fd215e --- /dev/null +++ b/internal/handlers/downNavigationHandler.go @@ -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 +} diff --git a/internal/handlers/downNavigationHandler_test.go b/internal/handlers/downNavigationHandler_test.go new file mode 100644 index 0000000..8f6f424 --- /dev/null +++ b/internal/handlers/downNavigationHandler_test.go @@ -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") + } +} diff --git a/internal/handlers/helpHander_test.go b/internal/handlers/helpHander_test.go new file mode 100644 index 0000000..70355bb --- /dev/null +++ b/internal/handlers/helpHander_test.go @@ -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()) + } + }) +} diff --git a/internal/handlers/helpHandler.go b/internal/handlers/helpHandler.go new file mode 100644 index 0000000..5f25e72 --- /dev/null +++ b/internal/handlers/helpHandler.go @@ -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 +} diff --git a/internal/handlers/keyMsgHandler.go b/internal/handlers/keyMsgHandler.go new file mode 100644 index 0000000..579e606 --- /dev/null +++ b/internal/handlers/keyMsgHandler.go @@ -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(), + } +} diff --git a/internal/handlers/leftNavigationHandler.go b/internal/handlers/leftNavigationHandler.go new file mode 100644 index 0000000..e81389a --- /dev/null +++ b/internal/handlers/leftNavigationHandler.go @@ -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 +} diff --git a/internal/handlers/leftNavigationHandler_test.go b/internal/handlers/leftNavigationHandler_test.go new file mode 100644 index 0000000..7ed0153 --- /dev/null +++ b/internal/handlers/leftNavigationHandler_test.go @@ -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") + } +} diff --git a/internal/handlers/mocks_test.go b/internal/handlers/mocks_test.go new file mode 100644 index 0000000..d4eb2dc --- /dev/null +++ b/internal/handlers/mocks_test.go @@ -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 +} diff --git a/internal/handlers/quitHandler.go b/internal/handlers/quitHandler.go new file mode 100644 index 0000000..fc6e6cd --- /dev/null +++ b/internal/handlers/quitHandler.go @@ -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 +} diff --git a/internal/handlers/quitHandler_test.go b/internal/handlers/quitHandler_test.go new file mode 100644 index 0000000..5169ac3 --- /dev/null +++ b/internal/handlers/quitHandler_test.go @@ -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) + } +} diff --git a/internal/handlers/rightNavigationHandler.go b/internal/handlers/rightNavigationHandler.go new file mode 100644 index 0000000..ef856b6 --- /dev/null +++ b/internal/handlers/rightNavigationHandler.go @@ -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 +} diff --git a/internal/handlers/rightNavigationHandler_test.go b/internal/handlers/rightNavigationHandler_test.go new file mode 100644 index 0000000..3a373b3 --- /dev/null +++ b/internal/handlers/rightNavigationHandler_test.go @@ -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") + } +} diff --git a/internal/handlers/trackWindowSizeHandler.go b/internal/handlers/trackWindowSizeHandler.go new file mode 100644 index 0000000..a927d3b --- /dev/null +++ b/internal/handlers/trackWindowSizeHandler.go @@ -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 +} diff --git a/internal/handlers/trackWindowSizeHandler_test.go b/internal/handlers/trackWindowSizeHandler_test.go new file mode 100644 index 0000000..6328bec --- /dev/null +++ b/internal/handlers/trackWindowSizeHandler_test.go @@ -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") + } +} diff --git a/internal/handlers/upNavigationHandler.go b/internal/handlers/upNavigationHandler.go new file mode 100644 index 0000000..747767d --- /dev/null +++ b/internal/handlers/upNavigationHandler.go @@ -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 +} diff --git a/internal/handlers/upNavigationHandler_test.go b/internal/handlers/upNavigationHandler_test.go new file mode 100644 index 0000000..f23761a --- /dev/null +++ b/internal/handlers/upNavigationHandler_test.go @@ -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") + } +} diff --git a/internal/handlers/winSizeMsgHandler.go b/internal/handlers/winSizeMsgHandler.go new file mode 100644 index 0000000..ac9a929 --- /dev/null +++ b/internal/handlers/winSizeMsgHandler.go @@ -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(), + } +}