diff options
author | T <t@tjp.lol> | 2025-08-05 12:36:30 -0600 |
---|---|---|
committer | T <t@tjp.lol> | 2025-08-06 12:13:11 -0600 |
commit | 65e2ed65775d64afbc6065a3b4ac1069020093ca (patch) | |
tree | f94fabfed5be2d2622429ebc7c8af1bf51085824 /internal/tui/keys.go | |
parent | 665bd389a0a1c8adadcaa1122e846cc81f5ead31 (diff) |
most features in TUI working, remaining unimplemented keybinds need a modal view
Diffstat (limited to 'internal/tui/keys.go')
-rw-r--r-- | internal/tui/keys.go | 486 |
1 files changed, 290 insertions, 196 deletions
diff --git a/internal/tui/keys.go b/internal/tui/keys.go index 7f23407..d2e08f4 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -1,229 +1,323 @@ package tui import ( + "slices" + tea "github.com/charmbracelet/bubbletea" ) -// KeyAction represents the action to take for a key press -type KeyAction int +type KeyBindingScope int const ( - // Global actions - ActionNone KeyAction = iota - ActionNextPane - ActionPrevPane - ActionPunchToggle - ActionSearch - ActionRefresh - ActionQuit - - // Timer pane actions - ActionTimerEnter - ActionTimerDescribe - - // Projects pane actions - ActionProjectsNext - ActionProjectsPrev - ActionProjectsEnter - ActionProjectsNewProject - ActionProjectsNewClient - - // History pane actions (level 1) - ActionHistoryNext - ActionHistoryPrev - ActionHistoryEnter - - // History pane actions (level 2) - ActionHistoryEdit - ActionHistoryDelete - ActionHistoryResume - ActionHistoryBack + ScopeGlobal KeyBindingScope = iota + ScopeTimerBox + ScopeProjectsBox + ScopeHistoryBoxSummaries + ScopeHistoryBoxDetails ) -// KeyHandler processes key messages and returns the appropriate action -func HandleKeyPress(msg tea.KeyMsg, selectedBox BoxType, historyLevel HistoryViewLevel, hasActiveTimer bool) KeyAction { - key := msg.String() - - // Global keybindings (always available) - switch key { - case "ctrl+n": - return ActionNextPane - case "ctrl+p": - return ActionPrevPane - case "p": - return ActionPunchToggle - case "/": - return ActionSearch - case "r": - return ActionRefresh - case "q", "ctrl+c", "ctrl+d": - return ActionQuit - } - - // Context-specific keybindings based on selected box - switch selectedBox { - case TimerBox: - return handleTimerKeys(key, hasActiveTimer) - case ClientsProjectsBox: - return handleProjectsKeys(key) - case HistoryBox: - return handleHistoryKeys(key, historyLevel) - } - - return ActionNone +// KeyBinding represents the available key bindings for a view +type KeyBinding struct { + Key string + Description func(AppModel) string + Scope KeyBindingScope + Result func(AppModel) tea.Cmd + Hide bool } -// handleTimerKeys handles keys specific to the timer box -func handleTimerKeys(key string, hasActiveTimer bool) KeyAction { - switch key { - case "enter": - return ActionTimerEnter - case "d": - if hasActiveTimer { - return ActionTimerDescribe - } - } - return ActionNone +type ( + createProjectMsg struct{} + createClientMsg struct{} + activateSearch struct{} + editHistoryEntry struct{} + deleteHistoryEntry struct{} +) + +func msgAsCmd(msg tea.Msg) tea.Cmd { + return func() tea.Msg { return msg } } -// handleProjectsKeys handles keys specific to the projects box -func handleProjectsKeys(key string) KeyAction { - switch key { - case "j", "down": - return ActionProjectsNext - case "k", "up": - return ActionProjectsPrev - case "enter": - return ActionProjectsEnter - case "n": - return ActionProjectsNewProject - case "N": - return ActionProjectsNewClient - } - return ActionNone +var Bindings map[KeyBindingScope]map[string]KeyBinding = map[KeyBindingScope]map[string]KeyBinding{ + ScopeGlobal: { + "ctrl+n": KeyBinding{ + Key: "Ctrl+n", + Description: func(AppModel) string { return "Next Pane" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return navigate(true) }, + }, + "ctrl+p": KeyBinding{ + Key: "Ctrl+p", + Description: func(AppModel) string { return "Prev Pane" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return navigate(false) }, + }, + "p": KeyBinding{ + Key: "p", + Description: func(am AppModel) string { + if am.timerBox.timerInfo.IsActive { + return "Punch Out" + } + return "Punch In" + }, + Scope: ScopeGlobal, + Result: func(am AppModel) tea.Cmd { + if am.timerBox.timerInfo.IsActive { + return punchOut(am) + } + return punchIn(am) + }, + }, + "/": KeyBinding{ + Key: "/", + Description: func(am AppModel) string { return "Search" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return msgAsCmd(activateSearch{}) }, + }, + "r": KeyBinding{ + Key: "r", + Description: func(am AppModel) string { return "Refresh" }, + Scope: ScopeGlobal, + Result: func(am AppModel) tea.Cmd { return am.refreshCmd }, + }, + "q": KeyBinding{ + Key: "q", + Description: func(am AppModel) string { return "Quit" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return tea.Quit }, + }, + "ctrl+c": KeyBinding{ + Key: "Ctrl+c", + Description: func(am AppModel) string { return "Quit" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return tea.Quit }, + Hide: true, + }, + "ctrl+d": KeyBinding{ + Key: "Ctrl+d", + Description: func(am AppModel) string { return "Quit" }, + Scope: ScopeGlobal, + Result: func(AppModel) tea.Cmd { return tea.Quit }, + Hide: true, + }, + }, + ScopeTimerBox: { + "enter": KeyBinding{ + Key: "Enter", + Description: func(am AppModel) string { + if am.timerBox.timerInfo.IsActive { + return "Punch Out" + } + return "Punch In" + }, + Scope: ScopeTimerBox, + Result: func(am AppModel) tea.Cmd { + if am.timerBox.timerInfo.IsActive { + return punchOut(am) + } + return punchIn(am) + }, + }, + }, + ScopeProjectsBox: { + "j": KeyBinding{ + Key: "j", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + }, + "k": KeyBinding{ + Key: "k", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + }, + "down": KeyBinding{ + Key: "down", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + Hide: true, + }, + "up": KeyBinding{ + Key: "up", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + Hide: true, + }, + "enter": KeyBinding{ + Key: "Enter", + Description: func(AppModel) string { return "Punch In on Selection" }, + Scope: ScopeProjectsBox, + Result: func(am AppModel) tea.Cmd { return punchInOnSelection(am) }, + }, + "n": KeyBinding{ + Key: "n", + Description: func(AppModel) string { return "New Project" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return msgAsCmd(createProjectMsg{}) }, + }, + "N": KeyBinding{ + Key: "N", + Description: func(AppModel) string { return "New Client" }, + Scope: ScopeProjectsBox, + Result: func(AppModel) tea.Cmd { return msgAsCmd(createClientMsg{}) }, + }, + }, + ScopeHistoryBoxSummaries: { + "j": KeyBinding{ + Key: "j", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeHistoryBoxSummaries, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + }, + "k": KeyBinding{ + Key: "k", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeHistoryBoxSummaries, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + }, + "down": KeyBinding{ + Key: "down", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeHistoryBoxSummaries, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + Hide: true, + }, + "up": KeyBinding{ + Key: "up", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeHistoryBoxSummaries, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + Hide: true, + }, + "enter": KeyBinding{ + Key: "Enter", + Description: func(AppModel) string { return "Select" }, + Scope: ScopeHistoryBoxSummaries, + Result: func(AppModel) tea.Cmd { return selectHistorySummary() }, + }, + }, + ScopeHistoryBoxDetails: { + "j": KeyBinding{ + Key: "j", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + }, + "k": KeyBinding{ + Key: "k", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + }, + "down": KeyBinding{ + Key: "Down", + Description: func(AppModel) string { return "Down" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return changeSelection(true) }, + Hide: true, + }, + "up": KeyBinding{ + Key: "Up", + Description: func(AppModel) string { return "Up" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return changeSelection(false) }, + Hide: true, + }, + "e": KeyBinding{ + Key: "e", + Description: func(AppModel) string { return "Edit" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return msgAsCmd(editHistoryEntry{}) }, + }, + "d": KeyBinding{ + Key: "d", + Description: func(AppModel) string { return "Delete" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return msgAsCmd(deleteHistoryEntry{}) }, + }, + "enter": KeyBinding{ + Key: "Enter", + Description: func(AppModel) string { return "Resume" }, + Scope: ScopeHistoryBoxDetails, + Result: func(am AppModel) tea.Cmd { return punchInOnSelection(am) }, + }, + "b": KeyBinding{ + Key: "b", + Description: func(AppModel) string { return "Back" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return backToHistorySummary() }, + }, + "esc": KeyBinding{ + Key: "Esc", + Description: func(AppModel) string { return "Back" }, + Scope: ScopeHistoryBoxDetails, + Result: func(AppModel) tea.Cmd { return backToHistorySummary() }, + Hide: true, + }, + }, } -// handleHistoryKeys handles keys specific to the history box -func handleHistoryKeys(key string, level HistoryViewLevel) KeyAction { - switch level { - case HistoryLevelSummary: - return handleHistoryLevel1Keys(key) - case HistoryLevelDetails: - return handleHistoryLevel2Keys(key) +// KeyHandler processes key messages and returns the appropriate action +func HandleKeyPress(msg tea.KeyMsg, data AppModel) tea.Cmd { + key := msg.String() + + if binding, ok := Bindings[ScopeGlobal][key]; ok { + return binding.Result(data) } - return ActionNone -} -// handleHistoryLevel1Keys handles keys for history summary view -func handleHistoryLevel1Keys(key string) KeyAction { - switch key { - case "j", "down": - return ActionHistoryNext - case "k", "up": - return ActionHistoryPrev - case "enter": - return ActionHistoryEnter + var local map[string]KeyBinding + switch data.selectedBox { + case TimerBox: + local = Bindings[ScopeTimerBox] + case ProjectsBox: + local = Bindings[ScopeProjectsBox] + case HistoryBox: + switch data.historyBox.viewLevel { + case HistoryLevelSummary: + local = Bindings[ScopeHistoryBoxSummaries] + case HistoryLevelDetails: + local = Bindings[ScopeHistoryBoxDetails] + } } - return ActionNone -} -// handleHistoryLevel2Keys handles keys for history details view -func handleHistoryLevel2Keys(key string) KeyAction { - switch key { - case "j", "down": - return ActionHistoryNext - case "k", "up": - return ActionHistoryPrev - case "e": - return ActionHistoryEdit - case "d": - return ActionHistoryDelete - case "enter": - return ActionHistoryResume - case "b", "escape": - return ActionHistoryBack + if binding, ok := local[key]; ok { + return binding.Result(data) } - return ActionNone + return nil } -// GetContextualKeyBindings returns the key bindings that should be shown in the bottom bar -func GetContextualKeyBindings(selectedBox BoxType, historyLevel HistoryViewLevel, hasActiveTimer bool) []KeyBinding { - var bindings []KeyBinding - - // Global bindings (always shown) - bindings = append(bindings, []KeyBinding{ - {"Ctrl+n", "Next"}, - {"Ctrl+p", "Prev"}, - }...) - - // Add punch toggle binding - if hasActiveTimer { - bindings = append(bindings, KeyBinding{"p", "Punch Out"}) - } else { - bindings = append(bindings, KeyBinding{"p", "Punch In"}) +func activeBindings(box BoxType, level HistoryViewLevel) []KeyBinding { + out := make([]KeyBinding, 0, len(Bindings[ScopeGlobal])) + for _, binding := range Bindings[ScopeGlobal] { + out = append(out, binding) } - - // Add search and refresh bindings - bindings = append(bindings, []KeyBinding{ - {"/", "Search"}, - {"r", "Refresh"}, - }...) - - // Context-specific bindings - switch selectedBox { + + var scope KeyBindingScope + switch box { case TimerBox: - bindings = append(bindings, getTimerKeyBindings(hasActiveTimer)...) - case ClientsProjectsBox: - bindings = append(bindings, getProjectsKeyBindings()...) + scope = ScopeTimerBox + case ProjectsBox: + scope = ScopeProjectsBox case HistoryBox: - bindings = append(bindings, getHistoryKeyBindings(historyLevel)...) - } - - // Always end with quit - bindings = append(bindings, KeyBinding{"q", "Quit"}) - - return bindings -} - -// getTimerKeyBindings returns key bindings for the timer box -func getTimerKeyBindings(hasActiveTimer bool) []KeyBinding { - if hasActiveTimer { - return []KeyBinding{ - {"Enter", "Punch Out"}, - {"d", "Describe"}, + switch level { + case HistoryLevelSummary: + scope = ScopeHistoryBoxSummaries + case HistoryLevelDetails: + scope = ScopeHistoryBoxDetails } } - return []KeyBinding{ - {"Enter", "Resume Recent"}, - } -} -// getProjectsKeyBindings returns key bindings for the projects box -func getProjectsKeyBindings() []KeyBinding { - return []KeyBinding{ - {"j/k", "Navigate"}, - {"Enter", "Select"}, - {"n", "New Project"}, - {"N", "New Client"}, + for _, binding := range Bindings[scope] { + out = append(out, binding) } -} -// getHistoryKeyBindings returns key bindings for the history box -func getHistoryKeyBindings(level HistoryViewLevel) []KeyBinding { - switch level { - case HistoryLevelSummary: - return []KeyBinding{ - {"j/k", "Navigate"}, - {"Enter", "Details"}, - } - case HistoryLevelDetails: - return []KeyBinding{ - {"j/k", "Navigate"}, - {"Enter", "Resume"}, - {"e", "Edit"}, - {"d", "Delete"}, - {"b", "Back"}, + slices.SortFunc(out, func(a, b KeyBinding) int { + if a.Key < b.Key { + return -1 } - } - return []KeyBinding{} + return 1 + }) + return out } |