diff options
| author | Paul Buetow <paul@buetow.org> | 2026-06-22 23:15:49 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-06-22 23:15:49 +0300 |
| commit | 0e2d685c44c9d31f3f22e4c36bff9a20f3258126 (patch) | |
| tree | 38b8d8b005c370eb14700ba29ae370fcdc587a0b /internal | |
| parent | ee8d6de342486c0065fed1ab7580b15789511677 (diff) | |
Refactor shared key dispatch registry for uq0
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/ui/keyhandlers.go | 158 | ||||
| -rw-r--r-- | internal/ui/keyhandlers_test.go | 37 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 2 | ||||
| -rw-r--r-- | internal/ui/ultra.go | 2 |
4 files changed, 125 insertions, 74 deletions
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index d00da3e..767dc9c 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -20,6 +20,54 @@ type sharedKeyHandlers struct { addTask func() (tea.Model, tea.Cmd) } +type keyBindingMode uint8 + +const ( + keyBindingNormal keyBindingMode = 1 << iota + keyBindingUltra + + keyBindingAll = keyBindingNormal | keyBindingUltra +) + +type keyBindingAction func(*Model, sharedKeyHandlers) (bool, tea.Model, tea.Cmd) + +type keyBinding struct { + keys []string + modes keyBindingMode + desc string + action keyBindingAction +} + +var sharedKeyBindings = []keyBinding{ + {keys: []string{"H"}, modes: keyBindingAll, desc: "toggle help", action: modelKeyAction((*Model).handleToggleHelp)}, + {keys: []string{"q"}, modes: keyBindingAll, desc: "quit or exit current view", action: modelKeyAction((*Model).handleQuitKey)}, + {keys: []string{"esc"}, modes: keyBindingAll, desc: "close help/input or cancel", action: modelKeyAction((*Model).handleEscapeKey)}, + {keys: []string{"e", "E"}, modes: keyBindingAll, desc: "edit selected task", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.editTask })}, + {keys: []string{"s"}, modes: keyBindingAll, desc: "start/stop task", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.toggleStart })}, + {keys: []string{"d"}, modes: keyBindingAll, desc: "mark task done", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.markDone })}, + {keys: []string{"D"}, modes: keyBindingAll, desc: "delete task/recurring series", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.deleteTask })}, + {keys: []string{"o"}, modes: keyBindingAll, desc: "open URL from description", action: modelKeyAction((*Model).handleOpenURL)}, + {keys: []string{"U"}, modes: keyBindingAll, desc: "undo last done/delete", action: modelKeyAction((*Model).handleUndo)}, + {keys: []string{"w"}, modes: keyBindingAll, desc: "set due date", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.setDueDate })}, + {keys: []string{"W"}, modes: keyBindingAll, desc: "remove due date", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.removeDueDate })}, + {keys: []string{"r"}, modes: keyBindingAll, desc: "set random due date", action: modelKeyAction((*Model).handleRandomDueDate)}, + {keys: []string{"R"}, modes: keyBindingAll, desc: "edit recurrence", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.setRecurrence })}, + {keys: []string{"p"}, modes: keyBindingAll, desc: "set priority", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.setPriority })}, + {keys: []string{"a"}, modes: keyBindingAll, desc: "add annotations", action: sharedAnnotateKeyAction(false)}, + {keys: []string{"A"}, modes: keyBindingAll, desc: "replace annotations", action: sharedAnnotateKeyAction(true)}, + {keys: []string{"f"}, modes: keyBindingAll, desc: "change filter", action: modelKeyAction((*Model).handleFilter)}, + {keys: []string{":"}, modes: keyBindingAll, desc: "run task command prompt", action: modelKeyAction((*Model).handleShellPrompt)}, + {keys: []string{";"}, modes: keyBindingAll, desc: "run task command prompt for selected task", action: modelKeyAction((*Model).handleShellPromptForSelectedTask)}, + {keys: []string{"+"}, modes: keyBindingAll, desc: "add new task", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.addTask })}, + {keys: []string{"t"}, modes: keyBindingAll, desc: "edit tags", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.editTags })}, + {keys: []string{"J"}, modes: keyBindingAll, desc: "edit project", action: sharedKeyAction(func(h sharedKeyHandlers) func() (tea.Model, tea.Cmd) { return h.editProject })}, + {keys: []string{"c"}, modes: keyBindingAll, desc: "random theme", action: modelKeyAction((*Model).handleRandomTheme)}, + {keys: []string{"C"}, modes: keyBindingAll, desc: "reset theme", action: modelKeyAction((*Model).handleResetTheme)}, + {keys: []string{"x"}, modes: keyBindingAll, desc: "toggle disco mode", action: modelKeyAction((*Model).handleToggleDisco)}, + {keys: []string{"B"}, modes: keyBindingAll, desc: "toggle blinking", action: modelKeyAction((*Model).handleToggleBlink)}, + {keys: []string{"space"}, modes: keyBindingAll, desc: "refresh tasks", action: modelKeyAction((*Model).handleRefresh)}, +} + // handleNormalMode handles keyboard input in normal mode (not editing) func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // If help is shown, handle special cases @@ -59,7 +107,7 @@ func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { } } - if handled, model, cmd := m.handleSharedKey(msg.String(), m.normalSharedKeyHandlers()); handled { + if handled, model, cmd := m.handleSharedKey(msg.String(), keyBindingNormal, m.normalSharedKeyHandlers()); handled { return model, cmd } @@ -130,83 +178,49 @@ func (m *Model) ultraSharedKeyHandlers() sharedKeyHandlers { } } -func (m *Model) handleSharedKey(key string, handlers sharedKeyHandlers) (bool, tea.Model, tea.Cmd) { +func (m *Model) handleSharedKey(key string, mode keyBindingMode, handlers sharedKeyHandlers) (bool, tea.Model, tea.Cmd) { if key == m.agentFilterHotkeyLabel() { model, cmd := m.handleToggleAgentFilter() return true, model, cmd } - switch key { - case "H": - model, cmd := m.handleToggleHelp() - return true, model, cmd - case "q": - model, cmd := m.handleQuitKey() - return true, model, cmd - case "esc": - model, cmd := m.handleEscapeKey() - return true, model, cmd - case "e", "E": - return callSharedKeyHandler(m, handlers.editTask) - case "s": - return callSharedKeyHandler(m, handlers.toggleStart) - case "d": - return callSharedKeyHandler(m, handlers.markDone) - case "D": - return callSharedKeyHandler(m, handlers.deleteTask) - case "o": - model, cmd := m.handleOpenURL() - return true, model, cmd - case "U": - model, cmd := m.handleUndo() - return true, model, cmd - case "w": - return callSharedKeyHandler(m, handlers.setDueDate) - case "W": - return callSharedKeyHandler(m, handlers.removeDueDate) - case "r": - model, cmd := m.handleRandomDueDate() - return true, model, cmd - case "R": - return callSharedKeyHandler(m, handlers.setRecurrence) - case "p": - return callSharedKeyHandler(m, handlers.setPriority) - case "a": - return callSharedAnnotateHandler(m, handlers.annotate, false) - case "A": - return callSharedAnnotateHandler(m, handlers.annotate, true) - case "f": - model, cmd := m.handleFilter() - return true, model, cmd - case ":": - model, cmd := m.handleShellPrompt() - return true, model, cmd - case ";": - model, cmd := m.handleShellPromptForSelectedTask() - return true, model, cmd - case "+": - return callSharedKeyHandler(m, handlers.addTask) - case "t": - return callSharedKeyHandler(m, handlers.editTags) - case "J": - return callSharedKeyHandler(m, handlers.editProject) - case "c": - model, cmd := m.handleRandomTheme() - return true, model, cmd - case "C": - model, cmd := m.handleResetTheme() - return true, model, cmd - case "x": - model, cmd := m.handleToggleDisco() - return true, model, cmd - case "B": - model, cmd := m.handleToggleBlink() - return true, model, cmd - case "space": - model, cmd := m.handleRefresh() + for _, binding := range sharedKeyBindings { + if binding.matches(key, mode) { + return binding.action(m, handlers) + } + } + + return false, m, nil +} + +func (b keyBinding) matches(key string, mode keyBindingMode) bool { + if b.modes&mode == 0 { + return false + } + for _, candidate := range b.keys { + if candidate == key { + return true + } + } + return false +} + +func modelKeyAction(handler func(*Model) (tea.Model, tea.Cmd)) keyBindingAction { + return func(m *Model, _ sharedKeyHandlers) (bool, tea.Model, tea.Cmd) { + model, cmd := handler(m) return true, model, cmd - default: - return false, m, nil + } +} + +func sharedKeyAction(selectHandler func(sharedKeyHandlers) func() (tea.Model, tea.Cmd)) keyBindingAction { + return func(m *Model, handlers sharedKeyHandlers) (bool, tea.Model, tea.Cmd) { + return callSharedKeyHandler(m, selectHandler(handlers)) + } +} + +func sharedAnnotateKeyAction(replace bool) keyBindingAction { + return func(m *Model, handlers sharedKeyHandlers) (bool, tea.Model, tea.Cmd) { + return callSharedAnnotateHandler(m, handlers.annotate, replace) } } diff --git a/internal/ui/keyhandlers_test.go b/internal/ui/keyhandlers_test.go new file mode 100644 index 0000000..d603aa1 --- /dev/null +++ b/internal/ui/keyhandlers_test.go @@ -0,0 +1,37 @@ +package ui + +import "testing" + +func TestSharedKeyBindingsHaveUsableMetadata(t *testing.T) { + seen := map[string]struct{}{} + for _, binding := range sharedKeyBindings { + if len(binding.keys) == 0 { + t.Fatalf("binding has no keys: %#v", binding) + } + if binding.modes == 0 { + t.Fatalf("binding %v has no modes", binding.keys) + } + if binding.desc == "" { + t.Fatalf("binding %v has no description", binding.keys) + } + if binding.action == nil { + t.Fatalf("binding %v has no action", binding.keys) + } + + for _, key := range binding.keys { + if key == "" { + t.Fatalf("binding %v contains an empty key", binding.keys) + } + if _, ok := seen[key]; ok { + t.Fatalf("duplicate shared key binding for %q", key) + } + seen[key] = struct{}{} + } + } + + for _, key := range []string{"p", "+", "H", "space"} { + if _, ok := seen[key]; !ok { + t.Fatalf("shared key registry is missing %q", key) + } + } +} diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index f19be5a..c102b82 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -2754,7 +2754,7 @@ func TestUltraPriorityOpUsesUltraSelection(t *testing.T) { } } -func TestSharedKeyBindingsWorkInNormalAndUltraModes(t *testing.T) { +func TestSharedKeyRegistryDispatchesInNormalAndUltraModes(t *testing.T) { tests := []struct { name string ultra bool diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go index 44249cb..656ffb7 100644 --- a/internal/ui/ultra.go +++ b/internal/ui/ultra.go @@ -1009,7 +1009,7 @@ func (m *Model) ultraEnsureVisible() { // handleUltraMode handles keyboard input in ultra mode. func (m *Model) handleUltraMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { - if handled, model, cmd := m.handleSharedKey(msg.String(), m.ultraSharedKeyHandlers()); handled { + if handled, model, cmd := m.handleSharedKey(msg.String(), keyBindingUltra, m.ultraSharedKeyHandlers()); handled { return model, cmd } |
