From 3475ca95c08011aadefd520e1f2caa3e7d045c1a Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 28 Oct 2026 09:22:29 -0400 Subject: [PATCH] Various changes. --- README.md | 2 +- build.zig | 4 ++-- build.zig.zon | 10 +++++----- flake.lock | 6 +++--- flake.nix | 9 +++++++-- mise.toml | 2 +- src/Browser.zig | 2 +- src/FindBar.zig | 6 +++--- src/Layouts.zig | 4 ++-- src/OpenLayoutDialog.zig | 8 ++++---- src/PaletteEditor.zig | 7 ++++--- src/PathEntry.zig | 14 +++++++------- src/Pty.zig | 12 ++++++------ src/Review.zig | 2 +- src/SaveLayoutDialog.zig | 6 +++--- src/Session.zig | 20 ++++++++++++++------ src/Settings.zig | 4 ++-- src/SettingsDialog.zig | 12 ++++++------ src/Snapshot.zig | 4 ++-- src/TabSettingsDialog.zig | 4 ++-- src/Window.zig | 11 ++++++----- src/emoji.zig | 2 +- src/main.zig | 4 ++-- src/notify.zig | 6 ++++-- src/paths.zig | 14 +++++++------- src/review/git.zig | 2 +- src/scope.zig | 4 ++-- src/script.zig | 4 ++-- src/shortcuts.zig | 10 +++++++++- 29 files changed, 110 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 93b5125..acf6f97 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ with the content inset to its right. ## Quick start ```sh -nix develop # Zig 0.16 + GTK4 + libadwaita + node, no host toolchain needed +nix develop # Zig 0.17 + GTK4 + libadwaita + node, no host toolchain needed mise run web-deps # once: npm deps for the review pane's UI zig build run ``` diff --git a/build.zig b/build.zig index 31dea02..3cf8898 100644 --- a/build.zig +++ b/build.zig @@ -110,7 +110,7 @@ pub fn build(b: *std.Build) void { const run_cmd = b.addRunArtifact(exe); run_cmd.step.dependOn(b.getInstallStep()); - if (b.args) |args| run_cmd.addArgs(args); + run_cmd.addPassthruArgs(); b.step("run", "Run the app").dependOn(&run_cmd.step); // Tests are rooted at Layouts.zig rather than main.zig: the parts of this @@ -319,7 +319,7 @@ fn addWebInputs(b: *std.Build, run: *std.Build.Step.Run) void { const io = b.graph.io; for ([_][]const u8{ "web/src", "web/public" }) |root| { - var dir = b.build_root.handle.openDir(io, root, .{ .iterate = true }) catch continue; + var dir = b.root.openDir(io, root, .{ .iterate = true }) catch continue; defer dir.close(io); var walker = dir.walk(b.allocator) catch continue; diff --git a/build.zig.zon b/build.zig.zon index 2ae70a9..4868923 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -4,7 +4,7 @@ // Half package-name hash, half per-package constant, so renaming the // package invalidates it. Regenerated when vtabs became playpen. .fingerprint = 0x5a26ca72bb4d2c53, - .minimum_zig_version = "0.16.0", + .minimum_zig_version = "0.17.0", .paths = .{ "build.zig", "build.zig.zon", @@ -12,12 +12,12 @@ }, .dependencies = .{ .ghostty = .{ - .url = "https://github.com/ghostty-org/ghostty/archive/94d775fefc21f74d9cc85a46b34c4e1d85318fd0.tar.gz", - .hash = "ghostty-1.3.2-dev-5UdBCyCSPwV-Pp82UMGLutqeo0gckMrI0GfbUbUY0-Jt", + .url = "https://github.com/ghostty-org/ghostty/archive/83fb95624b0439ee660c2679b43b38c8b63769cd.tar.gz", + .hash = "ghostty-1.4.0-5UdBC-qohwWqLS0BImOz05t4hVyTwbaKslyTPvKYvAtE", }, .gobject = .{ - .url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.10.0-2026-07-28-36-1/ghostty-gobject-0.10.0-2026-07-28-36-1.tar.zst", - .hash = "gobject-0.3.2-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3", + .url = "https://github.com/ghostty-org/zig-gobject/releases/download/0.11.0-zig-0.17.0-2026-10-02-38-1/ghostty-gobject-0.11.0-zig-0.17.0-2026-10-02-38-1.tar.zst", + .hash = "gobject-0.3.2-Skun7LuIogBbEh-6WrLeEzMaRCnOMB3eOf__RaPYSMCZ", }, }, } diff --git a/flake.lock b/flake.lock index 364ce8d..55f0e6a 100644 --- a/flake.lock +++ b/flake.lock @@ -66,11 +66,11 @@ ] }, "locked": { - "lastModified": 1786364596, - "narHash": "sha256-rMHN2/qJ10ORy+PfSm8hfG2mCdnsYHsWrKeq8DLraec=", + "lastModified": 1790988737, + "narHash": "sha256-b55etI4Uxu+WHhYZXym3HPIz63ipO9lqwJLAphjqtTc=", "owner": "mitchellh", "repo": "zig-overlay", - "rev": "840c2310085417892ec51f691002ca0a497d0287", + "rev": "dcffc7a25ed2cbbc7c1c3c448c312abf46ae6cb8", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 1d0537d..bd7d7f1 100644 --- a/flake.nix +++ b/flake.nix @@ -5,6 +5,11 @@ # Ghostty tracks nixpkgs-unstable to get GTK 4.20 / GNOME 49. We follow # suit so that our zig-gobject bindings (shared with Ghostty) match the # GObject introspection data of the GTK we link against. + # + # Follow the channel, not necessarily Ghostty's exact lock: the binary + # runs on the host's dynamic loader and libc, so a nixpkgs whose glibc is + # newer than the host's builds fine and then fails to start with + # "GLIBC_2.43 not found". What has to match is GTK's version, not the rev. nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"; flake-compat = { @@ -17,7 +22,7 @@ flake = false; }; - # Zig 0.16.0 is not in nixpkgs yet; libghostty-vt requires it. + # Zig 0.17.0 is not in nixpkgs yet; libghostty-vt requires it. zig = { url = "github:mitchellh/zig-overlay"; inputs = { @@ -45,7 +50,7 @@ name = "playpen"; nativeBuildInputs = [ - zig.packages.${system}."0.16.0" + zig.packages.${system}."0.17.0" pkgs.pkg-config pkgs.gdb diff --git a/mise.toml b/mise.toml index a6d6ce0..cbc66f9 100644 --- a/mise.toml +++ b/mise.toml @@ -11,7 +11,7 @@ run = "nix develop --command npm --prefix web run dev" [tasks.build] description = "Build a release binary into zig-out/bin" -run = "nix develop --command zig build -Doptimize=ReleaseFast" +run = "nix develop --command zig build -Doptimize=fast" [tasks.build-debug] description = "Build a debug binary (leak checking, safety checks)" diff --git a/src/Browser.zig b/src/Browser.zig index 56a42bd..7b65f41 100644 --- a/src/Browser.zig +++ b/src/Browser.zig @@ -264,7 +264,7 @@ fn resolve(self: *Browser, raw: []const u8) ?[:0]const u8 { return copyZ(&self.url_buf, input); } if (looksLikeHost(input)) { - return std.fmt.bufPrintZ(&self.url_buf, "https://{s}", .{input}) catch null; + return std.mem.printSentinel(&self.url_buf, "https://{s}", .{input}, 0) catch null; } return searchUrl(&self.url_buf, input); } diff --git a/src/FindBar.zig b/src/FindBar.zig index 9625027..9096e94 100644 --- a/src/FindBar.zig +++ b/src/FindBar.zig @@ -279,11 +279,11 @@ fn onCountedMatches(_: *webkit.FindController, matches: c_uint, self: *FindBar) // it gets a "or more" rather than a number that isn't the total. var buf: [32]u8 = undefined; const label = if (matches >= max_matches) - std.fmt.bufPrintZ(&buf, "{d}+ matches", .{max_matches}) catch return + std.mem.printSentinel(&buf, "{d}+ matches", .{max_matches}, 0) catch return else if (matches == 1) - std.fmt.bufPrintZ(&buf, "1 match", .{}) catch return + std.mem.printSentinel(&buf, "1 match", .{}, 0) catch return else - std.fmt.bufPrintZ(&buf, "{d} matches", .{matches}) catch return; + std.mem.printSentinel(&buf, "{d} matches", .{matches}, 0) catch return; self.status.setText(label); } diff --git a/src/Layouts.zig b/src/Layouts.zig index 98bf8ef..35f90c6 100644 --- a/src/Layouts.zig +++ b/src/Layouts.zig @@ -150,13 +150,13 @@ pub fn find(self: *Layouts, name: []const u8) ?*Layout { /// when that isn't set — whichever GLib reports as the user's config dir. pub fn configPath(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen/layouts.json", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen/layouts.json", .{dir}, 0) catch null; } /// The directory `configPath` lives in. fn configDir(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen", .{dir}, 0) catch null; } // ------------------------------------------------------------------------- diff --git a/src/OpenLayoutDialog.zig b/src/OpenLayoutDialog.zig index 65ce30a..ca59897 100644 --- a/src/OpenLayoutDialog.zig +++ b/src/OpenLayoutDialog.zig @@ -61,7 +61,7 @@ pub fn present( }; var title_buf: [128]u8 = undefined; - const title = std.fmt.bufPrintZ(&title_buf, "Open “{s}”", .{layout.name}) catch "Open layout"; + const title = std.mem.printSentinel(&title_buf, "Open “{s}”", .{layout.name}, 0) catch "Open layout"; self.window.setTitle(title); self.window.setTransientFor(parent); self.window.setModal(1); @@ -81,14 +81,14 @@ pub fn present( // is the fallback so a parameter is never unlabeled. var label_buf: [128]u8 = undefined; const text = if (param.description.len > 0) param.description else param.name; - label.setText(std.fmt.bufPrintZ(&label_buf, "{s}", .{text}) catch "parameter"); + label.setText(std.mem.printSentinel(&label_buf, "{s}", .{text}, 0) catch "parameter"); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-dialog-label"); fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1); var hint_buf: [128]u8 = undefined; const placeholder: ?[*:0]const u8 = - std.fmt.bufPrintZ(&hint_buf, "{{{{{s}}}}}", .{param.name}) catch null; + std.mem.printSentinel(&hint_buf, "{{{{{s}}}}}", .{param.name}, 0) catch null; const entry, const field = switch (param.type) { .string => blk: { @@ -150,7 +150,7 @@ pub fn present( fn setEntryText(entry: *gtk.Entry, text: []const u8) void { var buf: [1024]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return; + const z = std.mem.printSentinel(&buf, "{s}", .{text}, 0) catch return; entry.as(gtk.Editable).setText(z); } diff --git a/src/PaletteEditor.zig b/src/PaletteEditor.zig index 47922cd..35c9ebf 100644 --- a/src/PaletteEditor.zig +++ b/src/PaletteEditor.zig @@ -423,11 +423,11 @@ fn buildRow(self: *PaletteEditor, key: palette.Key) *gtk.Widget { // the settings file, since anyone hand-editing that file is looking at this // list to find out what to call things. var tip: [256]u8 = undefined; - const tooltip: [:0]const u8 = std.fmt.bufPrintZ(&tip, "{s}{s}{s}", .{ + const tooltip: [:0]const u8 = std.mem.printSentinel(&tip, "{s}{s}{s}", .{ key.hint() orelse "", if (key.hint() != null) "\n" else "", @tagName(key), - }) catch @tagName(key); + }, 0) catch @tagName(key); row.as(gtk.Widget).setTooltipText(tooltip); const hex = gtk.Label.new(null); @@ -497,11 +497,12 @@ fn reload(self: *PaletteEditor) void { const changes = Settings.get().colors.of(scheme); var buf: [192]u8 = undefined; - const heading: [:0]const u8 = std.fmt.bufPrintZ( + const heading: [:0]const u8 = std.mem.printSentinel( &buf, "Editing the {s} palette, the one on screen — changes apply as you make them. " ++ "Switch the theme above to edit the other.", .{scheme.label()}, + 0, ) catch "Changes apply as you make them."; self.scheme_hint.setText(heading); diff --git a/src/PathEntry.zig b/src/PathEntry.zig index c528859..52c2ef6 100644 --- a/src/PathEntry.zig +++ b/src/PathEntry.zig @@ -193,7 +193,7 @@ pub fn setText(self: *PathEntry, value: []const u8) void { defer self.quiet = false; var buf: [max_path]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}", .{value}) catch return; + const z = std.mem.printSentinel(&buf, "{s}", .{value}, 0) catch return; self.entry.as(gtk.Editable).setText(z); self.hide(); } @@ -243,7 +243,7 @@ fn fill(self: *PathEntry) void { for (offer.names) |name| { var buf: [max_path]u8 = undefined; - const label = gtk.Label.new(std.fmt.bufPrintZ(&buf, "{s}/", .{name}) catch continue); + const label = gtk.Label.new(std.mem.printSentinel(&buf, "{s}/", .{name}, 0) catch continue); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-path-row"); self.list.append(label.as(gtk.Widget)); @@ -253,7 +253,7 @@ fn fill(self: *PathEntry) void { if (hidden == 0) return; var buf: [64]u8 = undefined; - const counted = std.fmt.bufPrintZ(&buf, "… {d} more", .{hidden}) catch return; + const counted = std.mem.printSentinel(&buf, "… {d} more", .{hidden}, 0) catch return; const label = gtk.Label.new(counted); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-path-more"); @@ -309,10 +309,10 @@ fn accept(self: *PathEntry, index: usize) void { if (offer.dir_len > value.len) return self.hide(); var buf: [max_path]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}{s}/", .{ + const z = std.mem.printSentinel(&buf, "{s}{s}/", .{ value[0..offer.dir_len], offer.names[index], - }) catch return; + }, 0) catch return; // Not quiet: writing this re-reads the directory it names, so completing // `~/src/pl` to `~/src/playpen/` immediately offers what is inside it. @@ -338,10 +338,10 @@ fn complete(self: *PathEntry) void { if (shared <= typed) return; var buf: [max_path]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}{s}", .{ + const z = std.mem.printSentinel(&buf, "{s}{s}", .{ value[0..offer.dir_len], offer.names[0][0..shared], - }) catch return; + }, 0) catch return; self.entry.as(gtk.Editable).setText(z); self.entry.as(gtk.Editable).setPosition(-1); diff --git a/src/Pty.zig b/src/Pty.zig index 3c159f3..60024b4 100644 --- a/src/Pty.zig +++ b/src/Pty.zig @@ -126,7 +126,7 @@ pub fn create( const envp_z = try buildEnv(alloc, env_extra); defer freeEnv(alloc, envp_z); - const slave_path_z = try alloc.dupeZ(u8, slave_path); + const slave_path_z = try alloc.dupeSentinel(u8, slave_path, 0); defer alloc.free(slave_path_z); const pid = c.fork(); @@ -226,12 +226,12 @@ fn buildEnv( if (std.mem.startsWith(u8, span, "TERM=")) continue; if (std.mem.startsWith(u8, span, "COLORTERM=")) continue; if (shadowedBy(span, extra)) continue; - try list.append(alloc, (try alloc.dupeZ(u8, span)).ptr); + try list.append(alloc, (try alloc.dupeSentinel(u8, span, 0)).ptr); } - try list.append(alloc, (try alloc.dupeZ(u8, "TERM=xterm-256color")).ptr); - try list.append(alloc, (try alloc.dupeZ(u8, "COLORTERM=truecolor")).ptr); + try list.append(alloc, (try alloc.dupeSentinel(u8, "TERM=xterm-256color", 0)).ptr); + try list.append(alloc, (try alloc.dupeSentinel(u8, "COLORTERM=truecolor", 0)).ptr); for (extra) |entry| { - try list.append(alloc, (try alloc.dupeZ(u8, entry)).ptr); + try list.append(alloc, (try alloc.dupeSentinel(u8, entry, 0)).ptr); } const result = try alloc.allocSentinel(?[*:0]const u8, list.items.len, null); @@ -264,7 +264,7 @@ fn freeEnv(alloc: std.mem.Allocator, envp: [:null]?[*:0]const u8) void { /// portable API for it, and this app is Linux-only regardless. pub fn cwd(self: Pty, buf: []u8) ?[]const u8 { var link_buf: [64]u8 = undefined; - const link = std.fmt.bufPrintZ(&link_buf, "/proc/{d}/cwd", .{self.pid}) catch return null; + const link = std.mem.printSentinel(&link_buf, "/proc/{d}/cwd", .{self.pid}, 0) catch return null; const n = c.readlink(link.ptr, buf.ptr, buf.len); if (n <= 0) return null; diff --git a/src/Review.zig b/src/Review.zig index e40b33f..620e678 100644 --- a/src/Review.zig +++ b/src/Review.zig @@ -98,7 +98,7 @@ pub fn create( const self = try alloc.create(Review); errdefer alloc.destroy(self); - const url = try alloc.dupeZ(u8, opts.url); + const url = try alloc.dupeSentinel(u8, opts.url, 0); errdefer alloc.free(url); self.* = .{ diff --git a/src/SaveLayoutDialog.zig b/src/SaveLayoutDialog.zig index cd6112e..257805b 100644 --- a/src/SaveLayoutDialog.zig +++ b/src/SaveLayoutDialog.zig @@ -233,10 +233,10 @@ fn buildPaneSection(self: *SaveLayoutDialog, index: usize, leaf: *Layouts.Node) box.as(gtk.Widget).addCssClass("playpen-dialog-pane"); var title_buf: [64]u8 = undefined; - const title = std.fmt.bufPrintZ(&title_buf, "Pane {d} — {s}", .{ + const title = std.mem.printSentinel(&title_buf, "Pane {d} — {s}", .{ index + 1, @tagName(spec.kind), - }) catch "Pane"; + }, 0) catch "Pane"; const label = gtk.Label.new(title); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-dialog-label"); @@ -524,6 +524,6 @@ fn hint(text: [:0]const u8) *gtk.Widget { fn setEntryText(entry: *gtk.Entry, text: []const u8) void { var buf: [4096]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return; + const z = std.mem.printSentinel(&buf, "{s}", .{text}, 0) catch return; entry.as(gtk.Editable).setText(z); } diff --git a/src/Session.zig b/src/Session.zig index e572fcc..ab69652 100644 --- a/src/Session.zig +++ b/src/Session.zig @@ -185,7 +185,7 @@ pub fn create( defer alloc.free(argv0); const cwd_z: ?[:0]const u8 = if (opts.cwd.len > 0) - try alloc.dupeZ(u8, opts.cwd) + try alloc.dupeSentinel(u8, opts.cwd, 0) else null; defer if (cwd_z) |z| alloc.free(z); @@ -246,14 +246,14 @@ pub fn destroy(self: *Session) void { /// for systems with no usable passwd entry, such as minimal containers. fn defaultShell(alloc: std.mem.Allocator) ![:0]const u8 { var buf: [std.fs.max_path_bytes]u8 = undefined; - if (Pty.loginShell(&buf)) |shell| return alloc.dupeZ(u8, shell); + if (Pty.loginShell(&buf)) |shell| return alloc.dupeSentinel(u8, shell, 0); if (std.c.getenv("SHELL")) |sh| { const span = std.mem.span(sh); - if (span.len > 0) return alloc.dupeZ(u8, span); + if (span.len > 0) return alloc.dupeSentinel(u8, span, 0); } - return alloc.dupeZ(u8, "/bin/sh"); + return alloc.dupeSentinel(u8, "/bin/sh", 0); } /// Main loop callback: the PTY has data (or hung up). @@ -305,7 +305,7 @@ fn flushStartupCommand(self: *Session) void { } /// Effect callback: the terminal wants to send bytes back to the child. -fn effectWritePty(handler: *vt.TerminalStream.Handler, data: [:0]const u8) void { +fn effectWritePty(handler: *vt.TerminalStream.Handler, data: []const u8) void { const self = fromHandler(handler); self.pty.writeAll(data); } @@ -394,8 +394,16 @@ fn fromHandler(handler: *vt.TerminalStream.Handler) *Session { /// palette *defaults* to, so anything the program inside set for itself with /// OSC 4 survives the switch. The caller still has to queue a redraw — nothing /// here touches the widget. +/// +/// A palette other than libghostty's built-in one is copied into memory the +/// terminal owns. If that copy can't be made, fall back to the built-in +/// palette, which never allocates, rather than keep the previous scheme's. pub fn refreshPalette(self: *Session) void { - self.term.colors.palette.changeDefault(theme.ansiPalette()); + const palette = &self.term.colors.palette; + palette.changeDefault(self.alloc, theme.ansiPalette()) catch |err| { + std.log.err("could not change the palette: {s}", .{@errorName(err)}); + palette.resetDefault(self.alloc); + }; } /// Send user input to the child. diff --git a/src/Settings.zig b/src/Settings.zig index a10ed8a..8b0fdd7 100644 --- a/src/Settings.zig +++ b/src/Settings.zig @@ -740,12 +740,12 @@ fn serialize(self: *Settings) SaveError![]u8 { /// `~/.config/playpen/settings.json` when that isn't set. fn configPath(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen/settings.json", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen/settings.json", .{dir}, 0) catch null; } fn configDir(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserConfigDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen", .{dir}, 0) catch null; } // ------------------------------------------------------------------------- diff --git a/src/SettingsDialog.zig b/src/SettingsDialog.zig index ebd42ec..aede9f2 100644 --- a/src/SettingsDialog.zig +++ b/src/SettingsDialog.zig @@ -648,7 +648,7 @@ fn newLayoutPicker(self: *SettingsDialog, selected: []const u8) *gtk.DropDown { for (self.layouts.items.items) |layout| { var buf: [256]u8 = undefined; - const name = std.fmt.bufPrintZ(&buf, "{s}", .{layout.name}) catch continue; + const name = std.mem.printSentinel(&buf, "{s}", .{layout.name}, 0) catch continue; model.append(name.ptr); if (std.mem.eql(u8, layout.name, selected)) index = count; count += 1; @@ -656,7 +656,7 @@ fn newLayoutPicker(self: *SettingsDialog, selected: []const u8) *gtk.DropDown { if (selected.len > 0 and index == 0) { var buf: [256]u8 = undefined; - if (std.fmt.bufPrintZ(&buf, "{s}", .{selected})) |name| { + if (std.mem.printSentinel(&buf, "{s}", .{selected}, 0)) |name| { model.append(name.ptr); index = count; } else |_| {} @@ -712,7 +712,7 @@ fn fillParams( var hint_buf: [128]u8 = undefined; const placeholder: ?[*:0]const u8 = - std.fmt.bufPrintZ(&hint_buf, "{{{{{s}}}}}", .{param.name}) catch null; + std.mem.printSentinel(&hint_buf, "{{{{{s}}}}}", .{param.name}, 0) catch null; const value = valueOf(values, param.name) orelse param.default; // The same two shapes the layout prompt offers, for the same reason: a @@ -741,9 +741,9 @@ fn fillParams( errdefer self.alloc.free(field.name); var label_buf: [128]u8 = undefined; - const text = std.fmt.bufPrintZ(&label_buf, "{s}", .{ + const text = std.mem.printSentinel(&label_buf, "{s}", .{ if (param.description.len > 0) param.description else param.name, - }) catch "parameter"; + }, 0) catch "parameter"; const label = gtk.Label.new(text); label.setXalign(0); label.as(gtk.Widget).addCssClass("playpen-dialog-sublabel"); @@ -974,7 +974,7 @@ fn hint(text: [:0]const u8) *gtk.Widget { fn setEntryText(entry: *gtk.Entry, text: []const u8) void { var buf: [4096]u8 = undefined; - const z = std.fmt.bufPrintZ(&buf, "{s}", .{text}) catch return; + const z = std.mem.printSentinel(&buf, "{s}", .{text}, 0) catch return; entry.as(gtk.Editable).setText(z); } diff --git a/src/Snapshot.zig b/src/Snapshot.zig index c4d0c5e..cf5234f 100644 --- a/src/Snapshot.zig +++ b/src/Snapshot.zig @@ -141,12 +141,12 @@ pub fn add(self: *Snapshot, tab: Tab) !void { /// when that isn't set — whichever GLib reports as the user's state dir. fn statePath(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserStateDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen/session.json", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen/session.json", .{dir}, 0) catch null; } fn stateDir(buf: []u8) ?[:0]const u8 { const dir = std.mem.span(glib.getUserStateDir()); - return std.fmt.bufPrintZ(buf, "{s}/playpen", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}/playpen", .{dir}, 0) catch null; } // ------------------------------------------------------------------------- diff --git a/src/TabSettingsDialog.zig b/src/TabSettingsDialog.zig index 115b1fc..1403f36 100644 --- a/src/TabSettingsDialog.zig +++ b/src/TabSettingsDialog.zig @@ -116,9 +116,9 @@ pub fn present( }; var title_buf: [160]u8 = undefined; - const title = std.fmt.bufPrintZ(&title_buf, "Tab settings — {s}", .{ + const title = std.mem.printSentinel(&title_buf, "Tab settings — {s}", .{ clip(opts.tab_name, 96), - }) catch "Tab settings"; + }, 0) catch "Tab settings"; self.window.setTitle(title); self.window.setTransientFor(parent); diff --git a/src/Window.zig b/src/Window.zig index a0c5d1b..b56cc7a 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -577,7 +577,7 @@ fn newTabEmpty(self: *Window) !*Tab { .name = undefined, .name_len = 0, }; - const printed = std.fmt.bufPrintZ(&tab.name, "tab{d}", .{id}) catch unreachable; + const printed = std.mem.printSentinel(&tab.name, "tab{d}", .{id}, 0) catch unreachable; tab.name_len = printed.len; // ---- sidebar row --------------------------------------------------- @@ -960,7 +960,7 @@ fn refreshLayoutMenu(self: *Window) void { const line = gtk.Box.new(.horizontal, 4); var label_buf: [128]u8 = undefined; - const text = std.fmt.bufPrintZ(&label_buf, "{s}", .{layout.name}) catch continue; + const text = std.mem.printSentinel(&label_buf, "{s}", .{layout.name}, 0) catch continue; const open = gtk.Button.newWithLabel(text); open.as(gtk.Widget).addCssClass("flat"); @@ -1491,10 +1491,10 @@ fn offerRestore(self: *Window) void { // is, just without the number. const count = self.snapshot.tabs.items.len; var buf: [64]u8 = undefined; - const detail: [:0]const u8 = std.fmt.bufPrintZ(&buf, "{d} tab{s} from last time", .{ + const detail: [:0]const u8 = std.mem.printSentinel(&buf, "{d} tab{s} from last time", .{ count, if (count == 1) "" else "s", - }) catch "from last time"; + }, 0) catch "from last time"; self.restore_detail.setText(detail.ptr); self.restore_offered = true; @@ -2352,7 +2352,7 @@ fn askBeforeQuitting(self: *Window) void { // window. The buffer is sized so the format cannot fail, but the fallback // says the same thing without the number rather than nothing at all. var buf: [160]u8 = undefined; - const body: [:0]const u8 = std.fmt.bufPrintZ( + const body: [:0]const u8 = std.mem.printSentinel( &buf, "{d} tab{s} will close, and every shell in {s} will exit.", .{ @@ -2360,6 +2360,7 @@ fn askBeforeQuitting(self: *Window) void { if (self.tabs.items.len == 1) "" else "s", if (self.tabs.items.len == 1) "it" else "them", }, + 0, ) catch "Every tab will close, and every shell in them will exit."; dialog.setBody(body.ptr); diff --git a/src/emoji.zig b/src/emoji.zig index 1ad4f5c..c12ed3a 100644 --- a/src/emoji.zig +++ b/src/emoji.zig @@ -44,7 +44,7 @@ pub const Emoji = struct { pub fn matches(emoji: Emoji, query: []const u8) bool { var terms = std.mem.tokenizeAny(u8, query, " \t"); while (terms.next()) |term| { - if (std.ascii.indexOfIgnoreCase(emoji.keywords, term) == null) return false; + if (std.ascii.findIgnoreCase(emoji.keywords, term) == null) return false; } return true; } diff --git a/src/main.zig b/src/main.zig index d95d88c..3b73459 100644 --- a/src/main.zig +++ b/src/main.zig @@ -37,11 +37,11 @@ pub const std_options: std.Options = .{ var debug_gpa: std.heap.DebugAllocator(.{}) = .init; fn gpa() std.mem.Allocator { - return if (builtin.mode == .Debug) debug_gpa.allocator() else std.heap.c_allocator; + return if (builtin.mode == .debug) debug_gpa.allocator() else std.heap.c_allocator; } pub fn main() u8 { - defer if (builtin.mode == .Debug) { + defer if (builtin.mode == .debug) { _ = debug_gpa.deinit(); }; diff --git a/src/notify.zig b/src/notify.zig index 548be05..ff3053a 100644 --- a/src/notify.zig +++ b/src/notify.zig @@ -100,20 +100,22 @@ pub const Mute = union(enum) { .until => |deadline| blk: { const left_min = @divTrunc(deadline - now + 59_999, std.time.ms_per_min); if (left_min <= 1) break :blk "Muted for another minute"; - if (left_min < 60) break :blk std.fmt.bufPrintZ( + if (left_min < 60) break :blk std.mem.printSentinel( buf, "Muted for another {d} minutes", .{left_min}, + 0, ) catch "Muted"; // Rounded down and in hours past the first: someone who muted a // tab for eight hours wants to know it is still most of the // afternoon, not that it is 437 minutes. const left_hours = @divTrunc(left_min, 60); - break :blk std.fmt.bufPrintZ( + break :blk std.mem.printSentinel( buf, "Muted for another {d} hours", .{left_hours}, + 0, ) catch "Muted"; }, }; diff --git a/src/paths.zig b/src/paths.zig index 6cbf63c..6f69f5c 100644 --- a/src/paths.zig +++ b/src/paths.zig @@ -159,10 +159,10 @@ pub fn resolve(buf: []u8, dir: []const u8) ?[:0]const u8 { if (dir[0] == '~' and (dir.len == 1 or dir[1] == '/')) { const home = std.mem.span(glib.getHomeDir()); if (home.len == 0) return null; - return std.fmt.bufPrintZ(buf, "{s}{s}", .{ home, dir[1..] }) catch null; + return std.mem.printSentinel(buf, "{s}{s}", .{ home, dir[1..] }, 0) catch null; } - return std.fmt.bufPrintZ(buf, "{s}", .{dir}) catch null; + return std.mem.printSentinel(buf, "{s}", .{dir}, 0) catch null; } /// The typed value, if it is a directory that exists. Used for where to open @@ -179,7 +179,7 @@ pub fn existingDirectory(buf: []u8, value: []const u8) ?[:0]const u8 { fn isDirectory(dir: [:0]const u8, name: []const u8) bool { var buf: [max_path]u8 = undefined; const separator: []const u8 = if (std.mem.endsWith(u8, dir, "/")) "" else "/"; - const path = std.fmt.bufPrintZ(&buf, "{s}{s}{s}", .{ dir, separator, name }) catch + const path = std.mem.printSentinel(&buf, "{s}{s}{s}", .{ dir, separator, name }, 0) catch return false; return glib.fileTest(path.ptr, .{ .is_dir = true }) != 0; } @@ -214,17 +214,17 @@ const Tree = struct { }; defer glib.free(raw); - const root = try testing.allocator.dupeZ(u8, std.mem.span(raw)); + const root = try testing.allocator.dupeSentinel(u8, std.mem.span(raw), 0); for (names) |name| { var buf: [max_path]u8 = undefined; - const dir = try std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ root, name }); + const dir = try std.mem.printSentinel(&buf, "{s}/{s}", .{ root, name }, 0); if (glib.mkdirWithParents(dir.ptr, 0o700) != 0) return error.MkdirFailed; } for (files) |name| { var buf: [max_path]u8 = undefined; - const file = try std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ root, name }); + const file = try std.mem.printSentinel(&buf, "{s}/{s}", .{ root, name }, 0); if (glib.fileSetContents(file.ptr, "x", 1, &err) == 0) { if (err) |e| e.free(); return error.WriteFailed; @@ -253,7 +253,7 @@ const Tree = struct { fn remove(self: Tree, name: []const u8) void { var buf: [max_path]u8 = undefined; - const target = std.fmt.bufPrintZ(&buf, "{s}/{s}", .{ self.root, name }) catch return; + const target = std.mem.printSentinel(&buf, "{s}/{s}", .{ self.root, name }, 0) catch return; _ = glib.remove(target.ptr); } }; diff --git a/src/review/git.zig b/src/review/git.zig index 3fa7100..04ae0c7 100644 --- a/src/review/git.zig +++ b/src/review/git.zig @@ -243,7 +243,7 @@ pub fn suggestedBase(path: []const u8, branch: []const u8, all: []const []const /// release-branch flow. A path check because it has to work before anything has /// been read out of the repository. fn isAndroidPath(path: []const u8) bool { - return std.ascii.indexOfIgnoreCase(path, "android") != null; + return std.ascii.findIgnoreCase(path, "android") != null; } // ------------------------------------------------------------------------- diff --git a/src/scope.zig b/src/scope.zig index ab291e1..2bc01d2 100644 --- a/src/scope.zig +++ b/src/scope.zig @@ -63,10 +63,10 @@ pub fn adopt(pid: std.c.pid_t) void { // which it does whenever the shell left a daemon behind — Gradle's does this // as a matter of course — and the pid is later reused. var name_buf: [96]u8 = undefined; - const name = std.fmt.bufPrintZ(&name_buf, "app-playpen-shell-{d}-{x:0>8}.scope", .{ + const name = std.mem.printSentinel(&name_buf, "app-playpen-shell-{d}-{x:0>8}.scope", .{ pid, glib.randomInt(), - }) catch return; + }, 0) catch return; // `CollectMode` because a scope in which something *was* OOM-killed ends up // "failed" when it finally empties, and would otherwise sit in diff --git a/src/script.zig b/src/script.zig index 2523d88..d3fa17a 100644 --- a/src/script.zig +++ b/src/script.zig @@ -65,7 +65,7 @@ pub fn run(alloc: std.mem.Allocator, command: []const u8, vars: []const Var) Err var argv0 = "/bin/sh".*; var argv1 = "-c".*; - const command_z = try alloc.dupeZ(u8, command); + const command_z = try alloc.dupeSentinel(u8, command, 0); defer alloc.free(command_z); var argv = [_:null]?[*:0]u8{ &argv0, &argv1, command_z.ptr }; @@ -185,7 +185,7 @@ fn environment(alloc: std.mem.Allocator, vars: []const Var) Error![*:null]?[*:0] const name = envName(alloc, v.name) catch continue; defer alloc.free(name); - const value = alloc.dupeZ(u8, v.value) catch continue; + const value = alloc.dupeSentinel(u8, v.value, 0) catch continue; defer alloc.free(value); // Takes ownership of `envp` and hands back the updated list. diff --git a/src/shortcuts.zig b/src/shortcuts.zig index db4d584..6f82512 100644 --- a/src/shortcuts.zig +++ b/src/shortcuts.zig @@ -38,6 +38,14 @@ const vt = @import("ghostty-vt"); /// the letter keys are `key_a` and not `a`. pub const Key = vt.input.Key; +/// Every key by its enum name, for `keyFromName`. Built once here rather than +/// by `std.meta.stringToEnum` at the call: libghostty's key enum is large enough +/// that building the lookup overruns the default comptime branch quota. +const key_names = names: { + @setEvalBranchQuota(100_000_000); + break :names std.StaticStringMap(Key).initEnum(); +}; + /// The modifiers a shortcut can ask for. /// /// Deliberately not libghostty's `KeyMods`, which also carries the lock keys @@ -371,7 +379,7 @@ fn keyFromName(name: []const u8) ?Key { break :tag alias(lower); }; - const key = std.meta.stringToEnum(Key, tag) orelse return null; + const key = key_names.get(tag) orelse return null; // The enum's own name for "we could not tell", which is not something to // bind to and not something `key.zig` ever hands back.