tmux-ide 2.9.0-beta.33 → 2.9.0-beta.35
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/bin/cli.js +17 -1
- package/package.json +1 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +26 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-root-renderer.js +5 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-sidebar-shortcuts.js +31 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +19 -3
- package/packages/daemon/dist/tui/mirror/runtime/host-shift-capture.js +32 -0
- package/packages/daemon/dist/tui/mirror/runtime/terminal-links.js +3 -2
- package/packages/daemon/dist/tui/mirror/ui/key-hint.jsx +12 -5
- package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +1 -0
- package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +3 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +26 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-renderer.ts +5 -0
- package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +9 -10
- package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +6 -2
- package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +15 -2
- package/packages/daemon/src/tui/mirror/runtime/application-sidebar-shortcuts.ts +51 -0
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +21 -3
- package/packages/daemon/src/tui/mirror/runtime/host-shift-capture.ts +32 -0
- package/packages/daemon/src/tui/mirror/runtime/terminal-links.ts +3 -2
- package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +1 -0
- package/packages/daemon/src/tui/mirror/ui/key-hint.tsx +16 -5
- package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +1 -0
- package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +3 -0
package/bin/cli.js
CHANGED
|
@@ -11279,7 +11279,7 @@ var require_package = __commonJS({
|
|
|
11279
11279
|
"package.json"(exports, module) {
|
|
11280
11280
|
module.exports = {
|
|
11281
11281
|
name: "tmux-ide",
|
|
11282
|
-
version: "2.9.0-beta.
|
|
11282
|
+
version: "2.9.0-beta.35",
|
|
11283
11283
|
description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
|
|
11284
11284
|
type: "module",
|
|
11285
11285
|
bin: {
|
|
@@ -48541,6 +48541,8 @@ var init_terminal_replica_interpreter = __esm({
|
|
|
48541
48541
|
#widgetGate = false;
|
|
48542
48542
|
#markerTail = "";
|
|
48543
48543
|
#pendingResize = null;
|
|
48544
|
+
// null: no unpublished writes; undefined: a write requires full projection.
|
|
48545
|
+
#pendingWriteDirty = null;
|
|
48544
48546
|
#pendingRaw = [];
|
|
48545
48547
|
#pendingRawBytes = 0;
|
|
48546
48548
|
#rawContinuityLost = false;
|
|
@@ -48792,6 +48794,12 @@ var init_terminal_replica_interpreter = __esm({
|
|
|
48792
48794
|
this.#observability.nowMicros(),
|
|
48793
48795
|
trace
|
|
48794
48796
|
);
|
|
48797
|
+
const dirty = this.#backend.dirtyRange();
|
|
48798
|
+
const pending = this.#pendingWriteDirty;
|
|
48799
|
+
this.#pendingWriteDirty = pending === null ? dirty : pending === void 0 || dirty === void 0 ? void 0 : {
|
|
48800
|
+
start: Math.min(pending.start, dirty.start),
|
|
48801
|
+
end: Math.max(pending.end, dirty.end)
|
|
48802
|
+
};
|
|
48795
48803
|
if (this.#backend.requiresNativeReseed()) {
|
|
48796
48804
|
this.#commit(false);
|
|
48797
48805
|
return;
|
|
@@ -48847,7 +48855,15 @@ var init_terminal_replica_interpreter = __esm({
|
|
|
48847
48855
|
return;
|
|
48848
48856
|
}
|
|
48849
48857
|
const reduceStarted = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
48858
|
+
const pending = this.#pendingWriteDirty;
|
|
48859
|
+
if (pending !== null) {
|
|
48860
|
+
dirty = pending === void 0 || dirty === void 0 ? void 0 : {
|
|
48861
|
+
start: Math.min(pending.start, dirty.start),
|
|
48862
|
+
end: Math.max(pending.end, dirty.end)
|
|
48863
|
+
};
|
|
48864
|
+
}
|
|
48850
48865
|
const projected = this.#project(forceSeed ? void 0 : dirty);
|
|
48866
|
+
this.#pendingWriteDirty = null;
|
|
48851
48867
|
const previous = this.#snapshot;
|
|
48852
48868
|
const dirtyRows = [];
|
|
48853
48869
|
for (let index = 0; index < projected.grid.length; index += 1) {
|
package/package.json
CHANGED
|
@@ -38,6 +38,8 @@ export class TerminalReplicaInterpreter {
|
|
|
38
38
|
#widgetGate = false;
|
|
39
39
|
#markerTail = "";
|
|
40
40
|
#pendingResize = null;
|
|
41
|
+
// null: no unpublished writes; undefined: a write requires full projection.
|
|
42
|
+
#pendingWriteDirty = null;
|
|
41
43
|
#pendingRaw = [];
|
|
42
44
|
#pendingRawBytes = 0;
|
|
43
45
|
#rawContinuityLost = false;
|
|
@@ -306,6 +308,19 @@ export class TerminalReplicaInterpreter {
|
|
|
306
308
|
await this.#writeToBackend(this.#backend, data);
|
|
307
309
|
if (this.#observability.enabled)
|
|
308
310
|
this.#observability.recordSpan("parse", "terminal-replica-write", parseStarted, this.#observability.nowMicros(), trace);
|
|
311
|
+
// The parser resets its dirty tracker per write. Synchronized output may
|
|
312
|
+
// span many writes before publication, so retain their union until commit.
|
|
313
|
+
const dirty = this.#backend.dirtyRange();
|
|
314
|
+
const pending = this.#pendingWriteDirty;
|
|
315
|
+
this.#pendingWriteDirty =
|
|
316
|
+
pending === null
|
|
317
|
+
? dirty
|
|
318
|
+
: pending === undefined || dirty === undefined
|
|
319
|
+
? undefined
|
|
320
|
+
: {
|
|
321
|
+
start: Math.min(pending.start, dirty.start),
|
|
322
|
+
end: Math.max(pending.end, dirty.end),
|
|
323
|
+
};
|
|
309
324
|
// DEC synchronized-output is atomic: no intermediate frame leaks.
|
|
310
325
|
if (this.#backend.requiresNativeReseed()) {
|
|
311
326
|
this.#commit(false);
|
|
@@ -369,7 +384,18 @@ export class TerminalReplicaInterpreter {
|
|
|
369
384
|
return;
|
|
370
385
|
}
|
|
371
386
|
const reduceStarted = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
387
|
+
const pending = this.#pendingWriteDirty;
|
|
388
|
+
if (pending !== null) {
|
|
389
|
+
dirty =
|
|
390
|
+
pending === undefined || dirty === undefined
|
|
391
|
+
? undefined
|
|
392
|
+
: {
|
|
393
|
+
start: Math.min(pending.start, dirty.start),
|
|
394
|
+
end: Math.max(pending.end, dirty.end),
|
|
395
|
+
};
|
|
396
|
+
}
|
|
372
397
|
const projected = this.#project(forceSeed ? undefined : dirty);
|
|
398
|
+
this.#pendingWriteDirty = null;
|
|
373
399
|
const previous = this.#snapshot;
|
|
374
400
|
const dirtyRows = [];
|
|
375
401
|
for (let index = 0; index < projected.grid.length; index += 1) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { acquireHostShiftCapture } from "./host-shift-capture.js";
|
|
1
2
|
import { CliRenderEvents, createCliRenderer } from "@opentui/core";
|
|
2
3
|
import { tuiPerfMark, tuiPerfStream } from "./application-performance-log.js";
|
|
3
4
|
import { createRendererOutputTransport } from "./renderer-output-transport.js";
|
|
@@ -30,7 +31,10 @@ export async function createApplicationRootRenderer(kittyKeys, onOutputError, on
|
|
|
30
31
|
let resize = () => { };
|
|
31
32
|
let removeCapabilityObserver;
|
|
32
33
|
const preserveCaptureDump = ["true", "1", "on", "yes"].includes((process.env.OTUI_DUMP_CAPTURES ?? "").toLowerCase());
|
|
34
|
+
let releaseShiftCapture;
|
|
33
35
|
const disposeOutput = () => {
|
|
36
|
+
releaseShiftCapture?.();
|
|
37
|
+
releaseShiftCapture = undefined;
|
|
34
38
|
removeCapabilityObserver?.();
|
|
35
39
|
removeCapabilityObserver = undefined;
|
|
36
40
|
clearTimeout(resizeTimer);
|
|
@@ -80,6 +84,7 @@ export async function createApplicationRootRenderer(kittyKeys, onOutputError, on
|
|
|
80
84
|
disposeOutput();
|
|
81
85
|
throw error;
|
|
82
86
|
}
|
|
87
|
+
releaseShiftCapture = acquireHostShiftCapture();
|
|
83
88
|
const activeRenderer = renderer;
|
|
84
89
|
if (tuiPerfStream) {
|
|
85
90
|
let previous;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function isSidebarToggleKey(key) {
|
|
2
|
+
return (key.name.toLowerCase() === "f10" &&
|
|
3
|
+
!key.shift &&
|
|
4
|
+
!key.ctrl &&
|
|
5
|
+
!key.meta &&
|
|
6
|
+
key.eventType !== "release");
|
|
7
|
+
}
|
|
8
|
+
export function createApplicationSidebarShortcuts(surface, visible, setVisible, machines, palette) {
|
|
9
|
+
return (event) => {
|
|
10
|
+
if (event.name.toLowerCase() === "f5") {
|
|
11
|
+
machines.sidebar.onBlur?.();
|
|
12
|
+
palette.setOpen(true, "keyboard");
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (isSidebarToggleKey(event)) {
|
|
16
|
+
event.preventDefault();
|
|
17
|
+
event.stopPropagation();
|
|
18
|
+
if (event.eventType === "press")
|
|
19
|
+
setVisible((value) => !value);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
if (event.ctrl && event.name.toLowerCase() === "g") {
|
|
23
|
+
if (surface() === "home" || !visible())
|
|
24
|
+
machines.showSwitcher(false);
|
|
25
|
+
else
|
|
26
|
+
machines.focus();
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -1032,6 +1032,16 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1032
1032
|
const head = gestureLeaseCurrent(active.lease) && cell
|
|
1033
1033
|
? terminalSelectionCell(snapshot, cell.col, cell.row, scrollback.offset(active.paneId), selectionViewport(active.paneId, active.frame))
|
|
1034
1034
|
: null;
|
|
1035
|
+
if (active.linkUrl &&
|
|
1036
|
+
head &&
|
|
1037
|
+
!active.moved &&
|
|
1038
|
+
head.row === active.anchor.row &&
|
|
1039
|
+
head.col === active.anchor.col) {
|
|
1040
|
+
const url = active.linkUrl;
|
|
1041
|
+
endSelectionView();
|
|
1042
|
+
props.onOpenLink?.(url);
|
|
1043
|
+
return;
|
|
1044
|
+
}
|
|
1035
1045
|
if (!snapshot ||
|
|
1036
1046
|
!head ||
|
|
1037
1047
|
(active.unit === "cell" &&
|
|
@@ -1096,15 +1106,20 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1096
1106
|
const lease = captureGestureLease(hit.frame.paneId, hit.frame);
|
|
1097
1107
|
if (!lease)
|
|
1098
1108
|
return;
|
|
1109
|
+
let shiftLink;
|
|
1099
1110
|
if (props.onOpenLink && isTerminalLinkClick(event)) {
|
|
1100
1111
|
const cell = terminalSelectionCell(snapshot, hit.col, hit.row, scrollback.offset(hit.frame.paneId), selectionViewport(hit.frame.paneId, hit.frame));
|
|
1101
1112
|
const url = cell && terminalLinkAt(snapshot, cell);
|
|
1102
1113
|
if (url) {
|
|
1103
1114
|
event.stopPropagation?.();
|
|
1104
|
-
linkPointer = true;
|
|
1105
1115
|
lastSelectionClick = null;
|
|
1106
|
-
|
|
1107
|
-
|
|
1116
|
+
if (event.modifiers?.shift)
|
|
1117
|
+
shiftLink = url;
|
|
1118
|
+
else {
|
|
1119
|
+
linkPointer = true;
|
|
1120
|
+
props.onOpenLink(url);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1108
1123
|
}
|
|
1109
1124
|
}
|
|
1110
1125
|
const appMouse = terminalMouseActionSupported(snapshot, "down");
|
|
@@ -1183,6 +1198,7 @@ export function ApplicationTerminalWorkspace(props) {
|
|
|
1183
1198
|
frame: hit.frame,
|
|
1184
1199
|
lease: selectionLease,
|
|
1185
1200
|
moved: false,
|
|
1201
|
+
linkUrl: shiftLink ? (terminalLinkAt(selectionSnapshot, anchor) ?? undefined) : undefined,
|
|
1186
1202
|
};
|
|
1187
1203
|
setPointerSelecting(true);
|
|
1188
1204
|
setCommittedSelection(null);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { closeSync, openSync, writeSync } from "node:fs";
|
|
2
|
+
/** XTSHIFTESCAPE is a host request, not pane input or a persistent user setting. */
|
|
3
|
+
export function acquireHostShiftCapture(environment = process.env, write = (sequence) => {
|
|
4
|
+
const fd = openSync("/dev/tty", "w");
|
|
5
|
+
try {
|
|
6
|
+
writeSync(fd, sequence);
|
|
7
|
+
}
|
|
8
|
+
finally {
|
|
9
|
+
closeSync(fd);
|
|
10
|
+
}
|
|
11
|
+
}) {
|
|
12
|
+
// A surrounding multiplexer owns the outer terminal protocol. Do not send
|
|
13
|
+
// unqualified passthrough sequences to an unknown terminal through it.
|
|
14
|
+
if (environment.TERM_PROGRAM !== "ghostty" || environment.TMUX)
|
|
15
|
+
return () => { };
|
|
16
|
+
let active = true;
|
|
17
|
+
const request = (capture) => {
|
|
18
|
+
try {
|
|
19
|
+
write(capture ? "\u001b[>1s" : "\u001b[>0s");
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// A missing controlling terminal must not prevent startup or cleanup.
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
request(true);
|
|
26
|
+
return () => {
|
|
27
|
+
if (!active)
|
|
28
|
+
return;
|
|
29
|
+
active = false;
|
|
30
|
+
request(false);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -4,8 +4,9 @@ const MAX_LINK_LINE = 8192;
|
|
|
4
4
|
export function isTerminalLinkClick(event) {
|
|
5
5
|
return (event.type === "down" &&
|
|
6
6
|
event.button === 0 &&
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
(event.modifiers?.shift
|
|
8
|
+
? !event.modifiers.ctrl && !event.modifiers.meta
|
|
9
|
+
: Boolean(event.modifiers?.ctrl || event.modifiers?.meta)) &&
|
|
9
10
|
!event.modifiers?.alt);
|
|
10
11
|
}
|
|
11
12
|
export function safeTerminalLink(value) {
|
|
@@ -5,9 +5,11 @@ import { useKeyboardRoute } from "./keyboard-router.jsx";
|
|
|
5
5
|
/** A one-row, cell-aligned keyboard affordance with optional pointer/keyboard activation. */
|
|
6
6
|
export function KeyHint(props) {
|
|
7
7
|
const palette = () => componentPalette(props.theme, props, "neutral");
|
|
8
|
-
const background = () => props.
|
|
9
|
-
? props.theme.roles.surfaces.
|
|
10
|
-
:
|
|
8
|
+
const background = () => props.button
|
|
9
|
+
? props.theme.roles.surfaces.panelRaised
|
|
10
|
+
: props.quiet && !props.focused && !props.hovered
|
|
11
|
+
? props.theme.roles.surfaces.panel
|
|
12
|
+
: palette().background;
|
|
11
13
|
const foreground = () => props.quiet && !props.focused && !props.hovered
|
|
12
14
|
? props.theme.roles.text.muted
|
|
13
15
|
: palette().foreground;
|
|
@@ -39,9 +41,14 @@ export function KeyHint(props) {
|
|
|
39
41
|
activate();
|
|
40
42
|
}}>
|
|
41
43
|
<text fg={foreground()} bg={background()}>
|
|
42
|
-
{props.focused || props.selected ? (<strong>{content()}</strong>) : props.quiet && !props.presentation ? (<>
|
|
44
|
+
{props.focused || props.selected ? (<strong>{content()}</strong>) : (props.quiet || props.button) && !props.presentation ? (<>
|
|
43
45
|
{" "}
|
|
44
|
-
<span style={{
|
|
46
|
+
<span style={{
|
|
47
|
+
fg: props.button
|
|
48
|
+
? props.theme.roles.selection.selectionText
|
|
49
|
+
: props.theme.roles.text.primary,
|
|
50
|
+
bg: props.button ? props.theme.roles.selection.selection : background(),
|
|
51
|
+
}}>
|
|
45
52
|
{clipTerminal(props.keys, Math.max(0, width() - 1))}
|
|
46
53
|
</span>
|
|
47
54
|
{clipTerminal(props.label ? ` ${props.label} ` : " ", Math.max(0, width() - terminalDisplayWidth(props.keys) - 1))}
|
|
@@ -6,8 +6,11 @@ export const APPLICATION_SHORTCUTS = [
|
|
|
6
6
|
return { category: "Application", label: description.label, keys: description.shortcut };
|
|
7
7
|
}),
|
|
8
8
|
{ category: "Application", label: "Commands", keys: "F5" },
|
|
9
|
+
{ category: "Application", label: "Show / hide sidebar", keys: "F10" },
|
|
9
10
|
{ category: "Application", label: "Agent attention", keys: "F7" },
|
|
10
11
|
{ category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
|
|
12
|
+
{ category: "Terminals", label: "Open link", keys: "Shift+click" },
|
|
13
|
+
{ category: "Terminals", label: "Select and copy text", keys: "Shift+drag" },
|
|
11
14
|
{ category: "Home", label: "Find an agent or workspace", keys: "/" },
|
|
12
15
|
{ category: "Home", label: "Cycle machine filter", keys: "f" },
|
|
13
16
|
{ category: "Home", label: "Toggle attention filter", keys: "a" },
|
|
@@ -161,6 +161,8 @@ export class TerminalReplicaInterpreter {
|
|
|
161
161
|
#widgetGate = false;
|
|
162
162
|
#markerTail = "";
|
|
163
163
|
#pendingResize: { cols: number; rows: number } | null = null;
|
|
164
|
+
// null: no unpublished writes; undefined: a write requires full projection.
|
|
165
|
+
#pendingWriteDirty: { start: number; end: number } | null | undefined = null;
|
|
164
166
|
#pendingRaw: Uint8Array[] = [];
|
|
165
167
|
#pendingRawBytes = 0;
|
|
166
168
|
#rawContinuityLost = false;
|
|
@@ -464,6 +466,19 @@ export class TerminalReplicaInterpreter {
|
|
|
464
466
|
this.#observability.nowMicros(),
|
|
465
467
|
trace,
|
|
466
468
|
);
|
|
469
|
+
// The parser resets its dirty tracker per write. Synchronized output may
|
|
470
|
+
// span many writes before publication, so retain their union until commit.
|
|
471
|
+
const dirty = this.#backend.dirtyRange();
|
|
472
|
+
const pending = this.#pendingWriteDirty;
|
|
473
|
+
this.#pendingWriteDirty =
|
|
474
|
+
pending === null
|
|
475
|
+
? dirty
|
|
476
|
+
: pending === undefined || dirty === undefined
|
|
477
|
+
? undefined
|
|
478
|
+
: {
|
|
479
|
+
start: Math.min(pending.start, dirty.start),
|
|
480
|
+
end: Math.max(pending.end, dirty.end),
|
|
481
|
+
};
|
|
467
482
|
// DEC synchronized-output is atomic: no intermediate frame leaks.
|
|
468
483
|
if (this.#backend.requiresNativeReseed()) {
|
|
469
484
|
this.#commit(false);
|
|
@@ -531,7 +546,18 @@ export class TerminalReplicaInterpreter {
|
|
|
531
546
|
return;
|
|
532
547
|
}
|
|
533
548
|
const reduceStarted = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
549
|
+
const pending = this.#pendingWriteDirty;
|
|
550
|
+
if (pending !== null) {
|
|
551
|
+
dirty =
|
|
552
|
+
pending === undefined || dirty === undefined
|
|
553
|
+
? undefined
|
|
554
|
+
: {
|
|
555
|
+
start: Math.min(pending.start, dirty.start),
|
|
556
|
+
end: Math.max(pending.end, dirty.end),
|
|
557
|
+
};
|
|
558
|
+
}
|
|
534
559
|
const projected = this.#project(forceSeed ? undefined : dirty);
|
|
560
|
+
this.#pendingWriteDirty = null;
|
|
535
561
|
const previous = this.#snapshot;
|
|
536
562
|
const dirtyRows: CanonicalTerminalReplicaPatch["patch"]["rows"] = [];
|
|
537
563
|
for (let index = 0; index < projected.grid.length; index += 1) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { acquireHostShiftCapture } from "./host-shift-capture.ts";
|
|
1
2
|
import { CliRenderEvents, createCliRenderer } from "@opentui/core";
|
|
2
3
|
|
|
3
4
|
import { tuiPerfMark, tuiPerfStream } from "./application-performance-log.ts";
|
|
@@ -37,7 +38,10 @@ export async function createApplicationRootRenderer(
|
|
|
37
38
|
const preserveCaptureDump = ["true", "1", "on", "yes"].includes(
|
|
38
39
|
(process.env.OTUI_DUMP_CAPTURES ?? "").toLowerCase(),
|
|
39
40
|
);
|
|
41
|
+
let releaseShiftCapture: (() => void) | undefined;
|
|
40
42
|
const disposeOutput = () => {
|
|
43
|
+
releaseShiftCapture?.();
|
|
44
|
+
releaseShiftCapture = undefined;
|
|
41
45
|
removeCapabilityObserver?.();
|
|
42
46
|
removeCapabilityObserver = undefined;
|
|
43
47
|
clearTimeout(resizeTimer);
|
|
@@ -84,6 +88,7 @@ export async function createApplicationRootRenderer(
|
|
|
84
88
|
disposeOutput();
|
|
85
89
|
throw error;
|
|
86
90
|
}
|
|
91
|
+
releaseShiftCapture = acquireHostShiftCapture();
|
|
87
92
|
const activeRenderer = renderer;
|
|
88
93
|
if (tuiPerfStream) {
|
|
89
94
|
let previous: string | undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createApplicationSidebarShortcuts } from "./application-sidebar-shortcuts.ts";
|
|
1
2
|
import type { TmuxServerScope } from "@tmux-ide/contracts";
|
|
2
3
|
import { terminalWindowActionCallbacks } from "./application-terminal-workspace-policy.ts";
|
|
3
4
|
import { applicationRouteConnection } from "./application-route-connection.ts";
|
|
@@ -425,6 +426,13 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
|
|
|
425
426
|
focusedPane();
|
|
426
427
|
terminalInputIngress.adopt();
|
|
427
428
|
});
|
|
429
|
+
const sidebarShortcut = createApplicationSidebarShortcuts(
|
|
430
|
+
activeSurface,
|
|
431
|
+
sidebarVisible,
|
|
432
|
+
setSidebarVisible,
|
|
433
|
+
machines,
|
|
434
|
+
paletteCommands,
|
|
435
|
+
);
|
|
428
436
|
useKeyboard((event) => {
|
|
429
437
|
noteHostInteraction();
|
|
430
438
|
const name = event.name.toLowerCase();
|
|
@@ -438,11 +446,6 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
|
|
|
438
446
|
}
|
|
439
447
|
if (name === "f6" || name === "f7") paletteCommands.setOpen(false, "keyboard");
|
|
440
448
|
if (handleFleetShortcut(event, machines)) return;
|
|
441
|
-
if (name === "f5") {
|
|
442
|
-
machines.sidebar.onBlur?.();
|
|
443
|
-
paletteCommands.setOpen(true, "keyboard");
|
|
444
|
-
return;
|
|
445
|
-
}
|
|
446
449
|
if (appearance.handlePickerKey(event)) return;
|
|
447
450
|
if (paneRename.handleKey(event)) return;
|
|
448
451
|
if (
|
|
@@ -453,11 +456,7 @@ export async function startApplicationRoot(options: StartApplicationRootOptions
|
|
|
453
456
|
)
|
|
454
457
|
return;
|
|
455
458
|
if (paletteCommands.handleKey(event)) return;
|
|
456
|
-
if (event
|
|
457
|
-
if (activeSurface() === "home" || !sidebarVisible()) machines.showSwitcher(false);
|
|
458
|
-
else machines.focus();
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
459
|
+
if (sidebarShortcut(event)) return;
|
|
461
460
|
if (machines.focused() && !["f1", "f2"].includes(name) && !(event.ctrl && name === "q")) {
|
|
462
461
|
componentKeyboardRoutes.route(event);
|
|
463
462
|
return;
|
|
@@ -12,6 +12,7 @@ import { For, Show, createMemo } from "solid-js";
|
|
|
12
12
|
import { shellChromeLayout, type ShellChromeView } from "../shell-chrome.ts";
|
|
13
13
|
import type { SemanticThemeSnapshot } from "../theme.ts";
|
|
14
14
|
import { clipTerminal, friendlySessionLabel } from "../terminal-text.ts";
|
|
15
|
+
import { KeyHint } from "../ui/key-hint.tsx";
|
|
15
16
|
import { Button } from "../ui/button.tsx";
|
|
16
17
|
import { NavigationRow } from "../ui/navigation-row.tsx";
|
|
17
18
|
import type { OverlayLayer } from "../ui/overlay-host.tsx";
|
|
@@ -244,9 +245,12 @@ function CatalogStatusStrip(props: {
|
|
|
244
245
|
/>
|
|
245
246
|
)}
|
|
246
247
|
</For>
|
|
247
|
-
<
|
|
248
|
+
<KeyHint
|
|
248
249
|
theme={props.theme}
|
|
249
|
-
|
|
250
|
+
keys="F5"
|
|
251
|
+
label="Commands"
|
|
252
|
+
quiet
|
|
253
|
+
button
|
|
250
254
|
width={15}
|
|
251
255
|
onPress={props.onOpenCommands}
|
|
252
256
|
/>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isSidebarToggleKey } from "./application-sidebar-shortcuts.ts";
|
|
1
2
|
import {
|
|
2
3
|
ApplicationMachineSidebar,
|
|
3
4
|
type ApplicationMachineSidebarModel,
|
|
@@ -48,13 +49,25 @@ export { applicationShellViewport } from "./application-shell-viewport.ts";
|
|
|
48
49
|
type TerminalWorkspaceProps = ComponentProps<typeof ApplicationTerminalWorkspace>;
|
|
49
50
|
export type RootSurface = "home" | "terminals";
|
|
50
51
|
type InputSource = "keyboard" | "mouse";
|
|
51
|
-
export type ApplicationShellKeyAction =
|
|
52
|
+
export type ApplicationShellKeyAction =
|
|
53
|
+
| "home"
|
|
54
|
+
| "terminals"
|
|
55
|
+
| "palette-open"
|
|
56
|
+
| "palette-close"
|
|
57
|
+
| "sidebar-toggle";
|
|
52
58
|
|
|
53
59
|
export function applicationShellKeyAction(
|
|
54
|
-
key: {
|
|
60
|
+
key: {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly shift?: boolean;
|
|
63
|
+
readonly ctrl?: boolean;
|
|
64
|
+
readonly meta?: boolean;
|
|
65
|
+
readonly eventType?: string;
|
|
66
|
+
},
|
|
55
67
|
paletteOpen: boolean,
|
|
56
68
|
): ApplicationShellKeyAction | null {
|
|
57
69
|
const name = key.name.toLowerCase();
|
|
70
|
+
if (isSidebarToggleKey(key)) return "sidebar-toggle";
|
|
58
71
|
if (name === "f1") return "home";
|
|
59
72
|
if (name === "f2") return "terminals";
|
|
60
73
|
if (name === "f5") return "palette-open";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
interface Key {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly ctrl?: boolean;
|
|
4
|
+
readonly shift?: boolean;
|
|
5
|
+
readonly meta?: boolean;
|
|
6
|
+
readonly eventType?: string;
|
|
7
|
+
preventDefault(): void;
|
|
8
|
+
stopPropagation(): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isSidebarToggleKey(key: Omit<Key, "preventDefault" | "stopPropagation">): boolean {
|
|
12
|
+
return (
|
|
13
|
+
key.name.toLowerCase() === "f10" &&
|
|
14
|
+
!key.shift &&
|
|
15
|
+
!key.ctrl &&
|
|
16
|
+
!key.meta &&
|
|
17
|
+
key.eventType !== "release"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createApplicationSidebarShortcuts(
|
|
22
|
+
surface: () => string,
|
|
23
|
+
visible: () => boolean,
|
|
24
|
+
setVisible: (update: (value: boolean) => boolean) => unknown,
|
|
25
|
+
machines: {
|
|
26
|
+
showSwitcher(attention: boolean): void;
|
|
27
|
+
focus(): void;
|
|
28
|
+
sidebar: { onBlur?: () => void };
|
|
29
|
+
},
|
|
30
|
+
palette: { setOpen(open: boolean, source: "keyboard"): void },
|
|
31
|
+
): (event: Key) => boolean {
|
|
32
|
+
return (event) => {
|
|
33
|
+
if (event.name.toLowerCase() === "f5") {
|
|
34
|
+
machines.sidebar.onBlur?.();
|
|
35
|
+
palette.setOpen(true, "keyboard");
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
if (isSidebarToggleKey(event)) {
|
|
39
|
+
event.preventDefault();
|
|
40
|
+
event.stopPropagation();
|
|
41
|
+
if (event.eventType === "press") setVisible((value) => !value);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
if (event.ctrl && event.name.toLowerCase() === "g") {
|
|
45
|
+
if (surface() === "home" || !visible()) machines.showSwitcher(false);
|
|
46
|
+
else machines.focus();
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -512,6 +512,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
512
512
|
}>;
|
|
513
513
|
pointer: Readonly<{ x: number; y: number }>;
|
|
514
514
|
moved: boolean;
|
|
515
|
+
readonly linkUrl?: string;
|
|
515
516
|
} | null = null;
|
|
516
517
|
let linkPointer = false;
|
|
517
518
|
let liveReturnPointer = false;
|
|
@@ -1457,6 +1458,18 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1457
1458
|
selectionViewport(active.paneId, active.frame),
|
|
1458
1459
|
)
|
|
1459
1460
|
: null;
|
|
1461
|
+
if (
|
|
1462
|
+
active.linkUrl &&
|
|
1463
|
+
head &&
|
|
1464
|
+
!active.moved &&
|
|
1465
|
+
head.row === active.anchor.row &&
|
|
1466
|
+
head.col === active.anchor.col
|
|
1467
|
+
) {
|
|
1468
|
+
const url = active.linkUrl;
|
|
1469
|
+
endSelectionView();
|
|
1470
|
+
props.onOpenLink?.(url);
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1460
1473
|
if (
|
|
1461
1474
|
!snapshot ||
|
|
1462
1475
|
!head ||
|
|
@@ -1522,6 +1535,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1522
1535
|
if (!snapshot) return;
|
|
1523
1536
|
const lease = captureGestureLease(hit.frame.paneId, hit.frame);
|
|
1524
1537
|
if (!lease) return;
|
|
1538
|
+
let shiftLink: string | undefined;
|
|
1525
1539
|
if (props.onOpenLink && isTerminalLinkClick(event)) {
|
|
1526
1540
|
const cell = terminalSelectionCell(
|
|
1527
1541
|
snapshot,
|
|
@@ -1533,10 +1547,13 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1533
1547
|
const url = cell && terminalLinkAt(snapshot, cell);
|
|
1534
1548
|
if (url) {
|
|
1535
1549
|
event.stopPropagation?.();
|
|
1536
|
-
linkPointer = true;
|
|
1537
1550
|
lastSelectionClick = null;
|
|
1538
|
-
|
|
1539
|
-
|
|
1551
|
+
if (event.modifiers?.shift) shiftLink = url;
|
|
1552
|
+
else {
|
|
1553
|
+
linkPointer = true;
|
|
1554
|
+
props.onOpenLink(url);
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1540
1557
|
}
|
|
1541
1558
|
}
|
|
1542
1559
|
const appMouse = terminalMouseActionSupported(snapshot, "down");
|
|
@@ -1625,6 +1642,7 @@ export function ApplicationTerminalWorkspace(props: ApplicationTerminalWorkspace
|
|
|
1625
1642
|
frame: hit.frame,
|
|
1626
1643
|
lease: selectionLease,
|
|
1627
1644
|
moved: false,
|
|
1645
|
+
linkUrl: shiftLink ? (terminalLinkAt(selectionSnapshot, anchor) ?? undefined) : undefined,
|
|
1628
1646
|
};
|
|
1629
1647
|
setPointerSelecting(true);
|
|
1630
1648
|
setCommittedSelection(null);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { closeSync, openSync, writeSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** XTSHIFTESCAPE is a host request, not pane input or a persistent user setting. */
|
|
4
|
+
export function acquireHostShiftCapture(
|
|
5
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
6
|
+
write: (sequence: string) => void = (sequence) => {
|
|
7
|
+
const fd = openSync("/dev/tty", "w");
|
|
8
|
+
try {
|
|
9
|
+
writeSync(fd, sequence);
|
|
10
|
+
} finally {
|
|
11
|
+
closeSync(fd);
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
): () => void {
|
|
15
|
+
// A surrounding multiplexer owns the outer terminal protocol. Do not send
|
|
16
|
+
// unqualified passthrough sequences to an unknown terminal through it.
|
|
17
|
+
if (environment.TERM_PROGRAM !== "ghostty" || environment.TMUX) return () => {};
|
|
18
|
+
let active = true;
|
|
19
|
+
const request = (capture: boolean) => {
|
|
20
|
+
try {
|
|
21
|
+
write(capture ? "\u001b[>1s" : "\u001b[>0s");
|
|
22
|
+
} catch {
|
|
23
|
+
// A missing controlling terminal must not prevent startup or cleanup.
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
request(true);
|
|
27
|
+
return () => {
|
|
28
|
+
if (!active) return;
|
|
29
|
+
active = false;
|
|
30
|
+
request(false);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -18,8 +18,9 @@ export function isTerminalLinkClick(event: {
|
|
|
18
18
|
return (
|
|
19
19
|
event.type === "down" &&
|
|
20
20
|
event.button === 0 &&
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
(event.modifiers?.shift
|
|
22
|
+
? !event.modifiers.ctrl && !event.modifiers.meta
|
|
23
|
+
: Boolean(event.modifiers?.ctrl || event.modifiers?.meta)) &&
|
|
23
24
|
!event.modifiers?.alt
|
|
24
25
|
);
|
|
25
26
|
}
|
|
@@ -14,15 +14,19 @@ export interface KeyHintProps extends ComponentInteractionState {
|
|
|
14
14
|
presentation?: string;
|
|
15
15
|
onPress?: () => void;
|
|
16
16
|
quiet?: boolean;
|
|
17
|
+
/** Distinct footer button with an emphasized shortcut. */
|
|
18
|
+
button?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/** A one-row, cell-aligned keyboard affordance with optional pointer/keyboard activation. */
|
|
20
22
|
export function KeyHint(props: KeyHintProps) {
|
|
21
23
|
const palette = () => componentPalette(props.theme, props, "neutral");
|
|
22
24
|
const background = () =>
|
|
23
|
-
props.
|
|
24
|
-
? props.theme.roles.surfaces.
|
|
25
|
-
:
|
|
25
|
+
props.button
|
|
26
|
+
? props.theme.roles.surfaces.panelRaised
|
|
27
|
+
: props.quiet && !props.focused && !props.hovered
|
|
28
|
+
? props.theme.roles.surfaces.panel
|
|
29
|
+
: palette().background;
|
|
26
30
|
const foreground = () =>
|
|
27
31
|
props.quiet && !props.focused && !props.hovered
|
|
28
32
|
? props.theme.roles.text.muted
|
|
@@ -67,10 +71,17 @@ export function KeyHint(props: KeyHintProps) {
|
|
|
67
71
|
<text fg={foreground()} bg={background()}>
|
|
68
72
|
{props.focused || props.selected ? (
|
|
69
73
|
<strong>{content()}</strong>
|
|
70
|
-
) : props.quiet && !props.presentation ? (
|
|
74
|
+
) : (props.quiet || props.button) && !props.presentation ? (
|
|
71
75
|
<>
|
|
72
76
|
{" "}
|
|
73
|
-
<span
|
|
77
|
+
<span
|
|
78
|
+
style={{
|
|
79
|
+
fg: props.button
|
|
80
|
+
? props.theme.roles.selection.selectionText
|
|
81
|
+
: props.theme.roles.text.primary,
|
|
82
|
+
bg: props.button ? props.theme.roles.selection.selection : background(),
|
|
83
|
+
}}
|
|
84
|
+
>
|
|
74
85
|
{clipTerminal(props.keys, Math.max(0, width() - 1))}
|
|
75
86
|
</span>
|
|
76
87
|
{clipTerminal(
|
|
@@ -71,6 +71,7 @@ export function applicationCommandDescription(command: ApplicationPaletteCommand
|
|
|
71
71
|
id: command,
|
|
72
72
|
label: command === "hide-sidebar" ? "Hide sidebar" : "Show sidebar",
|
|
73
73
|
detail: "Terminal layout",
|
|
74
|
+
shortcut: "F10",
|
|
74
75
|
};
|
|
75
76
|
if (command === "switch-session")
|
|
76
77
|
return {
|
|
@@ -13,8 +13,11 @@ export const APPLICATION_SHORTCUTS: readonly ApplicationShortcut[] = [
|
|
|
13
13
|
return { category: "Application", label: description.label, keys: description.shortcut! };
|
|
14
14
|
}),
|
|
15
15
|
{ category: "Application", label: "Commands", keys: "F5" },
|
|
16
|
+
{ category: "Application", label: "Show / hide sidebar", keys: "F10" },
|
|
16
17
|
{ category: "Application", label: "Agent attention", keys: "F7" },
|
|
17
18
|
{ category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
|
|
19
|
+
{ category: "Terminals", label: "Open link", keys: "Shift+click" },
|
|
20
|
+
{ category: "Terminals", label: "Select and copy text", keys: "Shift+drag" },
|
|
18
21
|
{ category: "Home", label: "Find an agent or workspace", keys: "/" },
|
|
19
22
|
{ category: "Home", label: "Cycle machine filter", keys: "f" },
|
|
20
23
|
{ category: "Home", label: "Toggle attention filter", keys: "a" },
|