diff options
| -rw-r--r-- | README.md | 1 | ||||
| -rw-r--r-- | cmd/tasksamurai/main.go | 3 | ||||
| -rw-r--r-- | internal/ui/table.go | 23 | ||||
| -rw-r--r-- | internal/ui/table_test.go | 73 |
4 files changed, 85 insertions, 15 deletions
@@ -33,6 +33,7 @@ Task Samurai invokes the `task` command to read and modify tasks. The tasks are - `e` or `E`: edit task - `s`: toggle start/stop - `d`: mark task done +- `o`: open URL from description - `U`: undo last done - `D`: set due date - `+`: add task diff --git a/cmd/tasksamurai/main.go b/cmd/tasksamurai/main.go index 2238c82..8329fa1 100644 --- a/cmd/tasksamurai/main.go +++ b/cmd/tasksamurai/main.go @@ -13,6 +13,7 @@ import ( func main() { debugLog := flag.String("debug-log", "", "path to debug log file") + browserCmd := flag.String("browser-cmd", "firefox", "command used to open URLs") flag.Parse() if err := task.SetDebugLog(*debugLog); err != nil { @@ -20,7 +21,7 @@ func main() { os.Exit(1) } - m, err := ui.New(flag.Args()) + m, err := ui.New(flag.Args(), *browserCmd) if err != nil { fmt.Fprintln(os.Stderr, "failed to load tasks:", err) os.Exit(1) diff --git a/internal/ui/table.go b/internal/ui/table.go index 0ba0144..bac9f0b 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -3,6 +3,7 @@ package ui import ( "fmt" "math/rand" + "os/exec" "regexp" "strconv" "strings" @@ -21,6 +22,8 @@ import ( var priorityOptions = []string{"H", "M", "L", ""} +var urlRegex = regexp.MustCompile(`https?://\S+`) + func init() { rand.Seed(time.Now().UnixNano()) } @@ -79,6 +82,8 @@ type Model struct { undoStack []string + browserCmd string + editID int blinkID int @@ -150,8 +155,8 @@ func (m *Model) startBlink(id int, markDone bool) tea.Cmd { } // New creates a new UI model with the provided rows. -func New(filters []string) (Model, error) { - m := Model{filters: filters} +func New(filters []string, browserCmd string) (Model, error) { + m := Model{filters: filters, browserCmd: browserCmd} m.annotateInput = textinput.New() m.annotateInput.Prompt = "annotation: " m.descInput = textinput.New() @@ -625,6 +630,19 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, m.startBlink(id, true) } } + case "o": + if row := m.tbl.SelectedRow(); row != nil { + desc := m.tasks[m.tbl.Cursor()].Description + re := regexp.MustCompile(`https?://\S+`) + url := re.FindString(desc) + if url != "" { + _ = exec.Command(m.browserCmd, url).Run() + idStr := ansi.Strip(row[1]) + if id, err := strconv.Atoi(idStr); err == nil { + return m, m.startBlink(id, false) + } + } + } case "U": if n := len(m.undoStack); n > 0 { uuid := m.undoStack[n-1] @@ -874,6 +892,7 @@ func (m Model) View() string { "+: add task", "s: toggle start/stop", "d: mark task done", + "o: open URL", "U: undo done", "D: set due date", "r: random due date", diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 365180c..32891bb 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -41,7 +41,7 @@ func TestAnnotateHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -97,7 +97,7 @@ func TestReplaceAnnotationHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -157,7 +157,7 @@ func TestDoneHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -206,7 +206,7 @@ func TestUndoHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -237,6 +237,55 @@ func TestUndoHotkey(t *testing.T) { } } +func TestOpenURLHotkey(t *testing.T) { + tmp := t.TempDir() + taskPath := filepath.Join(tmp, "task") + openFile := filepath.Join(tmp, "open.txt") + browserPath := filepath.Join(tmp, "browser") + + taskScript := "#!/bin/sh\n" + + "if echo \"$@\" | grep -q export; then\n" + + " echo '{\"id\":1,\"uuid\":\"x\",\"description\":\"see 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 > " + openFile + "\n" + if err := os.WriteFile(browserPath, []byte(browserScript), 0o755); err != nil { + t.Fatal(err) + } + + origPath := os.Getenv("PATH") + os.Setenv("PATH", tmp+":"+origPath) + t.Cleanup(func() { os.Setenv("PATH", origPath) }) + + os.Setenv("TASKDATA", tmp) + os.Setenv("TASKRC", "/dev/null") + t.Cleanup(func() { + os.Unsetenv("TASKDATA") + os.Unsetenv("TASKRC") + }) + + m, err := New(nil, browserPath) + if err != nil { + t.Fatalf("New: %v", err) + } + + mv, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune{'o'}}) + m = mv.(Model) + + data, err := os.ReadFile(openFile) + if err != nil { + t.Fatalf("read open: %v", err) + } + if strings.TrimSpace(string(data)) != "https://example.com" { + t.Fatalf("browser not called with url: %q", data) + } +} + func TestDueDateHotkey(t *testing.T) { tmp := t.TempDir() taskPath := filepath.Join(tmp, "task") @@ -264,7 +313,7 @@ func TestDueDateHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -316,7 +365,7 @@ func TestRandomDueDateHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -371,7 +420,7 @@ func TestRecurrenceHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -422,7 +471,7 @@ func TestPriorityHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -469,7 +518,7 @@ func TestAddHotkey(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -519,7 +568,7 @@ func TestNavigationHotkeys(t *testing.T) { os.Unsetenv("TASKRC") }) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -576,7 +625,7 @@ func TestEscClosesHelp(t *testing.T) { taskPath := setupBasicTask(t, tmp) setupEnv(t, taskPath) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } @@ -599,7 +648,7 @@ func TestSearchExitHotkeys(t *testing.T) { taskPath := setupBasicTask(t, tmp) setupEnv(t, taskPath) - m, err := New(nil) + m, err := New(nil, "firefox") if err != nil { t.Fatalf("New: %v", err) } |
