@soleone/pi-tasks 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +95 -0
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/package.json +46 -0
- package/src/backend/adapters/beads.ts +305 -0
- package/src/backend/adapters/todo-md.ts +446 -0
- package/src/backend/api.ts +40 -0
- package/src/backend/resolver.ts +86 -0
- package/src/controllers/list.ts +208 -0
- package/src/controllers/show.ts +81 -0
- package/src/extension.ts +358 -0
- package/src/index.ts +7 -0
- package/src/lib/task-serialization.ts +45 -0
- package/src/models/list-item.ts +44 -0
- package/src/models/task.ts +117 -0
- package/src/ui/components/blur-editor.ts +78 -0
- package/src/ui/components/keyboard-help.ts +25 -0
- package/src/ui/components/min-height.ts +21 -0
- package/src/ui/pages/list.ts +540 -0
- package/src/ui/pages/show.ts +425 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import { DynamicBorder, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
|
|
2
|
+
import { Container, SelectList, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"
|
|
3
|
+
import { toKebabCase, type Task, type TaskStatus } from "../../models/task.ts"
|
|
4
|
+
import type { TaskUpdate } from "../../backend/api.ts"
|
|
5
|
+
import { DESCRIPTION_PART_SEPARATOR, buildListRowModel, decodeDescription, stripAnsi } from "../../models/list-item.ts"
|
|
6
|
+
import { buildListPrimaryHelpText, buildListSecondaryHelpText, resolveListIntent } from "../../controllers/list.ts"
|
|
7
|
+
import { KEYBOARD_HELP_PADDING_X, formatKeyboardHelp } from "../components/keyboard-help.ts"
|
|
8
|
+
import { MinHeightContainer } from "../components/min-height.ts"
|
|
9
|
+
|
|
10
|
+
const LIST_PAGE_CONTENT_MIN_HEIGHT = 20
|
|
11
|
+
|
|
12
|
+
export interface ListPageConfig {
|
|
13
|
+
title: string
|
|
14
|
+
subtitle?: string
|
|
15
|
+
tasks: Task[]
|
|
16
|
+
allowPriority?: boolean
|
|
17
|
+
allowSearch?: boolean
|
|
18
|
+
filterTerm?: string
|
|
19
|
+
priorities: string[]
|
|
20
|
+
priorityHotkeys?: Record<string, string>
|
|
21
|
+
ctrlQ: string
|
|
22
|
+
cycleStatus: (status: TaskStatus) => TaskStatus
|
|
23
|
+
cycleTaskType: (current: string | undefined) => string
|
|
24
|
+
onUpdateTask: (ref: string, update: TaskUpdate) => Promise<void>
|
|
25
|
+
onWork: (task: Task) => void
|
|
26
|
+
onInsert: (task: Task) => void
|
|
27
|
+
onEdit: (ref: string, task: Task | undefined) => Promise<{ updatedTask: Task | null; closeList: boolean }>
|
|
28
|
+
onCreate: () => Promise<Task | null>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function truncateDescription(desc: string | undefined, maxLines: number): string[] {
|
|
32
|
+
if (!desc || !desc.trim()) return ["(no description)"]
|
|
33
|
+
const allLines = desc.split(/\r?\n/)
|
|
34
|
+
const lines = allLines.slice(0, maxLines)
|
|
35
|
+
if (allLines.length > maxLines) lines.push("...")
|
|
36
|
+
return lines
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function matchesFilter(task: Task, term: string): boolean {
|
|
40
|
+
const lower = term.toLowerCase()
|
|
41
|
+
return (
|
|
42
|
+
task.title.toLowerCase().includes(lower) ||
|
|
43
|
+
(task.description ?? "").toLowerCase().includes(lower) ||
|
|
44
|
+
(task.id ?? "").toLowerCase().includes(lower) ||
|
|
45
|
+
toKebabCase(task.status).includes(lower)
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildHeaderText(
|
|
50
|
+
theme: any,
|
|
51
|
+
title: string,
|
|
52
|
+
subtitle: string | undefined,
|
|
53
|
+
searching: boolean,
|
|
54
|
+
searchBuffer: string,
|
|
55
|
+
filterTerm: string,
|
|
56
|
+
): string {
|
|
57
|
+
if (searching) return theme.fg("muted", theme.bold(`Search: ${searchBuffer}_`))
|
|
58
|
+
if (filterTerm) return theme.fg("muted", theme.bold(`${title} [filter: ${filterTerm}]`))
|
|
59
|
+
|
|
60
|
+
const subtitlePart = subtitle ? theme.fg("dim", ` • ${subtitle}`) : ""
|
|
61
|
+
return `${theme.fg("muted", theme.bold(title))}${subtitlePart}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function showTaskList(ctx: ExtensionCommandContext, config: ListPageConfig): Promise<void> {
|
|
65
|
+
const { title, subtitle, tasks, allowPriority = true, allowSearch = true } = config
|
|
66
|
+
|
|
67
|
+
const displayTasks = [...tasks]
|
|
68
|
+
let filterTerm = config.filterTerm || ""
|
|
69
|
+
let rememberedSelectedRef: string | undefined
|
|
70
|
+
|
|
71
|
+
while (true) {
|
|
72
|
+
const visible = filterTerm
|
|
73
|
+
? displayTasks.filter(i => matchesFilter(i, filterTerm))
|
|
74
|
+
: displayTasks
|
|
75
|
+
|
|
76
|
+
if (visible.length === 0 && filterTerm) {
|
|
77
|
+
ctx.ui.notify(`No matches for "${filterTerm}"`, "warning")
|
|
78
|
+
filterTerm = ""
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const getMaxLabelWidth = () => Math.max(...displayTasks.map(i =>
|
|
83
|
+
stripAnsi(buildListRowModel(i).label).length
|
|
84
|
+
))
|
|
85
|
+
|
|
86
|
+
let selectedRef: string | undefined
|
|
87
|
+
const result = await ctx.ui.custom<"cancel" | "select" | "create">((tui: any, theme: any, _kb: any, done: any) => {
|
|
88
|
+
const container = new Container()
|
|
89
|
+
let searching = false
|
|
90
|
+
let searchBuffer = ""
|
|
91
|
+
let descScroll = 0
|
|
92
|
+
|
|
93
|
+
const headerContainer = new Container()
|
|
94
|
+
const listAreaContainer = new Container()
|
|
95
|
+
const footerContainer = new Container()
|
|
96
|
+
const paddedListAreaContainer = new MinHeightContainer(listAreaContainer, LIST_PAGE_CONTENT_MIN_HEIGHT)
|
|
97
|
+
|
|
98
|
+
container.addChild(headerContainer)
|
|
99
|
+
container.addChild(paddedListAreaContainer)
|
|
100
|
+
container.addChild(footerContainer)
|
|
101
|
+
|
|
102
|
+
const titleText = new Text("", 1, 0)
|
|
103
|
+
|
|
104
|
+
const META_SUMMARY_SEPARATOR = " "
|
|
105
|
+
const accentMarker = "__ACCENT_MARKER__"
|
|
106
|
+
const accentedMarker = theme.fg("accent", accentMarker)
|
|
107
|
+
const markerIndex = accentedMarker.indexOf(accentMarker)
|
|
108
|
+
const accentPrefix = markerIndex >= 0 ? accentedMarker.slice(0, markerIndex) : ""
|
|
109
|
+
const accentSuffix = markerIndex >= 0 ? accentedMarker.slice(markerIndex + accentMarker.length) : "\x1b[0m"
|
|
110
|
+
const applyAccentWithAnsi = (text: string) => {
|
|
111
|
+
const normalized = text.replaceAll(DESCRIPTION_PART_SEPARATOR, META_SUMMARY_SEPARATOR)
|
|
112
|
+
if (!accentPrefix) return theme.fg("accent", normalized)
|
|
113
|
+
return `${accentPrefix}${normalized.replace(/\x1b\[0m/g, `\x1b[0m${accentPrefix}`)}${accentSuffix}`
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const styleDescription = (text: string) => {
|
|
117
|
+
const { meta, summary } = decodeDescription(text)
|
|
118
|
+
if (!summary) return theme.fg("muted", meta)
|
|
119
|
+
return `${theme.fg("muted", meta)}${META_SUMMARY_SEPARATOR}${summary}`
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const getItems = () => {
|
|
123
|
+
const filtered = filterTerm
|
|
124
|
+
? displayTasks.filter(i => matchesFilter(i, filterTerm))
|
|
125
|
+
: displayTasks
|
|
126
|
+
const maxLabelWidth = getMaxLabelWidth()
|
|
127
|
+
return filtered.map((task) => {
|
|
128
|
+
const row = buildListRowModel(task, { maxLabelWidth })
|
|
129
|
+
return {
|
|
130
|
+
value: row.ref,
|
|
131
|
+
label: row.label,
|
|
132
|
+
description: row.description,
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let items = getItems()
|
|
138
|
+
let selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
139
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
140
|
+
selectedText: (t: string) => applyAccentWithAnsi(t),
|
|
141
|
+
description: (t: string) => styleDescription(t),
|
|
142
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
143
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
if (rememberedSelectedRef) {
|
|
147
|
+
const rememberedIndex = items.findIndex(i => i.value === rememberedSelectedRef)
|
|
148
|
+
if (rememberedIndex >= 0) selectList.setSelectedIndex(rememberedIndex)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
selectList.onSelectionChange = () => {
|
|
152
|
+
const selected = selectList.getSelectedItem()
|
|
153
|
+
if (selected) rememberedSelectedRef = selected.value
|
|
154
|
+
updateDescPreview()
|
|
155
|
+
tui.requestRender()
|
|
156
|
+
}
|
|
157
|
+
selectList.onSelect = () => {
|
|
158
|
+
const sel = selectList.getSelectedItem()
|
|
159
|
+
if (sel) {
|
|
160
|
+
selectedRef = sel.value
|
|
161
|
+
rememberedSelectedRef = sel.value
|
|
162
|
+
}
|
|
163
|
+
done("select")
|
|
164
|
+
}
|
|
165
|
+
selectList.onCancel = () => {
|
|
166
|
+
if (filterTerm) {
|
|
167
|
+
filterTerm = ""
|
|
168
|
+
items = getItems()
|
|
169
|
+
selectList = new SelectList(items, Math.min(items.length, 10), selectList.theme)
|
|
170
|
+
selectList.onSelectionChange = selectList.onSelectionChange
|
|
171
|
+
selectList.onSelect = selectList.onSelect
|
|
172
|
+
selectList.onCancel = selectList.onCancel
|
|
173
|
+
container.invalidate()
|
|
174
|
+
tui.requestRender()
|
|
175
|
+
} else {
|
|
176
|
+
done("cancel")
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const renderListArea = () => {
|
|
181
|
+
while (listAreaContainer.children.length > 0) {
|
|
182
|
+
listAreaContainer.removeChild(listAreaContainer.children[0])
|
|
183
|
+
}
|
|
184
|
+
listAreaContainer.addChild(selectList)
|
|
185
|
+
listAreaContainer.addChild(new Spacer(1))
|
|
186
|
+
listAreaContainer.addChild(itemPreviewContainer)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const wrapText = (text: string, width: number, maxLines: number): string[] => {
|
|
190
|
+
const lines: string[] = []
|
|
191
|
+
const safeWidth = Math.max(1, width)
|
|
192
|
+
|
|
193
|
+
if (text.length === 0) return [""]
|
|
194
|
+
|
|
195
|
+
const words = text.split(" ")
|
|
196
|
+
let currentLine = ""
|
|
197
|
+
|
|
198
|
+
const flushLine = () => {
|
|
199
|
+
if (lines.length < maxLines) lines.push(currentLine)
|
|
200
|
+
currentLine = ""
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
for (const word of words) {
|
|
204
|
+
const candidate = currentLine ? `${currentLine} ${word}` : word
|
|
205
|
+
|
|
206
|
+
if (stripAnsi(candidate).length <= safeWidth) {
|
|
207
|
+
currentLine = candidate
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (currentLine) {
|
|
212
|
+
flushLine()
|
|
213
|
+
if (lines.length >= maxLines) break
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let remaining = word
|
|
217
|
+
while (stripAnsi(remaining).length > safeWidth) {
|
|
218
|
+
const chunk = remaining.slice(0, safeWidth)
|
|
219
|
+
if (lines.length < maxLines) lines.push(chunk)
|
|
220
|
+
if (lines.length >= maxLines) break
|
|
221
|
+
remaining = remaining.slice(safeWidth)
|
|
222
|
+
}
|
|
223
|
+
if (lines.length >= maxLines) break
|
|
224
|
+
currentLine = remaining
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (currentLine && lines.length < maxLines) lines.push(currentLine)
|
|
228
|
+
return lines.slice(0, maxLines)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const buildDescText = (descLines: string[], width: number): string => {
|
|
232
|
+
const wrappedLines: string[] = []
|
|
233
|
+
for (const line of descLines) {
|
|
234
|
+
const wrapped = wrapText(line, width, 7 - wrappedLines.length)
|
|
235
|
+
wrappedLines.push(...wrapped)
|
|
236
|
+
if (wrappedLines.length >= 7) break
|
|
237
|
+
}
|
|
238
|
+
while (wrappedLines.length < 7) wrappedLines.push("")
|
|
239
|
+
return wrappedLines.join("\n")
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const previewTitleText = new Text("", 0, 0)
|
|
243
|
+
const descTextComponent = new Text(buildDescText([], 80), 0, 0)
|
|
244
|
+
const itemPreviewContainer = new Container()
|
|
245
|
+
itemPreviewContainer.addChild(previewTitleText)
|
|
246
|
+
itemPreviewContainer.addChild(descTextComponent)
|
|
247
|
+
|
|
248
|
+
let lastWidth = 80
|
|
249
|
+
|
|
250
|
+
const updateDescPreview = () => {
|
|
251
|
+
const selected = selectList.getSelectedItem()
|
|
252
|
+
if (!selected) {
|
|
253
|
+
previewTitleText.setText("")
|
|
254
|
+
descTextComponent.setText(buildDescText([], lastWidth))
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
descScroll = 0
|
|
259
|
+
const task = displayTasks.find(i => i.ref === selected.value)
|
|
260
|
+
if (!task) {
|
|
261
|
+
previewTitleText.setText("")
|
|
262
|
+
descTextComponent.setText(buildDescText([], lastWidth))
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
previewTitleText.setText(theme.fg("accent", theme.bold(task.title)))
|
|
267
|
+
const descLines = truncateDescription(task.description, 100)
|
|
268
|
+
descTextComponent.setText(buildDescText(descLines, lastWidth))
|
|
269
|
+
}
|
|
270
|
+
if (items[0]) updateDescPreview()
|
|
271
|
+
|
|
272
|
+
headerContainer.addChild(new DynamicBorder((s: string) => theme.fg("dim", s)))
|
|
273
|
+
headerContainer.addChild(titleText)
|
|
274
|
+
|
|
275
|
+
const helpText = new Text("", KEYBOARD_HELP_PADDING_X, 0)
|
|
276
|
+
const shortcutsText = new Text(formatKeyboardHelp(theme, buildListSecondaryHelpText()), KEYBOARD_HELP_PADDING_X, 0)
|
|
277
|
+
|
|
278
|
+
footerContainer.addChild(new DynamicBorder((s: string) => theme.fg("dim", s)))
|
|
279
|
+
footerContainer.addChild(helpText)
|
|
280
|
+
footerContainer.addChild(shortcutsText)
|
|
281
|
+
footerContainer.addChild(new DynamicBorder((s: string) => theme.fg("dim", s)))
|
|
282
|
+
|
|
283
|
+
renderListArea()
|
|
284
|
+
|
|
285
|
+
const refreshDisplay = () => {
|
|
286
|
+
titleText.setText(buildHeaderText(theme, title, subtitle, searching, searchBuffer, filterTerm))
|
|
287
|
+
helpText.setText(formatKeyboardHelp(theme, buildListPrimaryHelpText({
|
|
288
|
+
searching,
|
|
289
|
+
filtered: !!filterTerm,
|
|
290
|
+
allowPriority,
|
|
291
|
+
allowSearch,
|
|
292
|
+
ctrlQ: config.ctrlQ,
|
|
293
|
+
priorities: config.priorities,
|
|
294
|
+
priorityHotkeys: config.priorityHotkeys,
|
|
295
|
+
})))
|
|
296
|
+
}
|
|
297
|
+
refreshDisplay()
|
|
298
|
+
|
|
299
|
+
const moveSelection = (delta: number) => {
|
|
300
|
+
if (items.length === 0) return
|
|
301
|
+
const selected = selectList.getSelectedItem()
|
|
302
|
+
const currentIndex = selected ? items.findIndex(i => i.value === selected.value) : 0
|
|
303
|
+
const normalizedIndex = currentIndex >= 0 ? currentIndex : 0
|
|
304
|
+
const nextIndex = (normalizedIndex + delta + items.length) % items.length
|
|
305
|
+
selectList.setSelectedIndex(nextIndex)
|
|
306
|
+
updateDescPreview()
|
|
307
|
+
container.invalidate()
|
|
308
|
+
tui.requestRender()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const getSelectedTask = (): Task | undefined => {
|
|
312
|
+
const selected = selectList.getSelectedItem()
|
|
313
|
+
if (!selected) return undefined
|
|
314
|
+
rememberedSelectedRef = selected.value
|
|
315
|
+
return displayTasks.find(i => i.ref === selected.value)
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const withSelectedTask = (run: (task: Task) => void): void => {
|
|
319
|
+
const task = getSelectedTask()
|
|
320
|
+
if (!task) return
|
|
321
|
+
run(task)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const rebuildAndRender = () => {
|
|
325
|
+
items = getItems()
|
|
326
|
+
const prevSelected = selectList.getSelectedItem()
|
|
327
|
+
|
|
328
|
+
selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
329
|
+
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
330
|
+
selectedText: (t: string) => applyAccentWithAnsi(t),
|
|
331
|
+
description: (t: string) => styleDescription(t),
|
|
332
|
+
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
333
|
+
noMatch: (t: string) => theme.fg("warning", t),
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
selectList.onSelectionChange = () => {
|
|
337
|
+
const selected = selectList.getSelectedItem()
|
|
338
|
+
if (selected) rememberedSelectedRef = selected.value
|
|
339
|
+
updateDescPreview()
|
|
340
|
+
tui.requestRender()
|
|
341
|
+
}
|
|
342
|
+
selectList.onSelect = () => {
|
|
343
|
+
const sel = selectList.getSelectedItem()
|
|
344
|
+
if (sel) {
|
|
345
|
+
selectedRef = sel.value
|
|
346
|
+
rememberedSelectedRef = sel.value
|
|
347
|
+
}
|
|
348
|
+
done("select")
|
|
349
|
+
}
|
|
350
|
+
selectList.onCancel = () => {
|
|
351
|
+
if (filterTerm) {
|
|
352
|
+
filterTerm = ""
|
|
353
|
+
rebuildAndRender()
|
|
354
|
+
} else {
|
|
355
|
+
done("cancel")
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
renderListArea()
|
|
360
|
+
|
|
361
|
+
if (prevSelected) {
|
|
362
|
+
const newIdx = items.findIndex(i => i.value === prevSelected.value)
|
|
363
|
+
if (newIdx >= 0) selectList.setSelectedIndex(newIdx)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
refreshDisplay()
|
|
367
|
+
updateDescPreview()
|
|
368
|
+
container.invalidate()
|
|
369
|
+
tui.requestRender()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
render: (w: number) => {
|
|
374
|
+
lastWidth = w
|
|
375
|
+
return container.render(w).map((l: string) => truncateToWidth(l, w))
|
|
376
|
+
},
|
|
377
|
+
invalidate: () => container.invalidate(),
|
|
378
|
+
handleInput: (data: string) => {
|
|
379
|
+
const intent = resolveListIntent(data, {
|
|
380
|
+
searching,
|
|
381
|
+
filtered: !!filterTerm,
|
|
382
|
+
allowSearch,
|
|
383
|
+
allowPriority,
|
|
384
|
+
ctrlQ: config.ctrlQ,
|
|
385
|
+
priorities: config.priorities,
|
|
386
|
+
priorityHotkeys: config.priorityHotkeys,
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
switch (intent.type) {
|
|
390
|
+
case "cancel":
|
|
391
|
+
done("cancel")
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
case "searchStart":
|
|
395
|
+
searching = true
|
|
396
|
+
searchBuffer = ""
|
|
397
|
+
refreshDisplay()
|
|
398
|
+
container.invalidate()
|
|
399
|
+
tui.requestRender()
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
case "searchCancel":
|
|
403
|
+
searching = false
|
|
404
|
+
searchBuffer = ""
|
|
405
|
+
refreshDisplay()
|
|
406
|
+
container.invalidate()
|
|
407
|
+
tui.requestRender()
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
case "searchApply":
|
|
411
|
+
filterTerm = searchBuffer.trim()
|
|
412
|
+
searching = false
|
|
413
|
+
rebuildAndRender()
|
|
414
|
+
refreshDisplay()
|
|
415
|
+
return
|
|
416
|
+
|
|
417
|
+
case "searchBackspace":
|
|
418
|
+
searchBuffer = searchBuffer.slice(0, -1)
|
|
419
|
+
refreshDisplay()
|
|
420
|
+
container.invalidate()
|
|
421
|
+
tui.requestRender()
|
|
422
|
+
return
|
|
423
|
+
|
|
424
|
+
case "searchAppend":
|
|
425
|
+
searchBuffer += intent.value
|
|
426
|
+
refreshDisplay()
|
|
427
|
+
container.invalidate()
|
|
428
|
+
tui.requestRender()
|
|
429
|
+
return
|
|
430
|
+
|
|
431
|
+
case "moveSelection":
|
|
432
|
+
moveSelection(intent.delta)
|
|
433
|
+
return
|
|
434
|
+
|
|
435
|
+
case "work":
|
|
436
|
+
withSelectedTask((task) => {
|
|
437
|
+
done("cancel")
|
|
438
|
+
config.onWork(task)
|
|
439
|
+
})
|
|
440
|
+
return
|
|
441
|
+
|
|
442
|
+
case "edit":
|
|
443
|
+
withSelectedTask((task) => {
|
|
444
|
+
selectedRef = task.ref
|
|
445
|
+
done("select")
|
|
446
|
+
})
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
case "toggleStatus":
|
|
450
|
+
withSelectedTask((task) => {
|
|
451
|
+
const newStatus = config.cycleStatus(task.status)
|
|
452
|
+
task.status = newStatus
|
|
453
|
+
void config.onUpdateTask(task.ref, { status: newStatus })
|
|
454
|
+
rebuildAndRender()
|
|
455
|
+
})
|
|
456
|
+
return
|
|
457
|
+
|
|
458
|
+
case "setPriority":
|
|
459
|
+
withSelectedTask((task) => {
|
|
460
|
+
if (task.priority === intent.priority) return
|
|
461
|
+
task.priority = intent.priority
|
|
462
|
+
void config.onUpdateTask(task.ref, { priority: intent.priority })
|
|
463
|
+
rebuildAndRender()
|
|
464
|
+
})
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
case "scrollDescription":
|
|
468
|
+
withSelectedTask((task) => {
|
|
469
|
+
const descLines = truncateDescription(task.description, 100)
|
|
470
|
+
const allWrapped: string[] = []
|
|
471
|
+
for (const line of descLines) {
|
|
472
|
+
const wrapped = wrapText(line, lastWidth, 100)
|
|
473
|
+
allWrapped.push(...wrapped)
|
|
474
|
+
}
|
|
475
|
+
const maxScroll = Math.max(0, allWrapped.length - 7)
|
|
476
|
+
if (intent.delta > 0 && descScroll < maxScroll) {
|
|
477
|
+
descScroll++
|
|
478
|
+
} else if (intent.delta < 0 && descScroll > 0) {
|
|
479
|
+
descScroll--
|
|
480
|
+
}
|
|
481
|
+
const visible = allWrapped.slice(descScroll, descScroll + 7)
|
|
482
|
+
while (visible.length < 7) visible.push("")
|
|
483
|
+
descTextComponent.setText(visible.join("\n"))
|
|
484
|
+
container.invalidate()
|
|
485
|
+
tui.requestRender()
|
|
486
|
+
})
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
case "toggleType":
|
|
490
|
+
withSelectedTask((task) => {
|
|
491
|
+
const newType = config.cycleTaskType(task.taskType)
|
|
492
|
+
task.taskType = newType
|
|
493
|
+
void config.onUpdateTask(task.ref, { taskType: newType })
|
|
494
|
+
rebuildAndRender()
|
|
495
|
+
})
|
|
496
|
+
return
|
|
497
|
+
|
|
498
|
+
case "create":
|
|
499
|
+
done("create")
|
|
500
|
+
return
|
|
501
|
+
|
|
502
|
+
case "insert":
|
|
503
|
+
withSelectedTask((task) => {
|
|
504
|
+
done("cancel")
|
|
505
|
+
config.onInsert(task)
|
|
506
|
+
})
|
|
507
|
+
return
|
|
508
|
+
|
|
509
|
+
case "delegate":
|
|
510
|
+
selectList.handleInput(data)
|
|
511
|
+
tui.requestRender()
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
},
|
|
515
|
+
}
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
if (result === "cancel") return
|
|
519
|
+
|
|
520
|
+
if (result === "create") {
|
|
521
|
+
const createdTask = await config.onCreate()
|
|
522
|
+
if (createdTask) {
|
|
523
|
+
displayTasks.unshift(createdTask)
|
|
524
|
+
rememberedSelectedRef = createdTask.ref
|
|
525
|
+
}
|
|
526
|
+
continue
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (result === "select" && selectedRef) {
|
|
530
|
+
rememberedSelectedRef = selectedRef
|
|
531
|
+
const currentTask = displayTasks.find(i => i.ref === selectedRef)
|
|
532
|
+
const editResult = await config.onEdit(selectedRef, currentTask)
|
|
533
|
+
if (editResult.updatedTask) {
|
|
534
|
+
const idx = displayTasks.findIndex(i => i.ref === selectedRef)
|
|
535
|
+
if (idx !== -1) displayTasks[idx] = editResult.updatedTask
|
|
536
|
+
}
|
|
537
|
+
if (editResult.closeList) return
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|