diff options
| -rw-r--r-- | internal/ui/handlers.go | 6 | ||||
| -rw-r--r-- | internal/ui/keyhandlers.go | 75 | ||||
| -rw-r--r-- | internal/ui/table.go | 7 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 74 | ||||
| -rw-r--r-- | internal/ui/ultra.go | 6 |
5 files changed, 153 insertions, 15 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go index a210ffc..8c417d3 100644 --- a/internal/ui/handlers.go +++ b/internal/ui/handlers.go @@ -615,8 +615,10 @@ func (m *Model) handleTaskDetailMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // Normal task detail view mode switch msg.String() { - case "q", "esc": - return m.handleQuitOrEscape() + case "q": + return m.handleQuitKey() + case "esc": + return m.handleEscapeKey() case "/", "?": m.detailSearching = true m.detailSearchInput.SetValue("") diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go index ffd52b1..890140d 100644 --- a/internal/ui/keyhandlers.go +++ b/internal/ui/keyhandlers.go @@ -19,8 +19,10 @@ func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { // If help is shown, handle special cases if m.showHelp { switch msg.String() { - case "H", "esc", "q": - return m.handleQuitOrEscape() + case "H", "q": + return m.handleQuitKey() + case "esc": + return m.handleEscapeKey() case "/", "?": return m.handleHelpSearch() case "n": @@ -54,8 +56,10 @@ func (m *Model) handleNormalMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "H": return m.handleToggleHelp() - case "q", "esc": - return m.handleQuitOrEscape() + case "q": + return m.handleQuitKey() + case "esc": + return m.handleEscapeKey() case "e", "E": return m.handleEditTask() case "s": @@ -144,7 +148,7 @@ func (m *Model) handleToggleHelp() (tea.Model, tea.Cmd) { return m, nil } -func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { +func (m *Model) handleQuitKey() (tea.Model, tea.Cmd) { if m.showHelp { m.showHelp = false // Clear help search state @@ -157,8 +161,8 @@ func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { return m, nil } if m.showUltra { - // Active search: q/esc clears the search filter first, same as in - // normal table mode. Only proceed to exit/quit when no search is active. + // Active search: q clears the search filter first, same as in normal + // table mode. Only proceed to exit/quit when no search is active. if m.ultraSearchRegex != nil { m.ultraSearchRegex = nil m.ultraFiltered = nil @@ -167,7 +171,7 @@ func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { return m, nil } // When started via --ultra flag there is no table view to return to, - // so q/esc exits the application directly. + // so q exits the application directly. if m.ultraStartup { return m, tea.Quit } @@ -199,6 +203,61 @@ func (m *Model) handleQuitOrEscape() (tea.Model, tea.Cmd) { return m, tea.Quit } +func (m *Model) handleEscapeKey() (tea.Model, tea.Cmd) { + if m.showHelp { + m.showHelp = false + // Clear help search state + m.helpSearchRegex = nil + m.helpSearchMatches = nil + m.helpSearchIndex = 0 + m.helpSearchInput.SetValue("") + // Reset help viewport + m.helpViewport = viewport.Model{} + return m, nil + } + if m.showUltra { + // Active search: esc clears the search filter first, same as in + // normal table mode. It never quits the application. + if m.ultraSearchRegex != nil { + m.ultraSearchRegex = nil + m.ultraFiltered = nil + m.ultraCursor = 0 + m.ultraOffset = 0 + return m, nil + } + // When started via --ultra flag there is no table view to return to, + // so esc just stays in ultra mode. + if m.ultraStartup { + return m, nil + } + m.ultraClearFocusedID() + m.showUltra = false + m.ultraSearchInput.SetValue("") + return m, nil + } + if m.showTaskDetail { + m.showTaskDetail = false + m.currentTaskDetail = nil + m.detailSearching = false + m.detailSearchRegex = nil + m.detailSearchInput.SetValue("") + return m, nil + } + if m.cellExpanded { + m.cellExpanded = false + m.updateTableHeight() + return m, nil + } + if m.searchRegex != nil { + m.searchRegex = nil + m.searchMatches = nil + m.searchIndex = 0 + m.reload() + return m, nil + } + return m, nil +} + func (m *Model) handleEditTask() (tea.Model, tea.Cmd) { id, err := m.getSelectedTaskID() if err != nil { diff --git a/internal/ui/table.go b/internal/ui/table.go index 4cb918f..b8b339b 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -91,7 +91,7 @@ type detailViewState struct { // ultraState holds the state for the ultra mode task list and its search UI. type ultraState struct { showUltra bool - ultraStartup bool // true when ultra was set via --ultra flag; q quits directly + ultraStartup bool // true when ultra was set via --ultra flag; q quits directly, esc never does ultraCursor int ultraOffset int ultraSearching bool @@ -1329,8 +1329,9 @@ func (m *Model) SetDisco(d bool) { // SetUltra enables or disables ultra mode, causing the UI to start directly // in the ultra task list view instead of the default table view. -// When u is true, q/esc quits the application immediately rather than -// returning to the table view, because there is no table view to return to. +// When u is true, q quits the application immediately rather than returning +// to the table view, because there is no table view to return to. esc always +// cancels/closes overlays instead of quitting. func (m *Model) SetUltra(u bool) { m.showUltra = u m.ultraStartup = u diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 74c7e70..6adaad1 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -762,6 +762,80 @@ func TestEscClosesHelp(t *testing.T) { } } +func TestEscDoesNotQuitFromTable(t *testing.T) { + tmp := t.TempDir() + taskPath := setupBasicTask(t, tmp) + setupEnv(t, taskPath) + + m, err := New(nil, "firefox") + if err != nil { + t.Fatalf("New: %v", err) + } + + mv, cmd := (&m).Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + if cmd != nil { + t.Fatalf("esc in table mode unexpectedly returned a command") + } + m = *mv.(*Model) + if m.showHelp || m.showTaskDetail || m.showUltra { + t.Fatalf("esc changed mode unexpectedly: help=%v detail=%v ultra=%v", m.showHelp, m.showTaskDetail, m.showUltra) + } + + mv, cmd = (&m).Update(tea.KeyPressMsg{Code: 'q', Text: "q"}) + if cmd == nil { + t.Fatalf("q in table mode did not return a quit command") + } + m = *mv.(*Model) + if m.showHelp || m.showTaskDetail || m.showUltra { + t.Fatalf("q changed mode unexpectedly: help=%v detail=%v ultra=%v", m.showHelp, m.showTaskDetail, m.showUltra) + } +} + +func TestEscDoesNotQuitUltraStartup(t *testing.T) { + tmp := t.TempDir() + taskPath := setupBasicTask(t, tmp) + setupEnv(t, taskPath) + + m, err := New(nil, "firefox") + if err != nil { + t.Fatalf("New: %v", err) + } + m.SetUltra(true) + + mv, cmd := (&m).Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + if cmd != nil { + t.Fatalf("esc in ultra startup unexpectedly returned a command") + } + m = *mv.(*Model) + if !m.showUltra { + t.Fatalf("esc in ultra startup exited ultra mode") + } + + m.ultraSearchRegex = regexp.MustCompile("alpha") + m.ultraFiltered = []int{0} + m.ultraSearchInput.SetValue("alpha") + + mv, cmd = (&m).Update(tea.KeyPressMsg{Code: tea.KeyEsc}) + if cmd != nil { + t.Fatalf("esc in ultra startup with search unexpectedly returned a command") + } + m = *mv.(*Model) + if !m.showUltra { + t.Fatalf("esc in ultra startup with search exited ultra mode") + } + if m.ultraSearchRegex != nil { + t.Fatalf("esc in ultra startup with search did not clear ultraSearchRegex") + } + if m.ultraFiltered != nil { + t.Fatalf("esc in ultra startup with search did not clear ultraFiltered") + } + + mv, cmd = (&m).Update(tea.KeyPressMsg{Code: 'q', Text: "q"}) + if cmd == nil { + t.Fatalf("q in ultra startup did not return a quit command") + } +} + func TestUltraHelpUsesUltraBindingsAndClosesBeforeLeavingUltra(t *testing.T) { tmp := t.TempDir() taskPath := setupBasicTask(t, tmp) diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go index 78866be..5c7b6d1 100644 --- a/internal/ui/ultra.go +++ b/internal/ui/ultra.go @@ -1010,8 +1010,10 @@ func (m *Model) handleUltraMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) { switch msg.String() { case "H": return m.handleToggleHelp() - case "q", "esc": - return m.handleQuitOrEscape() + case "q": + return m.handleQuitKey() + case "esc": + return m.handleEscapeKey() case "u": // Toggle back to the traditional table view. Works even when started // via --ultra because the table model always exists; it was just never |
