//! 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 panes = @import("panes.zig"); const layout = @import("layout.zig"); const config = @import("config.zig"); const builtin = @import("builtin"); const Header = @import("esp32p4_gpio.zig").Header; const limits = @import("memory.zig").limits; pub const capabilities: config.Runtime.Capabilities = .{ .font_picker = pardes.font_picker, .panel_transitions = pardes.hosted, .scene_shaders = pardes.platform == .gui or pardes.platform == .macos, // The tty's font belongs to its emulator, and the P4 firmware's belongs to // whatever terminal is on the other end of the serial line. .tagline_font_size = pardes.platform != .tty and pardes.platform != .esp32p4, .window_opacity = pardes.platform == .gui or pardes.platform == .macos, .window_blur = pardes.platform == .macos, }; pub const Ctx = struct { p: *Pardes, pane: *Pane, id: usize, txt: []const u8, arg: ?[]const u8, }; fn isEnabled(comptime T: type) bool { return !@hasDecl(T, "enabled") or T.enabled; } fn manualBuiltinCount() comptime_int { comptime { var count = 0; for (@typeInfo(@This()).@"struct".decls) |d| { if (@TypeOf(@field(@This(), d.name)) != type) continue; const T = @field(@This(), d.name); if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; if (@TypeOf(T.run) != fn (Ctx) void) @compileError(d.name ++ ".run must have signature fn (Ctx) void"); count += 1; } return count; } } fn manualBuiltinList() [manualBuiltinCount()]type { comptime { @setEvalBranchQuota(4000); var list: [manualBuiltinCount()]type = undefined; var count = 0; for (@typeInfo(@This()).@"struct".decls) |d| { if (@TypeOf(@field(@This(), d.name)) != type) continue; const T = @field(@This(), d.name); if (@typeInfo(T) != .@"struct" or !@hasDecl(T, "run") or !isEnabled(T)) continue; list[count] = T; count += 1; } return list; } } fn settingCount() comptime_int { comptime { var count = 0; for (config.Runtime.settings) |setting| if (setting.enabled(capabilities)) { count += 1; }; return count; } } fn settingList() [settingCount()]config.Runtime.Setting { comptime { var list: [settingCount()]config.Runtime.Setting = undefined; var count = 0; for (config.Runtime.settings) |setting| if (setting.enabled(capabilities)) { list[count] = setting; count += 1; }; return list; } } pub fn word(comptime T: type) []const u8 { const n = @typeName(T); const dot = std.mem.lastIndexOfScalar(u8, n, '.') orelse return n; return n[dot + 1 ..]; } pub const OutputTraits = struct { name: []const u8, steps: bool = false, jumps: bool = false, // This producer emits location rows whose labels and source excerpts may be styled. locations: bool = false, commands: bool = false, doc: bool = false, // Saving promotes this scratch buffer into an ordinary file. saves: bool = false, }; // Keep enum-dependent signatures out of the outer declaration walk. pub const registry = struct { pub fn Builtin() type { // Duplicate-name validation compares every pair. @setEvalBranchQuota(200_000); const manual = manualBuiltinList(); const generated = settingList(); const count = manual.len + generated.len; const Tag = std.math.IntFittingRange(0, count - 1); var names: [count][]const u8 = undefined; for (manual, 0..) |T, i| names[i] = word(T); for (generated, manual.len..) |setting, i| names[i] = setting.word; for (names, 0..) |name, i| for (names[i + 1 ..]) |later| if (std.mem.eql(u8, name, later)) @compileError("duplicate builtin name: " ++ name); return @Enum(Tag, .exhaustive, &names, &std.simd.iota(Tag, count)); } pub fn takesArg(b: Builtin()) bool { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return @hasDecl(T, "takes_arg") and T.takes_arg; inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return setting.takesArg(); unreachable; } pub fn outputTraits(b: Builtin()) ?OutputTraits { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return if (@hasDecl(T, "output")) T.output else null; inline for (comptime settingList(), manualBuiltinCount()..) |_, i| if (@intFromEnum(b) == i) return null; unreachable; } pub fn dispatch(b: Builtin(), c: Ctx) void { inline for (manualBuiltinList(), 0..) |T, i| if (@intFromEnum(b) == i) return T.run(c); inline for (comptime settingList(), manualBuiltinCount()..) |setting, i| if (@intFromEnum(b) == i) return c.p.applySettingBuiltin(setting, c.arg); unreachable; } }; test "capabilities exactly gate setting and effect-source builtins" { const Builtin = registry.Builtin(); for (config.Runtime.settings) |setting| { const registered = std.meta.stringToEnum(Builtin, setting.word) != null; try std.testing.expectEqual(setting.enabled(capabilities), registered); } const effect_code_registered = std.meta.stringToEnum(Builtin, "EffectCode") != null; try std.testing.expectEqual(EffectCode.enabled, effect_code_registered); } comptime { for ([_][]const u8{ "Peek", "Poke", "Hexdump" }) |name| if (@hasField(registry.Builtin(), name) != Board.enabled) @compileError( "bare-metal memory word gating leaked: " ++ name, ); } // ---- the two acme verbs ---- pub const Look = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { c.p.lookAt(c.id, c.arg orelse return); } }; pub const Exec = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { // the destination pane is Look's business (it focuses what answered); // an execute deliberately leaves you where you were _ = c.p.execute(c.id, c.arg orelse return); } }; // ---- session ---- pub const Kill = struct { pub fn run(c: Ctx) void { c.p.quit = true; c.p.emit(.quit); } }; pub const Dump = struct { pub fn run(c: Ctx) void { c.p.dumpState() catch {}; } }; pub const Restore = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { 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]; } }; 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; } fn ask(c: Ctx) void { const name = c.arg orelse ""; if (name.len > pardes.attach_name_max) return c.p.reportError(c.id, comptime word(@This()), error.NameTooLong); c.p.emit(.{ .attach = .{ .pane = @intCast(c.id), .name = .from(name) } }); } }; pub const Detach = struct { pub const enabled = pardes.can_attach; pub fn run(c: Ctx) void { if (comptime enabled) c.p.emit(.{ .detach = .{ .pane = @intCast(c.id) } }) else unreachable; } }; 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 ---- pub const Msg = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { if (c.arg) |text| c.p.setMessage(c.id, text) else c.p.reportError(c.id, comptime word(@This()), error.NoMessage); } }; pub const NextColor = struct { pub fn run(c: Ctx) void { c.p.setThemeIndex((@as(usize, c.p.settings.theme) + 1) % pardes.themes.len); } }; pub const ThemeSel = struct { pub const output: OutputTraits = .{ .name = config.themes_buffer, .steps = true, .commands = true }; pub fn run(c: Ctx) void { panes.Output.openThemes(c.p, c.id) catch |err| c.p.reportError(c.id, "themes", err); } }; pub const ThemeFile = struct { pub const takes_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime enabled) c.p.requestThemeFile(c.id, c.arg orelse return) else unreachable; } }; pub const DumpThemes = struct { pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime enabled) { if (c.p.opts.config_dir == null) { c.p.reportError(c.id, "dump themes", error.NoConfigDirectory); return; } c.p.emit(.{ .dump_themes = .{ .pane = @intCast(c.id) } }); } else unreachable; } }; pub const FontSel = struct { pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true }; pub const enabled = capabilities.font_picker; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { panes.Output.openFonts(c.p, c.id) catch |err| c.p.reportError(c.id, "fonts", err); } }; pub const TreeContext = struct { pub const enabled = pardes.syntax.enabled; pub const takes_arg = true; pub fn run(c: Ctx) void { if (comptime !enabled) unreachable; if (!panes.File.supportsContext(c.pane)) return; const file = &c.pane.file.?; const arg = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); const on = if (arg.len == 0) !file.tree_context else if (std.mem.eql(u8, arg, "on")) true else if (std.mem.eql(u8, arg, "off")) false else { c.p.reportError(c.id, "TreeContext", error.ExpectedOnOrOff); return; }; file.tree_context = on; file.syntax_dirty = true; c.pane.wrap_n = 0; c.pane.context_rows = 0; c.pane.body_rows = 0; c.pane.body_visible_rows = 0; c.pane.context_row_limit = null; c.p.surface.body_layers[c.id].rows = 0; } }; 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 { panes.Pdf.toggleFit(c.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 { panes.Pdf.toggleTint(c.pane); } }; pub const PdfSections = struct { pub const output: OutputTraits = .{ .name = config.pdf_sections_buffer, .steps = true, .locations = true }; pub const enabled = pardes.pdf_enabled; pub fn run(c: Ctx) void { if (comptime enabled) apply(c) else unreachable; } fn apply(c: Ctx) void { panes.Pdf.openSections(c.p, c.id); } }; /// glyph art over the host's pixels pub const Petscii = struct { pub fn run(c: Ctx) void { 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| 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| panes.Image.toggleAscii(state); } }; // ---- the system clipboard ---- pub const ClipYank = struct { pub fn run(c: Ctx) void { c.p.clipYank(c.pane, false); } }; /// helix `Y`: the PRIMARY selection alone, where `SPC y` joins every /// cursor's. One cursor makes them the same word. pub const ClipYankMain = struct { pub fn run(c: Ctx) void { c.p.clipYank(c.pane, true); } }; pub const ClipPaste = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .after); } }; pub const ClipPasteBefore = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .before); } }; pub const ClipReplace = struct { pub fn run(c: Ctx) void { c.p.clipRequest(c.id, .replace); } }; // ---- panes and columns ---- 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.startPrompt(c.pane, .save); } }; pub const New = struct { pub const output: OutputTraits = .{ .name = config.scratch_buffer, .doc = true, .saves = true }; pub fn run(c: Ctx) void { c.p.newScratchBelow(c.id); } }; /// The same empty scratch, opened in a fresh column beside the calling pane. pub const Newcol = struct { pub fn run(c: Ctx) void { c.p.newScratchColumn(c.id); } }; pub const Del = struct { pub fn run(c: Ctx) void { c.p.removePane(c.id) catch |err| c.p.reportError(c.id, "close", err); } }; pub const Filter = struct { pub fn run(c: Ctx) void { if (!c.pane.isTerminal()) return; c.pane.tty_filter = !c.pane.tty_filter; } }; /// Cycle the pane's input modes: terminals add raw tty mode to normal/insert. pub const Mode = struct { pub fn run(c: Ctx) void { c.p.cycleMode(c.id); } }; /// Switch this terminal between raw program input and normal editor mode. pub const Togglettymode = struct { pub fn run(c: Ctx) void { c.p.toggleTty(c.id); } }; /// Fold a pane to its tag, or restore its share of the column. pub const Collapse = struct { pub fn run(c: Ctx) void { layout.toggleCollapse(c.p, c.id); } }; pub const Delcol = struct { pub fn run(c: Ctx) void { c.p.removeColumn(c.id) catch |err| c.p.reportError(c.id, "close column", err); } }; /// A shell in the calling pane's directory, raw from the first frame. On every /// pane's tagline: the fast path from wherever you are to a prompt there. pub const Tty = struct { pub fn run(c: Ctx) void { c.p.spawnTty(c.id); } }; /// A Linux terminal with the current session mounted through kernel v9fs. pub const Tty9p = struct { pub const enabled = pardes.hosted and @import("builtin").os.tag == .linux; pub fn run(c: Ctx) void { c.p.spawnV9fsTty(c.id); } }; /// Fold the active pane's column into the one on its right, keeping its panes. /// The horizontal mirror of the vertical stacking `New` does. pub const Joincol = struct { pub fn run(c: Ctx) void { layout.joinCol(c.p); } }; pub const Tutor = struct { pub fn run(c: Ctx) void { const free = c.p.freeSlot() orelse return; const nt = c.p.openTutorView(free) catch return; c.p.placeDoc(c.id, free, nt); // a doc like any other } }; pub const Help = struct { pub const output: OutputTraits = .{ .name = config.help_buffer }; pub fn run(c: Ctx) void { panes.Output.openHelp(c.p, c.id, "") catch |err| c.p.reportError(c.id, "help", err); } }; pub const Config = struct { pub const output: OutputTraits = .{ .name = config.config_buffer }; pub fn run(c: Ctx) void { panes.Output.openConfig(c.p, c.id) catch |err| c.p.reportError(c.id, "config", err); } }; pub const LocationsConfig = struct { pub const takes_arg = true; pub const output: OutputTraits = .{ .name = "+LocationsConfig" }; pub fn run(c: Ctx) void { const next = c.p.locations_config.parse(c.arg orelse "") catch |err| { c.p.reportError(c.id, "LocationsConfig", err); return; }; c.p.locations_config = next; report(c) catch |err| c.p.reportError(c.id, "LocationsConfig", err); } fn report(c: Ctx) !void { var out: std.Io.Writer.Allocating = .init(c.p.gpa); errdefer out.deinit(); try out.writer.writeAll("LocationsConfig "); try c.p.locations_config.write(&out.writer); try out.writer.writeByte('\n'); const content = try out.toOwnedSlice(); const dir = if (c.pane.file) |file| std.fs.path.dirname(file.path) orelse "/" else c.pane.cwdSlice(); try panes.Output.fillResults(c.p, c.id, dir, .{ .cmd = .LocationsConfig }, "", content, null); } }; pub const Messages = struct { pub const output: OutputTraits = .{ .name = config.messages_buffer }; pub fn run(c: Ctx) void { panes.Output.openMessages(c.p, c.id) catch |err| c.p.reportError(c.id, "messages", err); } }; /// This build's version and what changed to reach it, printed into an output /// buffer the same way Config prints the live settings. pub const Changelog = struct { pub const output: OutputTraits = .{ .name = config.changelog_buffer }; pub fn run(c: Ctx) void { panes.Output.openChangelog(c.p, c.id) catch |err| c.p.reportError(c.id, "changelog", err); } }; 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) panes.Output.openEffectCode(c.p, c.id, c.arg orelse return) catch |err| c.p.reportError(c.id, "effect code", err) else unreachable; } }; /// An optional SDL workspace-tag companion; never consumes input or text cells. pub const Pet = struct { pub const takes_arg = true; pub const enabled = pardes.platform == .gui; pub fn run(c: Ctx) void { const name = std.mem.trim(u8, c.arg orelse return, " \t\r\n"); c.p.settings.pet = std.meta.stringToEnum(@TypeOf(c.p.settings.pet), name) orelse return; } }; pub const Mini = struct { pub const takes_arg = true; pub const output: OutputTraits = .{ .name = "Mini", .doc = true }; 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; pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) return c.p.runSearch(c.id, pat, .find, .top) catch |err| c.p.reportError(c.id, "find", err); c.p.startPrompt(c.pane, .{ .search = config.find_marker }); } }; /// Find's sibling: Find matches file NAMES under this pane's directory, Grep /// matches file CONTENTS under every pane's directory at once. pub const Grep = struct { pub const takes_arg = true; pub const output: OutputTraits = .{ .name = config.search_buffer, .steps = true, .locations = true }; pub fn run(c: Ctx) void { const pat = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (pat.len > 0) return c.p.runSearch(c.id, pat, .grep, .top) catch |err| c.p.reportError(c.id, "grep", err); c.p.startPrompt(c.pane, .{ .search = config.grep_marker }); } }; // ---- the window group ---- pub const Left = struct { pub fn run(c: Ctx) void { layout.focusDir(c.p, c.id, .left); } }; pub const Down = struct { pub fn run(c: Ctx) void { layout.focusDir(c.p, c.id, .down); } }; pub const Up = struct { pub fn run(c: Ctx) void { layout.focusDir(c.p, c.id, .up); } }; pub const Right = struct { pub fn run(c: Ctx) void { layout.focusDir(c.p, c.id, .right); } }; // ---- the jump group ---- /// Ctrl-o: one step back into the history. pub const Back = struct { pub fn run(c: Ctx) void { c.p.jumpBy(-1); } }; /// Ctrl-i: one step forward again, up to wherever Back started. pub const Forward = struct { pub fn run(c: Ctx) void { c.p.jumpBy(1); } }; 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]; // 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); } } }; pub const Jumplist = struct { pub const output: OutputTraits = .{ .name = config.jumps_buffer, .steps = true, .locations = true }; pub fn run(c: Ctx) void { panes.Output.openJumps(c.p, c.id) catch |err| c.p.reportError(c.id, "jumplist", err); } }; // ---- the language group ---- pub const Hover = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .hover, ""); } }; pub const CodeAction = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .code_action, ""); } }; pub const SelectRefs = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .select_refs, ""); } }; pub const Symbols = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .document_symbols, ""); } }; pub const Diagnostics = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .diagnostics, ""); } }; pub const WsDiagnostics = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .workspace_diagnostics, ""); } }; pub const Callers = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .incoming_calls, ""); } }; pub const Callees = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .outgoing_calls, ""); } }; pub const Supertypes = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .supertypes, ""); } }; pub const Subtypes = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .subtypes, ""); } }; 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.startPrompt(c.pane, .{ .search = config.rename_marker }); } }; pub const WsSymbols = struct { pub const takes_arg = true; pub fn run(c: Ctx) void { const a = std.mem.trim(u8, c.arg orelse "", " \t\r\n"); if (a.len > 0) return c.p.lspRequest(c.id, .workspace_symbols, a); c.p.startPrompt(c.pane, .{ .search = config.symbol_marker }); } }; pub const Lspinfo = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .status, ""); } }; pub const Lspwhy = struct { pub fn run(c: Ctx) void { c.p.lspRequest(c.id, .explain, ""); } }; pub const Peek = struct { pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.peek_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) { Board.peek(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "peek", err); } else unreachable; } }; pub const Poke = struct { pub const takes_arg = true; pub const enabled = Board.enabled; pub fn run(c: Ctx) void { if (comptime enabled) { Board.poke(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "poke", err); } else unreachable; } }; pub const Hexdump = struct { pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.hexdump_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) { Board.hexdump(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "hexdump", err); } else unreachable; } }; pub const Gpio = struct { pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.gpio_buffer }; pub fn run(c: Ctx) void { if (comptime enabled) { Board.gpio(c.p, c.id, c.arg orelse "") catch |err| c.p.reportError(c.id, "gpio", err); } else unreachable; } }; const Board = struct { const enabled = pardes.platform == .esp32p4; 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"); } // 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); } };