tmux-ide 2.6.0 → 2.7.0
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 +14 -9
- package/bin/cli.js +1024 -519
- package/bin/cli.ts +63 -6
- package/bunfig.toml +4 -0
- package/package.json +18 -7
- package/packages/contracts/package.json +22 -0
- package/packages/contracts/src/__tests__/ide-config.test.ts +46 -0
- package/packages/contracts/src/__tests__/terminals.test.ts +87 -0
- package/packages/contracts/src/actions-contract.ts +310 -0
- package/packages/contracts/src/actions-errors.ts +41 -0
- package/packages/contracts/src/domain.ts +36 -0
- package/packages/contracts/src/ide-config.ts +170 -0
- package/packages/contracts/src/index.ts +24 -0
- package/packages/contracts/src/lib-internal/auth.ts +13 -0
- package/packages/contracts/src/lib-internal/hq.ts +38 -0
- package/packages/contracts/src/terminals.ts +116 -0
- package/packages/contracts/src/tmux.ts +60 -0
- package/packages/contracts/src/workspace.ts +67 -0
- package/packages/daemon/dist/agent-explain.d.ts +8 -1
- package/packages/daemon/dist/agent-explain.js +19 -3
- package/packages/daemon/dist/lib/tui-binary.d.ts +57 -0
- package/packages/daemon/dist/lib/tui-binary.js +130 -0
- package/packages/daemon/dist/widgets/explorer/breadcrumbs.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/explorer/tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/lib/help-overlay.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/agent-naming.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/config-tree.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/detect-panel.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/field-editor.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/footer.d.ts +1 -1
- package/packages/daemon/dist/widgets/setup/layout-picker.d.ts +1 -1
- package/packages/daemon/src/agent-explain.ts +298 -0
- package/packages/daemon/src/attach.ts +20 -0
- package/packages/daemon/src/bin.ts +4 -0
- package/packages/daemon/src/canonical.ts +7 -0
- package/packages/daemon/src/cli.ts +499 -0
- package/packages/daemon/src/command-center/actions/contract.ts +2 -0
- package/packages/daemon/src/command-center/actions/dispatcher.ts +137 -0
- package/packages/daemon/src/command-center/actions/errors.ts +105 -0
- package/packages/daemon/src/command-center/actions/handlers/_project-context.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/_resolve-project.ts +78 -0
- package/packages/daemon/src/command-center/actions/handlers/app-set-remote-access.ts +118 -0
- package/packages/daemon/src/command-center/actions/handlers/config-actions.ts +113 -0
- package/packages/daemon/src/command-center/actions/handlers/daemon-shutdown.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-activate.ts +30 -0
- package/packages/daemon/src/command-center/actions/handlers/project-launch.ts +70 -0
- package/packages/daemon/src/command-center/actions/handlers/project-open-terminal.ts +87 -0
- package/packages/daemon/src/command-center/actions/handlers/project-restart.ts +38 -0
- package/packages/daemon/src/command-center/actions/handlers/project-stop.ts +62 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-respawn.ts +119 -0
- package/packages/daemon/src/command-center/actions/handlers/terminal-stop.ts +35 -0
- package/packages/daemon/src/command-center/actions/registry.ts +149 -0
- package/packages/daemon/src/command-center/discovery.ts +96 -0
- package/packages/daemon/src/command-center/index.ts +31 -0
- package/packages/daemon/src/command-center/schemas.ts +85 -0
- package/packages/daemon/src/command-center/server.ts +1260 -0
- package/packages/daemon/src/command-center/ws-events.ts +316 -0
- package/packages/daemon/src/config.ts +549 -0
- package/packages/daemon/src/detect.ts +248 -0
- package/packages/daemon/src/doctor.ts +242 -0
- package/packages/daemon/src/embed.ts +5 -0
- package/packages/daemon/src/index.ts +12 -0
- package/packages/daemon/src/init.ts +211 -0
- package/packages/daemon/src/inspect.ts +178 -0
- package/packages/daemon/src/js-yaml.d.ts +10 -0
- package/packages/daemon/src/launch.ts +349 -0
- package/packages/daemon/src/lib/active-projects.ts +49 -0
- package/packages/daemon/src/lib/agent-discovery.ts +121 -0
- package/packages/daemon/src/lib/app-config.ts +427 -0
- package/packages/daemon/src/lib/app-settings.ts +53 -0
- package/packages/daemon/src/lib/auth/auth-service.ts +227 -0
- package/packages/daemon/src/lib/auth/middleware.ts +56 -0
- package/packages/daemon/src/lib/auth/types.ts +2 -0
- package/packages/daemon/src/lib/auth-token.ts +5 -0
- package/packages/daemon/src/lib/authorship.ts +280 -0
- package/packages/daemon/src/lib/canonical-daemon.ts +122 -0
- package/packages/daemon/src/lib/cli-action-bridge.ts +216 -0
- package/packages/daemon/src/lib/daemon-embed.ts +782 -0
- package/packages/daemon/src/lib/daemon-watchdog.ts +111 -0
- package/packages/daemon/src/lib/daemon.ts +79 -0
- package/packages/daemon/src/lib/dot-path.ts +17 -0
- package/packages/daemon/src/lib/errors.ts +67 -0
- package/packages/daemon/src/lib/filesystem-browser.ts +292 -0
- package/packages/daemon/src/lib/launch-plan.ts +90 -0
- package/packages/daemon/src/lib/log.ts +134 -0
- package/packages/daemon/src/lib/output.ts +76 -0
- package/packages/daemon/src/lib/project-init-runner.ts +150 -0
- package/packages/daemon/src/lib/project-inspect.ts +80 -0
- package/packages/daemon/src/lib/project-onboard.ts +149 -0
- package/packages/daemon/src/lib/project-probe.ts +92 -0
- package/packages/daemon/src/lib/project-registry.ts +296 -0
- package/packages/daemon/src/lib/session-monitor.ts +122 -0
- package/packages/daemon/src/lib/session-options.ts +100 -0
- package/packages/daemon/src/lib/shell.ts +8 -0
- package/packages/daemon/src/lib/sizes.ts +36 -0
- package/packages/daemon/src/lib/skill-sync.ts +155 -0
- package/packages/daemon/src/lib/slugify.ts +10 -0
- package/packages/daemon/src/lib/terminals-store.ts +125 -0
- package/packages/daemon/src/lib/tui-binary.ts +165 -0
- package/packages/daemon/src/lib/update-check.ts +298 -0
- package/packages/daemon/src/lib/update.ts +158 -0
- package/packages/daemon/src/lib/workspace-registry.ts +229 -0
- package/packages/daemon/src/lib/worktree.ts +289 -0
- package/packages/daemon/src/lib/yaml-io.ts +27 -0
- package/packages/daemon/src/ls.ts +40 -0
- package/packages/daemon/src/restart.ts +24 -0
- package/packages/daemon/src/restore.ts +514 -0
- package/packages/daemon/src/schemas/domain.ts +2 -0
- package/packages/daemon/src/schemas/filesystem.ts +34 -0
- package/packages/daemon/src/schemas/ide-config.ts +2 -0
- package/packages/daemon/src/schemas/index.ts +59 -0
- package/packages/daemon/src/schemas/inspect.ts +67 -0
- package/packages/daemon/src/schemas/registry.ts +55 -0
- package/packages/daemon/src/schemas/ws-events.ts +135 -0
- package/packages/daemon/src/send.ts +171 -0
- package/packages/daemon/src/server/README.md +15 -0
- package/packages/daemon/src/server/index.ts +74 -0
- package/packages/daemon/src/server/pty-bridge.ts +532 -0
- package/packages/daemon/src/server/standalone.ts +18 -0
- package/packages/daemon/src/server/ws-route.ts +483 -0
- package/packages/daemon/src/status.ts +59 -0
- package/packages/daemon/src/stop.ts +28 -0
- package/packages/daemon/src/terminal/NodePtyAdapter.ts +271 -0
- package/packages/daemon/src/terminal/PtyAdapter.ts +140 -0
- package/packages/daemon/src/terminal/README.md +92 -0
- package/packages/daemon/src/tui/chrome/cheatsheet.ts +260 -0
- package/packages/daemon/src/tui/chrome/chip.ts +30 -0
- package/packages/daemon/src/tui/chrome/events.ts +119 -0
- package/packages/daemon/src/tui/chrome/kitty-keys.ts +55 -0
- package/packages/daemon/src/tui/chrome/menu.ts +289 -0
- package/packages/daemon/src/tui/chrome/notify.ts +382 -0
- package/packages/daemon/src/tui/chrome/panels.ts +111 -0
- package/packages/daemon/src/tui/chrome/sidebar.ts +222 -0
- package/packages/daemon/src/tui/chrome/snapshot.ts +425 -0
- package/packages/daemon/src/tui/chrome/statusline.ts +595 -0
- package/packages/daemon/src/tui/chrome/updater.ts +510 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +121 -0
- package/packages/daemon/src/tui/detect/classify.ts +208 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest.ts +199 -0
- package/packages/daemon/src/tui/detect/manifests.ts +354 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +217 -0
- package/packages/daemon/src/tui/detect/snapshot.ts +70 -0
- package/packages/daemon/src/tui/integrations/claude.ts +176 -0
- package/packages/daemon/src/tui/integrations/offer.ts +145 -0
- package/packages/daemon/src/tui/main.ts +82 -0
- package/packages/daemon/src/tui/mirror/ack-writer.ts +77 -0
- package/packages/daemon/src/tui/mirror/agent-chip.ts +97 -0
- package/packages/daemon/src/tui/mirror/agent-rows.ts +133 -0
- package/packages/daemon/src/tui/mirror/app-state.ts +179 -0
- package/packages/daemon/src/tui/mirror/app.tsx +5265 -0
- package/packages/daemon/src/tui/mirror/blit.ts +186 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +214 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/dialog-model.ts +298 -0
- package/packages/daemon/src/tui/mirror/dialog-stack.ts +354 -0
- package/packages/daemon/src/tui/mirror/diff-model.ts +112 -0
- package/packages/daemon/src/tui/mirror/editor-buffer.ts +117 -0
- package/packages/daemon/src/tui/mirror/file-tree.ts +97 -0
- package/packages/daemon/src/tui/mirror/focus-border.ts +57 -0
- package/packages/daemon/src/tui/mirror/folder-picker.ts +124 -0
- package/packages/daemon/src/tui/mirror/home-model.ts +174 -0
- package/packages/daemon/src/tui/mirror/input-coalescer.ts +105 -0
- package/packages/daemon/src/tui/mirror/menu-model.ts +187 -0
- package/packages/daemon/src/tui/mirror/palette.ts +274 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +561 -0
- package/packages/daemon/src/tui/mirror/pane-surface.tsx +415 -0
- package/packages/daemon/src/tui/mirror/perf-tap.ts +160 -0
- package/packages/daemon/src/tui/mirror/resize-model.ts +85 -0
- package/packages/daemon/src/tui/mirror/scrollbar-model.ts +88 -0
- package/packages/daemon/src/tui/mirror/search-model.ts +70 -0
- package/packages/daemon/src/tui/mirror/selection.ts +262 -0
- package/packages/daemon/src/tui/mirror/session-mirror.ts +443 -0
- package/packages/daemon/src/tui/mirror/settings-model.ts +345 -0
- package/packages/daemon/src/tui/mirror/size-truth.ts +77 -0
- package/packages/daemon/src/tui/mirror/spans.ts +46 -0
- package/packages/daemon/src/tui/mirror/status-grammar.ts +32 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +38 -0
- package/packages/daemon/src/tui/team/fuzzy.ts +133 -0
- package/packages/daemon/src/tui/team/home.ts +170 -0
- package/packages/daemon/src/tui/team/index.tsx +1521 -0
- package/packages/daemon/src/tui/team/input.ts +34 -0
- package/packages/daemon/src/tui/team/keymap.ts +127 -0
- package/packages/daemon/src/tui/team/mouse.ts +29 -0
- package/packages/daemon/src/tui/team/nav.ts +31 -0
- package/packages/daemon/src/tui/team/preview.ts +34 -0
- package/packages/daemon/src/tui/team/projects.ts +191 -0
- package/packages/daemon/src/tui/team/report.ts +83 -0
- package/packages/daemon/src/tui/team/sessions.ts +433 -0
- package/packages/daemon/src/tui/team/tree.ts +62 -0
- package/packages/daemon/src/types.ts +13 -0
- package/packages/daemon/src/ui/index.ts +32 -0
- package/packages/daemon/src/ui/terminal/index.ts +9 -0
- package/packages/daemon/src/ui/types.ts +91 -0
- package/packages/daemon/src/ui/web/base.css +80 -0
- package/packages/daemon/src/ui/web/components/Box.tsx +59 -0
- package/packages/daemon/src/ui/web/components/Input.tsx +32 -0
- package/packages/daemon/src/ui/web/components/ScrollBox.tsx +60 -0
- package/packages/daemon/src/ui/web/components/Text.tsx +28 -0
- package/packages/daemon/src/ui/web/hooks.ts +106 -0
- package/packages/daemon/src/ui/web/index.ts +27 -0
- package/packages/daemon/src/ui/web/render.ts +77 -0
- package/packages/daemon/src/ui/web/utils/color.ts +27 -0
- package/packages/daemon/src/validate.ts +217 -0
- package/packages/daemon/src/widgets/changes/README.md +3 -0
- package/packages/daemon/src/widgets/changes/index.tsx +691 -0
- package/packages/daemon/src/widgets/config/README.md +3 -0
- package/packages/daemon/src/widgets/config/index.tsx +481 -0
- package/packages/daemon/src/widgets/explorer/README.md +3 -0
- package/packages/daemon/src/widgets/explorer/breadcrumbs.tsx +77 -0
- package/packages/daemon/src/widgets/explorer/footer.tsx +20 -0
- package/packages/daemon/src/widgets/explorer/header.tsx +23 -0
- package/packages/daemon/src/widgets/explorer/index.tsx +456 -0
- package/packages/daemon/src/widgets/explorer/tree-model.ts +103 -0
- package/packages/daemon/src/widgets/explorer/tree.tsx +165 -0
- package/packages/daemon/src/widgets/lib/config-model.ts +116 -0
- package/packages/daemon/src/widgets/lib/files.ts +88 -0
- package/packages/daemon/src/widgets/lib/git.ts +88 -0
- package/packages/daemon/src/widgets/lib/grammar.ts +126 -0
- package/packages/daemon/src/widgets/lib/help-overlay.tsx +101 -0
- package/packages/daemon/src/widgets/lib/pane-comms.ts +209 -0
- package/packages/daemon/src/widgets/lib/theme.ts +194 -0
- package/packages/daemon/src/widgets/lib/watcher.ts +132 -0
- package/packages/daemon/src/widgets/preview/README.md +3 -0
- package/packages/daemon/src/widgets/preview/index.tsx +416 -0
- package/packages/daemon/src/widgets/resolve.ts +121 -0
- package/packages/daemon/src/widgets/setup/README.md +3 -0
- package/packages/daemon/src/widgets/setup/agent-naming.tsx +112 -0
- package/packages/daemon/src/widgets/setup/config-tree.tsx +246 -0
- package/packages/daemon/src/widgets/setup/detect-panel.tsx +72 -0
- package/packages/daemon/src/widgets/setup/field-editor.tsx +265 -0
- package/packages/daemon/src/widgets/setup/footer.tsx +107 -0
- package/packages/daemon/src/widgets/setup/index.tsx +341 -0
- package/packages/daemon/src/widgets/setup/layout-picker.tsx +96 -0
- package/packages/daemon/src/widgets/setup/orchestrator-panel.tsx +200 -0
- package/packages/daemon/src/widgets/setup/review-panel.tsx +140 -0
- package/packages/daemon/src/widgets/setup/setup-model.ts +188 -0
- package/packages/daemon/src/widgets/sidebar/index.tsx +527 -0
- package/packages/tmux-bridge/package.json +22 -0
- package/packages/tmux-bridge/src/errors.ts +28 -0
- package/packages/tmux-bridge/src/index.ts +31 -0
- package/packages/tmux-bridge/src/monitor.ts +77 -0
- package/packages/tmux-bridge/src/panes.ts +136 -0
- package/packages/tmux-bridge/src/runner.test.ts +501 -0
- package/packages/tmux-bridge/src/runner.ts +91 -0
- package/packages/tmux-bridge/src/sessions.ts +126 -0
- package/packages/tmux-bridge/src/targeting.test.ts +107 -0
- package/packages/tmux-bridge/src/targeting.ts +90 -0
- package/scripts/build-tui.mjs +11 -4
- package/scripts/perf-mirror.mjs +313 -0
- package/scripts/postinstall.js +26 -2
- package/skill/SKILL.md +22 -0
- package/templates/AGENTS.md +14 -7
- package/templates/agent-team-monorepo.yml +8 -0
- package/templates/agent-team-nextjs.yml +8 -0
- package/templates/agent-team.yml +10 -0
- package/templates/convex.yml +2 -0
- package/templates/default.yml +11 -5
- package/templates/go.yml +4 -0
- package/templates/missions.yml +6 -0
- package/templates/nextjs.yml +4 -0
- package/templates/python.yml +4 -0
- package/templates/skills/backend.md +5 -12
- package/templates/skills/frontend.md +5 -12
- package/templates/skills/general-worker.md +5 -12
- package/templates/skills/researcher.md +7 -12
- package/templates/skills/reviewer.md +7 -16
- package/templates/vite.yml +4 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure cell → framebuffer writers for the framebuffer pane-blit path (M21.3).
|
|
3
|
+
*
|
|
4
|
+
* These map a mirror grid straight into an OpenTUI `OptimizedBuffer`'s typed
|
|
5
|
+
* arrays — no `StyledRun[]` intermediary, no per-run `RGBA` allocation, no Solid
|
|
6
|
+
* subtree. One packed cell is `char[i]` (a codepoint), four `fg`/`bg` channels
|
|
7
|
+
* (`r,g,b,a`, 0–255) at `i*4`, and one `attributes[i]` bitmask — the exact layout
|
|
8
|
+
* `OptimizedBuffer.buffers` exposes.
|
|
9
|
+
*
|
|
10
|
+
* Kept dependency-free (plain typed arrays) so the packing is unit-testable
|
|
11
|
+
* without the native render lib; {@link PaneMirror.blit} does the xterm walk and
|
|
12
|
+
* feeds these.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** The spacer (second) half of a wide glyph: char `0`. The native renderer knows
|
|
16
|
+
* a cell is wide by MEASURING the lead cell's codepoint width and skips the
|
|
17
|
+
* spacer, so the spacer's char just needs to be empty — this is exactly what
|
|
18
|
+
* `OptimizedBuffer.setCell` writes for the trailing half (verified: a wide
|
|
19
|
+
* glyph → `char[i]=codepoint, char[i+1]=0`). NB the `0xC0000000`
|
|
20
|
+
* `CHAR_FLAG_CONTINUATION` seen in OptimizedBuffer is a DIFFERENT consumer (the
|
|
21
|
+
* JS `getSpanLines`/`getRealCharBytes` text reader, off our render path);
|
|
22
|
+
* writing it here blanks wide glyphs on screen (measured). */
|
|
23
|
+
export const CHAR_CONTINUATION = 0;
|
|
24
|
+
|
|
25
|
+
/** The space codepoint — the fill for blank / out-of-range cells. */
|
|
26
|
+
export const SPACE_CODE = 0x20;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* INVERSE is rendered by SWAPPING fg/bg, not by the OpenTUI INVERSE attribute
|
|
30
|
+
* bit: a framebuffer cell carrying that bit composites (drawFrameBuffer preserves
|
|
31
|
+
* it) but does NOT flush as reverse-video — the direct `<text>` path does, the
|
|
32
|
+
* framebuffer path doesn't (measured). Swapping colors always renders (truecolor
|
|
33
|
+
* channels propagate cleanly), and covers app-driven reverse (vim status line),
|
|
34
|
+
* the cursor cell, and the drag selection uniformly. Swaps compose like the old
|
|
35
|
+
* XOR: two swaps on a cell (selection over the cursor) return it to normal.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/** The four packed arrays behind an `OptimizedBuffer` (or a plain-array stand-in
|
|
39
|
+
* for tests). `fg`/`bg` hold four `u16` channels (0–255) per cell at `i*4`. */
|
|
40
|
+
export interface CellArrays {
|
|
41
|
+
char: Uint32Array;
|
|
42
|
+
fg: Uint16Array;
|
|
43
|
+
bg: Uint16Array;
|
|
44
|
+
attributes: Uint32Array;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A cell whose grapheme is more than one codepoint (ZWJ/flag emoji, combining
|
|
49
|
+
* marks) — a single `char[i]` u32 can't hold it. The fast blit writes its base
|
|
50
|
+
* codepoint; the caller (which owns the native `OptimizedBuffer`) re-writes these
|
|
51
|
+
* few cells with the full string via `setCell`, keeping full parity for the rare
|
|
52
|
+
* case without an allocation on the common path.
|
|
53
|
+
*/
|
|
54
|
+
export interface GraphemeOverride {
|
|
55
|
+
x: number;
|
|
56
|
+
y: number;
|
|
57
|
+
chars: string;
|
|
58
|
+
/** Packed `0xRRGGBB` or null (terminal default). */
|
|
59
|
+
fg: number | null;
|
|
60
|
+
bg: number | null;
|
|
61
|
+
attrs: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write one styled cell at flat index `idx`. `fg`/`bg` are packed `0xRRGGBB` or
|
|
66
|
+
* `null` for the terminal default (→ the supplied `d*` channels); alpha is always
|
|
67
|
+
* opaque (panes never blend). No allocation — just eight typed-array stores.
|
|
68
|
+
*/
|
|
69
|
+
export function writeCell(
|
|
70
|
+
buf: CellArrays,
|
|
71
|
+
idx: number,
|
|
72
|
+
codepoint: number,
|
|
73
|
+
fg: number | null,
|
|
74
|
+
bg: number | null,
|
|
75
|
+
attrs: number,
|
|
76
|
+
dfR: number,
|
|
77
|
+
dfG: number,
|
|
78
|
+
dfB: number,
|
|
79
|
+
dbR: number,
|
|
80
|
+
dbG: number,
|
|
81
|
+
dbB: number,
|
|
82
|
+
): void {
|
|
83
|
+
buf.char[idx] = codepoint;
|
|
84
|
+
const o = idx * 4;
|
|
85
|
+
if (fg === null) {
|
|
86
|
+
buf.fg[o] = dfR;
|
|
87
|
+
buf.fg[o + 1] = dfG;
|
|
88
|
+
buf.fg[o + 2] = dfB;
|
|
89
|
+
} else {
|
|
90
|
+
buf.fg[o] = (fg >> 16) & 0xff;
|
|
91
|
+
buf.fg[o + 1] = (fg >> 8) & 0xff;
|
|
92
|
+
buf.fg[o + 2] = fg & 0xff;
|
|
93
|
+
}
|
|
94
|
+
buf.fg[o + 3] = 255;
|
|
95
|
+
if (bg === null) {
|
|
96
|
+
buf.bg[o] = dbR;
|
|
97
|
+
buf.bg[o + 1] = dbG;
|
|
98
|
+
buf.bg[o + 2] = dbB;
|
|
99
|
+
} else {
|
|
100
|
+
buf.bg[o] = (bg >> 16) & 0xff;
|
|
101
|
+
buf.bg[o + 1] = (bg >> 8) & 0xff;
|
|
102
|
+
buf.bg[o + 2] = bg & 0xff;
|
|
103
|
+
}
|
|
104
|
+
buf.bg[o + 3] = 255;
|
|
105
|
+
buf.attributes[idx] = attrs;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Mark cell `idx` as the continuation (spacer) half of the wide glyph at `idx-1`,
|
|
110
|
+
* inheriting the glyph's colors so a colored wide glyph's background spans both
|
|
111
|
+
* cells. The renderer emits nothing for a continuation cell but still paints its
|
|
112
|
+
* background.
|
|
113
|
+
*/
|
|
114
|
+
export function writeContinuation(buf: CellArrays, idx: number): void {
|
|
115
|
+
buf.char[idx] = CHAR_CONTINUATION;
|
|
116
|
+
const o = idx * 4;
|
|
117
|
+
const p = (idx - 1) * 4;
|
|
118
|
+
buf.fg[o] = buf.fg[p]!;
|
|
119
|
+
buf.fg[o + 1] = buf.fg[p + 1]!;
|
|
120
|
+
buf.fg[o + 2] = buf.fg[p + 2]!;
|
|
121
|
+
buf.fg[o + 3] = buf.fg[p + 3]!;
|
|
122
|
+
buf.bg[o] = buf.bg[p]!;
|
|
123
|
+
buf.bg[o + 1] = buf.bg[p + 1]!;
|
|
124
|
+
buf.bg[o + 2] = buf.bg[p + 2]!;
|
|
125
|
+
buf.bg[o + 3] = buf.bg[p + 3]!;
|
|
126
|
+
buf.attributes[idx] = buf.attributes[idx - 1]!;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Swap fg/bg across cells `[from,to]` (inclusive, clamped to the row) of row `y`
|
|
131
|
+
* — the drag-selection reverse-video post-pass. A swap over the cursor cell (also
|
|
132
|
+
* swapped in the blit) returns it to normal, matching the run path's XOR.
|
|
133
|
+
*/
|
|
134
|
+
export function swapCells(
|
|
135
|
+
buf: CellArrays,
|
|
136
|
+
width: number,
|
|
137
|
+
y: number,
|
|
138
|
+
from: number,
|
|
139
|
+
to: number,
|
|
140
|
+
): void {
|
|
141
|
+
const lo = Math.max(0, from);
|
|
142
|
+
const hi = Math.min(width - 1, to);
|
|
143
|
+
const base = y * width;
|
|
144
|
+
for (let x = lo; x <= hi; x++) {
|
|
145
|
+
const o = (base + x) * 4;
|
|
146
|
+
const r = buf.fg[o]!;
|
|
147
|
+
const g = buf.fg[o + 1]!;
|
|
148
|
+
const b = buf.fg[o + 2]!;
|
|
149
|
+
const a = buf.fg[o + 3]!;
|
|
150
|
+
buf.fg[o] = buf.bg[o]!;
|
|
151
|
+
buf.fg[o + 1] = buf.bg[o + 1]!;
|
|
152
|
+
buf.fg[o + 2] = buf.bg[o + 2]!;
|
|
153
|
+
buf.fg[o + 3] = buf.bg[o + 3]!;
|
|
154
|
+
buf.bg[o] = r;
|
|
155
|
+
buf.bg[o + 1] = g;
|
|
156
|
+
buf.bg[o + 2] = b;
|
|
157
|
+
buf.bg[o + 3] = a;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Paint the background of cells `[from,to]` (inclusive, clamped) of row `y` to
|
|
163
|
+
* packed `bg` — the scrollback-search highlight post-pass (the twin of
|
|
164
|
+
* {@link invertCells}; distinct treatment so a match and the selection coexist).
|
|
165
|
+
*/
|
|
166
|
+
export function paintBg(
|
|
167
|
+
buf: CellArrays,
|
|
168
|
+
width: number,
|
|
169
|
+
y: number,
|
|
170
|
+
from: number,
|
|
171
|
+
to: number,
|
|
172
|
+
bg: number,
|
|
173
|
+
): void {
|
|
174
|
+
const lo = Math.max(0, from);
|
|
175
|
+
const hi = Math.min(width - 1, to);
|
|
176
|
+
const r = (bg >> 16) & 0xff;
|
|
177
|
+
const g = (bg >> 8) & 0xff;
|
|
178
|
+
const b = bg & 0xff;
|
|
179
|
+
for (let x = lo; x <= hi; x++) {
|
|
180
|
+
const o = (y * width + x) * 4;
|
|
181
|
+
buf.bg[o] = r;
|
|
182
|
+
buf.bg[o + 1] = g;
|
|
183
|
+
buf.bg[o + 2] = b;
|
|
184
|
+
buf.bg[o + 3] = 255;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tmux control-mode client.
|
|
3
|
+
*
|
|
4
|
+
* Spawns `tmux -C attach -t <target>` and speaks the control protocol over
|
|
5
|
+
* stdio: commands written to stdin get their replies back wrapped in
|
|
6
|
+
* `%begin`/`%end` blocks (FIFO order), while live pane bytes stream in as
|
|
7
|
+
* `%output` notifications between blocks. This is the same integration
|
|
8
|
+
* surface iTerm2 uses for its native tmux mode — tmux stays the multiplexer
|
|
9
|
+
* and persistence layer; the caller renders.
|
|
10
|
+
*
|
|
11
|
+
* stdout is read as latin1 (one JS char per byte) so pane bytes survive
|
|
12
|
+
* intact; `parseControlLine`/`decodeControlBytes` do the pure protocol work.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
15
|
+
import { appendFileSync } from "node:fs";
|
|
16
|
+
import { parseControlLine, textToHexKeys } from "./control.ts";
|
|
17
|
+
|
|
18
|
+
interface PendingReply {
|
|
19
|
+
discard?: false;
|
|
20
|
+
resolve: (lines: string[]) => void;
|
|
21
|
+
reject: (err: Error) => void;
|
|
22
|
+
lines: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** A fire-and-forget command's FIFO placeholder: its reply block is consumed
|
|
26
|
+
* and dropped (errors counted). `lines` collects the error body only when
|
|
27
|
+
* TMUX_IDE_MIRROR_DEBUG asks for it. */
|
|
28
|
+
interface DiscardedReply {
|
|
29
|
+
discard: true;
|
|
30
|
+
lines?: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Pending = PendingReply | DiscardedReply;
|
|
34
|
+
|
|
35
|
+
/** The shared no-debug placeholder — fire-and-forget input allocates NOTHING. */
|
|
36
|
+
const DISCARDED: DiscardedReply = { discard: true };
|
|
37
|
+
|
|
38
|
+
export interface ControlClientOptions {
|
|
39
|
+
/** Session (or other tmux target) to attach the control client to. */
|
|
40
|
+
attachTarget: string;
|
|
41
|
+
/** Live pane bytes: `%output` events, decoded. */
|
|
42
|
+
onOutput?: (pane: string, data: Uint8Array) => void;
|
|
43
|
+
/** Non-output notifications (layout-change, window-add, …). */
|
|
44
|
+
onNotify?: (name: string, rest: string) => void;
|
|
45
|
+
/** Control client ended (tmux exited or detached us). */
|
|
46
|
+
onExit?: (reason: string | null) => void;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class ControlModeClient {
|
|
50
|
+
private proc: ChildProcess | null = null;
|
|
51
|
+
private readonly pending: Pending[] = [];
|
|
52
|
+
private inReply = false;
|
|
53
|
+
private buffer = "";
|
|
54
|
+
private discardedErrors = 0;
|
|
55
|
+
private readonly opts: ControlClientOptions;
|
|
56
|
+
|
|
57
|
+
constructor(opts: ControlClientOptions) {
|
|
58
|
+
this.opts = opts;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Spawn the control client and resolve once tmux's greeting block passed. */
|
|
62
|
+
start(): Promise<void> {
|
|
63
|
+
const proc = spawn("tmux", ["-C", "attach", "-t", this.opts.attachTarget], {
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
+
env: { ...process.env, TMUX: "" },
|
|
66
|
+
});
|
|
67
|
+
this.proc = proc;
|
|
68
|
+
proc.stdout!.setEncoding("latin1");
|
|
69
|
+
proc.stdout!.on("data", (chunk: string) => this.feed(chunk));
|
|
70
|
+
proc.on("exit", () => this.opts.onExit?.(null));
|
|
71
|
+
|
|
72
|
+
// tmux opens the conversation with an unsolicited %begin/%end greeting
|
|
73
|
+
// block; queue a resolver for it so `start()` settles when the protocol
|
|
74
|
+
// is actually live (and so the pending queue stays aligned).
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
this.pending.push({ resolve: () => resolve(), reject, lines: [] });
|
|
77
|
+
proc.on("error", reject);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Run a tmux command over the control channel, resolving its reply body. */
|
|
82
|
+
command(cmd: string): Promise<string[]> {
|
|
83
|
+
const proc = this.proc;
|
|
84
|
+
if (!proc?.stdin?.writable) return Promise.reject(new Error("control client not running"));
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
this.pending.push({ resolve, reject, lines: [] });
|
|
87
|
+
proc.stdin!.write(`${cmd}\n`);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The INPUT FAST PATH (M21.5): write a command fire-and-forget. The bytes
|
|
93
|
+
* hit tmux's stdin exactly as immediately as `command()`'s do, but no
|
|
94
|
+
* Promise/resolver is allocated and nothing ever waits on the reply. Every
|
|
95
|
+
* control-mode command still produces exactly one `%begin/%end` block, so a
|
|
96
|
+
* placeholder is pushed onto the SAME pending FIFO — reply matching for
|
|
97
|
+
* reply-carrying commands (list-panes, capture-pane, …) stays aligned by
|
|
98
|
+
* construction. Errors are swallowed but counted ({@link inputErrorCount});
|
|
99
|
+
* with TMUX_IDE_MIRROR_DEBUG set the error body is appended to
|
|
100
|
+
* /tmp/zz-input-errors.log.
|
|
101
|
+
*/
|
|
102
|
+
send(cmd: string): void {
|
|
103
|
+
const proc = this.proc;
|
|
104
|
+
if (!proc?.stdin?.writable) return;
|
|
105
|
+
this.pending.push(process.env.TMUX_IDE_MIRROR_DEBUG ? { discard: true, lines: [] } : DISCARDED);
|
|
106
|
+
proc.stdin.write(`${cmd}\n`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Type literal text into a pane (UTF-8, sent as hex bytes — quote-proof).
|
|
110
|
+
* Fire-and-forget: input never queues behind a slow structural reply. */
|
|
111
|
+
sendText(pane: string, text: string): void {
|
|
112
|
+
this.send(`send-keys -t ${pane} -H ${textToHexKeys(text).join(" ")}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Send a named tmux key (Enter, Escape, Up, C-c, …) to a pane. Fire-and-forget. */
|
|
116
|
+
sendKey(pane: string, key: string): void {
|
|
117
|
+
this.send(`send-keys -t ${pane} ${key}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** How many fire-and-forget commands came back `%error` (debug/tests). */
|
|
121
|
+
get inputErrorCount(): number {
|
|
122
|
+
return this.discardedErrors;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
dispose(): void {
|
|
126
|
+
const proc = this.proc;
|
|
127
|
+
this.proc = null;
|
|
128
|
+
if (!proc) return;
|
|
129
|
+
try {
|
|
130
|
+
proc.stdin?.write("detach-client\n");
|
|
131
|
+
} catch {
|
|
132
|
+
// already gone
|
|
133
|
+
}
|
|
134
|
+
setTimeout(() => {
|
|
135
|
+
if (proc.exitCode === null) proc.kill();
|
|
136
|
+
}, 250).unref?.();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private feed(chunk: string): void {
|
|
140
|
+
const t0 = process.env.TMUX_IDE_ZZ_PERF ? performance.now() : 0;
|
|
141
|
+
this.buffer += chunk;
|
|
142
|
+
let nl: number;
|
|
143
|
+
while ((nl = this.buffer.indexOf("\n")) !== -1) {
|
|
144
|
+
// tmux terminates lines with \r\n in control mode; strip both.
|
|
145
|
+
let line = this.buffer.slice(0, nl);
|
|
146
|
+
if (line.endsWith("\r")) line = line.slice(0, -1);
|
|
147
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
148
|
+
this.handleLine(line);
|
|
149
|
+
}
|
|
150
|
+
if (t0) {
|
|
151
|
+
try {
|
|
152
|
+
appendFileSync(
|
|
153
|
+
"/tmp/zz-feed.log",
|
|
154
|
+
`${chunk.length} ${(performance.now() - t0).toFixed(2)}\n`,
|
|
155
|
+
);
|
|
156
|
+
} catch {
|
|
157
|
+
/* perf tap */
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private handleLine(line: string): void {
|
|
163
|
+
const event = parseControlLine(line, this.inReply);
|
|
164
|
+
switch (event.kind) {
|
|
165
|
+
case "begin":
|
|
166
|
+
this.inReply = true;
|
|
167
|
+
break;
|
|
168
|
+
case "reply-line": {
|
|
169
|
+
const head = this.pending[0];
|
|
170
|
+
// Discarded (fire-and-forget) replies skip body collection entirely —
|
|
171
|
+
// unless the debug placeholder brought its own lines buffer.
|
|
172
|
+
if (head) head.lines?.push(event.line);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "end":
|
|
176
|
+
case "error": {
|
|
177
|
+
this.inReply = false;
|
|
178
|
+
const reply = this.pending.shift();
|
|
179
|
+
if (!reply) break; // unsolicited block (e.g. greeting after a race)
|
|
180
|
+
if (reply.discard) {
|
|
181
|
+
if (event.kind === "error") {
|
|
182
|
+
this.discardedErrors++;
|
|
183
|
+
if (process.env.TMUX_IDE_MIRROR_DEBUG) {
|
|
184
|
+
try {
|
|
185
|
+
appendFileSync(
|
|
186
|
+
"/tmp/zz-input-errors.log",
|
|
187
|
+
`#${this.discardedErrors} ${reply.lines?.join(" | ") ?? ""}\n`,
|
|
188
|
+
);
|
|
189
|
+
} catch {
|
|
190
|
+
// debug tap only
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (event.kind === "error") {
|
|
197
|
+
reply.reject(new Error(reply.lines.join("\n") || "tmux command failed"));
|
|
198
|
+
} else {
|
|
199
|
+
reply.resolve(reply.lines);
|
|
200
|
+
}
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
case "output":
|
|
204
|
+
this.opts.onOutput?.(event.pane, event.data);
|
|
205
|
+
break;
|
|
206
|
+
case "exit":
|
|
207
|
+
this.opts.onExit?.(event.reason);
|
|
208
|
+
break;
|
|
209
|
+
case "notify":
|
|
210
|
+
this.opts.onNotify?.(event.name, event.rest);
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure parsing for tmux control mode (`tmux -C`).
|
|
3
|
+
*
|
|
4
|
+
* In control mode tmux writes newline-terminated event lines: command replies
|
|
5
|
+
* arrive wrapped in `%begin`/`%end` (or `%error`) blocks, and asynchronous
|
|
6
|
+
* notifications (`%output`, `%layout-change`, `%exit`, …) arrive between
|
|
7
|
+
* blocks. Pane output bytes in `%output` are escaped — every C0 control byte
|
|
8
|
+
* and the backslash itself appear as three-digit octal escapes (`\015`,
|
|
9
|
+
* `\033`, `\134`) — so the decoder here resolves those back to raw BYTES.
|
|
10
|
+
*
|
|
11
|
+
* Everything in this module is pure so the protocol handling can be
|
|
12
|
+
* unit-tested without a live tmux. The client reads tmux's stdout with
|
|
13
|
+
* latin1 encoding (one JS char per byte); `decodeControlBytes` returns a
|
|
14
|
+
* Uint8Array that xterm's `write()` interprets as UTF-8, which keeps
|
|
15
|
+
* multi-byte characters intact end to end.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type ControlEvent =
|
|
19
|
+
| { kind: "begin"; num: number }
|
|
20
|
+
| { kind: "end"; num: number }
|
|
21
|
+
| { kind: "error"; num: number }
|
|
22
|
+
| { kind: "output"; pane: string; data: Uint8Array }
|
|
23
|
+
| { kind: "exit"; reason: string | null }
|
|
24
|
+
| { kind: "notify"; name: string; rest: string }
|
|
25
|
+
| { kind: "reply-line"; line: string };
|
|
26
|
+
|
|
27
|
+
/** Decode a latin1 control-mode payload (with \ooo escapes) into raw bytes. */
|
|
28
|
+
export function decodeControlBytes(escaped: string): Uint8Array {
|
|
29
|
+
const out = new Uint8Array(escaped.length);
|
|
30
|
+
let n = 0;
|
|
31
|
+
for (let i = 0; i < escaped.length; i++) {
|
|
32
|
+
const ch = escaped.charCodeAt(i);
|
|
33
|
+
if (
|
|
34
|
+
ch === 0x5c && // backslash
|
|
35
|
+
i + 3 < escaped.length + 1 &&
|
|
36
|
+
isOctal(escaped.charCodeAt(i + 1)) &&
|
|
37
|
+
isOctal(escaped.charCodeAt(i + 2)) &&
|
|
38
|
+
isOctal(escaped.charCodeAt(i + 3))
|
|
39
|
+
) {
|
|
40
|
+
out[n++] = parseInt(escaped.slice(i + 1, i + 4), 8) & 0xff;
|
|
41
|
+
i += 3;
|
|
42
|
+
} else {
|
|
43
|
+
out[n++] = ch & 0xff;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.subarray(0, n);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isOctal(code: number | undefined): boolean {
|
|
50
|
+
return code !== undefined && code >= 0x30 && code <= 0x37; // '0'..'7'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Parse one control-mode line into an event.
|
|
55
|
+
*
|
|
56
|
+
* `insideReply` matters because reply-block bodies are arbitrary command
|
|
57
|
+
* output — a line starting with `%` inside a block is still body text unless
|
|
58
|
+
* it's the block terminator itself.
|
|
59
|
+
*/
|
|
60
|
+
export function parseControlLine(line: string, insideReply: boolean): ControlEvent {
|
|
61
|
+
if (line.startsWith("%end ") || line.startsWith("%error ")) {
|
|
62
|
+
const isError = line.startsWith("%error ");
|
|
63
|
+
const num = Number(line.split(" ")[2] ?? -1);
|
|
64
|
+
return { kind: isError ? "error" : "end", num };
|
|
65
|
+
}
|
|
66
|
+
if (insideReply) return { kind: "reply-line", line };
|
|
67
|
+
|
|
68
|
+
if (line.startsWith("%begin ")) {
|
|
69
|
+
return { kind: "begin", num: Number(line.split(" ")[2] ?? -1) };
|
|
70
|
+
}
|
|
71
|
+
if (line.startsWith("%output ")) {
|
|
72
|
+
const rest = line.slice("%output ".length);
|
|
73
|
+
const space = rest.indexOf(" ");
|
|
74
|
+
const pane = space === -1 ? rest : rest.slice(0, space);
|
|
75
|
+
const payload = space === -1 ? "" : rest.slice(space + 1);
|
|
76
|
+
return { kind: "output", pane, data: decodeControlBytes(payload) };
|
|
77
|
+
}
|
|
78
|
+
if (line.startsWith("%exit")) {
|
|
79
|
+
const reason = line.length > "%exit ".length ? line.slice("%exit ".length) : null;
|
|
80
|
+
return { kind: "exit", reason };
|
|
81
|
+
}
|
|
82
|
+
if (line.startsWith("%")) {
|
|
83
|
+
const space = line.indexOf(" ");
|
|
84
|
+
const name = space === -1 ? line.slice(1) : line.slice(1, space);
|
|
85
|
+
return { kind: "notify", name, rest: space === -1 ? "" : line.slice(space + 1) };
|
|
86
|
+
}
|
|
87
|
+
// Shouldn't happen outside a reply block, but never throw on protocol noise.
|
|
88
|
+
return { kind: "reply-line", line };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Encode text as a `send-keys -H` hex-byte argv tail (UTF-8 bytes). */
|
|
92
|
+
export function textToHexKeys(text: string): string[] {
|
|
93
|
+
const bytes = new TextEncoder().encode(text);
|
|
94
|
+
const hex: string[] = [];
|
|
95
|
+
for (const b of bytes) hex.push(b.toString(16).padStart(2, "0"));
|
|
96
|
+
return hex;
|
|
97
|
+
}
|