summaryrefslogtreecommitdiff
path: root/build.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-26 18:58:37 -0300
committerGabriel Schneider <[email protected]>2026-08-27 09:47:39 -0300
commit29ac9be75fdcafbd7d05c15aa9eb8490d74caa98 (patch)
tree6629cc215d6953090f6b29a7414b28cb9990e105 /build.zig
parent11f380f6d7222f2cad93c2cdf13701ea1f903d47 (diff)
downloadpardes-29ac9be75fdcafbd7d05c15aa9eb8490d74caa98.tar.gz
pardes-29ac9be75fdcafbd7d05c15aa9eb8490d74caa98.zip
An edited row keeps its colours, four copies of forkShell become one, and Esc stops recentring
## A terminal row's ANSI colours survive being edited The loudest colour bug this editor had: one keystroke anywhere in a coloured shell row turned EVERY column of it grey. `EditAnchors` anchored a buffer line only when it was BYTE-IDENTICAL to the shell row it stood over, so a single differing byte dropped the whole row's colour projection. Worst shape is invisible: append past the pane's right edge, where the text is clipped, and the row looks the same and only its colour goes. Anchoring is byte-level now. An edit leaves the row's own bytes at both ends, and being the same bytes they keep the same colours; only what was typed has no cell under it, so only that takes none. Live, on real `fastfetch`: a 32-column blue run split into 6 + 26 around one typed character. Three defects underneath it, all found by machinery rather than by reading: * A JOIN removes a buffer line while the buffer's covered span grows, so `lines == covered` and both aligned guesses — Nth line over the Nth covered row, and the same counted from the bottom — resolved to the SAME wrong row. Every untouched row below a join went plain. Anchoring is now a streaming monotone matching: one shell-row cursor that only ever moves forward, advanced once per buffer line, linear in the buffer where the version before it was quadratic. * An EMPTY line is not evidence. Splitting a row makes one, it equals every blank row in the span, and left free to look ahead it claimed the blank row below the last output and took every coloured row in between out of reach of the lines that owned them. * Reflow under a scrolled viewport. `PageList.getTopLeft(.viewport)` returns the viewport pin verbatim, x and all, while `PageList.pin` forces x to 0 — so after a reflow remapped a tracked pin into the middle of a row, the text pass dumped row 0 from that column while the colour pass paired the fragment with the row's FIRST cells. Row 0 wore its left half's colours until the pane snapped back to live output. `bodyText` dumps from column zero now, which is also what ghostty's own renderer draws. Also here: DECSCNM (reverse video) was silently dropped whenever `tty_filter` was off, because the raw path resolved a `.none` colour by role and never consulted the mode. The test that found the first two is the one worth keeping: random editing against an ABSOLUTE oracle — every row's own text names the colour it must have — because the differential oracle it replaced was blind by construction. It skipped the edited row, which is the row the user is complaining about. ## Esc returns to a pane without moving its view Esc in body normal mode runs `Last`, "the pane you were in before this one", and that went through `focusPaneLine`, which recentred a file on the target line unconditionally. So returning to a buffer repainted the whole screen to show a line that was already on it. `focusPaneLine` takes a landing now: `.center` for the three callers going somewhere you have not been (a look target, a path a pane already holds, `@pN:LINE:COL`), `.keep` for Esc. `.keep` leaves the view alone and lets `ensureCursorVisible` — which already existed and already scrolls by the minimum into the `scroll_off` band — be the only thing that may move anything. Not `line = 0`, which `focusPaneLine` already understands as "focus and touch nothing": a background pane's view can move while you are away, because the wheel scrolls the pane under the POINTER and a resize reveals no cursor, so the recorded cursor plus a minimal nudge is what actually gets you back. Ctrl-o and Ctrl-i keep centring, and the asymmetry is structural rather than arbitrary: `Last` only ever CROSSES panes, so the pane it lands on already holds the view you left it with, while `jumpBy` can land in the SAME pane, where a long in-file jump would arrive on the very top or bottom row with `scroll_off` lines of context on one side. Helix splits the same pair the same way — its jumplist centres, its buffer switch does not. One deliberate consequence: under `.keep` a PDF's page is not restored AT ALL, because a page reveal IS that pane's view and a reveal of the page you are already on still snaps `document_scroll_y` to that page's start, discarding where you had read to. When something moved the pane while you were away — the wheel again — Esc leaves it where the wheel left it, and Ctrl-o is how you reach the recorded page. ## host_io.zig: the machine-local half of a host, once `host.zig` is the seam. The part of the answer that is identical on every host with an operating system under it — fork a pane's shell, put bytes on a disk — was written FOUR times: in tty.zig, gui.zig, macos.zig and detached/server.zig. What those copies had in common says what they were for: all four were missing FD_CLOEXEC on the pty master, so in every shell pardes has shipped, a program in one pane could read another pane's terminal. One copy now, and the wire got smaller for it: `ServerMsg.spawn` is gone. A frontend never asked the server to fork anything — the server has an operating system under it and forks through `host_io` like every other host — and `decodeClient` lost the scratch buffer that message needed.
Diffstat (limited to 'build.zig')
-rw-r--r--build.zig80
1 files changed, 73 insertions, 7 deletions
diff --git a/build.zig b/build.zig
index 07620ee1..c67caf20 100644
--- a/build.zig
+++ b/build.zig
@@ -3,6 +3,9 @@ const builtin = @import("builtin");
const mupdf_build = @import("mupdf.zig");
const snap_build = @import("build/snap.zig");
const grammar_manifest = @import("src/grammar_manifest.zig");
+/// The package manifest, imported UNTYPED on purpose — see the `version`
+/// option in `shellOptions` for why that word is load-bearing.
+const zon = @import("build.zig.zon");
/// `esp32p4` is not a shell in this package at all: it is one freestanding
/// OBJECT, compiled for riscv32-freestanding, which the `zig_p4` firmware
@@ -17,11 +20,30 @@ pub const Platform = enum { tty, gui, web, macos, esp32p4 };
/// from this, so there is one string and no drift.
pub const macos_min_version: std.SemanticVersion = .{ .major = 13, .minor = 0, .patch = 0 };
-/// The ZLS the language backend links, spelled once. It names the commit
-/// build.zig.zon pins (0.16.x branch) and is passed BOTH to ZLS's own
-/// `-Dversion-string` (its build.zig otherwise shells out to `git describe`,
-/// which fails on a fetched package that has no .git) and to `SPC l i`.
+/// The ZLS the language backend links. It names the commit build.zig.zon pins
+/// (0.16.x branch) and is passed BOTH to ZLS's own `-Dversion-string` (its
+/// build.zig otherwise shells out to `git describe`, which fails on a fetched
+/// package that has no .git) and to `SPC l i`.
+///
+/// It is spelled TWICE — here and in `.dependencies.zls.url` — and the comment
+/// that used to claim "spelled once" was wrong. The duplication is unavoidable
+/// (the semver half exists nowhere in the manifest), so the `comptime` block
+/// below makes the two AGREE by construction: a .zon bump that forgets this
+/// line is now a build error instead of a `SPC l i` that names a build nobody
+/// linked.
const zls_version = "0.16.1-dev+3e0d0820";
+
+comptime {
+ const plus = std.mem.indexOfScalar(u8, zls_version, '+') orelse
+ @compileError("zls_version must end in +<short commit>");
+ const short = zls_version[plus + 1 ..];
+ const url = zon.dependencies.zls.url;
+ const hash = std.mem.indexOfScalar(u8, url, '#') orelse
+ @compileError("the zls dependency url must pin a commit with #<sha>");
+ if (!std.mem.startsWith(u8, url[hash + 1 ..], short)) @compileError(
+ "zls_version pins commit " ++ short ++ ", build.zig.zon pins " ++ url[hash + 1 ..],
+ );
+}
pub const TreeSitterGrammars = enum { disabled, zig, minimal, full };
/// The GUI shell's shaders, spelled ONCE. The runtime SPIR-V imports, their
@@ -481,6 +503,7 @@ pub fn build(b: *std.Build) void {
.theme_animation = theme_animation,
.prebuilt_shaders = prebuilt_shaders,
.zig_lib_dir = b.graph.zig_lib_directory.path orelse "",
+ .commit = gitCommit(b),
};
// ONE `pardes_config` per distinct frontend in this build: two when the
// cli and the gui are made together, one otherwise. hxdiff's core and
@@ -1810,6 +1833,39 @@ fn prefixIsUntouched(b: *std.Build) bool {
std.mem.eql(u8, b.h_dir, b.pathJoin(&.{ b.install_path, "include" }));
}
+/// The commit this build came from, or null when there is nothing to say.
+///
+/// Read at CONFIGURE time and handed to `pardes_config`, so the binary carries
+/// a string rather than the ability to shell out. That is the whole point: a
+/// `--version` that runs `git` itself reports the tree it happens to be
+/// standing in rather than the one it was built from, and on the board there is
+/// no `git` to run and no process to run it with.
+///
+/// ABSENCE IS NOT AN ERROR, and must not be. A release tarball has no `.git`, a
+/// container may have no `git` binary, and a source drop is not a repository —
+/// none of those is a reason to refuse to build. Every way of having no answer
+/// (no binary, no repository, a git that exits non-zero, a git that prints
+/// nothing) lands on the same `null`, and the frontends print the version alone.
+///
+/// Deliberately NOT `--dirty`. Marking a dirty tree costs a worktree stat on
+/// every configure, and — the real cost — it would change `pardes_config` on
+/// every file edit. Every module in this build imports that options module, so
+/// a dirty marker means editing one line rebuilds the world. The commit alone
+/// changes only when a commit does.
+fn gitCommit(b: *std.Build) ?[]const u8 {
+ var code: u8 = 0;
+ // `-C` the build root rather than trusting the cwd: `zig build` may be run
+ // from anywhere, and a `git` resolved against the wrong directory would
+ // cheerfully answer about a DIFFERENT repository.
+ const out = b.runAllowFail(
+ &.{ "git", "-C", b.build_root.path orelse ".", "rev-parse", "--short=12", "HEAD" },
+ &code,
+ .ignore,
+ ) catch return null;
+ const trimmed = std.mem.trim(u8, out, " \t\r\n");
+ return if (trimmed.len == 0) null else trimmed;
+}
+
/// Everything `pardes_config` says that does NOT depend on which frontend is
/// being built. Gathered into one value so the two options modules a default
/// build makes cannot drift apart in any field but the one that is supposed to
@@ -1827,6 +1883,10 @@ const ShellConfig = struct {
theme_animation: ?bool,
prebuilt_shaders: bool,
zig_lib_dir: []const u8,
+ /// Which commit this build came from, or null when there is no answer. Read
+ /// once in `build()` so the two shells of a default build cannot disagree,
+ /// and so `git` is spawned once rather than per frontend.
+ commit: ?[]const u8,
};
/// One `pardes_config` options module, for one frontend. `module_names` and
@@ -1852,9 +1912,15 @@ fn shellOptions(b: *std.Build, cfg: ShellConfig, platform: Platform) *std.Build.
// names: the .zon pins a commit, and a status screen that cannot say WHICH
// analyser answered is not worth opening.
o.addOption([]const u8, "zls_version", if (cfg.zls_backend) zls_version else "none");
- // Mirrors build.zig.zon's `.version`; kept in sync by hand because a typed
- // @import of the .zon rejects its other fields on this Zig 0.16 build.
- o.addOption([]const u8, "version", "0.0.1");
+ // THE version, read from the manifest rather than copied beside it. The
+ // `@import` being UNTYPED is what makes that possible: annotating its type
+ // would demand an exact field match and reject `.dependencies`, `.paths`
+ // and the rest, which is the failure the hand-synced literal that used to
+ // sit here was working around. Verified against this exact manifest.
+ o.addOption([]const u8, "version", zon.version);
+ // ...and the commit it was built from, when there is one. See `gitCommit`
+ // for why this is optional and why it is read at configure time.
+ o.addOption(?[]const u8, "commit", cfg.commit);
// The stdlib this binary was compiled against, so `gd` on `std.mem.count`
// can open the same mem.zig the compiler used. ZLS resolves `@import("std")`
// through `zig_lib_dir` and nothing else; without it every std symbol is a