@valyrianjs/terminal 0.2.0 → 0.2.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/dist/ansi.d.ts +2 -0
- package/dist/ansi.d.ts.map +1 -1
- package/dist/ansi.js +23 -13
- package/dist/ansi.js.map +1 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +10 -2
- 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/keymap.d.ts.map +1 -1
- package/dist/keymap.js +4 -2
- package/dist/keymap.js.map +1 -1
- package/dist/layout.d.ts +5 -1
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +55 -24
- package/dist/layout.js.map +1 -1
- package/dist/mouse.d.ts +6 -0
- package/dist/mouse.d.ts.map +1 -1
- package/dist/mouse.js +38 -17
- package/dist/mouse.js.map +1 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +8 -1
- package/dist/primitives.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +266 -70
- package/dist/render.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +13 -5
- package/dist/runtime.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +325 -83
- 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 +114 -0
- package/dist/text.js.map +1 -1
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +3 -0
- package/dist/theme.js.map +1 -1
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +18 -4
- package/dist/tree.js.map +1 -1
- package/dist/types.d.ts +41 -4
- package/dist/types.d.ts.map +1 -1
- package/docs/api-reference.md +18 -8
- package/docs/cookbook.md +1 -1
- package/docs/interaction-model.md +10 -8
- package/docs/primitive-gallery.md +9 -5
- 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 +38 -22
- package/package.json +3 -2
- package/src/ansi.ts +23 -13
- package/src/events.ts +6 -2
- package/src/frame-style.ts +36 -0
- package/src/keymap.ts +4 -2
- package/src/layout.ts +59 -25
- package/src/mouse.ts +41 -16
- package/src/primitives.ts +8 -1
- package/src/render.ts +286 -71
- package/src/runtime.ts +13 -5
- package/src/session.ts +343 -79
- package/src/text.ts +148 -0
- package/src/theme.ts +3 -0
- package/src/tree.ts +19 -4
- package/src/types.ts +48 -3
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.
|
|
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,16 +1196,20 @@ 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`, `renderItem`, `virtualized`, `onchange`, `onpress`, `ondoublepress`, `oncontextpress`.
|
|
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
|
|
|
1203
1203
|
```tsx
|
|
1204
|
-
<List id="menu" items={["Inbox", "Done"]}
|
|
1204
|
+
<List id="menu" items={["Inbox", "Done"]} itemKey={(item) => item}>
|
|
1205
|
+
{(item, ctx) => `${ctx.active ? "› " : " "}${item}`}
|
|
1206
|
+
</List>
|
|
1205
1207
|
```
|
|
1206
1208
|
|
|
1207
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`.
|
|
1208
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`. `Shift+Up` and `Shift+Down` are shown as app-defined commands, not built-in reorder behavior.
|
|
1212
|
+
|
|
1209
1213
|
### `Table`
|
|
1210
1214
|
|
|
1211
1215
|
Use `Table` for compact data detail and aligned text pairs.
|
|
@@ -1300,7 +1304,7 @@ Use `Overlay` for command panels, dialogs, palettes, and focused chooser surface
|
|
|
1300
1304
|
|
|
1301
1305
|
**What it is:** A positioned layer over the current frame.
|
|
1302
1306
|
**Use it for:** command panels, dialogs, palettes, and focused chooser surfaces.
|
|
1303
|
-
**Core props:** `id`, `margin`, `trapFocus`, `style`. `margin` is required and accepts a number, a percentage string, or `{ x, y }` with number or percentage-string values. When `Overlay` is the root passed to `renderTerminal()`, pass exact `{ cols, rows }` as the second argument so margin can resolve against terminal dimensions.
|
|
1307
|
+
**Core props:** `id`, `margin`, `trapFocus`, `style`. `margin` is required and accepts a number, a percentage string, or `{ x, y }` with number or percentage-string values. When `Overlay` is the root passed to `renderTerminal()`, pass exact `{ cols, rows }` as the second argument so margin can resolve against terminal dimensions. Later sibling overlays paint and receive pointer/focus routing above earlier overlays. Overlay background styles cover their full assigned surface, not just text cells.
|
|
1304
1308
|
|
|
1305
1309
|
**Minimal example:**
|
|
1306
1310
|
|
|
@@ -1355,7 +1359,7 @@ This guide explains how interactive primitives receive input and report events.
|
|
|
1355
1359
|
|
|
1356
1360
|
## Input events
|
|
1357
1361
|
|
|
1358
|
-
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.
|
|
1359
1363
|
|
|
1360
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.
|
|
1361
1365
|
|
|
@@ -1395,7 +1399,7 @@ Main handlers:
|
|
|
1395
1399
|
|
|
1396
1400
|
### `Editor`
|
|
1397
1401
|
|
|
1398
|
-
`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.
|
|
1399
1403
|
|
|
1400
1404
|
Supported behavior includes:
|
|
1401
1405
|
|
|
@@ -1436,15 +1440,17 @@ A quick second primary mouse press keeps the normal activation behavior and also
|
|
|
1436
1440
|
|
|
1437
1441
|
### `List`
|
|
1438
1442
|
|
|
1439
|
-
`List` models row selection and activation.
|
|
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.
|
|
1440
1444
|
|
|
1441
1445
|
Supported behavior includes:
|
|
1442
1446
|
|
|
1443
|
-
- `UP` and `
|
|
1444
|
-
- `
|
|
1445
|
-
- `
|
|
1447
|
+
- `UP` and `DOWN` move the active row and emit `onchange`
|
|
1448
|
+
- `PageUp`, `PageDown`, `Home`, and `End` jump the active row and keep it visible
|
|
1449
|
+
- `LEFT`, `RIGHT`, `SHIFT_UP`, and `SHIFT_DOWN` have no default list command, so applications can bind them
|
|
1450
|
+
- `ENTER` selects and activates the active row
|
|
1451
|
+
- mouse click selects and activates the target row
|
|
1446
1452
|
- mouse hover reports row details when mouse input is connected
|
|
1447
|
-
- quick primary mouse
|
|
1453
|
+
- quick primary mouse clicks on the same row emit `ondoublepress` without a second `onpress`
|
|
1448
1454
|
- secondary mouse presses emit `oncontextpress` for the target row
|
|
1449
1455
|
|
|
1450
1456
|
Main handlers:
|
|
@@ -1459,7 +1465,7 @@ Main handlers:
|
|
|
1459
1465
|
- `oncapturestart`
|
|
1460
1466
|
- `oncaptureend`
|
|
1461
1467
|
|
|
1462
|
-
`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.
|
|
1463
1469
|
|
|
1464
1470
|
### `ScrollView`
|
|
1465
1471
|
|
|
@@ -5038,7 +5044,7 @@ Use `style`, `styles`, and `state` for normal component styling:
|
|
|
5038
5044
|
- `styles` maps visual states to dot path recipes or inline style objects.
|
|
5039
5045
|
- `state` declares app-owned states such as `loading`, `warning`, `success`, `muted`, `expanded`, or `dropTarget`.
|
|
5040
5046
|
|
|
5041
|
-
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`.
|
|
5042
5048
|
|
|
5043
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.
|
|
5044
5050
|
|
|
@@ -5047,6 +5053,7 @@ Renderer notes:
|
|
|
5047
5053
|
- Raw ANSI escape control is not the normal public styling API. Use semantic styles and let `ansiOutput()` serialize frames.
|
|
5048
5054
|
- Layout margin is not supported; use padding, `gap`, `Fixed`, `Split`, or explicit text spacing.
|
|
5049
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.
|
|
5050
5057
|
|
|
5051
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).
|
|
5052
5059
|
|
|
@@ -5172,7 +5179,7 @@ Props:
|
|
|
5172
5179
|
|
|
5173
5180
|
Draws a clipped region over the current frame. `margin` sets the distance from the overlay to the frame edges; numbers use cells and percentage strings such as `"10%"` resolve per axis. Use `{ x, y }` when the two axes need different margins. `trapFocus` keeps traversal inside the overlay while it is active.
|
|
5174
5181
|
|
|
5175
|
-
Overlay calculates its size from the available frame, stays centered by its margins, and keeps app routing and pane movement in your app layer.
|
|
5182
|
+
Overlay calculates its size from the available frame, stays centered by its margins, and keeps app routing and pane movement in your app layer. Direct sibling overlays use source order: later overlays paint above earlier overlays, receive pointer hits first, and lead trapped focus traversal. Overlay styles paint the full assigned surface, including empty cells behind child controls.
|
|
5176
5183
|
|
|
5177
5184
|
### `FocusScope`
|
|
5178
5185
|
|
|
@@ -5227,7 +5234,9 @@ Props:
|
|
|
5227
5234
|
- `focusable?: boolean`
|
|
5228
5235
|
- `value?: string`
|
|
5229
5236
|
- `placeholder?: string`
|
|
5237
|
+
- `width?: number`
|
|
5230
5238
|
- `height?: number`
|
|
5239
|
+
- `fill?: boolean`
|
|
5231
5240
|
- shared visual props: `style`, `styles`, `state`
|
|
5232
5241
|
- `onchange?(event)`
|
|
5233
5242
|
- `oninput?(event)`
|
|
@@ -5240,7 +5249,7 @@ Payloads:
|
|
|
5240
5249
|
- `TerminalEditorSubmitEventPayload` - `{ type: "submit", id, value }`
|
|
5241
5250
|
- `TerminalEditorCancelEventPayload` - `{ type: "cancel", id, value }`
|
|
5242
5251
|
|
|
5243
|
-
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.
|
|
5244
5253
|
|
|
5245
5254
|
### `Button`
|
|
5246
5255
|
|
|
@@ -5269,11 +5278,17 @@ Props:
|
|
|
5269
5278
|
- `pointerCapture?: boolean`
|
|
5270
5279
|
- `items?: T[]`
|
|
5271
5280
|
- `virtualized?: boolean`
|
|
5281
|
+
- `height?: number`
|
|
5272
5282
|
- `itemHeight?: 1`
|
|
5273
5283
|
- `overscan?: number`
|
|
5284
|
+
- `wrap?: boolean`
|
|
5285
|
+
- `itemKey?(item, index): string | number`
|
|
5286
|
+
- `showActive?: boolean`
|
|
5274
5287
|
- shared visual props: `style`, `styles`, `state`
|
|
5275
|
-
- `
|
|
5288
|
+
- `children?(item, ctx): any`
|
|
5289
|
+
- `renderItem?(item, index): any`
|
|
5276
5290
|
- `onchange?(event)`
|
|
5291
|
+
- `onviewportchange?(event)`
|
|
5277
5292
|
- `onpress?(event)`
|
|
5278
5293
|
- `ondoublepress?(event)`
|
|
5279
5294
|
- `oncontextpress?(event)`
|
|
@@ -5285,12 +5300,13 @@ Props:
|
|
|
5285
5300
|
|
|
5286
5301
|
Payloads:
|
|
5287
5302
|
|
|
5288
|
-
- `TerminalListChangeEventPayload<T>` - `{ type: "change", id, index, value }`
|
|
5289
|
-
- `
|
|
5290
|
-
- `
|
|
5303
|
+
- `TerminalListChangeEventPayload<T>` - `{ type: "change", id, index, key?, value, activeIndex, selectedIndex, viewportOffset, viewportRows }`
|
|
5304
|
+
- `TerminalListViewportChangeEventPayload` - `{ type: "viewportchange", id, offset, rows, activeIndex, selectedIndex, viewportOffset, viewportRows }`
|
|
5305
|
+
- `TerminalListPressEventPayload<T>` - `{ type: "press" | "doublepress" | "contextpress", id, index, key?, value, activeIndex, selectedIndex, viewportOffset, viewportRows }`
|
|
5306
|
+
- `TerminalListPointerEventPayload<T>` - `{ type: "hover" | "rowenter" | "rowleave", id, row, index, key?, value, x, y }`
|
|
5291
5307
|
- `TerminalCaptureEventPayload` - `{ type: "capturestart" | "captureend", id, source, row, x, y }`
|
|
5292
5308
|
|
|
5293
|
-
Supports selection, activation, double press on the same row, context press for the target row, hover payloads, optional virtualization, and pointer capture.
|
|
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.
|
|
5294
5310
|
|
|
5295
5311
|
### `ScrollView`
|
|
5296
5312
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valyrianjs/terminal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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 { 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
|
|
|
@@ -5,6 +6,8 @@ export const ANSI_ENTER_ALTERNATE_SCREEN = "\u001b[?1049h";
|
|
|
5
6
|
export const ANSI_EXIT_ALTERNATE_SCREEN = "\u001b[?1049l";
|
|
6
7
|
export const ANSI_HIDE_CURSOR = "\u001b[?25l";
|
|
7
8
|
export const ANSI_SHOW_CURSOR = "\u001b[?25h";
|
|
9
|
+
export const ANSI_ENABLE_MOUSE_REPORTING = "\u001b[?1000h\u001b[?1002h\u001b[?1006h";
|
|
10
|
+
export const ANSI_DISABLE_MOUSE_REPORTING = "\u001b[?1002l\u001b[?1000l\u001b[?1006l";
|
|
8
11
|
|
|
9
12
|
type AnsiFrameOptions = {
|
|
10
13
|
showCursor?: boolean;
|
|
@@ -98,30 +101,39 @@ function renderAnsiLine(line: string, spans: TerminalStyleSpan[], y: number, the
|
|
|
98
101
|
const rowSpans = lineSpans(spans, y);
|
|
99
102
|
let activeSpanIndex = 0;
|
|
100
103
|
|
|
101
|
-
for (
|
|
104
|
+
for (const grapheme of terminalGraphemes(line)) {
|
|
102
105
|
while (rowSpans[activeSpanIndex]?.x1 === visibleColumn) {
|
|
103
106
|
output += spanAnsiOpen(rowSpans[activeSpanIndex], theme);
|
|
104
107
|
activeSpanIndex += 1;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
if (char === "|") {
|
|
110
|
+
if (grapheme === "|") {
|
|
109
111
|
continue;
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
output +=
|
|
113
|
-
visibleColumn +=
|
|
114
|
+
output += grapheme;
|
|
115
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
114
116
|
|
|
117
|
+
let closedSpan = false;
|
|
115
118
|
for (const span of rowSpans) {
|
|
116
119
|
if (span.x2 === visibleColumn) {
|
|
117
120
|
if (span.kind !== "focus") {
|
|
118
121
|
output += spanAnsiClose(span, theme);
|
|
122
|
+
closedSpan = true;
|
|
119
123
|
}
|
|
120
124
|
}
|
|
121
125
|
}
|
|
122
126
|
for (const span of rowSpans) {
|
|
123
127
|
if (span.x2 === visibleColumn && span.kind === "focus") {
|
|
124
128
|
output += spanAnsiClose(span, theme);
|
|
129
|
+
closedSpan = true;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (closedSpan && visibleColumn <= terminalCellWidth(line)) {
|
|
133
|
+
for (const span of rowSpans) {
|
|
134
|
+
if (span.x1 < visibleColumn && span.x2 > visibleColumn) {
|
|
135
|
+
output += spanAnsiOpen(span, theme);
|
|
136
|
+
}
|
|
125
137
|
}
|
|
126
138
|
}
|
|
127
139
|
}
|
|
@@ -162,22 +174,20 @@ function formatPlainLine(line: string, spans: TerminalStyleSpan[], y: number, th
|
|
|
162
174
|
&& !token?.plainSuffix
|
|
163
175
|
&& span.x1 === 1
|
|
164
176
|
&& y === firstFallbackFocusY
|
|
165
|
-
&& span.x2 <= line.length + 1
|
|
166
177
|
&& !line.trimStart().startsWith(">")
|
|
167
178
|
&& !line.includes("|")
|
|
168
179
|
&& !line.includes("[>");
|
|
169
180
|
return {
|
|
170
181
|
...span,
|
|
171
|
-
x2: fallbackFocusMarker ? Math.min(span.x2, line.trimEnd()
|
|
182
|
+
x2: fallbackFocusMarker ? Math.min(span.x2, terminalCellWidth(line.trimEnd()) + 1) : span.x2,
|
|
172
183
|
plainPrefix: token?.plainPrefix ?? span.style?.plainPrefix ?? (fallbackFocusMarker ? ">" : ""),
|
|
173
184
|
plainSuffix: token?.plainSuffix ?? span.style?.plainSuffix ?? (fallbackFocusMarker ? "<" : "")
|
|
174
185
|
};
|
|
175
186
|
}).filter((span) => Boolean(span.plainPrefix || span.plainSuffix));
|
|
176
187
|
|
|
177
|
-
for (
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
output += char;
|
|
188
|
+
for (const grapheme of terminalGraphemes(line)) {
|
|
189
|
+
if (grapheme === "|") {
|
|
190
|
+
output += grapheme;
|
|
181
191
|
continue;
|
|
182
192
|
}
|
|
183
193
|
|
|
@@ -187,8 +197,8 @@ function formatPlainLine(line: string, spans: TerminalStyleSpan[], y: number, th
|
|
|
187
197
|
}
|
|
188
198
|
}
|
|
189
199
|
|
|
190
|
-
output +=
|
|
191
|
-
visibleColumn +=
|
|
200
|
+
output += grapheme;
|
|
201
|
+
visibleColumn += terminalCellWidth(grapheme);
|
|
192
202
|
|
|
193
203
|
for (const span of rowSpans) {
|
|
194
204
|
if (span.x2 === visibleColumn) {
|
package/src/events.ts
CHANGED
|
@@ -146,14 +146,18 @@ 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";
|
|
152
154
|
if (value === "\u001b[1;3D" || value === "\u001bb") return "ALT_LEFT";
|
|
153
155
|
if (value === "\u001b[3~") return "DELETE";
|
|
156
|
+
if (value === "\u001b[5~") return "PAGEUP";
|
|
157
|
+
if (value === "\u001b[6~") return "PAGEDOWN";
|
|
154
158
|
if (value === "\u0008" || value === "\u007f") return "BACKSPACE";
|
|
155
|
-
if (value === "\u001b[H") return "HOME";
|
|
156
|
-
if (value === "\u001b[F") return "END";
|
|
159
|
+
if (value === "\u001b[H" || value === "\u001b[1~") return "HOME";
|
|
160
|
+
if (value === "\u001b[F" || value === "\u001b[4~") return "END";
|
|
157
161
|
if (value === "\u0001") return "CTRL_A";
|
|
158
162
|
if (value === "\u0003") return "CTRL_C";
|
|
159
163
|
if (value === "\u000b") return "CTRL_K";
|
|
@@ -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
|
+
}
|
package/src/keymap.ts
CHANGED
|
@@ -32,9 +32,11 @@ export const defaultTerminalKeyBindings: TerminalKeyBinding[] = [
|
|
|
32
32
|
{ key: "ENTER", command: { id: "button.press" }, scope: "button", when: { focusedTag: "terminal-button" } },
|
|
33
33
|
{ key: "SPACE", command: { id: "button.press" }, scope: "button", when: { focusedTag: "terminal-button" } },
|
|
34
34
|
{ key: "UP", command: { id: "list.prev" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
35
|
-
{ key: "LEFT", command: { id: "list.prev" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
36
35
|
{ key: "DOWN", command: { id: "list.next" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
37
|
-
{ key: "
|
|
36
|
+
{ key: "PAGEUP", command: { id: "list.pageUp" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
37
|
+
{ key: "PAGEDOWN", command: { id: "list.pageDown" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
38
|
+
{ key: "HOME", command: { id: "list.home" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
39
|
+
{ key: "END", command: { id: "list.end" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
38
40
|
{ key: "ENTER", command: { id: "list.press" }, scope: "list", when: { focusedTag: "terminal-list" } },
|
|
39
41
|
{ key: "UP", command: { id: "scroll.up" }, scope: "scroll", when: { focusedTag: "terminal-scroll" } },
|
|
40
42
|
{ key: "DOWN", command: { id: "scroll.down" }, scope: "scroll", when: { focusedTag: "terminal-scroll" } }
|
package/src/layout.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { cloneStyleSpan, isFullFrameSpan, isFullRowSpan } from "./frame-style.js";
|
|
2
|
+
import { dropTerminalCells, padEndTerminalCells, sliceTerminalCells, terminalCellWidth } from "./text.js";
|
|
1
3
|
import type { CursorPosition, TerminalFrame, TerminalHitbox, TerminalStyleSpan } from "./types.js";
|
|
2
4
|
|
|
3
5
|
function repeat(char: string, count: number) {
|
|
@@ -9,7 +11,7 @@ export function createFrame(lines: string[], hitboxes: TerminalHitbox[] = [], cu
|
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
export function getFrameWidth(frame: TerminalFrame) {
|
|
12
|
-
return frame.lines.reduce((max, line) => Math.max(max, line
|
|
14
|
+
return frame.lines.reduce((max, line) => Math.max(max, terminalCellWidth(line)), 0);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export function getFrameHeight(frame: TerminalFrame) {
|
|
@@ -25,15 +27,16 @@ export function shiftFrame(frame: TerminalFrame, dx: number, dy: number): Termin
|
|
|
25
27
|
x2: box.x2 + dx,
|
|
26
28
|
y1: box.y1 + dy,
|
|
27
29
|
y2: box.y2 + dy,
|
|
28
|
-
textStartX: typeof box.textStartX === "number" ? box.textStartX + dx : undefined
|
|
30
|
+
textStartX: typeof box.textStartX === "number" ? box.textStartX + dx : undefined,
|
|
31
|
+
contentY: typeof box.contentY === "number" ? box.contentY + dy : undefined
|
|
29
32
|
})),
|
|
30
33
|
cursor: frame.cursor ? { x: frame.cursor.x + dx, y: frame.cursor.y + dy } : null,
|
|
31
|
-
spans: frame.spans.map((span) => (
|
|
34
|
+
spans: frame.spans.map((span) => cloneStyleSpan(span, { x1: span.x1 + dx, x2: span.x2 + dx, y: span.y + dy }))
|
|
32
35
|
};
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
function normalizeLines(frame: TerminalFrame, width: number, height: number) {
|
|
36
|
-
const lines = frame.lines.map((line) => line
|
|
39
|
+
const lines = frame.lines.map((line) => padEndTerminalCells(line, width));
|
|
37
40
|
while (lines.length < height) {
|
|
38
41
|
lines.push(repeat(" ", width));
|
|
39
42
|
}
|
|
@@ -56,15 +59,15 @@ export function mergeVertical(frames: TerminalFrame[], options: { gap?: number }
|
|
|
56
59
|
|
|
57
60
|
for (let i = 0; i < filtered.length; i += 1) {
|
|
58
61
|
const frame = filtered[i];
|
|
59
|
-
const
|
|
60
|
-
lines.push(...
|
|
61
|
-
const shifted = shiftFrame(
|
|
62
|
+
const normalizedFrame = fitFrame(frame, width, getFrameHeight(frame), { expandFullFrameSpans: true });
|
|
63
|
+
lines.push(...normalizedFrame.lines);
|
|
64
|
+
const shifted = shiftFrame(normalizedFrame, 0, rowOffset);
|
|
62
65
|
hitboxes.push(...shifted.hitboxes);
|
|
63
66
|
spans.push(...shifted.spans);
|
|
64
67
|
if (!cursor && frame.cursor) {
|
|
65
68
|
cursor = { x: frame.cursor.x, y: frame.cursor.y + rowOffset };
|
|
66
69
|
}
|
|
67
|
-
rowOffset +=
|
|
70
|
+
rowOffset += normalizedFrame.lines.length;
|
|
68
71
|
if (i < filtered.length - 1 && gap > 0) {
|
|
69
72
|
for (let j = 0; j < gap; j += 1) {
|
|
70
73
|
lines.push(repeat(" ", width));
|
|
@@ -86,9 +89,8 @@ export function mergeHorizontal(frames: TerminalFrame[], options: { gap?: number
|
|
|
86
89
|
const widths = filtered.map(getFrameWidth);
|
|
87
90
|
const height = filtered.reduce((max, frame) => Math.max(max, getFrameHeight(frame)), 0);
|
|
88
91
|
const normalizedFrames = filtered.map((frame, index) => ({
|
|
89
|
-
frame,
|
|
90
|
-
width: widths[index]
|
|
91
|
-
lines: normalizeLines(frame, widths[index], height)
|
|
92
|
+
frame: fitFrame(frame, widths[index], height, { expandFullFrameSpans: true }),
|
|
93
|
+
width: widths[index]
|
|
92
94
|
}));
|
|
93
95
|
const gapText = repeat(" ", gap);
|
|
94
96
|
const lines = new Array<string>(height).fill("");
|
|
@@ -100,7 +102,7 @@ export function mergeHorizontal(frames: TerminalFrame[], options: { gap?: number
|
|
|
100
102
|
for (let index = 0; index < normalizedFrames.length; index += 1) {
|
|
101
103
|
const part = normalizedFrames[index];
|
|
102
104
|
for (let row = 0; row < height; row += 1) {
|
|
103
|
-
lines[row] += part.lines[row];
|
|
105
|
+
lines[row] += part.frame.lines[row];
|
|
104
106
|
if (index < normalizedFrames.length - 1) {
|
|
105
107
|
lines[row] += gapText;
|
|
106
108
|
}
|
|
@@ -124,20 +126,20 @@ export function padFrame(frame: TerminalFrame, padding: number) {
|
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
const width = getFrameWidth(frame);
|
|
127
|
-
const middle = frame.lines.map((line) => `${repeat(" ", amount)}${line
|
|
129
|
+
const middle = frame.lines.map((line) => `${repeat(" ", amount)}${padEndTerminalCells(line, width)}${repeat(" ", amount)}`);
|
|
128
130
|
const blank = repeat(" ", width + amount * 2);
|
|
129
131
|
const lines = [...new Array<string>(amount).fill(blank), ...middle, ...new Array<string>(amount).fill(blank)];
|
|
130
132
|
const shifted = shiftFrame(frame, amount, amount);
|
|
131
133
|
return createFrame(lines, shifted.hitboxes, frame.cursor ? { x: frame.cursor.x + amount, y: frame.cursor.y + amount } : null, shifted.spans);
|
|
132
134
|
}
|
|
133
135
|
|
|
134
|
-
export function fitFrame(frame: TerminalFrame, width?: number, height?: number) {
|
|
136
|
+
export function fitFrame(frame: TerminalFrame, width?: number, height?: number, options: { expandFullFrameSpans?: boolean } = {}) {
|
|
135
137
|
const nextWidth = Math.max(getFrameWidth(frame), Number(width || 0));
|
|
136
138
|
const nextHeight = Math.max(getFrameHeight(frame), Number(height || 0));
|
|
137
139
|
if (nextWidth === getFrameWidth(frame) && nextHeight === getFrameHeight(frame)) {
|
|
138
140
|
return frame;
|
|
139
141
|
}
|
|
140
|
-
return
|
|
142
|
+
return constrainFrame(frame, { width: nextWidth, height: nextHeight, expandFullFrameSpans: options.expandFullFrameSpans });
|
|
141
143
|
}
|
|
142
144
|
|
|
143
145
|
function constraintSize(value: number | undefined, fallback: number) {
|
|
@@ -159,13 +161,13 @@ function clamp(value: number, min: number, max: number) {
|
|
|
159
161
|
return Math.min(max, Math.max(min, value));
|
|
160
162
|
}
|
|
161
163
|
|
|
162
|
-
export function constrainFrame(frame: TerminalFrame, options: { width?: number; height?: number } = {}): TerminalFrame {
|
|
164
|
+
export function constrainFrame(frame: TerminalFrame, options: { width?: number; height?: number; expandFullFrameSpans?: boolean; expandFullRowSpans?: boolean } = {}): TerminalFrame {
|
|
163
165
|
const width = constraintSize(options.width, getFrameWidth(frame));
|
|
164
166
|
const height = constraintSize(options.height, getFrameHeight(frame));
|
|
165
167
|
const lines: string[] = [];
|
|
166
168
|
|
|
167
169
|
for (let row = 0; row < height; row += 1) {
|
|
168
|
-
lines.push((frame.lines[row] || ""
|
|
170
|
+
lines.push(padEndTerminalCells(sliceTerminalCells(frame.lines[row] || "", width), width));
|
|
169
171
|
}
|
|
170
172
|
|
|
171
173
|
const hitboxes = frame.hitboxes
|
|
@@ -183,8 +185,26 @@ export function constrainFrame(frame: TerminalFrame, options: { width?: number;
|
|
|
183
185
|
? { x: frame.cursor.x, y: frame.cursor.y }
|
|
184
186
|
: null;
|
|
185
187
|
|
|
188
|
+
const originalWidth = getFrameWidth(frame);
|
|
189
|
+
const originalHeight = getFrameHeight(frame);
|
|
186
190
|
const spanRightEdge = width + 1;
|
|
187
191
|
const spans = frame.spans.flatMap((span) => {
|
|
192
|
+
const coversCurrentFrame = options.expandFullFrameSpans === true && isFullFrameSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1;
|
|
193
|
+
if (coversCurrentFrame) {
|
|
194
|
+
if (width <= 0 || span.y < 1 || span.y > Math.min(originalHeight, height)) {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
return [cloneStyleSpan(span, { x1: 1, x2: spanRightEdge })];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const coversCurrentRow = (options.expandFullFrameSpans === true || options.expandFullRowSpans === true) && isFullRowSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1;
|
|
201
|
+
if (coversCurrentRow) {
|
|
202
|
+
if (width <= 0 || span.y < 1 || span.y > Math.min(originalHeight, height)) {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
return [cloneStyleSpan(span, { x1: 1, x2: spanRightEdge })];
|
|
206
|
+
}
|
|
207
|
+
|
|
188
208
|
if (width <= 0 || span.y < 1 || span.y > height || span.x2 <= 1 || span.x1 >= spanRightEdge) {
|
|
189
209
|
return [];
|
|
190
210
|
}
|
|
@@ -195,9 +215,22 @@ export function constrainFrame(frame: TerminalFrame, options: { width?: number;
|
|
|
195
215
|
return [];
|
|
196
216
|
}
|
|
197
217
|
|
|
198
|
-
return [
|
|
218
|
+
return [cloneStyleSpan(span, { x1: clippedX1, x2: clippedX2 })];
|
|
199
219
|
});
|
|
200
220
|
|
|
221
|
+
const fullFrameSpans = options.expandFullFrameSpans === true
|
|
222
|
+
? frame.spans.filter((span) => isFullFrameSpan(span) && span.x1 === 1 && span.x2 === originalWidth + 1)
|
|
223
|
+
: [];
|
|
224
|
+
const firstFullFrameRow = fullFrameSpans.reduce<number | null>((first, span) => first === null ? span.y : Math.min(first, span.y), null);
|
|
225
|
+
if (firstFullFrameRow !== null) {
|
|
226
|
+
const templates = fullFrameSpans.filter((span) => span.y === firstFullFrameRow);
|
|
227
|
+
for (let y = originalHeight + 1; y <= height; y += 1) {
|
|
228
|
+
for (const template of templates) {
|
|
229
|
+
spans.push(cloneStyleSpan(template, { x1: 1, x2: spanRightEdge, y }));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
201
234
|
return createFrame(lines, hitboxes, cursor, spans);
|
|
202
235
|
}
|
|
203
236
|
|
|
@@ -233,16 +266,20 @@ export function overlayFrame(base: TerminalFrame, overlay: TerminalFrame, option
|
|
|
233
266
|
|
|
234
267
|
const baseLine = lines[baseRow];
|
|
235
268
|
const start = x - 1;
|
|
236
|
-
if (start >= baseLine
|
|
269
|
+
if (start >= terminalCellWidth(baseLine)) {
|
|
237
270
|
continue;
|
|
238
271
|
}
|
|
239
272
|
|
|
240
|
-
const visibleWidth = Math.min(constrainedOverlay.lines[row]
|
|
273
|
+
const visibleWidth = Math.min(terminalCellWidth(constrainedOverlay.lines[row]), terminalCellWidth(baseLine) - start);
|
|
241
274
|
if (visibleWidth <= 0) {
|
|
242
275
|
continue;
|
|
243
276
|
}
|
|
244
277
|
|
|
245
|
-
|
|
278
|
+
const prefix = sliceTerminalCells(baseLine, start);
|
|
279
|
+
const suffix = dropTerminalCells(baseLine, start + visibleWidth);
|
|
280
|
+
const insert = sliceTerminalCells(constrainedOverlay.lines[row], visibleWidth);
|
|
281
|
+
const removedWidth = terminalCellWidth(baseLine) - terminalCellWidth(prefix) - terminalCellWidth(suffix);
|
|
282
|
+
lines[baseRow] = `${prefix}${padEndTerminalCells(insert, removedWidth)}${suffix}`;
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
const shiftedOverlay = shiftFrame(constrainedOverlay, x - 1, y - 1);
|
|
@@ -270,10 +307,7 @@ export function cropFrame(frame: TerminalFrame, offset: number, height: number)
|
|
|
270
307
|
}));
|
|
271
308
|
const spans = frame.spans
|
|
272
309
|
.filter((span) => span.y > start && span.y <= start + size)
|
|
273
|
-
.map((span) => ({
|
|
274
|
-
...span,
|
|
275
|
-
y: span.y - start
|
|
276
|
-
}));
|
|
310
|
+
.map((span) => cloneStyleSpan(span, { y: span.y - start }));
|
|
277
311
|
const cursor = frame.cursor && frame.cursor.y > start && frame.cursor.y <= start + size
|
|
278
312
|
? { x: frame.cursor.x, y: frame.cursor.y - start }
|
|
279
313
|
: null;
|