restty 0.1.13 → 0.1.15

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/README.md CHANGED
@@ -5,6 +5,7 @@ Powerful, lightweight browser terminal. Batteries included.
5
5
  Live demo: `https://restty.pages.dev/`
6
6
 
7
7
  Powered by:
8
+
8
9
  - `libghostty-vt` (WASM terminal core)
9
10
  - `WebGPU` (with WebGL2 fallback)
10
11
  - `text-shaper` (shaping + raster)
@@ -124,17 +125,37 @@ await restty.setFontSources([
124
125
  ]);
125
126
  ```
126
127
 
128
+ ### Touch behavior (pan-first by default)
129
+
130
+ On touch devices, restty defaults to pan-first scrolling with long-press selection.
131
+
132
+ ```ts
133
+ const restty = new Restty({
134
+ root: document.getElementById("terminal") as HTMLElement,
135
+ appOptions: {
136
+ // "long-press" (default) | "drag" | "off"
137
+ touchSelectionMode: "long-press",
138
+ // Optional tuning knobs:
139
+ touchSelectionLongPressMs: 450,
140
+ touchSelectionMoveThresholdPx: 10,
141
+ },
142
+ });
143
+ ```
144
+
127
145
  ## API Snapshot
128
146
 
129
147
  Primary class:
148
+
130
149
  - `new Restty({ root, ...options })`
131
150
  - `createRestty(options)`
132
151
 
133
152
  Pane access:
153
+
134
154
  - `panes()` / `pane(id)` / `activePane()` / `focusedPane()` / `forEachPane(visitor)`
135
155
  - `splitActivePane("vertical" | "horizontal")` / `splitPane(id, direction)` / `closePane(id)`
136
156
 
137
157
  Active-pane convenience:
158
+
138
159
  - `connectPty(url)` / `disconnectPty()` / `isPtyConnected()`
139
160
  - `setRenderer("auto" | "webgpu" | "webgl2")`
140
161
  - `setFontSize(number)` / `setFontSources([...])`
@@ -148,6 +169,7 @@ Active-pane convenience:
148
169
  ## Advanced / Internal Modules
149
170
 
150
171
  Use these only when you need lower-level control:
172
+
151
173
  - `restty/wasm`: low-level WASM ABI wrapper (`loadResttyWasm`, `ResttyWasm`)
152
174
  - `restty/input`: key/mouse/input encoding utilities
153
175
  - `restty/pty`: PTY transport helpers
@@ -2,7 +2,7 @@ import type { ResttyApp, ResttyAppOptions } from "./types";
2
2
  export { createResttyAppSession, getDefaultResttyAppSession } from "./session";
3
3
  export { createResttyPaneManager, createDefaultResttyPaneContextMenuItems, getResttyShortcutModifierLabel, } from "./panes";
4
4
  export { Restty } from "./restty";
5
- export type { ResttyAppElements, ResttyAppCallbacks, FontSource, ResttyFontSource, ResttyUrlFontSource, ResttyBufferFontSource, ResttyLocalFontSource, ResttyWasmLogListener, ResttyAppSession, ResttyAppOptions, ResttyApp, } from "./types";
5
+ export type { ResttyAppElements, ResttyAppCallbacks, FontSource, ResttyFontSource, ResttyTouchSelectionMode, ResttyUrlFontSource, ResttyBufferFontSource, ResttyLocalFontSource, ResttyWasmLogListener, ResttyAppSession, ResttyAppOptions, ResttyApp, } from "./types";
6
6
  export type { ResttyPaneSplitDirection, ResttyPaneContextMenuItem, ResttyPaneDefinition, ResttyPaneStyleOptions, ResttyPaneStylesOptions, ResttyPaneShortcutsOptions, ResttyPaneContextMenuOptions, CreateResttyPaneManagerOptions, ResttyPaneManager, ResttyPaneWithApp, CreateDefaultResttyPaneContextMenuItemsOptions, } from "./panes";
7
7
  export type { ResttyOptions } from "./restty";
8
8
  export declare function createResttyApp(options: ResttyAppOptions): ResttyApp;
package/dist/app/index.js CHANGED
@@ -8300,6 +8300,7 @@ class OutputFilter {
8300
8300
  altScreen = false;
8301
8301
  bracketedPaste = false;
8302
8302
  focusReporting = false;
8303
+ synchronizedOutput = false;
8303
8304
  windowOpHandler;
8304
8305
  getWindowMetrics;
8305
8306
  clipboardWrite;
@@ -8333,6 +8334,9 @@ class OutputFilter {
8333
8334
  isFocusReporting() {
8334
8335
  return this.focusReporting;
8335
8336
  }
8337
+ isSynchronizedOutput() {
8338
+ return this.synchronizedOutput;
8339
+ }
8336
8340
  replyOscColor(code, rgb) {
8337
8341
  const toHex4 = (value) => Math.round(Math.max(0, Math.min(255, value)) * 257).toString(16).padStart(4, "0");
8338
8342
  const r = toHex4(rgb[0]);
@@ -8398,6 +8402,8 @@ class OutputFilter {
8398
8402
  } else if (code === 1004) {
8399
8403
  this.focusReporting = enabled;
8400
8404
  handled = true;
8405
+ } else if (code === 2026) {
8406
+ this.synchronizedOutput = enabled;
8401
8407
  }
8402
8408
  }
8403
8409
  return handled;
@@ -8581,6 +8587,7 @@ function createInputHandler(options = {}) {
8581
8587
  isBracketedPaste: () => filter.isBracketedPaste(),
8582
8588
  isFocusReporting: () => filter.isFocusReporting(),
8583
8589
  isAltScreen: () => filter.isAltScreen(),
8590
+ isSynchronizedOutput: () => filter.isSynchronizedOutput(),
8584
8591
  sendMouseEvent: (kind, event) => mouse.sendMouseEvent(kind, event)
8585
8592
  };
8586
8593
  }
@@ -50316,6 +50323,17 @@ function createRestty(options) {
50316
50323
  }
50317
50324
 
50318
50325
  // src/app/index.ts
50326
+ function normalizeTouchSelectionMode(value) {
50327
+ if (value === "drag" || value === "long-press" || value === "off")
50328
+ return value;
50329
+ return "long-press";
50330
+ }
50331
+ function clampFiniteNumber(value, fallback, min, max, round = false) {
50332
+ if (!Number.isFinite(value))
50333
+ return fallback;
50334
+ const numeric = round ? Math.round(value) : Number(value);
50335
+ return Math.min(max, Math.max(min, numeric));
50336
+ }
50319
50337
  function createResttyApp(options) {
50320
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50321
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50338,6 +50356,9 @@ function createResttyApp(options) {
50338
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50339
50357
  const autoResize = options.autoResize ?? true;
50340
50358
  const debugExpose = options.debugExpose ?? false;
50359
+ const touchSelectionMode = normalizeTouchSelectionMode(options.touchSelectionMode);
50360
+ const touchSelectionLongPressMs = clampFiniteNumber(options.touchSelectionLongPressMs, 450, 120, 2000, true);
50361
+ const touchSelectionMoveThresholdPx = clampFiniteNumber(options.touchSelectionMoveThresholdPx, 10, 1, 64);
50341
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50342
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50343
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50443,6 +50464,14 @@ function createResttyApp(options) {
50443
50464
  let lastKeydownSeqAt = 0;
50444
50465
  let nextBlinkTime = performance.now() + CURSOR_BLINK_MS;
50445
50466
  const ptyTransport = options.ptyTransport ?? createWebSocketPtyTransport();
50467
+ const PTY_OUTPUT_IDLE_MS = 10;
50468
+ const PTY_OUTPUT_MAX_MS = 40;
50469
+ const SYNC_OUTPUT_RESET_MS = 1000;
50470
+ const SYNC_OUTPUT_RESET_SEQ = "\x1B[?2026l";
50471
+ let ptyOutputBuffer = "";
50472
+ let ptyOutputIdleTimer = 0;
50473
+ let ptyOutputMaxTimer = 0;
50474
+ let syncOutputResetTimer = 0;
50446
50475
  let lastCursorForCpr = { row: 1, col: 1 };
50447
50476
  let inputHandler = null;
50448
50477
  let activeTheme = null;
@@ -50531,6 +50560,14 @@ function createResttyApp(options) {
50531
50560
  anchor: null,
50532
50561
  focus: null
50533
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50534
50571
  const linkState = {
50535
50572
  hoverId: 0,
50536
50573
  hoverUri: ""
@@ -50552,6 +50589,27 @@ function createResttyApp(options) {
50552
50589
  }
50553
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50554
50591
  }
50592
+ function isTouchPointer(event) {
50593
+ return event.pointerType === "touch";
50594
+ }
50595
+ function clearPendingTouchSelection() {
50596
+ if (touchSelectionState.pendingTimer) {
50597
+ clearTimeout(touchSelectionState.pendingTimer);
50598
+ touchSelectionState.pendingTimer = 0;
50599
+ }
50600
+ touchSelectionState.pendingPointerId = null;
50601
+ touchSelectionState.pendingCell = null;
50602
+ }
50603
+ function beginSelectionDrag(cell, pointerId) {
50604
+ selectionState.active = true;
50605
+ selectionState.dragging = true;
50606
+ selectionState.anchor = cell;
50607
+ selectionState.focus = cell;
50608
+ touchSelectionState.activePointerId = pointerId;
50609
+ canvas.setPointerCapture?.(pointerId);
50610
+ updateCanvasCursor();
50611
+ needsRender = true;
50612
+ }
50555
50613
  function noteScrollActivity() {
50556
50614
  scrollbarState.lastInputAt = performance.now();
50557
50615
  }
@@ -51152,6 +51210,7 @@ function createResttyApp(options) {
51152
51210
  selectionState.dragging = false;
51153
51211
  selectionState.anchor = null;
51154
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51155
51214
  updateCanvasCursor();
51156
51215
  needsRender = true;
51157
51216
  }
@@ -51202,7 +51261,71 @@ function createResttyApp(options) {
51202
51261
  const label = status.active ? `${status.mode} (${status.detail})` : status.mode;
51203
51262
  setMouseStatus(label);
51204
51263
  }
51264
+ function cancelPtyOutputFlush() {
51265
+ if (ptyOutputIdleTimer) {
51266
+ clearTimeout(ptyOutputIdleTimer);
51267
+ ptyOutputIdleTimer = 0;
51268
+ }
51269
+ if (ptyOutputMaxTimer) {
51270
+ clearTimeout(ptyOutputMaxTimer);
51271
+ ptyOutputMaxTimer = 0;
51272
+ }
51273
+ }
51274
+ function cancelSyncOutputReset() {
51275
+ if (syncOutputResetTimer) {
51276
+ clearTimeout(syncOutputResetTimer);
51277
+ syncOutputResetTimer = 0;
51278
+ }
51279
+ }
51280
+ function scheduleSyncOutputReset() {
51281
+ if (syncOutputResetTimer)
51282
+ return;
51283
+ syncOutputResetTimer = setTimeout(() => {
51284
+ syncOutputResetTimer = 0;
51285
+ if (!inputHandler?.isSynchronizedOutput?.())
51286
+ return;
51287
+ const sanitized = inputHandler.filterOutput(SYNC_OUTPUT_RESET_SEQ) || SYNC_OUTPUT_RESET_SEQ;
51288
+ sendInput(sanitized, "pty");
51289
+ }, SYNC_OUTPUT_RESET_MS);
51290
+ }
51291
+ function flushPtyOutputBuffer() {
51292
+ const output = ptyOutputBuffer;
51293
+ ptyOutputBuffer = "";
51294
+ if (!output)
51295
+ return;
51296
+ sendInput(output, "pty");
51297
+ }
51298
+ function queuePtyOutput(text) {
51299
+ if (!text)
51300
+ return;
51301
+ ptyOutputBuffer += text;
51302
+ if (ptyOutputIdleTimer) {
51303
+ clearTimeout(ptyOutputIdleTimer);
51304
+ }
51305
+ ptyOutputIdleTimer = setTimeout(() => {
51306
+ ptyOutputIdleTimer = 0;
51307
+ if (ptyOutputMaxTimer) {
51308
+ clearTimeout(ptyOutputMaxTimer);
51309
+ ptyOutputMaxTimer = 0;
51310
+ }
51311
+ flushPtyOutputBuffer();
51312
+ }, PTY_OUTPUT_IDLE_MS);
51313
+ if (!ptyOutputMaxTimer) {
51314
+ ptyOutputMaxTimer = setTimeout(() => {
51315
+ ptyOutputMaxTimer = 0;
51316
+ if (ptyOutputIdleTimer) {
51317
+ clearTimeout(ptyOutputIdleTimer);
51318
+ ptyOutputIdleTimer = 0;
51319
+ }
51320
+ flushPtyOutputBuffer();
51321
+ }, PTY_OUTPUT_MAX_MS);
51322
+ }
51323
+ }
51205
51324
  function disconnectPty2() {
51325
+ flushPtyOutputBuffer();
51326
+ cancelPtyOutputFlush();
51327
+ cancelSyncOutputReset();
51328
+ ptyOutputBuffer = "";
51206
51329
  ptyTransport.disconnect();
51207
51330
  updateMouseStatus();
51208
51331
  setPtyStatus("disconnected");
@@ -51249,7 +51372,7 @@ function createResttyApp(options) {
51249
51372
  const sanitized = inputHandler ? inputHandler.filterOutput(text) : text;
51250
51373
  updateMouseStatus();
51251
51374
  if (sanitized)
51252
- sendInput(sanitized, "pty");
51375
+ queuePtyOutput(sanitized);
51253
51376
  }
51254
51377
  }
51255
51378
  });
@@ -51301,30 +51424,71 @@ function createResttyApp(options) {
51301
51424
  function bindCanvasEvents() {
51302
51425
  if (!attachCanvasEvents)
51303
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51304
51428
  const onPointerDown = (event) => {
51305
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51306
51430
  event.preventDefault();
51307
51431
  canvas.setPointerCapture?.(event.pointerId);
51308
51432
  return;
51309
51433
  }
51434
+ if (isTouchPointer(event)) {
51435
+ if (event.button !== 0)
51436
+ return;
51437
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51438
+ touchSelectionState.activePointerId = null;
51439
+ if (touchSelectionMode === "off")
51440
+ return;
51441
+ if (touchSelectionMode === "drag") {
51442
+ event.preventDefault();
51443
+ beginSelectionDrag(cell2, event.pointerId);
51444
+ return;
51445
+ }
51446
+ clearPendingTouchSelection();
51447
+ touchSelectionState.pendingPointerId = event.pointerId;
51448
+ touchSelectionState.pendingCell = cell2;
51449
+ touchSelectionState.pendingStartX = event.clientX;
51450
+ touchSelectionState.pendingStartY = event.clientY;
51451
+ touchSelectionState.pendingTimer = setTimeout(() => {
51452
+ if (touchSelectionState.pendingPointerId !== event.pointerId || !touchSelectionState.pendingCell) {
51453
+ return;
51454
+ }
51455
+ const pendingCell = touchSelectionState.pendingCell;
51456
+ clearPendingTouchSelection();
51457
+ beginSelectionDrag(pendingCell, event.pointerId);
51458
+ }, touchSelectionLongPressMs);
51459
+ return;
51460
+ }
51310
51461
  if (event.button !== 0)
51311
51462
  return;
51312
51463
  event.preventDefault();
51313
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51314
51465
  updateLinkHover(cell);
51315
- selectionState.active = true;
51316
- selectionState.dragging = true;
51317
- selectionState.anchor = cell;
51318
- selectionState.focus = cell;
51319
- canvas.setPointerCapture?.(event.pointerId);
51320
- updateCanvasCursor();
51321
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51322
51467
  };
51323
51468
  const onPointerMove = (event) => {
51324
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51325
51470
  event.preventDefault();
51326
51471
  return;
51327
51472
  }
51473
+ if (isTouchPointer(event)) {
51474
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51475
+ const dx = event.clientX - touchSelectionState.pendingStartX;
51476
+ const dy = event.clientY - touchSelectionState.pendingStartY;
51477
+ if (dx * dx + dy * dy >= touchSelectionMoveThresholdPx * touchSelectionMoveThresholdPx) {
51478
+ clearPendingTouchSelection();
51479
+ }
51480
+ return;
51481
+ }
51482
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51483
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51484
+ event.preventDefault();
51485
+ selectionState.focus = cell2;
51486
+ updateLinkHover(null);
51487
+ updateCanvasCursor();
51488
+ needsRender = true;
51489
+ }
51490
+ return;
51491
+ }
51328
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51329
51493
  if (!selectionState.dragging) {
51330
51494
  updateLinkHover(cell);
@@ -51341,6 +51505,27 @@ function createResttyApp(options) {
51341
51505
  event.preventDefault();
51342
51506
  return;
51343
51507
  }
51508
+ if (isTouchPointer(event)) {
51509
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51510
+ clearPendingTouchSelection();
51511
+ touchSelectionState.activePointerId = null;
51512
+ return;
51513
+ }
51514
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51515
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51516
+ event.preventDefault();
51517
+ selectionState.dragging = false;
51518
+ selectionState.focus = cell2;
51519
+ touchSelectionState.activePointerId = null;
51520
+ if (selectionState.anchor && selectionState.focus && selectionState.anchor.row === selectionState.focus.row && selectionState.anchor.col === selectionState.focus.col) {
51521
+ clearSelection();
51522
+ } else {
51523
+ updateCanvasCursor();
51524
+ needsRender = true;
51525
+ }
51526
+ }
51527
+ return;
51528
+ }
51344
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51345
51530
  if (selectionState.dragging) {
51346
51531
  event.preventDefault();
@@ -51359,6 +51544,21 @@ function createResttyApp(options) {
51359
51544
  openLink(linkState.hoverUri);
51360
51545
  }
51361
51546
  };
51547
+ const onPointerCancel = (event) => {
51548
+ if (isTouchPointer(event)) {
51549
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51550
+ clearPendingTouchSelection();
51551
+ }
51552
+ if (touchSelectionState.activePointerId === event.pointerId) {
51553
+ touchSelectionState.activePointerId = null;
51554
+ if (selectionState.dragging) {
51555
+ selectionState.dragging = false;
51556
+ updateCanvasCursor();
51557
+ needsRender = true;
51558
+ }
51559
+ }
51560
+ }
51561
+ };
51362
51562
  const onWheel = (event) => {
51363
51563
  const mouseActive = inputHandler.isMouseActive();
51364
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51401,6 +51601,7 @@ function createResttyApp(options) {
51401
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51402
51602
  canvas.addEventListener("pointermove", onPointerMove);
51403
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51404
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51405
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51406
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51408,9 +51609,11 @@ function createResttyApp(options) {
51408
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51409
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51410
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51411
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51412
51614
  canvas.removeEventListener("wheel", onWheel);
51413
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51414
51617
  });
51415
51618
  if (imeInput) {
51416
51619
  let suppressNextInput = false;
@@ -55497,7 +55700,7 @@ function createResttyApp(options) {
55497
55700
  return;
55498
55701
  if (!text)
55499
55702
  return;
55500
- const normalized = normalizeNewlines(text);
55703
+ const normalized = source === "pty" ? text : normalizeNewlines(text);
55501
55704
  if (source === "key") {
55502
55705
  const bytes = textEncoder3.encode(normalized);
55503
55706
  const hex = Array.from(bytes, (b3) => b3.toString(16).padStart(2, "0")).join(" ");
@@ -55519,8 +55722,13 @@ function createResttyApp(options) {
55519
55722
  clearSelection();
55520
55723
  }
55521
55724
  writeToWasm(wasmHandle, normalized);
55522
- wasm.renderUpdate(wasmHandle);
55523
55725
  flushWasmOutputToPty();
55726
+ if (source === "pty" && inputHandler?.isSynchronizedOutput?.()) {
55727
+ scheduleSyncOutputReset();
55728
+ return;
55729
+ }
55730
+ cancelSyncOutputReset();
55731
+ wasm.renderUpdate(wasmHandle);
55524
55732
  if (source === "key" && wasmExports?.restty_debug_cursor_x && wasmExports?.restty_debug_cursor_y) {
55525
55733
  const ax = wasmExports.restty_debug_cursor_x(wasmHandle);
55526
55734
  const ay = wasmExports.restty_debug_cursor_y(wasmHandle);
@@ -55749,6 +55957,7 @@ function createResttyApp(options) {
55749
55957
  clearTimeout(terminalResizeTimer);
55750
55958
  terminalResizeTimer = 0;
55751
55959
  }
55960
+ cancelSyncOutputReset();
55752
55961
  pendingTerminalResize = null;
55753
55962
  disconnectPty2();
55754
55963
  ptyTransport.destroy?.();
@@ -63,6 +63,7 @@ export type ResttyLocalFontSource = {
63
63
  export type ResttyFontSource = ResttyUrlFontSource | ResttyBufferFontSource | ResttyLocalFontSource;
64
64
  export type FontSource = ResttyFontSource;
65
65
  export type ResttyFontPreset = "default-cdn" | "none";
66
+ export type ResttyTouchSelectionMode = "drag" | "long-press" | "off";
66
67
  export type ResttyAppOptions = {
67
68
  canvas: HTMLCanvasElement;
68
69
  session?: ResttyAppSession;
@@ -83,6 +84,23 @@ export type ResttyAppOptions = {
83
84
  autoResize?: boolean;
84
85
  attachWindowEvents?: boolean;
85
86
  attachCanvasEvents?: boolean;
87
+ /**
88
+ * Touch selection behavior on pointerType=touch:
89
+ * - drag: immediate drag-selection (legacy behavior)
90
+ * - long-press: selection starts after press timeout (default)
91
+ * - off: disable touch selection, keep touch scrolling
92
+ */
93
+ touchSelectionMode?: ResttyTouchSelectionMode;
94
+ /**
95
+ * Long-press timeout in ms for touch selection intent.
96
+ * Only used when touchSelectionMode is "long-press".
97
+ */
98
+ touchSelectionLongPressMs?: number;
99
+ /**
100
+ * Pointer move threshold in CSS pixels before long-press selection is
101
+ * canceled and touch pan-scroll takes priority.
102
+ */
103
+ touchSelectionMoveThresholdPx?: number;
86
104
  debugExpose?: boolean;
87
105
  ptyTransport?: PtyTransport;
88
106
  };
package/dist/index.js CHANGED
@@ -8300,6 +8300,7 @@ class OutputFilter {
8300
8300
  altScreen = false;
8301
8301
  bracketedPaste = false;
8302
8302
  focusReporting = false;
8303
+ synchronizedOutput = false;
8303
8304
  windowOpHandler;
8304
8305
  getWindowMetrics;
8305
8306
  clipboardWrite;
@@ -8333,6 +8334,9 @@ class OutputFilter {
8333
8334
  isFocusReporting() {
8334
8335
  return this.focusReporting;
8335
8336
  }
8337
+ isSynchronizedOutput() {
8338
+ return this.synchronizedOutput;
8339
+ }
8336
8340
  replyOscColor(code, rgb) {
8337
8341
  const toHex4 = (value) => Math.round(Math.max(0, Math.min(255, value)) * 257).toString(16).padStart(4, "0");
8338
8342
  const r = toHex4(rgb[0]);
@@ -8398,6 +8402,8 @@ class OutputFilter {
8398
8402
  } else if (code === 1004) {
8399
8403
  this.focusReporting = enabled;
8400
8404
  handled = true;
8405
+ } else if (code === 2026) {
8406
+ this.synchronizedOutput = enabled;
8401
8407
  }
8402
8408
  }
8403
8409
  return handled;
@@ -8581,6 +8587,7 @@ function createInputHandler(options = {}) {
8581
8587
  isBracketedPaste: () => filter.isBracketedPaste(),
8582
8588
  isFocusReporting: () => filter.isFocusReporting(),
8583
8589
  isAltScreen: () => filter.isAltScreen(),
8590
+ isSynchronizedOutput: () => filter.isSynchronizedOutput(),
8584
8591
  sendMouseEvent: (kind, event) => mouse.sendMouseEvent(kind, event)
8585
8592
  };
8586
8593
  }
@@ -50316,6 +50323,17 @@ function createRestty(options) {
50316
50323
  }
50317
50324
 
50318
50325
  // src/app/index.ts
50326
+ function normalizeTouchSelectionMode(value) {
50327
+ if (value === "drag" || value === "long-press" || value === "off")
50328
+ return value;
50329
+ return "long-press";
50330
+ }
50331
+ function clampFiniteNumber(value, fallback, min, max, round = false) {
50332
+ if (!Number.isFinite(value))
50333
+ return fallback;
50334
+ const numeric = round ? Math.round(value) : Number(value);
50335
+ return Math.min(max, Math.max(min, numeric));
50336
+ }
50319
50337
  function createResttyApp(options) {
50320
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50321
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50338,6 +50356,9 @@ function createResttyApp(options) {
50338
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50339
50357
  const autoResize = options.autoResize ?? true;
50340
50358
  const debugExpose = options.debugExpose ?? false;
50359
+ const touchSelectionMode = normalizeTouchSelectionMode(options.touchSelectionMode);
50360
+ const touchSelectionLongPressMs = clampFiniteNumber(options.touchSelectionLongPressMs, 450, 120, 2000, true);
50361
+ const touchSelectionMoveThresholdPx = clampFiniteNumber(options.touchSelectionMoveThresholdPx, 10, 1, 64);
50341
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50342
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50343
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50443,6 +50464,14 @@ function createResttyApp(options) {
50443
50464
  let lastKeydownSeqAt = 0;
50444
50465
  let nextBlinkTime = performance.now() + CURSOR_BLINK_MS;
50445
50466
  const ptyTransport = options.ptyTransport ?? createWebSocketPtyTransport();
50467
+ const PTY_OUTPUT_IDLE_MS = 10;
50468
+ const PTY_OUTPUT_MAX_MS = 40;
50469
+ const SYNC_OUTPUT_RESET_MS = 1000;
50470
+ const SYNC_OUTPUT_RESET_SEQ = "\x1B[?2026l";
50471
+ let ptyOutputBuffer = "";
50472
+ let ptyOutputIdleTimer = 0;
50473
+ let ptyOutputMaxTimer = 0;
50474
+ let syncOutputResetTimer = 0;
50446
50475
  let lastCursorForCpr = { row: 1, col: 1 };
50447
50476
  let inputHandler = null;
50448
50477
  let activeTheme = null;
@@ -50531,6 +50560,14 @@ function createResttyApp(options) {
50531
50560
  anchor: null,
50532
50561
  focus: null
50533
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50534
50571
  const linkState = {
50535
50572
  hoverId: 0,
50536
50573
  hoverUri: ""
@@ -50552,6 +50589,27 @@ function createResttyApp(options) {
50552
50589
  }
50553
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50554
50591
  }
50592
+ function isTouchPointer(event) {
50593
+ return event.pointerType === "touch";
50594
+ }
50595
+ function clearPendingTouchSelection() {
50596
+ if (touchSelectionState.pendingTimer) {
50597
+ clearTimeout(touchSelectionState.pendingTimer);
50598
+ touchSelectionState.pendingTimer = 0;
50599
+ }
50600
+ touchSelectionState.pendingPointerId = null;
50601
+ touchSelectionState.pendingCell = null;
50602
+ }
50603
+ function beginSelectionDrag(cell, pointerId) {
50604
+ selectionState.active = true;
50605
+ selectionState.dragging = true;
50606
+ selectionState.anchor = cell;
50607
+ selectionState.focus = cell;
50608
+ touchSelectionState.activePointerId = pointerId;
50609
+ canvas.setPointerCapture?.(pointerId);
50610
+ updateCanvasCursor();
50611
+ needsRender = true;
50612
+ }
50555
50613
  function noteScrollActivity() {
50556
50614
  scrollbarState.lastInputAt = performance.now();
50557
50615
  }
@@ -51152,6 +51210,7 @@ function createResttyApp(options) {
51152
51210
  selectionState.dragging = false;
51153
51211
  selectionState.anchor = null;
51154
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51155
51214
  updateCanvasCursor();
51156
51215
  needsRender = true;
51157
51216
  }
@@ -51202,7 +51261,71 @@ function createResttyApp(options) {
51202
51261
  const label = status.active ? `${status.mode} (${status.detail})` : status.mode;
51203
51262
  setMouseStatus(label);
51204
51263
  }
51264
+ function cancelPtyOutputFlush() {
51265
+ if (ptyOutputIdleTimer) {
51266
+ clearTimeout(ptyOutputIdleTimer);
51267
+ ptyOutputIdleTimer = 0;
51268
+ }
51269
+ if (ptyOutputMaxTimer) {
51270
+ clearTimeout(ptyOutputMaxTimer);
51271
+ ptyOutputMaxTimer = 0;
51272
+ }
51273
+ }
51274
+ function cancelSyncOutputReset() {
51275
+ if (syncOutputResetTimer) {
51276
+ clearTimeout(syncOutputResetTimer);
51277
+ syncOutputResetTimer = 0;
51278
+ }
51279
+ }
51280
+ function scheduleSyncOutputReset() {
51281
+ if (syncOutputResetTimer)
51282
+ return;
51283
+ syncOutputResetTimer = setTimeout(() => {
51284
+ syncOutputResetTimer = 0;
51285
+ if (!inputHandler?.isSynchronizedOutput?.())
51286
+ return;
51287
+ const sanitized = inputHandler.filterOutput(SYNC_OUTPUT_RESET_SEQ) || SYNC_OUTPUT_RESET_SEQ;
51288
+ sendInput(sanitized, "pty");
51289
+ }, SYNC_OUTPUT_RESET_MS);
51290
+ }
51291
+ function flushPtyOutputBuffer() {
51292
+ const output = ptyOutputBuffer;
51293
+ ptyOutputBuffer = "";
51294
+ if (!output)
51295
+ return;
51296
+ sendInput(output, "pty");
51297
+ }
51298
+ function queuePtyOutput(text) {
51299
+ if (!text)
51300
+ return;
51301
+ ptyOutputBuffer += text;
51302
+ if (ptyOutputIdleTimer) {
51303
+ clearTimeout(ptyOutputIdleTimer);
51304
+ }
51305
+ ptyOutputIdleTimer = setTimeout(() => {
51306
+ ptyOutputIdleTimer = 0;
51307
+ if (ptyOutputMaxTimer) {
51308
+ clearTimeout(ptyOutputMaxTimer);
51309
+ ptyOutputMaxTimer = 0;
51310
+ }
51311
+ flushPtyOutputBuffer();
51312
+ }, PTY_OUTPUT_IDLE_MS);
51313
+ if (!ptyOutputMaxTimer) {
51314
+ ptyOutputMaxTimer = setTimeout(() => {
51315
+ ptyOutputMaxTimer = 0;
51316
+ if (ptyOutputIdleTimer) {
51317
+ clearTimeout(ptyOutputIdleTimer);
51318
+ ptyOutputIdleTimer = 0;
51319
+ }
51320
+ flushPtyOutputBuffer();
51321
+ }, PTY_OUTPUT_MAX_MS);
51322
+ }
51323
+ }
51205
51324
  function disconnectPty2() {
51325
+ flushPtyOutputBuffer();
51326
+ cancelPtyOutputFlush();
51327
+ cancelSyncOutputReset();
51328
+ ptyOutputBuffer = "";
51206
51329
  ptyTransport.disconnect();
51207
51330
  updateMouseStatus();
51208
51331
  setPtyStatus("disconnected");
@@ -51249,7 +51372,7 @@ function createResttyApp(options) {
51249
51372
  const sanitized = inputHandler ? inputHandler.filterOutput(text) : text;
51250
51373
  updateMouseStatus();
51251
51374
  if (sanitized)
51252
- sendInput(sanitized, "pty");
51375
+ queuePtyOutput(sanitized);
51253
51376
  }
51254
51377
  }
51255
51378
  });
@@ -51301,30 +51424,71 @@ function createResttyApp(options) {
51301
51424
  function bindCanvasEvents() {
51302
51425
  if (!attachCanvasEvents)
51303
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51304
51428
  const onPointerDown = (event) => {
51305
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51306
51430
  event.preventDefault();
51307
51431
  canvas.setPointerCapture?.(event.pointerId);
51308
51432
  return;
51309
51433
  }
51434
+ if (isTouchPointer(event)) {
51435
+ if (event.button !== 0)
51436
+ return;
51437
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51438
+ touchSelectionState.activePointerId = null;
51439
+ if (touchSelectionMode === "off")
51440
+ return;
51441
+ if (touchSelectionMode === "drag") {
51442
+ event.preventDefault();
51443
+ beginSelectionDrag(cell2, event.pointerId);
51444
+ return;
51445
+ }
51446
+ clearPendingTouchSelection();
51447
+ touchSelectionState.pendingPointerId = event.pointerId;
51448
+ touchSelectionState.pendingCell = cell2;
51449
+ touchSelectionState.pendingStartX = event.clientX;
51450
+ touchSelectionState.pendingStartY = event.clientY;
51451
+ touchSelectionState.pendingTimer = setTimeout(() => {
51452
+ if (touchSelectionState.pendingPointerId !== event.pointerId || !touchSelectionState.pendingCell) {
51453
+ return;
51454
+ }
51455
+ const pendingCell = touchSelectionState.pendingCell;
51456
+ clearPendingTouchSelection();
51457
+ beginSelectionDrag(pendingCell, event.pointerId);
51458
+ }, touchSelectionLongPressMs);
51459
+ return;
51460
+ }
51310
51461
  if (event.button !== 0)
51311
51462
  return;
51312
51463
  event.preventDefault();
51313
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51314
51465
  updateLinkHover(cell);
51315
- selectionState.active = true;
51316
- selectionState.dragging = true;
51317
- selectionState.anchor = cell;
51318
- selectionState.focus = cell;
51319
- canvas.setPointerCapture?.(event.pointerId);
51320
- updateCanvasCursor();
51321
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51322
51467
  };
51323
51468
  const onPointerMove = (event) => {
51324
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51325
51470
  event.preventDefault();
51326
51471
  return;
51327
51472
  }
51473
+ if (isTouchPointer(event)) {
51474
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51475
+ const dx = event.clientX - touchSelectionState.pendingStartX;
51476
+ const dy = event.clientY - touchSelectionState.pendingStartY;
51477
+ if (dx * dx + dy * dy >= touchSelectionMoveThresholdPx * touchSelectionMoveThresholdPx) {
51478
+ clearPendingTouchSelection();
51479
+ }
51480
+ return;
51481
+ }
51482
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51483
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51484
+ event.preventDefault();
51485
+ selectionState.focus = cell2;
51486
+ updateLinkHover(null);
51487
+ updateCanvasCursor();
51488
+ needsRender = true;
51489
+ }
51490
+ return;
51491
+ }
51328
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51329
51493
  if (!selectionState.dragging) {
51330
51494
  updateLinkHover(cell);
@@ -51341,6 +51505,27 @@ function createResttyApp(options) {
51341
51505
  event.preventDefault();
51342
51506
  return;
51343
51507
  }
51508
+ if (isTouchPointer(event)) {
51509
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51510
+ clearPendingTouchSelection();
51511
+ touchSelectionState.activePointerId = null;
51512
+ return;
51513
+ }
51514
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51515
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51516
+ event.preventDefault();
51517
+ selectionState.dragging = false;
51518
+ selectionState.focus = cell2;
51519
+ touchSelectionState.activePointerId = null;
51520
+ if (selectionState.anchor && selectionState.focus && selectionState.anchor.row === selectionState.focus.row && selectionState.anchor.col === selectionState.focus.col) {
51521
+ clearSelection();
51522
+ } else {
51523
+ updateCanvasCursor();
51524
+ needsRender = true;
51525
+ }
51526
+ }
51527
+ return;
51528
+ }
51344
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51345
51530
  if (selectionState.dragging) {
51346
51531
  event.preventDefault();
@@ -51359,6 +51544,21 @@ function createResttyApp(options) {
51359
51544
  openLink(linkState.hoverUri);
51360
51545
  }
51361
51546
  };
51547
+ const onPointerCancel = (event) => {
51548
+ if (isTouchPointer(event)) {
51549
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51550
+ clearPendingTouchSelection();
51551
+ }
51552
+ if (touchSelectionState.activePointerId === event.pointerId) {
51553
+ touchSelectionState.activePointerId = null;
51554
+ if (selectionState.dragging) {
51555
+ selectionState.dragging = false;
51556
+ updateCanvasCursor();
51557
+ needsRender = true;
51558
+ }
51559
+ }
51560
+ }
51561
+ };
51362
51562
  const onWheel = (event) => {
51363
51563
  const mouseActive = inputHandler.isMouseActive();
51364
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51401,6 +51601,7 @@ function createResttyApp(options) {
51401
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51402
51602
  canvas.addEventListener("pointermove", onPointerMove);
51403
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51404
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51405
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51406
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51408,9 +51609,11 @@ function createResttyApp(options) {
51408
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51409
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51410
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51411
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51412
51614
  canvas.removeEventListener("wheel", onWheel);
51413
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51414
51617
  });
51415
51618
  if (imeInput) {
51416
51619
  let suppressNextInput = false;
@@ -55497,7 +55700,7 @@ function createResttyApp(options) {
55497
55700
  return;
55498
55701
  if (!text)
55499
55702
  return;
55500
- const normalized = normalizeNewlines(text);
55703
+ const normalized = source === "pty" ? text : normalizeNewlines(text);
55501
55704
  if (source === "key") {
55502
55705
  const bytes = textEncoder3.encode(normalized);
55503
55706
  const hex = Array.from(bytes, (b3) => b3.toString(16).padStart(2, "0")).join(" ");
@@ -55519,8 +55722,13 @@ function createResttyApp(options) {
55519
55722
  clearSelection();
55520
55723
  }
55521
55724
  writeToWasm(wasmHandle, normalized);
55522
- wasm.renderUpdate(wasmHandle);
55523
55725
  flushWasmOutputToPty();
55726
+ if (source === "pty" && inputHandler?.isSynchronizedOutput?.()) {
55727
+ scheduleSyncOutputReset();
55728
+ return;
55729
+ }
55730
+ cancelSyncOutputReset();
55731
+ wasm.renderUpdate(wasmHandle);
55524
55732
  if (source === "key" && wasmExports?.restty_debug_cursor_x && wasmExports?.restty_debug_cursor_y) {
55525
55733
  const ax = wasmExports.restty_debug_cursor_x(wasmHandle);
55526
55734
  const ay = wasmExports.restty_debug_cursor_y(wasmHandle);
@@ -55749,6 +55957,7 @@ function createResttyApp(options) {
55749
55957
  clearTimeout(terminalResizeTimer);
55750
55958
  terminalResizeTimer = 0;
55751
55959
  }
55960
+ cancelSyncOutputReset();
55752
55961
  pendingTerminalResize = null;
55753
55962
  disconnectPty2();
55754
55963
  ptyTransport.destroy?.();
@@ -751,6 +751,7 @@ class OutputFilter {
751
751
  altScreen = false;
752
752
  bracketedPaste = false;
753
753
  focusReporting = false;
754
+ synchronizedOutput = false;
754
755
  windowOpHandler;
755
756
  getWindowMetrics;
756
757
  clipboardWrite;
@@ -784,6 +785,9 @@ class OutputFilter {
784
785
  isFocusReporting() {
785
786
  return this.focusReporting;
786
787
  }
788
+ isSynchronizedOutput() {
789
+ return this.synchronizedOutput;
790
+ }
787
791
  replyOscColor(code, rgb) {
788
792
  const toHex4 = (value) => Math.round(Math.max(0, Math.min(255, value)) * 257).toString(16).padStart(4, "0");
789
793
  const r = toHex4(rgb[0]);
@@ -849,6 +853,8 @@ class OutputFilter {
849
853
  } else if (code === 1004) {
850
854
  this.focusReporting = enabled;
851
855
  handled = true;
856
+ } else if (code === 2026) {
857
+ this.synchronizedOutput = enabled;
852
858
  }
853
859
  }
854
860
  return handled;
@@ -1032,6 +1038,7 @@ function createInputHandler(options = {}) {
1032
1038
  isBracketedPaste: () => filter.isBracketedPaste(),
1033
1039
  isFocusReporting: () => filter.isFocusReporting(),
1034
1040
  isAltScreen: () => filter.isAltScreen(),
1041
+ isSynchronizedOutput: () => filter.isSynchronizedOutput(),
1035
1042
  sendMouseEvent: (kind, event) => mouse.sendMouseEvent(kind, event)
1036
1043
  };
1037
1044
  }
@@ -33,6 +33,7 @@ export declare class OutputFilter {
33
33
  private altScreen;
34
34
  private bracketedPaste;
35
35
  private focusReporting;
36
+ private synchronizedOutput;
36
37
  private windowOpHandler?;
37
38
  private getWindowMetrics?;
38
39
  private clipboardWrite?;
@@ -45,6 +46,7 @@ export declare class OutputFilter {
45
46
  isAltScreen(): boolean;
46
47
  isBracketedPaste(): boolean;
47
48
  isFocusReporting(): boolean;
49
+ isSynchronizedOutput(): boolean;
48
50
  private replyOscColor;
49
51
  private handleOsc;
50
52
  private handleModeSeq;
@@ -152,6 +152,7 @@ export type InputHandler = {
152
152
  isBracketedPaste: () => boolean;
153
153
  isFocusReporting: () => boolean;
154
154
  isAltScreen: () => boolean;
155
+ isSynchronizedOutput: () => boolean;
155
156
  /**
156
157
  * Encode pointer input as terminal mouse events (SGR).
157
158
  */
package/dist/internal.js CHANGED
@@ -8300,6 +8300,7 @@ class OutputFilter {
8300
8300
  altScreen = false;
8301
8301
  bracketedPaste = false;
8302
8302
  focusReporting = false;
8303
+ synchronizedOutput = false;
8303
8304
  windowOpHandler;
8304
8305
  getWindowMetrics;
8305
8306
  clipboardWrite;
@@ -8333,6 +8334,9 @@ class OutputFilter {
8333
8334
  isFocusReporting() {
8334
8335
  return this.focusReporting;
8335
8336
  }
8337
+ isSynchronizedOutput() {
8338
+ return this.synchronizedOutput;
8339
+ }
8336
8340
  replyOscColor(code, rgb) {
8337
8341
  const toHex4 = (value) => Math.round(Math.max(0, Math.min(255, value)) * 257).toString(16).padStart(4, "0");
8338
8342
  const r = toHex4(rgb[0]);
@@ -8398,6 +8402,8 @@ class OutputFilter {
8398
8402
  } else if (code === 1004) {
8399
8403
  this.focusReporting = enabled;
8400
8404
  handled = true;
8405
+ } else if (code === 2026) {
8406
+ this.synchronizedOutput = enabled;
8401
8407
  }
8402
8408
  }
8403
8409
  return handled;
@@ -8581,6 +8587,7 @@ function createInputHandler(options = {}) {
8581
8587
  isBracketedPaste: () => filter.isBracketedPaste(),
8582
8588
  isFocusReporting: () => filter.isFocusReporting(),
8583
8589
  isAltScreen: () => filter.isAltScreen(),
8590
+ isSynchronizedOutput: () => filter.isSynchronizedOutput(),
8584
8591
  sendMouseEvent: (kind, event) => mouse.sendMouseEvent(kind, event)
8585
8592
  };
8586
8593
  }
@@ -50316,6 +50323,17 @@ function createRestty(options) {
50316
50323
  }
50317
50324
 
50318
50325
  // src/app/index.ts
50326
+ function normalizeTouchSelectionMode(value) {
50327
+ if (value === "drag" || value === "long-press" || value === "off")
50328
+ return value;
50329
+ return "long-press";
50330
+ }
50331
+ function clampFiniteNumber(value, fallback, min, max, round = false) {
50332
+ if (!Number.isFinite(value))
50333
+ return fallback;
50334
+ const numeric = round ? Math.round(value) : Number(value);
50335
+ return Math.min(max, Math.max(min, numeric));
50336
+ }
50319
50337
  function createResttyApp(options) {
50320
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50321
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50338,6 +50356,9 @@ function createResttyApp(options) {
50338
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50339
50357
  const autoResize = options.autoResize ?? true;
50340
50358
  const debugExpose = options.debugExpose ?? false;
50359
+ const touchSelectionMode = normalizeTouchSelectionMode(options.touchSelectionMode);
50360
+ const touchSelectionLongPressMs = clampFiniteNumber(options.touchSelectionLongPressMs, 450, 120, 2000, true);
50361
+ const touchSelectionMoveThresholdPx = clampFiniteNumber(options.touchSelectionMoveThresholdPx, 10, 1, 64);
50341
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50342
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50343
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50443,6 +50464,14 @@ function createResttyApp(options) {
50443
50464
  let lastKeydownSeqAt = 0;
50444
50465
  let nextBlinkTime = performance.now() + CURSOR_BLINK_MS;
50445
50466
  const ptyTransport = options.ptyTransport ?? createWebSocketPtyTransport();
50467
+ const PTY_OUTPUT_IDLE_MS = 10;
50468
+ const PTY_OUTPUT_MAX_MS = 40;
50469
+ const SYNC_OUTPUT_RESET_MS = 1000;
50470
+ const SYNC_OUTPUT_RESET_SEQ = "\x1B[?2026l";
50471
+ let ptyOutputBuffer = "";
50472
+ let ptyOutputIdleTimer = 0;
50473
+ let ptyOutputMaxTimer = 0;
50474
+ let syncOutputResetTimer = 0;
50446
50475
  let lastCursorForCpr = { row: 1, col: 1 };
50447
50476
  let inputHandler = null;
50448
50477
  let activeTheme = null;
@@ -50531,6 +50560,14 @@ function createResttyApp(options) {
50531
50560
  anchor: null,
50532
50561
  focus: null
50533
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50534
50571
  const linkState = {
50535
50572
  hoverId: 0,
50536
50573
  hoverUri: ""
@@ -50552,6 +50589,27 @@ function createResttyApp(options) {
50552
50589
  }
50553
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50554
50591
  }
50592
+ function isTouchPointer(event) {
50593
+ return event.pointerType === "touch";
50594
+ }
50595
+ function clearPendingTouchSelection() {
50596
+ if (touchSelectionState.pendingTimer) {
50597
+ clearTimeout(touchSelectionState.pendingTimer);
50598
+ touchSelectionState.pendingTimer = 0;
50599
+ }
50600
+ touchSelectionState.pendingPointerId = null;
50601
+ touchSelectionState.pendingCell = null;
50602
+ }
50603
+ function beginSelectionDrag(cell, pointerId) {
50604
+ selectionState.active = true;
50605
+ selectionState.dragging = true;
50606
+ selectionState.anchor = cell;
50607
+ selectionState.focus = cell;
50608
+ touchSelectionState.activePointerId = pointerId;
50609
+ canvas.setPointerCapture?.(pointerId);
50610
+ updateCanvasCursor();
50611
+ needsRender = true;
50612
+ }
50555
50613
  function noteScrollActivity() {
50556
50614
  scrollbarState.lastInputAt = performance.now();
50557
50615
  }
@@ -51152,6 +51210,7 @@ function createResttyApp(options) {
51152
51210
  selectionState.dragging = false;
51153
51211
  selectionState.anchor = null;
51154
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51155
51214
  updateCanvasCursor();
51156
51215
  needsRender = true;
51157
51216
  }
@@ -51202,7 +51261,71 @@ function createResttyApp(options) {
51202
51261
  const label = status.active ? `${status.mode} (${status.detail})` : status.mode;
51203
51262
  setMouseStatus(label);
51204
51263
  }
51264
+ function cancelPtyOutputFlush() {
51265
+ if (ptyOutputIdleTimer) {
51266
+ clearTimeout(ptyOutputIdleTimer);
51267
+ ptyOutputIdleTimer = 0;
51268
+ }
51269
+ if (ptyOutputMaxTimer) {
51270
+ clearTimeout(ptyOutputMaxTimer);
51271
+ ptyOutputMaxTimer = 0;
51272
+ }
51273
+ }
51274
+ function cancelSyncOutputReset() {
51275
+ if (syncOutputResetTimer) {
51276
+ clearTimeout(syncOutputResetTimer);
51277
+ syncOutputResetTimer = 0;
51278
+ }
51279
+ }
51280
+ function scheduleSyncOutputReset() {
51281
+ if (syncOutputResetTimer)
51282
+ return;
51283
+ syncOutputResetTimer = setTimeout(() => {
51284
+ syncOutputResetTimer = 0;
51285
+ if (!inputHandler?.isSynchronizedOutput?.())
51286
+ return;
51287
+ const sanitized = inputHandler.filterOutput(SYNC_OUTPUT_RESET_SEQ) || SYNC_OUTPUT_RESET_SEQ;
51288
+ sendInput(sanitized, "pty");
51289
+ }, SYNC_OUTPUT_RESET_MS);
51290
+ }
51291
+ function flushPtyOutputBuffer() {
51292
+ const output = ptyOutputBuffer;
51293
+ ptyOutputBuffer = "";
51294
+ if (!output)
51295
+ return;
51296
+ sendInput(output, "pty");
51297
+ }
51298
+ function queuePtyOutput(text) {
51299
+ if (!text)
51300
+ return;
51301
+ ptyOutputBuffer += text;
51302
+ if (ptyOutputIdleTimer) {
51303
+ clearTimeout(ptyOutputIdleTimer);
51304
+ }
51305
+ ptyOutputIdleTimer = setTimeout(() => {
51306
+ ptyOutputIdleTimer = 0;
51307
+ if (ptyOutputMaxTimer) {
51308
+ clearTimeout(ptyOutputMaxTimer);
51309
+ ptyOutputMaxTimer = 0;
51310
+ }
51311
+ flushPtyOutputBuffer();
51312
+ }, PTY_OUTPUT_IDLE_MS);
51313
+ if (!ptyOutputMaxTimer) {
51314
+ ptyOutputMaxTimer = setTimeout(() => {
51315
+ ptyOutputMaxTimer = 0;
51316
+ if (ptyOutputIdleTimer) {
51317
+ clearTimeout(ptyOutputIdleTimer);
51318
+ ptyOutputIdleTimer = 0;
51319
+ }
51320
+ flushPtyOutputBuffer();
51321
+ }, PTY_OUTPUT_MAX_MS);
51322
+ }
51323
+ }
51205
51324
  function disconnectPty2() {
51325
+ flushPtyOutputBuffer();
51326
+ cancelPtyOutputFlush();
51327
+ cancelSyncOutputReset();
51328
+ ptyOutputBuffer = "";
51206
51329
  ptyTransport.disconnect();
51207
51330
  updateMouseStatus();
51208
51331
  setPtyStatus("disconnected");
@@ -51249,7 +51372,7 @@ function createResttyApp(options) {
51249
51372
  const sanitized = inputHandler ? inputHandler.filterOutput(text) : text;
51250
51373
  updateMouseStatus();
51251
51374
  if (sanitized)
51252
- sendInput(sanitized, "pty");
51375
+ queuePtyOutput(sanitized);
51253
51376
  }
51254
51377
  }
51255
51378
  });
@@ -51301,30 +51424,71 @@ function createResttyApp(options) {
51301
51424
  function bindCanvasEvents() {
51302
51425
  if (!attachCanvasEvents)
51303
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51304
51428
  const onPointerDown = (event) => {
51305
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51306
51430
  event.preventDefault();
51307
51431
  canvas.setPointerCapture?.(event.pointerId);
51308
51432
  return;
51309
51433
  }
51434
+ if (isTouchPointer(event)) {
51435
+ if (event.button !== 0)
51436
+ return;
51437
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51438
+ touchSelectionState.activePointerId = null;
51439
+ if (touchSelectionMode === "off")
51440
+ return;
51441
+ if (touchSelectionMode === "drag") {
51442
+ event.preventDefault();
51443
+ beginSelectionDrag(cell2, event.pointerId);
51444
+ return;
51445
+ }
51446
+ clearPendingTouchSelection();
51447
+ touchSelectionState.pendingPointerId = event.pointerId;
51448
+ touchSelectionState.pendingCell = cell2;
51449
+ touchSelectionState.pendingStartX = event.clientX;
51450
+ touchSelectionState.pendingStartY = event.clientY;
51451
+ touchSelectionState.pendingTimer = setTimeout(() => {
51452
+ if (touchSelectionState.pendingPointerId !== event.pointerId || !touchSelectionState.pendingCell) {
51453
+ return;
51454
+ }
51455
+ const pendingCell = touchSelectionState.pendingCell;
51456
+ clearPendingTouchSelection();
51457
+ beginSelectionDrag(pendingCell, event.pointerId);
51458
+ }, touchSelectionLongPressMs);
51459
+ return;
51460
+ }
51310
51461
  if (event.button !== 0)
51311
51462
  return;
51312
51463
  event.preventDefault();
51313
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51314
51465
  updateLinkHover(cell);
51315
- selectionState.active = true;
51316
- selectionState.dragging = true;
51317
- selectionState.anchor = cell;
51318
- selectionState.focus = cell;
51319
- canvas.setPointerCapture?.(event.pointerId);
51320
- updateCanvasCursor();
51321
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51322
51467
  };
51323
51468
  const onPointerMove = (event) => {
51324
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51325
51470
  event.preventDefault();
51326
51471
  return;
51327
51472
  }
51473
+ if (isTouchPointer(event)) {
51474
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51475
+ const dx = event.clientX - touchSelectionState.pendingStartX;
51476
+ const dy = event.clientY - touchSelectionState.pendingStartY;
51477
+ if (dx * dx + dy * dy >= touchSelectionMoveThresholdPx * touchSelectionMoveThresholdPx) {
51478
+ clearPendingTouchSelection();
51479
+ }
51480
+ return;
51481
+ }
51482
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51483
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51484
+ event.preventDefault();
51485
+ selectionState.focus = cell2;
51486
+ updateLinkHover(null);
51487
+ updateCanvasCursor();
51488
+ needsRender = true;
51489
+ }
51490
+ return;
51491
+ }
51328
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51329
51493
  if (!selectionState.dragging) {
51330
51494
  updateLinkHover(cell);
@@ -51341,6 +51505,27 @@ function createResttyApp(options) {
51341
51505
  event.preventDefault();
51342
51506
  return;
51343
51507
  }
51508
+ if (isTouchPointer(event)) {
51509
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51510
+ clearPendingTouchSelection();
51511
+ touchSelectionState.activePointerId = null;
51512
+ return;
51513
+ }
51514
+ if (selectionState.dragging && touchSelectionState.activePointerId === event.pointerId) {
51515
+ const cell2 = normalizeSelectionCell(positionToCell(event));
51516
+ event.preventDefault();
51517
+ selectionState.dragging = false;
51518
+ selectionState.focus = cell2;
51519
+ touchSelectionState.activePointerId = null;
51520
+ if (selectionState.anchor && selectionState.focus && selectionState.anchor.row === selectionState.focus.row && selectionState.anchor.col === selectionState.focus.col) {
51521
+ clearSelection();
51522
+ } else {
51523
+ updateCanvasCursor();
51524
+ needsRender = true;
51525
+ }
51526
+ }
51527
+ return;
51528
+ }
51344
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51345
51530
  if (selectionState.dragging) {
51346
51531
  event.preventDefault();
@@ -51359,6 +51544,21 @@ function createResttyApp(options) {
51359
51544
  openLink(linkState.hoverUri);
51360
51545
  }
51361
51546
  };
51547
+ const onPointerCancel = (event) => {
51548
+ if (isTouchPointer(event)) {
51549
+ if (touchSelectionState.pendingPointerId === event.pointerId) {
51550
+ clearPendingTouchSelection();
51551
+ }
51552
+ if (touchSelectionState.activePointerId === event.pointerId) {
51553
+ touchSelectionState.activePointerId = null;
51554
+ if (selectionState.dragging) {
51555
+ selectionState.dragging = false;
51556
+ updateCanvasCursor();
51557
+ needsRender = true;
51558
+ }
51559
+ }
51560
+ }
51561
+ };
51362
51562
  const onWheel = (event) => {
51363
51563
  const mouseActive = inputHandler.isMouseActive();
51364
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51401,6 +51601,7 @@ function createResttyApp(options) {
51401
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51402
51602
  canvas.addEventListener("pointermove", onPointerMove);
51403
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51404
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51405
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51406
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51408,9 +51609,11 @@ function createResttyApp(options) {
51408
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51409
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51410
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51411
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51412
51614
  canvas.removeEventListener("wheel", onWheel);
51413
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51414
51617
  });
51415
51618
  if (imeInput) {
51416
51619
  let suppressNextInput = false;
@@ -55497,7 +55700,7 @@ function createResttyApp(options) {
55497
55700
  return;
55498
55701
  if (!text)
55499
55702
  return;
55500
- const normalized = normalizeNewlines(text);
55703
+ const normalized = source === "pty" ? text : normalizeNewlines(text);
55501
55704
  if (source === "key") {
55502
55705
  const bytes = textEncoder3.encode(normalized);
55503
55706
  const hex = Array.from(bytes, (b3) => b3.toString(16).padStart(2, "0")).join(" ");
@@ -55519,8 +55722,13 @@ function createResttyApp(options) {
55519
55722
  clearSelection();
55520
55723
  }
55521
55724
  writeToWasm(wasmHandle, normalized);
55522
- wasm.renderUpdate(wasmHandle);
55523
55725
  flushWasmOutputToPty();
55726
+ if (source === "pty" && inputHandler?.isSynchronizedOutput?.()) {
55727
+ scheduleSyncOutputReset();
55728
+ return;
55729
+ }
55730
+ cancelSyncOutputReset();
55731
+ wasm.renderUpdate(wasmHandle);
55524
55732
  if (source === "key" && wasmExports?.restty_debug_cursor_x && wasmExports?.restty_debug_cursor_y) {
55525
55733
  const ax = wasmExports.restty_debug_cursor_x(wasmHandle);
55526
55734
  const ay = wasmExports.restty_debug_cursor_y(wasmHandle);
@@ -55749,6 +55957,7 @@ function createResttyApp(options) {
55749
55957
  clearTimeout(terminalResizeTimer);
55750
55958
  terminalResizeTimer = 0;
55751
55959
  }
55960
+ cancelSyncOutputReset();
55752
55961
  pendingTerminalResize = null;
55753
55962
  disconnectPty2();
55754
55963
  ptyTransport.destroy?.();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restty",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Browser terminal rendering library powered by WASM, WebGPU/WebGL2, and TypeScript text shaping.",
5
5
  "keywords": [
6
6
  "terminal",