summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-14 21:26:47 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commita624a56e289e4a02a411f83741f0f2c9fc0f0b2e (patch)
treeb87e3ca10dd1c4417112164f792b5da1e26de0a6 /src/pardes.zig
parent95681ff7017b8a9e4c8f9fa6a7371d1233432f2f (diff)
downloadpardes-a624a56e289e4a02a411f83741f0f2c9fc0f0b2e.tar.gz
pardes-a624a56e289e4a02a411f83741f0f2c9fc0f0b2e.zip
Add Linux Tty9p mounted terminals and forward raw TTY keys
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig178
1 files changed, 113 insertions, 65 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 35080163..5ab4dcb9 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1361,6 +1361,25 @@ test "Tty spawns a raw shell in the caller's directory" {
try std.testing.expect(found);
}
+test "Tty9p marks only the new Linux terminal for a mounted shell" {
+ if (comptime !hosted or @import("builtin").os.tag != .linux) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
+ defer p.deinit();
+ p.fs.socket_path = "/tmp/pardes-test.sock";
+ try std.testing.expect(p.executeBuiltinLine(0, "Tty9p"));
+ try std.testing.expect(p.active != 0);
+ try std.testing.expect(!p.panes[0].?.v9fs_on_spawn);
+ const pane = p.panes[p.active].?;
+ try std.testing.expect(pane.isTerminal() and pane.mode == .tty);
+ try std.testing.expect(pane.v9fs_on_spawn);
+ try std.testing.expect(!pane.greet);
+ try std.testing.expect(!p.takesCommandLine(p.active));
+ panes.Terminal.feedOutput(p, pane, "\x1b]133;A\x07$ \x1b]133;B\x07");
+ try std.testing.expect(p.takesCommandLine(p.active));
+ panes.Terminal.feedOutput(p, pane, "\x1b]133;C\x07");
+ try std.testing.expect(!p.takesCommandLine(p.active));
+}
+
test "first document fallback splits like Tty from the originating pane" {
for ([_]u16{ 100, 198 }) |width| for (0..3) |from| {
for ([_]bool{ false, true }) |collapsed| {
@@ -6200,6 +6219,10 @@ pub const Pardes = struct {
fn takesCommandLine(p: *const Pardes, id: usize) bool {
const pane = p.panes[id] orelse return false;
if (!pane.isTerminal()) return false;
+ // A mounted shell sits behind sudo's process supervisor. Its OSC 133
+ // prompt marks identify input readiness; the launcher's executable is
+ // not the shell executable used by the ordinary process heuristic.
+ if (pane.v9fs_on_spawn) return panes.Terminal.promptInputReady(pane);
return !p.hostTtyTaken(id);
}
@@ -7696,6 +7719,15 @@ pub const Pardes = struct {
fn handleKey(p: *Pardes, key: Key) void {
if (p.topbar_col != null) return p.topbarKey(key);
const pane = p.panes[p.active] orelse return;
+ if (pane.isTerminal() and pane.mode == .tty and !pane.tag_edit) {
+ p.leader_on = false;
+ p.ctrl_w_pending = false;
+ if (key.ctrl and !key.alt and key.cp == p.opts.tty_toggle)
+ return p.toggleTty(p.active);
+ if (key.cp == Key.escape and !key.ctrl and !key.alt and !key.shift and p.takesCommandLine(p.active))
+ return p.runBuiltin(.Last, p.active, "", null);
+ return panes.Terminal.forwardKey(p, p.active, key);
+ }
if (p.leader_on) return p.leaderKey(key);
if (p.ctrl_w_pending) {
p.ctrl_w_pending = false;
@@ -7705,7 +7737,7 @@ pub const Pardes = struct {
return;
}
// insert mode owns Ctrl-w (delete word backward, helix); the focus
- // prefix keeps normal/tty
+ // prefix applies outside raw tty mode
if (hit(key, config.window_prefix) and pane.mode != .insert) {
p.ctrl_w_pending = true;
return;
@@ -7736,18 +7768,8 @@ pub const Pardes = struct {
const tty_alt = hit(key, config.tty_toggle_alt);
const tty_toggle = (key.ctrl and key.cp == p.opts.tty_toggle) or tty_alt;
if (pane.isTerminal() and tty_toggle) {
- if (pane.mode == .tty and tty_alt) return p.runBuiltin(.Last, p.active, "", null);
return p.toggleTty(p.active);
}
- // A shell prompt is a pane you can leave: plain Esc there is Shift-Esc.
- if (pane.isTerminal() and pane.mode == .tty and hit(key, config.escape) and p.takesCommandLine(p.active))
- return p.runBuiltin(.Last, p.active, "", null);
- if (pane.isTerminal() and pane.mode == .tty) {
- // Clipboard first: `hit` ignores a shift no binding asked for, so
- // the plain Ctrl-V below would otherwise swallow Ctrl-Shift-V.
- if (hit(key, config.tty_paste_clipboard)) return p.clipRequest(p.active, .after);
- if (hit(key, config.tty_paste)) return p.typeToTty(p.active, pane, p.yank orelse return);
- }
if (pane.prompt != .none and (hit(key, config.search_submit) or hit(key, config.escape))) {
const submit = hit(key, config.search_submit);
switch (pane.prompt) {
@@ -11287,10 +11309,23 @@ pub const Pardes = struct {
/// Tty: a shell in the caller's directory, raw from the first frame,
/// stacked below the caller like Alt-n's new shell.
pub fn spawnTty(p: *Pardes, from: usize) void {
+ p.spawnTtyWithMount(from, false);
+ }
+
+ pub fn spawnV9fsTty(p: *Pardes, from: usize) void {
+ if (comptime !hosted or @import("builtin").os.tag != .linux)
+ return p.reportError(from, "Tty9p", error.LinuxRequired);
+ if (p.fs.socket_path.len == 0)
+ return p.reportError(from, "Tty9p", error.SessionSocketRequired);
+ p.spawnTtyWithMount(from, true);
+ }
+
+ fn spawnTtyWithMount(p: *Pardes, from: usize, v9fs: bool) void {
const src = p.panes[from] orelse return;
const free = p.freeSlot() orelse return;
const nt = p.newShell(free, paneDir(src)) catch return;
- nt.greet = true;
+ nt.greet = !v9fs;
+ nt.v9fs_on_spawn = v9fs;
nt.mode = .tty;
const parent = layout.splitParent(p, from);
const f = layout.findPane(p, parent).?;
@@ -13181,7 +13216,7 @@ test "Esc back into a tty leaves its view at the prompt" {
const live = sp.terminal.?.vt.screens.active.pages.scrollbar().offset;
try std.testing.expect(live > 0);
- p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); // out to the doc
+ p.update(.{ .key = .{ .cp = Key.escape } }); // shell-prompt heuristic: out to the doc
try std.testing.expect(p.active != shell);
p.update(.{ .key = .{ .cp = Key.escape } }); // ...and back in
p.sync();
@@ -13234,33 +13269,42 @@ test "Esc back into a file leaves its view where it was" {
try std.testing.expectEqual(@as(usize, @intCast(row)) -| @as(usize, dp.rows) / 2, dp.file.?.scroll);
}
-test "Shift-Esc in tty hops to the doc and leaves the shell in tty" {
+test "raw tty Escape only leaves at a shell prompt" {
if (platform == .web) return;
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .cols = 80, .rows = 24, .file = "mise.toml" });
+ const p = try Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24, .file = "mise.toml" });
defer p.deinit();
- p.update(.{ .resize = .{ .cols = 80, .rows = 24 } });
- p.update(.{ .key = .{ .cp = 'n', .alt = true } }); // a shell under the doc
+ p.spawnTty(0);
const shell = p.active;
- const shell_pane = p.panes[shell].?;
- try std.testing.expect(shell_pane.isTerminal());
-
- // Shift-Esc still gets you IN, exactly as the configured Ctrl-key does.
- const shift_esc: Key = .{ .cp = Key.escape, .shift = true };
- p.update(.{ .key = shift_esc });
- try std.testing.expectEqual(Pane.Mode.tty, shell_pane.mode);
-
- p.update(.{ .key = shift_esc });
- try std.testing.expect(p.active != shell);
- try std.testing.expect(!p.panes[p.active].?.isTerminal());
- try std.testing.expectEqual(Pane.Mode.tty, shell_pane.mode);
+ const pane = p.panes[shell].?;
+ var buf: [256]u8 = undefined;
+ _ = drainWrites(p, &buf);
+ for ([_]Key{
+ .{ .cp = Key.escape, .shift = true },
+ }) |key| {
+ p.update(.{ .key = key });
+ try std.testing.expectEqual(shell, p.active);
+ try std.testing.expectEqual(Pane.Mode.tty, pane.mode);
+ try std.testing.expect(drainWrites(p, &buf).len > 0);
+ }
+ p.update(.{ .key = .{ .cp = Key.escape } });
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+ try std.testing.expectEqualStrings("", drainWrites(p, &buf));
+ try std.testing.expectEqual(Pane.Mode.tty, pane.mode);
+}
- // The configured Ctrl-key is now the only thing that leaves tty in place.
- p.update(.{ .key = .{ .cp = Key.escape, .shift = true } }); // back to the shell
- try std.testing.expectEqual(shell, p.active);
- p.update(.{ .key = .{ .cp = p.opts.tty_toggle, .ctrl = true } });
- try std.testing.expectEqual(Pane.Mode.normal, shell_pane.mode);
+test "raw tty Ctrl-B switches to editor mode without sending child input" {
+ if (platform == .web) return;
+ const p = try Pardes.init(std.testing.allocator, .{ .file = "mise.toml" });
+ defer p.deinit();
+ p.spawnTty(0);
+ const shell = p.active;
+ p.sync();
+ var buf: [256]u8 = undefined;
+ _ = drainWrites(p, &buf);
+ p.update(.{ .key = .{ .cp = 'b', .ctrl = true } });
try std.testing.expectEqual(shell, p.active);
+ try std.testing.expectEqual(Pane.Mode.normal, p.panes[shell].?.mode);
+ try std.testing.expectEqualStrings("", drainWrites(p, &buf));
}
test "hopping between two panes does not grow the jump stack" {
@@ -13387,41 +13431,45 @@ fn drainWrites(p: *Pardes, buf: []u8) []const u8 {
return buf[0..n];
}
-test "Ctrl-V and Ctrl-Shift-V paste into the program a tty pane is running" {
+test "raw tty forwards Ctrl letters and former global shortcuts to the child" {
if (platform == .web) return;
- const gpa = std.testing.allocator;
- const p = try Pardes.init(gpa, .{ .tty_only = true, .cols = 80, .rows = 24 });
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
defer p.deinit();
- p.update(.{ .resize = .{ .cols = 80, .rows = 24 } });
var buf: [256]u8 = undefined;
_ = drainWrites(p, &buf);
-
- const pane = p.panes[0].?;
- panes.Terminal.enterTty(p, 0);
- try std.testing.expectEqual(Pane.Mode.tty, pane.mode);
- p.setYank("one\ntwo");
-
- p.update(.{ .key = .{ .cp = 'v', .ctrl = true } });
- try std.testing.expectEqualStrings("one\rtwo", drainWrites(p, &buf));
- // and it asked the desktop for nothing on the way
- try std.testing.expect(p.clip_pending == null);
-
- // Under mode 2004 the same keystroke brackets instead, which is what stops
- // readline from RUNNING a multi-line paste.
- pane.terminal.?.vt.modes.set(.bracketed_paste, true);
- p.update(.{ .key = .{ .cp = 'v', .ctrl = true } });
- try std.testing.expectEqualStrings("\x1b[200~one\ntwo\x1b[201~", drainWrites(p, &buf));
-
- // Ctrl-Shift-V is the other store: it ASKS, types nothing yet, and the
- // answer lands at the program rather than in an edit buffer.
- pane.terminal.?.vt.modes.set(.bracketed_paste, false);
+ p.setYank("must not be pasted");
+ for ('a'..'z' + 1) |letter| {
+ if (letter == p.opts.tty_toggle) continue; // editor/TTY mode shortcut
+ p.update(.{ .key = .{ .cp = @intCast(letter), .ctrl = true } });
+ const expected = [_]u8{@intCast(letter - 'a' + 1)};
+ // The terminal encoder distinguishes Ctrl-I/M from Tab/Return.
+ const encoded: []const u8 = switch (letter) {
+ 'i' => "\x1b[105;5u",
+ 'm' => "\x1b[109;5u",
+ else => &expected,
+ };
+ try std.testing.expectEqualStrings(encoded, drainWrites(p, &buf));
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+ try std.testing.expectEqual(Pane.Mode.tty, p.panes[0].?.mode);
+ try std.testing.expect(!p.ctrl_w_pending and p.clip_pending == null);
+ }
p.update(.{ .key = .{ .cp = 'v', .ctrl = true, .shift = true } });
- try std.testing.expect(p.clip_pending != null);
- try std.testing.expectEqualStrings("", drainWrites(p, &buf));
- p.update(.{ .paste = "from-desktop" });
- try std.testing.expectEqualStrings("from-desktop", drainWrites(p, &buf));
- // the clipboard did not clobber the register on its way through
- try std.testing.expectEqualStrings("one\ntwo", p.yank orelse "");
+ try std.testing.expectEqualStrings("\x1b[118;6u", drainWrites(p, &buf));
+ try std.testing.expect(p.clip_pending == null);
+ for ([_]u8{ 'n', 'c' }) |letter| {
+ p.update(.{ .key = .{ .cp = letter, .alt = true } });
+ const expected = [_]u8{ 0x1b, letter };
+ try std.testing.expectEqualStrings(&expected, drainWrites(p, &buf));
+ try std.testing.expectEqual(@as(usize, 0), p.active);
+ try std.testing.expect(p.panes[1] == null);
+ }
+ p.update(.{ .key = .{ .cp = Key.up } });
+ try std.testing.expectEqualStrings("\x1b[A", drainWrites(p, &buf));
+ p.panes[0].?.terminal.?.vt.modes.set(.cursor_keys, true);
+ p.update(.{ .key = .{ .cp = Key.up } });
+ try std.testing.expectEqualStrings("\x1bOA", drainWrites(p, &buf));
+ p.update(.{ .key = .{ .cp = Key.left, .ctrl = true } });
+ try std.testing.expectEqualStrings("\x1b[1;5D", drainWrites(p, &buf));
}
test "an unasked desktop paste reaches a tty pane's program, not its buffer" {