diff --git a/server/__tests__/pty-attach.test.js b/server/__tests__/pty-attach.test.js index 8c65654..dc08a76 100644 --- a/server/__tests__/pty-attach.test.js +++ b/server/__tests__/pty-attach.test.js @@ -5,10 +5,11 @@ * or PTY spawned). * @author Nguyễn Ngọc Trí Vĩ */ -const { describe, it, beforeEach } = require("node:test"); +const { describe, it, beforeEach, afterEach } = require("node:test"); const assert = require("node:assert/strict"); const { EventEmitter } = require("node:events"); const ptyAttach = require("../lib/pty-attach"); +const tmux = require("../lib/tmux"); function makeFakePty() { const emitter = new EventEmitter(); @@ -41,6 +42,10 @@ describe("pty-attach", () => { beforeEach(() => { fakePty = makeFakePty(); ptyAttach.__setSpawnImpl(() => fakePty); + tmux.__setExecImpl(() => ""); + }); + afterEach(() => { + tmux.__reset(); }); it("rejects a runId that doesn't match ccam-lane-", () => { @@ -52,6 +57,25 @@ describe("pty-attach", () => { assert.doesNotThrow(() => ptyAttach.validateRunId("ccam-lane-42")); }); + it("turns tmux mouse mode on for the session it attaches to", () => { + const calls = []; + tmux.__setExecImpl((args) => { + calls.push(args); + return ""; + }); + ptyAttach.attach(makeFakeWs(), "ccam-lane-1", { cols: 80, rows: 24 }); + assert.deepEqual(calls[0], ["set-option", "-t", "ccam-lane-1", "mouse", "on"]); + }); + + it("still attaches even if the mouse-mode set-option call fails", () => { + tmux.__setExecImpl(() => { + throw new Error("no such session: ccam-lane-1"); + }); + assert.doesNotThrow(() => + ptyAttach.attach(makeFakeWs(), "ccam-lane-1", { cols: 80, rows: 24 }) + ); + }); + it("wires PTY data to binary WS frames and WS binary frames to PTY writes", () => { const ws = makeFakeWs(); ptyAttach.attach(ws, "ccam-lane-1", { cols: 80, rows: 24 }); diff --git a/server/lib/pty-attach.js b/server/lib/pty-attach.js index 1d8fe34..60cfc0d 100644 --- a/server/lib/pty-attach.js +++ b/server/lib/pty-attach.js @@ -10,6 +10,8 @@ * @author Nguyễn Ngọc Trí Vĩ */ +const tmux = require("./tmux"); + const RUN_ID_RE = /^ccam-lane-\d+$/; /** @@ -38,6 +40,14 @@ function __setSpawnImpl(fn) { */ function attach(ws, runId, { cols, rows }) { validateRunId(runId); + // Retroactively covers sessions started before mouse mode was wired into + // newSession (or before a server restart picked that change up) — see + // tmux.js's enableMouse. + try { + tmux.enableMouse(runId); + } catch { + /* session gone or tmux unavailable — attach below will surface that */ + } const pty = spawnImpl("tmux", ["attach-session", "-t", runId], { name: "xterm-256color", cols: cols || 80, diff --git a/server/lib/tmux.js b/server/lib/tmux.js index b6fe237..677b470 100644 --- a/server/lib/tmux.js +++ b/server/lib/tmux.js @@ -40,10 +40,17 @@ function hasSession(name) { */ function newSession({ name, cwd, argv }) { execImpl(["new-session", "-d", "-s", name, "-c", cwd, "--", ...argv]); - // Without this the pane has no scrollbar/drag scroll at all — wheel events - // just pass through to the running program instead of entering tmux's own - // copy-mode scrollback. xterm.js forwards tmux's mouse-tracking escapes - // automatically once mouse mode is on, so no client-side change is needed. + enableMouse(name); +} + +// Without this the pane has no scrollbar/drag scroll at all — wheel events +// just pass through to the running program instead of entering tmux's own +// copy-mode scrollback. xterm.js forwards tmux's mouse-tracking escapes +// automatically once mouse mode is on, so no client-side change is needed. +// Called on every attach (not just at creation) so a session started before +// this option existed — or before a server restart picked up the change — +// still gets it; `set-option` is idempotent, so re-running it is harmless. +function enableMouse(name) { execImpl(["set-option", "-t", name, "mouse", "on"]); } @@ -112,6 +119,7 @@ function isTmuxAvailable() { module.exports = { hasSession, newSession, + enableMouse, paneCommand, sendCommand, killSession,