summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-27 11:41:41 +0200
committerPaul Buetow <paul@buetow.org>2026-03-27 11:41:41 +0200
commit2d35eeb40eee8beed7163f41afab279bce2976ef (patch)
treefee5de2c646826cba17aab79f8c57c408c96eb71
parentf57ccb885d20c48e1dae65b8ad5c46375b9a11a9 (diff)
Implement command registry for askcli commands
-rw-r--r--internal/askcli/command_complete_uuids.go3
-rw-r--r--internal/askcli/command_complete_uuids_test.go6
-rw-r--r--internal/askcli/command_fish.go4
-rw-r--r--internal/askcli/command_urgency.go3
-rw-r--r--internal/askcli/commands_registry.go207
-rw-r--r--internal/askcli/completion.go41
-rw-r--r--internal/askcli/dispatch.go45
7 files changed, 225 insertions, 84 deletions
diff --git a/internal/askcli/command_complete_uuids.go b/internal/askcli/command_complete_uuids.go
index 3a2ef9e..32af2a5 100644
--- a/internal/askcli/command_complete_uuids.go
+++ b/internal/askcli/command_complete_uuids.go
@@ -7,7 +7,8 @@ import (
"io"
)
-func (d *Dispatcher) handleCompleteUUIDs(ctx context.Context, stdout, stderr io.Writer) (int, error) {
+func (d *Dispatcher) handleCompleteUUIDs(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ _ = args
var outBuf bytes.Buffer
code, err := d.runner.Run(ctx, []string{"status:pending", "export"}, nil, &outBuf, stderr)
if code != 0 {
diff --git a/internal/askcli/command_complete_uuids_test.go b/internal/askcli/command_complete_uuids_test.go
index 4a68e51..94e5693 100644
--- a/internal/askcli/command_complete_uuids_test.go
+++ b/internal/askcli/command_complete_uuids_test.go
@@ -32,7 +32,7 @@ func TestHandleCompleteUUIDs_PrintsPendingUUIDs(t *testing.T) {
}})
var stdout, stderr bytes.Buffer
- code, err := d.handleCompleteUUIDs(context.Background(), &stdout, &stderr)
+ code, err := d.handleCompleteUUIDs(context.Background(), nil, &stdout, &stderr)
if err != nil {
t.Fatalf("handleCompleteUUIDs returned error: %v", err)
}
@@ -66,7 +66,7 @@ func TestHandleCompleteUUIDs_ParseError(t *testing.T) {
}})
var stdout, stderr bytes.Buffer
- code, err := d.handleCompleteUUIDs(context.Background(), &stdout, &stderr)
+ code, err := d.handleCompleteUUIDs(context.Background(), nil, &stdout, &stderr)
if err != nil {
t.Fatalf("handleCompleteUUIDs returned error: %v", err)
}
@@ -101,7 +101,7 @@ func TestHandleCompleteUUIDs_WarnsOnInvalidAliasCache(t *testing.T) {
}})
var stdout, stderr bytes.Buffer
- code, err := d.handleCompleteUUIDs(context.Background(), &stdout, &stderr)
+ code, err := d.handleCompleteUUIDs(context.Background(), nil, &stdout, &stderr)
if err != nil {
t.Fatalf("handleCompleteUUIDs returned error: %v", err)
}
diff --git a/internal/askcli/command_fish.go b/internal/askcli/command_fish.go
index 3d3e709..f4ca9e0 100644
--- a/internal/askcli/command_fish.go
+++ b/internal/askcli/command_fish.go
@@ -1,12 +1,14 @@
package askcli
import (
+ "context"
"fmt"
"io"
"os"
)
-func (d *Dispatcher) handleFish(args []string, stdout, stderr io.Writer) (int, error) {
+func (d *Dispatcher) handleFish(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ _ = ctx
if len(args) != 1 {
fmt.Fprintln(stderr, "usage: ask fish")
return 1, nil
diff --git a/internal/askcli/command_urgency.go b/internal/askcli/command_urgency.go
index 9911f42..bc7d4fd 100644
--- a/internal/askcli/command_urgency.go
+++ b/internal/askcli/command_urgency.go
@@ -8,7 +8,8 @@ import (
"sort"
)
-func (d *Dispatcher) handleUrgency(ctx context.Context, stdout, stderr io.Writer) (int, error) {
+func (d *Dispatcher) handleUrgency(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
+ _ = args
var outBuf bytes.Buffer
code, err := d.runner.Run(ctx, []string{"export"}, nil, &outBuf, stderr)
if code != 0 {
diff --git a/internal/askcli/commands_registry.go b/internal/askcli/commands_registry.go
new file mode 100644
index 0000000..4d8d9ba
--- /dev/null
+++ b/internal/askcli/commands_registry.go
@@ -0,0 +1,207 @@
+package askcli
+
+import (
+ "context"
+ "io"
+)
+
+type commandHandler func(d *Dispatcher, ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
+
+type simpleCommand func(d *Dispatcher, ctx context.Context, args []string, stdout, stderr io.Writer) (int, error)
+
+func wrapSimpleCommand(handler simpleCommand) commandHandler {
+ return func(d *Dispatcher, ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ return handler(d, ctx, args, stdout, stderr)
+ }
+}
+
+type commandEntry struct {
+ name string
+ description string
+ handler commandHandler
+ includeInCompletion bool
+ singleSelector bool
+}
+
+type commandTable struct {
+ entries []commandEntry
+ lookup map[string]int
+}
+
+func newCommandTable(entries []commandEntry) commandTable {
+ lookup := make(map[string]int, len(entries))
+ for i := range entries {
+ entry := entries[i]
+ lookup[entry.name] = i
+ }
+ return commandTable{entries: entries, lookup: lookup}
+}
+
+func (t commandTable) get(name string) (*commandEntry, bool) {
+ idx, ok := t.lookup[name]
+ if !ok {
+ return nil, false
+ }
+ return &t.entries[idx], true
+}
+
+func (t commandTable) rootCompletionEntries() []commandEntry {
+ var entries []commandEntry
+ for _, entry := range t.entries {
+ if entry.includeInCompletion {
+ entries = append(entries, entry)
+ }
+ }
+ return entries
+}
+
+func (t commandTable) singleSelectorNames() []string {
+ var names []string
+ for _, entry := range t.entries {
+ if entry.singleSelector {
+ names = append(names, entry.name)
+ }
+ }
+ return names
+}
+
+func (t *commandTable) add(entry commandEntry) {
+ t.entries = append(t.entries, entry)
+ t.lookup[entry.name] = len(t.entries) - 1
+}
+
+var commandRegistry = newCommandTable([]commandEntry{
+ {
+ name: "add",
+ description: "Create a new task",
+ handler: wrapSimpleCommand((*Dispatcher).handleAdd),
+ includeInCompletion: true,
+ },
+ {
+ name: "list",
+ description: "List active tasks",
+ handler: wrapSimpleCommand((*Dispatcher).handleList),
+ includeInCompletion: true,
+ },
+ {
+ name: "all",
+ description: "List all tasks",
+ handler: wrapSimpleCommand((*Dispatcher).handleAll),
+ includeInCompletion: true,
+ },
+ {
+ name: "ready",
+ description: "List READY tasks",
+ handler: wrapSimpleCommand((*Dispatcher).handleReady),
+ includeInCompletion: true,
+ },
+ {
+ name: "info",
+ description: "Show task details",
+ handler: wrapSimpleCommand((*Dispatcher).handleInfo),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "annotate",
+ description: "Add an annotation",
+ handler: wrapSimpleCommand((*Dispatcher).handleAnnotate),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "start",
+ description: "Start a task",
+ handler: wrapSimpleCommand((*Dispatcher).handleStart),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "stop",
+ description: "Stop a task",
+ handler: wrapSimpleCommand((*Dispatcher).handleStop),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "done",
+ description: "Mark a task complete",
+ handler: wrapSimpleCommand((*Dispatcher).handleDone),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "priority",
+ description: "Set priority",
+ handler: wrapSimpleCommand((*Dispatcher).handlePriority),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "tag",
+ description: "Add or remove a tag",
+ handler: wrapSimpleCommand((*Dispatcher).handleTag),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "modify",
+ description: "Modify task fields",
+ handler: wrapSimpleCommand((*Dispatcher).handleModify),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "denotate",
+ description: "Remove an annotation",
+ handler: wrapSimpleCommand((*Dispatcher).handleDenotate),
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "delete",
+ description: "Delete a task",
+ handler: (*Dispatcher).handleDelete,
+ includeInCompletion: true,
+ singleSelector: true,
+ },
+ {
+ name: "dep",
+ description: "Manage dependencies",
+ handler: wrapSimpleCommand((*Dispatcher).handleDep),
+ includeInCompletion: true,
+ },
+ {
+ name: "urgency",
+ description: "List tasks sorted by urgency",
+ handler: wrapSimpleCommand((*Dispatcher).handleUrgency),
+ includeInCompletion: true,
+ },
+})
+
+func init() {
+ commandRegistry.add(commandEntry{
+ name: "fish",
+ description: "Emit Fish shell completion script",
+ handler: wrapSimpleCommand((*Dispatcher).handleFish),
+ includeInCompletion: true,
+ })
+ commandRegistry.add(commandEntry{
+ name: "help",
+ description: "Show help",
+ handler: func(d *Dispatcher, ctx context.Context, args []string, stdin io.Reader, stdout, stderr io.Writer) (int, error) {
+ _ = ctx
+ _ = args
+ _ = stdin
+ _ = stderr
+ return d.help(stdout)
+ },
+ includeInCompletion: true,
+ })
+ commandRegistry.add(commandEntry{
+ name: "complete-uuids",
+ description: "Emit task selector list",
+ handler: wrapSimpleCommand((*Dispatcher).handleCompleteUUIDs),
+ includeInCompletion: false,
+ })
+}
diff --git a/internal/askcli/completion.go b/internal/askcli/completion.go
index b4d8620..889bbc8 100644
--- a/internal/askcli/completion.go
+++ b/internal/askcli/completion.go
@@ -9,40 +9,6 @@ type fishCompletionItem struct {
description string
}
-var askSingleSelectorCompletionCommands = []string{
- "info",
- "annotate",
- "start",
- "stop",
- "done",
- "priority",
- "tag",
- "modify",
- "denotate",
- "delete",
-}
-
-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: "fish", description: "Emit Fish shell completion script"},
- {name: "help", description: "Show help"},
-}
-
var askDepCompletionItems = []fishCompletionItem{
{name: "add", description: "Add a dependency"},
{name: "rm", description: "Remove a dependency"},
@@ -54,7 +20,7 @@ func fishSingleSelectorCompletionContext(positional []string) bool {
return false
}
- for _, command := range askSingleSelectorCompletionCommands {
+ for _, command := range commandRegistry.singleSelectorNames() {
if positional[0] == command {
return true
}
@@ -99,7 +65,8 @@ func FishCompletionFor(binaryPath string) string {
writeFishAddDependencyModifierFunction(&b)
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 {
+ for _, entry := range commandRegistry.rootCompletionEntries() {
+ item := fishCompletionItem{name: entry.name, description: entry.description}
writeFishCompletionLine(&b, "__ask_needs_root_completion", item)
}
for _, item := range askDepCompletionItems {
@@ -182,7 +149,7 @@ func writeFishUUIDContextFunction(b *strings.Builder) {
b.WriteString(" end\n")
b.WriteString(" switch $positional[1]\n")
b.WriteString(" case ")
- b.WriteString(strings.Join(askSingleSelectorCompletionCommands, " "))
+ b.WriteString(strings.Join(commandRegistry.singleSelectorNames(), " "))
b.WriteString("\n")
b.WriteString(" return 0\n")
b.WriteString(" case '*'\n")
diff --git a/internal/askcli/dispatch.go b/internal/askcli/dispatch.go
index 142f7b4..c2c7aab 100644
--- a/internal/askcli/dispatch.go
+++ b/internal/askcli/dispatch.go
@@ -47,51 +47,14 @@ func (d *Dispatcher) Dispatch(ctx context.Context, args []string, stdin io.Reade
d.jsonOutput = jsonOutput
if len(args) == 0 {
- return d.handleList(ctx, []string{"list"}, stdout, stderr)
+ args = []string{"list"}
}
subcommand := args[0]
- switch subcommand {
- case "info":
- return d.handleInfo(ctx, args, stdout, stderr)
- case "add":
- return d.handleAdd(ctx, args, stdout, stderr)
- case "list":
- return d.handleList(ctx, args, stdout, stderr)
- case "all":
- return d.handleAll(ctx, args, stdout, stderr)
- case "ready":
- return d.handleReady(ctx, args, stdout, stderr)
- case "dep":
- return d.handleDep(ctx, args, stdout, stderr)
- case "urgency":
- return d.handleUrgency(ctx, stdout, stderr)
- case "annotate":
- return d.handleAnnotate(ctx, args, stdout, stderr)
- case "start":
- return d.handleStart(ctx, args, stdout, stderr)
- case "stop":
- return d.handleStop(ctx, args, stdout, stderr)
- case "done":
- return d.handleDone(ctx, args, stdout, stderr)
- case "priority":
- return d.handlePriority(ctx, args, stdout, stderr)
- case "tag":
- return d.handleTag(ctx, args, stdout, stderr)
- case "modify":
- return d.handleModify(ctx, args, stdout, stderr)
- case "denotate":
- return d.handleDenotate(ctx, args, stdout, stderr)
- case "delete":
- return d.handleDelete(ctx, args, stdin, stdout, stderr)
- case "fish":
- return d.handleFish(args, stdout, stderr)
- case "help":
- return d.help(stdout)
- case "complete-uuids":
- return d.handleCompleteUUIDs(ctx, stdout, stderr)
- default:
+ entry, ok := commandRegistry.get(subcommand)
+ if !ok {
return d.unknownCommand(stderr, subcommand)
}
+ return entry.handler(d, ctx, args, stdin, stdout, stderr)
}
func (d *Dispatcher) help(w io.Writer) (int, error) {