pi-libtui 0.3.2
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/LICENSE +21 -0
- package/README.md +205 -0
- package/package.json +55 -0
- package/src/activity-presentation.ts +75 -0
- package/src/appearance.ts +632 -0
- package/src/background-surface.ts +125 -0
- package/src/color/palette.ts +186 -0
- package/src/color/preview.ts +157 -0
- package/src/color/resolver.ts +75 -0
- package/src/color/theme.ts +531 -0
- package/src/component-stack.ts +247 -0
- package/src/content/markdown-code.ts +60 -0
- package/src/content/terminal-text.ts +135 -0
- package/src/content/text.ts +105 -0
- package/src/controls/action-panel.ts +420 -0
- package/src/controls/dialog-button-bar.ts +287 -0
- package/src/controls/multi-select.ts +467 -0
- package/src/controls/picker-panel.ts +447 -0
- package/src/controls/screen-icon-actions.ts +134 -0
- package/src/controls/searchable-select.ts +394 -0
- package/src/controls/select-box.ts +224 -0
- package/src/controls/selectable-list.ts +433 -0
- package/src/controls/selection-action-bar.ts +382 -0
- package/src/controls/semantic-input.ts +23 -0
- package/src/controls/tab.ts +189 -0
- package/src/cursor.ts +288 -0
- package/src/decoration/editor-pills.ts +156 -0
- package/src/decoration/glyphs.ts +326 -0
- package/src/decoration/pointer-interaction.ts +123 -0
- package/src/decoration/powerline-pill.ts +104 -0
- package/src/decoration/status.ts +185 -0
- package/src/decoration/transient-pill.ts +176 -0
- package/src/diff/index.ts +21 -0
- package/src/diff/model.ts +76 -0
- package/src/diff/parse.ts +310 -0
- package/src/diff/pierre-syntax.ts +86 -0
- package/src/diff/render.ts +504 -0
- package/src/diff/structured.ts +264 -0
- package/src/diff/view.ts +75 -0
- package/src/editor/chrome.ts +341 -0
- package/src/editor/composition.ts +53 -0
- package/src/editor/layout.ts +81 -0
- package/src/editor/presentation.ts +35 -0
- package/src/editor/protocol.ts +188 -0
- package/src/editor.ts +41 -0
- package/src/extension.ts +30 -0
- package/src/folding.ts +192 -0
- package/src/host/cursor-bridge.ts +210 -0
- package/src/host/editor-bridge.ts +148 -0
- package/src/host/extension-host.ts +231 -0
- package/src/host/mouse-bridge.ts +212 -0
- package/src/host/mouse-dispatch.ts +400 -0
- package/src/host/mouse-input.ts +85 -0
- package/src/host/mouse-targets.ts +238 -0
- package/src/host/pi-layout-adapter.ts +298 -0
- package/src/host/pi-native-selection.ts +227 -0
- package/src/host/split-pane-bridge.ts +720 -0
- package/src/host/transcript-bridge.ts +236 -0
- package/src/index.ts +334 -0
- package/src/line-layout.ts +6 -0
- package/src/motion.ts +1409 -0
- package/src/mouse/events.ts +55 -0
- package/src/mouse/fullscreen-layout.ts +260 -0
- package/src/mouse/registry.ts +348 -0
- package/src/mouse.ts +37 -0
- package/src/native-binary.ts +189 -0
- package/src/overlay/anchored.ts +91 -0
- package/src/overlay/detail-card.ts +158 -0
- package/src/overlay/dialog.ts +266 -0
- package/src/overlay/floating.ts +106 -0
- package/src/overlay/framed-editor.ts +236 -0
- package/src/overlay/fullscreen.ts +170 -0
- package/src/overlay/hover-tooltip.ts +109 -0
- package/src/overlay/modal-mount.ts +118 -0
- package/src/panels.ts +207 -0
- package/src/render-cache.ts +80 -0
- package/src/render-epoch.ts +48 -0
- package/src/request-animation.ts +160 -0
- package/src/scrollbar.ts +27 -0
- package/src/selection.ts +148 -0
- package/src/split-pane.ts +232 -0
- package/src/status-presentation.ts +411 -0
- package/src/stream.ts +543 -0
- package/src/syntax.ts +316 -0
- package/src/terminal/bridge-client.ts +243 -0
- package/src/terminal/embedding.ts +6 -0
- package/src/terminal/output.ts +151 -0
- package/src/terminal/projection.ts +362 -0
- package/src/terminal/pty-host.ts +263 -0
- package/src/terminal/pty-pane.ts +308 -0
- package/src/terminal-colors.ts +334 -0
- package/src/tool/action.ts +170 -0
- package/src/tool/activity.ts +500 -0
- package/src/tool/call-preview.ts +44 -0
- package/src/tool/disclosure-action.ts +89 -0
- package/src/tool/expanded-region-viewport.ts +103 -0
- package/src/tool/index.ts +26 -0
- package/src/tool/output.ts +271 -0
- package/src/tool/transcript.ts +150 -0
- package/src/tool/view-region.ts +581 -0
- package/test/appearance.test.ts +183 -0
- package/test/bounded-stream.test.ts +150 -0
- package/test/bridge-client.test.ts +42 -0
- package/test/color-preview.test.ts +23 -0
- package/test/component-stack.test.ts +206 -0
- package/test/components.test.ts +1165 -0
- package/test/content-text.test.ts +21 -0
- package/test/cursor-bridge.test.ts +151 -0
- package/test/cursor.test.ts +147 -0
- package/test/editor-chrome.test.ts +119 -0
- package/test/editor-layout.test.ts +30 -0
- package/test/editor-registry.test.ts +203 -0
- package/test/editor-theme.test.ts +33 -0
- package/test/editor-token-pill.test.ts +72 -0
- package/test/floating-overlay.test.ts +57 -0
- package/test/folding.test.ts +147 -0
- package/test/harmonious-theme.test.ts +23 -0
- package/test/interaction.test.ts +104 -0
- package/test/key-icons.test.ts +62 -0
- package/test/markdown-code.test.ts +24 -0
- package/test/modal-overlay-mount.test.ts +94 -0
- package/test/motion.test.ts +649 -0
- package/test/mouse/bridge.test.ts +1302 -0
- package/test/mouse/extension.test.ts +205 -0
- package/test/mouse/registry.test.ts +140 -0
- package/test/mouse/screen-decoration.test.ts +167 -0
- package/test/native-binary.test.ts +52 -0
- package/test/palette.test.ts +32 -0
- package/test/panels.test.ts +74 -0
- package/test/pointer-components.test.ts +151 -0
- package/test/progress.test.ts +54 -0
- package/test/pty-host.test.ts +126 -0
- package/test/pty-pane.test.ts +62 -0
- package/test/public-surface.test.ts +75 -0
- package/test/render-cache.test.ts +25 -0
- package/test/render-epoch.test.ts +119 -0
- package/test/request-animation.test.ts +193 -0
- package/test/screen-icon-actions.test.ts +69 -0
- package/test/select-box.test.ts +86 -0
- package/test/select-pointer.test.ts +369 -0
- package/test/selectable-list.test.ts +258 -0
- package/test/selection/registry.test.ts +44 -0
- package/test/selection-action-bar.test.ts +204 -0
- package/test/split-pane-bridge.test.ts +944 -0
- package/test/split-pane.test.ts +134 -0
- package/test/status-presentation.test.ts +39 -0
- package/test/syntax.test.ts +59 -0
- package/test/terminal-colors.test.ts +115 -0
- package/test/terminal-embedding.test.ts +12 -0
- package/test/terminal-projection.test.ts +312 -0
- package/test/theme-colors.test.ts +175 -0
- package/test/tool-output.test.ts +160 -0
- package/test/tool-transcript.test.ts +1190 -0
- package/test/unified-diff.test.ts +691 -0
- package/themes/harmonious.json +61 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { type TuiForegroundToken, type TuiTheme, tuiTheme } from "../color/theme.ts";
|
|
5
|
+
import { sanitizeTuiText } from "../content/terminal-text.ts";
|
|
6
|
+
import {
|
|
7
|
+
activityFrame,
|
|
8
|
+
animationSpeedMultiplier,
|
|
9
|
+
mountConfiguredAnimation,
|
|
10
|
+
type MotionMount,
|
|
11
|
+
type MotionScheduler,
|
|
12
|
+
} from "../motion.ts";
|
|
13
|
+
import type { TuiAnimationSpeed } from "../appearance.ts";
|
|
14
|
+
import { RenderedLinesCache } from "../render-cache.ts";
|
|
15
|
+
import { icon, type TuiIconName } from "./glyphs.ts";
|
|
16
|
+
|
|
17
|
+
/** Options for the pure determinate or indeterminate progress renderer. */
|
|
18
|
+
export interface ProgressFrameOptions {
|
|
19
|
+
width: number;
|
|
20
|
+
/** A finite value from zero to one. Omit for an indeterminate moving segment. */
|
|
21
|
+
value?: number;
|
|
22
|
+
elapsedMs?: number;
|
|
23
|
+
animationSpeed?: TuiAnimationSpeed;
|
|
24
|
+
reducedMotion?: boolean;
|
|
25
|
+
filled?: string;
|
|
26
|
+
empty?: string;
|
|
27
|
+
tone?: TuiForegroundToken;
|
|
28
|
+
trackTone?: TuiForegroundToken;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Render a fixed-width progress frame using semantic foreground colors. */
|
|
32
|
+
export function progressFrame(colors: TuiTheme, options: ProgressFrameOptions): string {
|
|
33
|
+
const width = Math.max(0, Math.floor(options.width));
|
|
34
|
+
if (width === 0) return "";
|
|
35
|
+
const filled = cellGlyph(options.filled, "━");
|
|
36
|
+
const empty = cellGlyph(options.empty, "─");
|
|
37
|
+
const tone = options.tone ?? "accent";
|
|
38
|
+
const trackTone = options.trackTone ?? "text.muted";
|
|
39
|
+
if (options.value !== undefined && Number.isFinite(options.value)) {
|
|
40
|
+
const count = Math.round(Math.max(0, Math.min(1, options.value)) * width);
|
|
41
|
+
return `${colors.fg(tone, filled.repeat(count))}${colors.fg(trackTone, empty.repeat(width - count))}`;
|
|
42
|
+
}
|
|
43
|
+
if (options.reducedMotion) return colors.fg(trackTone, empty.repeat(width));
|
|
44
|
+
const segmentWidth = Math.max(1, Math.min(width, Math.ceil(width / 4)));
|
|
45
|
+
const travel = width + segmentWidth;
|
|
46
|
+
const elapsedMs = Math.max(0, options.elapsedMs ?? 0) * animationSpeedMultiplier(options.animationSpeed);
|
|
47
|
+
const offset = Math.floor(elapsedMs / 70) % travel;
|
|
48
|
+
const start = Math.max(0, offset - segmentWidth);
|
|
49
|
+
const end = Math.min(width, offset);
|
|
50
|
+
return `${colors.fg(trackTone, empty.repeat(start))}${colors.fg(tone, filled.repeat(Math.max(0, end - start)))}${colors.fg(trackTone, empty.repeat(width - end))}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Construction options for a stream-friendly activity row. */
|
|
54
|
+
export interface ActivityIndicatorOptions {
|
|
55
|
+
theme: Theme;
|
|
56
|
+
label: string;
|
|
57
|
+
detail?: string;
|
|
58
|
+
requestRender(): void;
|
|
59
|
+
reducedMotion?: boolean;
|
|
60
|
+
spinnerFrames?: readonly string[];
|
|
61
|
+
scheduler?: MotionScheduler;
|
|
62
|
+
/** Optional repaint deadline for callers that cannot own component disposal. */
|
|
63
|
+
maxDurationMs?: number;
|
|
64
|
+
/** Initial-time seam for deterministic component tests. */
|
|
65
|
+
now?: () => number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A compact animated activity row backed by the shared cadence scheduler. */
|
|
69
|
+
export class ActivityIndicator implements Component {
|
|
70
|
+
private readonly cache = new RenderedLinesCache();
|
|
71
|
+
private readonly startedAtMs: number;
|
|
72
|
+
private nowMs: number;
|
|
73
|
+
private readonly mount: MotionMount;
|
|
74
|
+
|
|
75
|
+
constructor(private readonly options: ActivityIndicatorOptions) {
|
|
76
|
+
this.startedAtMs = options.now?.() ?? performance.now();
|
|
77
|
+
this.nowMs = this.startedAtMs;
|
|
78
|
+
this.mount = mountConfiguredAnimation(options, {
|
|
79
|
+
reducedMotion: options.reducedMotion,
|
|
80
|
+
maxDurationMs: options.maxDurationMs,
|
|
81
|
+
scheduler: options.scheduler,
|
|
82
|
+
onFrame: (nowMs) => {
|
|
83
|
+
this.nowMs = nowMs;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
render(width: number): string[] {
|
|
89
|
+
const elapsedMs = Math.max(0, this.nowMs - this.startedAtMs);
|
|
90
|
+
const frame = activityFrame(tuiTheme(this.options.theme), rowText(this.options.label), elapsedMs, {
|
|
91
|
+
frames: this.options.spinnerFrames,
|
|
92
|
+
reducedMotion: this.options.reducedMotion,
|
|
93
|
+
});
|
|
94
|
+
const marker = frame.marker ? `${frame.marker} ` : "";
|
|
95
|
+
const detailText = this.options.detail === undefined ? undefined : rowText(this.options.detail);
|
|
96
|
+
const key = `${frame.marker}\0${frame.text}\0${detailText ?? ""}`;
|
|
97
|
+
return this.cache.get(width, key, () => {
|
|
98
|
+
if (width <= 0) return [];
|
|
99
|
+
const colors = tuiTheme(this.options.theme);
|
|
100
|
+
const separator = detailText ? colors.fg("text.muted", " · ") : "";
|
|
101
|
+
const detail = detailText ? colors.fg("text.secondary", detailText) : "";
|
|
102
|
+
const line = `${marker}${frame.text}${separator}${detail}`;
|
|
103
|
+
return [truncateToWidth(line, width, "…")];
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
invalidate(): void {
|
|
108
|
+
this.cache.clear();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Release the scheduler registration. Safe to call more than once. */
|
|
112
|
+
dispose(): void {
|
|
113
|
+
this.mount.dispose();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Construction options for a cached progress-bar component. */
|
|
118
|
+
export interface ProgressBarOptions extends Omit<ProgressFrameOptions, "width"> {
|
|
119
|
+
theme: Theme;
|
|
120
|
+
label?: string;
|
|
121
|
+
showPercentage?: boolean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** A static, allocation-bounded determinate progress bar. */
|
|
125
|
+
export class ProgressBar implements Component {
|
|
126
|
+
private readonly cache = new RenderedLinesCache();
|
|
127
|
+
|
|
128
|
+
constructor(private readonly options: ProgressBarOptions) {}
|
|
129
|
+
|
|
130
|
+
render(width: number): string[] {
|
|
131
|
+
const value = this.options.value;
|
|
132
|
+
const percentage = value === undefined ? "" : `${Math.round(Math.max(0, Math.min(1, value)) * 100)}%`;
|
|
133
|
+
const key = `${value ?? "indeterminate"}\0${this.options.elapsedMs ?? 0}\0${this.options.reducedMotion ?? false}`;
|
|
134
|
+
return this.cache.get(width, key, () => {
|
|
135
|
+
if (width <= 0) return [];
|
|
136
|
+
const colors = tuiTheme(this.options.theme);
|
|
137
|
+
const labelText = this.options.label === undefined ? "" : rowText(this.options.label);
|
|
138
|
+
const label = labelText ? colors.fg("text.secondary", labelText) : "";
|
|
139
|
+
const suffix = this.options.showPercentage && percentage ? colors.fg("text.muted", percentage) : "";
|
|
140
|
+
const fixed = visibleWidth(label) + visibleWidth(suffix) + (label ? 1 : 0) + (suffix ? 1 : 0);
|
|
141
|
+
const barWidth = Math.max(1, width - fixed);
|
|
142
|
+
const bar = progressFrame(colors, { ...this.options, width: barWidth });
|
|
143
|
+
return [truncateToWidth(`${label}${label ? " " : ""}${bar}${suffix ? ` ${suffix}` : ""}`, width, "")];
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
invalidate(): void {
|
|
148
|
+
this.cache.clear();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cellGlyph(value: string | undefined, fallback: string): string {
|
|
153
|
+
if (value === undefined) return fallback;
|
|
154
|
+
const safe = sanitizeTuiText(value);
|
|
155
|
+
return safe === value && safe.trim().length > 0 && visibleWidth(safe) === 1 ? safe : fallback;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function rowText(value: string): string {
|
|
159
|
+
return sanitizeTuiText(value).replace(/[\n\r]+/gu, " ");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** A title label with an optional icon resolved from the active semantic icon pack. */
|
|
163
|
+
export interface TuiTitle {
|
|
164
|
+
/** Human-readable title text. */
|
|
165
|
+
label: string;
|
|
166
|
+
/** Semantic icon token rendered before the label. */
|
|
167
|
+
icon?: TuiIconName;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** A plain title string or a structured semantic title. */
|
|
171
|
+
export type TuiTitleValue = string | TuiTitle;
|
|
172
|
+
|
|
173
|
+
/** A title value or callback resolved on every render for dynamic labels. */
|
|
174
|
+
export type TuiTitleSource = TuiTitleValue | (() => TuiTitleValue);
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Resolve a static or dynamic semantic title into display text.
|
|
178
|
+
* @param source Title value or callback evaluated at call time.
|
|
179
|
+
* @returns The label, prefixed by the active icon-pack glyph when an icon exists.
|
|
180
|
+
*/
|
|
181
|
+
export function resolveTuiTitle(source: TuiTitleSource): string {
|
|
182
|
+
const title = typeof source === "function" ? source() : source;
|
|
183
|
+
if (typeof title === "string") return title;
|
|
184
|
+
return title.icon ? `${icon(title.icon)} ${title.label}` : title.label;
|
|
185
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { compositeTuiLine, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { type TuiForegroundToken, tuiTheme } from "../color/theme.ts";
|
|
4
|
+
import type { ViewportRect } from "../mouse.ts";
|
|
5
|
+
import type { SelectionPoint } from "../selection.ts";
|
|
6
|
+
import { getTuiPillSeparators, icon, type TuiIconName } from "./glyphs.ts";
|
|
7
|
+
import { backgroundAnsiAtColumn, renderPill } from "./powerline-pill.ts";
|
|
8
|
+
|
|
9
|
+
/** Content displayed by a short-lived cursor-local pill. */
|
|
10
|
+
export interface TransientPillMessage {
|
|
11
|
+
/** Short status text. */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Semantic icon displayed before the label. */
|
|
14
|
+
icon: TuiIconName;
|
|
15
|
+
/** Semantic foreground applied to the icon. */
|
|
16
|
+
tone: TuiForegroundToken;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Bounds used to place a transient pill around a terminal cell. */
|
|
20
|
+
export interface TransientPillPlacementRequest {
|
|
21
|
+
/** Absolute cell around which the pill is centered. */
|
|
22
|
+
anchor: SelectionPoint;
|
|
23
|
+
/** Natural pill width before visible-bounds clamping. */
|
|
24
|
+
width: number;
|
|
25
|
+
/** Complete terminal width. */
|
|
26
|
+
screenWidth: number;
|
|
27
|
+
/** Complete terminal height. */
|
|
28
|
+
screenHeight: number;
|
|
29
|
+
/** Optional transcript viewport used as tighter visible bounds. */
|
|
30
|
+
viewport?: ViewportRect;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Construction options for {@link TransientPill}. */
|
|
34
|
+
export interface TransientPillOptions {
|
|
35
|
+
/** Active Pi theme used for semantic colors. */
|
|
36
|
+
theme: Theme;
|
|
37
|
+
/** Request a host render after feedback appears or expires. */
|
|
38
|
+
requestRender(): void;
|
|
39
|
+
/** Visibility duration in milliseconds; defaults to 1500. */
|
|
40
|
+
durationMs?: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function clamp(value: number, minimum: number, maximum: number): number {
|
|
44
|
+
return Math.max(minimum, Math.min(value, maximum));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Center a one-row pill above its anchor, flipping below and clamping to visible bounds.
|
|
49
|
+
* @param request Anchor, terminal bounds, and natural pill width.
|
|
50
|
+
* @returns A visible one-row rectangle, or undefined when no adjacent row exists.
|
|
51
|
+
*/
|
|
52
|
+
export function placeTransientPill(request: TransientPillPlacementRequest):
|
|
53
|
+
| {
|
|
54
|
+
x: number;
|
|
55
|
+
y: number;
|
|
56
|
+
width: number;
|
|
57
|
+
height: 1;
|
|
58
|
+
}
|
|
59
|
+
| undefined {
|
|
60
|
+
const left = request.viewport?.x ?? 0;
|
|
61
|
+
const top = request.viewport?.y ?? 0;
|
|
62
|
+
const right = request.viewport
|
|
63
|
+
? Math.min(request.screenWidth, request.viewport.x + request.viewport.width)
|
|
64
|
+
: request.screenWidth;
|
|
65
|
+
const bottom = request.viewport
|
|
66
|
+
? Math.min(request.screenHeight, request.viewport.y + request.viewport.height)
|
|
67
|
+
: request.screenHeight;
|
|
68
|
+
const width = Math.min(Math.max(0, Math.floor(request.width)), Math.max(0, right - left));
|
|
69
|
+
if (width === 0 || bottom <= top) return undefined;
|
|
70
|
+
const anchorRow = clamp(Math.floor(request.anchor.row), top, bottom - 1);
|
|
71
|
+
const y = anchorRow - 1 >= top ? anchorRow - 1 : anchorRow + 1 < bottom ? anchorRow + 1 : undefined;
|
|
72
|
+
if (y === undefined) return undefined;
|
|
73
|
+
const center = Math.floor(request.anchor.col) + 0.5;
|
|
74
|
+
const x = clamp(Math.round(center - width / 2), left, right - width);
|
|
75
|
+
return { x, y, width, height: 1 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A reusable, timed pill composited near a terminal cursor or selection anchor. */
|
|
79
|
+
export class TransientPill {
|
|
80
|
+
private current: { message: TransientPillMessage; anchor: SelectionPoint } | undefined;
|
|
81
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
82
|
+
|
|
83
|
+
/** @param options Theme, render callback, and optional display duration. */
|
|
84
|
+
constructor(private readonly options: TransientPillOptions) {}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Display feedback and restart its expiry timer.
|
|
88
|
+
* @param message Semantic icon, tone, and short label.
|
|
89
|
+
* @param anchor Absolute cursor or selection cell around which to place the pill.
|
|
90
|
+
*/
|
|
91
|
+
show(message: TransientPillMessage, anchor: SelectionPoint): void {
|
|
92
|
+
this.current = { message, anchor };
|
|
93
|
+
if (this.timer) clearTimeout(this.timer);
|
|
94
|
+
this.timer = setTimeout(() => {
|
|
95
|
+
this.current = undefined;
|
|
96
|
+
this.timer = undefined;
|
|
97
|
+
this.options.requestRender();
|
|
98
|
+
}, this.options.durationMs ?? 1_500);
|
|
99
|
+
this.options.requestRender();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Hide current feedback and request a render. */
|
|
103
|
+
clear(): void {
|
|
104
|
+
if (this.timer) clearTimeout(this.timer);
|
|
105
|
+
this.timer = undefined;
|
|
106
|
+
if (!this.current) return;
|
|
107
|
+
this.current = undefined;
|
|
108
|
+
this.options.requestRender();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Cancel expiry and release the current message without requesting another render. */
|
|
112
|
+
dispose(): void {
|
|
113
|
+
if (this.timer) clearTimeout(this.timer);
|
|
114
|
+
this.timer = undefined;
|
|
115
|
+
this.current = undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Composite current feedback over a styled terminal screen.
|
|
120
|
+
* @param screen Complete styled terminal rows.
|
|
121
|
+
* @param bounds Terminal dimensions and optional transcript viewport.
|
|
122
|
+
* @returns A decorated screen copy, or an unchanged copy when no pill is visible.
|
|
123
|
+
*/
|
|
124
|
+
composite(screen: readonly string[], bounds: { width: number; height: number; viewport?: ViewportRect }): string[] {
|
|
125
|
+
if (!this.current) return [...screen];
|
|
126
|
+
const [leftCap, rightCap] = getTuiPillSeparators();
|
|
127
|
+
const naturalWidth =
|
|
128
|
+
visibleWidth(leftCap) +
|
|
129
|
+
visibleWidth(icon(this.current.message.icon)) +
|
|
130
|
+
1 +
|
|
131
|
+
visibleWidth(this.current.message.label) +
|
|
132
|
+
visibleWidth(rightCap);
|
|
133
|
+
const placement = placeTransientPill({
|
|
134
|
+
anchor: this.current.anchor,
|
|
135
|
+
width: naturalWidth,
|
|
136
|
+
screenWidth: bounds.width,
|
|
137
|
+
screenHeight: bounds.height,
|
|
138
|
+
viewport: bounds.viewport,
|
|
139
|
+
});
|
|
140
|
+
if (!placement) return [...screen];
|
|
141
|
+
const result = [...screen];
|
|
142
|
+
const base = result[placement.y];
|
|
143
|
+
if (base === undefined) return result;
|
|
144
|
+
const destination = backgroundAnsiAtColumn(base, placement.x);
|
|
145
|
+
const colors = tuiTheme(this.options.theme);
|
|
146
|
+
const iconGlyph = icon(this.current.message.icon);
|
|
147
|
+
const labelWidth = Math.max(
|
|
148
|
+
0,
|
|
149
|
+
placement.width - visibleWidth(leftCap) - visibleWidth(iconGlyph) - 1 - visibleWidth(rightCap),
|
|
150
|
+
);
|
|
151
|
+
const label = truncateToWidth(this.current.message.label, labelWidth, "…");
|
|
152
|
+
const blue = colors.color({ hue: "blue", shade: 2 });
|
|
153
|
+
const background =
|
|
154
|
+
this.current.message.tone === "warning"
|
|
155
|
+
? "badge.warning"
|
|
156
|
+
: this.current.message.tone === "negative"
|
|
157
|
+
? "badge.negative"
|
|
158
|
+
: blue;
|
|
159
|
+
const pill = renderPill(
|
|
160
|
+
this.options.theme,
|
|
161
|
+
{ icon: this.current.message.icon, iconTone: this.current.message.tone, label },
|
|
162
|
+
background,
|
|
163
|
+
"text.primary",
|
|
164
|
+
undefined,
|
|
165
|
+
destination,
|
|
166
|
+
);
|
|
167
|
+
result[placement.y] = compositeTuiLine(
|
|
168
|
+
base,
|
|
169
|
+
pill,
|
|
170
|
+
placement.x,
|
|
171
|
+
placement.width,
|
|
172
|
+
Math.max(visibleWidth(base), placement.x + placement.width),
|
|
173
|
+
);
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
CreateUnifiedDiffModelOptions,
|
|
3
|
+
ParseUnifiedDiffOptions,
|
|
4
|
+
UnifiedDiffFile,
|
|
5
|
+
UnifiedDiffFileInput,
|
|
6
|
+
UnifiedDiffHunk,
|
|
7
|
+
UnifiedDiffHunkInput,
|
|
8
|
+
UnifiedDiffLine,
|
|
9
|
+
UnifiedDiffLineKind,
|
|
10
|
+
UnifiedDiffModel,
|
|
11
|
+
UnifiedDiffRowInput,
|
|
12
|
+
} from "./model.ts";
|
|
13
|
+
export { createUnifiedDiffModel, parseUnifiedDiff } from "./parse.ts";
|
|
14
|
+
export type {
|
|
15
|
+
RenderUnifiedDiffOptions,
|
|
16
|
+
UnifiedDiffRenderResult,
|
|
17
|
+
UnifiedDiffViewport,
|
|
18
|
+
} from "./render.ts";
|
|
19
|
+
export { renderUnifiedDiff } from "./render.ts";
|
|
20
|
+
export type { UnifiedDiffViewOptions } from "./view.ts";
|
|
21
|
+
export { UnifiedDiffView } from "./view.ts";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type UnifiedDiffLineKind = "context" | "added" | "removed" | "metadata" | "malformed";
|
|
2
|
+
|
|
3
|
+
export interface UnifiedDiffLine {
|
|
4
|
+
/** Stable within a parsed model and suitable for renderer/highlighter caches. */
|
|
5
|
+
readonly ref: string;
|
|
6
|
+
readonly kind: UnifiedDiffLineKind;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly oldLine?: number;
|
|
9
|
+
readonly newLine?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface UnifiedDiffHunk {
|
|
13
|
+
readonly ref: string;
|
|
14
|
+
readonly header: string;
|
|
15
|
+
readonly oldStart?: number;
|
|
16
|
+
readonly oldCount?: number;
|
|
17
|
+
readonly newStart?: number;
|
|
18
|
+
readonly newCount?: number;
|
|
19
|
+
readonly lines: readonly UnifiedDiffLine[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UnifiedDiffFile {
|
|
23
|
+
readonly ref: string;
|
|
24
|
+
readonly oldPath?: string;
|
|
25
|
+
readonly newPath?: string;
|
|
26
|
+
readonly headerLines: readonly string[];
|
|
27
|
+
readonly hunks: readonly UnifiedDiffHunk[];
|
|
28
|
+
readonly additions: number;
|
|
29
|
+
readonly removals: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface UnifiedDiffModel {
|
|
33
|
+
readonly revision: string;
|
|
34
|
+
readonly files: readonly UnifiedDiffFile[];
|
|
35
|
+
readonly preamble: readonly string[];
|
|
36
|
+
readonly additions: number;
|
|
37
|
+
readonly removals: number;
|
|
38
|
+
readonly sourceRows: number;
|
|
39
|
+
readonly truncated: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ParseUnifiedDiffOptions {
|
|
43
|
+
/** Hard input limit. Parsing stops before allocating rows beyond this boundary. */
|
|
44
|
+
readonly maxCharacters?: number;
|
|
45
|
+
/** Hard logical-row limit, including file headers. */
|
|
46
|
+
readonly maxRows?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface UnifiedDiffRowInput {
|
|
50
|
+
readonly kind: UnifiedDiffLineKind;
|
|
51
|
+
readonly text: string;
|
|
52
|
+
readonly oldLine?: number;
|
|
53
|
+
readonly newLine?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UnifiedDiffHunkInput {
|
|
57
|
+
readonly header?: string;
|
|
58
|
+
readonly oldStart?: number;
|
|
59
|
+
readonly oldCount?: number;
|
|
60
|
+
readonly newStart?: number;
|
|
61
|
+
readonly newCount?: number;
|
|
62
|
+
readonly rows: readonly UnifiedDiffRowInput[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface UnifiedDiffFileInput {
|
|
66
|
+
readonly oldPath?: string;
|
|
67
|
+
readonly newPath?: string;
|
|
68
|
+
readonly headerLines?: readonly string[];
|
|
69
|
+
readonly hunks: readonly UnifiedDiffHunkInput[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface CreateUnifiedDiffModelOptions extends ParseUnifiedDiffOptions {
|
|
73
|
+
readonly revision?: string;
|
|
74
|
+
/** Caller-owned truncation performed before the structured model was built. */
|
|
75
|
+
readonly truncated?: boolean;
|
|
76
|
+
}
|