summaryrefslogtreecommitdiff
path: root/src/config.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/config.zig')
-rw-r--r--src/config.zig244
1 files changed, 206 insertions, 38 deletions
diff --git a/src/config.zig b/src/config.zig
index 78787ad2..186b29a1 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -1,6 +1,7 @@
const std = @import("std");
const builtin = @import("builtin");
const layout = @import("layout.zig");
+const animation = @import("animation.zig");
const limits = @import("memory.zig").limits;
const pardes = @import("pardes.zig");
const builtins = @import("builtins.zig");
@@ -129,8 +130,8 @@ pub const leader_path = paths: {
}
if (builtins.capabilities.scene_shaders) {
table.set(.Crt, "tr");
- table.set(.Ripple, "tR");
- table.set(.Glitch, "tg");
+ table.set(.Shader, null);
+ table.set(.ShaderAnimation, null);
}
if (builtins.EffectCode.enabled) table.set(.EffectCode, null);
if (builtins.TreeContext.enabled) table.set(.TreeContext, null);
@@ -653,10 +654,92 @@ pub const Runtime = struct {
tagline_percent: u8 = 100,
} = .{},
- panel_transition: layout.Transition = .off,
- scene_effects: layout.SceneEffect = .{},
+ panel_transition: animation.Transition = .off,
+ /// The post chain (docs/render-pipeline.md ยง6), in the order it runs.
+ post: Post = .{},
+ /// When the chain redraws on its own, past the frames it post-processes:
+ /// never, while the window has the focus, or always (ghostty's
+ /// custom-shader-animation).
+ shader_animation: ShaderAnimation = .on,
- pub fn toggleTransition(state: *Runtime, effect: layout.Transition) void {
+ pub const ShaderAnimation = enum { off, on, always };
+
+ /// Each pass the bundled Crt or a Shadertoy file.
+ pub const Post = struct {
+ pub const max = 8;
+ pub const Entry = struct {
+ scene: ?Scene = null,
+ path: Text(limits.host_path_cap) = .{},
+ /// A bundled pass's strength, 1 to `max_level`; 0 for a file.
+ level: u8 = 0,
+ };
+ pub const max_level = 3;
+ /// What `on` and a bare word turn a bundled pass on at.
+ pub const default_level = 2;
+ entries: [max]Entry = @splat(.{}),
+ len: u8 = 0,
+ /// Bumped on every change: a shell compiles the chain again when it
+ /// moves.
+ generation: u32 = 0,
+
+ pub fn list(post: *const Post) []const Entry {
+ return post.entries[0..post.len];
+ }
+
+ pub fn has(post: *const Post, scene: Scene) bool {
+ return post.level(scene) != 0;
+ }
+
+ /// A bundled pass's level; 0 when it is not in the chain.
+ pub fn level(post: *const Post, scene: Scene) u8 {
+ for (post.list()) |entry| if (entry.scene == scene) return entry.level;
+ return 0;
+ }
+
+ /// Sets a bundled pass's level: 0 takes it out, anything else puts it
+ /// in at the end or changes it where it is.
+ pub fn setLevel(post: *Post, scene: Scene, to: u8) bool {
+ std.debug.assert(to <= max_level);
+ if (to == 0) return post.set(scene, "", false);
+ for (post.entries[0..post.len]) |*entry| if (entry.scene == scene) {
+ if (entry.level != to) post.generation +%= 1;
+ entry.level = to;
+ return true;
+ };
+ if (!post.set(scene, "", true)) return false;
+ post.entries[post.len - 1].level = to;
+ return true;
+ }
+
+ /// Puts the pass in the chain, at its end, or takes it out; `want`
+ /// null flips it. False when the chain is full or the path too long.
+ pub fn set(post: *Post, scene: ?Scene, path: []const u8, want: ?bool) bool {
+ const at: ?usize = for (post.list(), 0..) |entry, i| {
+ if (entry.scene == scene and std.mem.eql(u8, entry.path.get(), path)) break i;
+ } else null;
+ const on = want orelse (at == null);
+ if (on == (at != null)) return true;
+ post.generation +%= 1;
+ if (at) |i| {
+ std.mem.copyForwards(Entry, post.entries[i .. post.len - 1], post.entries[i + 1 .. post.len]);
+ post.len -= 1;
+ return true;
+ }
+ if (post.len == max) return false;
+ post.entries[post.len] = .{ .scene = scene };
+ if (!post.entries[post.len].path.set(path)) return false;
+ post.len += 1;
+ return true;
+ }
+
+ pub fn clear(post: *Post) void {
+ if (post.len == 0) return;
+ post.len = 0;
+ post.generation +%= 1;
+ }
+ };
+
+ pub fn toggleTransition(state: *Runtime, effect: animation.Transition) void {
std.debug.assert(effect != .off);
state.panel_transition = if (state.panel_transition == effect) .off else effect;
}
@@ -733,7 +816,8 @@ pub const Runtime = struct {
pub const MessageTime = enum { linger, fall, dissolve };
pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, column_tags, syntax_bold, verbose, message_animation, ligatures };
- pub const Scene = std.meta.FieldEnum(layout.SceneEffect);
+ /// The bundled post pass, a Shadertoy file in shaders/post/.
+ pub const Scene = enum { crt };
pub const Action = union(enum) {
toggle: Toggle,
@@ -745,8 +829,12 @@ pub const Runtime = struct {
window_blur,
message_ms: MessageTime,
dump_dir,
- transition: layout.Transition,
+ transition: animation.Transition,
scene: Scene,
+ /// A Shadertoy file into the post chain or out of it; `off` empties
+ /// it of files.
+ shader,
+ shader_animation,
};
pub const Setting = struct {
@@ -797,8 +885,8 @@ pub const Runtime = struct {
.{ .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 },
+ .{ .word = "Shader", .action = .shader, .availability = .scene_shaders },
+ .{ .word = "ShaderAnimation", .action = .shader_animation, .availability = .scene_shaders },
};
pub fn find(name: []const u8) ?Setting {
@@ -832,7 +920,7 @@ pub const Runtime = struct {
@compileError("window blur must use the window-blur capability"),
.transition => if (setting.availability != .panel_transitions)
@compileError("panel effects must use the panel-transition capability"),
- .scene => if (setting.availability != .scene_shaders)
+ .scene, .shader, .shader_animation => if (setting.availability != .scene_shaders)
@compileError("scene effects must use the scene-shader capability"),
// A toggle is plain state any shell can hold, except one for
// what only some shells draw: that one exists where they do.
@@ -850,7 +938,7 @@ pub const Runtime = struct {
if (actionCount(.shell) != 1 or actionCount(.theme) != 1 or actionCount(.font) != 1 or
actionCount(.tagline_size) != 1 or actionCount(.window_opacity) != 1 or actionCount(.window_blur) != 1)
@compileError("Shell, Theme, Font, TaglineSize, WindowOpacity, and WindowBlur actions must each occur exactly once");
- for (std.enums.values(layout.Transition)) |effect| {
+ for (std.enums.values(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));
@@ -858,9 +946,9 @@ pub const Runtime = struct {
for (std.enums.values(Scene)) |effect| {
if (actionCount(.{ .scene = effect }) != 1)
@compileError("scene effect must occur exactly once: " ++ @tagName(effect));
- if (@FieldType(layout.SceneEffect, @tagName(effect)) != bool)
- @compileError("scene effect fields must be booleans: " ++ @tagName(effect));
}
+ if (actionCount(.shader) != 1 or actionCount(.shader_animation) != 1)
+ @compileError("Shader and ShaderAnimation must each occur exactly once");
}
pub fn apply(state: *Runtime, setting: Setting, argument: ?[]const u8) bool {
@@ -926,13 +1014,36 @@ pub const Runtime = struct {
}
},
.scene => |effect| {
- const want = switchedTo(argument) catch return false;
- switch (effect) {
- inline else => |field| {
- const value = &@field(state.scene_effects, @tagName(field));
- value.* = want orelse !value.*;
- },
+ // A level, 0 (off) to 3; `on` is the default level, `off` 0,
+ // and bare flips between the two.
+ const text = std.mem.trim(u8, argument orelse "", " \t\r\n");
+ const to: u8 = if (text.len == 0)
+ (if (state.post.has(effect)) 0 else Post.default_level)
+ else if (std.mem.eql(u8, text, "on"))
+ Post.default_level
+ else if (std.mem.eql(u8, text, "off"))
+ 0
+ else
+ std.fmt.parseInt(u8, text, 10) catch return false;
+ if (to > Post.max_level) return false;
+ return state.post.setLevel(effect, to);
+ },
+ .shader => {
+ const path = std.mem.trim(u8, argument orelse return false, " \t\r\n");
+ if (path.len == 0) return false;
+ if (std.mem.eql(u8, path, "off")) {
+ var i: usize = 0;
+ while (i < state.post.len) {
+ const entry = state.post.entries[i];
+ if (entry.scene == null) _ = state.post.set(null, entry.path.get(), false) else i += 1;
+ }
+ return true;
}
+ return state.post.set(null, path, null);
+ },
+ .shader_animation => {
+ const text = std.mem.trim(u8, argument orelse return false, " \t\r\n");
+ state.shader_animation = std.meta.stringToEnum(ShaderAnimation, text) orelse return false;
},
.theme, .font => return false,
}
@@ -973,7 +1084,7 @@ pub const Runtime = struct {
return if (text.len == 0) "(none)" else text;
}
- fn transitionSettingName(transition: layout.Transition) []const u8 {
+ fn transitionSettingName(transition: animation.Transition) []const u8 {
if (transition == .off) return "off";
return findAction(.{ .transition = transition }).?.word;
}
@@ -1084,11 +1195,25 @@ pub const Runtime = struct {
try out.print("{s} unsupported\n", .{setting.word});
continue;
}
- const enabled = switch (effect) {
- inline else => |field| @field(state.scene_effects, @tagName(field)),
+ const level = state.post.level(effect);
+ if (level == 0) try out.print("{s} off\n", .{setting.word}) else try out.print("{s} {d}\n", .{ setting.word, level });
+ },
+ .shader => {
+ if (!setting.enabled(context.capabilities)) {
+ try out.print("{s} unsupported\n", .{setting.word});
+ continue;
+ }
+ var files: usize = 0;
+ for (state.post.list()) |entry| if (entry.scene == null) {
+ try out.print("{s} {s}\n", .{ setting.word, entry.path.get() });
+ files += 1;
};
- try out.print("{s} {s}\n", .{ setting.word, onOff(enabled) });
+ if (files == 0) try out.print("{s} off\n", .{setting.word});
},
+ .shader_animation => if (setting.enabled(context.capabilities))
+ try out.print("{s} {s}\n", .{ setting.word, @tagName(state.shader_animation) })
+ else
+ try out.print("{s} unsupported\n", .{setting.word}),
};
if (context.startup_config_path) |path|
@@ -1124,20 +1249,18 @@ pub const Runtime = struct {
const slide = findAction(.{ .transition = .slide }).?;
const zoom = findAction(.{ .transition = .zoom }).?;
try std.testing.expect(apply(&state, slide, "on"));
- try std.testing.expectEqual(layout.Transition.slide, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.slide, state.panel_transition);
try std.testing.expect(apply(&state, zoom, "off"));
- try std.testing.expectEqual(layout.Transition.slide, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.slide, state.panel_transition);
try std.testing.expect(apply(&state, slide, "on"));
- try std.testing.expectEqual(layout.Transition.slide, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.slide, state.panel_transition);
try std.testing.expect(apply(&state, slide, null));
- try std.testing.expectEqual(layout.Transition.off, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.off, state.panel_transition);
try std.testing.expect(!apply(&state, slide, "yes"));
const scene = findAction(.{ .scene = std.enums.values(Scene)[0] }).?;
try std.testing.expect(apply(&state, scene, "on"));
try std.testing.expect(apply(&state, scene, "on"));
- switch (std.enums.values(Scene)[0]) {
- inline else => |f| try std.testing.expect(@field(state.scene_effects, @tagName(f))),
- }
+ try std.testing.expect(state.post.has(std.enums.values(Scene)[0]));
}
test "a toggle flips when bare and sets when told on or off" {
@@ -1170,11 +1293,56 @@ pub const Runtime = struct {
try std.testing.expectEqualStrings("fish", state.shell.requested.get());
try std.testing.expect(state.shell.pending);
try std.testing.expect(apply(&state, find("PanelAscii").?, null));
- try std.testing.expectEqual(layout.Transition.ascii, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.ascii, state.panel_transition);
try std.testing.expect(apply(&state, find("PanelAscii").?, null));
- try std.testing.expectEqual(layout.Transition.off, state.panel_transition);
+ try std.testing.expectEqual(animation.Transition.off, state.panel_transition);
try std.testing.expect(apply(&state, find("Crt").?, null));
- try std.testing.expect(state.scene_effects.crt);
+ try std.testing.expect(state.post.has(.crt));
+ }
+
+ test "the post chain runs in the order its passes were put in, and Shader off empties it of files" {
+ var state: Runtime = .{};
+ try std.testing.expect(apply(&state, find("Shader").?, " ~/a.glsl\n"));
+ try std.testing.expect(apply(&state, find("Crt").?, "on"));
+ try std.testing.expect(apply(&state, find("Shader").?, "/b.glsl"));
+ const chain = state.post.list();
+ try std.testing.expectEqual(@as(usize, 3), chain.len);
+ try std.testing.expectEqualStrings("~/a.glsl", chain[0].path.get());
+ try std.testing.expectEqual(Scene.crt, chain[1].scene.?);
+ try std.testing.expectEqualStrings("/b.glsl", chain[2].path.get());
+ const moved = state.post.generation;
+ // A file named again leaves; the others keep their order.
+ try std.testing.expect(apply(&state, find("Shader").?, "~/a.glsl"));
+ try std.testing.expect(state.post.generation != moved);
+ try std.testing.expectEqual(Scene.crt, state.post.list()[0].scene.?);
+ try std.testing.expect(apply(&state, find("Shader").?, "off"));
+ try std.testing.expectEqual(@as(usize, 1), state.post.len);
+ try std.testing.expect(!apply(&state, find("Shader").?, null));
+ try std.testing.expect(apply(&state, find("ShaderAnimation").?, "always"));
+ try std.testing.expectEqual(ShaderAnimation.always, state.shader_animation);
+ try std.testing.expect(!apply(&state, find("ShaderAnimation").?, "sometimes"));
+ }
+
+ test "a bundled pass takes a level: on is the default, 0 and off take it out, and the old words still flip" {
+ var state: Runtime = .{};
+ const crt = find("Crt").?;
+ try std.testing.expect(apply(&state, crt, "on"));
+ try std.testing.expectEqual(@as(u8, Post.default_level), state.post.level(.crt));
+ try std.testing.expect(apply(&state, find("Shader").?, "/after.glsl"));
+ const moved = state.post.generation;
+ try std.testing.expect(apply(&state, crt, "3"));
+ try std.testing.expectEqual(@as(u8, 3), state.post.level(.crt));
+ // A new level keeps its place in the chain.
+ try std.testing.expectEqual(Scene.crt, state.post.list()[0].scene.?);
+ try std.testing.expect(state.post.generation != moved);
+ try std.testing.expect(!apply(&state, crt, "4"));
+ try std.testing.expect(!apply(&state, crt, "loud"));
+ try std.testing.expect(apply(&state, crt, "0"));
+ try std.testing.expect(!state.post.has(.crt));
+ try std.testing.expect(apply(&state, crt, null));
+ try std.testing.expectEqual(@as(u8, Post.default_level), state.post.level(.crt));
+ try std.testing.expect(apply(&state, crt, "off"));
+ try std.testing.expectEqual(@as(u8, 0), state.post.level(.crt));
}
test "tagline size validates before mutating live state" {
@@ -1295,6 +1463,8 @@ pub const Runtime = struct {
.window_blur => "40",
.message_ms => "1500",
.dump_dir => "~/dumps",
+ .shader => "~/crt.glsl",
+ .shader_animation => "always",
else => null,
};
try std.testing.expect(apply(&state, setting, argument));
@@ -1341,10 +1511,10 @@ pub const Runtime = struct {
.typewriter => "Panel transition PanelType\n",
},
.scene => |effect| switch (effect) {
- .crt => "Crt on\n",
- .ripple => "Ripple on\n",
- .glitch => "Glitch on\n",
+ .crt => "Crt 2\n",
},
+ .shader => "Shader ~/crt.glsl\n",
+ .shader_animation => "ShaderAnimation always\n",
.theme, .font => unreachable,
};
try std.testing.expect(std.mem.indexOf(u8, report, expected) != null);
@@ -1412,8 +1582,6 @@ pub const Runtime = struct {
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);