@luan.sh/pi-libtui 0.3.8

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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +208 -0
  3. package/native-revision +1 -0
  4. package/package.json +58 -0
  5. package/src/activity-presentation.ts +75 -0
  6. package/src/appearance.ts +632 -0
  7. package/src/background-surface.ts +125 -0
  8. package/src/color/palette.ts +186 -0
  9. package/src/color/preview.ts +157 -0
  10. package/src/color/resolver.ts +75 -0
  11. package/src/color/theme.ts +531 -0
  12. package/src/component-stack.ts +247 -0
  13. package/src/content/markdown-code.ts +60 -0
  14. package/src/content/terminal-text.ts +135 -0
  15. package/src/content/text.ts +105 -0
  16. package/src/controls/action-panel.ts +420 -0
  17. package/src/controls/dialog-button-bar.ts +287 -0
  18. package/src/controls/multi-select.ts +467 -0
  19. package/src/controls/picker-panel.ts +447 -0
  20. package/src/controls/screen-icon-actions.ts +134 -0
  21. package/src/controls/searchable-select.ts +394 -0
  22. package/src/controls/select-box.ts +224 -0
  23. package/src/controls/selectable-list.ts +433 -0
  24. package/src/controls/selection-action-bar.ts +382 -0
  25. package/src/controls/semantic-input.ts +23 -0
  26. package/src/controls/tab.ts +189 -0
  27. package/src/cursor.ts +288 -0
  28. package/src/decoration/editor-pills.ts +156 -0
  29. package/src/decoration/glyphs.ts +326 -0
  30. package/src/decoration/pointer-interaction.ts +123 -0
  31. package/src/decoration/powerline-pill.ts +104 -0
  32. package/src/decoration/status.ts +185 -0
  33. package/src/decoration/transient-pill.ts +176 -0
  34. package/src/diff/index.ts +21 -0
  35. package/src/diff/model.ts +76 -0
  36. package/src/diff/parse.ts +310 -0
  37. package/src/diff/pierre-syntax.ts +86 -0
  38. package/src/diff/render.ts +504 -0
  39. package/src/diff/structured.ts +264 -0
  40. package/src/diff/view.ts +75 -0
  41. package/src/editor/chrome.ts +341 -0
  42. package/src/editor/composition.ts +53 -0
  43. package/src/editor/layout.ts +81 -0
  44. package/src/editor/presentation.ts +35 -0
  45. package/src/editor/protocol.ts +188 -0
  46. package/src/editor.ts +41 -0
  47. package/src/extension.ts +30 -0
  48. package/src/folding.ts +192 -0
  49. package/src/host/cursor-bridge.ts +210 -0
  50. package/src/host/editor-bridge.ts +148 -0
  51. package/src/host/extension-host.ts +231 -0
  52. package/src/host/mouse-bridge.ts +212 -0
  53. package/src/host/mouse-dispatch.ts +400 -0
  54. package/src/host/mouse-input.ts +85 -0
  55. package/src/host/mouse-targets.ts +238 -0
  56. package/src/host/pi-layout-adapter.ts +298 -0
  57. package/src/host/pi-native-selection.ts +227 -0
  58. package/src/host/split-pane-bridge.ts +720 -0
  59. package/src/host/transcript-bridge.ts +236 -0
  60. package/src/index.ts +334 -0
  61. package/src/line-layout.ts +6 -0
  62. package/src/motion.ts +1409 -0
  63. package/src/mouse/events.ts +55 -0
  64. package/src/mouse/fullscreen-layout.ts +260 -0
  65. package/src/mouse/registry.ts +348 -0
  66. package/src/mouse.ts +37 -0
  67. package/src/native-binary.ts +189 -0
  68. package/src/overlay/anchored.ts +91 -0
  69. package/src/overlay/detail-card.ts +158 -0
  70. package/src/overlay/dialog.ts +266 -0
  71. package/src/overlay/floating.ts +106 -0
  72. package/src/overlay/framed-editor.ts +236 -0
  73. package/src/overlay/fullscreen.ts +170 -0
  74. package/src/overlay/hover-tooltip.ts +109 -0
  75. package/src/overlay/modal-mount.ts +118 -0
  76. package/src/panels.ts +207 -0
  77. package/src/render-cache.ts +80 -0
  78. package/src/render-epoch.ts +48 -0
  79. package/src/request-animation.ts +160 -0
  80. package/src/scrollbar.ts +27 -0
  81. package/src/selection.ts +148 -0
  82. package/src/split-pane.ts +232 -0
  83. package/src/status-presentation.ts +411 -0
  84. package/src/stream.ts +543 -0
  85. package/src/syntax.ts +316 -0
  86. package/src/terminal/bridge-client.ts +243 -0
  87. package/src/terminal/embedding.ts +6 -0
  88. package/src/terminal/output.ts +151 -0
  89. package/src/terminal/projection.ts +362 -0
  90. package/src/terminal/pty-host.ts +263 -0
  91. package/src/terminal/pty-pane.ts +308 -0
  92. package/src/terminal-colors.ts +334 -0
  93. package/src/tool/action.ts +170 -0
  94. package/src/tool/activity.ts +500 -0
  95. package/src/tool/call-preview.ts +44 -0
  96. package/src/tool/disclosure-action.ts +89 -0
  97. package/src/tool/expanded-region-viewport.ts +103 -0
  98. package/src/tool/index.ts +26 -0
  99. package/src/tool/output.ts +271 -0
  100. package/src/tool/transcript.ts +150 -0
  101. package/src/tool/view-region.ts +581 -0
  102. package/test/appearance.test.ts +183 -0
  103. package/test/bounded-stream.test.ts +150 -0
  104. package/test/bridge-client.test.ts +42 -0
  105. package/test/color-preview.test.ts +23 -0
  106. package/test/component-stack.test.ts +206 -0
  107. package/test/components.test.ts +1165 -0
  108. package/test/content-text.test.ts +21 -0
  109. package/test/cursor-bridge.test.ts +151 -0
  110. package/test/cursor.test.ts +147 -0
  111. package/test/editor-chrome.test.ts +119 -0
  112. package/test/editor-layout.test.ts +30 -0
  113. package/test/editor-registry.test.ts +203 -0
  114. package/test/editor-theme.test.ts +33 -0
  115. package/test/editor-token-pill.test.ts +72 -0
  116. package/test/floating-overlay.test.ts +57 -0
  117. package/test/folding.test.ts +147 -0
  118. package/test/harmonious-theme.test.ts +23 -0
  119. package/test/interaction.test.ts +104 -0
  120. package/test/key-icons.test.ts +62 -0
  121. package/test/markdown-code.test.ts +24 -0
  122. package/test/modal-overlay-mount.test.ts +94 -0
  123. package/test/motion.test.ts +649 -0
  124. package/test/mouse/bridge.test.ts +1302 -0
  125. package/test/mouse/extension.test.ts +205 -0
  126. package/test/mouse/registry.test.ts +140 -0
  127. package/test/mouse/screen-decoration.test.ts +167 -0
  128. package/test/native-binary.test.ts +52 -0
  129. package/test/palette.test.ts +32 -0
  130. package/test/panels.test.ts +74 -0
  131. package/test/pointer-components.test.ts +151 -0
  132. package/test/progress.test.ts +54 -0
  133. package/test/pty-host.test.ts +126 -0
  134. package/test/pty-pane.test.ts +62 -0
  135. package/test/public-surface.test.ts +75 -0
  136. package/test/render-cache.test.ts +25 -0
  137. package/test/render-epoch.test.ts +119 -0
  138. package/test/request-animation.test.ts +193 -0
  139. package/test/screen-icon-actions.test.ts +69 -0
  140. package/test/select-box.test.ts +86 -0
  141. package/test/select-pointer.test.ts +369 -0
  142. package/test/selectable-list.test.ts +258 -0
  143. package/test/selection/registry.test.ts +44 -0
  144. package/test/selection-action-bar.test.ts +204 -0
  145. package/test/split-pane-bridge.test.ts +944 -0
  146. package/test/split-pane.test.ts +134 -0
  147. package/test/status-presentation.test.ts +39 -0
  148. package/test/syntax.test.ts +59 -0
  149. package/test/terminal-colors.test.ts +115 -0
  150. package/test/terminal-embedding.test.ts +12 -0
  151. package/test/terminal-projection.test.ts +312 -0
  152. package/test/theme-colors.test.ts +175 -0
  153. package/test/tool-output.test.ts +160 -0
  154. package/test/tool-transcript.test.ts +1190 -0
  155. package/test/unified-diff.test.ts +691 -0
  156. package/themes/harmonious.json +61 -0
  157. package/tsconfig.json +16 -0
