summaryrefslogtreecommitdiff
path: root/src/config.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 21:05:07 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commitdb2a259a18b9247edc23b1128dcfda703858f120 (patch)
tree11a877ab15b552be8fe7d79a4f1402a54b9f3d58 /src/config.zig
parent68dd567218505838814d73473460a0c0ff8e97b3 (diff)
downloadpardes-db2a259a18b9247edc23b1128dcfda703858f120.tar.gz
pardes-db2a259a18b9247edc23b1128dcfda703858f120.zip
A one-page A4 cheatsheet, the first file of a Typst docs corpus in docs/typ
docs/typ/cheatsheet.typ holds the facts as semantic calls (#key, #btn, #chord, #word, #tag, #addr, #file, #cmd, #doc); docs/typ/style.typ defines those once, drawing them for paged output and emitting HTML elements under --features html; docs/typ/cheatsheet-a4.typ is the page. `zig build cheatsheet` renders it when typst is on PATH. A config.zig test reads every .typ in docs/typ and fails when one names a builtin, key binding or default tag pardes no longer has. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/config.zig')
-rw-r--r--src/config.zig121
1 files changed, 121 insertions, 0 deletions
diff --git a/src/config.zig b/src/config.zig
index 6bb4d6e4..d3da47c1 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -2027,3 +2027,124 @@ test {
_ = Runtime;
_ = User;
}
+
+/// The docs in docs/typ name words, keys and tags; each must still be here.
+const doc_typ = struct {
+ /// The string argument of every `name("...")` call in a source.
+ fn calls(gpa: std.mem.Allocator, source: []const u8, comptime name: []const u8, out: *std.ArrayList([]const u8)) !void {
+ var at: usize = 0;
+ while (std.mem.indexOfPos(u8, source, at, name ++ "(\"")) |i| {
+ at = i + name.len + 2;
+ if (i > 0 and (std.ascii.isAlphanumeric(source[i - 1]) or source[i - 1] == '-')) continue;
+ const end = std.mem.indexOfScalarPos(u8, source, at, '"') orelse return error.Unterminated;
+ try out.append(gpa, source[at..end]);
+ }
+ }
+
+ fn word(w: []const u8) bool {
+ const name = w[0 .. std.mem.indexOfScalar(u8, w, '+') orelse w.len];
+ return std.meta.stringToEnum(Builtin, name) != null;
+ }
+
+ fn chord(tok: []const u8) ?Chord {
+ var c: Chord = .{ .cp = 0 };
+ var rest = tok;
+ while (rest.len > 2) {
+ if (std.mem.startsWith(u8, rest, "Ctrl-")) c.ctrl = true else if (std.mem.startsWith(u8, rest, "Alt-")) c.alt = true else if (std.mem.startsWith(u8, rest, "Shift-")) c.shift = true else break;
+ rest = rest[std.mem.indexOfScalar(u8, rest, '-').? + 1 ..];
+ }
+ const named = [_]struct { []const u8, u21 }{ .{ "Esc", Key.escape }, .{ "Enter", Key.enter }, .{ "Tab", Key.tab }, .{ "SPC", ' ' } };
+ for (named) |n| if (std.mem.eql(u8, rest, n[0])) {
+ c.cp = n[1];
+ return c;
+ };
+ if (rest.len != 1) return null;
+ c.cp = rest[0];
+ return c;
+ }
+
+ fn same(a: Chord, b: Chord) bool {
+ return a.cp == b.cp and a.ctrl == b.ctrl and a.alt == b.alt and a.shift == b.shift;
+ }
+
+ fn hasChordField(T: type) bool {
+ if (@typeInfo(T) != .array or @typeInfo(@typeInfo(T).array.child) != .@"struct") return false;
+ for (@typeInfo(@typeInfo(T).array.child).@"struct".fields) |f| if (f.type == Chord) return true;
+ return false;
+ }
+
+ /// Bound somewhere in this file: a binding, a prefix or a key list.
+ fn bound(c: Chord) bool {
+ const config = @import("config.zig");
+ inline for (@typeInfo(config).@"struct".decls) |d| {
+ const v = @field(config, d.name);
+ const T = @TypeOf(v);
+ if (T == []const Chord) {
+ for (v) |b| if (same(b, c)) return true;
+ } else if (T == comptime_int or T == u21 or T == u8) {
+ // a prefix or a bare key; only the tty toggle carries Ctrl
+ const plain = !c.alt and !c.shift and (!c.ctrl or comptime std.mem.indexOf(u8, d.name, "toggle") != null);
+ if (plain and v >= 0x20 and v < 0x7f and v == c.cp) return true;
+ } else if (comptime hasChordField(T)) {
+ for (v) |e| inline for (@typeInfo(@TypeOf(e)).@"struct".fields) |f|
+ if (f.type == Chord and same(@field(e, f.name), c)) return true;
+ }
+ }
+ return false;
+ }
+
+ fn key(k: []const u8) bool {
+ var toks = std.mem.tokenizeScalar(u8, k, ' ');
+ const first = toks.next() orelse return false;
+ if (std.mem.eql(u8, first, "SPC")) {
+ var path: [8]u8 = undefined;
+ var n: usize = 0;
+ while (toks.next()) |t| : (n += 1) {
+ if (t.len != 1 or n == path.len) return false;
+ path[n] = t[0];
+ }
+ if (n == 0) return leader.len == 1 and leader[0].cp == ' ';
+ for (std.enums.values(Builtin)) |b| if (leader_path.get(b)) |p|
+ if (std.mem.eql(u8, p, path[0..n])) return true;
+ return false;
+ }
+ toks.reset();
+ while (toks.next()) |t| if (!bound(chord(t) orelse return false)) return false;
+ return true;
+ }
+
+ fn tag(t: []const u8) bool {
+ var norm: [256]u8 = undefined;
+ var w: std.Io.Writer = .fixed(&norm);
+ var words = std.mem.tokenizeScalar(u8, t, ' ');
+ while (words.next()) |x| {
+ if (w.end > 0) w.writeByte(' ') catch return false;
+ w.writeAll(x[0 .. std.mem.indexOfScalar(u8, x, '+') orelse x.len]) catch return false;
+ }
+ const tags = [_][]const u8{ topbar_str, @import("tagline.zig").column_tag_default, pane_builtins_str, pdf_pane_builtins_str, file_pane_builtins_str, terminal_pane_builtins_str, command_pane_builtins_str };
+ for (tags) |d| if (std.mem.eql(u8, d, w.buffered())) return true;
+ return false;
+ }
+};
+
+test "docs/typ names only words, keys and default tags that exist" {
+ var arena: std.heap.ArenaAllocator = .init(std.testing.allocator);
+ defer arena.deinit();
+ const gpa = arena.allocator();
+ const io = std.testing.io;
+ var dir = try std.Io.Dir.cwd().openDir(io, @import("doc_typ").dir, .{ .iterate = true });
+ defer dir.close(io);
+ var found: [3]std.ArrayList([]const u8) = @splat(.empty);
+ var it = dir.iterate();
+ while (try it.next(io)) |entry| if (entry.kind == .file and std.mem.endsWith(u8, entry.name, ".typ")) {
+ const source = try dir.readFileAlloc(io, entry.name, gpa, .limited(1 << 20));
+ inline for (.{ "word", "key", "tag" }, &found) |name, *list| try doc_typ.calls(gpa, source, name, list);
+ };
+ inline for (.{ "word", "key", "tag" }, found) |name, list| {
+ try std.testing.expect(list.items.len >= 5);
+ for (list.items) |arg| if (!@field(doc_typ, name)(arg)) {
+ std.debug.print("docs/typ: {s}(\"{s}\") is not in pardes\n", .{ name, arg });
+ return error.DocDrift;
+ };
+ }
+}