diff options
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 178 |
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" { |
