summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 15:38:17 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 16:13:46 +0300
commit02f45bd1707639a081d68f90ecc00c429de6d962 (patch)
treee00ab76060fe8d5c2f6cb6db350bbf0a73f85365
parenta08c45e934bed5fded2708df535e1c8f8776fa23 (diff)
fix(l0): keep ESC from quitting
-rw-r--r--internal/ui/handlers.go6
-rw-r--r--internal/ui/keyhandlers.go75
-rw-r--r--internal/ui/table.go7
-rw-r--r--internal/ui/table_test.go74
-rw-r--r--internal/ui/ultra.go6
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