diff --git a/assets/icons/README.md b/assets/icons/README.md
new file mode 100644
index 0000000..e579e53
--- /dev/null
+++ b/assets/icons/README.md
@@ -0,0 +1,15 @@
+# Bundled icons
+
+Symbolic icons copied from [adwaita-icon-theme](https://gitlab.gnome.org/GNOME/adwaita-icon-theme)
+50.0 (the version pinned by `flake.nix`), licensed under CC BY-SA 3.0 or
+LGPL v3, at the theme's option.
+
+They are compiled into the binary (see `icons.gresource.xml`, `build.zig`,
+and `src/icons.zig`) because the app refers to them by freedesktop name, and
+recent icon-theme releases dropped many of these legacy names — whether a
+host machine still resolves them is luck. GNOME's own guidance is that apps
+ship the icons they use.
+
+Everything lives under `scalable/actions/` regardless of the icon's original
+category: GTK's resource lookup goes by name, not category, and one flat
+directory keeps the manifest honest.
diff --git a/assets/icons/icons.gresource.xml b/assets/icons/icons.gresource.xml
new file mode 100644
index 0000000..f54bcf4
--- /dev/null
+++ b/assets/icons/icons.gresource.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ scalable/actions/document-edit-symbolic.svg
+ scalable/actions/edit-undo-symbolic.svg
+ scalable/actions/emblem-system-symbolic.svg
+ scalable/actions/go-down-symbolic.svg
+ scalable/actions/go-next-symbolic.svg
+ scalable/actions/go-previous-symbolic.svg
+ scalable/actions/go-up-symbolic.svg
+ scalable/actions/list-add-symbolic.svg
+ scalable/actions/list-remove-symbolic.svg
+ scalable/actions/media-record-symbolic.svg
+ scalable/actions/process-stop-symbolic.svg
+ scalable/actions/tab-new-symbolic.svg
+ scalable/actions/user-trash-symbolic.svg
+ scalable/actions/utilities-terminal-symbolic.svg
+ scalable/actions/view-fullscreen-symbolic.svg
+ scalable/actions/view-grid-symbolic.svg
+ scalable/actions/view-refresh-symbolic.svg
+ scalable/actions/view-restore-symbolic.svg
+ scalable/actions/web-browser-symbolic.svg
+ scalable/actions/window-close-symbolic.svg
+
+
diff --git a/assets/icons/scalable/actions/document-edit-symbolic.svg b/assets/icons/scalable/actions/document-edit-symbolic.svg
new file mode 100644
index 0000000..d5d54f6
--- /dev/null
+++ b/assets/icons/scalable/actions/document-edit-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/edit-undo-symbolic.svg b/assets/icons/scalable/actions/edit-undo-symbolic.svg
new file mode 100644
index 0000000..bd8f05c
--- /dev/null
+++ b/assets/icons/scalable/actions/edit-undo-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/emblem-system-symbolic.svg b/assets/icons/scalable/actions/emblem-system-symbolic.svg
new file mode 100644
index 0000000..c01314d
--- /dev/null
+++ b/assets/icons/scalable/actions/emblem-system-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/go-down-symbolic.svg b/assets/icons/scalable/actions/go-down-symbolic.svg
new file mode 100644
index 0000000..0159fbd
--- /dev/null
+++ b/assets/icons/scalable/actions/go-down-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/go-next-symbolic.svg b/assets/icons/scalable/actions/go-next-symbolic.svg
new file mode 100644
index 0000000..0ff48be
--- /dev/null
+++ b/assets/icons/scalable/actions/go-next-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/go-previous-symbolic.svg b/assets/icons/scalable/actions/go-previous-symbolic.svg
new file mode 100644
index 0000000..1b7901f
--- /dev/null
+++ b/assets/icons/scalable/actions/go-previous-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/go-up-symbolic.svg b/assets/icons/scalable/actions/go-up-symbolic.svg
new file mode 100644
index 0000000..a32393d
--- /dev/null
+++ b/assets/icons/scalable/actions/go-up-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/list-add-symbolic.svg b/assets/icons/scalable/actions/list-add-symbolic.svg
new file mode 100644
index 0000000..cf68622
--- /dev/null
+++ b/assets/icons/scalable/actions/list-add-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/list-remove-symbolic.svg b/assets/icons/scalable/actions/list-remove-symbolic.svg
new file mode 100644
index 0000000..b20ba0b
--- /dev/null
+++ b/assets/icons/scalable/actions/list-remove-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/media-record-symbolic.svg b/assets/icons/scalable/actions/media-record-symbolic.svg
new file mode 100644
index 0000000..a2fbb44
--- /dev/null
+++ b/assets/icons/scalable/actions/media-record-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/process-stop-symbolic.svg b/assets/icons/scalable/actions/process-stop-symbolic.svg
new file mode 100644
index 0000000..19b9537
--- /dev/null
+++ b/assets/icons/scalable/actions/process-stop-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/tab-new-symbolic.svg b/assets/icons/scalable/actions/tab-new-symbolic.svg
new file mode 100644
index 0000000..38e455b
--- /dev/null
+++ b/assets/icons/scalable/actions/tab-new-symbolic.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/assets/icons/scalable/actions/user-trash-symbolic.svg b/assets/icons/scalable/actions/user-trash-symbolic.svg
new file mode 100644
index 0000000..2e20f9c
--- /dev/null
+++ b/assets/icons/scalable/actions/user-trash-symbolic.svg
@@ -0,0 +1,8 @@
+
+
diff --git a/assets/icons/scalable/actions/utilities-terminal-symbolic.svg b/assets/icons/scalable/actions/utilities-terminal-symbolic.svg
new file mode 100644
index 0000000..eba46d6
--- /dev/null
+++ b/assets/icons/scalable/actions/utilities-terminal-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/scalable/actions/view-fullscreen-symbolic.svg b/assets/icons/scalable/actions/view-fullscreen-symbolic.svg
new file mode 100644
index 0000000..e65d6d6
--- /dev/null
+++ b/assets/icons/scalable/actions/view-fullscreen-symbolic.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/assets/icons/scalable/actions/view-grid-symbolic.svg b/assets/icons/scalable/actions/view-grid-symbolic.svg
new file mode 100644
index 0000000..1ab7b14
--- /dev/null
+++ b/assets/icons/scalable/actions/view-grid-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/view-refresh-symbolic.svg b/assets/icons/scalable/actions/view-refresh-symbolic.svg
new file mode 100644
index 0000000..3248fe7
--- /dev/null
+++ b/assets/icons/scalable/actions/view-refresh-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/assets/icons/scalable/actions/view-restore-symbolic.svg b/assets/icons/scalable/actions/view-restore-symbolic.svg
new file mode 100644
index 0000000..3bd41d8
--- /dev/null
+++ b/assets/icons/scalable/actions/view-restore-symbolic.svg
@@ -0,0 +1,7 @@
+
+
diff --git a/assets/icons/scalable/actions/web-browser-symbolic.svg b/assets/icons/scalable/actions/web-browser-symbolic.svg
new file mode 100644
index 0000000..c808611
--- /dev/null
+++ b/assets/icons/scalable/actions/web-browser-symbolic.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/icons/scalable/actions/window-close-symbolic.svg b/assets/icons/scalable/actions/window-close-symbolic.svg
new file mode 100644
index 0000000..f68a861
--- /dev/null
+++ b/assets/icons/scalable/actions/window-close-symbolic.svg
@@ -0,0 +1,4 @@
+
+
diff --git a/build.zig b/build.zig
index 2dbfd64..17edd67 100644
--- a/build.zig
+++ b/build.zig
@@ -32,6 +32,21 @@ pub fn build(b: *std.Build) void {
exe.root_module.addImport("ghostty-vt", ghostty.module("ghostty-vt"));
+ // The symbolic icons the app names are compiled into the binary, so that
+ // showing a terminal or web-browser glyph doesn't depend on which icon
+ // theme the host has installed. glib-compile-resources comes from the same
+ // Nix shell as GTK itself; the compiled bundle is embedded by src/icons.zig
+ // and registered at startup.
+ const gresource = b.addSystemCommand(&.{"glib-compile-resources"});
+ gresource.addArg("--sourcedir");
+ gresource.addDirectoryArg(b.path("assets/icons"));
+ gresource.addArg("--target");
+ const icons_gresource = gresource.addOutputFileArg("icons.gresource");
+ gresource.addFileArg(b.path("assets/icons/icons.gresource.xml"));
+ exe.root_module.addAnonymousImport("icons.gresource", .{
+ .root_source_file = icons_gresource,
+ });
+
const gobject_imports = .{
.{ "adw", "adw1" },
.{ "cairo", "cairo1" },
diff --git a/src/icons.zig b/src/icons.zig
new file mode 100644
index 0000000..75740d4
--- /dev/null
+++ b/src/icons.zig
@@ -0,0 +1,51 @@
+//! The symbolic icons the app names, carried inside the binary.
+//!
+//! Widgets here ask for icons by freedesktop name — `utilities-terminal-
+//! symbolic`, `web-browser-symbolic`, and so on — and by default GTK resolves
+//! those against whatever icon theme the host has installed. Recent
+//! adwaita-icon-theme releases dropped many of these legacy names, so whether
+//! a given machine still shows them is luck. This module removes the luck:
+//! the icons are compiled into the binary as a GResource (see
+//! `assets/icons/icons.gresource.xml` and the glib-compile-resources step in
+//! `build.zig`) and registered with the icon theme at startup.
+//!
+//! Registration is deliberately as a *fallback*: `addResourcePath` folds the
+//! bundle into hicolor, the theme of last resort, so a host theme that does
+//! provide a name keeps winning and the app stays native-looking. The bundle
+//! only answers for names the host can't.
+
+const std = @import("std");
+const gdk = @import("gdk");
+const gio = @import("gio");
+const glib = @import("glib");
+const gtk = @import("gtk");
+
+const data = @embedFile("icons.gresource");
+
+/// Register the bundled icons. Called once at activation, before the first
+/// window is built, so nothing is ever looked up ahead of it.
+///
+/// Failure is logged and swallowed: a missing icon renders as the
+/// broken-image glyph, which is exactly the state this module exists to
+/// avoid, but not a reason to refuse to open a window.
+pub fn init() void {
+ var err: ?*glib.Error = null;
+
+ // newFromData copies if the embedded bytes aren't suitably aligned, so
+ // static is safe here regardless of where @embedFile landed them.
+ const bytes = glib.Bytes.newStatic(data, data.len);
+ defer bytes.unref();
+
+ const resource = gio.Resource.newFromData(bytes, &err) orelse {
+ if (err) |e| {
+ std.log.warn("failed to load bundled icons: {s}", .{e.f_message orelse "unknown"});
+ e.free();
+ }
+ return;
+ };
+ gio.resourcesRegister(resource);
+
+ if (gdk.Display.getDefault()) |display| {
+ gtk.IconTheme.getForDisplay(display).addResourcePath("/dev/greyson/playpen/icons");
+ }
+}
diff --git a/src/main.zig b/src/main.zig
index ae37714..2373a9d 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -12,6 +12,7 @@ const gio = @import("gio");
const Settings = @import("Settings.zig");
const Window = @import("Window.zig");
const appearance = @import("appearance.zig");
+const icons = @import("icons.zig");
/// libghostty-vt logs unimplemented sequences at debug level, which is very
/// chatty against a real shell. Keep the app's own warnings and errors.
@@ -50,6 +51,10 @@ fn onActivate(app: *adw.Application, _: ?*anyopaque) callconv(.c) void {
// user chose rather than repainted into it a moment later.
appearance.init();
+ // Same reasoning: the bundled icons are registered before anything asks
+ // for one by name.
+ icons.init();
+
const window = Window.create(gpa.allocator(), app) catch |err| {
std.log.err("failed to create window: {s}", .{@errorName(err)});
return;