restty 0.1.14 → 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
@@ -50323,6 +50323,17 @@ function createRestty(options) {
50323
50323
  }
50324
50324
 
50325
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
+ }
50326
50337
  function createResttyApp(options) {
50327
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50328
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50345,6 +50356,9 @@ function createResttyApp(options) {
50345
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50346
50357
  const autoResize = options.autoResize ?? true;
50347
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);
50348
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50349
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50350
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50546,6 +50560,14 @@ function createResttyApp(options) {
50546
50560
  anchor: null,
50547
50561
  focus: null
50548
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50549
50571
  const linkState = {
50550
50572
  hoverId: 0,
50551
50573
  hoverUri: ""
@@ -50567,6 +50589,27 @@ function createResttyApp(options) {
50567
50589
  }
50568
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50569
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
+ }
50570
50613
  function noteScrollActivity() {
50571
50614
  scrollbarState.lastInputAt = performance.now();
50572
50615
  }
@@ -51167,6 +51210,7 @@ function createResttyApp(options) {
51167
51210
  selectionState.dragging = false;
51168
51211
  selectionState.anchor = null;
51169
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51170
51214
  updateCanvasCursor();
51171
51215
  needsRender = true;
51172
51216
  }
@@ -51380,30 +51424,71 @@ function createResttyApp(options) {
51380
51424
  function bindCanvasEvents() {
51381
51425
  if (!attachCanvasEvents)
51382
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51383
51428
  const onPointerDown = (event) => {
51384
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51385
51430
  event.preventDefault();
51386
51431
  canvas.setPointerCapture?.(event.pointerId);
51387
51432
  return;
51388
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
+ }
51389
51461
  if (event.button !== 0)
51390
51462
  return;
51391
51463
  event.preventDefault();
51392
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51393
51465
  updateLinkHover(cell);
51394
- selectionState.active = true;
51395
- selectionState.dragging = true;
51396
- selectionState.anchor = cell;
51397
- selectionState.focus = cell;
51398
- canvas.setPointerCapture?.(event.pointerId);
51399
- updateCanvasCursor();
51400
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51401
51467
  };
51402
51468
  const onPointerMove = (event) => {
51403
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51404
51470
  event.preventDefault();
51405
51471
  return;
51406
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
+ }
51407
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51408
51493
  if (!selectionState.dragging) {
51409
51494
  updateLinkHover(cell);
@@ -51420,6 +51505,27 @@ function createResttyApp(options) {
51420
51505
  event.preventDefault();
51421
51506
  return;
51422
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
+ }
51423
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51424
51530
  if (selectionState.dragging) {
51425
51531
  event.preventDefault();
@@ -51438,6 +51544,21 @@ function createResttyApp(options) {
51438
51544
  openLink(linkState.hoverUri);
51439
51545
  }
51440
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
+ };
51441
51562
  const onWheel = (event) => {
51442
51563
  const mouseActive = inputHandler.isMouseActive();
51443
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51480,6 +51601,7 @@ function createResttyApp(options) {
51480
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51481
51602
  canvas.addEventListener("pointermove", onPointerMove);
51482
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51483
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51484
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51485
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51487,9 +51609,11 @@ function createResttyApp(options) {
51487
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51488
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51489
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51490
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51491
51614
  canvas.removeEventListener("wheel", onWheel);
51492
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51493
51617
  });
51494
51618
  if (imeInput) {
51495
51619
  let suppressNextInput = false;
@@ -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
@@ -50323,6 +50323,17 @@ function createRestty(options) {
50323
50323
  }
50324
50324
 
50325
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
+ }
50326
50337
  function createResttyApp(options) {
50327
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50328
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50345,6 +50356,9 @@ function createResttyApp(options) {
50345
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50346
50357
  const autoResize = options.autoResize ?? true;
50347
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);
50348
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50349
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50350
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50546,6 +50560,14 @@ function createResttyApp(options) {
50546
50560
  anchor: null,
50547
50561
  focus: null
50548
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50549
50571
  const linkState = {
50550
50572
  hoverId: 0,
50551
50573
  hoverUri: ""
@@ -50567,6 +50589,27 @@ function createResttyApp(options) {
50567
50589
  }
50568
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50569
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
+ }
50570
50613
  function noteScrollActivity() {
50571
50614
  scrollbarState.lastInputAt = performance.now();
50572
50615
  }
@@ -51167,6 +51210,7 @@ function createResttyApp(options) {
51167
51210
  selectionState.dragging = false;
51168
51211
  selectionState.anchor = null;
51169
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51170
51214
  updateCanvasCursor();
51171
51215
  needsRender = true;
51172
51216
  }
@@ -51380,30 +51424,71 @@ function createResttyApp(options) {
51380
51424
  function bindCanvasEvents() {
51381
51425
  if (!attachCanvasEvents)
51382
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51383
51428
  const onPointerDown = (event) => {
51384
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51385
51430
  event.preventDefault();
51386
51431
  canvas.setPointerCapture?.(event.pointerId);
51387
51432
  return;
51388
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
+ }
51389
51461
  if (event.button !== 0)
51390
51462
  return;
51391
51463
  event.preventDefault();
51392
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51393
51465
  updateLinkHover(cell);
51394
- selectionState.active = true;
51395
- selectionState.dragging = true;
51396
- selectionState.anchor = cell;
51397
- selectionState.focus = cell;
51398
- canvas.setPointerCapture?.(event.pointerId);
51399
- updateCanvasCursor();
51400
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51401
51467
  };
51402
51468
  const onPointerMove = (event) => {
51403
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51404
51470
  event.preventDefault();
51405
51471
  return;
51406
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
+ }
51407
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51408
51493
  if (!selectionState.dragging) {
51409
51494
  updateLinkHover(cell);
@@ -51420,6 +51505,27 @@ function createResttyApp(options) {
51420
51505
  event.preventDefault();
51421
51506
  return;
51422
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
+ }
51423
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51424
51530
  if (selectionState.dragging) {
51425
51531
  event.preventDefault();
@@ -51438,6 +51544,21 @@ function createResttyApp(options) {
51438
51544
  openLink(linkState.hoverUri);
51439
51545
  }
51440
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
+ };
51441
51562
  const onWheel = (event) => {
51442
51563
  const mouseActive = inputHandler.isMouseActive();
51443
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51480,6 +51601,7 @@ function createResttyApp(options) {
51480
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51481
51602
  canvas.addEventListener("pointermove", onPointerMove);
51482
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51483
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51484
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51485
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51487,9 +51609,11 @@ function createResttyApp(options) {
51487
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51488
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51489
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51490
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51491
51614
  canvas.removeEventListener("wheel", onWheel);
51492
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51493
51617
  });
51494
51618
  if (imeInput) {
51495
51619
  let suppressNextInput = false;
package/dist/internal.js CHANGED
@@ -50323,6 +50323,17 @@ function createRestty(options) {
50323
50323
  }
50324
50324
 
50325
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
+ }
50326
50337
  function createResttyApp(options) {
50327
50338
  const { canvas: canvasInput, imeInput: imeInputInput, elements, callbacks } = options;
50328
50339
  const session = options.session ?? getDefaultResttyAppSession();
@@ -50345,6 +50356,9 @@ function createResttyApp(options) {
50345
50356
  const attachCanvasEvents = options.attachCanvasEvents ?? true;
50346
50357
  const autoResize = options.autoResize ?? true;
50347
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);
50348
50362
  const nerdIconScale = Number.isFinite(options.nerdIconScale) ? Number(options.nerdIconScale) : 1;
50349
50363
  const alphaBlending = options.alphaBlending ?? "linear-corrected";
50350
50364
  const srgbChannelToLinear = (c3) => c3 <= 0.04045 ? c3 / 12.92 : Math.pow((c3 + 0.055) / 1.055, 2.4);
@@ -50546,6 +50560,14 @@ function createResttyApp(options) {
50546
50560
  anchor: null,
50547
50561
  focus: null
50548
50562
  };
50563
+ const touchSelectionState = {
50564
+ pendingPointerId: null,
50565
+ activePointerId: null,
50566
+ pendingCell: null,
50567
+ pendingStartX: 0,
50568
+ pendingStartY: 0,
50569
+ pendingTimer: 0
50570
+ };
50549
50571
  const linkState = {
50550
50572
  hoverId: 0,
50551
50573
  hoverUri: ""
@@ -50567,6 +50589,27 @@ function createResttyApp(options) {
50567
50589
  }
50568
50590
  canvas.style.cursor = linkState.hoverId ? "pointer" : "default";
50569
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
+ }
50570
50613
  function noteScrollActivity() {
50571
50614
  scrollbarState.lastInputAt = performance.now();
50572
50615
  }
@@ -51167,6 +51210,7 @@ function createResttyApp(options) {
51167
51210
  selectionState.dragging = false;
51168
51211
  selectionState.anchor = null;
51169
51212
  selectionState.focus = null;
51213
+ touchSelectionState.activePointerId = null;
51170
51214
  updateCanvasCursor();
51171
51215
  needsRender = true;
51172
51216
  }
@@ -51380,30 +51424,71 @@ function createResttyApp(options) {
51380
51424
  function bindCanvasEvents() {
51381
51425
  if (!attachCanvasEvents)
51382
51426
  return;
51427
+ canvas.style.touchAction = touchSelectionMode === "drag" ? "none" : "pan-y pinch-zoom";
51383
51428
  const onPointerDown = (event) => {
51384
51429
  if (inputHandler.sendMouseEvent("down", event)) {
51385
51430
  event.preventDefault();
51386
51431
  canvas.setPointerCapture?.(event.pointerId);
51387
51432
  return;
51388
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
+ }
51389
51461
  if (event.button !== 0)
51390
51462
  return;
51391
51463
  event.preventDefault();
51392
51464
  const cell = normalizeSelectionCell(positionToCell(event));
51393
51465
  updateLinkHover(cell);
51394
- selectionState.active = true;
51395
- selectionState.dragging = true;
51396
- selectionState.anchor = cell;
51397
- selectionState.focus = cell;
51398
- canvas.setPointerCapture?.(event.pointerId);
51399
- updateCanvasCursor();
51400
- needsRender = true;
51466
+ beginSelectionDrag(cell, event.pointerId);
51401
51467
  };
51402
51468
  const onPointerMove = (event) => {
51403
51469
  if (inputHandler.sendMouseEvent("move", event)) {
51404
51470
  event.preventDefault();
51405
51471
  return;
51406
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
+ }
51407
51492
  const cell = normalizeSelectionCell(positionToCell(event));
51408
51493
  if (!selectionState.dragging) {
51409
51494
  updateLinkHover(cell);
@@ -51420,6 +51505,27 @@ function createResttyApp(options) {
51420
51505
  event.preventDefault();
51421
51506
  return;
51422
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
+ }
51423
51529
  const cell = normalizeSelectionCell(positionToCell(event));
51424
51530
  if (selectionState.dragging) {
51425
51531
  event.preventDefault();
@@ -51438,6 +51544,21 @@ function createResttyApp(options) {
51438
51544
  openLink(linkState.hoverUri);
51439
51545
  }
51440
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
+ };
51441
51562
  const onWheel = (event) => {
51442
51563
  const mouseActive = inputHandler.isMouseActive();
51443
51564
  const altScreen = inputHandler.isAltScreen ? inputHandler.isAltScreen() : false;
@@ -51480,6 +51601,7 @@ function createResttyApp(options) {
51480
51601
  canvas.addEventListener("pointerdown", onPointerDown);
51481
51602
  canvas.addEventListener("pointermove", onPointerMove);
51482
51603
  canvas.addEventListener("pointerup", onPointerUp);
51604
+ canvas.addEventListener("pointercancel", onPointerCancel);
51483
51605
  canvas.addEventListener("pointerleave", onPointerLeave);
51484
51606
  canvas.addEventListener("wheel", onWheel, { passive: false });
51485
51607
  canvas.addEventListener("contextmenu", onContextMenu);
@@ -51487,9 +51609,11 @@ function createResttyApp(options) {
51487
51609
  canvas.removeEventListener("pointerdown", onPointerDown);
51488
51610
  canvas.removeEventListener("pointermove", onPointerMove);
51489
51611
  canvas.removeEventListener("pointerup", onPointerUp);
51612
+ canvas.removeEventListener("pointercancel", onPointerCancel);
51490
51613
  canvas.removeEventListener("pointerleave", onPointerLeave);
51491
51614
  canvas.removeEventListener("wheel", onWheel);
51492
51615
  canvas.removeEventListener("contextmenu", onContextMenu);
51616
+ clearPendingTouchSelection();
51493
51617
  });
51494
51618
  if (imeInput) {
51495
51619
  let suppressNextInput = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "restty",
3
- "version": "0.1.14",
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",