summaryrefslogtreecommitdiff
path: root/src/output_pane.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/output_pane.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/output_pane.zig')
-rw-r--r--src/output_pane.zig695
1 files changed, 0 insertions, 695 deletions
diff --git a/src/output_pane.zig b/src/output_pane.zig
deleted file mode 100644
index f4721136..00000000
--- a/src/output_pane.zig
+++ /dev/null
@@ -1,695 +0,0 @@
-//! Output panes: acme's +Errors, a file pane with no file behind it, holding
-//! text the core produced itself (+Search results, +Help, the LSP answer
-//! buffers). It IS a file pane — every mode, motion, chord and look works for
-//! free — and that reuse is the point.
-//!
-//! What it is NOT any more is a file pane with a bool on it. An output buffer
-//! remembers THE COMMAND THAT OPENED IT (`Origin`), and every special case it
-//! gets is one `traits` lookup on that field. Before this, a dozen places
-//! re-derived what a pane was from the outside, each asking a different wrong
-//! question: `endsWith(path, "+Search")` (the NAME decided what a pane WAS,
-//! which is backwards — a name is a consequence), a `search_kind` field on the
-//! pane that ran the search rather than on the buffer that answered it, and
-//! `f.output` booleans sprinkled through kind-agnostic layout code. Now the
-//! buffer knows, and the table below is the whole answer: one screen you read
-//! top to bottom to see every way an output pane differs from a file, and one
-//! row to add to introduce another kind.
-const std = @import("std");
-const pardes = @import("pardes.zig");
-const Pardes = pardes.Pardes;
-const Pane = pardes.Pane;
-const file_pane = @import("file_pane.zig");
-const modal = @import("modal.zig");
-const builtins = @import("builtins.zig");
-const runtime_config = @import("runtime_config.zig");
-const effect_sources = @import("effect_sources.zig");
-const Builtin = builtins.registry.Builtin();
-const config = @import("config.zig");
-const build_options = @import("pardes_config");
-const dump = @import("dump.zig");
-const lsp = @import("lsp/lsp.zig");
-const gui_shader_source_mode = effect_sources.guiShaderSourceMode();
-
-/// the installed fonts, for openFonts. GUI only, behind the same comptime
-/// branch builtins.zig imports it through — see the note there.
-const fonts = if (builtins.capabilities.font_picker) @import("fonts.zig") else struct {};
-
-/// What opened this buffer — THE field, and the only input to `traits`.
-///
-/// Two vocabularies because the core has exactly two: a `Builtin` is a word
-/// you can execute anywhere, and that covers Find, Grep, Help and every
-/// language query that has a name (Hover, Diagnostics...). The rest are KEYS —
-/// helix binds the five gotos and `=` as motions and `/` as a search input,
-/// and a motion has no word to click. Recording the key's `lsp.Kind` (or
-/// `.search` for the bare `/`) is not a parallel tag enum: both are the values
-/// the caller already holds when it opens the buffer.
-pub const Origin = union(enum) {
- cmd: Builtin,
- query: lsp.Kind,
- /// the bare `/` — the pane's own text, searched in core
- search,
- /// acme's `+Errors`: whatever a script wrote to a pane's `errors` file or
- /// to the top-level `cons`. Not a command at all — the third vocabulary
- /// is "somebody else's output", and it has no word to click because the
- /// writer is a process, not a keystroke.
- errors,
-};
-
-/// The exact command identity. A search prompt is bounded by the same one-line
-/// cap as a tag, so retaining that whole bound keeps refill and dump/restore
-/// identity exact without adding a per-output allocation.
-pub const max_arg = dump.max_origin_arg;
-
-/// An output buffer's own state, hung off `file_pane.State.output`.
-pub const Output = struct {
- from: Origin,
- /// the command's ARGUMENT: the pattern a Grep matched, the new name a
- /// Rename took, the SPC prefix a Help lists. Inline rather than allocated:
- /// tag input already enforces this exact cap.
- arg_buf: [max_arg]u8 = undefined,
- arg_len: u16 = 0,
-
- pub fn arg(o: *const Output) []const u8 {
- return o.arg_buf[0..o.arg_len];
- }
-};
-
-pub fn setArg(o: *Output, text: []const u8) error{ArgumentTooLong}!void {
- if (text.len > max_arg) return error.ArgumentTooLong;
- o.arg_len = @intCast(text.len);
- @memcpy(o.arg_buf[0..o.arg_len], text);
-}
-
-/// Every way an output pane differs from a file pane. Manual builtins already
-/// declare this exact row beside their implementation; use that schema here
-/// too instead of copying it into a parallel struct.
-pub const Traits = builtins.OutputTraits;
-
-/// A REAL file pane, as a row of the same table — so kind-agnostic code asks
-/// one question and gets one answer whichever it is holding. `name` is unused:
-/// a file already has a path.
-const file_row: Traits = .{ .name = "", .doc = true, .saves = true };
-
-/// THE TABLE. Everything above, answered from the command that opened the
-/// buffer. Exhaustive on purpose: a new `lsp.Kind` or a new output-opening
-/// builtin should not compile until someone has said what its buffer does.
-pub fn traits(o: Origin) Traits {
- return switch (o) {
- // rows are `location text`, so n/N walk them
- .search => .{ .name = config.search_buffer, .steps = true },
- // A transcript, not a list: rows are whatever a program printed, so
- // n/N walks its words like any prose buffer, and there is nothing to
- // Save — acme's +Errors is not a file either.
- .errors => .{ .name = config.errors_buffer, .doc = true },
- .cmd => |b| builtins.registry.outputTraits(b) orelse unreachable,
- .query => |k| switch (k) {
- .hover => .{ .name = config.hover_buffer },
- // prose: an action list, a diff, a report about the backend
- .code_action, .format, .status, .explain => .{ .name = config.lsp_buffer },
- // Rename usually resolves to edit records the core consumes before
- // any buffer opens; what RENDERS is the multi-file PREVIEW — one
- // location row per would-be edit — which n/N step like any list.
- .rename => .{ .name = config.search_buffer, .steps = true },
- .definition, .declaration, .type_definition, .implementation, .references => .{
- .name = config.search_buffer,
- .steps = true,
- .jumps = true,
- },
- // The hierarchy kinds behave like references: a list of places,
- // and a lone answer (one caller, one subtype) is a jump.
- .incoming_calls, .outgoing_calls, .supertypes, .subtypes => .{
- .name = config.search_buffer,
- .steps = true,
- .jumps = true,
- },
- // completion lists WHAT COULD GO HERE, one row per candidate's
- // definition. It does not jump on a single row where the gotos do:
- // a goto answers a question whose answer is a place, so landing
- // there IS the answer, whereas the question here is "what can I
- // write", and being teleported into the one candidate's
- // declaration instead of being shown it is not that.
- .document_symbols, .workspace_symbols, .diagnostics, .workspace_diagnostics, .select_refs, .completion => .{
- .name = config.search_buffer,
- .steps = true,
- },
- },
- };
-}
-
-/// The same table asked of a file pane's `output` field, null (a real file)
-/// included. This is what the kind-agnostic code in pardes.zig calls.
-pub fn fileTraits(out: ?Output) Traits {
- return traits((out orelse return file_row).from);
-}
-
-/// How much of a row ONE n/N step selects (Pardes.lookSpanIn). Derived from
-/// the two columns above rather than a third one, because it is not a fact
-/// about a buffer — it is what those facts MEAN to the walk.
-pub const Grain = enum {
- /// every look-able word, several to a line, in document order. Free text:
- /// a terminal's scrollback, a file, a PDF, and an output buffer of PROSE,
- /// where the place you want may be mid-sentence.
- word,
- /// the location at the head of the row, and one stop per row. A results
- /// buffer is a LIST: the words after a row's location are the matched
- /// text, and stepping onto them was stepping onto the same hit twice.
- line,
- /// the whole row: a command list, where the line is the word.
- whole,
-};
-
-/// The grain of a pane's rows, off its `output` field — null (a real file)
-/// included, which is why a file pane is unaffected by any of this.
-pub fn grain(out: ?Output) Grain {
- const tr = fileTraits(out);
- if (tr.commands) return .whole;
- return if (tr.steps) .line else .word;
-}
-
-/// How the dump spells an origin. A WORD, never an integer, for the reason the
-/// dump already stores tag words: reordering builtins.zig stays free. Nothing
-/// collides — a builtin is CamelCase, an lsp.Kind is snake_case, and `/` is
-/// neither.
-pub fn word(o: Origin) []const u8 {
- return switch (o) {
- .cmd => |b| @tagName(b),
- .query => |k| @tagName(k),
- .search => "/",
- .errors => config.errors_buffer,
- };
-}
-
-/// the inverse; null for "" (a real file) and for a word this build no longer
-/// has, which is a dump from another version and not a reason to fail a load
-pub fn fromWord(w: []const u8) ?Origin {
- if (w.len == 0) return null;
- if (std.mem.eql(u8, w, "/")) return .search;
- if (std.mem.eql(u8, w, config.errors_buffer)) return .errors;
- if (std.meta.stringToEnum(Builtin, w)) |b|
- if (builtins.registry.outputTraits(b) != null) return .{ .cmd = b };
- if (std.meta.stringToEnum(lsp.Kind, w)) |k| return .{ .query = k };
- return null;
-}
-
-/// Is the results buffer `pane`'s n/N is armed on the one `from` filled?
-/// `]d`/`[d` are the only keys that care WHICH search is showing — they step
-/// the diagnostics list when it is up and ask for one when it is not — and
-/// this is how they ask now that the buffer remembers: `search_pane` is a
-/// SLOT, so this doubles as the check that the slot is still ours.
-pub fn resultsFrom(p: *Pardes, pane: *Pane, from: Origin) bool {
- const rp = p.panes[pane.search_pane orelse return false] orelse return false;
- const f = rp.file orelse return false;
- const o = f.output orelse return false;
- return std.meta.eql(o.from, from);
-}
-
-/// Open one. `content` is gpa-owned and adopted; the NAME comes from the table
-/// (the caller says what ran, not what to call it) and carries `dir` so looks
-/// inside the buffer resolve like anywhere else.
-pub fn open(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8) !*Pane {
- const path = try std.fmt.allocPrint(p.gpa, "{s}/{s}", .{
- std.mem.trimEnd(u8, dir, "/"), traits(from).name,
- });
- errdefer p.gpa.free(path);
- var out: Output = .{ .from = from };
- try setArg(&out, arg);
- const pane = try p.newDocPane(id);
- pane.file = .{ .path = path, .content = content, .output = out };
- pane.cur_pinned = true;
- return pane;
-}
-
-/// Land a freshly produced list of rows in the buffer it belongs in — the one
-/// rule every results buffer follows, whichever side of the core made them.
-///
-/// The SAME command asked again REFILLS the list it already opened rather than
-/// stacking a byte-identical twin under the pane. That was runSearch's rule
-/// from the start (right-clicking a word in four places is one +Search walked
-/// four times) and language answers turned out to need it far more urgently:
-/// Tab after a dot makes a query an ordinary typing keystroke, and without the
-/// refill twenty of them fill every slot and the key is eaten for the rest of
-/// the session — see docs/lsp.md.
-///
-/// What "the same command" means comes off the ORIGIN. A search is identified
-/// by its PATTERN, so `foo`, `bar`, `foo` re-arms foo's own buffer and leaves
-/// bar's open; a language query is asked about a different symbol every time
-/// with the same (usually empty) arg, so the arg cannot tell two apart and the
-/// KIND is the natural unit — a second `gr` replaces the first list. Same
-/// directory only, because the rows are written relative to it, and never the
-/// asking pane itself (a `/` inside a +Search writes its own rows).
-///
-/// `content` is gpa-owned: adopted by the buffer, or freed here when there is
-/// nowhere to put it. `anchor` is the row n/N step from, null for the top.
-pub fn fillResults(p: *Pardes, id: usize, dir: []const u8, from: Origin, arg: []const u8, content: []u8, anchor: ?usize) !void {
- errdefer p.gpa.free(content);
- const pane = p.panes[id] orelse return error.MissingPane;
- const by_arg = std.meta.activeTag(from) != .query;
- for (p.panes, 0..) |slot, i| {
- if (i == id) continue;
- const rp = slot orelse continue;
- const rf = if (rp.file) |*f| f else continue;
- const o = if (rf.output) |*x| x else continue;
- if (!std.meta.eql(o.from, from)) continue;
- if (by_arg and !std.mem.eql(u8, o.arg(), arg)) continue;
- if (!std.mem.eql(u8, std.fs.path.dirname(rf.path) orelse "", dir)) continue;
- try setArg(o, arg);
- // a refill that changes NOTHING keeps its place: a right click on an
- // already-armed word is an `n`, and throwing the list back to the top
- // only to scroll down to the stepped row is a jump with no information
- // in it.
- const same = std.mem.eql(u8, rf.content, content);
- file_pane.setContent(p, rf, content);
- if (!same) rf.scroll = 0;
- p.active = id;
- if (traits(from).steps) {
- pane.search_pane = i;
- pane.search_row = anchor;
- p.armLookWalk(i);
- }
- return;
- }
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, content);
- p.placeDoc(id, free, np);
- p.active = id;
- // prose is not a list of locations: n/N over a hover blurb would step to
- // nowhere, so only stepping buffers arm the stepper — and WHICH command
- // filled it is the buffer's own record, not a field on the asking pane.
- if (traits(from).steps) {
- pane.search_pane = free;
- pane.search_row = anchor;
- p.armLookWalk(free);
- }
-}
-
-/// The Jumplist builtin: the focus history (Pardes.jumps) written out as text,
-/// one row per location, oldest first — the same `location text` shape every
-/// results buffer here has, which is what buys n/N stepping and Look-on-a-row
-/// for nothing: the leading word is an ordinary look target, and the ordinary
-/// look path is what goes there.
-///
-/// A RENDERING, never a second list. The rows are spelled from the stack at
-/// the moment you ask and go stale the moment you jump, exactly like a search
-/// result — which is also why this opens a fresh buffer per press instead of
-/// refreshing one the way Help does: Help is a document, this is a snapshot.
-///
-/// How a location is spelled is the rule runSearch already follows: a REAL
-/// file names itself (its path is absolute, so the row resolves from any
-/// pane's directory), and everything else — a terminal, an output buffer, an
-/// image — has no file to point at and gets `@pN`. The trailing text is the
-/// content line for anything holding text, else the pane's directory: enough
-/// to recognise the place without opening it.
-pub fn openJumps(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- for (p.jumps[0..p.njumps]) |j| {
- const jp = p.panes[j.pane] orelse continue;
- var idbuf: [16]u8 = undefined;
- const pdf_path: ?[]const u8 = if (comptime pardes.pdf_enabled) jp.pdfPath() else null;
- const has_path = if (jp.file) |f| f.output == null else pdf_path != null;
- const loc: []const u8 = if (has_path)
- (if (jp.file) |f| f.path else pdf_path.?)
- else
- std.fmt.bufPrint(&idbuf, config.pane_addr ++ "{d}", .{j.pane}) catch unreachable;
- const what: []const u8 = if (jp.file) |f|
- std.mem.trim(u8, modal.lineSlice(f.content, j.line -| 1), " \t\r")
- else if (jp.image) |iv|
- iv.path
- else if (pdf_path) |path|
- path
- else
- jp.cwdSlice();
- var cut = @min(what.len, 120);
- while (cut > 0 and cut < what.len and what[cut] & 0xc0 == 0x80) cut -= 1;
- if (j.line == 0)
- try out.writer.print("{s} {s}\n", .{ loc, what[0..cut] })
- else
- try out.writer.print("{s}:{d}:{d} {s}\n", .{ loc, j.line, j.col, what[0..cut] });
- }
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .Jumplist }, content);
-}
-
-/// The ThemeSel builtin: the theme ring written out as one `Theme <name>` row
-/// per theme — the ordinary builtin with its argument, exactly the line you
-/// would type — into a buffer whose `commands` trait says the rows are words
-/// and not places. n/N therefore select each row WHOLE and Tab runs it, so
-/// walking the list is trying the themes on and stopping on one is choosing
-/// it: no picker mode, no preview state, nothing to commit or cancel.
-///
-/// The command's own name comes from the runtime setting descriptor rather
-/// than a second literal. The descriptor generates the builtin too, so a
-/// rename cannot leave picker rows naming a command that is gone.
-pub fn openThemes(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- for (pardes.themes) |t|
- try out.writer.print(comptime runtime_config.findAction(.theme).?.word ++ " {s}\n", .{t.name});
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .ThemeSel }, content);
-}
-
-/// The FontSel builtin: openThemes over the fonts installed on the machine
-/// instead of the themes compiled into the binary — one `Font <name>` row
-/// each, in a buffer whose rows n/N RUN, so walking it wears the fonts and
-/// stopping picks one. Everything that makes that work is already above; this
-/// is the same one-pass writer pointed at a different list.
-///
-/// Only where the shell draws its own text. The same `font_picker` availability
-/// bit that generates the Font/FontSel builtins keeps non-GUI builds from
-/// analysing font discovery here.
-pub fn openFonts(p: *Pardes, id: usize) !void {
- if (builtins.capabilities.font_picker) {
- const arena = p.scratch.allocator();
- const font_list = fonts.list(arena, null);
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- for (font_list) |f|
- try out.writer.print(comptime runtime_config.findAction(.font).?.word ++ " {s}\n", .{f.name});
- const content = try out.toOwnedSlice();
- try openStepped(p, id, .{ .cmd = .FontSel }, content);
- }
-}
-
-/// Open a buffer n/N will walk, and arm them on it: the shared tail of every
-/// builtin that answers with a list. `content` is gpa-owned and adopted by the
-/// new pane, or freed here if opening it fails.
-///
-/// Focus stays with the pane that ASKED, exactly as it does after a search:
-/// n/N are read there, and they step the buffer they just armed.
-fn openStepped(p: *Pardes, id: usize, from: Origin, content: []u8) !void {
- errdefer p.gpa.free(content);
- const pane = p.panes[id] orelse return error.MissingPane;
- const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, "", content);
- p.placeDoc(id, free, np);
- p.active = id;
- pane.search_pane = free;
- pane.search_row = null;
- p.armLookWalk(free);
-}
-
-/// The Help builtin: THE INDEX of builtins — every one of them, and every way
-/// to run it — filtered to what `prefix` can still reach, written into an
-/// output buffer (acme's +Errors). Ordinary text, so the names in it are LIVE:
-/// middle-click `Tutor` there and the tutor opens. Reuses the open +Help
-/// buffer instead of piling panes up, and focus follows: you asked to read it.
-///
-/// ONE builtin and not two. The complete index and the mid-chord "what can
-/// `SPC h` still reach" are the same array (pardes.builtin_rows) read with a
-/// different prefix — the empty one matches every row, including the builtins
-/// SPC cannot reach at all, so the reference page IS the filter's degenerate
-/// case. A second builtin would have been a second renderer over a superset of
-/// these rows, and the two would have drifted the first time a column moved.
-fn helpContent(gpa: std.mem.Allocator, prefix: []const u8) ![]u8 {
- const full_header = "pardes builtins, and how to run each:\nSPC and its keys, a chord, a button, the\ntopbar - or the name, executed anywhere.\n\n";
- const group_header = "pardes builtins under SPC";
- // The LANGUAGE KEYS are the one part of the keymap Help would otherwise
- // never show: they are motions and modes, not words, so no builtin row
- // carries them — yet they are the keys a reader comes looking for. Full
- // listing only; a mid-chord `SPC l` view stays a pure filter.
- const language_footer =
- "\nlanguage keys (motions, not words):\n" ++
- "gd gD gy gi gr goto: definition,\n" ++
- " declaration, type-def,\n" ++
- " implementation, refs\n" ++
- "]d [d ]D [D diagnostics: next,\n" ++
- " prev, last, first\n" ++
- "= format (applies, one\n" ++
- " undo step)\n" ++
- "Tab after a . completion, in insert\n" ++
- "C-left-click definition, by mouse\n" ++
- "SPC l ... hover, rename, symbols,\n" ++
- " calls, types: above\n";
- var len: usize = if (prefix.len == 0)
- full_header.len + language_footer.len
- else
- group_header.len + prefix.len * 2 + 2;
- for (pardes.builtin_rows) |row| {
- // a path-less builtin filters as the empty path: in the full listing
- // (which starts with nothing) and out of every group
- if (!std.mem.startsWith(u8, row.path orelse "", prefix)) continue;
- len += row.line.len + 1;
- }
- const content = try gpa.alloc(u8, len);
- var at: usize = 0;
- if (prefix.len == 0) {
- @memcpy(content[0..full_header.len], full_header);
- at = full_header.len;
- } else {
- @memcpy(content[0..group_header.len], group_header);
- at = group_header.len;
- for (prefix) |c| {
- content[at] = ' ';
- content[at + 1] = c;
- at += 2;
- }
- content[at] = '\n';
- content[at + 1] = '\n';
- at += 2;
- }
- for (pardes.builtin_rows) |row| {
- if (!std.mem.startsWith(u8, row.path orelse "", prefix)) continue;
- @memcpy(content[at..][0..row.line.len], row.line);
- at += row.line.len;
- content[at] = '\n';
- at += 1;
- }
- if (prefix.len == 0) {
- @memcpy(content[at..][0..language_footer.len], language_footer);
- at += language_footer.len;
- }
- std.debug.assert(at == content.len);
- return content;
-}
-
-pub fn openHelp(p: *Pardes, id: usize, prefix: []const u8) !void {
- const content = try helpContent(p.gpa, prefix);
- // content is handed off unfreed on purpose: openRead adopts it or frees
- // it, and nothing between the alloc above and this line can fail.
- return openRead(p, id, .{ .cmd = .Help }, prefix, content);
-}
-
-test "full Help renders every builtin row, then the language keys" {
- const content = try helpContent(std.testing.allocator, "");
- defer std.testing.allocator.free(content);
-
- // FIRST blank line: the end of the header (the footer opens with one too)
- const body = content[(std.mem.indexOf(u8, content, "\n\n") orelse
- return error.MissingHelpHeader) + 2 ..];
- var lines = std.mem.splitScalar(u8, body, '\n');
- for (pardes.builtin_rows) |row|
- try std.testing.expectEqualStrings(row.line, lines.next() orelse
- return error.MissingBuiltinHelpRow);
- // ...and after the last row, the language-keys section: the one part of
- // the keymap no builtin row can carry, closing the page.
- try std.testing.expectEqualStrings("", lines.next() orelse
- return error.MissingLanguageKeys);
- try std.testing.expectEqualStrings("language keys (motions, not words):", lines.next() orelse
- return error.MissingLanguageKeys);
- try std.testing.expect(std.mem.indexOf(u8, body, "\ngd gD gy gi gr goto: definition,\n") != null);
- // the group view stays a pure filter: no footer under a prefix
- const group = try helpContent(std.testing.allocator, "l");
- defer std.testing.allocator.free(group);
- try std.testing.expect(std.mem.indexOf(u8, group, "language keys") == null);
-}
-
-/// The complete live Config report: the generated settings and the host facts
-/// needed to interpret them. The startup path remains ordinary selectable text
-/// in the report, so Look still opens the exact file the launcher consulted.
-pub fn openConfig(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try runtime_config.writeReport(&out.writer, .{
- .startup_config_path = p.opts.startup_config_path,
- .platform = @tagName(pardes.platform),
- .theme_name = p.theme().name,
- .compiled_default_shell = config.default_shell,
- .gui_shader_source_mode = if (gui_shader_source_mode) |mode|
- mode.label()
- else
- null,
- .hover_delay_frames = config.look_preview_delay_frames,
- .native_images = p.native_images,
- .capabilities = builtins.capabilities,
- .state = &p.settings,
- });
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Config }, "", content);
-}
-
-/// The message-row log, oldest first — the lines that were said in passing and
-/// then cleared by the next keystroke.
-pub fn openMessages(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- var i: usize = 0;
- while (p.messageLog(i)) |m| : (i += 1) {
- if (m.pane != 0xff) try out.writer.print("{d}: ", .{m.pane});
- try out.writer.writeAll(m.slice());
- if (m.repeats > 1) try out.writer.print(" (x{d})", .{m.repeats});
- try out.writer.writeByte('\n');
- }
- if (i == 0) try out.writer.writeAll("nothing has been said yet\n");
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Messages }, "", content);
-}
-
-/// The version banner plus the embedded CHANGELOG, so an installed binary can
-/// say what it is and what changed without a repository beside it.
-pub fn openChangelog(p: *Pardes, id: usize) !void {
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try out.writer.print("pardes {s}\n\n", .{build_options.version});
- try out.writer.writeAll(@embedFile("CHANGELOG.md"));
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .Changelog }, "", content);
-}
-
-/// Print the implementation that this build actually uses for one effect.
-/// Sources are build inputs embedded as bytes, so this stays useful from an
-/// installed binary with no repository beside it.
-pub fn openEffectCode(p: *Pardes, id: usize, argument: []const u8) !void {
- const name = std.mem.trim(u8, argument, " \t\r\n");
- const setting = runtime_config.find(name) orelse return error.UnknownEffect;
- switch (setting.action) {
- .transition, .scene => {},
- else => return error.NotAnEffect,
- }
- if (!setting.enabled(builtins.capabilities)) return error.EffectUnavailable;
- const segments = effect_sources.forSetting(setting) orelse
- return error.EffectUnavailable;
-
- var out: std.Io.Writer.Allocating = .init(p.gpa);
- errdefer out.deinit();
- try out.writer.print("EffectCode {s} ({s})\n", .{ setting.word, @tagName(effect_sources.backend) });
- if (gui_shader_source_mode) |mode|
- try out.writer.print("GUI shader source: {s}\n", .{mode.label()});
- try out.writer.writeByte('\n');
- for (segments) |segment| {
- try out.writer.print("--- {s} ---\n", .{segment.path});
- try out.writer.writeAll(segment.source);
- if (!std.mem.endsWith(u8, segment.source, "\n")) try out.writer.writeByte('\n');
- try out.writer.writeByte('\n');
- }
- const content = try out.toOwnedSlice();
- return openRead(p, id, .{ .cmd = .EffectCode }, setting.word, content);
-}
-
-/// Open a buffer you READ, and go there: the shared tail of every builtin
-/// whose answer is a document rather than a list. Asking again REFRESHES the
-/// one already open instead of stacking a twin beside it — found by its
-/// ORIGIN, never by matching its name, for the reason the whole file exists.
-///
-/// The mirror of `openStepped`, and the difference is the two lines at the
-/// ends: focus comes HERE (you asked to read it) where a results buffer
-/// leaves you in the pane that asked, and n/N are not armed, because prose
-/// has nowhere to step to.
-///
-/// `content` is gpa-owned: adopted by the buffer, or freed here when there is
-/// nowhere to put it.
-/// Put a report in this directory's `+Errors` buffer — acme's own name for
-/// output that came from the PROGRAM rather than from a word somebody clicked.
-/// Refills the one already open rather than stacking a twin, which is what a
-/// second failed filter wants: the newest reason is the one being read.
-pub fn openErrors(p: *Pardes, id: usize, content: []u8) !void {
- return openRead(p, id, .errors, "", content);
-}
-
-fn openRead(p: *Pardes, id: usize, from: Origin, arg: []const u8, content: []u8) !void {
- errdefer p.gpa.free(content);
- const pane = p.panes[id] orelse return error.MissingPane;
- for (p.panes, 0..) |slot, i| {
- const hp = slot orelse continue;
- const hf = if (hp.file) |*f| f else continue;
- const ho = if (hf.output) |*o| o else continue;
- if (!std.meta.eql(ho.from, from)) continue;
- try setArg(ho, arg);
- file_pane.setContent(p, hf, content);
- hf.scroll = 0;
- hp.cur_row = 0;
- hp.msel.active = false;
- p.active = i;
- return;
- }
- const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice();
- const free = p.freeSlot() orelse return error.NoPaneSlots;
- const np = try open(p, free, dir, from, arg, content);
- p.placeDoc(id, free, np);
- p.active = free;
-}
-
-test "dump origins accept only builtins that actually own output panes" {
- try std.testing.expectEqual(Origin{ .cmd = .Help }, fromWord("Help").?);
- try std.testing.expect(fromWord("Kill") == null);
- try std.testing.expect(fromWord("Theme") == null);
-}
-
-test "result refill identity retains the full bounded argument" {
- const p = try Pardes.init(std.testing.allocator, .{
- .tty_only = true,
- .cols = 80,
- .rows = 24,
- });
- defer p.deinit();
-
- var first: [max_arg]u8 = @splat('a');
- var second = first;
- first[200] = 'x';
- second[200] = 'y';
-
- try fillResults(
- p,
- 0,
- "/tmp",
- .search,
- &first,
- try p.gpa.dupe(u8, "first\n"),
- null,
- );
- const first_id = p.panes[0].?.search_pane orelse return error.MissingResults;
- const next_slot = p.freeSlot();
-
- try fillResults(
- p,
- 0,
- "/tmp",
- .search,
- &first,
- try p.gpa.dupe(u8, "refilled\n"),
- null,
- );
- try std.testing.expectEqual(first_id, p.panes[0].?.search_pane.?);
- try std.testing.expectEqual(next_slot, p.freeSlot());
- try std.testing.expectEqualStrings("refilled\n", p.panes[first_id].?.file.?.content);
-
- try fillResults(
- p,
- 0,
- "/tmp",
- .search,
- &second,
- try p.gpa.dupe(u8, "second\n"),
- null,
- );
- try std.testing.expect(p.panes[0].?.search_pane.? != first_id);
-
- var output: Output = .{ .from = .search };
- var oversized: [max_arg + 1]u8 = @splat('z');
- try std.testing.expectError(error.ArgumentTooLong, setArg(&output, &oversized));
- const slot_before_error = p.freeSlot();
- try std.testing.expectError(
- error.ArgumentTooLong,
- fillResults(
- p,
- 0,
- "/tmp",
- .search,
- &oversized,
- try p.gpa.dupe(u8, "must be freed\n"),
- null,
- ),
- );
- try std.testing.expectEqual(slot_before_error, p.freeSlot());
-}