package tui import ( "slices" tea "github.com/charmbracelet/bubbletea" ) type KeyBindingScope int const ( ScopeGlobal KeyBindingScope = iota ScopeTimerBox ScopeProjectsBox ScopeHistoryBoxSummaries ScopeHistoryBoxDetails ) // 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 } type ( createProjectMsg struct{} createClientMsg struct{} activateSearch struct{} editHistoryEntry struct{} deleteHistoryEntry struct{} ) func msgAsCmd(msg tea.Msg) tea.Cmd { return func() tea.Msg { return msg } } 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, }, }, } // 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) } 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] } } if binding, ok := local[key]; ok { return binding.Result(data) } return nil } func activeBindings(box BoxType, level HistoryViewLevel) []KeyBinding { out := make([]KeyBinding, 0, len(Bindings[ScopeGlobal])) for _, binding := range Bindings[ScopeGlobal] { out = append(out, binding) } var scope KeyBindingScope switch box { case TimerBox: scope = ScopeTimerBox case ProjectsBox: scope = ScopeProjectsBox case HistoryBox: switch level { case HistoryLevelSummary: scope = ScopeHistoryBoxSummaries case HistoryLevelDetails: scope = ScopeHistoryBoxDetails } } for _, binding := range Bindings[scope] { out = append(out, binding) } slices.SortFunc(out, func(a, b KeyBinding) int { if a.Key < b.Key { return -1 } return 1 }) return out }