fix(tui): let GNU screen veto tmux hyperlinks

STY is GNU screen's explicit session marker and can coexist with TMUX in
nested multiplexer sessions. The previous ordering let tmux >= 3.4 enable
OSC 8 before screen was considered, even though a screen layer anywhere in
the path cannot forward OSC 8.

Move the STY veto ahead of the tmux version gate while keeping TERM=screen*
under TMUX eligible for the tmux gate, preserving support for tmux's
historical default-terminal=screen-256color. Add regression coverage for
STY + TMUX + TERM_PROGRAM_VERSION=3.4/3.5a resolving false.

Reported on #2404 review.
This commit is contained in:
roboomp
2026-06-12 16:39:46 +00:00
parent 459264a96e
commit 33cc59f532
2 changed files with 42 additions and 9 deletions
+16 -9
View File
@@ -280,7 +280,9 @@ function parseTmuxVersionFromEnv(env: NodeJS.ProcessEnv): { major: number; minor
* on). Opt-out wins ties.
* 2. Static terminal capability — terminals whose {@link TerminalInfo} marks
* `hyperlinks: false` (e.g. `base`) stay off unless the user forced on.
* 3. tmux session (`TMUX` set): enabled when tmux self-reports >= 3.4 via
* 3. GNU screen's explicit session marker (`STY`) always off, even if tmux is
* also present: a screen layer anywhere in the path cannot forward OSC 8.
* 4. tmux session (`TMUX` set): enabled when tmux self-reports >= 3.4 via
* `TERM_PROGRAM_VERSION` (tmux 3.4 stores OSC 8 as a cell attribute and
* forwards it to outer terminals whose `terminal-features` include
* `hyperlinks`). Older or unknown versions stay off; on outer terminals
@@ -288,11 +290,11 @@ function parseTmuxVersionFromEnv(env: NodeJS.ProcessEnv): { major: number; minor
* identical to today. Checked before the screen-family TERM heuristic
* because tmux's historical `default-terminal` is `screen-256color`, so
* `TERM=screen*` inside a tmux session must NOT short-circuit to off.
* 4. GNU screen (`STY` set, or screen-family TERM without `TMUX`) always off:
* screen never gained OSC 8 support.
* 5. tmux-family TERM without `TMUX` env — unusual (e.g. inspection scripts);
* 5. screen-family TERM without `TMUX` always off: screen never gained OSC 8
* support.
* 6. tmux-family TERM without `TMUX` env — unusual (e.g. inspection scripts);
* no version available, so off.
* 6. Otherwise honor the static terminal capability.
* 7. Otherwise honor the static terminal capability.
*/
export function shouldEnableHyperlinksByDefault(
env: NodeJS.ProcessEnv = Bun.env,
@@ -303,9 +305,14 @@ export function shouldEnableHyperlinksByDefault(
if (!getTerminalInfo(terminalId).hyperlinks) return false;
// tmux check first: TMUX is the authoritative current-session signal and
// supersedes TERM, which may be `screen-256color` under tmux's historical
// default-terminal setting.
// STY is GNU screen's explicit session marker. It vetoes tmux enabling when
// multiplexers are nested because screen cannot forward OSC 8 anywhere in the
// path.
if (env.STY) return false;
// tmux check before TERM heuristics: TMUX is the authoritative current-session
// signal and supersedes TERM, which may be `screen-256color` under tmux's
// historical default-terminal setting.
if (env.TMUX) {
const version = parseTmuxVersionFromEnv(env);
if (!version) return false;
@@ -313,7 +320,7 @@ export function shouldEnableHyperlinksByDefault(
}
const term = env.TERM?.toLowerCase() ?? "";
if (env.STY || term.startsWith("screen")) return false;
if (term.startsWith("screen")) return false;
if (term.startsWith("tmux")) return false;
return true;
@@ -167,6 +167,32 @@ describe("shouldEnableHyperlinksByDefault", () => {
).toBe(false);
});
it("lets GNU screen's STY marker veto tmux enabling in nested multiplexer sessions", () => {
expect(
shouldEnableHyperlinksByDefault(
{
STY: "1234.pts-0.host",
TMUX: "/tmp/tmux-1000/default,1,0",
TERM_PROGRAM: "tmux",
TERM_PROGRAM_VERSION: "3.4",
},
"wezterm",
),
).toBe(false);
expect(
shouldEnableHyperlinksByDefault(
{
STY: "1234.pts-0.host",
TMUX: "/tmp/tmux-1000/default,1,0",
TERM: "screen-256color",
TERM_PROGRAM: "tmux",
TERM_PROGRAM_VERSION: "3.5a",
},
"kitty",
),
).toBe(false);
});
it("keeps tmux off when no version is reported (old tmux without TERM_PROGRAM_VERSION)", () => {
expect(shouldEnableHyperlinksByDefault({ TMUX: "/tmp/tmux-1000/default,1,0" }, "wezterm")).toBe(false);
expect(shouldEnableHyperlinksByDefault({ TERM: "tmux-256color" }, "wezterm")).toBe(false);