@soleone/pi-tasks 0.2.2 → 0.3.0
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/package.json
CHANGED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { Key, matchesKey, truncateToWidth, type Component, type SelectItem, type SelectListTheme } from "@mariozechner/pi-tui"
|
|
2
|
+
|
|
3
|
+
// Local variant of pi-tui SelectList with configurable value/description column layout.
|
|
4
|
+
|
|
5
|
+
const normalizeToSingleLine = (text: string): string => text.replace(/[\r\n]+/g, " ").trim()
|
|
6
|
+
|
|
7
|
+
export interface SelectListColumnLayout {
|
|
8
|
+
valueMaxWidth?: number
|
|
9
|
+
valueColumnWidth?: number
|
|
10
|
+
minDescriptionWidth?: number
|
|
11
|
+
minWidthForDescription?: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ResolvedSelectListColumnLayout {
|
|
15
|
+
valueMaxWidth: number
|
|
16
|
+
valueColumnWidth: number
|
|
17
|
+
minDescriptionWidth: number
|
|
18
|
+
minWidthForDescription: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const DEFAULT_COLUMN_LAYOUT: ResolvedSelectListColumnLayout = {
|
|
22
|
+
valueMaxWidth: 30,
|
|
23
|
+
valueColumnWidth: 32,
|
|
24
|
+
minDescriptionWidth: 10,
|
|
25
|
+
minWidthForDescription: 40,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SelectListWithColumns implements Component {
|
|
29
|
+
private items: SelectItem[] = []
|
|
30
|
+
private filteredItems: SelectItem[] = []
|
|
31
|
+
private selectedIndex = 0
|
|
32
|
+
private maxVisible = 5
|
|
33
|
+
private theme: SelectListTheme
|
|
34
|
+
private layout: ResolvedSelectListColumnLayout
|
|
35
|
+
|
|
36
|
+
public onSelect?: (item: SelectItem) => void
|
|
37
|
+
public onCancel?: () => void
|
|
38
|
+
public onSelectionChange?: (item: SelectItem) => void
|
|
39
|
+
|
|
40
|
+
constructor(items: SelectItem[], maxVisible: number, theme: SelectListTheme, layout: SelectListColumnLayout = {}) {
|
|
41
|
+
this.items = items
|
|
42
|
+
this.filteredItems = items
|
|
43
|
+
this.maxVisible = maxVisible
|
|
44
|
+
this.theme = theme
|
|
45
|
+
this.layout = {
|
|
46
|
+
...DEFAULT_COLUMN_LAYOUT,
|
|
47
|
+
...layout,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setSelectedIndex(index: number): void {
|
|
52
|
+
this.selectedIndex = Math.max(0, Math.min(index, this.filteredItems.length - 1))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
invalidate(): void {
|
|
56
|
+
// No cached state.
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
render(width: number): string[] {
|
|
60
|
+
const lines: string[] = []
|
|
61
|
+
|
|
62
|
+
if (this.filteredItems.length === 0) {
|
|
63
|
+
lines.push(this.theme.noMatch(" No matching commands"))
|
|
64
|
+
return lines
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const startIndex = Math.max(
|
|
68
|
+
0,
|
|
69
|
+
Math.min(this.selectedIndex - Math.floor(this.maxVisible / 2), this.filteredItems.length - this.maxVisible),
|
|
70
|
+
)
|
|
71
|
+
const endIndex = Math.min(startIndex + this.maxVisible, this.filteredItems.length)
|
|
72
|
+
|
|
73
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
74
|
+
const item = this.filteredItems[i]
|
|
75
|
+
if (!item) continue
|
|
76
|
+
|
|
77
|
+
const isSelected = i === this.selectedIndex
|
|
78
|
+
const descriptionSingleLine = item.description ? normalizeToSingleLine(item.description) : undefined
|
|
79
|
+
const displayValue = item.label || item.value
|
|
80
|
+
const prefix = isSelected ? "→ " : " "
|
|
81
|
+
|
|
82
|
+
if (!descriptionSingleLine || width <= this.layout.minWidthForDescription) {
|
|
83
|
+
lines.push(this.renderValueOnlyLine(prefix, displayValue, width, isSelected))
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const maxValueWidth = Math.min(this.layout.valueMaxWidth, width - prefix.length - 4)
|
|
88
|
+
const truncatedValue = truncateToWidth(displayValue, maxValueWidth, "")
|
|
89
|
+
const spacing = " ".repeat(Math.max(1, this.layout.valueColumnWidth - truncatedValue.length))
|
|
90
|
+
const descriptionStart = prefix.length + truncatedValue.length + spacing.length
|
|
91
|
+
const descriptionWidth = width - descriptionStart - 2
|
|
92
|
+
|
|
93
|
+
if (descriptionWidth <= this.layout.minDescriptionWidth) {
|
|
94
|
+
lines.push(this.renderValueOnlyLine(prefix, displayValue, width, isSelected))
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const truncatedDesc = truncateToWidth(descriptionSingleLine, descriptionWidth, "")
|
|
99
|
+
if (isSelected) {
|
|
100
|
+
lines.push(this.theme.selectedText(`${prefix}${truncatedValue}${spacing}${truncatedDesc}`))
|
|
101
|
+
} else {
|
|
102
|
+
lines.push(`${prefix}${truncatedValue}${this.theme.description(spacing + truncatedDesc)}`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (startIndex > 0 || endIndex < this.filteredItems.length) {
|
|
107
|
+
const scrollText = ` (${this.selectedIndex + 1}/${this.filteredItems.length})`
|
|
108
|
+
lines.push(this.theme.scrollInfo(truncateToWidth(scrollText, width - 2, "")))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return lines
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
handleInput(keyData: string): void {
|
|
115
|
+
if (matchesKey(keyData, Key.up)) {
|
|
116
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredItems.length - 1 : this.selectedIndex - 1
|
|
117
|
+
this.notifySelectionChange()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (matchesKey(keyData, Key.down)) {
|
|
122
|
+
this.selectedIndex = this.selectedIndex === this.filteredItems.length - 1 ? 0 : this.selectedIndex + 1
|
|
123
|
+
this.notifySelectionChange()
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (matchesKey(keyData, Key.enter)) {
|
|
128
|
+
const selectedItem = this.filteredItems[this.selectedIndex]
|
|
129
|
+
if (selectedItem && this.onSelect) this.onSelect(selectedItem)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (matchesKey(keyData, Key.escape) || keyData === "\u0003") {
|
|
134
|
+
if (this.onCancel) this.onCancel()
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
getSelectedItem(): SelectItem | null {
|
|
139
|
+
const item = this.filteredItems[this.selectedIndex]
|
|
140
|
+
return item || null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private renderValueOnlyLine(prefix: string, displayValue: string, width: number, isSelected: boolean): string {
|
|
144
|
+
const maxWidth = width - prefix.length - 2
|
|
145
|
+
const line = `${prefix}${truncateToWidth(displayValue, maxWidth, "")}`
|
|
146
|
+
return isSelected ? this.theme.selectedText(line) : line
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private notifySelectionChange(): void {
|
|
150
|
+
const selectedItem = this.filteredItems[this.selectedIndex]
|
|
151
|
+
if (selectedItem && this.onSelectionChange) this.onSelectionChange(selectedItem)
|
|
152
|
+
}
|
|
153
|
+
}
|
package/src/ui/pages/list.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import { DynamicBorder, type ExtensionCommandContext } from "@mariozechner/pi-coding-agent"
|
|
2
|
-
import { Container,
|
|
2
|
+
import { Container, Spacer, Text, truncateToWidth } from "@mariozechner/pi-tui"
|
|
3
3
|
import { toKebabCase, type Task, type TaskStatus } from "../../models/task.ts"
|
|
4
4
|
import type { TaskUpdate } from "../../backend/api.ts"
|
|
5
5
|
import { DESCRIPTION_PART_SEPARATOR, buildListRowModel, decodeDescription, stripAnsi } from "../../models/list-item.ts"
|
|
6
6
|
import { buildListPrimaryHelpText, buildListSecondaryHelpText, resolveListIntent } from "../../controllers/list.ts"
|
|
7
7
|
import { KEYBOARD_HELP_PADDING_X, formatKeyboardHelp } from "../components/keyboard-help.ts"
|
|
8
8
|
import { MinHeightContainer } from "../components/min-height.ts"
|
|
9
|
+
import { SelectListWithColumns } from "../components/select-list-with-columns.ts"
|
|
9
10
|
|
|
10
11
|
const LIST_PAGE_CONTENT_MIN_HEIGHT = 20
|
|
12
|
+
const TASK_LIST_ROW_LAYOUT = {
|
|
13
|
+
valueMaxWidth: 60,
|
|
14
|
+
valueColumnWidth: 62,
|
|
15
|
+
}
|
|
11
16
|
|
|
12
17
|
export interface ListPageConfig {
|
|
13
18
|
title: string
|
|
@@ -134,14 +139,16 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
|
|
|
134
139
|
})
|
|
135
140
|
}
|
|
136
141
|
|
|
137
|
-
|
|
138
|
-
let selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
142
|
+
const selectListTheme = {
|
|
139
143
|
selectedPrefix: (t: string) => theme.fg("accent", t),
|
|
140
144
|
selectedText: (t: string) => applyAccentWithAnsi(t),
|
|
141
145
|
description: (t: string) => styleDescription(t),
|
|
142
146
|
scrollInfo: (t: string) => theme.fg("dim", t),
|
|
143
147
|
noMatch: (t: string) => theme.fg("warning", t),
|
|
144
|
-
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let items = getItems()
|
|
151
|
+
let selectList = new SelectListWithColumns(items, Math.min(items.length, 10), selectListTheme, TASK_LIST_ROW_LAYOUT)
|
|
145
152
|
|
|
146
153
|
if (rememberedSelectedRef) {
|
|
147
154
|
const rememberedIndex = items.findIndex(i => i.value === rememberedSelectedRef)
|
|
@@ -165,13 +172,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
|
|
|
165
172
|
selectList.onCancel = () => {
|
|
166
173
|
if (filterTerm) {
|
|
167
174
|
filterTerm = ""
|
|
168
|
-
|
|
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
|
+
rebuildAndRender()
|
|
175
176
|
} else {
|
|
176
177
|
done("cancel")
|
|
177
178
|
}
|
|
@@ -325,13 +326,7 @@ export async function showTaskList(ctx: ExtensionCommandContext, config: ListPag
|
|
|
325
326
|
items = getItems()
|
|
326
327
|
const prevSelected = selectList.getSelectedItem()
|
|
327
328
|
|
|
328
|
-
selectList = new
|
|
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
|
-
})
|
|
329
|
+
selectList = new SelectListWithColumns(items, Math.min(items.length, 10), selectListTheme, TASK_LIST_ROW_LAYOUT)
|
|
335
330
|
|
|
336
331
|
selectList.onSelectionChange = () => {
|
|
337
332
|
const selected = selectList.getSelectedItem()
|