From f5eaddb2cf28a9b72d5dd5b3b10502a316d4de85 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 09:41:55 -0300 Subject: Split control messages by scope: the root ctl takes the session's builtins and reads the settings, a pane's ctl its own Every builtin could only be clicked, or written to exec, and the settings could be read only as the Config window's prose. acme keeps window verbs on a window's ctl, and webfs and upas/fs keep session settings on a root ctl. Each builtin now declares its scope (scope = .session; settings are all session, the rest pane), read by the registry. The root /ctl takes session builtins and reads every setting in the words a write takes, so its read written back changes nothing (panel and scene effects now take on/off like the toggles, to make that true); a pane's ctl takes the pane's builtins beside get, lock and unlock. Writes are checked whole and refused in Plan 9's ctl words (unknown control message "X", wrong #args ...), which 9ns now maps to EINVAL (cloud9 re-pinned at a8c7a715). A builtin that would prompt for its argument fails the write instead, and a refusal is answered at once, not after the frame. Co-Authored-By: Claude Opus 5.5 --- src/builtins.zig | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 3 deletions(-) (limited to 'src/builtins.zig') diff --git a/src/builtins.zig b/src/builtins.zig index ffb00ff6..ed0a07dd 100644 --- a/src/builtins.zig +++ b/src/builtins.zig @@ -30,6 +30,14 @@ pub const capabilities: config.Runtime.Capabilities = .{ .ligatures = pardes.platform == .gui, }; +/// Where a builtin acts: at the pane it runs from, or on the editor as a +/// whole. acme keeps its window verbs on a window's ctl and its session +/// commands elsewhere, as webfs and upas/fs keep session settings on a root +/// ctl; pardes's root `ctl` takes the session builtins and a pane's `ctl` +/// the pane ones. A builtin says `pub const scope: Scope = .session;`, +/// and every setting is one; the rest act on their pane. +pub const Scope = enum { pane, session }; + pub const Ctx = struct { p: *Pardes, pane: *Pane, @@ -133,9 +141,14 @@ pub const registry = struct { 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; + // Every setting takes its value, and a switch flips when bare. + return true; + } + + pub fn scope(b: Builtin()) Scope { + inline for (manualBuiltinList(), 0..) |T, i| + if (@intFromEnum(b) == i) return if (@hasDecl(T, "scope")) T.scope else .pane; + return .session; // a setting } pub fn outputTraits(b: Builtin()) ?OutputTraits { @@ -173,6 +186,17 @@ test "capabilities exactly gate setting and effect-source builtins" { try std.testing.expectEqual(EffectCode.enabled, effect_code_registered); } +test "a builtin's scope is the pane unless it says session, and every setting is the session's" { + const Builtin = registry.Builtin(); + try std.testing.expectEqual(Scope.pane, registry.scope(Builtin.Del)); + try std.testing.expectEqual(Scope.pane, registry.scope(Builtin.Save)); + try std.testing.expectEqual(Scope.session, registry.scope(Builtin.Newcol)); + try std.testing.expectEqual(Scope.session, registry.scope(Builtin.Kill)); + try std.testing.expectEqual(Scope.session, registry.scope(Builtin.Verbose)); + for (comptime settingList()) |setting| + try std.testing.expectEqual(Scope.session, registry.scope(std.meta.stringToEnum(Builtin, setting.word).?)); +} + test "Look does not announce itself on the message row; Del still does" { const Builtin = registry.Builtin(); try std.testing.expect(!registry.announces(Builtin.Look)); @@ -233,6 +257,7 @@ pub const Exec = struct { // ---- session ---- pub const Kill = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { c.p.quit = true; c.p.emit(.quit); @@ -240,12 +265,14 @@ pub const Kill = struct { }; pub const Dump = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { dump.dumpState(c.p) catch {}; } }; pub const Restore = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub fn run(c: Ctx) void { const path = c.arg orelse (c.p.last_dump orelse return); @@ -256,6 +283,7 @@ pub const Restore = struct { }; pub const Attach = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = pardes.can_attach; pub fn run(c: Ctx) void { @@ -270,6 +298,7 @@ pub const Attach = struct { }; pub const Detach = struct { + pub const scope: Scope = .session; pub const enabled = pardes.can_attach; pub fn run(c: Ctx) void { if (comptime enabled) @@ -280,6 +309,7 @@ pub const Detach = struct { }; pub const Mount = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { @@ -293,6 +323,7 @@ pub const Mount = struct { }; pub const Unmount = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { @@ -318,12 +349,14 @@ pub const Msg = struct { }; pub const NextColor = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { colors.setThemeIndex(c.p, (@as(usize, c.p.settings.theme) + 1) % pardes.themes.len); } }; pub const ThemeSel = struct { + pub const scope: Scope = .session; 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); @@ -331,6 +364,7 @@ pub const ThemeSel = struct { }; pub const ThemeFile = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { @@ -342,6 +376,7 @@ pub const ThemeFile = struct { }; pub const DumpThemes = struct { + pub const scope: Scope = .session; pub const enabled = pardes.hosted; pub fn run(c: Ctx) void { if (comptime enabled) { @@ -355,6 +390,7 @@ pub const DumpThemes = struct { }; pub const FontSel = struct { + pub const scope: Scope = .session; pub const output: OutputTraits = .{ .name = config.fonts_buffer, .steps = true, .commands = true }; pub const enabled = capabilities.font_picker; pub fn run(c: Ctx) void { @@ -496,6 +532,7 @@ pub const New = struct { /// The same empty scratch, opened in a fresh column beside the calling pane. pub const Newcol = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { c.p.newScratchColumn(c.id); } @@ -603,12 +640,14 @@ pub const Tty9p = struct { /// 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 const scope: Scope = .session; pub fn run(c: Ctx) void { layout.joinCol(c.p); } }; pub const Tutor = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { const free = c.p.freeSlot() orelse return; const nt = c.p.openTutorView(free) catch return; @@ -617,6 +656,7 @@ pub const Tutor = struct { }; pub const Help = struct { + pub const scope: Scope = .session; 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); @@ -624,6 +664,7 @@ pub const Help = struct { }; pub const Config = struct { + pub const scope: Scope = .session; 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); @@ -631,6 +672,7 @@ pub const Config = struct { }; pub const LocationsConfig = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const output: OutputTraits = .{ .name = "+LocationsConfig" }; @@ -660,6 +702,7 @@ pub const LocationsConfig = struct { }; pub const Messages = struct { + pub const scope: Scope = .session; 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); @@ -669,6 +712,7 @@ pub const Messages = struct { /// 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 scope: Scope = .session; 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); @@ -676,6 +720,7 @@ pub const Changelog = struct { }; pub const EffectCode = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = capabilities.panel_transitions or capabilities.scene_shaders; pub const output: OutputTraits = .{ .name = config.effect_code_buffer }; @@ -690,6 +735,7 @@ pub const EffectCode = struct { /// An optional SDL workspace-tag companion; never consumes input or text cells. pub const Pet = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = pardes.platform == .gui; pub fn run(c: Ctx) void { @@ -793,6 +839,7 @@ pub const Right = struct { /// Ctrl-o: one step back into the history. pub const Back = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { c.p.jumpBy(-1); } @@ -800,6 +847,7 @@ pub const Back = struct { /// Ctrl-i: one step forward again, up to wherever Back started. pub const Forward = struct { + pub const scope: Scope = .session; pub fn run(c: Ctx) void { c.p.jumpBy(1); } @@ -821,6 +869,7 @@ pub const Last = struct { }; pub const Jumplist = struct { + pub const scope: Scope = .session; 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); @@ -920,6 +969,7 @@ pub const Lspwhy = struct { }; pub const Peek = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.peek_buffer }; @@ -932,6 +982,7 @@ pub const Peek = struct { }; pub const Poke = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = Board.enabled; pub fn run(c: Ctx) void { @@ -943,6 +994,7 @@ pub const Poke = struct { }; pub const Hexdump = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.hexdump_buffer }; @@ -955,6 +1007,7 @@ pub const Hexdump = struct { }; pub const Gpio = struct { + pub const scope: Scope = .session; pub const takes_arg = true; pub const enabled = Board.enabled; pub const output: OutputTraits = .{ .name = config.gpio_buffer }; -- cgit v1.3