mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 02:29:33 +01:00
fix(tmux): stop nested prefix keys and TUI Alt+digits from moving other clients
Docker and owned-remote tmux servers kept the stock prefix table, so Ctrl+Q then )/(/s/w/L switch-client'd the inner client onto a sibling. Those create chains now leave only C-q send-prefix. codeman tui bound M-1..M-9 in the server-wide root table. The keys stay, but switch-client runs only for the attaching client (tty plus a one-shot pid). A browser tab's Esc-then-digit no longer jumps sessions, and a killed TUI leaves a binding that matches no live client. Co-authored-by: DDT <1786035110@qq.com>
This commit is contained in:
14 files changed
+728
-62
No files matched your search
+18
-5
@@ -128,7 +128,7 @@ import {
|
||||
|
||||
import { EXEC_TIMEOUT_MS } from './config/exec-timeout.js';
|
||||
import { ensureDeepSeekStatusShim } from './deepseek-status-shim.js';
|
||||
import { restrictTmuxPrefixTable } from './tmux-prefix-table.js';
|
||||
import { NESTED_TMUX_PREFIX, nestedPrefixTableResetCommands, restrictTmuxPrefixTable } from './tmux-prefix-table.js';
|
||||
|
||||
/** How long a cached process snapshot stays usable. */
|
||||
const PROC_SNAPSHOT_TTL_MS = 2000;
|
||||
@@ -1229,7 +1229,8 @@ export function remoteTmuxLocation(remote: SessionRemote, sessionId: string): {
|
||||
* ssh -o BatchMode=yes -t [<COD-107 connection opts>] user@host \
|
||||
* 'tmux -L codeman-remote new-session -A -s codeman-ssh-<id> -c <path> "cd <path> && exec <cli>" \
|
||||
* \; set -t codeman-ssh-<id> status off \; set -t codeman-ssh-<id> mouse off \
|
||||
* \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0'
|
||||
* \; set -t codeman-ssh-<id> prefix C-q \; set -s escape-time 0 \; \
|
||||
* unbind-key -a -T prefix \; bind-key -T prefix C-q send-prefix'
|
||||
*
|
||||
* COD-107 — the connection options (`-p`, `-i`, `-J`, SOCKS `-o ProxyCommand`,
|
||||
* arbitrary `-o`) come from the shared `buildSshConnectionArgs(remote)`, so the
|
||||
@@ -1343,7 +1344,7 @@ export function buildRemoteLaunchCommand(options: {
|
||||
`tmux -L ${REMOTE_TMUX_SOCKET} new-session -A -s ${remoteName} -c ${shellescape(remote.remotePath)} ${shellescape(paneCommand)}`,
|
||||
`set -t ${remoteName} status off`,
|
||||
`set -t ${remoteName} mouse off`,
|
||||
`set -t ${remoteName} prefix C-q`,
|
||||
`set -t ${remoteName} prefix ${NESTED_TMUX_PREFIX}`,
|
||||
'set -s escape-time 0',
|
||||
// COD-106 — shared/collaborative sessions: tmux defaults to sizing a window
|
||||
// to the SMALLEST attached client, so two Codemans at different viewports
|
||||
@@ -1352,6 +1353,13 @@ export function buildRemoteLaunchCommand(options: {
|
||||
// Per-session scoped (`set -t <name>`, matching #145's hardening) so a shared
|
||||
// remote tmux server's other sessions keep their own sizing behavior.
|
||||
`set -t ${remoteName} window-size latest`,
|
||||
// The prefix TABLE is server-global (unlike `prefix C-q`, which is per
|
||||
// session). This socket is dedicated to Codeman's owned sessions, so the
|
||||
// same reduction as the local socket applies: Ctrl+Q then )/(/s/w/L must
|
||||
// not switch-client / choose-tree onto a sibling. BEFORE remain-on-exit,
|
||||
// which stays last because an old remote tmux rejects `failed` and aborts
|
||||
// the rest of the chain.
|
||||
...nestedPrefixTableResetCommands(),
|
||||
// #210: keep a CRASHED pane so the failure is still on screen. Without this,
|
||||
// tmux destroys the pane -> window -> session (and, being the only session,
|
||||
// the whole remote server) the instant the pane command exits, which tears the
|
||||
@@ -1368,7 +1376,8 @@ export function buildRemoteLaunchCommand(options: {
|
||||
// LAST in the chain on purpose: tmux aborts the remaining commands of a `\;`
|
||||
// sequence once one errors (also verified), and `failed` needs tmux >= 3.2 on
|
||||
// the REMOTE host. Trailing, a rejection costs only this option; leading, it
|
||||
// would silently drop status/mouse/prefix/escape-time/window-size with it.
|
||||
// would silently drop status/mouse/prefix/escape-time/window-size and the
|
||||
// prefix-table reduction with it.
|
||||
`set -t ${remoteName} remain-on-exit failed`,
|
||||
].join(' \\; ');
|
||||
|
||||
@@ -1540,8 +1549,12 @@ export function buildDockerLaunchCommand(opts: DockerLaunchOptions): string {
|
||||
`new-session -A -s ${dkrName} -c ${workdir} ${shellescape(paneCommand)}`,
|
||||
`set -t ${dkrName} status off`,
|
||||
`set -t ${dkrName} mouse off`,
|
||||
`set -t ${dkrName} prefix C-q`,
|
||||
`set -t ${dkrName} prefix ${NESTED_TMUX_PREFIX}`,
|
||||
'set -s escape-time 0',
|
||||
// Same reduction as the owned-remote chain: the in-container server is
|
||||
// Codeman's (`-L codeman-docker`), and its stock prefix table would move
|
||||
// the inner client to a sibling session on Ctrl+Q then )/(/s/w/L.
|
||||
...nestedPrefixTableResetCommands(),
|
||||
].join(' \\; ');
|
||||
|
||||
const execEnvFlags: string[] = [];
|
||||
|
||||
@@ -27,11 +27,18 @@
|
||||
* per tmux server in practice.
|
||||
*
|
||||
* That tolerance is server-wide like every key table: while such an alias is
|
||||
* bound, a browser tab's client can detach itself with it too. The same goes
|
||||
* for the `root` keys an attach claims, including Alt+1..9, which run
|
||||
* `switch-client`: a tab that receives Esc then a digit within tmux's
|
||||
* `escape-time` is moved onto another session for as long as those bindings
|
||||
* exist. Leaving `root` alone keeps the attach working; it does not close that.
|
||||
* bound, a browser tab's client can detach itself with it too. F1 in `root`
|
||||
* is the same kind of way out. Alt+1..9 are not: `codeman tui` binds those
|
||||
* through `if-shell -F` so `switch-client` runs only for the TUI's own client
|
||||
* (see `bindSwitchKey` in src/tui/tui-client.ts). This file still does not
|
||||
* touch `root`.
|
||||
*
|
||||
* Docker cases and owned remote sessions run a second tmux (`-L codeman-docker`,
|
||||
* `-L codeman-remote`) with `prefix C-q`. That server is not this socket, so
|
||||
* the reset above never sees it, and its stock prefix table still switches
|
||||
* the inner client. `nestedPrefixTableResetCommands` is the same reduction,
|
||||
* appended to those create chains. A non-owned remote attach joins another
|
||||
* Codeman's `-L codeman` and must not run it.
|
||||
*
|
||||
* @module tmux-prefix-table
|
||||
*/
|
||||
@@ -43,6 +50,26 @@ import { shellescape } from './remote-hosts.js';
|
||||
/** tmux's stock prefix, used when `show-options` cannot be read. */
|
||||
const DEFAULT_PREFIX = 'C-b';
|
||||
|
||||
/**
|
||||
* Prefix on the dedicated tmux inside a Docker case and an owned remote
|
||||
* session. Codeman's own socket keeps the user's prefix; these nested servers
|
||||
* always use C-q so it does not collide with the outer prefix.
|
||||
*/
|
||||
export const NESTED_TMUX_PREFIX = 'C-q';
|
||||
|
||||
/**
|
||||
* Commands that reduce a nested tmux's prefix table to `C-q send-prefix`.
|
||||
*
|
||||
* Joined with `\;` onto the `new-session` that creates the server
|
||||
* (`buildRemoteLaunchCommand`, `buildDockerLaunchCommand`). No `tmux` binary
|
||||
* and no `-g`: the table is server-global, which is what we want on these
|
||||
* dedicated sockets and nowhere else. Put them BEFORE `remain-on-exit failed`
|
||||
* so a remote tmux that rejects `failed` does not drop the reduction.
|
||||
*/
|
||||
export function nestedPrefixTableResetCommands(): readonly [string, string] {
|
||||
return ['unbind-key -a -T prefix', `bind-key -T prefix ${NESTED_TMUX_PREFIX} send-prefix`];
|
||||
}
|
||||
|
||||
/** One `list-keys -T prefix` line: key and command. */
|
||||
const PREFIX_LINE = /^bind-key\s+(?:-\S+\s+)*?-T\s+prefix\s+(\S+)\s+(.+)$/;
|
||||
|
||||
|
||||
+34
-8
@@ -56,6 +56,8 @@ import { composerScroll, composerStep, composerText, createComposer, type TuiCom
|
||||
import { formatAwayDigest } from './tui-digest.js';
|
||||
import {
|
||||
ATTACH_BANNER_MARKER,
|
||||
isTuiClientTty,
|
||||
readControllingTty,
|
||||
TuiClient,
|
||||
type TuiSessionOptions,
|
||||
type TuiWindowSizing,
|
||||
@@ -550,7 +552,14 @@ export async function beginAttachHandoff(
|
||||
label: string,
|
||||
tabs: readonly TuiAttachTab[] = [],
|
||||
cols?: number,
|
||||
rows?: number
|
||||
rows?: number,
|
||||
/**
|
||||
* Tty the attach client will inherit. Omit to read this process's terminal.
|
||||
* `null` means there is no usable tty: switch keys are not bound (an unscoped
|
||||
* `switch-client` would move every client on the socket, including a browser
|
||||
* tab). Tests pass a tty so they do not depend on stdin.
|
||||
*/
|
||||
clientTty?: string | null
|
||||
): Promise<TuiAttachHandoff> {
|
||||
const prefix = (await client.readPrefixKey(muxName)) ?? undefined;
|
||||
// Read, not assumed: see detachChord() for the `d` vs `D` mix-up this closes.
|
||||
@@ -584,13 +593,29 @@ export async function beginAttachHandoff(
|
||||
// usable rather than decorative. Only the first nine, only sessions that
|
||||
// really have a pane, and only keys tmux reports as free — the same rule the
|
||||
// way-out key follows, so a binding of the user's own is never shadowed.
|
||||
//
|
||||
// The key table is server-global. The command is guarded to this terminal's
|
||||
// client (see bindSwitchKey): without a tty we bind nothing, and a leaked
|
||||
// unscoped `switch-client` from an older TUI is removed so it cannot keep
|
||||
// moving browser tabs. When we do claim the keys, drop whatever previous
|
||||
// attach left on M-1..M-9 first — including keys this strip will not reuse.
|
||||
const switchKeys: string[] = [];
|
||||
for (const tab of tabs.slice(0, 9)) {
|
||||
if (!tab.muxName) continue;
|
||||
const key = `M-${tab.index}`;
|
||||
const bound = await client.readPrefixBinding(key, 'root');
|
||||
if (bound !== null && !bound.startsWith('switch-client')) continue;
|
||||
if (await client.bindSwitchKey(key, tab.muxName)) switchKeys.push(key);
|
||||
const tty = clientTty === undefined ? readControllingTty() : clientTty || null;
|
||||
const switchTabs = tabs.slice(0, 9).filter((tab) => tab.index >= 1 && tab.index <= 9 && tab.muxName);
|
||||
const guarded = Boolean(tty && isTuiClientTty(tty));
|
||||
if (!guarded) {
|
||||
for (let index = 1; index <= 9; index++) await client.unbindSwitchKey(`M-${index}`, '');
|
||||
} else if (switchTabs.length > 0 && (await client.armTuiSwitchGuard(tty as string))) {
|
||||
for (let index = 1; index <= 9; index++) await client.unbindSwitchKey(`M-${index}`);
|
||||
for (const tab of switchTabs) {
|
||||
const key = `M-${tab.index}`;
|
||||
const bound = await client.readPrefixBinding(key, 'root');
|
||||
// Takeover just cleared ours. Anything still here belongs to the user.
|
||||
if (bound !== null) continue;
|
||||
if (await client.bindSwitchKey(key, tab.muxName as string, tty as string)) switchKeys.push(key);
|
||||
}
|
||||
} else {
|
||||
for (let index = 1; index <= 9; index++) await client.unbindSwitchKey(`M-${index}`, '');
|
||||
}
|
||||
const bannerFor = (activeMux: string, ownLabel: string): Record<string, string> =>
|
||||
buildAttachBanner({
|
||||
@@ -638,7 +663,8 @@ export async function beginAttachHandoff(
|
||||
return {
|
||||
chord: oneKey ? ONE_KEY_DETACH : fallbackDetachChord(prefix, detachKey, claimed && alias ? alias : undefined),
|
||||
async restore(): Promise<void> {
|
||||
for (const key of switchKeys) await client.unbindSwitchKey(key);
|
||||
for (const key of switchKeys) await client.unbindSwitchKey(key, tty ?? '');
|
||||
if (tty && isTuiClientTty(tty)) await client.disarmTuiSwitchGuard(tty);
|
||||
if (oneKey) await client.unbindDetachKey(ONE_KEY_DETACH, 'root');
|
||||
if (claimed && alias) await client.unbindDetachKey(alias);
|
||||
// Options first, then the size: dropping the status bar gives its row
|
||||
|
||||
+183
-26
@@ -41,7 +41,7 @@
|
||||
* @module tui/tui-client
|
||||
*/
|
||||
|
||||
import { execFile as execFileCb } from 'node:child_process';
|
||||
import { execFile as execFileCb, execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import http from 'node:http';
|
||||
import https from 'node:https';
|
||||
@@ -445,6 +445,86 @@ export function parsePrefixBinding(stdout: string, key: string, table: TuiKeyTab
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* User option holding the tmux client pid of the TUI attach that owns M-1..M-9.
|
||||
* Set by `TUI_SWITCH_ATTACH_HOOK` when that client attaches, read by the
|
||||
* `if-shell -F` guard on those keys. A dead pid matches no live client.
|
||||
*/
|
||||
export const TUI_SWITCH_CLIENT_PID = '@codeman-tui-pid';
|
||||
|
||||
/**
|
||||
* Indexed so it does not replace a user's own `client-attached` hook. High
|
||||
* index to stay clear of `[0]`. One-shot: the hook unsets itself after it
|
||||
* records the pid, so a later client that reuses the pty does not re-arm it.
|
||||
*/
|
||||
export const TUI_SWITCH_ATTACH_HOOK = 'client-attached[90]';
|
||||
|
||||
/** Linux `/dev/pts/N` and macOS `/dev/ttysNNN`. Anything else is not a client tty we will interpolate into a format. */
|
||||
const TUI_CLIENT_TTY = /^\/dev\/(?:pts\/[0-9]+|ttys[0-9]+)$/;
|
||||
|
||||
export function isTuiClientTty(value: string): boolean {
|
||||
return TUI_CLIENT_TTY.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* The terminal this process is attached to, when it is a safe tmux client tty.
|
||||
* `codeman tui` inherits that tty into `tmux attach-session`, which is how the
|
||||
* guard knows its own client before the client exists. A pipe (tests, no TTY)
|
||||
* is null: callers then must not bind `switch-client` for every client.
|
||||
*/
|
||||
export function readControllingTty(): string | null {
|
||||
try {
|
||||
const out = execFileSync('tty', { encoding: 'utf8', stdio: ['inherit', 'pipe', 'ignore'] }).trim();
|
||||
return isTuiClientTty(out) ? out : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `if-shell -F` guard for a TUI switch key. True only for the client whose tty
|
||||
* is `tty` AND whose pid is the one the attach hook stored. Evaluated when the
|
||||
* key is pressed, against the client that pressed it.
|
||||
*/
|
||||
export function tuiSwitchGuard(tty: string): string {
|
||||
return `#{&&:#{==:#{client_tty},${tty}},#{==:#{client_pid},#{${TUI_SWITCH_CLIENT_PID}}}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* `set-hook` command for `TUI_SWITCH_ATTACH_HOOK`. `\#{client_pid}` is deferred
|
||||
* so tmux expands it when the hook fires, not when the hook is stored (a bare
|
||||
* `#{client_pid}` is expanded at `set-hook` time, in no client, and stored empty).
|
||||
*/
|
||||
export function tuiSwitchAttachHook(tty: string): string {
|
||||
return (
|
||||
`if-shell -F "#{==:#{client_tty},${tty}}" ` +
|
||||
`"set -Fg ${TUI_SWITCH_CLIENT_PID} \\"\\#{client_pid}\\"; set-hook -gu ${TUI_SWITCH_ATTACH_HOOK}"`
|
||||
);
|
||||
}
|
||||
|
||||
const LEGACY_TUI_SWITCH = /^switch-client -t (\S+?)(?: \\; set-window-option -t \1 window-size latest)?$/;
|
||||
|
||||
/**
|
||||
* A root binding this TUI installed: the guarded `if-shell` form, or the
|
||||
* pre-guard `switch-client -t <mux>` form a killed TUI can leave behind.
|
||||
* A user's own `switch-client -n` or a target that is not a Codeman session
|
||||
* is not ours.
|
||||
*/
|
||||
export function isTuiSwitchBinding(command: string): boolean {
|
||||
const text = command.trim();
|
||||
if (text.includes(TUI_SWITCH_CLIENT_PID) && text.includes('#{client_tty}') && text.includes('switch-client -t ')) {
|
||||
return true;
|
||||
}
|
||||
const legacy = LEGACY_TUI_SWITCH.exec(text);
|
||||
return legacy !== null && MUX_NAME_PATTERN.test(legacy[1] ?? '');
|
||||
}
|
||||
|
||||
/** The tty embedded in a guarded switch binding, or null for a legacy one. */
|
||||
export function tuiSwitchBindingTty(command: string): string | null {
|
||||
const match = /#\{==:#\{client_tty\},(\/dev\/(?:pts\/[0-9]+|ttys[0-9]+))\}/.exec(command);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The phrase the attach status bar carries. It doubles as the marker that tells
|
||||
* OUR bar apart from a user's own when sweeping one that leaked, so
|
||||
@@ -959,21 +1039,25 @@ export class TuiClient {
|
||||
* `switch-client` rather than detach-then-attach: it keeps the terminal, so
|
||||
* the move is instant and the TUI stays blocked in its `spawnSync` exactly as
|
||||
* before, still holding the restore it owes.
|
||||
*
|
||||
* The binding is NOT server-wide in effect. Key tables are, so the key is
|
||||
* bound for every client, but the command is `if-shell -F` and runs only
|
||||
* when the client that pressed it is this TUI (`clientTty` plus the pid the
|
||||
* attach hook stored). A browser tab's Esc-then-digit, which tmux reads as
|
||||
* the same `M-<digit>` inside `escape-time`, does not match. An unsafe tty
|
||||
* refuses the bind rather than falling back to an unscoped `switch-client`.
|
||||
*
|
||||
* The two commands live in the if-shell argument, where `;` is a separator
|
||||
* inside that string. A bare `;` as its own argv element would end the
|
||||
* `bind-key` and run `window-size latest` immediately, which is the bug the
|
||||
* old `\;` argument existed to avoid. The second command is `window-size
|
||||
* latest`: the window is already the right size on arrival (see
|
||||
* presizeWindow), so it changes nothing then, and a resize while sitting in
|
||||
* that session follows this terminal.
|
||||
*/
|
||||
async bindSwitchKey(key: string, target: string): Promise<boolean> {
|
||||
if (!MUX_NAME_PATTERN.test(target)) return false;
|
||||
async bindSwitchKey(key: string, target: string, clientTty: string): Promise<boolean> {
|
||||
if (!MUX_NAME_PATTERN.test(target) || !isTuiClientTty(clientTty)) return false;
|
||||
try {
|
||||
// Two commands: go there, then let that window follow this terminal again.
|
||||
// It is already the right size (see presizeWindow), so `latest` changes
|
||||
// nothing on arrival and costs no repaint — it matters only if the
|
||||
// terminal is resized while sitting in that session.
|
||||
//
|
||||
// ⚠️ The separator MUST be an escaped `\;`. A bare `;` is a command
|
||||
// separator to tmux's own parser, so it ends the `bind-key` and RUNS
|
||||
// what follows immediately: the binding kept only `switch-client`, and
|
||||
// every session got `window-size latest` executed on it at attach time.
|
||||
// That is what left sessions on `latest` after a detach, since the
|
||||
// snapshot was then taken from already-corrupted state.
|
||||
await this.exec('tmux', [
|
||||
'-L',
|
||||
this.socket,
|
||||
@@ -981,15 +1065,10 @@ export class TuiClient {
|
||||
'-T',
|
||||
'root',
|
||||
key,
|
||||
'switch-client',
|
||||
'-t',
|
||||
target,
|
||||
'\\;',
|
||||
'set-window-option',
|
||||
'-t',
|
||||
target,
|
||||
'window-size',
|
||||
'latest',
|
||||
'if-shell',
|
||||
'-F',
|
||||
tuiSwitchGuard(clientTty),
|
||||
`switch-client -t ${target} ; set-window-option -t ${target} window-size latest`,
|
||||
]);
|
||||
return true;
|
||||
} catch {
|
||||
@@ -997,10 +1076,88 @@ export class TuiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/** Give back a switch key, but only while it still points at a session. */
|
||||
async unbindSwitchKey(key: string): Promise<void> {
|
||||
/**
|
||||
* Record this TUI's client pid when it attaches, then drop the hook so a
|
||||
* later client on a reused pty does not take the binding over. Best effort:
|
||||
* a failure means Alt+1..9 do nothing, which is the closed direction.
|
||||
*/
|
||||
async armTuiSwitchGuard(clientTty: string): Promise<boolean> {
|
||||
if (!isTuiClientTty(clientTty)) return false;
|
||||
try {
|
||||
await this.exec('tmux', [
|
||||
'-L',
|
||||
this.socket,
|
||||
'set-hook',
|
||||
'-g',
|
||||
TUI_SWITCH_ATTACH_HOOK,
|
||||
tuiSwitchAttachHook(clientTty),
|
||||
]);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo `armTuiSwitchGuard` for this tty. The hook is removed only while it
|
||||
* still names `clientTty` (a second TUI may have replaced it). The pid option
|
||||
* is removed only when no live client still has it, so a second attach that
|
||||
* already recorded its own pid is left alone.
|
||||
*/
|
||||
async disarmTuiSwitchGuard(clientTty: string): Promise<void> {
|
||||
if (!isTuiClientTty(clientTty)) return;
|
||||
try {
|
||||
const { stdout } = await this.exec('tmux', ['-L', this.socket, 'show-hooks', '-g']);
|
||||
const hook = stdout.split('\n').find((line) => line.startsWith(`${TUI_SWITCH_ATTACH_HOOK} `));
|
||||
if (hook?.includes(clientTty)) {
|
||||
await this.exec('tmux', ['-L', this.socket, 'set-hook', '-gu', TUI_SWITCH_ATTACH_HOOK]);
|
||||
}
|
||||
} catch {
|
||||
/* no server, or the hook is already gone */
|
||||
}
|
||||
let pid = '';
|
||||
try {
|
||||
const { stdout } = await this.exec('tmux', ['-L', this.socket, 'show-options', '-gv', TUI_SWITCH_CLIENT_PID]);
|
||||
pid = stdout.trim();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!pid) return;
|
||||
let live = '';
|
||||
try {
|
||||
({ stdout: live } = await this.exec('tmux', ['-L', this.socket, 'list-clients', '-F', '#{client_pid}']));
|
||||
} catch {
|
||||
live = '';
|
||||
}
|
||||
const pids = new Set(
|
||||
live
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line.length > 0)
|
||||
);
|
||||
if (pids.has(pid)) return;
|
||||
try {
|
||||
await this.exec('tmux', ['-L', this.socket, 'set-option', '-gu', TUI_SWITCH_CLIENT_PID]);
|
||||
} catch {
|
||||
/* already unset */
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Give back a switch key, but only while it is still one of ours.
|
||||
*
|
||||
* `ownerTty` set: keep a binding that names a different tty, so this attach's
|
||||
* restore does not take a second TUI's keys. `ownerTty` omitted: any of our
|
||||
* bindings on the key goes (the attaching TUI is taking the table over).
|
||||
* Pass `''` to drop only a legacy unscoped binding.
|
||||
*/
|
||||
async unbindSwitchKey(key: string, ownerTty?: string): Promise<void> {
|
||||
const bound = await this.readPrefixBinding(key, 'root');
|
||||
if (!bound || !bound.startsWith('switch-client')) return;
|
||||
if (!bound || !isTuiSwitchBinding(bound)) return;
|
||||
if (ownerTty !== undefined) {
|
||||
const boundTty = tuiSwitchBindingTty(bound);
|
||||
if (boundTty !== null && boundTty !== ownerTty) return;
|
||||
}
|
||||
try {
|
||||
await this.exec('tmux', ['-L', this.socket, 'unbind-key', '-T', 'root', key]);
|
||||
} catch {
|
||||
|
||||
Reference in new issue
Block a user