toolcraft-openapi 0.0.152 → 0.0.154
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/composition.json +2 -2
- package/dist/generate.js +1 -0
- package/node_modules/toolcraft-design/README.md +30 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/dashboard/terminal-width.js +18 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/actions.js +22 -4
- package/node_modules/toolcraft-design/dist/explorer/filter.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/index.d.ts +2 -4
- package/node_modules/toolcraft-design/dist/explorer/index.js +1 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.d.ts +2 -2
- package/node_modules/toolcraft-design/dist/explorer/jobs.js +3 -5
- package/node_modules/toolcraft-design/dist/explorer/keymap.d.ts +13 -3
- package/node_modules/toolcraft-design/dist/explorer/keymap.js +89 -348
- package/node_modules/toolcraft-design/dist/explorer/layout.d.ts +1 -0
- package/node_modules/toolcraft-design/dist/explorer/layout.js +14 -4
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +58 -9
- package/node_modules/toolcraft-design/dist/explorer/render/detail.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/detail.js +43 -7
- package/node_modules/toolcraft-design/dist/explorer/render/footer.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/footer.js +6 -3
- package/node_modules/toolcraft-design/dist/explorer/render/header.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/header.js +6 -2
- package/node_modules/toolcraft-design/dist/explorer/render/index.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/index.js +2 -1
- package/node_modules/toolcraft-design/dist/explorer/render/list.d.ts +16 -2
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +13 -34
- package/node_modules/toolcraft-design/dist/explorer/render/modal.d.ts +1 -1
- package/node_modules/toolcraft-design/dist/explorer/render/pane.d.ts +5 -2
- package/node_modules/toolcraft-design/dist/explorer/render/pane.js +9 -6
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +2 -3
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +68 -63
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.d.ts +23 -37
- package/node_modules/toolcraft-design/dist/explorer/runtime.test-helpers.js +32 -83
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +47 -5
- package/node_modules/toolcraft-design/dist/explorer/state.js +56 -12
- package/node_modules/toolcraft-design/dist/index.d.ts +8 -2
- package/node_modules/toolcraft-design/dist/index.js +5 -1
- package/node_modules/toolcraft-design/dist/screen/ansi-text.d.ts +2 -0
- package/node_modules/toolcraft-design/dist/screen/ansi-text.js +35 -0
- package/node_modules/toolcraft-design/dist/screen/screen.d.ts +40 -0
- package/node_modules/toolcraft-design/dist/screen/screen.js +122 -0
- package/node_modules/toolcraft-design/dist/screen/style.d.ts +16 -0
- package/node_modules/toolcraft-design/dist/screen/style.js +55 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.d.ts +33 -0
- package/node_modules/toolcraft-design/dist/terminal/driver.js +61 -0
- package/node_modules/toolcraft-design/dist/terminal/input.d.ts +25 -0
- package/node_modules/toolcraft-design/dist/terminal/input.js +181 -0
- package/node_modules/toolcraft-design/dist/terminal/output.d.ts +8 -0
- package/node_modules/toolcraft-design/dist/terminal/output.js +42 -0
- package/node_modules/toolcraft-schema/package.json +1 -1
- package/package.json +3 -3
- package/node_modules/toolcraft-design/dist/explorer/two-pane.d.ts +0 -102
- package/node_modules/toolcraft-design/dist/explorer/two-pane.js +0 -510
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.d.ts +0 -2
- package/node_modules/toolcraft-design/dist/run-two-pane-explorer.js +0 -1
|
@@ -1,4 +1,34 @@
|
|
|
1
1
|
import { resolveBindings } from "./keymap.js";
|
|
2
|
+
export function normalizeExplorerConfig(config) {
|
|
3
|
+
if (config.panes === undefined) {
|
|
4
|
+
if (config.rows === undefined || config.detail === undefined)
|
|
5
|
+
throw new Error("Explorer requires panes");
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
if (config.panes.length < 1 || config.panes.length > 3)
|
|
9
|
+
throw new Error("Explorer requires 1 to 3 panes");
|
|
10
|
+
const list = config.panes.find((pane) => pane.kind === "list");
|
|
11
|
+
if (list === undefined)
|
|
12
|
+
throw new Error("Explorer requires a list pane");
|
|
13
|
+
const companion = config.panes.find(pane => pane !== list);
|
|
14
|
+
const detail = companion?.kind === "detail"
|
|
15
|
+
? { items: async (row, ctx) => {
|
|
16
|
+
const content = await companion.render(row, ctx);
|
|
17
|
+
if (ctx.signal.aborted)
|
|
18
|
+
return [];
|
|
19
|
+
return [{ id: row.id, render: () => content, renderedContent: content }];
|
|
20
|
+
} }
|
|
21
|
+
: companion?.kind === "list"
|
|
22
|
+
? { items: async () => (await companion.rows()).map(row => ({ ...row, render: () => "" })), actions: config.actions }
|
|
23
|
+
: { items: async () => [] };
|
|
24
|
+
return {
|
|
25
|
+
...config,
|
|
26
|
+
rows: list.rows,
|
|
27
|
+
detail,
|
|
28
|
+
multiSelect: list.multiSelect ?? config.multiSelect,
|
|
29
|
+
emptyHint: list.emptyHint ?? config.emptyHint
|
|
30
|
+
};
|
|
31
|
+
}
|
|
2
32
|
export const REGION_HEADER = 1 << 0;
|
|
3
33
|
export const REGION_LIST = 1 << 1;
|
|
4
34
|
export const REGION_DETAIL = 1 << 2;
|
|
@@ -12,18 +42,20 @@ export const REGION_ALL = REGION_HEADER |
|
|
|
12
42
|
REGION_MODAL |
|
|
13
43
|
REGION_TOAST;
|
|
14
44
|
export function createInitialState(config, size) {
|
|
45
|
+
const normalizedConfig = normalizeExplorerConfig(config);
|
|
15
46
|
const normalizedSize = {
|
|
16
47
|
cols: normalizeSize(size.cols),
|
|
17
48
|
rows: normalizeSize(size.rows)
|
|
18
49
|
};
|
|
19
|
-
const multiSelect =
|
|
20
|
-
const initialRows =
|
|
21
|
-
const initialFilter =
|
|
50
|
+
const multiSelect = normalizedConfig.multiSelect ?? true;
|
|
51
|
+
const initialRows = normalizedConfig.initialRows ?? [];
|
|
52
|
+
const initialFilter = normalizedConfig.initialFilter ?? "";
|
|
22
53
|
// Defer filtering to first rowsLoaded when empty; seed list immediately when provided.
|
|
23
54
|
return {
|
|
24
|
-
title:
|
|
25
|
-
emptyHint:
|
|
55
|
+
title: normalizedConfig.title,
|
|
56
|
+
emptyHint: normalizedConfig.emptyHint ?? "No detail",
|
|
26
57
|
rows: initialRows,
|
|
58
|
+
rowsLoading: true,
|
|
27
59
|
filtered: initialRows.map((_, index) => index),
|
|
28
60
|
matchPositions: new Map(),
|
|
29
61
|
cursor: 0,
|
|
@@ -33,6 +65,8 @@ export function createInitialState(config, size) {
|
|
|
33
65
|
detail: {
|
|
34
66
|
rowId: initialRows[0]?.id ?? null,
|
|
35
67
|
items: null,
|
|
68
|
+
allItems: null,
|
|
69
|
+
filter: "",
|
|
36
70
|
cursor: 0,
|
|
37
71
|
scroll: 0,
|
|
38
72
|
token: initialRows.length > 0 ? 1 : 0,
|
|
@@ -44,17 +78,27 @@ export function createInitialState(config, size) {
|
|
|
44
78
|
toast: null,
|
|
45
79
|
dirty: REGION_ALL,
|
|
46
80
|
size: normalizedSize,
|
|
47
|
-
layout: resolveExplorerLayoutMode(normalizedSize.cols),
|
|
48
|
-
bindings: resolveBindings(
|
|
49
|
-
actionState: createInitialActionState(
|
|
81
|
+
layout: resolveExplorerLayoutMode(normalizedSize.cols, normalizedSize.rows),
|
|
82
|
+
bindings: resolveBindings(normalizedConfig),
|
|
83
|
+
actionState: createInitialActionState(normalizedConfig),
|
|
84
|
+
suspended: false,
|
|
85
|
+
paneDefinitions: normalizedConfig.panes?.map(({ id, title, kind }) => ({ id, title, kind })) ?? [
|
|
86
|
+
{ id: "list", title: normalizedConfig.title, kind: "list" },
|
|
87
|
+
{ id: "detail", title: "Preview", kind: "detail" }
|
|
88
|
+
]
|
|
50
89
|
};
|
|
51
90
|
}
|
|
52
91
|
function createInitialActionState(config) {
|
|
53
92
|
const state = new Map();
|
|
54
|
-
for (const [source, actions] of [["row", config.actions], ["detail", config.detail
|
|
93
|
+
for (const [source, actions] of [["row", config.actions], ["detail", config.detail?.actions ?? []]]) {
|
|
55
94
|
for (const action of actions) {
|
|
56
95
|
if (state.has(action.id)) {
|
|
57
|
-
|
|
96
|
+
const isSharedListAction = config.panes?.some(pane => pane.kind === "list" && pane !== config.panes?.[0]) === true;
|
|
97
|
+
if (!isSharedListAction)
|
|
98
|
+
throw new Error(`Duplicate explorer action id: ${action.id}`);
|
|
99
|
+
const existing = state.get(action.id);
|
|
100
|
+
state.set(action.id, { ...existing, source: "both" });
|
|
101
|
+
continue;
|
|
58
102
|
}
|
|
59
103
|
state.set(action.id, {
|
|
60
104
|
available: true,
|
|
@@ -66,8 +110,8 @@ function createInitialActionState(config) {
|
|
|
66
110
|
}
|
|
67
111
|
return state;
|
|
68
112
|
}
|
|
69
|
-
export function resolveExplorerLayoutMode(cols) {
|
|
70
|
-
if (cols <
|
|
113
|
+
export function resolveExplorerLayoutMode(cols, rows = 24) {
|
|
114
|
+
if (cols < 60 || rows < 8) {
|
|
71
115
|
return "too-narrow";
|
|
72
116
|
}
|
|
73
117
|
if (cols < 80) {
|
|
@@ -37,8 +37,8 @@ export * as dashboard from "./dashboard/index.js";
|
|
|
37
37
|
export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
|
|
38
38
|
export type { Dashboard, DashboardOptions } from "./dashboard/index.js";
|
|
39
39
|
export * as explorer from "./explorer/index.js";
|
|
40
|
-
export { runExplorer,
|
|
41
|
-
export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext,
|
|
40
|
+
export { runExplorer, singleDetail, normalizeExplorerConfig } from "./explorer/index.js";
|
|
41
|
+
export type { Row, DetailItem, Detail, DetailCtx, Action, ActionContext, ExplorerConfig, PaneConfig, ListPaneConfig, DetailPaneConfig, PaneRuntimeState, ReorderContext, Tone } from "./explorer/index.js";
|
|
42
42
|
export * as prompts from "./prompts/index.js";
|
|
43
43
|
export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
|
|
44
44
|
export type { SelectOptions, MultiselectOptions, TextOptions, ConfirmOptions, PasswordOptions, SpinnerOptions, WithSpinnerOptions } from "./prompts/index.js";
|
|
@@ -54,3 +54,9 @@ export { configureTheme, getThemeConfig, resetTheme } from "./internal/theme-sta
|
|
|
54
54
|
export { stripAnsi } from "./internal/strip-ansi.js";
|
|
55
55
|
export { resolveOutputFormat, resetOutputFormatCache, withOutputFormat } from "./internal/output-format.js";
|
|
56
56
|
export type { OutputFormat } from "./internal/output-format.js";
|
|
57
|
+
export { createTerminalDriver } from "./terminal/driver.js";
|
|
58
|
+
export type { TerminalDriver, TerminalInputEvent, Size as TerminalSize } from "./terminal/driver.js";
|
|
59
|
+
export { Screen } from "./screen/screen.js";
|
|
60
|
+
export type { Cell as ScreenCell, ScreenSize, ScreenSurface } from "./screen/screen.js";
|
|
61
|
+
export { packStyle, styleToSgrDelta } from "./screen/style.js";
|
|
62
|
+
export type { PackedStyle } from "./screen/style.js";
|
|
@@ -29,7 +29,7 @@ export * as dashboard from "./dashboard/index.js";
|
|
|
29
29
|
export { createDashboard, shouldUseInteractiveDashboard } from "./dashboard/index.js";
|
|
30
30
|
// Explorer
|
|
31
31
|
export * as explorer from "./explorer/index.js";
|
|
32
|
-
export { runExplorer,
|
|
32
|
+
export { runExplorer, singleDetail, normalizeExplorerConfig } from "./explorer/index.js";
|
|
33
33
|
// Prompts
|
|
34
34
|
export * as prompts from "./prompts/index.js";
|
|
35
35
|
export { intro, introPlain, outro, note, select, multiselect, text as promptText, confirm, confirmOrCancel, password, spinner, withSpinner, isCancel, cancel, log, PromptCancelledError } from "./prompts/index.js";
|
|
@@ -44,3 +44,7 @@ export { getTheme, resolveThemeName, resetThemeCache } from "./internal/theme-de
|
|
|
44
44
|
export { configureTheme, getThemeConfig, resetTheme } from "./internal/theme-state.js";
|
|
45
45
|
export { stripAnsi } from "./internal/strip-ansi.js";
|
|
46
46
|
export { resolveOutputFormat, resetOutputFormatCache, withOutputFormat } from "./internal/output-format.js";
|
|
47
|
+
// Terminal and screen primitives
|
|
48
|
+
export { createTerminalDriver } from "./terminal/driver.js";
|
|
49
|
+
export { Screen } from "./screen/screen.js";
|
|
50
|
+
export { packStyle, styleToSgrDelta } from "./screen/style.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseAnsi } from "../dashboard/ansi.js";
|
|
2
|
+
import { graphemes, graphemeWidth } from "../dashboard/terminal-width.js";
|
|
3
|
+
import { packStyle } from "./style.js";
|
|
4
|
+
const COLORS = new Map([
|
|
5
|
+
["black", 1], ["red", 1], ["green", 2], ["yellow", 3], ["blue", 4],
|
|
6
|
+
["magenta", 5], ["cyan", 6], ["white", 7], ["gray", 8]
|
|
7
|
+
]);
|
|
8
|
+
export function ansiToCells(text) {
|
|
9
|
+
const output = [];
|
|
10
|
+
const lines = parseAnsi(text);
|
|
11
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
12
|
+
if (lineIndex > 0)
|
|
13
|
+
output.push(cell("\n", 1, 0));
|
|
14
|
+
for (const segment of lines[lineIndex].segments) {
|
|
15
|
+
const style = packed(segment.style);
|
|
16
|
+
for (const grapheme of graphemes(segment.text))
|
|
17
|
+
output.push(cell(grapheme, graphemeWidth(grapheme) > 1 ? 2 : 1, style));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
function packed(style) {
|
|
23
|
+
return packStyle({
|
|
24
|
+
bold: style.bold,
|
|
25
|
+
dim: style.dim,
|
|
26
|
+
underline: style.underline,
|
|
27
|
+
inverse: style.inverse,
|
|
28
|
+
fg: colorNumber(style.fg),
|
|
29
|
+
bg: colorNumber(style.bg)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
function colorNumber(value) { return value === undefined ? 0 : (COLORS.get(value) ?? 0); }
|
|
33
|
+
function cell(ch, width, style) {
|
|
34
|
+
return { ch, width, style, fg: (style >> 8) & 0xff, bg: (style >> 16) & 0xff };
|
|
35
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CellStyle, Rect } from "../dashboard/types.js";
|
|
2
|
+
import { type PackedStyle } from "./style.js";
|
|
3
|
+
export interface Cell {
|
|
4
|
+
ch: string;
|
|
5
|
+
width: 1 | 2;
|
|
6
|
+
style: PackedStyle;
|
|
7
|
+
fg: number;
|
|
8
|
+
bg: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ScreenSize {
|
|
11
|
+
cols: number;
|
|
12
|
+
rows: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ScreenSurface {
|
|
15
|
+
readonly width: number;
|
|
16
|
+
readonly height: number;
|
|
17
|
+
put(x: number, y: number, text: string, style?: CellStyle | PackedStyle): void;
|
|
18
|
+
clearRect(rect: Rect, style?: CellStyle | PackedStyle): void;
|
|
19
|
+
}
|
|
20
|
+
export declare class Screen {
|
|
21
|
+
private cols;
|
|
22
|
+
private rows;
|
|
23
|
+
private front;
|
|
24
|
+
private back;
|
|
25
|
+
private outputStyle;
|
|
26
|
+
private readonly colors;
|
|
27
|
+
constructor(size?: ScreenSize, options?: {
|
|
28
|
+
colors?: boolean;
|
|
29
|
+
});
|
|
30
|
+
get width(): number;
|
|
31
|
+
get height(): number;
|
|
32
|
+
resize(size: ScreenSize): void;
|
|
33
|
+
cell(x: number, y: number, ch: string, style?: PackedStyle): void;
|
|
34
|
+
text(x: number, y: number, text: string, style?: PackedStyle): void;
|
|
35
|
+
put(x: number, y: number, text: string, style?: CellStyle | PackedStyle): void;
|
|
36
|
+
clearRect(rect: Rect, style?: CellStyle | PackedStyle): void;
|
|
37
|
+
flush(): string;
|
|
38
|
+
private index;
|
|
39
|
+
private inBounds;
|
|
40
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { graphemes, graphemeWidth } from "../dashboard/terminal-width.js";
|
|
2
|
+
import { background, foreground, styleToSgrDelta } from "./style.js";
|
|
3
|
+
const EMPTY = { ch: " ", width: 1, style: 0, fg: 0, bg: 0 };
|
|
4
|
+
export class Screen {
|
|
5
|
+
cols = 0;
|
|
6
|
+
rows = 0;
|
|
7
|
+
front = [];
|
|
8
|
+
back = [];
|
|
9
|
+
outputStyle = 0;
|
|
10
|
+
colors;
|
|
11
|
+
constructor(size = { cols: 0, rows: 0 }, options = {}) {
|
|
12
|
+
this.colors = options.colors ?? (process.env.NO_COLOR === undefined && process.env.TERM !== "dumb");
|
|
13
|
+
this.resize(size);
|
|
14
|
+
}
|
|
15
|
+
get width() { return this.cols; }
|
|
16
|
+
get height() { return this.rows; }
|
|
17
|
+
resize(size) {
|
|
18
|
+
this.cols = normalize(size.cols);
|
|
19
|
+
this.rows = normalize(size.rows);
|
|
20
|
+
this.front = cells(this.cols * this.rows);
|
|
21
|
+
this.back = cells(this.cols * this.rows);
|
|
22
|
+
}
|
|
23
|
+
cell(x, y, ch, style = 0) {
|
|
24
|
+
if (!this.inBounds(x, y) || ch.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
const segment = graphemes(ch)[0];
|
|
27
|
+
if (segment === undefined)
|
|
28
|
+
return;
|
|
29
|
+
const measured = graphemeWidth(segment);
|
|
30
|
+
const width = measured > 1 ? 2 : 1;
|
|
31
|
+
this.back[this.index(x, y)] = makeCell(segment, width, style);
|
|
32
|
+
if (width === 2 && this.inBounds(x + 1, y))
|
|
33
|
+
this.back[this.index(x + 1, y)] = makeCell("", 1, style);
|
|
34
|
+
}
|
|
35
|
+
text(x, y, text, style = 0) {
|
|
36
|
+
let offset = 0;
|
|
37
|
+
for (const segment of graphemes(text)) {
|
|
38
|
+
const width = Math.max(0, graphemeWidth(segment));
|
|
39
|
+
if (width > 0)
|
|
40
|
+
this.cell(x + offset, y, segment, style);
|
|
41
|
+
offset += width;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
put(x, y, text, style = 0) {
|
|
45
|
+
this.text(x, y, text, typeof style === "number" ? style : packLegacyStyle(style));
|
|
46
|
+
}
|
|
47
|
+
clearRect(rect, style = 0) {
|
|
48
|
+
const packed = typeof style === "number" ? style : packLegacyStyle(style);
|
|
49
|
+
const startX = Math.max(0, rect.x);
|
|
50
|
+
const startY = Math.max(0, rect.y);
|
|
51
|
+
const endX = Math.min(this.cols, rect.x + Math.max(0, rect.width));
|
|
52
|
+
const endY = Math.min(this.rows, rect.y + Math.max(0, rect.height));
|
|
53
|
+
for (let y = startY; y < endY; y += 1)
|
|
54
|
+
for (let x = startX; x < endX; x += 1)
|
|
55
|
+
this.cell(x, y, " ", packed);
|
|
56
|
+
}
|
|
57
|
+
flush() {
|
|
58
|
+
const changed = new Set();
|
|
59
|
+
for (let index = 0; index < this.back.length; index += 1) {
|
|
60
|
+
if (!equal(this.front[index], this.back[index])) {
|
|
61
|
+
changed.add(index);
|
|
62
|
+
const old = this.front[index];
|
|
63
|
+
const next = this.back[index];
|
|
64
|
+
if ((old.width === 2 || next.width === 2) && (index % this.cols) + 1 < this.cols)
|
|
65
|
+
changed.add(index + 1);
|
|
66
|
+
if (old.ch === "" && index % this.cols > 0)
|
|
67
|
+
changed.add(index - 1);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let output = "";
|
|
71
|
+
let currentStyle = this.outputStyle;
|
|
72
|
+
const ordered = [...changed].sort((left, right) => left - right);
|
|
73
|
+
let cursor = 0;
|
|
74
|
+
while (cursor < ordered.length) {
|
|
75
|
+
const start = ordered[cursor];
|
|
76
|
+
const y = Math.floor(start / this.cols);
|
|
77
|
+
let end = start;
|
|
78
|
+
while (cursor + 1 < ordered.length && ordered[cursor + 1] === end + 1 && Math.floor(ordered[cursor + 1] / this.cols) === y) {
|
|
79
|
+
cursor += 1;
|
|
80
|
+
end += 1;
|
|
81
|
+
}
|
|
82
|
+
output += `\u001b[${y + 1};${(start % this.cols) + 1}H`;
|
|
83
|
+
for (let index = start; index <= end; index += 1) {
|
|
84
|
+
const cell = this.back[index];
|
|
85
|
+
const delta = styleToSgrDelta(currentStyle, cell.style, this.colors);
|
|
86
|
+
output += delta;
|
|
87
|
+
currentStyle = cell.style;
|
|
88
|
+
output += cell.ch.length === 0 ? "" : cell.ch;
|
|
89
|
+
}
|
|
90
|
+
cursor += 1;
|
|
91
|
+
}
|
|
92
|
+
this.front = this.back;
|
|
93
|
+
this.back = cells(this.cols * this.rows);
|
|
94
|
+
this.outputStyle = currentStyle;
|
|
95
|
+
return output;
|
|
96
|
+
}
|
|
97
|
+
index(x, y) { return y * this.cols + x; }
|
|
98
|
+
inBounds(x, y) { return x >= 0 && y >= 0 && x < this.cols && y < this.rows; }
|
|
99
|
+
}
|
|
100
|
+
function makeCell(ch, width, style) {
|
|
101
|
+
return { ch, width, style, fg: foreground(style), bg: background(style) };
|
|
102
|
+
}
|
|
103
|
+
function cells(length) { return Array.from({ length }, () => ({ ...EMPTY })); }
|
|
104
|
+
function equal(left, right) { return left.ch === right.ch && left.width === right.width && left.style === right.style; }
|
|
105
|
+
function normalize(value) { return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0; }
|
|
106
|
+
function packLegacyStyle(style) {
|
|
107
|
+
return (style.bold ? 1 : 0)
|
|
108
|
+
| (style.dim ? 2 : 0)
|
|
109
|
+
| (style.underline ? 4 : 0)
|
|
110
|
+
| (style.inverse ? 8 : 0)
|
|
111
|
+
| (legacyColor(style.fg) << 8)
|
|
112
|
+
| (legacyColor(style.bg) << 16);
|
|
113
|
+
}
|
|
114
|
+
function legacyColor(value) {
|
|
115
|
+
if (value === undefined)
|
|
116
|
+
return 0;
|
|
117
|
+
const names = ["", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "gray"];
|
|
118
|
+
const exact = names.indexOf(value);
|
|
119
|
+
if (exact >= 0)
|
|
120
|
+
return exact;
|
|
121
|
+
return value.startsWith("#") ? 7 : 0;
|
|
122
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const STYLE_BOLD: number;
|
|
2
|
+
export declare const STYLE_DIM: number;
|
|
3
|
+
export declare const STYLE_UNDERLINE: number;
|
|
4
|
+
export declare const STYLE_INVERSE: number;
|
|
5
|
+
export type PackedStyle = number;
|
|
6
|
+
export declare function packStyle(style: {
|
|
7
|
+
bold?: boolean;
|
|
8
|
+
dim?: boolean;
|
|
9
|
+
underline?: boolean;
|
|
10
|
+
inverse?: boolean;
|
|
11
|
+
fg?: number;
|
|
12
|
+
bg?: number;
|
|
13
|
+
}): PackedStyle;
|
|
14
|
+
export declare function foreground(style: PackedStyle): number;
|
|
15
|
+
export declare function background(style: PackedStyle): number;
|
|
16
|
+
export declare function styleToSgrDelta(previous: PackedStyle, next: PackedStyle, colors?: boolean): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const STYLE_BOLD = 1 << 0;
|
|
2
|
+
export const STYLE_DIM = 1 << 1;
|
|
3
|
+
export const STYLE_UNDERLINE = 1 << 2;
|
|
4
|
+
export const STYLE_INVERSE = 1 << 3;
|
|
5
|
+
const FG_SHIFT = 8;
|
|
6
|
+
const BG_SHIFT = 16;
|
|
7
|
+
const COLOR_MASK = 0xff;
|
|
8
|
+
export function packStyle(style) {
|
|
9
|
+
let packed = 0;
|
|
10
|
+
if (style.bold)
|
|
11
|
+
packed |= STYLE_BOLD;
|
|
12
|
+
if (style.dim)
|
|
13
|
+
packed |= STYLE_DIM;
|
|
14
|
+
if (style.underline)
|
|
15
|
+
packed |= STYLE_UNDERLINE;
|
|
16
|
+
if (style.inverse)
|
|
17
|
+
packed |= STYLE_INVERSE;
|
|
18
|
+
packed |= ((style.fg ?? 0) & COLOR_MASK) << FG_SHIFT;
|
|
19
|
+
packed |= ((style.bg ?? 0) & COLOR_MASK) << BG_SHIFT;
|
|
20
|
+
return packed;
|
|
21
|
+
}
|
|
22
|
+
export function foreground(style) { return (style >> FG_SHIFT) & COLOR_MASK; }
|
|
23
|
+
export function background(style) { return (style >> BG_SHIFT) & COLOR_MASK; }
|
|
24
|
+
export function styleToSgrDelta(previous, next, colors = process.env.NO_COLOR === undefined && process.env.TERM !== "dumb") {
|
|
25
|
+
if (!colors)
|
|
26
|
+
return "";
|
|
27
|
+
if (previous === next)
|
|
28
|
+
return "";
|
|
29
|
+
const codes = [];
|
|
30
|
+
const previousIntensity = previous & (STYLE_BOLD | STYLE_DIM);
|
|
31
|
+
const nextIntensity = next & (STYLE_BOLD | STYLE_DIM);
|
|
32
|
+
if (previousIntensity !== nextIntensity) {
|
|
33
|
+
if ((previousIntensity & ~nextIntensity) !== 0)
|
|
34
|
+
codes.push(22);
|
|
35
|
+
if ((nextIntensity & STYLE_BOLD) !== 0 && ((previousIntensity & ~nextIntensity) !== 0 || (previousIntensity & STYLE_BOLD) === 0))
|
|
36
|
+
codes.push(1);
|
|
37
|
+
if ((nextIntensity & STYLE_DIM) !== 0 && ((previousIntensity & ~nextIntensity) !== 0 || (previousIntensity & STYLE_DIM) === 0))
|
|
38
|
+
codes.push(2);
|
|
39
|
+
}
|
|
40
|
+
flagDelta(codes, previous, next, STYLE_UNDERLINE, 4, 24);
|
|
41
|
+
flagDelta(codes, previous, next, STYLE_INVERSE, 7, 27);
|
|
42
|
+
const previousFg = foreground(previous);
|
|
43
|
+
const nextFg = foreground(next);
|
|
44
|
+
if (previousFg !== nextFg)
|
|
45
|
+
codes.push(nextFg === 0 ? 39 : 30 + nextFg);
|
|
46
|
+
const previousBg = background(previous);
|
|
47
|
+
const nextBg = background(next);
|
|
48
|
+
if (previousBg !== nextBg)
|
|
49
|
+
codes.push(nextBg === 0 ? 49 : 40 + nextBg);
|
|
50
|
+
return codes.length === 0 ? "" : `\u001b[${[...new Set(codes)].join(";")}m`;
|
|
51
|
+
}
|
|
52
|
+
function flagDelta(codes, previous, next, flag, on, off) {
|
|
53
|
+
if ((previous & flag) !== (next & flag))
|
|
54
|
+
codes.push((next & flag) === 0 ? off : on);
|
|
55
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type TerminalInputEvent } from "./input.js";
|
|
2
|
+
export interface Size {
|
|
3
|
+
cols: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
}
|
|
6
|
+
export interface TerminalDriver {
|
|
7
|
+
start(): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
onEvent(fn: (event: TerminalInputEvent) => void): () => void;
|
|
10
|
+
onResize(fn: (size: Size) => void): () => void;
|
|
11
|
+
getSize(): Size;
|
|
12
|
+
writeFrame(ansi: string): void;
|
|
13
|
+
}
|
|
14
|
+
interface InputStream {
|
|
15
|
+
setRawMode?: (enabled: boolean) => void;
|
|
16
|
+
resume(): void;
|
|
17
|
+
pause(): void;
|
|
18
|
+
on(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
19
|
+
off(event: "data", listener: (chunk: Buffer | string) => void): unknown;
|
|
20
|
+
}
|
|
21
|
+
interface OutputStream {
|
|
22
|
+
columns?: number;
|
|
23
|
+
rows?: number;
|
|
24
|
+
write(value: string): boolean;
|
|
25
|
+
on(event: "resize", listener: () => void): unknown;
|
|
26
|
+
off(event: "resize", listener: () => void): unknown;
|
|
27
|
+
}
|
|
28
|
+
export declare function createTerminalDriver(options?: {
|
|
29
|
+
input?: InputStream;
|
|
30
|
+
output?: OutputStream;
|
|
31
|
+
escTimeoutMs?: number;
|
|
32
|
+
}): TerminalDriver;
|
|
33
|
+
export type { TerminalInputEvent } from "./input.js";
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { createInputParser } from "./input.js";
|
|
2
|
+
import { createFrameWriter } from "./output.js";
|
|
3
|
+
export function createTerminalDriver(options = {}) {
|
|
4
|
+
const input = options.input ?? process.stdin;
|
|
5
|
+
const output = options.output ?? process.stdout;
|
|
6
|
+
const eventListeners = new Set();
|
|
7
|
+
const resizeListeners = new Set();
|
|
8
|
+
const emit = (event) => {
|
|
9
|
+
for (const listener of eventListeners)
|
|
10
|
+
listener(event);
|
|
11
|
+
};
|
|
12
|
+
const parser = createInputParser({ escTimeoutMs: options.escTimeoutMs, onEvent: emit });
|
|
13
|
+
const writer = createFrameWriter(output);
|
|
14
|
+
let started = false;
|
|
15
|
+
const onData = (chunk) => {
|
|
16
|
+
for (const event of parser.feed(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)))
|
|
17
|
+
emit(event);
|
|
18
|
+
};
|
|
19
|
+
const getSize = () => ({ cols: dimension(output.columns), rows: dimension(output.rows) });
|
|
20
|
+
const onTerminalResize = () => {
|
|
21
|
+
const size = getSize();
|
|
22
|
+
for (const listener of resizeListeners)
|
|
23
|
+
listener(size);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
start() {
|
|
27
|
+
if (started)
|
|
28
|
+
return;
|
|
29
|
+
started = true;
|
|
30
|
+
input.setRawMode?.(true);
|
|
31
|
+
input.resume();
|
|
32
|
+
input.on("data", onData);
|
|
33
|
+
output.on("resize", onTerminalResize);
|
|
34
|
+
writer.open();
|
|
35
|
+
},
|
|
36
|
+
stop() {
|
|
37
|
+
if (!started)
|
|
38
|
+
return;
|
|
39
|
+
started = false;
|
|
40
|
+
input.off("data", onData);
|
|
41
|
+
output.off("resize", onTerminalResize);
|
|
42
|
+
parser.destroy();
|
|
43
|
+
writer.close();
|
|
44
|
+
input.setRawMode?.(false);
|
|
45
|
+
input.pause();
|
|
46
|
+
},
|
|
47
|
+
onEvent(fn) {
|
|
48
|
+
eventListeners.add(fn);
|
|
49
|
+
return () => { eventListeners.delete(fn); };
|
|
50
|
+
},
|
|
51
|
+
onResize(fn) {
|
|
52
|
+
resizeListeners.add(fn);
|
|
53
|
+
return () => { resizeListeners.delete(fn); };
|
|
54
|
+
},
|
|
55
|
+
getSize,
|
|
56
|
+
writeFrame: writer.writeFrame
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function dimension(value) {
|
|
60
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type TerminalInputEvent = {
|
|
2
|
+
type: "key";
|
|
3
|
+
name: string;
|
|
4
|
+
ch?: string;
|
|
5
|
+
ctrl: boolean;
|
|
6
|
+
alt: boolean;
|
|
7
|
+
shift: boolean;
|
|
8
|
+
} | {
|
|
9
|
+
type: "paste";
|
|
10
|
+
text: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: "wheel";
|
|
13
|
+
direction: "up" | "down";
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
};
|
|
17
|
+
export interface InputParser {
|
|
18
|
+
feed(chunk: Buffer): TerminalInputEvent[];
|
|
19
|
+
flush(): TerminalInputEvent[];
|
|
20
|
+
destroy(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function createInputParser(options?: {
|
|
23
|
+
escTimeoutMs?: number;
|
|
24
|
+
onEvent?: (event: TerminalInputEvent) => void;
|
|
25
|
+
}): InputParser;
|