summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Bütow <1224732+snonux@users.noreply.github.com>2025-06-24 15:37:49 +0300
committerGitHub <noreply@github.com>2025-06-24 15:37:49 +0300
commitdbfba814e68bd6b679b2fd77bf10d34336485238 (patch)
treec307059026d5d9d7a81aa97014c570080eda2c51
parentf88fdbbf69710464139bf2bb0b14ca35293df720 (diff)
parentf7275ade1db4b188fa33acf2d3519b3a0c3bada7 (diff)
Merge pull request #91 from snonux/codex/add-u-hotkey-for-url-extraction-and-open
Add URL open hotkey
-rw-r--r--README.md1
-rw-r--r--cmd/tasksamurai/main.go3
-rw-r--r--internal/ui/table.go23
-rw-r--r--internal/ui/table_test.go73
4 files changed, 85 insertions, 15 deletions
diff --git a/README.md b/README.md
index 8a71b5d..808fc2f 100644
--- a/README.md
+++ b/README.md
@@ -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)
}