tmux-ide 2.6.0 → 2.6.1
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 +13 -5
- package/bin/cli.ts +1 -1
- package/bunfig.toml +4 -0
- package/package.json +11 -3
- 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/src/agent-explain.ts +270 -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 +336 -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/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 +195 -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 +428 -0
- package/packages/daemon/src/tui/chrome/welcome.ts +124 -0
- package/packages/daemon/src/tui/compiled.ts +113 -0
- package/packages/daemon/src/tui/detect/classify.ts +193 -0
- package/packages/daemon/src/tui/detect/manifest-loader.ts +192 -0
- package/packages/daemon/src/tui/detect/manifest.ts +184 -0
- package/packages/daemon/src/tui/detect/manifests.ts +226 -0
- package/packages/daemon/src/tui/detect/process-tree.ts +197 -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 +70 -0
- package/packages/daemon/src/tui/mirror/control-client.ts +143 -0
- package/packages/daemon/src/tui/mirror/control.ts +97 -0
- package/packages/daemon/src/tui/mirror/pane-mirror.ts +81 -0
- package/packages/daemon/src/tui/mirror/viewer.tsx +166 -0
- package/packages/daemon/src/tui/team/CONTROL.md +50 -0
- package/packages/daemon/src/tui/team/entry.ts +11 -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 +73 -0
- package/packages/daemon/src/tui/team/sessions.ts +344 -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/postinstall.js +26 -2
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tmux-ide status bar — native chrome.
|
|
3
|
+
*
|
|
4
|
+
* Instead of wrapping or re-rendering tmux, tmux-ide renders itself INTO
|
|
5
|
+
* tmux's own status line: `adoptSession` gives a session a second status row
|
|
6
|
+
* whose content is a pre-rendered `@tmux_ide_status` user option that a single
|
|
7
|
+
* background updater ({@link ./updater.ts}) keeps fresh — the bar reads a bare
|
|
8
|
+
* `#{@tmux_ide_status}`, so tmux spawns nothing per tick. The row lists every
|
|
9
|
+
* project/session with a live agent-state glyph and persists no matter which
|
|
10
|
+
* pane has focus — tmux draws the chrome, so there's no nesting and the user's
|
|
11
|
+
* real sessions stay untouched otherwise.
|
|
12
|
+
*
|
|
13
|
+
* `buildStatusline` is pure (tested); the io wrappers are thin.
|
|
14
|
+
*/
|
|
15
|
+
import { runTmux } from "@tmux-ide/tmux-bridge";
|
|
16
|
+
import { DEFAULT_THEME, getAppConfig, type AppKeys, type AppTheme } from "../../lib/app-config.ts";
|
|
17
|
+
import type { AgentStatus } from "../detect/classify.ts";
|
|
18
|
+
import type { TeamProject } from "../team/projects.ts";
|
|
19
|
+
import { cheatsheetBindCommand, cheatsheetPopupCommand } from "./cheatsheet.ts";
|
|
20
|
+
import {
|
|
21
|
+
menuBindCommand,
|
|
22
|
+
menuPaneBindCommand,
|
|
23
|
+
menuPaneUnbindCommand,
|
|
24
|
+
menuStatusBindCommand,
|
|
25
|
+
menuStatusUnbindCommand,
|
|
26
|
+
} from "./menu.ts";
|
|
27
|
+
import { PANEL_POPUPS, panelKey, panelPopupBindCommand } from "./panels.ts";
|
|
28
|
+
import { sidebarToggleBindCommand } from "./sidebar.ts";
|
|
29
|
+
import { maybeShowWelcomePopup } from "./welcome.ts";
|
|
30
|
+
import { maybeOfferIntegrationPopup } from "../integrations/offer.ts";
|
|
31
|
+
import { kittyEscapeFor, kittyUserKeyIndex, kittyUserKeyName } from "./kitty-keys.ts";
|
|
32
|
+
import {
|
|
33
|
+
ADOPTED_OPTION,
|
|
34
|
+
CHIP_OPTION,
|
|
35
|
+
listAdoptedSessions,
|
|
36
|
+
seedSessionStatus,
|
|
37
|
+
startUpdaterIfNeeded,
|
|
38
|
+
STATUS_OPTION,
|
|
39
|
+
stopUpdater,
|
|
40
|
+
} from "./updater.ts";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* PURE — tmux style markup for a status color token. `blocked` reads bold (an
|
|
44
|
+
* urgency cue kept from the original STATUS_STYLE); every other state is a plain
|
|
45
|
+
* `#[fg=…]`. Shared with the pane chip ({@link ./chip.ts}) so a chip matches its
|
|
46
|
+
* session's rollup glyph.
|
|
47
|
+
*/
|
|
48
|
+
export function statusStyle(status: AgentStatus, theme: AppTheme): string {
|
|
49
|
+
const color = theme.status[status];
|
|
50
|
+
return status === "blocked" ? `#[fg=${color},bold]` : `#[fg=${color}]`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* PURE — the glyph character for a running project's state: the filled `active`
|
|
55
|
+
* token for a known state, but `·` for `unknown` (a "no signal" marker with no
|
|
56
|
+
* token of its own).
|
|
57
|
+
*/
|
|
58
|
+
export function statusGlyph(status: AgentStatus, theme: AppTheme): string {
|
|
59
|
+
return status === "unknown" ? "·" : theme.glyphs.active;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Whether a name is INTERNAL — the host shell (`_tmux-ide`) and any
|
|
64
|
+
* `_`-prefixed scratch/plumbing session. Internal names are hidden from both
|
|
65
|
+
* the status bar and the switcher: they're infrastructure, not projects.
|
|
66
|
+
*/
|
|
67
|
+
export function isInternalName(name: string): boolean {
|
|
68
|
+
return name.startsWith("_");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* PURE — the session names `adopt --all` should adopt: every live session that
|
|
73
|
+
* isn't internal (`_`-prefixed plumbing like the updater/scratch sessions).
|
|
74
|
+
*/
|
|
75
|
+
export function adoptableSessionNames(names: string[]): string[] {
|
|
76
|
+
return names.filter((name) => name.length > 0 && !isInternalName(name));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build the status-bar string with tmux `#[...]` markup.
|
|
81
|
+
*
|
|
82
|
+
* One segment per entry: running projects show their rolled-up status glyph;
|
|
83
|
+
* stopped registered projects show a muted `○`. The `active` session's
|
|
84
|
+
* segment is highlighted (bold + underscore) so you can see where you are.
|
|
85
|
+
* `maxItems` caps the segment count (tmux clips overflow anyway; capping
|
|
86
|
+
* keeps the useful part visible).
|
|
87
|
+
*
|
|
88
|
+
* Interactivity (tmux status-line mouse ranges — see {@link statusClickBindCommand}):
|
|
89
|
+
* - Each RUNNING project is wrapped in a `#[range=user|sw<session>]…#[norange]`
|
|
90
|
+
* range keyed by its first live session, so a click switches to it. The
|
|
91
|
+
* prefix is a bare `sw` (NO colon): a `:` inside the `#{s/^sw//:…}` used to
|
|
92
|
+
* extract the session name is swallowed by tmux's modifier parser (verified
|
|
93
|
+
* live — the extraction yields ""). Stopped projects stay un-ranged (they'd
|
|
94
|
+
* need a launch flow to click — out of scope).
|
|
95
|
+
* - A right-aligned `switcher` TRIGGER button ends the row so the popup is
|
|
96
|
+
* discoverable from the footer, not just via the M-p key.
|
|
97
|
+
*/
|
|
98
|
+
export function buildStatusline(
|
|
99
|
+
projects: TeamProject[],
|
|
100
|
+
active: string | null,
|
|
101
|
+
maxItems = 12,
|
|
102
|
+
theme: AppTheme = DEFAULT_THEME,
|
|
103
|
+
extraSegment = "",
|
|
104
|
+
): string {
|
|
105
|
+
const visible = projects.filter((p) => !isInternalName(p.name));
|
|
106
|
+
const segments: string[] = [];
|
|
107
|
+
for (const project of visible.slice(0, maxItems)) {
|
|
108
|
+
const isActive =
|
|
109
|
+
active !== null &&
|
|
110
|
+
(project.name === active || project.sessions.some((s) => s.name === active));
|
|
111
|
+
const glyph = project.running
|
|
112
|
+
? `${statusStyle(project.status, theme)}${statusGlyph(project.status, theme)}#[default]`
|
|
113
|
+
: `#[fg=${theme.muted}]${theme.glyphs.inactive}#[default]`;
|
|
114
|
+
const name = isActive
|
|
115
|
+
? `#[fg=colour231,bold,underscore]${project.name}#[default]`
|
|
116
|
+
: project.running
|
|
117
|
+
? `#[fg=${theme.fg}]${project.name}#[default]`
|
|
118
|
+
: `#[fg=${theme.muted}]${project.name}#[default]`;
|
|
119
|
+
const label = `${glyph} ${name}`;
|
|
120
|
+
// Only running projects are clickable — wrap them in a session-keyed range.
|
|
121
|
+
const session = project.sessions[0]?.name;
|
|
122
|
+
segments.push(
|
|
123
|
+
project.running && session ? `#[range=user|sw${session}]${label}#[norange]` : label,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
if (visible.length > maxItems) {
|
|
127
|
+
segments.push(`#[fg=${theme.muted}]+${visible.length - maxItems}#[default]`);
|
|
128
|
+
}
|
|
129
|
+
const body = segments.join(" ");
|
|
130
|
+
// A reserved right-side slot the update-flow card (0047) feeds "⬆ vX.Y.Z"
|
|
131
|
+
// through; empty by default so it takes no space. Sits before the triggers.
|
|
132
|
+
const extra = extraSegment ? `${extraSegment} ` : "";
|
|
133
|
+
// Two button-like, right-aligned triggers. `keys` (muted) floats the cheat
|
|
134
|
+
// sheet; `switcher` (primary) opens the picker popup — switch stays visually
|
|
135
|
+
// dominant, the sheet is the quieter companion just to its left.
|
|
136
|
+
// Key hints advertise the PREFIX form (^b h) — the binding that works under
|
|
137
|
+
// every keyboard protocol. The Alt twins still exist but die when a focused
|
|
138
|
+
// app (Claude Code) switches the terminal's key encoding, so they'd be a
|
|
139
|
+
// misleading promise on exactly the sessions people live in.
|
|
140
|
+
const keysTrigger = `#[range=user|keys]#[fg=colour244][ ? keys ^b k ]#[default]#[norange]`;
|
|
141
|
+
// The home trigger floats the full fleet cockpit (the same prefix-h / M-h keys
|
|
142
|
+
// open) so "get me back to the home screen" is discoverable from the dock,
|
|
143
|
+
// not just via the key. Muted like `keys` — switch stays the dominant action.
|
|
144
|
+
const homeTrigger = `#[range=user|home]#[fg=colour244][ ⌂ home ^b h ]#[default]#[norange]`;
|
|
145
|
+
const trigger = `#[range=user|switcher]#[fg=${theme.accent},bold][ ⧉ switch ^b j ]#[default]#[norange]`;
|
|
146
|
+
return `#[fg=${theme.accent},bold] tmux-ide #[default] ${body}#[align=right]${extra}${homeTrigger} ${keysTrigger} ${trigger} `;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The root-table key that opens the floating switcher popup. Chosen to avoid
|
|
151
|
+
* tmux defaults: `M-p` (Alt+p) is unbound by stock tmux, and — unlike prefix
|
|
152
|
+
* `p` (previous-window) — it lives in the ROOT table so it fires without the
|
|
153
|
+
* prefix from any adopted session. Alt-letter keys are directional/rare in
|
|
154
|
+
* terminal apps, so grabbing one at the root is low-collision.
|
|
155
|
+
*/
|
|
156
|
+
export const POPUP_KEY = "M-p";
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The root-table key that floats the full fleet HOME cockpit: `M-h` (Alt+h).
|
|
160
|
+
* Like {@link POPUP_KEY} it lives in the ROOT table so it fires without the
|
|
161
|
+
* prefix from any adopted session. Where the switcher is the compact picker,
|
|
162
|
+
* this opens the full two-column home (fleet tree + detail) as a large popup —
|
|
163
|
+
* "get me home from anywhere", the first half of the discovery flow.
|
|
164
|
+
*/
|
|
165
|
+
export const HOME_KEY = "M-h";
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The root-table key that opens the right-click actions menu: `M-m` (Alt+m).
|
|
169
|
+
* Like {@link POPUP_KEY} it lives in the ROOT table so it fires without the
|
|
170
|
+
* prefix from any adopted session, and sits with `M-p`/`M-k` as the third chrome
|
|
171
|
+
* shortcut. The menu itself is drawn by tmux's `display-menu` (see ./menu.ts).
|
|
172
|
+
*/
|
|
173
|
+
export const MENU_KEY = "M-m";
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The root-table mouse key for a RIGHT-click on the status bar. tmux fires this
|
|
177
|
+
* for a right-click landing anywhere on a status line; the menu is
|
|
178
|
+
* range-independent (unlike the left-click router — see {@link STATUS_CLICK_KEY}),
|
|
179
|
+
* so it opens the same actions menu regardless of what's under the click.
|
|
180
|
+
*/
|
|
181
|
+
// MouseUP, deliberately: the menu is built via a ~150ms CLI hop, so a DOWN bind
|
|
182
|
+
// opens it under a still-held button and the user's release lands on/near the
|
|
183
|
+
// fresh menu and dismisses it instantly (measured live; -O does not save it).
|
|
184
|
+
// Opening on release means no trailing button event exists to kill the menu.
|
|
185
|
+
export const MENU_STATUS_KEY = "MouseUp3Status";
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The root-table mouse key for a RIGHT-click on ANY pane body (not just the
|
|
189
|
+
* chrome row): `MouseDown3Pane`. tmux fires this for a right-click landing inside
|
|
190
|
+
* a pane, so the actions menu is reachable from anywhere — any pane, any session
|
|
191
|
+
* — not only the dock row. Like {@link MENU_STATUS_KEY} it opens the same
|
|
192
|
+
* `display-menu`. NOTE: a pane whose app grabs the mouse (vim, an agent TUI, …)
|
|
193
|
+
* consumes the event before tmux sees it — graceful degradation, the `M-m` key
|
|
194
|
+
* still opens the menu there.
|
|
195
|
+
*/
|
|
196
|
+
export const MENU_PANE_KEY = "MouseUp3Pane";
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* PURE — the `display-popup` command STRING that floats the switcher (shared by
|
|
200
|
+
* the M-p bind, the bar's left-click router, and the actions menu's "Switch
|
|
201
|
+
* session…" item, so all three open an identical popup). Mirror of the sizing in
|
|
202
|
+
* {@link popupBindCommand}.
|
|
203
|
+
*/
|
|
204
|
+
export function switcherPopupCommand(switcherCmd = "tmux-ide switcher"): string {
|
|
205
|
+
return `display-popup -E -w 80% -h 60% "${switcherCmd}"`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* PURE — the tmux argv that binds the popup key: `M-p` opens a `display-popup`
|
|
210
|
+
* running the compact switcher, which `switch-client`s you to whatever you
|
|
211
|
+
* pick and then exits (closing the popup).
|
|
212
|
+
*
|
|
213
|
+
* The bound command is just `<switcherCmd>` (default `tmux-ide switcher`). We
|
|
214
|
+
* deliberately do NOT append `--client '#{client_name}'`: on tmux 3.6 a
|
|
215
|
+
* `#{...}` format in a `display-popup -E` command argument is NOT expanded at
|
|
216
|
+
* invocation (verified live — the literal string survives to the shell). The
|
|
217
|
+
* switcher instead resolves its own invoking client from inside the popup via
|
|
218
|
+
* `tmux display-message -p '#{client_name}'`, which DOES resolve correctly, and
|
|
219
|
+
* switches with an explicit `-c <client>`.
|
|
220
|
+
*
|
|
221
|
+
* Bindings are SERVER-wide (there is no per-session `bind-key`), so this is a
|
|
222
|
+
* global root-table bind — see the note on {@link unadoptSession}.
|
|
223
|
+
*/
|
|
224
|
+
export function popupBindCommand(switcherCmd = "tmux-ide switcher", key = POPUP_KEY): string[] {
|
|
225
|
+
return ["bind-key", "-n", key, "display-popup", "-E", "-w", "80%", "-h", "60%", switcherCmd];
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** PURE — the tmux argv that removes the popup key binding. */
|
|
229
|
+
export function popupUnbindCommand(key = POPUP_KEY): string[] {
|
|
230
|
+
return ["unbind-key", "-n", key];
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* PURE — the `display-popup` command STRING that floats the full HOME cockpit
|
|
235
|
+
* (shared by the M-h bind, the dock's `[ ⌂ home ⌥h ]` trigger, and the actions
|
|
236
|
+
* menu's Home item, so all three open an identical popup). The bound command is
|
|
237
|
+
* `tmux-ide team --popup`: the `--popup` flag tells the cockpit it's floating
|
|
238
|
+
* over a tmux client, so — like the switcher popup — Enter `switch-client`s the
|
|
239
|
+
* invoking client and closes instead of attaching in place. Large (95%×95%)
|
|
240
|
+
* because it's the full two-column layout, not the compact picker.
|
|
241
|
+
*/
|
|
242
|
+
export function homePopupCommand(homeCmd = "tmux-ide team --popup"): string {
|
|
243
|
+
return `display-popup -E -w 95% -h 95% "${homeCmd}"`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* PURE — the tmux argv that binds {@link HOME_KEY} (`M-h`, root table) to the
|
|
248
|
+
* home cockpit popup. Server-wide bind, mirroring the switcher's M-p — see the
|
|
249
|
+
* note on {@link unadoptSession}. Idempotent: re-binding overwrites.
|
|
250
|
+
*/
|
|
251
|
+
export function homeBindCommand(homeCmd = "tmux-ide team --popup", key = HOME_KEY): string[] {
|
|
252
|
+
return ["bind-key", "-n", key, "display-popup", "-E", "-w", "95%", "-h", "95%", homeCmd];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** PURE — the tmux argv that removes the home key binding. */
|
|
256
|
+
export function homeUnbindCommand(key = HOME_KEY): string[] {
|
|
257
|
+
return ["unbind-key", "-n", key];
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* PURE — the `display-popup` command STRING for the dock's `⬆ v<latest>` update
|
|
262
|
+
* chip (see {@link ./updater.ts updateSegment}). It runs `tmux-ide update
|
|
263
|
+
* --dry-run` — which prints the exact command to run for THIS install (dev
|
|
264
|
+
* checkout, npm/pnpm/bun global) — then holds the popup open until Enter so the
|
|
265
|
+
* instruction is readable (a bare `-E` popup would close the instant the CLI
|
|
266
|
+
* exits). Shell-portable: `read _` (no bash-only `-n1`), no backslash escapes.
|
|
267
|
+
*/
|
|
268
|
+
export function updatePopupCommand(updateCmd = "tmux-ide update --dry-run"): string {
|
|
269
|
+
const shell = `${updateCmd}; echo ''; echo '[ press Enter to close ]'; read _`;
|
|
270
|
+
return `display-popup -E -w 70% -h 50% "${shell}"`;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The root-table mouse key that routes clicks on the status bar. tmux fires
|
|
275
|
+
* this for a click landing on a NAMED range in any status line (our chrome
|
|
276
|
+
* row's `user|…` ranges), exposing the range name via `#{mouse_status_range}`.
|
|
277
|
+
*/
|
|
278
|
+
export const STATUS_CLICK_KEY = "MouseDown1Status";
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Wrap a tmux command string as a double-quoted argument for embedding ONE
|
|
282
|
+
* level deeper: escape backslashes first, then double quotes. Composable — the
|
|
283
|
+
* innermost command can be `dq`'d once per nesting level and each layer's
|
|
284
|
+
* dequote peels exactly one level off (verified live), which is what lets the
|
|
285
|
+
* dispatch chain nest `keys → sw*` without hand-counting escapes.
|
|
286
|
+
*/
|
|
287
|
+
function dq(cmd: string): string {
|
|
288
|
+
return `"${cmd.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* PURE — the tmux argv that binds status-bar clicks (server-wide, root table).
|
|
293
|
+
*
|
|
294
|
+
* Dispatch is on `#{mouse_status_range}` (the `#[range=user|<name>]` under the
|
|
295
|
+
* click, surfaced as `<name>`), a nested if-shell chain:
|
|
296
|
+
* - `switcher` → the SAME `display-popup` the M-p key runs.
|
|
297
|
+
* - `home` → the home-cockpit `display-popup` the M-h key runs.
|
|
298
|
+
* - `keys` → the cheat-sheet `display-popup` the M-k key runs.
|
|
299
|
+
* - `sw<session>` → switch the clicking client to `<session>`.
|
|
300
|
+
* - anything else → tmux's default `select-window -t =` (window-list clicks
|
|
301
|
+
* on the primary status line keep working).
|
|
302
|
+
*
|
|
303
|
+
* WHY run-shell for the switch (verified live on tmux 3.6):
|
|
304
|
+
* - A `switch-client -t <target>` target is NOT format-expanded — passing
|
|
305
|
+
* `-t "#{…}"` fails with `can't find session: #{…}`. So the session name
|
|
306
|
+
* can't be handed to `switch-client` as a format directly.
|
|
307
|
+
* - `run-shell` DOES expand `#{…}` in its command, so it re-invokes
|
|
308
|
+
* `tmux switch-client` with the name already extracted via
|
|
309
|
+
* `#{s/^sw//:mouse_status_range}` and the clicking client from
|
|
310
|
+
* `#{client_name}`.
|
|
311
|
+
*
|
|
312
|
+
* The two popup branches stay tmux-native (identical to {@link popupBindCommand}
|
|
313
|
+
* / {@link cheatsheetBindCommand}), so a click behaves exactly like M-p / M-k.
|
|
314
|
+
* The `sw*` branch nests one level under `keys`, so its inner commands are
|
|
315
|
+
* `dq`'d twice — the chain stays balanced (see {@link dq}).
|
|
316
|
+
*
|
|
317
|
+
* SERVER-wide bind — tmux has no per-session key table — so this replaces the
|
|
318
|
+
* global `MouseDown1Status` for every client (same caveat as the M-p popup key;
|
|
319
|
+
* see {@link unadoptSession}). Idempotent: re-binding overwrites.
|
|
320
|
+
*/
|
|
321
|
+
export function statusClickBindCommand(
|
|
322
|
+
switcherCmd = "tmux-ide switcher",
|
|
323
|
+
cheatsheetCmd = "tmux-ide cheatsheet",
|
|
324
|
+
): string[] {
|
|
325
|
+
const popup = switcherPopupCommand(switcherCmd);
|
|
326
|
+
const cheatsheet = cheatsheetPopupCommand(cheatsheetCmd);
|
|
327
|
+
const home = homePopupCommand();
|
|
328
|
+
const update = updatePopupCommand();
|
|
329
|
+
// run-shell re-enters tmux with the name/client already format-expanded.
|
|
330
|
+
const switchClient = `run-shell "tmux switch-client -c '#{client_name}' -t '#{s/^sw//:mouse_status_range}'"`;
|
|
331
|
+
// `sw*` → switch, else window-list default. Its args are one level deep here.
|
|
332
|
+
const swBranch = `if-shell -F "#{m:sw*,#{mouse_status_range}}" ${dq(switchClient)} "select-window -t ="`;
|
|
333
|
+
// `keys` → cheat sheet, else the sw branch. Both args nest one level deeper,
|
|
334
|
+
// so they're `dq`'d again — dequoting peels the layers off in order.
|
|
335
|
+
const keysBranch = `if-shell -F "#{==:#{mouse_status_range},keys}" ${dq(cheatsheet)} ${dq(swBranch)}`;
|
|
336
|
+
// `home` → the home cockpit popup, else the keys branch. One more nesting
|
|
337
|
+
// level, so both args are `dq`'d once more on top of what they already carry.
|
|
338
|
+
const homeBranch = `if-shell -F "#{==:#{mouse_status_range},home}" ${dq(home)} ${dq(keysBranch)}`;
|
|
339
|
+
// `update` → the update-flow popup (the dock's `⬆ v<latest>` chip), else the
|
|
340
|
+
// home branch. Another nesting level, so both args carry one more `dq` layer.
|
|
341
|
+
const updateBranch = `if-shell -F "#{==:#{mouse_status_range},update}" ${dq(update)} ${dq(homeBranch)}`;
|
|
342
|
+
return [
|
|
343
|
+
"bind-key",
|
|
344
|
+
"-n",
|
|
345
|
+
STATUS_CLICK_KEY,
|
|
346
|
+
"if-shell",
|
|
347
|
+
"-F",
|
|
348
|
+
"#{==:#{mouse_status_range},switcher}",
|
|
349
|
+
popup,
|
|
350
|
+
updateBranch,
|
|
351
|
+
];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** PURE — the tmux argv that removes the status-click binding. */
|
|
355
|
+
export function statusClickUnbindCommand(): string[] {
|
|
356
|
+
return ["unbind-key", "-n", STATUS_CLICK_KEY];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Adopt a session: add the chrome row (status line 2) that renders the
|
|
361
|
+
* pre-computed `@tmux_ide_status` var (a bare `#{…}` read — tmux spawns
|
|
362
|
+
* nothing per tick; a single background updater keeps the var fresh), bind the
|
|
363
|
+
* popup key so `M-p` opens the floating switcher from anywhere in the session,
|
|
364
|
+
* mark the session adopted, seed its bar immediately, and make sure the updater
|
|
365
|
+
* loop is running. Status/marker options are set per-session (`-t`) so only
|
|
366
|
+
* adopted sessions change; the key binds are server-wide (tmux has no
|
|
367
|
+
* per-session bind). `unadoptSession` reverses all of it.
|
|
368
|
+
*
|
|
369
|
+
* The var form is plain `#{@tmux_ide_status}` (not `#{E:…}`): styles (`#[…]`)
|
|
370
|
+
* are parsed at draw time, so the stored markup renders its colors directly and
|
|
371
|
+
* the `E:` re-expansion buys nothing but a double-`#` hazard (verified live on
|
|
372
|
+
* tmux 3.6 — both forms render identically, so the simpler one wins).
|
|
373
|
+
*/
|
|
374
|
+
/**
|
|
375
|
+
* PURE — the per-session `set-option` argv adopt applies: the second status row
|
|
376
|
+
* (which renders the pre-computed `@tmux_ide_status` var — a bare `#{…}` read,
|
|
377
|
+
* no per-tick spawn), mouse mode for status clicks, and the adopted marker the
|
|
378
|
+
* updater enumerates by. All `-t <session>` so only adopted sessions change.
|
|
379
|
+
*/
|
|
380
|
+
export function adoptOptionCommands(session: string): string[][] {
|
|
381
|
+
const format = `#[align=left]#{${STATUS_OPTION}}`;
|
|
382
|
+
// Per-pane agent chip on the bottom border: render `@tmux_ide_chip` (kept
|
|
383
|
+
// fresh by the updater) when set, else fall back to the pane title. NOTE:
|
|
384
|
+
// tmux hides pane borders when a window has only ONE pane, so a chip only
|
|
385
|
+
// shows once a window has been split — acceptable (there's no border to paint
|
|
386
|
+
// in a single-pane window).
|
|
387
|
+
const borderFormat = ` #{?#{${CHIP_OPTION}},#{${CHIP_OPTION}},#{pane_title}} `;
|
|
388
|
+
return [
|
|
389
|
+
["set-option", "-t", session, "status", "2"],
|
|
390
|
+
["set-option", "-t", session, "status-interval", "2"],
|
|
391
|
+
["set-option", "-t", session, "status-format[1]", format],
|
|
392
|
+
// Status-line clicks need mouse mode ON. NOTE: this also changes scroll
|
|
393
|
+
// behavior (the wheel enters copy-mode / scrolls pane history instead of the
|
|
394
|
+
// terminal's native scrollback). Per-session (`-t`) so only adopted change.
|
|
395
|
+
["set-option", "-t", session, "mouse", "on"],
|
|
396
|
+
// Per-pane agent chips on the bottom border (see borderFormat above).
|
|
397
|
+
["set-option", "-t", session, "pane-border-status", "bottom"],
|
|
398
|
+
["set-option", "-t", session, "pane-border-format", borderFormat],
|
|
399
|
+
// Marker the updater enumerates by (readable in list-sessions -F formats).
|
|
400
|
+
["set-option", "-t", session, ADOPTED_OPTION, "1"],
|
|
401
|
+
];
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* PURE — the per-session `set-option -u` argv unadopt applies: revert the status
|
|
406
|
+
* row/mouse to inherited, and drop the adopted marker + status var.
|
|
407
|
+
*/
|
|
408
|
+
export function unadoptOptionCommands(session: string): string[][] {
|
|
409
|
+
return [
|
|
410
|
+
["set-option", "-u", "-t", session, "status"],
|
|
411
|
+
["set-option", "-u", "-t", session, "status-interval"],
|
|
412
|
+
["set-option", "-u", "-t", session, "status-format[1]"],
|
|
413
|
+
["set-option", "-u", "-t", session, "mouse"],
|
|
414
|
+
["set-option", "-u", "-t", session, "pane-border-status"],
|
|
415
|
+
["set-option", "-u", "-t", session, "pane-border-format"],
|
|
416
|
+
["set-option", "-u", "-t", session, ADOPTED_OPTION],
|
|
417
|
+
["set-option", "-u", "-t", session, STATUS_OPTION],
|
|
418
|
+
];
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* The root-table ALT-KEY binds adopt registers, as `(tmux key, bind argv)`
|
|
423
|
+
* pairs, in a STABLE order: popup, home, cheat sheet, menu, sidebar, then the
|
|
424
|
+
* panels. Threaded through ONE place so adopt, unadopt, AND the kitty-protocol
|
|
425
|
+
* User-key fallbacks all agree — the bind argv's action (everything after
|
|
426
|
+
* `bind-key -n <key>`) is reused verbatim for the `UserN` twin (see
|
|
427
|
+
* {@link ./kitty-keys.ts}). The mouse binds (status-click / right-click) are
|
|
428
|
+
* NOT here — they aren't Alt keys and need no kitty fallback.
|
|
429
|
+
*/
|
|
430
|
+
export function altKeyBinds(
|
|
431
|
+
keys: AppKeys,
|
|
432
|
+
switcherCmd = "tmux-ide switcher",
|
|
433
|
+
): Array<{ key: string; bind: string[] }> {
|
|
434
|
+
return [
|
|
435
|
+
{ key: keys.popup, bind: popupBindCommand(switcherCmd, keys.popup) },
|
|
436
|
+
{ key: keys.home, bind: homeBindCommand("tmux-ide team --popup", keys.home) },
|
|
437
|
+
{ key: keys.cheatsheet, bind: cheatsheetBindCommand("tmux-ide cheatsheet", keys.cheatsheet) },
|
|
438
|
+
{ key: keys.menu, bind: menuBindCommand("tmux-ide menu", keys.menu) },
|
|
439
|
+
{ key: keys.sidebar, bind: sidebarToggleBindCommand("tmux-ide sidebar-toggle", keys.sidebar) },
|
|
440
|
+
...PANEL_POPUPS.map((panel) => {
|
|
441
|
+
const key = panelKey(panel, keys.panels);
|
|
442
|
+
return { key, bind: panelPopupBindCommand(panel, key) };
|
|
443
|
+
}),
|
|
444
|
+
];
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Lowercase letters tmux binds by DEFAULT in the prefix table — deriving a
|
|
449
|
+
* prefix twin must never clobber these (c/n/p window ops, m mark, s/w choosers,
|
|
450
|
+
* o pane rotate, l last, d detach, x kill, z zoom, f find, i info, q display,
|
|
451
|
+
* r refresh, t clock).
|
|
452
|
+
*/
|
|
453
|
+
const PREFIX_TAKEN = new Set([..."cdfilmnopqrstwxz"]);
|
|
454
|
+
|
|
455
|
+
/** Alt keys whose letter is taken get an explicit, documented prefix home. */
|
|
456
|
+
const PREFIX_REMAP: Record<string, string> = {
|
|
457
|
+
"M-m": "u", // menu — m is mark-pane
|
|
458
|
+
"M-p": "j", // switcher — p is previous-window; j = "jump"
|
|
459
|
+
"M-,": "v", // config panel — , is rename-window
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* PURE — the `prefix + <letter>` twins of every Alt-key action. The tmux PREFIX
|
|
464
|
+
* survives every keyboard-protocol flavor (it's how you use tmux at all), so
|
|
465
|
+
* these are the RELIABLE bindings — the Alt forms are the bonus for terminals
|
|
466
|
+
* that deliver them. Same letter as the Alt key where tmux's defaults allow,
|
|
467
|
+
* {@link PREFIX_REMAP} where they don't; anything unmappable is skipped.
|
|
468
|
+
*/
|
|
469
|
+
export function prefixKeyBinds(
|
|
470
|
+
keys: AppKeys,
|
|
471
|
+
switcherCmd = "tmux-ide switcher",
|
|
472
|
+
): Array<{ pkey: string; bind: string[] }> {
|
|
473
|
+
const out: Array<{ pkey: string; bind: string[] }> = [];
|
|
474
|
+
for (const { key, bind } of altKeyBinds(keys, switcherCmd)) {
|
|
475
|
+
const remapped = PREFIX_REMAP[key];
|
|
476
|
+
const letter = remapped ?? /^M-([a-z])$/.exec(key)?.[1];
|
|
477
|
+
if (!letter || (!remapped && PREFIX_TAKEN.has(letter))) continue;
|
|
478
|
+
out.push({ pkey: letter, bind: ["bind-key", "-T", "prefix", letter, ...bind.slice(3)] });
|
|
479
|
+
}
|
|
480
|
+
return out;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Binds retired by newer chrome generations — adopt clears them so a server
|
|
485
|
+
* that lived through an upgrade doesn't keep firing the old behavior alongside
|
|
486
|
+
* the new (the menu moved from MouseDown3 to MouseUp3: with both bound, the
|
|
487
|
+
* press opened a menu the release instantly killed).
|
|
488
|
+
*/
|
|
489
|
+
const LEGACY_BINDS: string[][] = [
|
|
490
|
+
["unbind-key", "-n", "MouseDown3Status"],
|
|
491
|
+
["unbind-key", "-n", "MouseDown3Pane"],
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
export function adoptSession(session: string, switcherCmd = "tmux-ide switcher"): void {
|
|
495
|
+
for (const argv of adoptOptionCommands(session)) runTmux(argv);
|
|
496
|
+
// Clear binds retired by newer chrome generations (best-effort).
|
|
497
|
+
for (const legacy of LEGACY_BINDS) {
|
|
498
|
+
try {
|
|
499
|
+
runTmux(legacy);
|
|
500
|
+
} catch {
|
|
501
|
+
// nothing stale bound — fine
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Key binds are configurable via ~/.tmux-ide/config.json (keys.*); re-adopting
|
|
505
|
+
// after a config change rebinds them. Server-wide + idempotent (re-binding a
|
|
506
|
+
// key just overwrites it).
|
|
507
|
+
const keys = getAppConfig().keys;
|
|
508
|
+
// The mouse binds: the status-click router and the two right-click menu
|
|
509
|
+
// openers (chrome row + any pane body). Not Alt keys → no kitty fallback.
|
|
510
|
+
runTmux(statusClickBindCommand(switcherCmd));
|
|
511
|
+
runTmux(menuStatusBindCommand());
|
|
512
|
+
runTmux(menuPaneBindCommand());
|
|
513
|
+
// Every root-table Alt-key bind, PLUS a kitty-protocol User-key fallback so the
|
|
514
|
+
// shortcut still fires when a focused Claude Code pane has the Kitty keyboard
|
|
515
|
+
// protocol on (tmux 3.6 doesn't normalize the `ESC[<code>;3:1u` full form back
|
|
516
|
+
// to `M-…`). The UserN twin runs the identical action argv (`bind.slice(3)` —
|
|
517
|
+
// everything after `bind-key -n <key>`). See {@link ./kitty-keys.ts}.
|
|
518
|
+
altKeyBinds(keys, switcherCmd).forEach(({ key, bind }, i) => {
|
|
519
|
+
runTmux(bind);
|
|
520
|
+
const escape = kittyEscapeFor(key);
|
|
521
|
+
if (escape === null) return; // not an `M-<c>` key → no fallback
|
|
522
|
+
const idx = kittyUserKeyIndex(i);
|
|
523
|
+
runTmux(["set-option", "-s", `user-keys[${idx}]`, escape]);
|
|
524
|
+
runTmux(["bind-key", "-n", kittyUserKeyName(i), ...bind.slice(3)]);
|
|
525
|
+
});
|
|
526
|
+
// The RELIABLE twins: prefix + letter for every action (the prefix survives
|
|
527
|
+
// kitty/CSI-u keyboard protocols; Alt keys don't always).
|
|
528
|
+
for (const { bind } of prefixKeyBinds(keys, switcherCmd)) runTmux(bind);
|
|
529
|
+
// Seed the bar now so it's never blank, then make sure the loop that keeps it
|
|
530
|
+
// fresh is up.
|
|
531
|
+
seedSessionStatus(session);
|
|
532
|
+
startUpdaterIfNeeded();
|
|
533
|
+
// First-run: float the one-time welcome card on the adopting client (gated by
|
|
534
|
+
// the marker file + config; best-effort, never blocks or fails the adopt).
|
|
535
|
+
maybeShowWelcomePopup();
|
|
536
|
+
// First-adopt: if Claude Code is on PATH but the integration isn't installed,
|
|
537
|
+
// offer to install it (one-time, marker + config gated; best-effort — never
|
|
538
|
+
// blocks or fails the adopt). See ../integrations/offer.ts.
|
|
539
|
+
maybeOfferIntegrationPopup();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Remove the chrome row from a session (revert to inherited options), drop the
|
|
544
|
+
* adopted marker + status var, and stop the background updater when this was the
|
|
545
|
+
* LAST adopted session.
|
|
546
|
+
*/
|
|
547
|
+
export function unadoptSession(session: string): void {
|
|
548
|
+
for (const argv of unadoptOptionCommands(session)) runTmux(argv);
|
|
549
|
+
// KNOWN SIMPLIFICATION: the popup key AND the status-click router are
|
|
550
|
+
// SERVER-wide binds, so unadopting one session removes them for ALL adopted
|
|
551
|
+
// sessions. Acceptable for now — best-effort so a missing bind (already
|
|
552
|
+
// unadopted) doesn't throw. Unbind the SAME configured keys adopt bound.
|
|
553
|
+
const keys = getAppConfig().keys;
|
|
554
|
+
// The mouse binds (status-click router + the two right-click menu openers).
|
|
555
|
+
for (const undo of [
|
|
556
|
+
statusClickUnbindCommand(),
|
|
557
|
+
menuStatusUnbindCommand(),
|
|
558
|
+
menuPaneUnbindCommand(),
|
|
559
|
+
]) {
|
|
560
|
+
try {
|
|
561
|
+
runTmux(undo);
|
|
562
|
+
} catch {
|
|
563
|
+
// no such key bound — nothing to undo
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// Every Alt-key bind AND its kitty User-key fallback + the user-keys slot,
|
|
567
|
+
// walked in the SAME order adopt registered them so the slot indices line up.
|
|
568
|
+
altKeyBinds(keys, "tmux-ide switcher").forEach(({ key }, i) => {
|
|
569
|
+
try {
|
|
570
|
+
runTmux(["unbind-key", "-n", key]);
|
|
571
|
+
} catch {
|
|
572
|
+
// no such key bound — nothing to undo
|
|
573
|
+
}
|
|
574
|
+
if (kittyEscapeFor(key) === null) return;
|
|
575
|
+
try {
|
|
576
|
+
runTmux(["unbind-key", "-n", kittyUserKeyName(i)]);
|
|
577
|
+
} catch {
|
|
578
|
+
// no such User key bound — nothing to undo
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
runTmux(["set-option", "-su", `user-keys[${kittyUserKeyIndex(i)}]`]);
|
|
582
|
+
} catch {
|
|
583
|
+
// no such user-keys slot set — nothing to undo
|
|
584
|
+
}
|
|
585
|
+
});
|
|
586
|
+
for (const { pkey } of prefixKeyBinds(keys, "tmux-ide switcher")) {
|
|
587
|
+
try {
|
|
588
|
+
runTmux(["unbind-key", "-T", "prefix", pkey]);
|
|
589
|
+
} catch {
|
|
590
|
+
// not bound — nothing to undo
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
// The updater only needs to run while something is adopted.
|
|
594
|
+
if (listAdoptedSessions().length === 0) stopUpdater();
|
|
595
|
+
}
|