summaryrefslogtreecommitdiff
path: root/internal/tui/keys.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tui/keys.go')
-rw-r--r--internal/tui/keys.go486
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
}