summaryrefslogtreecommitdiff
path: root/src/builtins.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-06 18:11:36 -0300
committerGabriel Schneider <[email protected]>2026-09-07 13:59:12 -0300
commit60367d8fe23f6af98ec28e3cf6c2094dfe332df0 (patch)
tree310fc734173cf771881f4691c71909135fadde97 /src/builtins.zig
parentfa82cac885cb4738fe36d1e49b4749b5a3e31a4a (diff)
downloadpardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.tar.gz
pardes-60367d8fe23f6af98ec28e3cf6c2094dfe332df0.zip
Refactor panes and filesystem; replace FUSE with 9P
Consolidate pane, layout, memory and host code. Serve 9P by default over Unix sockets, with runtime mounts and optional TCP/QUIC transports. Remove FUSE and obsolete proof-of-concept examples. Fix highlighting and terminal-history performance, expand differential and stress-test infrastructure, sort navigation results while preserving the next occurrence, add syntax-colored Braille minimaps, remove SPC-k, and document 9P interaction as a repository skill.
Diffstat (limited to 'src/builtins.zig')
-rw-r--r--src/builtins.zig863
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);
}
};