diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-28 10:12:55 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-28 10:30:44 -0300 |
| commit | 7df45a1586568476fb7f3fc5ffe8d644bbe13c93 (patch) | |
| tree | e7a602ee30ae432d99a602b065b2d411fb6e8312 | |
| parent | 337c2c66f40a08b67aa7ce4c73d01c335b9ae829 (diff) | |
| download | pardes-7df45a1586568476fb7f3fc5ffe8d644bbe13c93.tar.gz pardes-7df45a1586568476fb7f3fc5ffe8d644bbe13c93.zip | |
gui: the headless grid host keeps the core's own clipboard, so paste works there
`Shell.host()` hands the headless `PARDES_TEST_GRID` harness `grid_vtable`, which
inherited `push_set_clipboard` and `pull_read_clipboard` from the windowed
vtable. Both of those functions opened with `if (s.gui == null) return;` — and
in grid mode `s.gui` is null by definition, so the pair were methods that could
never answer. host.zig says what a NULL method means in as many words: "Null
answers immediately from the in-process clipboard instead, so a request never
goes unanswered." A method present and mute is the one case that contract does
not cover: `SPC y` went nowhere and `SPC p` waited for a reply nobody was going
to send, so paste was dead in the only mode of this shell a test can drive —
which is also why the SDL shell's clipboard had no coverage at all.
Null them, and the grid harness runs on the same in-process clipboard
`pardes-isolate` does. The two SDL functions then have no reachable
`gui == null` path left, so the dead guards go with them.
The regression is the round trip through the real seam: build the Shell the
harness builds, take the host off `Shell.host()`, assert it picked
`grid_vtable`, then `SPC y` and `SPC p` and check the bytes came back. It fails
on the old vtable at `clip_pending == null` — the paste that never arrived.
Verified end to end as well: `PARDES_TEST_GRID` with `x`, `SPC y`, `SPC p`
duplicates the line and marks the buffer dirty; the shipped binary does
nothing. The windowed shell is unchanged and still reads the desktop
clipboard (checked on wayland and x11, keys injected at evdev level, against
a real focused window).
| -rw-r--r-- | src/gui/gui.zig | 90 |
1 files changed, 84 insertions, 6 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index f7c4c51c..bfd02ff9 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -3599,12 +3599,24 @@ const Shell = struct { /// `wait_input` here, and this process never sleeps in grid mode. It starts /// neither the watcher nor the nested listener, so `drainQueue`'s /// `files_changed` and `command` arms cannot fire behind it. + /// + /// And it has NO SDL: `SDL_Init` is never called on this path, so the two + /// desktop-clipboard methods are nulled rather than left pointing at + /// functions that cannot answer. A null `pull_read_clipboard` is not a + /// missing feature, it is the core's OWN clipboard (host.zig: "Null + /// answers immediately from the in-process clipboard instead, so a request + /// never goes unanswered") — the same one `pardes-isolate` runs on. With + /// the methods present and returning on `s.gui == null`, `SPC y` went + /// nowhere and `SPC p` was answered by nobody, so paste was dead in the + /// one mode of this shell a test can drive. const grid_vtable: pardes.Host.VTable = vt: { var v = vtable; v.pull_wait_input = null; v.push_poll_frame = gridPollFrame; v.push_present = gridPresent; v.push_post_present = gridPostPresent; + v.push_set_clipboard = null; + v.pull_read_clipboard = null; break :vt v; }; @@ -4019,20 +4031,86 @@ fn takeClipboard() ?[:0]u8 { return text; } +/// Both of these are only ever reached through `Shell.vtable`, which +/// `Shell.host` installs only when there IS a window: the headless grid +/// harness nulls them and keeps the core's own clipboard. So neither needs a +/// `s.gui == null` guard, and neither may have one — a method that returns +/// without answering is exactly what left `SPC p` unanswered in grid mode. fn setClipboard(ctx: ?*anyopaque, text: []const u8) void { - const s = shellOf(ctx); - if (s.gui == null) return; - putClipboard(s.gpa, text); + putClipboard(shellOf(ctx).gpa, text); } fn readClipboard(ctx: ?*anyopaque) void { - const s = shellOf(ctx); - if (s.gui == null) return; // SDL answers synchronously, so the paste the core is waiting on lands // inside this same drain — nothing to remember, no reply path to plumb. const text = takeClipboard() orelse return; defer c.SDL_free(text.ptr); - s.core.update(.{ .paste = text }); + shellOf(ctx).core.update(.{ .paste = text }); +} + +test "the headless grid host round-trips a yank back as a paste" { + // The GUI shell's testable mode, driven through the SAME host the grid + // harness installs — `Shell.host()` picks `grid_vtable` off `gui == null`, + // so this is the real seam and not a hand-built one. Before the two + // clipboard methods were nulled, `SPC y` reached a function that returned + // on `gui == null` and `SPC p` was answered by nobody: the content below + // never changed, in the one mode of this file a test can run. + const gpa = std.testing.allocator; + const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24, .file = "mise.toml" }); + defer core.deinit(); + core.update(.{ .resize = .{ .cols = 80, .rows = 24 } }); + + // Everything a Shell needs that this path never touches, at its zero + // value; `io` alone is undefined, because a clipboard is not IO the + // std.Io interface knows about and no arm reached here reads it. + var ptys: [pardes.MAX_PANES]?Pty = @splat(null); + var gens: [pardes.MAX_PANES]u32 = @splat(0); + var lsp_workers: LspWorkers = .{}; + var queue: Queue = .{ + .gpa = gpa, + .lsp_allocator = gpa, + .lsp_workers = &lsp_workers, + .sdl_wake = false, + }; + var pipe_tasks: PipeTasks = .{}; + var watches: file_watch.Table = @splat(null); + var prompt_rcs = shell_bin.PromptRcs.init(); + defer prompt_rcs.deinit(); + var shell: Shell = .{ + .core = core, + .gui = null, + .io = undefined, + .gpa = gpa, + .lsp_allocator = gpa, + .prompt_rcs = &prompt_rcs, + .ptys = &ptys, + .gens = &gens, + .queue = &queue, + .pipe_tasks = &pipe_tasks, + .inotify_fd = -1, + .watches = &watches, + }; + core.host = shell.host(); + try std.testing.expect(core.host.vtable == &Shell.grid_vtable); + + // `SPC y`: the selection to the system clipboard. Headless, "the system" + // is the core's in-process one. + const pane = core.panes[0].?; + core.update(.{ .key = .{ .cp = ' ' } }); + core.update(.{ .key = .{ .cp = 'y' } }); + while (core.nextEffect()) |e| core.perform(e); + const yanked = core.yank orelse return error.MissingYank; + try std.testing.expect(yanked.len > 0); + + // ...and `SPC p` gets it back, as an ordinary paste event, inside the + // drain. A host that cannot answer leaves the file exactly as it was. + const before = pane.file.?.content.len; + core.update(.{ .key = .{ .cp = ' ' } }); + core.update(.{ .key = .{ .cp = 'p' } }); + try std.testing.expect(core.clip_pending != null); + while (core.nextEffect()) |e| core.perform(e); + try std.testing.expect(core.clip_pending == null); + try std.testing.expectEqual(before + yanked.len, pane.file.?.content.len); } fn openLink(ctx: ?*anyopaque, url: []const u8) void { |
