Various changes.

This commit is contained in:
Greyson Parrelli committed 2026-10-28 09:22:29 -04:00
1 parent bb90255002
commit 3475ca95c0
29 files changed
+110 -85

No files matched your search

+1 -1
View File
@@ -17,7 +17,7 @@ with the content inset to its right.
## Quick start ## Quick start
```sh ```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 mise run web-deps # once: npm deps for the review pane's UI
zig build run zig build run
``` ```
+2 -2
View File
@@ -110,7 +110,7 @@ pub fn build(b: *std.Build) void {
const run_cmd = b.addRunArtifact(exe); const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep()); 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); b.step("run", "Run the app").dependOn(&run_cmd.step);
// Tests are rooted at Layouts.zig rather than main.zig: the parts of this // 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; const io = b.graph.io;
for ([_][]const u8{ "web/src", "web/public" }) |root| { 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); defer dir.close(io);
var walker = dir.walk(b.allocator) catch continue; var walker = dir.walk(b.allocator) catch continue;
+5 -5
View File
@@ -4,7 +4,7 @@
// Half package-name hash, half per-package constant, so renaming the // Half package-name hash, half per-package constant, so renaming the
// package invalidates it. Regenerated when vtabs became playpen. // package invalidates it. Regenerated when vtabs became playpen.
.fingerprint = 0x5a26ca72bb4d2c53, .fingerprint = 0x5a26ca72bb4d2c53,
.minimum_zig_version = "0.16.0", .minimum_zig_version = "0.17.0",
.paths = .{ .paths = .{
"build.zig", "build.zig",
"build.zig.zon", "build.zig.zon",
@@ -12,12 +12,12 @@
}, },
.dependencies = .{ .dependencies = .{
.ghostty = .{ .ghostty = .{
.url = "https://github.com/ghostty-org/ghostty/archive/94d775fefc21f74d9cc85a46b34c4e1d85318fd0.tar.gz", .url = "https://github.com/ghostty-org/ghostty/archive/83fb95624b0439ee660c2679b43b38c8b63769cd.tar.gz",
.hash = "ghostty-1.3.2-dev-5UdBCyCSPwV-Pp82UMGLutqeo0gckMrI0GfbUbUY0-Jt", .hash = "ghostty-1.4.0-5UdBC-qohwWqLS0BImOz05t4hVyTwbaKslyTPvKYvAtE",
}, },
.gobject = .{ .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", .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-Skun7F6HogCMynX2JqeSHS7xr-8pK4ob-qRFIcEasVi3", .hash = "gobject-0.3.2-Skun7LuIogBbEh-6WrLeEzMaRCnOMB3eOf__RaPYSMCZ",
}, },
}, },
} }
Generated
+3 -3
View File
@@ -66,11 +66,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1786364596, "lastModified": 1790988737,
"narHash": "sha256-rMHN2/qJ10ORy+PfSm8hfG2mCdnsYHsWrKeq8DLraec=", "narHash": "sha256-b55etI4Uxu+WHhYZXym3HPIz63ipO9lqwJLAphjqtTc=",
"owner": "mitchellh", "owner": "mitchellh",
"repo": "zig-overlay", "repo": "zig-overlay",
"rev": "840c2310085417892ec51f691002ca0a497d0287", "rev": "dcffc7a25ed2cbbc7c1c3c448c312abf46ae6cb8",
"type": "github" "type": "github"
}, },
"original": { "original": {
+7 -2
View File
@@ -5,6 +5,11 @@
# Ghostty tracks nixpkgs-unstable to get GTK 4.20 / GNOME 49. We follow # 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 # suit so that our zig-gobject bindings (shared with Ghostty) match the
# GObject introspection data of the GTK we link against. # 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"; nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
flake-compat = { flake-compat = {
@@ -17,7 +22,7 @@
flake = false; 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 = { zig = {
url = "github:mitchellh/zig-overlay"; url = "github:mitchellh/zig-overlay";
inputs = { inputs = {
@@ -45,7 +50,7 @@
name = "playpen"; name = "playpen";
nativeBuildInputs = [ nativeBuildInputs = [
zig.packages.${system}."0.16.0" zig.packages.${system}."0.17.0"
pkgs.pkg-config pkgs.pkg-config
pkgs.gdb pkgs.gdb
+1 -1
View File
@@ -11,7 +11,7 @@ run = "nix develop --command npm --prefix web run dev"
[tasks.build] [tasks.build]
description = "Build a release binary into zig-out/bin" 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] [tasks.build-debug]
description = "Build a debug binary (leak checking, safety checks)" description = "Build a debug binary (leak checking, safety checks)"
+1 -1
View File
@@ -264,7 +264,7 @@ fn resolve(self: *Browser, raw: []const u8) ?[:0]const u8 {
return copyZ(&self.url_buf, input); return copyZ(&self.url_buf, input);
} }
if (looksLikeHost(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); return searchUrl(&self.url_buf, input);
} }
+3 -3
View File
@@ -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. // it gets a "or more" rather than a number that isn't the total.
var buf: [32]u8 = undefined; var buf: [32]u8 = undefined;
const label = if (matches >= max_matches) 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) else if (matches == 1)
std.fmt.bufPrintZ(&buf, "1 match", .{}) catch return std.mem.printSentinel(&buf, "1 match", .{}, 0) catch return
else else
std.fmt.bufPrintZ(&buf, "{d} matches", .{matches}) catch return; std.mem.printSentinel(&buf, "{d} matches", .{matches}, 0) catch return;
self.status.setText(label); self.status.setText(label);
} }
+2 -2
View File
@@ -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. /// when that isn't set — whichever GLib reports as the user's config dir.
pub fn configPath(buf: []u8) ?[:0]const u8 { pub fn configPath(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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. /// The directory `configPath` lives in.
fn configDir(buf: []u8) ?[:0]const u8 { fn configDir(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+4 -4
View File
@@ -61,7 +61,7 @@ pub fn present(
}; };
var title_buf: [128]u8 = undefined; 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.setTitle(title);
self.window.setTransientFor(parent); self.window.setTransientFor(parent);
self.window.setModal(1); self.window.setModal(1);
@@ -81,14 +81,14 @@ pub fn present(
// is the fallback so a parameter is never unlabeled. // is the fallback so a parameter is never unlabeled.
var label_buf: [128]u8 = undefined; var label_buf: [128]u8 = undefined;
const text = if (param.description.len > 0) param.description else param.name; 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.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-dialog-label"); label.as(gtk.Widget).addCssClass("playpen-dialog-label");
fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1); fields.attach(label.as(gtk.Widget), 0, @intCast(i), 1, 1);
var hint_buf: [128]u8 = undefined; var hint_buf: [128]u8 = undefined;
const placeholder: ?[*:0]const u8 = 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) { const entry, const field = switch (param.type) {
.string => blk: { .string => blk: {
@@ -150,7 +150,7 @@ pub fn present(
fn setEntryText(entry: *gtk.Entry, text: []const u8) void { fn setEntryText(entry: *gtk.Entry, text: []const u8) void {
var buf: [1024]u8 = undefined; 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); entry.as(gtk.Editable).setText(z);
} }
+4 -3
View File
@@ -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 // the settings file, since anyone hand-editing that file is looking at this
// list to find out what to call things. // list to find out what to call things.
var tip: [256]u8 = undefined; 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 "", key.hint() orelse "",
if (key.hint() != null) "\n" else "", if (key.hint() != null) "\n" else "",
@tagName(key), @tagName(key),
}) catch @tagName(key); }, 0) catch @tagName(key);
row.as(gtk.Widget).setTooltipText(tooltip); row.as(gtk.Widget).setTooltipText(tooltip);
const hex = gtk.Label.new(null); const hex = gtk.Label.new(null);
@@ -497,11 +497,12 @@ fn reload(self: *PaletteEditor) void {
const changes = Settings.get().colors.of(scheme); const changes = Settings.get().colors.of(scheme);
var buf: [192]u8 = undefined; var buf: [192]u8 = undefined;
const heading: [:0]const u8 = std.fmt.bufPrintZ( const heading: [:0]const u8 = std.mem.printSentinel(
&buf, &buf,
"Editing the {s} palette, the one on screen — changes apply as you make them. " ++ "Editing the {s} palette, the one on screen — changes apply as you make them. " ++
"Switch the theme above to edit the other.", "Switch the theme above to edit the other.",
.{scheme.label()}, .{scheme.label()},
0,
) catch "Changes apply as you make them."; ) catch "Changes apply as you make them.";
self.scheme_hint.setText(heading); self.scheme_hint.setText(heading);
+7 -7
View File
@@ -193,7 +193,7 @@ pub fn setText(self: *PathEntry, value: []const u8) void {
defer self.quiet = false; defer self.quiet = false;
var buf: [max_path]u8 = undefined; 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.entry.as(gtk.Editable).setText(z);
self.hide(); self.hide();
} }
@@ -243,7 +243,7 @@ fn fill(self: *PathEntry) void {
for (offer.names) |name| { for (offer.names) |name| {
var buf: [max_path]u8 = undefined; 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.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-path-row"); label.as(gtk.Widget).addCssClass("playpen-path-row");
self.list.append(label.as(gtk.Widget)); self.list.append(label.as(gtk.Widget));
@@ -253,7 +253,7 @@ fn fill(self: *PathEntry) void {
if (hidden == 0) return; if (hidden == 0) return;
var buf: [64]u8 = undefined; 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); const label = gtk.Label.new(counted);
label.setXalign(0); label.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-path-more"); 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(); if (offer.dir_len > value.len) return self.hide();
var buf: [max_path]u8 = undefined; 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], value[0..offer.dir_len],
offer.names[index], offer.names[index],
}) catch return; }, 0) catch return;
// Not quiet: writing this re-reads the directory it names, so completing // Not quiet: writing this re-reads the directory it names, so completing
// `~/src/pl` to `~/src/playpen/` immediately offers what is inside it. // `~/src/pl` to `~/src/playpen/` immediately offers what is inside it.
@@ -338,10 +338,10 @@ fn complete(self: *PathEntry) void {
if (shared <= typed) return; if (shared <= typed) return;
var buf: [max_path]u8 = undefined; 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], value[0..offer.dir_len],
offer.names[0][0..shared], offer.names[0][0..shared],
}) catch return; }, 0) catch return;
self.entry.as(gtk.Editable).setText(z); self.entry.as(gtk.Editable).setText(z);
self.entry.as(gtk.Editable).setPosition(-1); self.entry.as(gtk.Editable).setPosition(-1);
+6 -6
View File
@@ -126,7 +126,7 @@ pub fn create(
const envp_z = try buildEnv(alloc, env_extra); const envp_z = try buildEnv(alloc, env_extra);
defer freeEnv(alloc, envp_z); 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); defer alloc.free(slave_path_z);
const pid = c.fork(); const pid = c.fork();
@@ -226,12 +226,12 @@ fn buildEnv(
if (std.mem.startsWith(u8, span, "TERM=")) continue; if (std.mem.startsWith(u8, span, "TERM=")) continue;
if (std.mem.startsWith(u8, span, "COLORTERM=")) continue; if (std.mem.startsWith(u8, span, "COLORTERM=")) continue;
if (shadowedBy(span, extra)) 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.dupeSentinel(u8, "TERM=xterm-256color", 0)).ptr);
try list.append(alloc, (try alloc.dupeZ(u8, "COLORTERM=truecolor")).ptr); try list.append(alloc, (try alloc.dupeSentinel(u8, "COLORTERM=truecolor", 0)).ptr);
for (extra) |entry| { 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); 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. /// portable API for it, and this app is Linux-only regardless.
pub fn cwd(self: Pty, buf: []u8) ?[]const u8 { pub fn cwd(self: Pty, buf: []u8) ?[]const u8 {
var link_buf: [64]u8 = undefined; 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); const n = c.readlink(link.ptr, buf.ptr, buf.len);
if (n <= 0) return null; if (n <= 0) return null;
+1 -1
View File
@@ -98,7 +98,7 @@ pub fn create(
const self = try alloc.create(Review); const self = try alloc.create(Review);
errdefer alloc.destroy(self); 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); errdefer alloc.free(url);
self.* = .{ self.* = .{
+3 -3
View File
@@ -233,10 +233,10 @@ fn buildPaneSection(self: *SaveLayoutDialog, index: usize, leaf: *Layouts.Node)
box.as(gtk.Widget).addCssClass("playpen-dialog-pane"); box.as(gtk.Widget).addCssClass("playpen-dialog-pane");
var title_buf: [64]u8 = undefined; 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, index + 1,
@tagName(spec.kind), @tagName(spec.kind),
}) catch "Pane"; }, 0) catch "Pane";
const label = gtk.Label.new(title); const label = gtk.Label.new(title);
label.setXalign(0); label.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-dialog-label"); 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 { fn setEntryText(entry: *gtk.Entry, text: []const u8) void {
var buf: [4096]u8 = undefined; 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); entry.as(gtk.Editable).setText(z);
} }
+14 -6
View File
@@ -185,7 +185,7 @@ pub fn create(
defer alloc.free(argv0); defer alloc.free(argv0);
const cwd_z: ?[:0]const u8 = if (opts.cwd.len > 0) 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 else
null; null;
defer if (cwd_z) |z| alloc.free(z); 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. /// for systems with no usable passwd entry, such as minimal containers.
fn defaultShell(alloc: std.mem.Allocator) ![:0]const u8 { fn defaultShell(alloc: std.mem.Allocator) ![:0]const u8 {
var buf: [std.fs.max_path_bytes]u8 = undefined; 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| { if (std.c.getenv("SHELL")) |sh| {
const span = std.mem.span(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). /// 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. /// 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); const self = fromHandler(handler);
self.pty.writeAll(data); 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 /// 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 /// OSC 4 survives the switch. The caller still has to queue a redraw — nothing
/// here touches the widget. /// 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 { 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. /// Send user input to the child.
+2 -2
View File
@@ -740,12 +740,12 @@ fn serialize(self: *Settings) SaveError![]u8 {
/// `~/.config/playpen/settings.json` when that isn't set. /// `~/.config/playpen/settings.json` when that isn't set.
fn configPath(buf: []u8) ?[:0]const u8 { fn configPath(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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 { fn configDir(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserConfigDir()); 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;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+6 -6
View File
@@ -648,7 +648,7 @@ fn newLayoutPicker(self: *SettingsDialog, selected: []const u8) *gtk.DropDown {
for (self.layouts.items.items) |layout| { for (self.layouts.items.items) |layout| {
var buf: [256]u8 = undefined; 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); model.append(name.ptr);
if (std.mem.eql(u8, layout.name, selected)) index = count; if (std.mem.eql(u8, layout.name, selected)) index = count;
count += 1; count += 1;
@@ -656,7 +656,7 @@ fn newLayoutPicker(self: *SettingsDialog, selected: []const u8) *gtk.DropDown {
if (selected.len > 0 and index == 0) { if (selected.len > 0 and index == 0) {
var buf: [256]u8 = undefined; 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); model.append(name.ptr);
index = count; index = count;
} else |_| {} } else |_| {}
@@ -712,7 +712,7 @@ fn fillParams(
var hint_buf: [128]u8 = undefined; var hint_buf: [128]u8 = undefined;
const placeholder: ?[*:0]const u8 = 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; const value = valueOf(values, param.name) orelse param.default;
// The same two shapes the layout prompt offers, for the same reason: a // 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); errdefer self.alloc.free(field.name);
var label_buf: [128]u8 = undefined; 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, if (param.description.len > 0) param.description else param.name,
}) catch "parameter"; }, 0) catch "parameter";
const label = gtk.Label.new(text); const label = gtk.Label.new(text);
label.setXalign(0); label.setXalign(0);
label.as(gtk.Widget).addCssClass("playpen-dialog-sublabel"); 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 { fn setEntryText(entry: *gtk.Entry, text: []const u8) void {
var buf: [4096]u8 = undefined; 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); entry.as(gtk.Editable).setText(z);
} }
+2 -2
View File
@@ -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. /// when that isn't set — whichever GLib reports as the user's state dir.
fn statePath(buf: []u8) ?[:0]const u8 { fn statePath(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserStateDir()); 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 { fn stateDir(buf: []u8) ?[:0]const u8 {
const dir = std.mem.span(glib.getUserStateDir()); 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;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+2 -2
View File
@@ -116,9 +116,9 @@ pub fn present(
}; };
var title_buf: [160]u8 = undefined; 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), clip(opts.tab_name, 96),
}) catch "Tab settings"; }, 0) catch "Tab settings";
self.window.setTitle(title); self.window.setTitle(title);
self.window.setTransientFor(parent); self.window.setTransientFor(parent);
+6 -5
View File
@@ -577,7 +577,7 @@ fn newTabEmpty(self: *Window) !*Tab {
.name = undefined, .name = undefined,
.name_len = 0, .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; tab.name_len = printed.len;
// ---- sidebar row --------------------------------------------------- // ---- sidebar row ---------------------------------------------------
@@ -960,7 +960,7 @@ fn refreshLayoutMenu(self: *Window) void {
const line = gtk.Box.new(.horizontal, 4); const line = gtk.Box.new(.horizontal, 4);
var label_buf: [128]u8 = undefined; 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); const open = gtk.Button.newWithLabel(text);
open.as(gtk.Widget).addCssClass("flat"); open.as(gtk.Widget).addCssClass("flat");
@@ -1491,10 +1491,10 @@ fn offerRestore(self: *Window) void {
// is, just without the number. // is, just without the number.
const count = self.snapshot.tabs.items.len; const count = self.snapshot.tabs.items.len;
var buf: [64]u8 = undefined; 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, count,
if (count == 1) "" else "s", if (count == 1) "" else "s",
}) catch "from last time"; }, 0) catch "from last time";
self.restore_detail.setText(detail.ptr); self.restore_detail.setText(detail.ptr);
self.restore_offered = true; 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 // 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. // says the same thing without the number rather than nothing at all.
var buf: [160]u8 = undefined; var buf: [160]u8 = undefined;
const body: [:0]const u8 = std.fmt.bufPrintZ( const body: [:0]const u8 = std.mem.printSentinel(
&buf, &buf,
"{d} tab{s} will close, and every shell in {s} will exit.", "{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) "" else "s",
if (self.tabs.items.len == 1) "it" else "them", if (self.tabs.items.len == 1) "it" else "them",
}, },
0,
) catch "Every tab will close, and every shell in them will exit."; ) catch "Every tab will close, and every shell in them will exit.";
dialog.setBody(body.ptr); dialog.setBody(body.ptr);
+1 -1
View File
@@ -44,7 +44,7 @@ pub const Emoji = struct {
pub fn matches(emoji: Emoji, query: []const u8) bool { pub fn matches(emoji: Emoji, query: []const u8) bool {
var terms = std.mem.tokenizeAny(u8, query, " \t"); var terms = std.mem.tokenizeAny(u8, query, " \t");
while (terms.next()) |term| { 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; return true;
} }
+2 -2
View File
@@ -37,11 +37,11 @@ pub const std_options: std.Options = .{
var debug_gpa: std.heap.DebugAllocator(.{}) = .init; var debug_gpa: std.heap.DebugAllocator(.{}) = .init;
fn gpa() std.mem.Allocator { 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 { pub fn main() u8 {
defer if (builtin.mode == .Debug) { defer if (builtin.mode == .debug) {
_ = debug_gpa.deinit(); _ = debug_gpa.deinit();
}; };
+4 -2
View File
@@ -100,20 +100,22 @@ pub const Mute = union(enum) {
.until => |deadline| blk: { .until => |deadline| blk: {
const left_min = @divTrunc(deadline - now + 59_999, std.time.ms_per_min); 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 <= 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, buf,
"Muted for another {d} minutes", "Muted for another {d} minutes",
.{left_min}, .{left_min},
0,
) catch "Muted"; ) catch "Muted";
// Rounded down and in hours past the first: someone who muted a // 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 // tab for eight hours wants to know it is still most of the
// afternoon, not that it is 437 minutes. // afternoon, not that it is 437 minutes.
const left_hours = @divTrunc(left_min, 60); const left_hours = @divTrunc(left_min, 60);
break :blk std.fmt.bufPrintZ( break :blk std.mem.printSentinel(
buf, buf,
"Muted for another {d} hours", "Muted for another {d} hours",
.{left_hours}, .{left_hours},
0,
) catch "Muted"; ) catch "Muted";
}, },
}; };
+7 -7
View File
@@ -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] == '/')) { if (dir[0] == '~' and (dir.len == 1 or dir[1] == '/')) {
const home = std.mem.span(glib.getHomeDir()); const home = std.mem.span(glib.getHomeDir());
if (home.len == 0) return null; 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 /// 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 { fn isDirectory(dir: [:0]const u8, name: []const u8) bool {
var buf: [max_path]u8 = undefined; var buf: [max_path]u8 = undefined;
const separator: []const u8 = if (std.mem.endsWith(u8, dir, "/")) "" else "/"; 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 false;
return glib.fileTest(path.ptr, .{ .is_dir = true }) != 0; return glib.fileTest(path.ptr, .{ .is_dir = true }) != 0;
} }
@@ -214,17 +214,17 @@ const Tree = struct {
}; };
defer glib.free(raw); 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| { for (names) |name| {
var buf: [max_path]u8 = undefined; 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; if (glib.mkdirWithParents(dir.ptr, 0o700) != 0) return error.MkdirFailed;
} }
for (files) |name| { for (files) |name| {
var buf: [max_path]u8 = undefined; 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 (glib.fileSetContents(file.ptr, "x", 1, &err) == 0) {
if (err) |e| e.free(); if (err) |e| e.free();
return error.WriteFailed; return error.WriteFailed;
@@ -253,7 +253,7 @@ const Tree = struct {
fn remove(self: Tree, name: []const u8) void { fn remove(self: Tree, name: []const u8) void {
var buf: [max_path]u8 = undefined; 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); _ = glib.remove(target.ptr);
} }
}; };
+1 -1
View File
@@ -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 /// release-branch flow. A path check because it has to work before anything has
/// been read out of the repository. /// been read out of the repository.
fn isAndroidPath(path: []const u8) bool { fn isAndroidPath(path: []const u8) bool {
return std.ascii.indexOfIgnoreCase(path, "android") != null; return std.ascii.findIgnoreCase(path, "android") != null;
} }
// ------------------------------------------------------------------------- // -------------------------------------------------------------------------
+2 -2
View File
@@ -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 // 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. // as a matter of course — and the pid is later reused.
var name_buf: [96]u8 = undefined; 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, pid,
glib.randomInt(), glib.randomInt(),
}) catch return; }, 0) catch return;
// `CollectMode` because a scope in which something *was* OOM-killed ends up // `CollectMode` because a scope in which something *was* OOM-killed ends up
// "failed" when it finally empties, and would otherwise sit in // "failed" when it finally empties, and would otherwise sit in
+2 -2
View File
@@ -65,7 +65,7 @@ pub fn run(alloc: std.mem.Allocator, command: []const u8, vars: []const Var) Err
var argv0 = "/bin/sh".*; var argv0 = "/bin/sh".*;
var argv1 = "-c".*; 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); defer alloc.free(command_z);
var argv = [_:null]?[*:0]u8{ &argv0, &argv1, command_z.ptr }; 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; const name = envName(alloc, v.name) catch continue;
defer alloc.free(name); 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); defer alloc.free(value);
// Takes ownership of `envp` and hands back the updated list. // Takes ownership of `envp` and hands back the updated list.
+9 -1
View File
@@ -38,6 +38,14 @@ const vt = @import("ghostty-vt");
/// the letter keys are `key_a` and not `a`. /// the letter keys are `key_a` and not `a`.
pub const Key = vt.input.Key; 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. /// The modifiers a shortcut can ask for.
/// ///
/// Deliberately not libghostty's `KeyMods`, which also carries the lock keys /// 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); 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 // 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. // bind to and not something `key.zig` ever hands back.