summaryrefslogtreecommitdiff
path: root/internal/ui
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-08 15:41:33 +0300
committerPaul Buetow <paul@buetow.org>2026-04-08 16:14:11 +0300
commite2394dab62d54a3ae40044c06a7fec0b953a78e0 (patch)
tree3125e82250aeb388ff28f377c4e1358aa12cce4f /internal/ui
parent02f45bd1707639a081d68f90ecc00c429de6d962 (diff)
m0: fix ultra open-url hotkey
Diffstat (limited to 'internal/ui')
-rw-r--r--internal/ui/handlers.go18
-rw-r--r--internal/ui/keyhandlers.go2
-rw-r--r--internal/ui/table_test.go73
-rw-r--r--internal/ui/ultra.go3
4 files changed, 92 insertions, 4 deletions
diff --git a/internal/ui/handlers.go b/internal/ui/handlers.go
index 8c417d3..5428ebd 100644
--- a/internal/ui/handlers.go
+++ b/internal/ui/handlers.go
@@ -582,6 +582,24 @@ func (m *Model) getTaskAtCursor() *task.Task {
return &m.tasks[cursor]
}
+// getTaskForOpenURL returns the task that should be used by the open-URL
+// hotkey, honoring the active view's highlighted task.
+func (m *Model) getTaskForOpenURL() *task.Task {
+ if m.showTaskDetail && m.currentTaskDetail != nil {
+ return m.currentTaskDetail
+ }
+
+ if m.showUltra {
+ tasks := m.ultraTaskList()
+ if m.ultraCursor < 0 || m.ultraCursor >= len(tasks) {
+ return nil
+ }
+ return &tasks[m.ultraCursor]
+ }
+
+ return m.getTaskAtCursor()
+}
+
// handleTaskDetailMode handles keyboard input in task detail view
func (m *Model) handleTaskDetailMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
if m.detailSearching {
diff --git a/internal/ui/keyhandlers.go b/internal/ui/keyhandlers.go
index 890140d..33c7dc0 100644
--- a/internal/ui/keyhandlers.go
+++ b/internal/ui/keyhandlers.go
@@ -307,7 +307,7 @@ func (m *Model) handleMarkDone() (tea.Model, tea.Cmd) {
}
func (m *Model) handleOpenURL() (tea.Model, tea.Cmd) {
- task := m.getTaskAtCursor()
+ task := m.getTaskForOpenURL()
if task == nil {
return m, nil
}
diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go
index 6adaad1..3d3601e 100644
--- a/internal/ui/table_test.go
+++ b/internal/ui/table_test.go
@@ -877,9 +877,8 @@ func TestUltraHelpUsesUltraBindingsAndClosesBeforeLeavingUltra(t *testing.T) {
if !strings.Contains(view, "exit ultra mode") {
t.Fatalf("ultra help content missing ultra exit binding: %q", view)
}
- // "open URL from description" is now available in ultra mode (o key).
if !strings.Contains(view, "open URL from description") {
- t.Fatalf("ultra help missing o/open-URL binding: %q", view)
+ t.Fatalf("ultra help content missing open-url binding: %q", view)
}
if strings.Contains(view, "edit current field") {
t.Fatalf("ultra help rendered normal-only inline edit binding: %q", view)
@@ -927,7 +926,7 @@ func TestUltraHelpSearchUsesUltraHelpLines(t *testing.T) {
step(tea.KeyPressMsg{Code: 'u', Text: "u"})
step(tea.KeyPressMsg{Code: 'H', Text: "H"})
step(tea.KeyPressMsg{Code: '/', Text: "/"})
- for _, r := range "URL" {
+ for _, r := range "view task details" {
step(tea.KeyPressMsg{Code: r, Text: string(r)})
}
step(tea.KeyPressMsg{Code: tea.KeyEnter})
@@ -1527,6 +1526,74 @@ func TestUltraPriorityOpUsesUltraSelection(t *testing.T) {
}
}
+func TestUltraOpenURLHotkeyUsesUltraSelection(t *testing.T) {
+ tmp := t.TempDir()
+ taskPath := filepath.Join(tmp, "task")
+ openLog := filepath.Join(tmp, "open.log")
+ browserPath := filepath.Join(tmp, "browser")
+
+ taskScript := "#!/bin/sh\n" +
+ "if echo \"$@\" | grep -q export; then\n" +
+ " echo '{\"id\":1,\"uuid\":\"1\",\"description\":\"alpha\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n" +
+ " echo '{\"id\":2,\"uuid\":\"2\",\"description\":\"beta https://example.com\",\"status\":\"pending\",\"entry\":\"\",\"priority\":\"\",\"urgency\":0}'\n" +
+ " exit 0\n" +
+ "fi\n"
+ if err := os.WriteFile(taskPath, []byte(taskScript), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ browserScript := "#!/bin/sh\n" +
+ "echo \"$1\" >> " + openLog + "\n"
+ if err := os.WriteFile(browserPath, []byte(browserScript), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ setupEnv(t, taskPath)
+
+ m, err := New(nil, browserPath)
+ if err != nil {
+ t.Fatalf("New: %v", err)
+ }
+
+ m.showUltra = true
+ m.tbl.SetCursor(0)
+ m.ultraCursor = 1
+
+ mv, cmd := (&m).Update(tea.KeyPressMsg{Code: 'o', Text: "o"})
+ if cmd == nil {
+ // Opening a URL starts the blink animation, so a command is expected.
+ t.Fatalf("ultra open URL unexpectedly returned no command")
+ }
+ m = *mv.(*Model)
+
+ data, err := os.ReadFile(openLog)
+ if err != nil {
+ t.Fatalf("read open log: %v", err)
+ }
+ if strings.TrimSpace(string(data)) != "https://example.com" {
+ t.Fatalf("browser not called with ultra-selected url: %q", data)
+ }
+
+ // Clear the blink state so we can test the no-URL path with a second keypress.
+ m.blinkID = 0
+ m.blinkOn = false
+ m.ultraCursor = 0
+
+ mv, cmd = (&m).Update(tea.KeyPressMsg{Code: 'o', Text: "o"})
+ if cmd != nil {
+ t.Fatalf("ultra open URL for task without url unexpectedly returned a command")
+ }
+ m = *mv.(*Model)
+
+ data, err = os.ReadFile(openLog)
+ if err != nil {
+ t.Fatalf("read open log after no-url task: %v", err)
+ }
+ if strings.TrimSpace(string(data)) != "https://example.com" {
+ t.Fatalf("browser was called for task without url: %q", data)
+ }
+}
+
func TestUltraReloadPreservesFilteredSelection(t *testing.T) {
tmp := t.TempDir()
taskPath, phaseFile := setupUltraReloadTaskSet(t, tmp)
diff --git a/internal/ui/ultra.go b/internal/ui/ultra.go
index 5c7b6d1..4bb9542 100644
--- a/internal/ui/ultra.go
+++ b/internal/ui/ultra.go
@@ -91,6 +91,7 @@ func (m Model) ultraHelpSections() []helpSection {
title: "Task Management",
items: []helpItem{
{key: "Enter, e, E", desc: "edit selected task"},
+ {key: "o", desc: "open URL from description"},
{key: "s", desc: "start/stop task"},
{key: "d", desc: "mark task done"},
{key: "U", desc: "undo last done"},
@@ -1059,6 +1060,8 @@ func (m *Model) handleUltraMode(msg tea.KeyPressMsg) (tea.Model, tea.Cmd) {
return m.handleUltraToggleStart()
case "d":
return m.handleUltraMarkDone()
+ case "o":
+ return m.handleOpenURL()
case "p":
return m.handleUltraSetPriority()
case "w":