//! The builtins: one struct each, plus setting commands generated from one //! runtime_config table. //! //! Executing a builtin's NAME (middle-click / Tab) runs it through the one //! dispatcher (Pardes.runBuiltin), no matter where the name appears. The //! struct's DECL NAME is the user-visible word — the one in the topbar, the //! one sitting in a tag, the one Help prints, the one you execute — so //! `std.meta.stringToEnum` is the lookup and there is no name table to sync. //! //! A zig file IS a struct, so THIS FILE'S declarations are the manual list: //! the registry walks them at comptime and appends the enabled settings from //! runtime_config.settings. There is no hand-maintained enum or dispatcher //! switch to keep in sync; declarations and setting descriptors are the data. //! //! A manual builtin is a struct declaring `pub fn run(Ctx) void`; an optional //! explicit `enabled` declaration gates it. Helpers have no `run`, and a //! claimed builtin with the wrong signature is a compile error. //! //! What is NOT here: the key bindings. `leader_path` is ONE table in //! config.zig next to `topbar_str` and every other syntactic choice — the //! whole remapping surface belongs in one file a user can read top to bottom, //! not scattered a line at a time across thirty-three structs. const std = @import("std"); const pardes = @import("pardes.zig"); const Pardes = pardes.Pardes; const Pane = pardes.Pane; const output_pane = @import("output_pane.zig"); const image_pane = @import("image_pane.zig"); const config = @import("config.zig"); const runtime_config = @import("runtime_config.zig"); const board_memory = @import("board_memory.zig"); /// The platform's runtime-setting facilities, stated once as plain data. /// Registry generation, leader paths, Config, and EffectCode all consume this /// exact value rather than rebuilding equivalent-looking boolean expressions. pub const capabilities: runtime_config.Capabilities = .{ .font_picker = pardes.font_picker, // A transition is composited by the shell, and EffectCode has to be able // to show WHICH compositor: only the three hosted shells are in this // package, so the hostless platforms have no honest source to print. .panel_transitions = pardes.hosted, .scene_shaders = pardes.platform == .gui or pardes.platform == .macos, // The tty's font belongs to its emulator, and the P4 firmware's belongs to // whatever terminal is on the other end of the serial line. .tagline_font_size = pardes.platform != .tty and pardes.platform != .p4, }; /// What a builtin gets to act on. One bundle rather than five parameters /// because most builtins want two of them and zig rejects the unused rest. /// `txt` is the executed text (Restore reads its path back out of it) and /// `arg` the execute's ARGUMENT — text typed after the name, or the selection /// a mouse chord kept, which is why Grep and Find run straight away when there /// is one instead of asking. The leader passes "" and null: a key path names a /// builtin, never an argument. pub const Ctx = struct { p: *Pardes, /// pane `id`, already resolved — the dispatcher's null check is the one /// guard every builtin used to share. pane: *Pane, id: usize, txt: []const u8, arg: ?[]const u8, }; /// Every enabled builtin, in source order. Feature gates are explicit data; /// a declaration that claims to be enabled but has the wrong run signature is /// a compile error instead of silently disappearing from the command enum. fn isEnabled(comptime T: type) bool { return !@hasDecl(T, "enabled") or T.enabled; } fn manualBuiltinCount() comptime_int { comptime { var count = 0; for (@typeInfo(@This()).@"struct".decls) |d| { if (@TypeOf(@field(@This(), d.name)) != type) continue; const T = @field(@This(), d.name); if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; if (@TypeOf(T.run) != fn (Ctx) void) @compileError(d.name ++ ".run must have signature fn (Ctx) void"); count += 1; } return count; } } fn manualBuiltinList() [manualBuiltinCount()]type { comptime { @setEvalBranchQuota(4000); var list: [manualBuiltinCount()]type = undefined; var count = 0; for (@typeInfo(@This()).@"struct".decls) |d| { if (@TypeOf(@field(@This(), d.name)) != type) continue; const T = @field(@This(), d.name); if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; list[count] = T; count += 1; } return list; } } fn settingCount() comptime_int { comptime { var count = 0; for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) { count += 1; }; return count; } } fn settingList() [settingCount()]runtime_config.Setting { comptime { var list: [settingCount()]runtime_config.Setting = undefined; var count = 0; for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) { list[count] = setting; count += 1; }; return list; } } /// A builtin's user-visible word: the LAST dotted segment of `@typeName`, /// because @typeName spells a file-scope struct fully qualified /// ("builtins.Kill"). Deriving it beats a `pub const name` field per struct, /// which would be the same word written twice with nothing keeping the two /// honest. A name that is not a legal identifier would be spelled `@"..."`. pub fn word(comptime T: type) []const u8 { const n = @typeName(T); const dot = std.mem.lastIndexOfScalar(u8, n, '.') orelse return n; return n[dot + 1 ..]; } pub const OutputTraits = struct { name: []const u8, steps: bool = false, jumps: bool = false, commands: bool = false, doc: bool = false, /// the buffer BECOMES an ordinary file once written (the New scratch, and a /// real file, which is one already). Every other output buffer is a /// RENDERING: Save writes its text out and the buffer stays what it is, /// refillable and steppable, because a saved copy of a search is a copy of /// a search and not the search. saves: bool = false, }; /// The enum: field name = struct name, value = index into `all()`. Everything /// downstream (leader_path's EnumArray, leader_rows, the topbar's comptime /// check, stringToEnum) speaks it exactly as it did when it was hand-written. /// /// Registry-dependent APIs live in one namespace so the outer declaration /// walk only sees this namespace's type, not functions whose signatures depend /// on the builtin enum being constructed. pub const registry = struct { pub fn Builtin() type { // The duplicate-name check below is O(n^2) string comparisons over every manual builtin AND // every generated setting, so this quota grows quadratically with the builtin count. 20,000 // was enough until three more (Peek/Poke/Hexdump) tipped `-Dplatform=gui` over with // "evaluation exceeded 20000 backwards branches". Raised with room rather than to the next // value that happens to pass, so the next builtin does not have to rediscover this. @setEvalBranchQuota(200_000); const manual = manualBuiltinList(); const generated = settingList(); const count = manual.len + generated.len; const Tag = std.math.IntFittingRange(0, count - 1); var names: [count][]const u8 = undefined; for (manual, 0..) |T, i| names[i] = word(T); for (generated, manual.len..) |setting, i| names[i] = setting.word; for (names, 0..) |name, i| for (names[i + 1 ..]) |later| if (std.mem.eql(u8, name, later)) @compileError("duplicate builtin name: " ++ name); return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, count)); } pub fn takesArg(b: Builtin()) bool { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return @hasDecl(T, "takes_arg") and T.takes_arg; inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return setting.takesArg(); unreachable; } pub fn outputTraits(b: Builtin()) ?OutputTraits { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return if (@hasDecl(T, "output")) T.output else null; inline for (comptime settingList(), manualBuiltinCount()..) |_, i| if (@intFromEnum(b) == i) return null; unreachable; } pub fn dispatch(b: Builtin(), c: Ctx) void { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return T.run(c); inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return c.p.applySettingBuiltin(setting, c.arg); unreachable; } }; test "capabilities exactly gate setting and effect-source builtins" { const Builtin = registry.Builtin(); for (runtime_config.settings) |setting| { const registered = std.meta.stringToEnum(Builtin, setting.word) != null; try std.testing.expectEqual(setting.enabled(capabilities), registered); } const effect_code_registered = std.meta.stringToEnum(Builtin, "EffectCode") != null; try std.testing.expectEqual(EffectCode.enabled, effect_code_registered); } // The three memory words, checked the same way but at COMPTIME rather than in // a test, because the property is about builds this test binary is not: the // tty suite can only ever observe its own platform, and what matters is that // `-Dplatform=web -Dtarget=wasm32-freestanding` does not quietly hand a // browser tab a Poke. Every build of every platform now proves its own half. comptime { for ([_][]const u8{ "Peek", "Poke", "Hexdump" }) |name| if (@hasField(registry.Builtin(), name) != board_memory.enabled) @compileError( "bare-metal memory word gating leaked: " ++ name, ); } // ---- the two acme verbs ---- // Look and Execute are the verbs the whole environment is built on, and they // are BUILTINS: `Look main.zig` typed in a tag and executed is the same look a // right click on `main.zig` is, `Exec ls` the same as a middle click on `ls`. // The mouse buttons and Enter/Tab are not a second path into them any more — // they are two bindings pointing here (config.look_cmd / exec_cmd), the status // `SPC f s` has relative to Save. That is the whole feature: what used to be a // `button` parameter threaded through every keyboard call site, with the // builtin dispatch nested INSIDE it, is now one word each. // // The operand is `arg` in both — a name's tail (`Look main.zig`), else the // selection a chord kept, else the word the gesture pointed at, which the // gesture resolves and passes. Nothing to act on means nothing happens, the // way `Save` on a terminal is inert. pub const Look = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { c.p.lookAt(c.id, c.arg orelse return); } }; pub const Exec = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were _ = c.p.execute(c.id, c.arg orelse return); } }; // ---- session ---- pub const Kill = struct { pub fn run(c: Ctx) void { c.p.quit = true; c.p.emit(.quit); } }; pub const Dump = struct { pub fn run(c: Ctx) void { c.p.dumpState() catch {}; } }; pub const Restore = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { var it = std.mem.tokenizeAny(u8, c.txt, " \t"); _ = it.next(); // the word "Restore" const path = it.next() orelse (c.p.last_dump orelse return); if (path.len > c.p.restore_buf.len) return; @memcpy(c.p.restore_buf[0..path.len], path); c.p.restore_req = c.p.restore_buf[0..path.len]; } }; // ---- the message row ---- /// TEXT onto this pane's transient message row — the row a failed save, a /// refused Look and a language server that would not start all report through /// (Pardes.setMessage, and Pardes.reportError one line above it). Every writer /// of that row is something going wrong, so until this word there was no way /// to look at it without breaking something on purpose: no wording could be /// checked against a narrow pane, and no test could pin the row without /// arranging a real failure first. /// /// Bare, it reports ITSELF through the error path, because that is the other /// half of the same machinery — `reportError` is `setMessage` plus an /// `: ` — and because a word that needs no argument to /// demonstrate one is a word you can also just click. /// /// Whether the row is FREE is not asked here and is not this word's business: /// an armed prompt outranks a message at render time, so posting under one is /// stored and invisible, exactly as a save finishing under one is. Nor does /// anything here decide when it goes away — your next key or click does, on /// every pane at once, because a message is exactly as old as your last input. pub const Msg = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { if (c.arg) |text| c.p.setMessage(c.id, text) else c.p.reportError(c.id, comptime word(@This()), error.NoMessage); } }; // ---- display choices ---- // // The plain toggles, named theme/shell/font setters and animation effects are // generated from runtime_config.settings. Keeping their command metadata and // their query order in the same value table is what prevents a settable choice // from disappearing from Config. Hand-written commands continue below. /// One step along the ring. With 228 themes in it this is no longer a way to /// REACH a theme — ThemeSel is — but it is still the way to browse one, and the /// browse got better rather than worse: the generated half is sorted by name, so /// the neighbours of wherever you are are that theme's own variants (light, /// hard, soft, the whole gruvbox family in a row). Kept as the topbar word and /// SPC t n it has always been; a ring you can walk off the end of in three /// clicks was never what made it useful. pub const NextColor = struct { pub fn run(c: Ctx) void { c.p.setThemeIndex((@as(usize, c.p.settings.theme) + 1) % pardes.themes.len); } }; /// The theme BY NAME — `Theme acme`. The ring grew past the point where /// cycling to the one you want is reasonable, so this is the way to ask for /// one, and NextColor stays as the way to browse. Inert without an argument /// (there is no theme called nothing), which is also why it has no leader path: /// a key path names a builtin and can never carry the name of a theme. /// /// A LINEAR SCAN over 228 names, on a keystroke: the alternative is a comptime /// name->index map, which is a second copy of the ring to build for a lookup /// nobody will ever measure. 228 short string compares is microseconds, and it /// happens once per theme change, not once per frame. /// ...and the list of what Theme takes, as a buffer you walk. Its rows are /// `Theme ` COMMANDS rather than locations, which is one flag on the /// buffer (output_pane.Traits.commands) and changes what a step SELECTS: the /// whole line, since there is no path inside it to pick out. Tab on what n /// selected wears that theme — the same middle click on the row is — so /// walking the list with n/Tab is trying them on, and stopping is choosing. pub const ThemeSel = struct { pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .commands = true }; pub fn run(c: Ctx) void { output_pane.openThemes(c.p, c.id) catch |err| c.p.reportError(c.id, "themes", err); } }; /// Load one complete Theme value from a .zon file. Relative paths are rooted /// at the per-user pardes directory, so an init line can simply say /// `ThemeFile themes/mine.zon`. The native host owns the read and watch; the /// core owns parsing and keeps the last valid value across a bad live edit. pub const ThemeFile = struct { pub const takes_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime enabled) c.p.requestThemeFile(c.id, c.arg orelse return) else unreachable; } }; /// Materialize every compiled theme as editable ZON under /// `/themes/builtin`. Filesystem work remains a host effect, just like /// Dump and Save; the build-time ring itself is the sole source of the data. pub const DumpThemes = struct { pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime enabled) { if (c.p.opts.config_dir == null) { c.p.reportError(c.id, "dump themes", error.NoConfigDirectory); return; } c.p.emit(.{ .dump_themes = .{ .pane = @intCast(c.id) } }); } else unreachable; } }; // ---- the GUI's font list, and NOTHING on any other platform ---- // // Runtime settings such as Font are generated from runtime_config.settings. // FontSel remains hand-written because it opens a result pane. Its explicit // `enabled` bit is the same feature gate the registry uses for PDF commands: // disabled commands have no enum field, help row, or dispatcher case. /// The GUI font BY NAME — `Font DejaVuSansMono-Regular`, the way `Theme ` /// takes a theme, and inert without an argument for the same reason (there is /// no font called nothing). The name is a font FILE's stem, which is what the /// picker lists; resolving it is a walk of the font directories that stops at /// the first match, so nothing is cached and an install five seconds ago is /// findable. /// /// The core cannot load a font — it has no rasterizer, no atlas and no window /// — so this asks: the resolved PATH goes in runtime config, the shell takes it on /// its next pass and re-rasters. Exactly the shape Restore already has. /// ...and the list of what Font takes: every MONOSPACE font on the machine, /// one `Font ` row each, in the picker ThemeSel already is (rows that /// are commands, so n/N select each one WHOLE and Tab runs it — walking with /// n and pressing Tab wears each font in turn, and picking one is stopping /// there). /// /// Monospace only, which is the one judgement in the feature: the grid is a /// fixed cell, so a proportional face is not a worse-looking option but an /// unreadable one — and this picker EXECUTES what it steps onto, so listing /// them would mean the list wearing one on the way past. See fonts.monospaced. pub const FontSel = struct { pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true }; pub const enabled = capabilities.font_picker; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { output_pane.openFonts(c.p, c.id) catch |err| c.p.reportError(c.id, "fonts", err); } }; /// Toggle a native PDF between the reading-oriented fit-width view and the /// whole-page-height view. The explicit feature gate omits the command, help /// row, and dispatcher case when MuPDF is disabled. pub const PdfFit = struct { pub const enabled = pardes.pdf_enabled; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { pardes.pdf_pane.toggleFit(c.pane); } }; /// Cycle a native PDF through original pixels, a chroma-preserving themed /// filter, and a full theme duotone. It has the same explicit feature gate as /// PdfFit: absent without MuPDF and inert off a PDF pane. pub const PdfTint = struct { pub const enabled = pardes.pdf_enabled; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { pardes.pdf_pane.toggleTint(c.pane); } }; /// Show this PDF's document outline as a live, steppable output pane. The /// command is absent from non-MuPDF builds and deliberately inert on every /// other pane kind, like the two PDF display toggles above. pub const PdfSections = struct { pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true }; pub const enabled = pardes.pdf_enabled; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { pardes.pdf_pane.openSections(c.p, c.id); } }; // The image pane's three renderer toggles. They used to be executable words // interpreted by a special tag dispatcher; as ordinary builtins they are // pressable under SPC and listed by `SPC ?`. The tag now reports their plain // live values without becoming a second mutation path. Each acts on the pane // it runs in and is inert elsewhere, the way Save is on a terminal. /// glyph art over the host's pixels pub const Petscii = struct { pub fn run(c: Ctx) void { if (c.pane.image) |*state| image_pane.togglePetscii(state); } }; /// the C64 palette or the terminal's own 16 pub const Palette = struct { pub fn run(c: Ctx) void { if (c.pane.image) |*state| image_pane.togglePalette(state); } }; /// add the printable ASCII bitmaps to the matcher's glyph set pub const Ascii = struct { pub fn run(c: Ctx) void { if (c.pane.image) |*state| image_pane.toggleAscii(state); } }; // ---- the system clipboard ---- // helix's `` clipboard menu, and the ONLY five words in pardes that // touch the desktop's clipboard. Everything else — `y`, `d`, `c`, `p`, `P`, // `R`, the acme cut/paste chords — lives entirely in the internal register, // which is helix's arrangement and, less abstractly, the reason deleting a // character no longer throws away whatever you had copied from a browser. // // They are builtins rather than bare chords because the leader table is the // remapping surface and a leader path names a builtin: spelling them here // puts them in Help's index, makes them executable words like every other // verb, and costs no second mechanism. Their paths ARE helix's letters, on // the same leader helix uses — see config.leader_path. // // The two directions are not symmetric, and cannot be. Writing is a fire-off: // the core owns the bytes and the shell copies them out. READING has to leave // the core and come back — SDL and NSPasteboard answer inside the same drain, // a browser answers a promise later, and a terminal answers over OSC 52 or, // far more often, refuses outright. So a paste is a REQUEST (the // read_clipboard effect) that may simply never be answered, and a `SPC p` // that does nothing in a locked-down terminal is the honest outcome rather // than a bug to paper over with the internal register. pub const ClipYank = struct { pub fn run(c: Ctx) void { c.p.clipYank(c.pane, false); } }; /// helix `Y`: the PRIMARY selection alone, where `SPC y` joins every /// cursor's. One cursor makes them the same word. pub const ClipYankMain = struct { pub fn run(c: Ctx) void { c.p.clipYank(c.pane, true); } }; pub const ClipPaste = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .after); } }; pub const ClipPasteBefore = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .before); } }; pub const ClipReplace = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .replace); } }; // ---- panes and columns ---- /// Write this pane's text out. A pane with a real file behind it writes THAT /// file with no argument — acme's Put, what `:w` has always meant — and /// that is the only pane Save can serve without being told where. /// /// Everywhere else the path is REQUIRED, so a bare `Save` asks for one exactly /// the way Find and Grep ask for a pattern: the tag input arms prefilled with /// the pane's directory and Enter commits it. A terminal writes its plaintext /// scrollback and stays a terminal; an output buffer writes its rows and stays /// an output buffer, still refillable and still walked by n/N — with the one /// exception the New scratch has always been, an empty buffer whose whole /// purpose is to become the file you name (output traits: `saves`). /// /// Images and PDFs hold nothing of their own that is unwritten, so the word is /// inert there and absent from their tag. pub const Save = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const path = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (path.len > 0) return c.p.saveTo(c.id, path); if (c.pane.file) |file| if (file.output == null) return c.p.saveFile(c.id); if (c.pane.file != null or c.pane.isTerminal()) c.p.startSavePrompt(c.pane); } }; /// An empty scratch buffer below the calling pane, inheriting its directory. /// No file exists yet, so Save asks for a path prefilled with that directory /// and, once written, the buffer becomes an ordinary file pane. pub const New = struct { pub const output: OutputTraits = .{ .name = config.scratch_buffer, .doc = true, .saves = true }; pub fn run(c: Ctx) void { c.p.newScratchBelow(c.id); } }; /// The same empty scratch, opened in a fresh column beside the calling pane. pub const Newcol = struct { pub fn run(c: Ctx) void { c.p.newScratchColumn(c.id); } }; pub const Del = struct { pub fn run(c: Ctx) void { c.p.absorbVWeight(c.id); c.p.layoutRemove(c.id); c.p.deinitPane(c.pane); c.p.panes[c.id] = null; if (c.p.active == c.id) c.p.active = c.p.prevFocus(c.id) orelse { c.p.quit = true; c.p.emit(.quit); return; }; } }; /// Project this terminal's displayed cell foregrounds and backgrounds through /// the current Pardes theme. The emulator keeps its original colour state; /// only this pane's rendered cells change, so OSC queries and later resets /// remain truthful. pub const Filter = struct { pub fn run(c: Ctx) void { if (!c.pane.isTerminal()) return; c.pane.tty_filter = !c.pane.tty_filter; } }; pub const Delcol = struct { pub fn run(c: Ctx) void { const f = c.p.layoutFindTerm(c.id) orelse return; var ids: [pardes.MAX_PANES]usize = undefined; const nids = c.p.col_n[f.col]; for (0..nids) |k| ids[k] = c.p.col_terms[f.col][k]; for (ids[0..nids]) |tid| { if (c.p.panes[tid]) |tt| { c.p.layoutRemove(tid); c.p.deinitPane(tt); c.p.panes[tid] = null; } } if (c.p.panes[c.p.active] == null) c.p.active = c.p.prevFocus(c.p.active) orelse { c.p.quit = true; c.p.emit(.quit); return; }; } }; /// A shell in the calling pane's directory, raw from the first frame. On every /// pane's tagline: the fast path from wherever you are to a prompt there. pub const Newtty = struct { pub fn run(c: Ctx) void { c.p.spawnTty(c.id); } }; /// Fold the active pane's column into the one on its right, keeping its panes. /// The horizontal mirror of the vertical stacking `New` does. pub const Joincol = struct { pub fn run(c: Ctx) void { c.p.joinCol(); } }; pub const Tutor = struct { pub fn run(c: Ctx) void { const free = c.p.freeSlot() orelse return; const nt = c.p.openTutorView(free) catch return; c.p.placeDoc(c.id, free, nt); // a doc like any other } }; pub const Help = struct { pub const output: OutputTraits = .{ .name = config.help_buffer }; pub fn run(c: Ctx) void { output_pane.openHelp(c.p, c.id, "") catch |err| c.p.reportError(c.id, "help", err); } }; /// Where pardes read its startup commands from — the path, printed into an /// output buffer, `SPC f c` or the word executed anywhere. /// /// The one question docs/config.md cannot answer, because the answer depends /// on the machine: XDG_CONFIG_HOME if it is set and absolute, else /// ~/Library/Application Support/pardes/init on macOS and /// ~/.config/pardes/init everywhere else. Printing it beats documenting it — /// the row is ordinary text, so a right click on it opens the file, and when /// there is no file there yet the path is still exactly what you needed to /// know. pub const Config = struct { pub const output: OutputTraits = .{ .name = config.config_buffer }; pub fn run(c: Ctx) void { output_pane.openConfig(c.p, c.id) catch |err| c.p.reportError(c.id, "config", err); } }; /// This build's version and what changed to reach it, printed into an output /// buffer the same way Config prints the live settings. pub const Changelog = struct { pub const output: OutputTraits = .{ .name = config.changelog_buffer }; pub fn run(c: Ctx) void { output_pane.openChangelog(c.p, c.id) catch |err| c.p.reportError(c.id, "changelog", err); } }; /// Source code for the concrete backend implementation of a Panel*/scene /// effect. The bytes are embedded at build time, so this works from an /// installed executable rather than depending on a source checkout. pub const EffectCode = struct { pub const takes_arg = true; pub const enabled = capabilities.panel_transitions or capabilities.scene_shaders; pub const output: OutputTraits = .{ .name = config.effect_code_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) output_pane.openEffectCode(c.p, c.id, c.arg orelse return) catch |err| c.p.reportError(c.id, "effect code", err) else unreachable; } }; // ---- search ---- // The two builtins that ASK for something — Find walks file NAMES under this // pane's directory, Grep file CONTENTS under every pane's. With an argument // there is nothing to ask: it IS the pattern, so the walk runs now (this is // what a `Grep` executed with a selection chorded to it means). Without one // they arm the same tag input `/` does, and Enter runs it (submitSearch). pub const Find = struct { pub const takes_arg = true; pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top) catch |err| c.p.reportError(c.id, "find", err); c.p.startSearch(c.pane, config.find_marker); } }; /// Find's sibling: Find matches file NAMES under this pane's directory, Grep /// matches file CONTENTS under every pane's directory at once. pub const Grep = struct { pub const takes_arg = true; pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top) catch |err| c.p.reportError(c.id, "grep", err); c.p.startSearch(c.pane, config.grep_marker); } }; // ---- the window group ---- // The DESTINATION is the name — a word, the way a tag holds Del or Save — // because these names live in the same vocabulary as everything else here: // `Wh` would be a leader key path leaking into the text you can middle-click. // Plain English words are safe for exactly these five: focus is the cheapest // thing to change by accident (nothing is edited, closed or written) and the // way back is the opposite word. pub const Left = struct { pub fn run(c: Ctx) void { c.p.focusDir(c.id, .left); } }; pub const Down = struct { pub fn run(c: Ctx) void { c.p.focusDir(c.id, .down); } }; pub const Up = struct { pub fn run(c: Ctx) void { c.p.focusDir(c.id, .up); } }; pub const Right = struct { pub fn run(c: Ctx) void { c.p.focusDir(c.id, .right); } }; // ---- the jump group ---- // Where focus HAS BEEN, as three verbs and a list over the one stack pardes // keeps (Pardes.jumps — see trackJump for what gets onto it). Builtins rather // than bare key handlers for the same reason the four directions are: one // implementation, reachable by chord, by `SPC j ...`, and by executing the // word wherever it is written. /// Ctrl-o: one step back into the history. pub const Back = struct { pub fn run(c: Ctx) void { c.p.jumpBy(-1); } }; /// Ctrl-i: one step forward again, up to wherever Back started. pub const Forward = struct { pub fn run(c: Ctx) void { c.p.jumpBy(1); } }; /// vim's Ctrl-^: the pane you were in before this one, whichever it was — the /// hop you press twice a minute and never want to count steps for. Body-normal /// Esc is this, which is what makes alternating between two panes one key you /// hold down: two files, or a file and its shell, or a file and a +Search. /// /// It does NOT move the stack cursor: it goes somewhere, so trackJump records /// it like any other move, and that is exactly what makes it an involution — /// after the hop, the pane you came from is the newest OTHER pane, so pressing /// it again comes straight back. Back/Forward walk history; this one makes it. /// /// It replaced a `Toggleterm` that hopped specifically between the newest DOC /// and the newest TERMINAL. That distinction never earned its keep: it made Esc /// unpredictable (which of three panes you landed on depended on their kinds), /// and it could not alternate between two files at all — the case you hit most. /// "The pane before this one" needs no kinds and is the same key twice. pub const Last = struct { pub fn run(c: Ctx) void { var i = c.p.njumps; while (i > 0) { i -= 1; const j = c.p.jumps[i]; if (j.pane != c.id) return c.p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }); } } }; /// The same stack, as text you can read and click. Not a copy of it and not a /// second list kept in step — the buffer is RENDERED from the stack when you /// ask, the way +Search is rendered from a walk. pub const Jumplist = struct { pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true }; pub fn run(c: Ctx) void { output_pane.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err); } }; // ---- the language group ---- // Reached as `SPC l ` — see leader_path for why the prefix // exists. They are builtins rather than bare keys for the same reason Save is // one: the word is executable wherever it appears, so a middle-click on // `Hover` in a tag does what `SPC l k` does. The five GOTOS are not here — // helix binds them under `g` as motions, and a motion has no business being a // word you can click. // // Most of them are one call: ask, and let the answer land in lspResponse. // Nothing here blocks or knows what a backend is — swapping backends changes // lsp.query and not one line below. pub const Hover = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .hover, ""); } }; pub const CodeAction = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .code_action, ""); } }; pub const SelectRefs = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .select_refs, ""); } }; pub const Symbols = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .document_symbols, ""); } }; pub const Diagnostics = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .diagnostics, ""); } }; pub const WsDiagnostics = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .workspace_diagnostics, ""); } }; // The two that need a word from the user, handled exactly the way Find and // Grep are: an argument means run it now (a selection chorded onto the name), // no argument arms the tag input and Enter submits (submitSearch). pub const Rename = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (a.len > 0) return c.p.lspRequest(c.id, .rename, a); c.p.startSearch(c.pane, config.rename_marker); } }; pub const WsSymbols = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a); c.p.startSearch(c.pane, config.symbol_marker); } }; // Introspection. A language backend that answers nothing looks exactly like // one that is broken — from the outside, `gd` doing nothing is both "there is // no definition" and "the analyser threw and we swallowed it". These two are // how you tell: Lspinfo says what the backend IS, Lspwhy says what it just DID // and where it stopped. pub const Lspinfo = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .status, ""); } }; pub const Lspwhy = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .explain, ""); } }; // ---- the machine's address space (bare metal only) ---- // // Three words gated by `board_memory.enabled`, which is a fact about the // TARGET (freestanding, and not wasm) rather than about `pardes.platform` — // see the reasoning there. Today that is exactly `-Dplatform=p4`; what makes // it the right predicate is that a second bare-metal port gets them without // anyone remembering to add an enum arm, and the browser never does. // Elsewhere they are absent from the command enum, the help index, the leader // table and the dispatcher, which is the gate ThemeFile and DumpThemes // already use. // // They are not a debugger and not a privilege: with no OS there is no MMU, no // supervisor and no process, so pardes IS the system software and all 2^32 // addresses are already its own. RAM, the peripheral registers behind the // console it is talking to you over, and its own .text are one flat space, and // a word that could reach only part of it would be pretending to be an // application. What you actually reach for these for is the case a hosted // editor never has: the display did not come up, and the question is whether // the register you thought you wrote holds what you thought you wrote. // // Implementation, parsing, the volatile accesses and the clamp are all in // board_memory.zig, the way the PDF words live in pdf_pane.zig — these three // structs are the words, their argument contract, and where the answer goes. /// `Peek [count]` — count 32-bit words (default 1) as `addr: value` /// rows, hex or decimal address, refused rather than trapped when unaligned. pub const Peek = struct { pub const takes_arg = true; pub const enabled = board_memory.enabled; pub const output: OutputTraits = .{ .name = config.peek_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { board_memory.peek(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "peek", err); } }; /// `Poke ` — one 32-bit store, answered on the message row with /// the value written AND the value that reads back, which on MMIO is the /// interesting half (see board_memory.poke). pub const Poke = struct { pub const takes_arg = true; pub const enabled = board_memory.enabled; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { board_memory.poke(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "poke", err); } }; /// `Hexdump [len]` — len bytes (default 256) in `hexdump -C`'s layout. pub const Hexdump = struct { pub const takes_arg = true; pub const enabled = board_memory.enabled; pub const output: OutputTraits = .{ .name = config.hexdump_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { board_memory.hexdump(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "hexdump", err); } };