diff options
Diffstat (limited to 'src/runtime_config.zig')
| -rw-r--r-- | src/runtime_config.zig | 579 |
1 files changed, 0 insertions, 579 deletions
diff --git a/src/runtime_config.zig b/src/runtime_config.zig deleted file mode 100644 index 69d2cdfe..00000000 --- a/src/runtime_config.zig +++ /dev/null @@ -1,579 +0,0 @@ -//! Runtime choices in one plain, owned record. -//! -//! `Setting` is compile-time metadata for the builtin registry; `State` is the -//! data it mutates and the Config report reads. Neither contains callbacks. -const std = @import("std"); -const panel_animation = @import("panel_animation.zig"); -const limits = @import("limits.zig"); - -/// Tagline glyphs retain body-cell geometry, so allowing a face larger than -/// the body would clip into neighbouring cells. Zero would make the role -/// invisible. Keep the runtime command on the same 1...100 contract as the -/// build-time default in config.zig. -pub const tagline_percent_min: u8 = 1; -pub const tagline_percent_max: u8 = 100; - -pub fn Text(comptime capacity: usize) type { - return struct { - bytes: [capacity]u8 = @splat(0), - len: std.math.IntFittingRange(0, capacity) = 0, - - pub fn get(value: *const @This()) []const u8 { - return value.bytes[0..value.len]; - } - - pub fn set(value: *@This(), text: []const u8) bool { - if (text.len > capacity) return false; - @memcpy(value.bytes[0..text.len], text); - value.len = @intCast(text.len); - return true; - } - - pub fn clear(value: *@This()) void { - value.len = 0; - } - }; -} - -pub const State = struct { - theme: usize = 0, - colors: bool = true, - wrap: bool = true, - tag_bottom: bool = false, - debug: bool = false, - - /// Empty requested text means config.default_shell. Resolution belongs to - /// the native host (the core deliberately has no filesystem), so retain - /// the executable it actually chose separately and mark a changed request - /// pending until the next terminal spawn acknowledges it. - shell: struct { - requested: Text(255) = .{}, - // One data schema across native, browser and freestanding builds; only - // its one absolute-path field follows `limits.host_path_cap`. - effective: Text(limits.host_path_cap) = .{}, - pending: bool = true, - } = .{}, - - /// The shell acknowledges a font before `effective` changes. A rejected - /// request therefore remains queryable without claiming it is on screen. - font: struct { - requested_path: Text(limits.host_path_cap) = .{}, - requested_name: Text(255) = .{}, - effective_name: Text(255) = .{}, - pending: bool = false, - effective_size_hundredths: u16 = 0, - effective_size_unit: FontSizeUnit = .unknown, - // Pardes.init copies config.gui_tagline_font_percent here. Keeping - // the compiled choice in the live record makes Config truthful while - // avoiding the config -> builtins -> runtime_config import cycle. - tagline_percent: u8 = 100, - } = .{}, - - panel_transition: panel_animation.Transition = .off, - scene_effects: panel_animation.SceneEffect = .{}, - - pub fn toggleTransition(state: *State, effect: panel_animation.Transition) void { - std.debug.assert(effect != .off); - state.panel_transition = if (state.panel_transition == effect) .off else effect; - } -}; - -pub const FontSizeUnit = enum { unknown, pixels, points }; - -/// Replace the requested font tuple atomically. Both fixed strings are -/// preflighted before either changes, so a rejected long name cannot leave a -/// new path paired with stale metadata. -pub fn requestFont(state: *State, path: []const u8, name: []const u8) bool { - if (path.len > state.font.requested_path.bytes.len or - name.len > state.font.requested_name.bytes.len) return false; - std.debug.assert(state.font.requested_path.set(path)); - std.debug.assert(state.font.requested_name.set(name)); - state.font.pending = true; - return true; -} - -/// Backend facilities are plain data supplied once by the platform-facing -/// registry. The same value gates command generation, leader paths, source -/// queries, and the Config report, so "unsupported" cannot mean four subtly -/// different things at those four call sites. -pub const Capabilities = struct { - font_picker: bool, - panel_transitions: bool, - scene_shaders: bool, - /// A smaller tagline face is also supported by the browser, which does - /// not own a native font picker. Keep this fact deliberately independent. - tagline_font_size: bool, -}; - -pub const Capability = std.meta.FieldEnum(Capabilities); - -pub const Toggle = enum { colors, wrap, tag_bottom, debug }; -pub const Scene = std.meta.FieldEnum(panel_animation.SceneEffect); - -pub const Action = union(enum) { - toggle: Toggle, - shell, - theme, - font, - tagline_size, - transition: panel_animation.Transition, - scene: Scene, -}; - -pub const Setting = struct { - word: []const u8, - action: Action, - availability: ?Capability = null, - - pub fn takesArg(setting: Setting) bool { - return switch (setting.action) { - .shell, .theme, .font, .tagline_size => true, - else => false, - }; - } - - pub fn enabled(setting: Setting, capabilities: Capabilities) bool { - const capability = setting.availability orelse return true; - return switch (capability) { - inline else => |field| @field(capabilities, @tagName(field)), - }; - } -}; - -/// This table is both the generated-setting builtin input and the Config -/// report order. Adding mutable config without adding a query row is therefore -/// impossible unless it is deliberately kept out of this user-facing state. -pub const settings = [_]Setting{ - .{ .word = "Colors", .action = .{ .toggle = .colors } }, - .{ .word = "Wrap", .action = .{ .toggle = .wrap } }, - .{ .word = "Tagbottom", .action = .{ .toggle = .tag_bottom } }, - .{ .word = "Debug", .action = .{ .toggle = .debug } }, - .{ .word = "Theme", .action = .theme }, - .{ .word = "Shell", .action = .shell }, - .{ .word = "Font", .action = .font, .availability = .font_picker }, - .{ .word = "TaglineSize", .action = .tagline_size, .availability = .font_picker }, - .{ .word = "PanelSlide", .action = .{ .transition = .slide }, .availability = .panel_transitions }, - .{ .word = "PanelZoom", .action = .{ .transition = .zoom }, .availability = .panel_transitions }, - .{ .word = "PanelDissolve", .action = .{ .transition = .dissolve }, .availability = .panel_transitions }, - .{ .word = "PanelAscii", .action = .{ .transition = .ascii }, .availability = .panel_transitions }, - .{ .word = "PanelVertical", .action = .{ .transition = .vertical }, .availability = .panel_transitions }, - .{ .word = "PanelEdges", .action = .{ .transition = .edges }, .availability = .panel_transitions }, - .{ .word = "PanelFall", .action = .{ .transition = .fall }, .availability = .panel_transitions }, - .{ .word = "PanelWave", .action = .{ .transition = .wave }, .availability = .panel_transitions }, - .{ .word = "PanelCurtain", .action = .{ .transition = .curtain }, .availability = .panel_transitions }, - .{ .word = "PanelScramble", .action = .{ .transition = .scramble }, .availability = .panel_transitions }, - .{ .word = "PanelType", .action = .{ .transition = .typewriter }, .availability = .panel_transitions }, - .{ .word = "Crt", .action = .{ .scene = .crt }, .availability = .scene_shaders }, - .{ .word = "Ripple", .action = .{ .scene = .ripple }, .availability = .scene_shaders }, - .{ .word = "Glitch", .action = .{ .scene = .glitch }, .availability = .scene_shaders }, -}; - -pub fn find(name: []const u8) ?Setting { - for (settings) |setting| if (std.mem.eql(u8, setting.word, name)) return setting; - return null; -} - -/// The table is also the sole action-to-command vocabulary. Compile-time -/// callers use this for picker rows; the Config report uses it for the active -/// transition. No parallel enum or transition-name switch can drift. -pub fn findAction(action: Action) ?Setting { - for (settings) |setting| if (std.meta.eql(setting.action, action)) return setting; - return null; -} - -fn actionCount(comptime action: Action) comptime_int { - var count = 0; - for (settings) |setting| count += @intFromBool(std.meta.eql(setting.action, action)); - return count; -} - -comptime { - @setEvalBranchQuota(20_000); - for (settings, 0..) |setting, i| { - if (setting.word.len == 0) @compileError("runtime setting has an empty command word"); - for (settings[i + 1 ..]) |later| if (std.mem.eql(u8, setting.word, later.word)) - @compileError("duplicate runtime setting command word: " ++ setting.word); - switch (setting.action) { - .font, .tagline_size => if (setting.availability != .font_picker) - @compileError("native font settings must use the font-picker capability"), - .transition => if (setting.availability != .panel_transitions) - @compileError("panel effects must use the panel-transition capability"), - .scene => if (setting.availability != .scene_shaders) - @compileError("scene effects must use the scene-shader capability"), - else => if (setting.availability != null) - @compileError("unconditional settings cannot carry a backend capability"), - } - } - for (std.enums.values(Toggle)) |field| if (actionCount(.{ .toggle = field }) != 1) - @compileError("runtime toggle must occur exactly once: " ++ @tagName(field)); - if (actionCount(.shell) != 1 or actionCount(.theme) != 1 or actionCount(.font) != 1 or - actionCount(.tagline_size) != 1) - @compileError("Shell, Theme, Font, and TaglineSize actions must each occur exactly once"); - for (std.enums.values(panel_animation.Transition)) |effect| { - const expected: comptime_int = @intFromBool(effect != .off); - if (actionCount(.{ .transition = effect }) != expected) - @compileError("non-off panel transition must occur exactly once: " ++ @tagName(effect)); - } - for (std.enums.values(Scene)) |effect| { - if (actionCount(.{ .scene = effect }) != 1) - @compileError("scene effect must occur exactly once: " ++ @tagName(effect)); - if (@FieldType(panel_animation.SceneEffect, @tagName(effect)) != bool) - @compileError("scene effect fields must be booleans: " ++ @tagName(effect)); - } -} - -/// Apply settings whose operation is independent of themes, font discovery, -/// or a shell. Those three remain explicit at the generated builtin's edge. -pub fn applySimple(state: *State, setting: Setting, argument: ?[]const u8) bool { - switch (setting.action) { - .toggle => |field| switch (field) { - .colors => state.colors = !state.colors, - .wrap => state.wrap = !state.wrap, - .tag_bottom => state.tag_bottom = !state.tag_bottom, - .debug => state.debug = !state.debug, - }, - .shell => { - const value = std.mem.trim(u8, argument orelse return false, " \t\r\n"); - if (value.len == 0 or !state.shell.requested.set(value)) return false; - state.shell.pending = true; - }, - .tagline_size => { - const text = std.mem.trim(u8, argument orelse return false, " \t\r\n"); - const percent = std.fmt.parseInt(u16, text, 10) catch return false; - if (percent < tagline_percent_min or percent > tagline_percent_max) return false; - // Parse and validate before the sole write: malformed commands - // cannot partially alter the live configuration. - state.font.tagline_percent = @intCast(percent); - }, - .transition => |effect| state.toggleTransition(effect), - .scene => |effect| switch (effect) { - inline else => |field| { - const value = &@field(state.scene_effects, @tagName(field)); - value.* = !value.*; - }, - }, - .theme, .font => return false, - } - return true; -} - -/// The runtime facts which do not belong to mutable `State`, supplied by the -/// core when it materialises +Config. Slices are borrowed for this one write. -pub const ReportContext = struct { - startup_config_path: ?[]const u8, - platform: []const u8, - theme_name: []const u8, - compiled_default_shell: []const u8, - /// Null outside the SDL GUI. The borrowed label comes from effect_sources, - /// which knows whether its embedded GLSL import is live or paired prebuilt. - gui_shader_source_mode: ?[]const u8 = null, - hover_delay_frames: ?u16, - native_images: bool, - capabilities: Capabilities, - state: *const State, -}; - -fn onOff(value: bool) []const u8 { - return if (value) "on" else "off"; -} - -fn shown(text: []const u8) []const u8 { - return if (text.len == 0) "(none)" else text; -} - -/// Name the setting which selected the one active transition. This keeps the -/// report vocabulary identical to the generated builtin registry. -fn transitionSettingName(transition: panel_animation.Transition) []const u8 { - if (transition == .off) return "off"; - return findAction(.{ .transition = transition }).?.word; -} - -/// Write the complete live Config report without allocation. Setting-backed -/// rows follow `settings` order; host facts follow them as a compact footer. -pub fn writeReport(out: *std.Io.Writer, context: ReportContext) !void { - const state = context.state; - var wrote_transition = false; - for (settings) |setting| switch (setting.action) { - .toggle => |field| { - const value = switch (field) { - .colors => state.colors, - .wrap => state.wrap, - .tag_bottom => state.tag_bottom, - .debug => state.debug, - }; - try out.print("{s}: {s}\n", .{ setting.word, onOff(value) }); - }, - .theme => try out.print("{s}: {s}\n", .{ setting.word, context.theme_name }), - .shell => { - const chosen = state.shell.requested.get(); - try out.print( - "{s} requested (new panes): {s}{s}\n" ++ - "{s} effective (last spawn): {s}\n" ++ - "{s} pending: {s}\n", - .{ - setting.word, - if (chosen.len == 0) context.compiled_default_shell else chosen, - if (chosen.len == 0) " (default)" else "", - setting.word, - shown(state.shell.effective.get()), - setting.word, - onOff(state.shell.pending), - }, - ); - }, - .font => if (!setting.enabled(context.capabilities)) - try out.print("{s}: unsupported\n", .{setting.word}) - else - try out.print( - "{s} requested: {s}\n" ++ - "{s} requested path: {s}\n" ++ - "{s} effective: {s}\n" ++ - "{s} pending: {s}\n" ++ - "{s} effective size: {d}.{d:0>2} {s}\n", - .{ - setting.word, - shown(state.font.requested_name.get()), - setting.word, - shown(state.font.requested_path.get()), - setting.word, - shown(state.font.effective_name.get()), - setting.word, - onOff(state.font.pending), - setting.word, - state.font.effective_size_hundredths / 100, - state.font.effective_size_hundredths % 100, - @tagName(state.font.effective_size_unit), - }, - ), - .tagline_size => if (!context.capabilities.tagline_font_size) - try out.print("{s}: unsupported\n", .{setting.word}) - else if (!setting.enabled(context.capabilities)) - try out.print("{s}: {d}% (build-time only)\n", .{ setting.word, state.font.tagline_percent }) - else - try out.print("{s}: {d}%\n", .{ setting.word, state.font.tagline_percent }), - .transition => { - if (wrote_transition) continue; - wrote_transition = true; - if (!setting.enabled(context.capabilities)) - try out.writeAll("Panel transition: unsupported\n") - else - try out.print("Panel transition: {s}\n", .{transitionSettingName(state.panel_transition)}); - }, - .scene => |effect| { - if (!setting.enabled(context.capabilities)) { - try out.print("{s}: unsupported\n", .{setting.word}); - continue; - } - const enabled = switch (effect) { - inline else => |field| @field(state.scene_effects, @tagName(field)), - }; - try out.print("{s}: {s}\n", .{ setting.word, onOff(enabled) }); - }, - }; - - if (context.startup_config_path) |path| - try out.print("Startup config: {s}\n", .{path}) - else - try out.writeAll("Startup config: no per-user config path\n"); - try out.print( - "Platform: {s}\n" ++ - "Compiled default shell: {s}\n", - .{ context.platform, context.compiled_default_shell }, - ); - if (context.gui_shader_source_mode) |mode| - try out.print("GUI shader source: {s}\n", .{mode}); - if (context.hover_delay_frames) |frames| - try out.print("Look hover delay: {d} frames\n", .{frames}) - else - try out.writeAll("Look hover delay: off\n"); - try out.print("Native images: {s}\n", .{onOff(context.native_images)}); -} - -test "setting names are unique and argument metadata follows actions" { - for (settings, 0..) |setting, i| { - try std.testing.expect(setting.word.len > 0); - for (settings[i + 1 ..]) |later| - try std.testing.expect(!std.mem.eql(u8, setting.word, later.word)); - try std.testing.expectEqual(switch (setting.action) { - .shell, .theme, .font, .tagline_size => true, - else => false, - }, setting.takesArg()); - } -} - -test "simple setting application mutates only its plain field" { - var state: State = .{}; - try std.testing.expect(applySimple(&state, find("Colors").?, null)); - try std.testing.expect(!state.colors); - try std.testing.expect(applySimple(&state, find("Shell").?, " fish\n")); - try std.testing.expectEqualStrings("fish", state.shell.requested.get()); - try std.testing.expect(state.shell.pending); - try std.testing.expect(applySimple(&state, find("PanelAscii").?, null)); - try std.testing.expectEqual(panel_animation.Transition.ascii, state.panel_transition); - try std.testing.expect(applySimple(&state, find("PanelAscii").?, null)); - try std.testing.expectEqual(panel_animation.Transition.off, state.panel_transition); - try std.testing.expect(applySimple(&state, find("Crt").?, null)); - try std.testing.expect(state.scene_effects.crt); -} - -test "tagline size validates before mutating live state" { - const setting = find("TaglineSize").?; - var state: State = .{}; - - for ([_][]const u8{ "1", " 82\n", "100" }) |argument| { - try std.testing.expect(applySimple(&state, setting, argument)); - try std.testing.expectEqual(try std.fmt.parseInt(u8, std.mem.trim(u8, argument, " \t\r\n"), 10), state.font.tagline_percent); - } - - state.font.tagline_percent = 67; - for ([_]?[]const u8{ null, "", "0", "101", "-1", "50%", "999999999999999999999" }) |argument| { - try std.testing.expect(!applySimple(&state, setting, argument)); - try std.testing.expectEqual(@as(u8, 67), state.font.tagline_percent); - } -} - -test "font request tuple rejects atomically" { - var state: State = .{}; - try std.testing.expect(requestFont(&state, "/fonts/old.ttf", "Old")); - var too_long: [256]u8 = @splat('x'); - try std.testing.expect(!requestFont(&state, "/fonts/new.ttf", &too_long)); - try std.testing.expectEqualStrings("/fonts/old.ttf", state.font.requested_path.get()); - try std.testing.expectEqualStrings("Old", state.font.requested_name.get()); -} - -test "Config report observes every simple setting and all live context" { - var state: State = .{}; - var storage: [4096]u8 = undefined; - const context: ReportContext = .{ - .startup_config_path = "/tmp/pardes/init", - .platform = "gui", - .theme_name = "acme", - .compiled_default_shell = "/bin/sh", - .gui_shader_source_mode = "live GLSL compiled during this build", - .hover_delay_frames = 18, - .native_images = true, - .capabilities = .{ - .font_picker = true, - .panel_transitions = true, - .scene_shaders = true, - .tagline_font_size = true, - }, - .state = &state, - }; - - for (settings) |setting| { - switch (setting.action) { - .theme, .font => continue, - else => {}, - } - const argument: ?[]const u8 = switch (setting.action) { - .shell => "fish", - .tagline_size => "73", - else => null, - }; - try std.testing.expect(applySimple(&state, setting, argument)); - - var out: std.Io.Writer = .fixed(&storage); - try writeReport(&out, context); - const report = storage[0..out.end]; - const expected = switch (setting.action) { - .toggle => |field| switch (field) { - .colors => "Colors: off\n", - .wrap => "Wrap: off\n", - .tag_bottom => "Tagbottom: on\n", - .debug => "Debug: on\n", - }, - .shell => "Shell requested (new panes): fish\n", - .tagline_size => "TaglineSize: 73%\n", - .transition => |transition| switch (transition) { - .off => unreachable, - .slide => "Panel transition: PanelSlide\n", - .zoom => "Panel transition: PanelZoom\n", - .dissolve => "Panel transition: PanelDissolve\n", - .ascii => "Panel transition: PanelAscii\n", - .vertical => "Panel transition: PanelVertical\n", - .edges => "Panel transition: PanelEdges\n", - .fall => "Panel transition: PanelFall\n", - .wave => "Panel transition: PanelWave\n", - .curtain => "Panel transition: PanelCurtain\n", - .scramble => "Panel transition: PanelScramble\n", - .typewriter => "Panel transition: PanelType\n", - }, - .scene => |effect| switch (effect) { - .crt => "Crt: on\n", - .ripple => "Ripple: on\n", - .glitch => "Glitch: on\n", - }, - .theme, .font => unreachable, - }; - try std.testing.expect(std.mem.indexOf(u8, report, expected) != null); - } - - try std.testing.expect(state.font.requested_name.set("Wanted Mono")); - try std.testing.expect(state.font.requested_path.set("/fonts/wanted.ttf")); - try std.testing.expect(state.font.effective_name.set("Effective Mono")); - state.font.pending = true; - state.font.effective_size_hundredths = 1375; - state.font.effective_size_unit = .points; - state.font.tagline_percent = 82; - - var out: std.Io.Writer = .fixed(&storage); - try writeReport(&out, context); - const report = storage[0..out.end]; - for ([_][]const u8{ - "Theme: acme\n", - "Font requested: Wanted Mono\n", - "Font requested path: /fonts/wanted.ttf\n", - "Font effective: Effective Mono\n", - "Font pending: on\n", - "Font effective size: 13.75 points\n", - "TaglineSize: 82%\n", - "Startup config: /tmp/pardes/init\n", - "Platform: gui\n", - "Compiled default shell: /bin/sh\n", - "GUI shader source: live GLSL compiled during this build\n", - "Look hover delay: 18 frames\n", - "Native images: on\n", - }) |expected| try std.testing.expect(std.mem.indexOf(u8, report, expected) != null); - - var defaults: State = .{}; - var defaults_context = context; - defaults_context.startup_config_path = null; - defaults_context.platform = "tty"; - defaults_context.gui_shader_source_mode = null; - defaults_context.hover_delay_frames = null; - defaults_context.native_images = false; - defaults_context.capabilities = .{ - .font_picker = false, - .panel_transitions = true, - .scene_shaders = false, - .tagline_font_size = false, - }; - defaults_context.state = &defaults; - out = .fixed(&storage); - try writeReport(&out, defaults_context); - const defaults_report = storage[0..out.end]; - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell requested (new panes): /bin/sh (default)\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell effective (last spawn): (none)\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Shell pending: on\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Startup config: no per-user config path\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "GUI shader source:") == null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Font: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Font requested:") == null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Panel transition: off\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Crt: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Ripple: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Glitch: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "TaglineSize: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Look hover delay: off\n") != null); - try std.testing.expect(std.mem.indexOf(u8, defaults_report, "Native images: off\n") != null); - - defaults_context.platform = "web"; - defaults_context.capabilities.panel_transitions = false; - defaults_context.capabilities.tagline_font_size = true; - out = .fixed(&storage); - try writeReport(&out, defaults_context); - const web_report = storage[0..out.end]; - try std.testing.expect(std.mem.indexOf(u8, web_report, "Panel transition: unsupported\n") != null); - try std.testing.expect(std.mem.indexOf(u8, web_report, "TaglineSize: 100% (build-time only)\n") != null); -} |