@@ -0,0 +1,287 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { type Component, type KeyId, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { getTuiAppearance } from "../appearance.ts";
4
+ import { type TuiBackgroundToken, type TuiForegroundToken, tuiTheme } from "../color/theme.ts";
5
+ import { icon, keyHintGlyph, keyIcon, type TuiIconName } from "../decoration/glyphs.ts";
6
+ import { renderPill } from "../decoration/powerline-pill.ts";
7
+ import type { TuiMouseEvent } from "../mouse.ts";
8
+ import type { ActionPanelMouseEvent, ActionPanelRect } from "./action-panel.ts";
9
+
10
+ /** One action rendered by {@link DialogButtonBar}. */
11
+ export interface DialogButtonSpec<Value extends string = string> {
12
+ /** Stable value passed to the activation callback. */
13
+ value: Value;
14
+ /** Static label or lazy label read on each render. */
15
+ label: string | (() => string);
16
+ /** Semantic icon resolved from the active pi-libtui appearance pack. */
17
+ icon?: TuiIconName;
18
+ /** Semantic foreground token for the label and icon. */
19
+ foreground: TuiForegroundToken;
20
+ /** Semantic background token for the resting button surface. */
21
+ background: TuiBackgroundToken;
22
+ /** User-configured shortcuts; the first is displayed and every shortcut activates. */
23
+ shortcuts?: readonly KeyId[];
24
+ /** Start/end buttons grow from their edge; center buttons share one centered group. */
25
+ align?: "start" | "center" | "end";
26
+ }
27
+
28
+ /** Interactive geometry and identity for one rendered dialog button. */
29
+ export interface DialogButtonGeometry<Value extends string = string> extends ActionPanelRect {
30
+ /** Zero-based index in the configured button list. */
31
+ index: number;
32
+ /** Stable value associated with the button. */
33
+ value: Value;
34
+ }
35
+
36
+ /** Geometry for the most recent dialog-button-bar render. */
37
+ export interface DialogButtonBarGeometry<Value extends string = string> extends ActionPanelRect {
38
+ /** Interactive geometry for each button that fit in the row. */
39
+ buttons: readonly DialogButtonGeometry<Value>[];
40
+ }
41
+
42
+ /** Construction options for {@link DialogButtonBar}. */
43
+ export interface DialogButtonBarOptions<Value extends string> {
44
+ /** Active Pi theme used to derive semantic libtui colors. */
45
+ theme: Theme;
46
+ /** Buttons in source order; alignment determines their rendered edge. */
47
+ buttons: readonly DialogButtonSpec<Value>[];
48
+ /** Gap between buttons in terminal cells; defaults to one. */
49
+ gap?: number;
50
+ /** Optional content fitted into the space before the end-aligned actions. */
51
+ leading?: (width: number) => string;
52
+ /** Request a host render after hover or press state changes. */
53
+ requestRender(): void;
54
+ /** Receive the stable value of a clicked button or matched shortcut. */
55
+ onActivate(value: Value): void;
56
+ }
57
+
58
+ /** A domain-free, one-row dialog action bar with structural pointer handling. */
59
+ export class DialogButtonBar<Value extends string = string> implements Component {
60
+ private hoverIndex: number | undefined;
61
+ private pressedIndex: number | undefined;
62
+ private geometry: DialogButtonBarGeometry<Value> | undefined;
63
+
64
+ /**
65
+ * Create a dialog action row.
66
+ * @param config Theme, actions, layout options, and activation callback.
67
+ */
68
+ constructor(private readonly config: DialogButtonBarOptions<Value>) {}
69
+
70
+ /**
71
+ * Match and activate a configured keyboard shortcut.
72
+ * @param data Raw terminal input from Pi.
73
+ * @returns True when a button shortcut consumed the input.
74
+ */
75
+ handleInput(data: string): boolean {
76
+ const index = this.config.buttons.findIndex((button) => button.shortcuts?.some((key) => matchesKey(data, key)));
77
+ if (index < 0) return false;
78
+ this.activate(index);
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Handle pointer input expressed in button-bar-local coordinates.
84
+ * @param event Pointer lifecycle event to route to buttons.
85
+ * @returns True when the pointer is over a rendered button.
86
+ */
87
+ handleMouse(event: ActionPanelMouseEvent): boolean {
88
+ if (event.type === "leave") {
89
+ const changed = this.hoverIndex !== undefined || this.pressedIndex !== undefined;
90
+ this.hoverIndex = undefined;
91
+ this.pressedIndex = undefined;
92
+ if (changed) this.config.requestRender();
93
+ return false;
94
+ }
95
+
96
+ const geometry = this.geometry;
97
+ if (!geometry) return false;
98
+ const button = geometry.buttons.find(
99
+ (candidate) =>
100
+ event.col >= candidate.x &&
101
+ event.col < candidate.x + candidate.width &&
102
+ event.row >= candidate.y &&
103
+ event.row < candidate.y + candidate.height,
104
+ );
105
+ this.updateHover(button?.index);
106
+
107
+ if (event.type === "press" && (event.button === undefined || event.button === 0)) {
108
+ this.pressedIndex = button?.index;
109
+ } else if (event.type === "release" && (event.button === undefined || event.button === 0)) {
110
+ const pressed = this.pressedIndex;
111
+ this.pressedIndex = undefined;
112
+ if (button && pressed === button.index) this.activate(button.index);
113
+ }
114
+ return button !== undefined;
115
+ }
116
+
117
+ /**
118
+ * Structural pointer entry point used by pi-libtui's shared component host.
119
+ * @param event Component-local pointer event from the shared host.
120
+ * @returns True when a supported event targets a rendered button.
121
+ */
122
+ onMouse(event: TuiMouseEvent): boolean {
123
+ if (event.type === "drag" || event.type === "wheel") return false;
124
+ return this.handleMouse({
125
+ type: event.type,
126
+ row: event.row,
127
+ col: event.col,
128
+ button: event.button,
129
+ });
130
+ }
131
+
132
+ /** @returns A defensive copy of the latest button geometry, if rendered. */
133
+ getGeometry(): DialogButtonBarGeometry<Value> | undefined {
134
+ if (!this.geometry) return undefined;
135
+ return {
136
+ ...this.geometry,
137
+ buttons: this.geometry.buttons.map((button) => ({ ...button })),
138
+ };
139
+ }
140
+
141
+ /** Clear cached button geometry after external state changes. */
142
+ invalidate(): void {
143
+ this.geometry = undefined;
144
+ }
145
+
146
+ /**
147
+ * Render one action row and update its interactive geometry.
148
+ * @param width Available width in terminal cells.
149
+ * @returns One rendered row, or no rows when width is zero.
150
+ */
151
+ render(width: number): string[] {
152
+ const boundedWidth = Math.max(0, Math.floor(width));
153
+ if (boundedWidth === 0) {
154
+ this.geometry = undefined;
155
+ return [];
156
+ }
157
+ const gap = Math.max(0, Math.floor(this.config.gap ?? 1));
158
+ const cells = Array.from({ length: boundedWidth }, () => " ");
159
+ const geometry: DialogButtonGeometry<Value>[] = [];
160
+ let startX = 0;
161
+ let endX = boundedWidth;
162
+
163
+ for (const [index, button] of this.config.buttons.entries()) {
164
+ if ((button.align ?? "end") !== "start") continue;
165
+ const label = ` ${plainLabel(button)} `;
166
+ const labelWidth = visibleWidth(label);
167
+ const buttonWidth = buttonWidthFor(labelWidth);
168
+ if (startX + buttonWidth > endX) continue;
169
+ cells[startX] = this.renderButton(button, index, fitLabel(label, labelWidth));
170
+ for (let offset = 1; offset < buttonWidth; offset += 1) cells[startX + offset] = "";
171
+ geometry.push({
172
+ x: startX,
173
+ y: 0,
174
+ width: buttonWidth,
175
+ height: 1,
176
+ index,
177
+ value: button.value,
178
+ });
179
+ startX += buttonWidth + gap;
180
+ }
181
+
182
+ for (const [index, button] of [...this.config.buttons.entries()].reverse()) {
183
+ if ((button.align ?? "end") !== "end") continue;
184
+ const label = ` ${plainLabel(button)} `;
185
+ const labelWidth = visibleWidth(label);
186
+ const buttonWidth = buttonWidthFor(labelWidth);
187
+ const x = endX - buttonWidth;
188
+ if (x < startX) continue;
189
+ cells[x] = this.renderButton(button, index, fitLabel(label, labelWidth));
190
+ for (let offset = 1; offset < buttonWidth; offset += 1) cells[x + offset] = "";
191
+ geometry.push({
192
+ x,
193
+ y: 0,
194
+ width: buttonWidth,
195
+ height: 1,
196
+ index,
197
+ value: button.value,
198
+ });
199
+ endX = x - gap;
200
+ }
201
+
202
+ const centered = [...this.config.buttons.entries()].filter(([, button]) => button.align === "center");
203
+ const centeredWidths = centered.map(([, button]) => buttonWidthFor(visibleWidth(` ${plainLabel(button)} `)));
204
+ const centeredWidth =
205
+ centeredWidths.reduce((total, width) => total + width, 0) + gap * Math.max(0, centered.length - 1);
206
+ let centerX = Math.max(startX, Math.min(Math.floor((boundedWidth - centeredWidth) / 2), endX - centeredWidth));
207
+ for (const [[index, button], buttonWidth] of centered.map(
208
+ (entry, index) => [entry, centeredWidths[index]!] as const,
209
+ )) {
210
+ if (centerX + buttonWidth > endX) break;
211
+ const label = ` ${plainLabel(button)} `;
212
+ const labelWidth = visibleWidth(label);
213
+ cells[centerX] = this.renderButton(button, index, fitLabel(label, labelWidth));
214
+ for (let offset = 1; offset < buttonWidth; offset += 1) cells[centerX + offset] = "";
215
+ geometry.push({ x: centerX, y: 0, width: buttonWidth, height: 1, index, value: button.value });
216
+ centerX += buttonWidth + gap;
217
+ }
218
+
219
+ const leadingWidth = Math.max(0, endX - startX);
220
+ if (this.config.leading && leadingWidth > 0 && centered.length === 0) {
221
+ const content = truncateToWidth(this.config.leading(leadingWidth), leadingWidth, "");
222
+ cells[startX] = content;
223
+ for (let offset = 1; offset < visibleWidth(content); offset += 1) cells[startX + offset] = "";
224
+ }
225
+
226
+ geometry.sort((left, right) => left.x - right.x);
227
+ this.geometry = {
228
+ x: 0,
229
+ y: 0,
230
+ width: boundedWidth,
231
+ height: 1,
232
+ buttons: geometry,
233
+ };
234
+ return [cells.join("")];
235
+ }
236
+
237
+ private renderButton(button: DialogButtonSpec<Value>, index: number, label: string): string {
238
+ const colors = tuiTheme(this.config.theme);
239
+ const hovered = index === this.hoverIndex || index === this.pressedIndex;
240
+ const background = hovered ? colors.contrastBackground(colors.color(button.background)) : button.background;
241
+ const shortcut = button.shortcuts?.[0];
242
+ const renderedLabel = shortcut ? label.replace(shortcutSuffix(shortcut), keyHintGlyph(shortcut)) : label;
243
+ if (getTuiAppearance().powerlineButtons) {
244
+ return renderPill(
245
+ this.config.theme,
246
+ { icon: false, label: renderedLabel },
247
+ background,
248
+ button.foreground,
249
+ undefined,
250
+ undefined,
251
+ true,
252
+ );
253
+ }
254
+ const foreground = colors.fg(button.foreground, renderedLabel);
255
+ return colors.bg(background, foreground);
256
+ }
257
+
258
+ private updateHover(index: number | undefined): void {
259
+ if (this.hoverIndex === index) return;
260
+ this.hoverIndex = index;
261
+ this.config.requestRender();
262
+ }
263
+
264
+ private activate(index: number): void {
265
+ const button = this.config.buttons[index];
266
+ if (button) this.config.onActivate(button.value);
267
+ }
268
+ }
269
+
270
+ function fitLabel(label: string, width: number): string {
271
+ return truncateToWidth(label, width, "") + " ".repeat(Math.max(0, width - visibleWidth(label)));
272
+ }
273
+
274
+ function buttonWidthFor(labelWidth: number): number {
275
+ return labelWidth + (getTuiAppearance().powerlineButtons ? 2 : 0);
276
+ }
277
+
278
+ function plainLabel(button: DialogButtonSpec): string {
279
+ const label = typeof button.label === "function" ? button.label() : button.label;
280
+ const content = button.icon ? `${icon(button.icon)} ${label}` : label;
281
+ const shortcut = button.shortcuts?.[0];
282
+ return shortcut ? `${content} ${shortcutSuffix(shortcut)}` : content;
283
+ }
284
+
285
+ function shortcutSuffix(shortcut: KeyId): string {
286
+ return keyIcon(shortcut);
287
+ }