@aurigax-ai/ghostty-web 0.3.8 → 0.3.9
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/lib/ghostty.ts +57 -2
- package/lib/headless.ts +29 -0
- package/lib/input-handler.ts +3 -0
- package/package.json +1 -1
package/lib/ghostty.ts
CHANGED
|
@@ -232,8 +232,8 @@ export interface TrackedRow {
|
|
|
232
232
|
const POINT_ACTIVE = 'ACTIVE';
|
|
233
233
|
const POINT_HISTORY = 'HISTORY';
|
|
234
234
|
const POINT_SCREEN = 'SCREEN';
|
|
235
|
-
type PointSpace = typeof POINT_ACTIVE | typeof POINT_HISTORY;
|
|
236
|
-
type PointTag = PointSpace
|
|
235
|
+
type PointSpace = typeof POINT_ACTIVE | typeof POINT_HISTORY | typeof POINT_SCREEN;
|
|
236
|
+
type PointTag = PointSpace;
|
|
237
237
|
|
|
238
238
|
const GRAPHEME_CAP = 16;
|
|
239
239
|
const GRAPHEME_CLUSTER_MODE = 2027;
|
|
@@ -1037,6 +1037,61 @@ export class GhosttyTerminal {
|
|
|
1037
1037
|
return out;
|
|
1038
1038
|
}
|
|
1039
1039
|
|
|
1040
|
+
/**
|
|
1041
|
+
* The rows `from` to `to` (0 is the oldest scrollback row) of the screen that
|
|
1042
|
+
* is showing, as text with its colours and styles as escape sequences. A
|
|
1043
|
+
* wrapped line comes back as one line, lines end in CR LF, and empty rows at
|
|
1044
|
+
* the end are dropped. One call into the engine, whatever the number of rows.
|
|
1045
|
+
*/
|
|
1046
|
+
formatRows(from: number, to: number): string {
|
|
1047
|
+
const abi = this.abi;
|
|
1048
|
+
const optionsSize = abi.sizeOf('GhosttyFormatterTerminalOptions');
|
|
1049
|
+
const selectionSize = abi.sizeOf('GhosttySelection');
|
|
1050
|
+
const refSize = abi.sizeOf('GhosttyGridRef');
|
|
1051
|
+
const lastCol = this._cols - 1;
|
|
1052
|
+
return abi.with(optionsSize + selectionSize, (base) => {
|
|
1053
|
+
const options = base;
|
|
1054
|
+
const selection = base + optionsSize;
|
|
1055
|
+
const O = (field: string) => options + abi.offset('GhosttyFormatterTerminalOptions', field);
|
|
1056
|
+
const S = (field: string) => selection + abi.offset('GhosttySelection', field);
|
|
1057
|
+
abi.view().setUint32(selection, selectionSize, true);
|
|
1058
|
+
const place = (field: string, x: number, y: number) =>
|
|
1059
|
+
this.gridRef(POINT_SCREEN, x, y, (ref) => {
|
|
1060
|
+
abi.bytes().copyWithin(S(field), ref, ref + refSize);
|
|
1061
|
+
return true;
|
|
1062
|
+
});
|
|
1063
|
+
if (!place('start', 0, from) || !place('end', lastCol, to)) return '';
|
|
1064
|
+
const view = abi.view();
|
|
1065
|
+
view.setUint32(O('size'), optionsSize, true);
|
|
1066
|
+
view.setInt32(O('emit'), abi.enumValue('GhosttyFormatterFormat', 'VT'), true);
|
|
1067
|
+
view.setUint8(O('unwrap'), 1);
|
|
1068
|
+
view.setUint8(O('trim'), 1);
|
|
1069
|
+
view.setUint32(O('extra'), abi.sizeOf('GhosttyFormatterTerminalExtra'), true);
|
|
1070
|
+
view.setUint32(O('selection'), selection, true);
|
|
1071
|
+
const slot = abi.call('ghostty_wasm_alloc_opaque');
|
|
1072
|
+
try {
|
|
1073
|
+
abi.check('ghostty_formatter_terminal_new', 0, slot, this.handle, options);
|
|
1074
|
+
const formatter = abi.call('ghostty_wasm_take_opaque', slot);
|
|
1075
|
+
try {
|
|
1076
|
+
const written = abi.scratch(4);
|
|
1077
|
+
const probe = abi.call('ghostty_formatter_format_buf', formatter, 0, 0, written);
|
|
1078
|
+
if (probe === GHOSTTY_SUCCESS) return '';
|
|
1079
|
+
if (probe !== GHOSTTY_OUT_OF_SPACE)
|
|
1080
|
+
throw new GhosttyCallError('ghostty_formatter_format_buf', probe);
|
|
1081
|
+
const need = abi.view().getUint32(written, true);
|
|
1082
|
+
return abi.with(need, (buf) => {
|
|
1083
|
+
abi.check('ghostty_formatter_format_buf', formatter, buf, need, written);
|
|
1084
|
+
return abi.string(buf, abi.view().getUint32(written, true));
|
|
1085
|
+
});
|
|
1086
|
+
} finally {
|
|
1087
|
+
abi.call('ghostty_formatter_free', formatter);
|
|
1088
|
+
}
|
|
1089
|
+
} finally {
|
|
1090
|
+
abi.call('ghostty_wasm_free_opaque', slot);
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1040
1095
|
getGrapheme(row: number, col: number): number[] | null {
|
|
1041
1096
|
return this.gridRef(POINT_ACTIVE, col, row, (ref) => this.graphemesAt(ref)) ?? null;
|
|
1042
1097
|
}
|
package/lib/headless.ts
CHANGED
|
@@ -150,6 +150,35 @@ export class HeadlessTerminal implements IDisposable {
|
|
|
150
150
|
this.refreshMarkers(engine.rowsDiscarded());
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/**
|
|
154
|
+
* The logical lines of the rows `from` to `to` (0 is the oldest scrollback
|
|
155
|
+
* row) of the screen that is showing, each with its colours and styles as
|
|
156
|
+
* escape sequences. A wrapped line is one line, so the count is the number
|
|
157
|
+
* of rows that do not continue the row before them, empty rows at the end
|
|
158
|
+
* included. Costs one call into the engine, not one per row or cell.
|
|
159
|
+
*/
|
|
160
|
+
serialize(range: { from: number; to: number }): string[] {
|
|
161
|
+
const engine = this.wasmTerm;
|
|
162
|
+
if (!engine || range.to < range.from) return [];
|
|
163
|
+
const text = engine.formatRows(range.from, range.to);
|
|
164
|
+
const printed = text === '' ? [] : text.split('\r\n');
|
|
165
|
+
const scrollback = engine.isAlternateScreen() ? 0 : engine.getScrollbackLength();
|
|
166
|
+
const wrapped = (y: number): boolean =>
|
|
167
|
+
y < scrollback ? engine.isScrollbackRowWrapped(y) : engine.isRowWrapped(y - scrollback);
|
|
168
|
+
const starts = [range.from];
|
|
169
|
+
for (let y = range.from + 1; y <= range.to; y++) if (!wrapped(y)) starts.push(y);
|
|
170
|
+
const lines: string[] = [];
|
|
171
|
+
let at = 0;
|
|
172
|
+
for (const [i, start] of starts.entries()) {
|
|
173
|
+
const rows = (starts[i + 1] ?? range.to + 1) - start;
|
|
174
|
+
let blank = 0;
|
|
175
|
+
while (blank < rows - 1 && engine.formatRows(start + blank, start + blank) === '') blank++;
|
|
176
|
+
at += blank;
|
|
177
|
+
lines.push(' '.repeat(blank * this.cols) + (printed[at++] ?? ''));
|
|
178
|
+
}
|
|
179
|
+
return lines;
|
|
180
|
+
}
|
|
181
|
+
|
|
153
182
|
registerMarker(cursorYOffset = 0): IMarker | undefined {
|
|
154
183
|
const engine = this.wasmTerm;
|
|
155
184
|
if (!engine) return undefined;
|
package/lib/input-handler.ts
CHANGED
|
@@ -850,6 +850,7 @@ export class InputHandler {
|
|
|
850
850
|
private handleMouseDown(event: MouseEvent): void {
|
|
851
851
|
if (this.isDisposed) return;
|
|
852
852
|
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
853
|
+
if (event.shiftKey) return;
|
|
853
854
|
|
|
854
855
|
const cell = this.pixelToCell(event);
|
|
855
856
|
if (!cell) return;
|
|
@@ -875,6 +876,7 @@ export class InputHandler {
|
|
|
875
876
|
private handleMouseUp(event: MouseEvent): void {
|
|
876
877
|
if (this.isDisposed) return;
|
|
877
878
|
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
879
|
+
if (event.shiftKey) return;
|
|
878
880
|
|
|
879
881
|
const cell = this.pixelToCell(event);
|
|
880
882
|
if (!cell) return;
|
|
@@ -895,6 +897,7 @@ export class InputHandler {
|
|
|
895
897
|
private handleMouseMove(event: MouseEvent): void {
|
|
896
898
|
if (this.isDisposed) return;
|
|
897
899
|
if (!this.mouseConfig?.hasMouseTracking()) return;
|
|
900
|
+
if (event.shiftKey) return;
|
|
898
901
|
|
|
899
902
|
// Check if button motion mode or any-event tracking is enabled
|
|
900
903
|
// Mode 1002 = button motion, Mode 1003 = any motion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aurigax-ai/ghostty-web",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Ghostty's terminal engine (libghostty-vt) in the browser: WebAssembly build, an xterm.js-shaped API and WebGL and canvas renderers. A hard fork of coder/ghostty-web.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.ts",
|