summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-26 22:07:07 +0200
committerPaul Buetow <paul@buetow.org>2026-03-26 22:07:07 +0200
commitb10c6247e14a3acfe20e78426e131691e205a601 (patch)
tree907bb5b3ffa8239584ae9b7c2bfc34667b095b62 /internal
parentbef3cc7dd95745a5724d3569e45fe7be4aba02ee (diff)
ask: add fish completions for task CLI
Diffstat (limited to 'internal')
-rw-r--r--internal/askcli/completion.go96
-rw-r--r--internal/askcli/completion_test.go46
2 files changed, 142 insertions, 0 deletions
diff --git a/internal/askcli/completion.go b/internal/askcli/completion.go
new file mode 100644
index 0000000..f787849
--- /dev/null
+++ b/internal/askcli/completion.go
@@ -0,0 +1,96 @@
+package askcli
+
+import (
+ "strings"
+)
+
+type fishCompletionItem struct {
+ name string
+ description string
+}
+
+var askRootCompletionItems = []fishCompletionItem{
+ {name: "add", description: "Create a new task"},
+ {name: "list", description: "List active tasks"},
+ {name: "all", description: "List all tasks"},
+ {name: "ready", description: "List READY tasks"},
+ {name: "info", description: "Show task details"},
+ {name: "annotate", description: "Add an annotation"},
+ {name: "start", description: "Start a task"},
+ {name: "stop", description: "Stop a task"},
+ {name: "done", description: "Mark a task complete"},
+ {name: "priority", description: "Set priority"},
+ {name: "tag", description: "Add or remove a tag"},
+ {name: "dep", description: "Manage dependencies"},
+ {name: "urgency", description: "List tasks sorted by urgency"},
+ {name: "modify", description: "Modify task fields"},
+ {name: "denotate", description: "Remove an annotation"},
+ {name: "delete", description: "Delete a task"},
+ {name: "help", description: "Show help"},
+}
+
+var askDepCompletionItems = []fishCompletionItem{
+ {name: "add", description: "Add a dependency"},
+ {name: "rm", description: "Remove a dependency"},
+ {name: "list", description: "List dependencies"},
+}
+
+func FishCompletion() string {
+ var b strings.Builder
+ b.WriteString("# Fish completion for ask.\n")
+ b.WriteString("# Install as ~/.config/fish/completions/ask.fish or")
+ b.WriteString(" $XDG_CONFIG_HOME/fish/completions/ask.fish.\n\n")
+ b.WriteString("function __ask_needs_root_completion\n")
+ b.WriteString(" set -l tokens (commandline -opc)\n")
+ b.WriteString(" if test (count $tokens) -le 1\n")
+ b.WriteString(" return 0\n")
+ b.WriteString(" end\n")
+ b.WriteString(" for token in $tokens[2..-1]\n")
+ b.WriteString(" if not string match -qr '^-' -- $token\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" end\n")
+ b.WriteString(" return 0\n")
+ b.WriteString("end\n\n")
+ b.WriteString("function __ask_in_dep_context\n")
+ b.WriteString(" set -l tokens (commandline -opc)\n")
+ b.WriteString(" if test (count $tokens) -lt 2\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" set -l seen_dep 0\n")
+ b.WriteString(" for token in $tokens[2..-1]\n")
+ b.WriteString(" if string match -qr '^-' -- $token\n")
+ b.WriteString(" continue\n")
+ b.WriteString(" end\n")
+ b.WriteString(" if test $seen_dep -eq 0\n")
+ b.WriteString(" if test $token = dep\n")
+ b.WriteString(" set seen_dep 1\n")
+ b.WriteString(" else\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" else\n")
+ b.WriteString(" return 1\n")
+ b.WriteString(" end\n")
+ b.WriteString(" end\n")
+ b.WriteString(" test $seen_dep -eq 1\n")
+ b.WriteString("end\n\n")
+ b.WriteString("complete -c ask -f\n")
+ b.WriteString("complete -c ask -s j -l json -d 'Emit JSON output'\n")
+ for _, item := range askRootCompletionItems {
+ writeFishCompletionLine(&b, "__ask_needs_root_completion", item)
+ }
+ for _, item := range askDepCompletionItems {
+ writeFishCompletionLine(&b, "__ask_in_dep_context", item)
+ }
+ return b.String()
+}
+
+func writeFishCompletionLine(b *strings.Builder, condition string, item fishCompletionItem) {
+ b.WriteString("complete -c ask -n '")
+ b.WriteString(condition)
+ b.WriteString("' -a '")
+ b.WriteString(item.name)
+ b.WriteString("' -d '")
+ b.WriteString(strings.ReplaceAll(item.description, "'", "\\'"))
+ b.WriteString("'\n")
+}
diff --git a/internal/askcli/completion_test.go b/internal/askcli/completion_test.go
new file mode 100644
index 0000000..e717575
--- /dev/null
+++ b/internal/askcli/completion_test.go
@@ -0,0 +1,46 @@
+package askcli
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+func TestFishCompletion_MatchesAsset(t *testing.T) {
+ _, file, _, ok := runtime.Caller(0)
+ if !ok {
+ t.Fatal("runtime.Caller failed")
+ }
+ assetPath := filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..", "assets", "ask.fish"))
+ want, err := os.ReadFile(assetPath)
+ if err != nil {
+ t.Fatalf("read asset: %v", err)
+ }
+ got := FishCompletion()
+ if got != string(want) {
+ t.Fatalf("fish completion asset mismatch\n--- got ---\n%s\n--- want ---\n%s", got, string(want))
+ }
+}
+
+func TestFishCompletion_IncludesCommandsAndExcludesExport(t *testing.T) {
+ script := FishCompletion()
+ for _, name := range []string{"add", "list", "all", "ready", "info", "annotate", "start", "stop", "done", "priority", "tag", "dep", "urgency", "modify", "denotate", "delete", "help"} {
+ if !strings.Contains(script, " -a '"+name+"' ") {
+ t.Fatalf("script missing root completion for %q", name)
+ }
+ }
+ for _, line := range []string{
+ "complete -c ask -n '__ask_in_dep_context' -a 'add' -d 'Add a dependency'",
+ "complete -c ask -n '__ask_in_dep_context' -a 'rm' -d 'Remove a dependency'",
+ "complete -c ask -n '__ask_in_dep_context' -a 'list' -d 'List dependencies'",
+ } {
+ if !strings.Contains(script, line) {
+ t.Fatalf("script missing dep completion line %q", line)
+ }
+ }
+ if strings.Contains(script, "ask export") {
+ t.Fatalf("script should not advertise non-existent export command")
+ }
+}