1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
package askcli
import (
"bytes"
"context"
"fmt"
"io"
"strings"
)
// completionDescriptionMaxLen is the maximum number of characters of a task
// description included in fish shell completion suggestions.
const completionDescriptionMaxLen = 60
func (d *Dispatcher) handleCompleteUUIDs(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
return d.completeTaskSelectors(ctx, args, stdout, stderr, taskCompletionItems)
}
func (d *Dispatcher) handleCompleteAliases(ctx context.Context, args []string, stdout, stderr io.Writer) (int, error) {
return d.completeTaskSelectors(ctx, args, stdout, stderr, taskCompletionAliasItems)
}
// taskCompletionLinesFn builds tab-separated "selector\tdescription" lines for
// completion output (full list vs alias-only for Fish).
type taskCompletionLinesFn func(tasks []TaskExport, aliases map[string]string) []string
func (d *Dispatcher) completeTaskSelectors(ctx context.Context, args []string, stdout, stderr io.Writer, lines taskCompletionLinesFn) (int, error) {
_ = args
var outBuf bytes.Buffer
code, err := d.runner.Run(ctx, []string{"status:pending", "export"}, nil, &outBuf, stderr)
if code != 0 {
return code, err
}
tasks, err := ParseTaskExport(&outBuf)
if err != nil {
fmt.Fprintf(stderr, "error: failed to parse task data: %v\n", err)
return 1, nil
}
aliases, err := ensureTaskAliases(tasks)
if err != nil {
fmt.Fprintf(stderr, "warning: failed to update task alias cache: %v\n", err)
aliases = nil
}
// Each line is "selector\tdescription" so fish shell can show the task
// summary alongside the ID in the autocompletion menu.
for _, item := range lines(tasks, aliases) {
_, _ = io.WriteString(stdout, item+"\n")
}
return 0, nil
}
// taskCompletionItems returns tab-separated "selector\tdescription" strings
// for each task, placing the short alias before the UUID when available.
// Fish shell interprets the tab-separated format to display descriptions.
func taskCompletionItems(tasks []TaskExport, aliases map[string]string) []string {
items := make([]string, 0, len(tasks)*2)
for _, task := range tasks {
if task.UUID == "" {
continue
}
desc := truncateDescription(task.Description, completionDescriptionMaxLen)
if alias := displayTaskAlias(task.UUID, aliases); alias != "" && alias != task.UUID {
items = append(items, alias+"\t"+desc)
}
items = append(items, task.UUID+"\t"+desc)
}
return items
}
// taskCompletionAliasItems returns tab-separated "selector\tdescription" lines
// only for short alias IDs (never the raw UUID). Used by Fish completion via
// `complete-aliases`; non-shell callers still use complete-uuids for full
// selector lists.
func taskCompletionAliasItems(tasks []TaskExport, aliases map[string]string) []string {
items := make([]string, 0, len(tasks))
for _, task := range tasks {
if task.UUID == "" {
continue
}
desc := truncateDescription(task.Description, completionDescriptionMaxLen)
if alias := displayTaskAlias(task.UUID, aliases); alias != "" && alias != task.UUID {
items = append(items, alias+"\t"+desc)
}
}
return items
}
// truncateDescription collapses any embedded newlines to keep the completion
// item on a single line, then shortens s to at most maxLen characters,
// appending "…" when the string is cut, so the completion hint fits on one line.
func truncateDescription(s string, maxLen int) string {
s = oneLineDescription(s)
runes := []rune(s)
if len(runes) <= maxLen {
return s
}
return string(runes[:maxLen]) + "…"
}
// taskCompletionSelectors returns plain selector strings (no descriptions) for
// use in contexts that do not support tab-separated fish completion items.
func taskCompletionSelectors(tasks []TaskExport, aliases map[string]string) []string {
selectors := make([]string, 0, len(tasks)*2)
for _, task := range tasks {
if task.UUID == "" {
continue
}
if alias := displayTaskAlias(task.UUID, aliases); alias != "" && alias != task.UUID {
selectors = append(selectors, alias)
}
selectors = append(selectors, task.UUID)
}
return selectors
}
// completionItemSelector extracts the selector (before the tab) from a
// tab-separated completion item returned by taskCompletionItems.
func completionItemSelector(item string) string {
if idx := strings.IndexByte(item, '\t'); idx >= 0 {
return item[:idx]
}
return item
}
|