diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-06 18:11:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-07 13:59:12 -0300 |
| commit | 60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch) | |
| tree | 310fc734173cf771881f4691c71909135fadde97 /src/builtins.zig | |
| parent | fa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff) | |
| download | pardes-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/builtins.zig')
| -rw-r--r-- | src/builtins.zig | 863 |
1 files changed, 369 insertions, 494 deletions
diff --git a/src/builtins.zig b/src/builtins.zig index f7822caf..e55a418f 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -1,48 +1,17 @@ -//! 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. +//! Command structs and runtime settings form the builtin registry; bindings live in config.zig. 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 panes = @import("panes.zig"); +const layout = @import("layout.zig"); const config = @import("config.zig"); -const runtime_config = @import("runtime_config.zig"); -const board_memory = @import("board_memory.zig"); -/// The host half of the 9P client, for the `9p` word at the bottom. Imported -/// unconditionally and gated on `fs9_client.supported`, exactly like -/// board_memory above: nothing in it is analysed for a build whose platform -/// has no unix sockets, because the word is not registered there at all. -const fs9_client = @import("fs9_client.zig"); +const builtin = @import("builtin"); +const Header = @import("esp32p4_gpio.zig").Header; +const limits = @import("memory.zig").limits; -/// 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 = .{ +pub const capabilities: config.Runtime.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 @@ -50,26 +19,14 @@ pub const capabilities: runtime_config.Capabilities = .{ .tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4, }; -/// 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; } @@ -108,18 +65,18 @@ fn manualBuiltinList() [manualBuiltinCount()]type { fn settingCount() comptime_int { comptime { var count = 0; - for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) { + for (config.Runtime.settings) |setting| if (setting.enabled(capabilities)) { count += 1; }; return count; } } -fn settingList() [settingCount()]runtime_config.Setting { +fn settingList() [settingCount()]config.Runtime.Setting { comptime { - var list: [settingCount()]runtime_config.Setting = undefined; + var list: [settingCount()]config.Runtime.Setting = undefined; var count = 0; - for (runtime_config.settings) |setting| if (setting.enabled(capabilities)) { + for (config.Runtime.settings) |setting| if (setting.enabled(capabilities)) { list[count] = setting; count += 1; }; @@ -127,11 +84,6 @@ fn settingList() [settingCount()]runtime_config.Setting { } } -/// 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; @@ -144,28 +96,14 @@ pub const OutputTraits = struct { 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. + // Saving promotes this scratch buffer into an ordinary file. 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. +// Keep enum-dependent signatures out of the outer declaration walk. 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. + // Duplicate-name validation compares every pair. @setEvalBranchQuota(200_000); const manual = manualBuiltinList(); const generated = settingList(); @@ -206,7 +144,7 @@ pub const registry = struct { test "capabilities exactly gate setting and effect-source builtins" { const Builtin = registry.Builtin(); - for (runtime_config.settings) |setting| { + for (config.Runtime.settings) |setting| { const registered = std.meta.stringToEnum(Builtin, setting.word) != null; try std.testing.expectEqual(setting.enabled(capabilities), registered); } @@ -214,34 +152,15 @@ test "capabilities exactly gate setting and effect-source builtins" { 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( + if (@hasField(registry.Builtin(), name) != Board.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 { @@ -276,38 +195,19 @@ pub const Dump = struct { 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); + const path = c.arg 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]; } }; -/// `Attach [name]` — hand this frontend's screen to a detached core, the one -/// `pardes --detach [name]` left running. Bare, it means "the session that is -/// there", which is the case worth typing: one detached session, and one word -/// to walk back into it. -/// -/// Nothing is torn down HERE, and that is the feature rather than an omission. -/// The effect only ASKS; the shell connects first and swaps second, so an -/// Attach that reaches nothing leaves this instance with every pane and every -/// undo exactly where they were and a line on the message row. Absent where -/// there is no unix socket to attach to — the browser and the board — and also -/// absent where the frontend would never NOTICE the request: see -/// `pardes.can_attach`, which is narrower than `hosted` because macOS never -/// polls `takeAttach`, so the word would have queued an effect and then done -/// nothing at all. pub const Attach = struct { pub const takes_arg = true; pub const enabled = pardes.can_attach; pub fn run(c: Ctx) void { if (comptime enabled) ask(c) else unreachable; } - /// A name too long for `Effect.attach` is too long for `sun_path` several - /// times over, so it can never name a session: reporting it here is the - /// same answer a failed connect gets, one round trip earlier. fn ask(c: Ctx) void { const name = c.arg orelse ""; if (name.len > pardes.attach_name_max) @@ -316,17 +216,6 @@ pub const Attach = struct { } }; -/// `Detach` — leave the session and let it carry on without you, which is -/// tmux's detach-client. Executed inside an ATTACHED frontend, where it -/// travels to the daemon as an ordinary command line, is run by the core that -/// owns the panes, and comes back as the effect that dismisses the screen -/// which asked for it. Hence no argument: the daemon knows who typed. -/// -/// It is NOT `Attach` backwards, and no word here is. Making a live local -/// session outlive its terminal means setsid and a fork; a word that pretended -/// to would hand you a session that dies with the window it was typed in. Run -/// locally this therefore REPORTS rather than acts — see the `.detach` arm of -/// Pardes.perform, which finds no host method to call. pub const Detach = struct { pub const enabled = pardes.can_attach; pub fn run(c: Ctx) void { @@ -337,26 +226,33 @@ pub const Detach = struct { } }; +pub const Mount = struct { + pub const takes_arg = true; + pub const enabled = pardes.hosted; + pub fn run(c: Ctx) void { + if (comptime !enabled) unreachable; + var args = std.mem.tokenizeAny(u8, c.arg orelse "", " \t"); + const name = args.next() orelse return c.p.reportError(c.id, "Mount name dial", error.MissingArgument); + const dial = std.mem.trim(u8, args.rest(), " \t"); + if (dial.len == 0) return c.p.reportError(c.id, "Mount name dial", error.MissingArgument); + c.p.fs.mount(c.p.gpa, name, dial) catch |err| return c.p.reportError(c.id, "Mount", err); + } +}; + +pub const Unmount = struct { + pub const takes_arg = true; + pub const enabled = pardes.hosted; + pub fn run(c: Ctx) void { + if (comptime !enabled) unreachable; + var args = std.mem.tokenizeAny(u8, c.arg orelse "", " \t"); + const name = args.next() orelse return c.p.reportError(c.id, "Unmount name", error.MissingArgument); + if (args.next() != null) return c.p.reportError(c.id, "Unmount name", error.TooManyArguments); + pardes.filesystem.unmount(c.p, name) catch |err| return c.p.reportError(c.id, "Unmount", err); + } +}; + // ---- 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 -/// `<operation>: <Error>` — 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 { @@ -367,53 +263,19 @@ pub const Msg = struct { } }; -// ---- 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 <name>` 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); + panes.Output.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; @@ -425,9 +287,6 @@ pub const ThemeFile = struct { } }; -/// Materialize every compiled theme as editable ZON under -/// `<config>/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 { @@ -441,33 +300,6 @@ pub const DumpThemes = struct { } }; -// ---- 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 <name>` -/// 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 <name>` 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; @@ -475,39 +307,30 @@ pub const FontSel = struct { 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); + panes.Output.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); + panes.Pdf.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); + panes.Pdf.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; @@ -515,60 +338,33 @@ pub const PdfSections = struct { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { - pardes.pdf_pane.openSections(c.p, c.id); + panes.Pdf.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); + if (c.pane.image) |*state| panes.Image.toggleGlyphArt(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); + if (c.pane.image) |*state| panes.Image.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); + if (c.pane.image) |*state| panes.Image.toggleAscii(state); } }; // ---- the system clipboard ---- -// helix's `<space>` 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); @@ -603,33 +399,16 @@ pub const ClipReplace = struct { // ---- 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<Tab>` 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); + if (c.pane.file != null or c.pane.isTerminal()) c.p.startPrompt(c.pane, .save); } }; -/// 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 { @@ -646,22 +425,10 @@ pub const Newcol = struct { 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; - }; + c.p.removePane(c.id) catch |err| c.p.reportError(c.id, "close", err); } }; -/// 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; @@ -671,22 +438,7 @@ pub const Filter = struct { 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; - }; + c.p.removeColumn(c.id) catch |err| c.p.reportError(c.id, "close column", err); } }; @@ -702,7 +454,7 @@ pub const Newtty = struct { /// The horizontal mirror of the vertical stacking `New` does. pub const Joincol = struct { pub fn run(c: Ctx) void { - c.p.joinCol(); + layout.joinCol(c.p); } }; @@ -717,35 +469,21 @@ pub const Tutor = struct { 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); + panes.Output.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); + panes.Output.openConfig(c.p, c.id) catch |err| c.p.reportError(c.id, "config", err); } }; -/// Read back what the message rows said. A message row is cleared by the next -/// keystroke, so anything reported while you were looking at another pane was -/// gone before you could read it — a failed save, a watcher's reload, a -/// builtin's complaint. pub const Messages = struct { pub const output: OutputTraits = .{ .name = config.messages_buffer }; pub fn run(c: Ctx) void { - output_pane.openMessages(c.p, c.id) catch |err| c.p.reportError(c.id, "messages", err); + panes.Output.openMessages(c.p, c.id) catch |err| c.p.reportError(c.id, "messages", err); } }; @@ -754,33 +492,34 @@ pub const Messages = struct { 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); + panes.Output.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| + panes.Output.openEffectCode(c.p, c.id, c.arg orelse return) catch |err| c.p.reportError(c.id, "effect code", err) else unreachable; } }; -// ---- search ---- +pub const Mini = struct { + pub const takes_arg = true; + pub const output: OutputTraits = .{ .name = "Mini", .doc = true }; -// 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 fn run(c: Ctx) void { + panes.Mini.open(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "mini", err); + } +}; + +// ---- search ---- pub const Find = struct { pub const takes_arg = true; @@ -789,7 +528,7 @@ pub const Find = struct { 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); + c.p.startPrompt(c.pane, .{ .search = config.find_marker }); } }; @@ -802,51 +541,38 @@ pub const Grep = struct { 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); + c.p.startPrompt(c.pane, .{ .search = 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); + layout.focusDir(c.p, c.id, .left); } }; pub const Down = struct { pub fn run(c: Ctx) void { - c.p.focusDir(c.id, .down); + layout.focusDir(c.p, c.id, .down); } }; pub const Up = struct { pub fn run(c: Ctx) void { - c.p.focusDir(c.id, .up); + layout.focusDir(c.p, c.id, .up); } }; pub const Right = struct { pub fn run(c: Ctx) void { - c.p.focusDir(c.id, .right); + layout.focusDir(c.p, 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 { @@ -861,65 +587,27 @@ pub const Forward = struct { } }; -/// 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]; - // `.keep`: Esc is a RETURN, and the pane still holds the view it - // was left with. Recentring it moved the whole screen to show a line - // that was, nearly always, already on it. - // - // Not `line = 0`, which focusPaneLine already understands as "focus - // and touch nothing": a background pane's view CAN move while you - // are away — the wheel scrolls the pane under the pointer, not the - // active one, and a resize recomputes geometry without revealing any - // cursor — so `.keep` restores the recorded cursor and lets - // ensureCursorVisible pull it back on screen by the least it can. + // Restore the cursor without recentering the pane's retained view. if (j.pane != c.id) return c.p.focusPaneLine(j.pane, .{ .line = j.line, .col = j.col }, .keep); } } }; -/// 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); + panes.Output.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err); } }; // ---- the language group ---- -// Reached as `SPC l <helix's letter>` — 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, ""); @@ -956,10 +644,6 @@ pub const WsDiagnostics = struct { } }; -// The four hierarchy words, protocol-only (LSP 3.16/3.17): the in-process -// Zig backend has no analyser for them, so in a `.zig` pane they answer -// nothing. helix has no binding for any of the four. - pub const Callers = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .incoming_calls, ""); @@ -984,16 +668,12 @@ pub const Subtypes = struct { } }; -// 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); + c.p.startPrompt(c.pane, .{ .search = config.rename_marker }); } }; @@ -1002,16 +682,10 @@ pub const WsSymbols = struct { 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); + c.p.startPrompt(c.pane, .{ .search = 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, ""); @@ -1024,123 +698,324 @@ pub const Lspwhy = struct { } }; -// ---- 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=esp32p4`; 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 <addr> [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 enabled = Board.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); + if (comptime enabled) { + Board.peek(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "peek", err); + } else unreachable; } }; -/// `Poke <addr> <value>` — 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 const enabled = Board.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); + if (comptime enabled) { + Board.poke(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "poke", err); + } else unreachable; } }; -/// `Hexdump <addr> [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 enabled = Board.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); + if (comptime enabled) { + Board.hexdump(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "hexdump", err); + } else unreachable; } }; -/// `Gpio <pin>` — flip one pad, answered on the message row as `0->1`. Bare `Gpio` draws JP1's -/// pinout into a pane instead, because the first question about a header is which pins it has. -/// -/// The only word here whose argument is DECIMAL, and `board_memory.gpio` says why at length: a -/// GPIO number is part of a name, not an address. pub const Gpio = struct { pub const takes_arg = true; - pub const enabled = board_memory.enabled; + pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.gpio_buffer }; pub fn run(c: Ctx) void { - if (comptime enabled) apply(c) else unreachable; - } - fn apply(c: Ctx) void { - board_memory.gpio(c.p, c.id, c.arg orelse "") catch |err| - c.p.reportError(c.id, "gpio", err); + if (comptime enabled) { + Board.gpio(c.p, c.id, c.arg orelse "") catch |err| + c.p.reportError(c.id, "gpio", err); + } else unreachable; } }; -// ---- somebody else's tree ---- +const Board = struct { + const enabled = pardes.platform == .esp32p4; -/// `9p <dial> <path>` — walk to a file in ANOTHER pardes's tree, read it, and -/// open the bytes in a pane. -/// -/// THE OTHER END OF `--fs9`, and the reason the client in `src/9p.zig` is not a -/// library with no caller: one pardes serves acme's control filesystem over -/// 9P2000 on a unix socket, and this word is the second one reading it. `9p -/// work /1/body` shows you what pane 1 of the session called `work` is holding, -/// from a pane in this session, with no mount and no `plan9port` in the way. -/// -/// A DIAL IS A NAME OR A PATH: `work` resolves through the same -/// `fs9_service.socketPath` that bound it, and anything with a `/` in it is a -/// socket path taken as given. Unix sockets only for now — a 9P server across a -/// network is tunnelled (docs/9p.typ §10), and this word is not the place to -/// decide otherwise. -/// -/// It BLOCKS while it fetches, bounded by `fs9_client.budget_ms`, exactly the -/// way Look blocks on a disk read; `src/fs9_client.zig` argues that at length -/// and enforces it with a deadline rather than a promise. -pub const @"9p" = struct { - pub const takes_arg = true; - pub const enabled = fs9_client.supported; - /// Prose and not a list: the bytes are a file's, so n/N walks its words the - /// way it walks any document's, and there is nothing here to step to. - pub const output: OutputTraits = .{ .name = config.ninep_buffer, .doc = true }; - pub fn run(c: Ctx) void { - if (comptime enabled) apply(c) else unreachable; + comptime { + if (enabled and pardes.hosted) @compileError("an OS is not bare metal"); + if (enabled and builtin.os.tag != .freestanding) @compileError("the P4 firmware is freestanding"); + if (enabled and builtin.target.cpu.arch.isWasm()) @compileError("wasm addresses are not a bus"); } - fn apply(c: Ctx) void { - fs9_client.fetch(c.p, c.id, c.arg orelse "") catch |err| - c.p.reportError(c.id, "9p", err); + + // Bound output sent over the board's 115200-baud console. + const max_bytes: u32 = 4096; + const max_words: u32 = max_bytes / 4; + + const address_space_end: u64 = 1 << 32; + + const Error = error{ + MissingAddress, + BadAddress, + BadCount, + MissingValue, + BadValue, + MisalignedAddress, + ExtraArgument, + BadPin, + NoPads, + }; + + fn parseHex(comptime T: type, tok: []const u8, bad: Error) Error!T { + const body = if (tok.len > 2 and tok[0] == '0' and (tok[1] | 0x20) == 'x') tok[2..] else tok; + return std.fmt.parseInt(T, body, 16) catch bad; + } + + fn parseAddr(tok: []const u8) Error!u32 { + return parseHex(u32, tok, Error.BadAddress); + } + + fn parseCount(tok: []const u8) Error!u64 { + return parseHex(u64, tok, Error.BadCount); + } + + fn parseValue(tok: []const u8) Error!u32 { + return parseHex(u32, tok, Error.BadValue); + } + + // Callers reject unaligned words; volatile accesses must reach the device. + fn readWord(addr: u32) u32 { + const cell: *allowzero const volatile u32 = @ptrFromInt(@as(usize, addr)); + return cell.*; + } + + fn writeWord(addr: u32, value: u32) void { + const cell: *allowzero volatile u32 = @ptrFromInt(@as(usize, addr)); + cell.* = value; + } + + fn readByte(addr: u32) u8 { + const cell: *allowzero const volatile u8 = @ptrFromInt(@as(usize, addr)); + return cell.*; + } + + const Limit = enum { + console, + space, + }; + + const Extent = struct { + count: u32, + limit: ?Limit, + }; + + fn extent(addr: u32, requested: u64, unit: u32, cap: u32) Extent { + var count = requested; + var limit: ?Limit = null; + if (count > cap) { + count = cap; + limit = .console; + } + const fits = (address_space_end - addr) / unit; + if (count > fits) { + count = fits; + limit = .space; + } + return .{ .count = @intCast(count), .limit = limit }; + } + + fn writeNote(w: *std.Io.Writer, e: Extent, requested: u64, unit_name: []const u8) !void { + switch (e.limit orelse return) { + .console => try w.print( + "clamped: 0x{x} {s} requested, 0x{x} shown (0x{x}-byte cap, one 115200-baud console)\n", + .{ requested, unit_name, e.count, max_bytes }, + ), + .space => try w.print( + "clamped: 0x{x} {s} requested, 0x{x} shown (the 32-bit address space ends at 0x100000000)\n", + .{ requested, unit_name, e.count }, + ), + } + } + + fn peek(p: *Pardes, id: usize, argument: []const u8) !void { + var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); + const addr = try parseAddr(it.next() orelse return Error.MissingAddress); + const requested = if (it.next()) |tok| try parseCount(tok) else 0x1; + if (it.next() != null) return Error.ExtraArgument; + if (addr % 4 != 0) return Error.MisalignedAddress; + + const e = extent(addr, requested, 4, max_words); + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); + try writeNote(&out.writer, e, requested, "words"); + for (0..e.count) |i| { + const at = addr + @as(u32, @intCast(i * 4)); + try out.writer.print("{x:0>8}: {x:0>8}\n", .{ at, readWord(at) }); + } + const content = try out.toOwnedSlice(); + try fill(p, id, .{ .cmd = .Peek }, content); + } + + fn poke(p: *Pardes, id: usize, argument: []const u8) !void { + var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); + const addr = try parseAddr(it.next() orelse return Error.MissingAddress); + const value = try parseValue(it.next() orelse return Error.MissingValue); + if (it.next() != null) return Error.ExtraArgument; + if (addr % 4 != 0) return Error.MisalignedAddress; + + writeWord(addr, value); + const back = readWord(addr); + var buf: [96]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint( + &buf, + "{x:0>8}: wrote {x:0>8}, reads {x:0>8}", + .{ addr, value, back }, + ) catch unreachable); + } + + fn gpio(p: *Pardes, id: usize, argument: []const u8) !void { + var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); + const tok = it.next() orelse { + const content = try p.gpa.dupe(u8, Header.text); + return fill(p, id, .{ .cmd = .Gpio }, content); + }; + if (it.next() != null) return Error.ExtraArgument; + const pin = std.fmt.parseInt(u16, tok, 10) catch return Error.BadPin; + + const toggle = p.host.vtable.gpio_toggle orelse return Error.NoPads; + var was: u8 = 0; + var now: u8 = 0; + if (!toggle(p.host.ctx, pin, &was, &now)) return Error.BadPin; + + var buf: [48]u8 = undefined; + p.setMessage(id, std.fmt.bufPrint(&buf, "GPIO {d}: {d}->{d}", .{ pin, was, now }) catch unreachable); + } + + const row_bytes: u32 = limits.hexdump_row_bytes; + + fn hexdump(p: *Pardes, id: usize, argument: []const u8) !void { + var it = std.mem.tokenizeAny(u8, argument, " \t\r\n"); + const addr = try parseAddr(it.next() orelse return Error.MissingAddress); + const requested = if (it.next()) |tok| try parseCount(tok) else 0x100; + if (it.next() != null) return Error.ExtraArgument; + + const e = extent(addr, requested, 1, max_bytes); + var out: std.Io.Writer.Allocating = .init(p.gpa); + errdefer out.deinit(); + try writeNote(&out.writer, e, requested, "bytes"); + var row: u32 = 0; + while (row < e.count) : (row += row_bytes) { + const n = @min(row_bytes, e.count - row); + var bytes: [row_bytes]u8 = undefined; + for (0..n) |i| bytes[i] = readByte(addr + row + @as(u32, @intCast(i))); + try out.writer.print("{x:0>8} ", .{addr + row}); + for (0..row_bytes) |i| { + if (i == row_bytes / 2) try out.writer.writeByte(' '); + if (i < n) + try out.writer.print(" {x:0>2}", .{bytes[i]}) + else + try out.writer.writeAll(" "); + } + try out.writer.writeAll(" |"); + for (0..n) |i| try out.writer.writeByte( + if (bytes[i] >= 0x20 and bytes[i] < 0x7f) bytes[i] else '.', + ); + try out.writer.writeAll("|\n"); + } + const content = try out.toOwnedSlice(); + try fill(p, id, .{ .cmd = .Hexdump }, content); + } + + fn fill(p: *Pardes, id: usize, from: panes.Output.Origin, content: []u8) !void { + const pane = p.panes[id] orelse { + p.gpa.free(content); + return error.MissingPane; + }; + const dir = if (pane.file) |f| (std.fs.path.dirname(f.path) orelse "/") else pane.cwdSlice(); + try panes.Output.fillResults(p, id, dir, from, "", content, null); + } + + test "the clamp reports the tighter bound and never wraps the address space" { + const eq = std.testing.expectEqual; + try eq(Extent{ .count = 3, .limit = null }, extent(0x4ff40000, 3, 4, max_words)); + try eq(Extent{ .count = max_words, .limit = .console }, extent(0x4ff40000, 99_999, 4, max_words)); + try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0, 100_000, 1, max_bytes)); + try eq(Extent{ .count = 16, .limit = .space }, extent(0xfffffff0, 64, 1, max_bytes)); + try eq(Extent{ .count = 4, .limit = .space }, extent(0xfffffff0, 64, 4, max_words)); + try eq(Extent{ .count = 0, .limit = .space }, extent(0xffffffff, 1, 4, max_words)); + try eq(Extent{ .count = max_bytes, .limit = .console }, extent(0xffff0000, 1 << 20, 1, max_bytes)); + } + + test "a clamp note is written exactly when something was clamped" { + var buf: [256]u8 = undefined; + var w: std.Io.Writer = .fixed(&buf); + + try writeNote(&w, extent(0x4ff40000, 3, 4, max_words), 3, "words"); + try std.testing.expectEqualStrings("", w.buffered()); + + try writeNote(&w, extent(0x4ff40000, 99_999, 4, max_words), 99_999, "words"); + try std.testing.expectEqualStrings( + "clamped: 0x1869f words requested, 0x400 shown (0x1000-byte cap, one 115200-baud console)\n", + w.buffered(), + ); + + w = .fixed(&buf); + try writeNote(&w, extent(0xfffffff0, 64, 1, max_bytes), 64, "bytes"); + try std.testing.expectEqualStrings( + "clamped: 0x40 bytes requested, 0x10 shown (the 32-bit address space ends at 0x100000000)\n", + w.buffered(), + ); + } + + test "every literal is hex, with or without the prefix" { + const eq = std.testing.expectEqual; + try eq(0x4ff40000, parseAddr("0x4ff40000")); + try eq(0x4ff40000, parseAddr("4ff40000")); + try eq(0x4ff40000, parseAddr("0X4FF40000")); + try eq(0x4ff40000, parseAddr("4FF40000")); + try eq(0x100, parseCount("100")); + try eq(0x256, parseCount("256")); + try eq(0xdeadbeef, parseValue("deadbeef")); + try std.testing.expectError(Error.BadAddress, parseAddr("0x100000000")); + try std.testing.expectError(Error.BadAddress, parseAddr("0x")); + try std.testing.expectError(Error.BadAddress, parseAddr("nope")); + try std.testing.expectError(Error.BadAddress, parseAddr("12g4")); + try std.testing.expectError(Error.BadCount, parseCount("-1")); + try std.testing.expectError(Error.BadValue, parseValue("0x1_0000_0000")); + } + + test "the pinout fits the board's own grid, in two aligned columns" { + const cols: usize = @import("pardes_config").esp32p4_cols; + const usable = cols - 7; + + var rows: usize = 0; + var pins: usize = 0; + var first_bar: ?usize = null; + var it = std.mem.splitScalar(u8, Header.text, '\n'); + while (it.next()) |line| { + try std.testing.expect(line.len <= usable); + rows += 1; + const bar = std.mem.indexOfScalar(u8, line, '|') orelse continue; + if (line[line.len - 1] == '+') continue; + pins += 1; + if (first_bar) |b| try std.testing.expectEqual(b, bar) else first_bar = bar; + } + try std.testing.expectEqual(@as(usize, 13), pins); + try std.testing.expect(rows > 15); + + try std.testing.expect(std.mem.indexOf(u8, Header.text, "GPIO 20 | 17 |") != null); + try std.testing.expect(std.mem.indexOf(u8, Header.text, "| 8 | --") != null); + } + + test "board addresses are hexadecimal and GPIO numbers are decimal" { + try std.testing.expectEqual(@as(u16, 20), try std.fmt.parseInt(u16, "20", 10)); + try std.testing.expectEqual(@as(u32, 0x20), try parseAddr("20")); + try std.testing.expect(20 != 0x20); } }; |
