@valyrianjs/terminal 0.2.1 → 0.2.3
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/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +177 -17
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +4 -0
- package/dist/events.js.map +1 -1
- package/dist/frame-style.d.ts +7 -0
- package/dist/frame-style.d.ts.map +1 -0
- package/dist/frame-style.js +27 -0
- package/dist/frame-style.js.map +1 -0
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +53 -23
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +8 -1
- package/dist/mouse.js.map +1 -1
- package/dist/render-internal.d.ts +10 -0
- package/dist/render-internal.d.ts.map +1 -0
- package/dist/render-internal.js +1295 -0
- package/dist/render-internal.js.map +1 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +13 -1205
- package/dist/render.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +78 -4
- package/dist/session.js.map +1 -1
- package/dist/text.d.ts +7 -0
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +125 -0
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +18 -2
- package/dist/theme.js.map +1 -1
- package/dist/types.d.ts +3 -2
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +6 -3
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +5 -5
- package/docs/primitive-gallery.md +4 -4
- package/examples/basic.tsx +22 -0
- package/examples/cli.tsx +55 -0
- package/examples/demo.tsx +98 -0
- package/examples/docs/background-fill.tsx +107 -0
- package/examples/docs/component-composition.tsx +140 -0
- package/examples/docs/cursor.tsx +121 -0
- package/examples/docs/employees-list.tsx +138 -0
- package/examples/docs/hello.tsx +98 -0
- package/examples/docs/interactive-note.tsx +111 -0
- package/examples/docs/module-api-dashboard.tsx +307 -0
- package/examples/docs/module-flux-store.tsx +181 -0
- package/examples/docs/module-form-workflow.tsx +339 -0
- package/examples/docs/module-forms.tsx +218 -0
- package/examples/docs/module-money.tsx +175 -0
- package/examples/docs/module-native-store.tsx +188 -0
- package/examples/docs/module-pulses.tsx +142 -0
- package/examples/docs/module-query.tsx +209 -0
- package/examples/docs/module-request.tsx +194 -0
- package/examples/docs/module-state-workbench.tsx +283 -0
- package/examples/docs/module-tasks.tsx +223 -0
- package/examples/docs/module-translate.tsx +194 -0
- package/examples/docs/module-utils.tsx +168 -0
- package/examples/docs/module-valyrian-core.tsx +159 -0
- package/examples/docs/pizza-builder.tsx +463 -0
- package/examples/docs/primitive-activity-console.tsx +113 -0
- package/examples/docs/primitive-command-panel.tsx +186 -0
- package/examples/docs/primitive-data-explorer.tsx +155 -0
- package/examples/docs/primitive-input-workbench.tsx +128 -0
- package/examples/docs/primitive-layout-shell.tsx +115 -0
- package/examples/docs/responsive-split.tsx +186 -0
- package/examples/docs/style-system.tsx +209 -0
- package/examples/docs/theme-colors.tsx +225 -0
- package/examples/docs/virtualized-list-workbench.tsx +232 -0
- package/examples/opencode-dogfood-app.tsx +215 -0
- package/examples/opencode-dogfood-lifecycle.tsx +194 -0
- package/examples/opencode-dogfood.tsx +11 -0
- package/llms-full.txt +16 -13
- package/package.json +3 -2
- package/src/ansi.ts +207 -17
- package/src/events.ts +2 -0
- package/src/frame-style.ts +36 -0
- package/src/layout.ts +57 -24
- package/src/mouse.ts +10 -1
- package/src/render-internal.ts +1441 -0
- package/src/render.ts +14 -1324
- package/src/session.ts +99 -12
- package/src/text.ts +160 -0
- package/src/theme.ts +22 -2
- package/src/types.ts +3 -2
package/llms-full.txt
CHANGED
|
@@ -666,7 +666,7 @@ console.log(session.output());
|
|
|
666
666
|
session.destroy();
|
|
667
667
|
```
|
|
668
668
|
|
|
669
|
-
For large lists, add `virtualized` and place the list in a bounded region such as a filled pane or split. The virtualized workbench
|
|
669
|
+
For large lists, add `virtualized` and place the list in a bounded region such as a filled pane or split. The virtualized workbench demonstrates the uncontrolled List API with `itemKey`, the `children` callback, event payloads, active row, selected row, and viewport values: [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Shift+Up and Shift+Down are available for custom app actions. The List does not reorder items by default. Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`.
|
|
670
670
|
|
|
671
671
|
## Local state to Valyrian state module bridge
|
|
672
672
|
|
|
@@ -1144,12 +1144,12 @@ Use `Editor` for notes, descriptions, and longer messages.
|
|
|
1144
1144
|
|
|
1145
1145
|
**What it is:** A multi-line editable field.
|
|
1146
1146
|
**Use it for:** notes, descriptions, and longer messages.
|
|
1147
|
-
**Core props:** `id`, `value`, `placeholder`, `height`, `onchange`, `oninput`, `onsubmit`, `oncancel`.
|
|
1147
|
+
**Core props:** `id`, `value`, `placeholder`, `width`, `height`, `fill`, `onchange`, `oninput`, `onsubmit`, `oncancel`.
|
|
1148
1148
|
|
|
1149
1149
|
**Minimal example:**
|
|
1150
1150
|
|
|
1151
1151
|
```tsx
|
|
1152
|
-
<Editor id="details" height={4} value={details} onchange={(event) => { details = event.value; }} />
|
|
1152
|
+
<Editor id="details" fill height={4} value={details} onchange={(event) => { details = event.value; }} />
|
|
1153
1153
|
```
|
|
1154
1154
|
|
|
1155
1155
|
**Complete demo:** [`examples/docs/primitive-input-workbench.tsx`](../examples/docs/primitive-input-workbench.tsx). Run it with `bun examples/docs/primitive-input-workbench.tsx` and quit with `Ctrl+C`.
|
|
@@ -1196,7 +1196,7 @@ Use `List` for menus, choosers, command lists, and item browsers.
|
|
|
1196
1196
|
|
|
1197
1197
|
**What it is:** A focusable collection control with selection and press events.
|
|
1198
1198
|
**Use it for:** menus, choosers, command lists, and item browsers.
|
|
1199
|
-
**Core props:** `id`, `items`, `children` callback, `renderItem`, `itemKey`, `virtualized`, `showActive`, `onchange`, `onviewportchange`, `onpress`, `ondoublepress`, `oncontextpress`. `List`
|
|
1199
|
+
**Core props:** `id`, `items`, `children` callback, `renderItem`, `itemKey`, `virtualized`, `showActive`, `onchange`, `onviewportchange`, `onpress`, `ondoublepress`, `oncontextpress`. `List` manages its active row, selected row, and viewport state. Read those values from event payloads.
|
|
1200
1200
|
|
|
1201
1201
|
**Minimal example:**
|
|
1202
1202
|
|
|
@@ -1208,7 +1208,7 @@ Use `List` for menus, choosers, command lists, and item browsers.
|
|
|
1208
1208
|
|
|
1209
1209
|
**Complete demo:** [`examples/docs/primitive-data-explorer.tsx`](../examples/docs/primitive-data-explorer.tsx). Run it with `bun examples/docs/primitive-data-explorer.tsx` and quit with `Ctrl+C`.
|
|
1210
1210
|
|
|
1211
|
-
**Virtualized demo:** [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`.
|
|
1211
|
+
**Virtualized demo:** [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx). Run it with `bun examples/docs/virtualized-list-workbench.tsx`, move active rows with `J/K` or `Up`/`Down`, jump with `PageUp`, `PageDown`, `Home`, or `End`, select with `Enter`, and quit with `Ctrl+C`. `Shift+Up` and `Shift+Down` are shown as app-defined commands, not built-in reorder behavior.
|
|
1212
1212
|
|
|
1213
1213
|
### `Table`
|
|
1214
1214
|
|
|
@@ -1359,7 +1359,7 @@ This guide explains how interactive primitives receive input and report events.
|
|
|
1359
1359
|
|
|
1360
1360
|
## Input events
|
|
1361
1361
|
|
|
1362
|
-
Keyboard dispatch uses normalized key names such as `ENTER`, `TAB`, `SHIFT_TAB`, `LEFT`, `RIGHT`, `CTRL_V`, and single-character strings. When a session is connected to `stdin`, terminal input is parsed and routed to the focused primitive.
|
|
1362
|
+
Keyboard dispatch uses normalized key names such as `ENTER`, `TAB`, `SHIFT_TAB`, `LEFT`, `RIGHT`, `SHIFT_UP`, `SHIFT_DOWN`, `CTRL_V`, and single-character strings. When a session is connected to `stdin`, terminal input is parsed and routed to the focused primitive.
|
|
1363
1363
|
|
|
1364
1364
|
Pasted text is treated as text input for the focused primitive. Do not treat pasted strings or key sequences as terminal control instructions inside rendered content.
|
|
1365
1365
|
|
|
@@ -1399,7 +1399,7 @@ Main handlers:
|
|
|
1399
1399
|
|
|
1400
1400
|
### `Editor`
|
|
1401
1401
|
|
|
1402
|
-
`Editor` is a multiline editing primitive.
|
|
1402
|
+
`Editor` is a multiline editing primitive. Use `height` for a bounded multiline viewport, `width` for a fixed frame width, and `fill` when the editor should behave like a textarea surface that consumes available width and height. Explicit dimensions win over `fill`; without those props, existing content-sized rendering stays intact.
|
|
1403
1403
|
|
|
1404
1404
|
Supported behavior includes:
|
|
1405
1405
|
|
|
@@ -1440,13 +1440,13 @@ A quick second primary mouse press keeps the normal activation behavior and also
|
|
|
1440
1440
|
|
|
1441
1441
|
### `List`
|
|
1442
1442
|
|
|
1443
|
-
`List` models active row movement, optional visible selection, viewport scroll, and activation. In virtualized mode
|
|
1443
|
+
`List` models active row movement, optional visible selection, viewport scroll, and activation. In virtualized mode, pass the full `items` array; the list shows the visible viewport, wheel input scrolls it, and keyboard list commands move the active row and emit `change`. A JSX `children` callback receives `(item, ctx)` with `ctx.active`, `ctx.selected`, `ctx.index`, `ctx.key`, and `ctx.viewportIndex`; it takes precedence over `renderItem`. Use `itemKey` when rows have stable identities across reorder or filtering.
|
|
1444
1444
|
|
|
1445
1445
|
Supported behavior includes:
|
|
1446
1446
|
|
|
1447
1447
|
- `UP` and `DOWN` move the active row and emit `onchange`
|
|
1448
1448
|
- `PageUp`, `PageDown`, `Home`, and `End` jump the active row and keep it visible
|
|
1449
|
-
- `LEFT` and `
|
|
1449
|
+
- `LEFT`, `RIGHT`, `SHIFT_UP`, and `SHIFT_DOWN` have no default list command, so applications can bind them
|
|
1450
1450
|
- `ENTER` selects and activates the active row
|
|
1451
1451
|
- mouse click selects and activates the target row
|
|
1452
1452
|
- mouse hover reports row details when mouse input is connected
|
|
@@ -1465,7 +1465,7 @@ Main handlers:
|
|
|
1465
1465
|
- `oncapturestart`
|
|
1466
1466
|
- `oncaptureend`
|
|
1467
1467
|
|
|
1468
|
-
`pointerCapture` lets a list keep drag interaction even when the pointer leaves the initial hitbox.
|
|
1468
|
+
`pointerCapture` lets a list keep drag interaction even when the pointer leaves the initial hitbox. If your app needs reordering, priority changes, or other modified-arrow behavior, bind `SHIFT_UP` or `SHIFT_DOWN` through `keymap.bindings` and handle the custom command in `keymap.onCommand`. The list will not mutate item order by default.
|
|
1469
1469
|
|
|
1470
1470
|
### `ScrollView`
|
|
1471
1471
|
|
|
@@ -5044,7 +5044,7 @@ Use `style`, `styles`, and `state` for normal component styling:
|
|
|
5044
5044
|
- `styles` maps visual states to dot path recipes or inline style objects.
|
|
5045
5045
|
- `state` declares app-owned states such as `loading`, `warning`, `success`, `muted`, `expanded`, or `dropTarget`.
|
|
5046
5046
|
|
|
5047
|
-
Renderer-owned states come from the runtime when the renderer has the fact itself. Focus, hover, selection, current rows, and pointer capture fall in this group when the primitive supports them. App-owned state should describe product state that the app already knows. Do not mark a ready button as `loading` or an editable field as `readonly
|
|
5047
|
+
Renderer-owned states come from the runtime when the renderer has the fact itself. Focus, hover, selection, current rows, and pointer capture fall in this group when the primitive supports them. App-owned state should describe product state that the app already knows. Do not use app-owned states for conditions that are not true. For example, do not mark a ready button as `loading` or an editable field as `readonly`.
|
|
5048
5048
|
|
|
5049
5049
|
Precedence: the renderer starts with the automatic `<element>.base` recipe when the primitive has one, applies the instance `style`, then applies `styles` entries for current states in stable visual-state order. Geometry fields such as supported padding and border come from the automatic base recipe plus the instance `style` so layout, hitboxes, and cursors stay stable across focus, hover, and pressed state changes. State styles affect the rendered spans after layout; later current states override earlier visual fields when they set the same property. Inline style objects and dot path recipes use the same merge path after resolution.
|
|
5050
5050
|
|
|
@@ -5053,6 +5053,7 @@ Renderer notes:
|
|
|
5053
5053
|
- Raw ANSI escape control is not the normal public styling API. Use semantic styles and let `ansiOutput()` serialize frames.
|
|
5054
5054
|
- Layout margin is not supported; use padding, `gap`, `Fixed`, `Split`, or explicit text spacing.
|
|
5055
5055
|
- The renderer owns terminal frame spans, clipping, focus, and selection; the app owns business state and labels.
|
|
5056
|
+
- When a styled public primitive resolves a final frame, its background spans cover that final frame, including cells introduced by layout composition, clipping, or fill. Child backgrounds cover their cells and parent backgrounds resume after child spans close in ANSI output.
|
|
5056
5057
|
|
|
5057
5058
|
Run `bun examples/docs/style-system.tsx`, try `Tab`, `Enter`, and `W`, and quit with `Ctrl+C`; or open [`examples/docs/style-system.tsx`](../examples/docs/style-system.tsx). Run `bun examples/docs/theme-colors.tsx`, press `Tab`, and quit with `Ctrl+C`; or open [`examples/docs/theme-colors.tsx`](../examples/docs/theme-colors.tsx).
|
|
5058
5059
|
|
|
@@ -5233,7 +5234,9 @@ Props:
|
|
|
5233
5234
|
- `focusable?: boolean`
|
|
5234
5235
|
- `value?: string`
|
|
5235
5236
|
- `placeholder?: string`
|
|
5237
|
+
- `width?: number`
|
|
5236
5238
|
- `height?: number`
|
|
5239
|
+
- `fill?: boolean`
|
|
5237
5240
|
- shared visual props: `style`, `styles`, `state`
|
|
5238
5241
|
- `onchange?(event)`
|
|
5239
5242
|
- `oninput?(event)`
|
|
@@ -5246,7 +5249,7 @@ Payloads:
|
|
|
5246
5249
|
- `TerminalEditorSubmitEventPayload` - `{ type: "submit", id, value }`
|
|
5247
5250
|
- `TerminalEditorCancelEventPayload` - `{ type: "cancel", id, value }`
|
|
5248
5251
|
|
|
5249
|
-
Supports multiline editing, submit, cancel, cursor movement, paste, and deletion. Cursor columns use JavaScript string columns rather than a complete terminal-width model.
|
|
5252
|
+
Supports multiline editing, submit, cancel, cursor movement, paste, and deletion. Without `fill`, `width`, or `height`, `Editor` keeps content-sized rendering. `height` constrains the multiline viewport, `width` fixes the final frame width, and `fill` uses the available render context for both axes. Explicit `width` and `height` win over `fill`; for example, `fill` plus `height` fills only the width. Use `Editor` for textarea-like surfaces; `Input` remains the single-line primitive. Cursor columns use JavaScript string columns rather than a complete terminal-width model.
|
|
5250
5253
|
|
|
5251
5254
|
### `Button`
|
|
5252
5255
|
|
|
@@ -5303,7 +5306,7 @@ Payloads:
|
|
|
5303
5306
|
- `TerminalListPointerEventPayload<T>` - `{ type: "hover" | "rowenter" | "rowleave", id, row, index, key?, value, x, y }`
|
|
5304
5307
|
- `TerminalCaptureEventPayload` - `{ type: "capturestart" | "captureend", id, source, row, x, y }`
|
|
5305
5308
|
|
|
5306
|
-
Supports
|
|
5309
|
+
Supports active row movement, selection, activation, double press on the same row, context press for the target row, hover payloads, optional wrapping, optional virtualization, viewport scroll, and pointer capture. `Up`/`Down`, `PageUp`, `PageDown`, `Home`, and `End` are built-in list navigation keys; `Left`, `Right`, `Shift+Up`, and `Shift+Down` stay available for application bindings. The library never reorders or mutates list items from modified arrow keys by default; priority changes and reordering belong in app-defined commands. Keyboard navigation moves the active row and emits `change`; `Enter` selects and activates the active row. Pointer click selects and activates the clicked row. A quick second primary click on the same row emits `doublepress` without a second `press`. The default renderer displays the active row as `list.current`; when the selected row differs from the active row, it displays the selected row as `list.selected`. A JSX `children` callback is the recommended item renderer and wins over `renderItem` when both are present. `children` receives `ctx.active` and `ctx.selected` separately. `renderItem` remains as a compatibility alias with the legacy `(item, index)` signature. `wrap` wraps long default-rendered item text to the list width. Virtualized lists receive the full `items` array and render the visible viewport; use `itemKey` for stable event identity when items can reorder. Observe the resulting active row, selected row, and viewport through the list event payloads. Run [`examples/docs/virtualized-list-workbench.tsx`](../examples/docs/virtualized-list-workbench.tsx) with `bun examples/docs/virtualized-list-workbench.tsx` to inspect those values from events in one bounded split layout.
|
|
5307
5310
|
|
|
5308
5311
|
### `ScrollView`
|
|
5309
5312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valyrianjs/terminal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Terminal adapter for valyrian.js",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"files": [
|
|
24
24
|
"dist",
|
|
25
25
|
"docs",
|
|
26
|
+
"examples",
|
|
26
27
|
"src",
|
|
27
28
|
"!docs/local-demo.md",
|
|
28
29
|
"!docs/plans",
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
"llms-full.txt"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
|
-
"test": "bun test --max-concurrency=1",
|
|
36
|
+
"test": "bun run build && bun test --max-concurrency=1",
|
|
36
37
|
"demo:dogfood": "bun examples/opencode-dogfood.tsx",
|
|
37
38
|
"llms:full": "bun scripts/generate-llms-full.ts",
|
|
38
39
|
"replay:ansi": "bun scripts/replay-ansi.ts",
|
package/src/ansi.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { dropTerminalCells, terminalCellWidth, terminalGraphemes } from "./text.js";
|
|
1
2
|
import { resolveTerminalStyle, resolveTerminalStyleToken } from "./theme.js";
|
|
2
3
|
import type { CursorPosition, TerminalFrame, TerminalStyleSpan, TerminalTheme } from "./types.js";
|
|
3
4
|
|
|
@@ -100,19 +101,18 @@ function renderAnsiLine(line: string, spans: TerminalStyleSpan[], y: number, the
|
|
|
100
101
|
const rowSpans = lineSpans(spans, y);
|
|
101
102
|
let activeSpanIndex = 0;
|
|
102
103
|
|
|
103
|
-
for (
|
|
104
|
+
for (const grapheme of terminalGraphemes(line)) {
|
|
104
105
|
while (rowSpans[activeSpanIndex]?.x1 === visibleColumn) {
|
|
105
106
|
output += spanAnsiOpen(rowSpans[activeSpanIndex], theme);
|
|
106
107
|
activeSpanIndex += 1;
|
|
107
108
|
}
|
|
108
109
|
|
|
109
|
-
|
|
110
|
-
if (char === "|") {
|
|
110
|
+
if (grapheme === "|") {
|
|
111
111
|
continue;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
output +=
|
|
115
|
-
visibleColumn +=
|
|
114
|
+
output += grapheme;
|
|
115
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
116
116
|
|
|
117
117
|
let closedSpan = false;
|
|
118
118
|
for (const span of rowSpans) {
|
|
@@ -129,7 +129,7 @@ function renderAnsiLine(line: string, spans: TerminalStyleSpan[], y: number, the
|
|
|
129
129
|
closedSpan = true;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
|
-
if (closedSpan && visibleColumn <= line
|
|
132
|
+
if (closedSpan && visibleColumn <= terminalCellWidth(line)) {
|
|
133
133
|
for (const span of rowSpans) {
|
|
134
134
|
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
135
135
|
output += spanAnsiOpen(span, theme);
|
|
@@ -174,22 +174,20 @@ function formatPlainLine(line: string, spans: TerminalStyleSpan[], y: number, th
|
|
|
174
174
|
&& !token?.plainSuffix
|
|
175
175
|
&& span.x1 === 1
|
|
176
176
|
&& y === firstFallbackFocusY
|
|
177
|
-
&& span.x2 <= line.length + 1
|
|
178
177
|
&& !line.trimStart().startsWith(">")
|
|
179
178
|
&& !line.includes("|")
|
|
180
179
|
&& !line.includes("[>");
|
|
181
180
|
return {
|
|
182
181
|
...span,
|
|
183
|
-
x2: fallbackFocusMarker ? Math.min(span.x2, line.trimEnd()
|
|
182
|
+
x2: fallbackFocusMarker ? Math.min(span.x2, terminalCellWidth(line.trimEnd()) + 1) : span.x2,
|
|
184
183
|
plainPrefix: token?.plainPrefix ?? span.style?.plainPrefix ?? (fallbackFocusMarker ? ">" : ""),
|
|
185
184
|
plainSuffix: token?.plainSuffix ?? span.style?.plainSuffix ?? (fallbackFocusMarker ? "<" : "")
|
|
186
185
|
};
|
|
187
186
|
}).filter((span) => Boolean(span.plainPrefix || span.plainSuffix));
|
|
188
187
|
|
|
189
|
-
for (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
output += char;
|
|
188
|
+
for (const grapheme of terminalGraphemes(line)) {
|
|
189
|
+
if (grapheme === "|") {
|
|
190
|
+
output += grapheme;
|
|
193
191
|
continue;
|
|
194
192
|
}
|
|
195
193
|
|
|
@@ -199,8 +197,8 @@ function formatPlainLine(line: string, spans: TerminalStyleSpan[], y: number, th
|
|
|
199
197
|
}
|
|
200
198
|
}
|
|
201
199
|
|
|
202
|
-
output +=
|
|
203
|
-
visibleColumn +=
|
|
200
|
+
output += grapheme;
|
|
201
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
204
202
|
|
|
205
203
|
for (const span of rowSpans) {
|
|
206
204
|
if (span.x2 === visibleColumn) {
|
|
@@ -236,6 +234,134 @@ export function toAnsiFrame(lines: string[], cursor: CursorPosition | null, span
|
|
|
236
234
|
return `\u001b[?25l\u001b[H${ansiLines.join("\n")}${cursorCode}`;
|
|
237
235
|
}
|
|
238
236
|
|
|
237
|
+
function commonPrefixCellWidth(previousLine: string, nextLine: string) {
|
|
238
|
+
const previousGraphemes = terminalGraphemes(previousLine);
|
|
239
|
+
const nextGraphemes = terminalGraphemes(nextLine);
|
|
240
|
+
const length = Math.min(previousGraphemes.length, nextGraphemes.length);
|
|
241
|
+
let width = 0;
|
|
242
|
+
|
|
243
|
+
for (let index = 0; index < length; index += 1) {
|
|
244
|
+
if (previousGraphemes[index] !== nextGraphemes[index]) {
|
|
245
|
+
return width;
|
|
246
|
+
}
|
|
247
|
+
if (previousGraphemes[index] !== "|") {
|
|
248
|
+
width += terminalCellWidth(previousGraphemes[index]);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return width;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function renderAnsiLineSuffix(line: string, spans: TerminalStyleSpan[], y: number, startColumn: number, theme?: TerminalTheme) {
|
|
256
|
+
let output = "";
|
|
257
|
+
let visibleColumn = Math.max(1, startColumn);
|
|
258
|
+
const rowSpans = lineSpans(spans, y);
|
|
259
|
+
let activeSpanIndex = rowSpans.findIndex((span) => span.x1 >= visibleColumn);
|
|
260
|
+
if (activeSpanIndex < 0) {
|
|
261
|
+
activeSpanIndex = rowSpans.length;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const span of rowSpans) {
|
|
265
|
+
if (span.x1 <= visibleColumn && span.x2 > visibleColumn) {
|
|
266
|
+
output += spanAnsiOpen(span, theme);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
for (const grapheme of terminalGraphemes(dropTerminalCells(line, visibleColumn - 1))) {
|
|
271
|
+
while (rowSpans[activeSpanIndex]?.x1 === visibleColumn) {
|
|
272
|
+
output += spanAnsiOpen(rowSpans[activeSpanIndex], theme);
|
|
273
|
+
activeSpanIndex += 1;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (grapheme === "|") {
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
output += grapheme;
|
|
281
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
282
|
+
|
|
283
|
+
let closedSpan = false;
|
|
284
|
+
for (const span of rowSpans) {
|
|
285
|
+
if (span.x2 === visibleColumn) {
|
|
286
|
+
if (span.kind !== "focus") {
|
|
287
|
+
output += spanAnsiClose(span, theme);
|
|
288
|
+
closedSpan = true;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
for (const span of rowSpans) {
|
|
293
|
+
if (span.x2 === visibleColumn && span.kind === "focus") {
|
|
294
|
+
output += spanAnsiClose(span, theme);
|
|
295
|
+
closedSpan = true;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (closedSpan && visibleColumn <= terminalCellWidth(line)) {
|
|
299
|
+
for (const span of rowSpans) {
|
|
300
|
+
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
301
|
+
output += spanAnsiOpen(span, theme);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
for (const span of rowSpans) {
|
|
308
|
+
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
309
|
+
if (span.kind !== "focus") {
|
|
310
|
+
output += spanAnsiClose(span, theme);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
for (const span of rowSpans) {
|
|
315
|
+
if (span.x1 < visibleColumn && span.x2 > visibleColumn && span.kind === "focus") {
|
|
316
|
+
output += spanAnsiClose(span, theme);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return output;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
function sameSpanStyle(previous: TerminalStyleSpan["style"], next: TerminalStyleSpan["style"]) {
|
|
325
|
+
if (previous === next) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
if (!previous || !next) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return previous.color === next.color
|
|
332
|
+
&& previous.background === next.background
|
|
333
|
+
&& previous.plainPrefix === next.plainPrefix
|
|
334
|
+
&& previous.plainSuffix === next.plainSuffix
|
|
335
|
+
&& JSON.stringify(previous.border) === JSON.stringify(next.border)
|
|
336
|
+
&& JSON.stringify(previous.padding) === JSON.stringify(next.padding);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function sameLineSpans(previousSpans: TerminalStyleSpan[], nextSpans: TerminalStyleSpan[], y: number) {
|
|
340
|
+
if (previousSpans.length === 0 && nextSpans.length === 0) {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const previousRowSpans = lineSpans(previousSpans, y);
|
|
345
|
+
const nextRowSpans = lineSpans(nextSpans, y);
|
|
346
|
+
if (previousRowSpans.length !== nextRowSpans.length) {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
for (let index = 0; index < previousRowSpans.length; index += 1) {
|
|
351
|
+
const previous = previousRowSpans[index];
|
|
352
|
+
const next = nextRowSpans[index];
|
|
353
|
+
if (previous.kind !== next.kind
|
|
354
|
+
|| previous.x1 !== next.x1
|
|
355
|
+
|| previous.x2 !== next.x2
|
|
356
|
+
|| previous.y !== next.y
|
|
357
|
+
|| !sameSpanStyle(previous.style, next.style)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
239
365
|
export function createAnsiFrameDiff(
|
|
240
366
|
previousAnsiLines: string[],
|
|
241
367
|
nextAnsiLines: string[],
|
|
@@ -269,13 +395,77 @@ export function createAnsiFrameDiff(
|
|
|
269
395
|
};
|
|
270
396
|
}
|
|
271
397
|
|
|
398
|
+
function createAnsiFramePatch(
|
|
399
|
+
previousLines: string[],
|
|
400
|
+
previousAnsiLines: string[],
|
|
401
|
+
nextLines: string[],
|
|
402
|
+
previousSpans: TerminalStyleSpan[],
|
|
403
|
+
nextSpans: TerminalStyleSpan[],
|
|
404
|
+
cursor: CursorPosition | null,
|
|
405
|
+
options: AnsiFrameOptions = {}
|
|
406
|
+
): AnsiFrameDiffResult & { nextAnsiLines: string[] } {
|
|
407
|
+
const nextAnsiLines: string[] = [];
|
|
408
|
+
const changedLineIndexes: number[] = [];
|
|
409
|
+
const writes: string[] = ["\u001b[?25l"];
|
|
410
|
+
const maxLines = Math.max(previousAnsiLines.length, nextLines.length);
|
|
411
|
+
|
|
412
|
+
for (let i = 0; i < maxLines; i += 1) {
|
|
413
|
+
const previousLine = previousLines[i] || "";
|
|
414
|
+
const nextLine = nextLines[i] || "";
|
|
415
|
+
const previousAnsiLine = previousAnsiLines[i] || "";
|
|
416
|
+
const spansMatch = sameLineSpans(previousSpans, nextSpans, i + 1);
|
|
417
|
+
const nextAnsiLine = i < nextLines.length && previousLine === nextLine && spansMatch
|
|
418
|
+
? previousAnsiLine
|
|
419
|
+
: i < nextLines.length
|
|
420
|
+
? renderAnsiLine(nextLine, nextSpans, i + 1, options.theme)
|
|
421
|
+
: "";
|
|
422
|
+
nextAnsiLines[i] = nextAnsiLine;
|
|
423
|
+
if (nextAnsiLine === previousAnsiLine) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
changedLineIndexes.push(i);
|
|
428
|
+
const canPatchSuffix = cursor?.y === i + 1
|
|
429
|
+
&& lineSpans(previousSpans, i + 1).length === 0
|
|
430
|
+
&& lineSpans(nextSpans, i + 1).length === 0;
|
|
431
|
+
const prefixWidth = canPatchSuffix && i < previousLines.length && i < nextLines.length && previousLine !== nextLine
|
|
432
|
+
? commonPrefixCellWidth(previousLine, nextLine)
|
|
433
|
+
: 0;
|
|
434
|
+
|
|
435
|
+
if (prefixWidth > 0) {
|
|
436
|
+
const startColumn = prefixWidth + 1;
|
|
437
|
+
const suffix = renderAnsiLineSuffix(nextLine, nextSpans, i + 1, startColumn, options.theme);
|
|
438
|
+
writes.push(`\u001b[${i + 1};${startColumn}H${suffix}\u001b[0m\u001b[K`);
|
|
439
|
+
} else {
|
|
440
|
+
writes.push(`\u001b[${i + 1};1H${nextAnsiLine}\u001b[0m\u001b[K`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
if (cursor) {
|
|
445
|
+
writes.push(`\u001b[${cursor.y};${cursor.x}H`);
|
|
446
|
+
}
|
|
447
|
+
if (options.showCursor !== false || (options.showCursorWhenFrameHasCursor === true && cursor)) {
|
|
448
|
+
writes.push(ANSI_SHOW_CURSOR);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return {
|
|
452
|
+
changedLineIndexes,
|
|
453
|
+
outputChunk: writes.join(""),
|
|
454
|
+
restoresCursor: Boolean(cursor),
|
|
455
|
+
nextAnsiLines: nextAnsiLines.slice(0, nextLines.length)
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
272
459
|
export function createAnsiDiffWriter(options: AnsiFrameOptions = {}) {
|
|
460
|
+
let previousLines: string[] = [];
|
|
273
461
|
let previousAnsiLines: string[] = [];
|
|
462
|
+
let previousSpans: TerminalStyleSpan[] = [];
|
|
274
463
|
|
|
275
464
|
return function toAnsiDiff(lines: string[], cursor: CursorPosition | null, spans: TerminalStyleSpan[] = []) {
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
previousAnsiLines =
|
|
465
|
+
const diff = createAnsiFramePatch(previousLines, previousAnsiLines, lines, previousSpans, spans, cursor, options);
|
|
466
|
+
previousLines = lines.slice();
|
|
467
|
+
previousAnsiLines = diff.nextAnsiLines.slice();
|
|
468
|
+
previousSpans = spans.map((span) => ({ ...span }));
|
|
279
469
|
return diff.outputChunk;
|
|
280
470
|
};
|
|
281
471
|
}
|
package/src/events.ts
CHANGED
|
@@ -146,6 +146,8 @@ export function parseTerminalKey(chunk: string | Uint8Array) {
|
|
|
146
146
|
if (value === "\u001b[B") return "DOWN";
|
|
147
147
|
if (value === "\u001b[C") return "RIGHT";
|
|
148
148
|
if (value === "\u001b[D") return "LEFT";
|
|
149
|
+
if (value === "\u001b[1;2A") return "SHIFT_UP";
|
|
150
|
+
if (value === "\u001b[1;2B") return "SHIFT_DOWN";
|
|
149
151
|
if (value === "\u001b[1;2C") return "SHIFT_RIGHT";
|
|
150
152
|
if (value === "\u001b[1;2D") return "SHIFT_LEFT";
|
|
151
153
|
if (value === "\u001b[1;3C" || value === "\u001bf") return "ALT_RIGHT";
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TerminalStyleSpan } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const FULL_FRAME_SPAN = Symbol("valyrian.terminal.fullFrameSpan");
|
|
4
|
+
const FULL_ROW_SPAN = Symbol("valyrian.terminal.fullRowSpan");
|
|
5
|
+
|
|
6
|
+
type InternalFullFrameSpan = TerminalStyleSpan & { [FULL_FRAME_SPAN]?: true };
|
|
7
|
+
type InternalFullRowSpan = TerminalStyleSpan & { [FULL_ROW_SPAN]?: true };
|
|
8
|
+
|
|
9
|
+
export function markFullFrameSpan(span: TerminalStyleSpan): TerminalStyleSpan {
|
|
10
|
+
Object.defineProperty(span, FULL_FRAME_SPAN, { value: true, enumerable: false, configurable: true });
|
|
11
|
+
return span;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function isFullFrameSpan(span: TerminalStyleSpan) {
|
|
15
|
+
return (span as InternalFullFrameSpan)[FULL_FRAME_SPAN] === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function markFullRowSpan(span: TerminalStyleSpan): TerminalStyleSpan {
|
|
19
|
+
Object.defineProperty(span, FULL_ROW_SPAN, { value: true, enumerable: false, configurable: true });
|
|
20
|
+
return span;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isFullRowSpan(span: TerminalStyleSpan) {
|
|
24
|
+
return (span as InternalFullRowSpan)[FULL_ROW_SPAN] === true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function cloneStyleSpan(span: TerminalStyleSpan, patch: Partial<TerminalStyleSpan>): TerminalStyleSpan {
|
|
28
|
+
const next = { ...span, ...patch };
|
|
29
|
+
if (isFullFrameSpan(span)) {
|
|
30
|
+
markFullFrameSpan(next);
|
|
31
|
+
}
|
|
32
|
+
if (isFullRowSpan(span)) {
|
|
33
|
+
markFullRowSpan(next);
|
|
34
|
+
}
|
|
35
|
+
return next;
|
|
36
|
+
}
|