mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +03:00
16
src/App.zig
16
src/App.zig
@ -16,6 +16,7 @@ const Config = @import("config.zig").Config;
|
||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||
const renderer = @import("renderer.zig");
|
||||
const font = @import("font/main.zig");
|
||||
const internal_os = @import("os/main.zig");
|
||||
const macos = @import("macos");
|
||||
const objc = @import("objc");
|
||||
|
||||
@ -40,6 +41,10 @@ mailbox: Mailbox.Queue,
|
||||
/// Set to true once we're quitting. This never goes false again.
|
||||
quit: bool,
|
||||
|
||||
/// The app resources directory, equivalent to zig-out/share when we build
|
||||
/// from source. This is null if we can't detect it.
|
||||
resources_dir: ?[]const u8 = null,
|
||||
|
||||
/// Initialize the main app instance. This creates the main window, sets
|
||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||
/// "startup" logic.
|
||||
@ -48,11 +53,21 @@ pub fn create(
|
||||
) !*App {
|
||||
var app = try alloc.create(App);
|
||||
errdefer alloc.destroy(app);
|
||||
|
||||
// Find our resources directory once for the app so every launch
|
||||
// hereafter can use this cached value.
|
||||
var resources_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const resources_dir = if (try internal_os.resourcesDir(&resources_buf)) |dir|
|
||||
try alloc.dupe(u8, dir)
|
||||
else
|
||||
null;
|
||||
|
||||
app.* = .{
|
||||
.alloc = alloc,
|
||||
.surfaces = .{},
|
||||
.mailbox = .{},
|
||||
.quit = false,
|
||||
.resources_dir = resources_dir,
|
||||
};
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
@ -64,6 +79,7 @@ pub fn destroy(self: *App) void {
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
|
||||
if (self.resources_dir) |dir| self.alloc.free(dir);
|
||||
self.alloc.destroy(self);
|
||||
}
|
||||
|
||||
|
@ -182,6 +182,7 @@ pub fn init(
|
||||
alloc: Allocator,
|
||||
config: *const configpkg.Config,
|
||||
app_mailbox: App.Mailbox,
|
||||
app_resources_dir: ?[]const u8,
|
||||
rt_surface: *apprt.runtime.Surface,
|
||||
) !void {
|
||||
// Initialize our renderer with our initialized surface.
|
||||
@ -405,6 +406,7 @@ pub fn init(
|
||||
.screen_size = screen_size,
|
||||
.full_config = config,
|
||||
.config = try termio.Impl.DerivedConfig.init(alloc, config),
|
||||
.resources_dir = app_resources_dir,
|
||||
.renderer_state = &self.renderer_state,
|
||||
.renderer_wakeup = render_thread.wakeup,
|
||||
.renderer_mailbox = render_thread.mailbox,
|
||||
|
@ -177,6 +177,7 @@ pub const Surface = struct {
|
||||
app.core_app.alloc,
|
||||
&config,
|
||||
.{ .rt_app = app, .mailbox = &app.core_app.mailbox },
|
||||
app.core_app.resources_dir,
|
||||
self,
|
||||
);
|
||||
errdefer self.core_surface.deinit();
|
||||
|
@ -363,6 +363,7 @@ pub const Surface = struct {
|
||||
app.app.alloc,
|
||||
&config,
|
||||
.{ .rt_app = app, .mailbox = &app.app.mailbox },
|
||||
app.app.resources_dir,
|
||||
self,
|
||||
);
|
||||
errdefer self.core_surface.deinit();
|
||||
|
@ -298,6 +298,9 @@ const Window = struct {
|
||||
/// The background CSS for the window (if any).
|
||||
css_window_background: ?[]u8 = null,
|
||||
|
||||
/// The resources directory for the icon (if any).
|
||||
icon_search_dir: ?[:0]const u8 = null,
|
||||
|
||||
pub fn init(self: *Window, app: *App) !void {
|
||||
// Set up our own state
|
||||
self.* = .{
|
||||
@ -314,6 +317,31 @@ const Window = struct {
|
||||
c.gtk_window_set_title(gtk_window, "Ghostty");
|
||||
c.gtk_window_set_default_size(gtk_window, 1000, 600);
|
||||
|
||||
// If we don't have the icon then we'll try to add our resources dir
|
||||
// to the search path and see if we can find it there.
|
||||
const icon_name = "com.mitchellh.ghostty";
|
||||
const icon_theme = c.gtk_icon_theme_get_for_display(c.gtk_widget_get_display(window));
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) icon: {
|
||||
const base = self.app.core_app.resources_dir orelse {
|
||||
log.info("gtk app missing Ghostty icon and no resources dir detected", .{});
|
||||
log.info("gtk app will not have Ghostty icon", .{});
|
||||
break :icon;
|
||||
};
|
||||
|
||||
// Note that this method for adding the icon search path is
|
||||
// a fallback mechanism. The recommended mechanism is the
|
||||
// Freedesktop Icon Theme Specification. We distribute a ".desktop"
|
||||
// file in zig-out/share that should be installed to the proper
|
||||
// place.
|
||||
const dir = try std.fmt.allocPrintZ(app.core_app.alloc, "{s}/icons", .{base});
|
||||
self.icon_search_dir = dir;
|
||||
c.gtk_icon_theme_add_search_path(icon_theme, dir.ptr);
|
||||
if (c.gtk_icon_theme_has_icon(icon_theme, icon_name) == 0) {
|
||||
log.warn("Ghostty icon for gtk app not found", .{});
|
||||
}
|
||||
}
|
||||
c.gtk_window_set_icon_name(gtk_window, icon_name);
|
||||
|
||||
// Apply background opacity if we have it
|
||||
if (app.config.@"background-opacity" < 1) {
|
||||
var css = try std.fmt.allocPrint(
|
||||
@ -361,6 +389,7 @@ const Window = struct {
|
||||
|
||||
pub fn deinit(self: *Window) void {
|
||||
if (self.css_window_background) |ptr| self.app.core_app.alloc.free(ptr);
|
||||
if (self.icon_search_dir) |ptr| self.app.core_app.alloc.free(ptr);
|
||||
}
|
||||
|
||||
/// Add a new tab to this window.
|
||||
@ -765,6 +794,7 @@ pub const Surface = struct {
|
||||
self.app.core_app.alloc,
|
||||
&config,
|
||||
.{ .rt_app = self.app, .mailbox = &self.app.core_app.mailbox },
|
||||
self.app.core_app.resources_dir,
|
||||
self,
|
||||
);
|
||||
errdefer self.core_surface.deinit();
|
||||
|
@ -7,6 +7,7 @@ pub usingnamespace @import("homedir.zig");
|
||||
pub usingnamespace @import("locale.zig");
|
||||
pub usingnamespace @import("macos_version.zig");
|
||||
pub usingnamespace @import("mouse.zig");
|
||||
pub usingnamespace @import("resourcesdir.zig");
|
||||
pub const TempDir = @import("TempDir.zig");
|
||||
pub const passwd = @import("passwd.zig");
|
||||
pub const xdg = @import("xdg.zig");
|
||||
|
77
src/os/resourcesdir.zig
Normal file
77
src/os/resourcesdir.zig
Normal file
@ -0,0 +1,77 @@
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// Gets the directory to the bundled resources directory, if it
|
||||
/// exists (not all platforms or packages have it). The output is
|
||||
/// written to the given buffer if we need to allocate. Note that
|
||||
/// the output is not ALWAYS written to the buffer and may refer to
|
||||
/// static memory.
|
||||
///
|
||||
/// This is highly Ghostty-specific and can likely be generalized at
|
||||
/// some point but we can cross that bridge if we ever need to.
|
||||
///
|
||||
/// This returns error.OutOfMemory is buffer is not big enough.
|
||||
pub fn resourcesDir(buf: []u8) !?[]const u8 {
|
||||
// If we have an environment variable set, we always use that.
|
||||
if (std.os.getenv("GHOSTTY_RESOURCES_DIR")) |dir| {
|
||||
if (dir.len > 0) {
|
||||
return dir;
|
||||
}
|
||||
}
|
||||
|
||||
// This is the sentinel value we look for in the path to know
|
||||
// we've found the resources directory.
|
||||
const sentinel = "terminfo/ghostty.termcap";
|
||||
|
||||
// Get the path to our running binary
|
||||
var exe_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
|
||||
|
||||
// We have an exe path! Climb the tree looking for the terminfo
|
||||
// bundle as we expect it.
|
||||
while (std.fs.path.dirname(exe)) |dir| {
|
||||
exe = dir;
|
||||
|
||||
// On MacOS, we look for the app bundle path.
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
if (try maybeDir(buf, dir, "Contents/Resources", sentinel)) |v| {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// On all platforms, we look for a /usr/share style path. This
|
||||
// is valid even on Mac since there is nothing that requires
|
||||
// Ghostty to be in an app bundle.
|
||||
if (try maybeDir(buf, dir, "share", sentinel)) |v| {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Little helper to check if the "base/sub/suffix" directory exists and
|
||||
/// if so return true. The "suffix" is just used as a way to verify a directory
|
||||
/// seems roughly right.
|
||||
///
|
||||
/// "buf" must be large enough to fit base + sub + suffix. This is generally
|
||||
/// MAX_PATH_BYTES so its not a big deal.
|
||||
pub fn maybeDir(
|
||||
buf: []u8,
|
||||
base: []const u8,
|
||||
sub: []const u8,
|
||||
suffix: []const u8,
|
||||
) !?[]const u8 {
|
||||
const path = try std.fmt.bufPrint(buf, "{s}/{s}/{s}", .{ base, sub, suffix });
|
||||
|
||||
if (std.fs.accessAbsolute(path, .{})) {
|
||||
const len = path.len - suffix.len - 1;
|
||||
return buf[0..len];
|
||||
} else |_| {
|
||||
// Folder doesn't exist. If a different error happens its okay
|
||||
// we just ignore it and move on.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
@ -544,34 +544,22 @@ const Subprocess = struct {
|
||||
};
|
||||
errdefer env.deinit();
|
||||
|
||||
// Get our bundled resources directory, if it exists. We use this
|
||||
// for terminfo, shell-integration, etc.
|
||||
// If we have a resources dir then set our env var
|
||||
const resources_key = "GHOSTTY_RESOURCES_DIR";
|
||||
const resources_dir = dir: {
|
||||
if (env.get(resources_key)) |dir| {
|
||||
if (dir.len > 0) {
|
||||
log.info("using Ghostty resources dir from env var: {s}", .{dir});
|
||||
break :dir dir;
|
||||
}
|
||||
}
|
||||
|
||||
if (try resourcesDir(alloc)) |dir| {
|
||||
if (opts.resources_dir) |dir| {
|
||||
log.info("found Ghostty resources dir: {s}", .{dir});
|
||||
try env.put(resources_key, dir);
|
||||
break :dir dir;
|
||||
}
|
||||
|
||||
log.warn("Ghostty resources dir not found, some features disabled", .{});
|
||||
break :dir null;
|
||||
};
|
||||
|
||||
// Set our TERM var. This is a bit complicated because we want to use
|
||||
// the ghostty TERM value but we want to only do that if we have
|
||||
// ghostty in the TERMINFO database.
|
||||
//
|
||||
// For now, we just look up a bundled dir but in the future we should
|
||||
// also load the terminfo database and look for it.
|
||||
if (try terminfoDir(alloc, resources_dir)) |dir| {
|
||||
if (opts.resources_dir) |base| {
|
||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const dir = try std.fmt.bufPrint(&buf, "{s}/terminfo", .{base});
|
||||
try env.put("TERM", "xterm-ghostty");
|
||||
try env.put("COLORTERM", "truecolor");
|
||||
try env.put("TERMINFO", dir);
|
||||
@ -641,7 +629,7 @@ const Subprocess = struct {
|
||||
.zsh => .zsh,
|
||||
};
|
||||
|
||||
const dir = resources_dir orelse break :shell null;
|
||||
const dir = opts.resources_dir orelse break :shell null;
|
||||
break :shell try shell_integration.setup(
|
||||
dir,
|
||||
final_path,
|
||||
@ -860,73 +848,6 @@ const Subprocess = struct {
|
||||
fn killCommandFlatpak(command: *FlatpakHostCommand) !void {
|
||||
try command.signal(c.SIGHUP, true);
|
||||
}
|
||||
|
||||
/// Gets the directory to the terminfo database, if it can be detected.
|
||||
/// The memory returned can't be easily freed so the alloc should be
|
||||
/// an arena or something similar.
|
||||
fn terminfoDir(alloc: Allocator, base: ?[]const u8) !?[]const u8 {
|
||||
const dir = base orelse return null;
|
||||
return try tryDir(alloc, dir, "terminfo", "");
|
||||
}
|
||||
|
||||
/// Gets the directory to the bundled resources directory, if it
|
||||
/// exists (not all platforms or packages have it).
|
||||
///
|
||||
/// The memory returned can't be easily freed so the alloc should be
|
||||
/// an arena or something similar.
|
||||
fn resourcesDir(alloc: Allocator) !?[]const u8 {
|
||||
// This is the sentinel value we look for in the path to know
|
||||
// we've found the resources directory.
|
||||
const sentinel = "terminfo/ghostty.termcap";
|
||||
|
||||
// Get the path to our running binary
|
||||
var exe_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
var exe: []const u8 = std.fs.selfExePath(&exe_buf) catch return null;
|
||||
|
||||
// We have an exe path! Climb the tree looking for the terminfo
|
||||
// bundle as we expect it.
|
||||
while (std.fs.path.dirname(exe)) |dir| {
|
||||
exe = dir;
|
||||
|
||||
// On MacOS, we look for the app bundle path.
|
||||
if (comptime builtin.target.isDarwin()) {
|
||||
if (try tryDir(alloc, dir, "Contents/Resources", sentinel)) |v| {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
// On all platforms, we look for a /usr/share style path. This
|
||||
// is valid even on Mac since there is nothing that requires
|
||||
// Ghostty to be in an app bundle.
|
||||
if (try tryDir(alloc, dir, "share", sentinel)) |v| {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Little helper to check if the "base/sub/suffix" directory exists and
|
||||
/// if so, duplicate the "base/sub" path and return it.
|
||||
fn tryDir(
|
||||
alloc: Allocator,
|
||||
base: []const u8,
|
||||
sub: []const u8,
|
||||
suffix: []const u8,
|
||||
) !?[]const u8 {
|
||||
var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined;
|
||||
const path = try std.fmt.bufPrint(&buf, "{s}/{s}/{s}", .{ base, sub, suffix });
|
||||
|
||||
if (std.fs.accessAbsolute(path, .{})) {
|
||||
const len = path.len - suffix.len - 1;
|
||||
return try alloc.dupe(u8, path[0..len]);
|
||||
} else |_| {
|
||||
// Folder doesn't exist. If a different error happens its okay
|
||||
// we just ignore it and move on.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/// The read thread sits in a loop doing the following pseudo code:
|
||||
|
@ -20,6 +20,9 @@ full_config: *const Config,
|
||||
/// The derived configuration for this termio implementation.
|
||||
config: termio.Impl.DerivedConfig,
|
||||
|
||||
/// The application resources directory.
|
||||
resources_dir: ?[]const u8,
|
||||
|
||||
/// The render state. The IO implementation can modify anything here. The
|
||||
/// surface thread will setup the initial "terminal" pointer but the IO impl
|
||||
/// is free to change that if that is useful (i.e. doing some sort of dual
|
||||
|
Reference in New Issue
Block a user