Merge branch 'ghostty-org:main' into dolphin-action

This commit is contained in:
Andrej Daskalov
2024-11-29 00:26:42 +01:00
committed by GitHub
28 changed files with 885 additions and 219 deletions

View File

@ -107,25 +107,40 @@ palette = 7=#a89984
palette = 15=#fbf1c7
```
You can view all available configuration options and their documentation
by executing the command `ghostty +show-config --default --docs`. Note that
this will output the full default configuration with docs to stdout, so
you may want to pipe that through a pager, an editor, etc.
#### Configuration Documentation
There are multiple places to find documentation on the configuration options.
All locations are identical (they're all generated from the same source):
1. There are HTML and Markdown formatted docs in the
`$prefix/share/ghostty/docs` directory. This directory is created
when you build or install Ghostty. The `$prefix` is `zig-out` if you're
building from source (or the specified `--prefix` flag). On macOS,
`$prefix` is the `Contents/Resources` subdirectory of the `.app` bundle.
2. There are man pages in the `$prefix/share/man` directory. This directory
is created when you build or install Ghostty.
3. In the CLI, you can run `ghostty +show-config --default --docs`.
Note that this will output the full default configuration with docs to
stdout, so you may want to pipe that through a pager, an editor, etc.
4. In the source code, you can find the configuration structure in the
[Config structure](https://github.com/ghostty-org/ghostty/blob/main/src/config/Config.zig).
The available keys are the keys verbatim, and their possible values are typically
documented in the comments.
5. Not documentation per se, but you can search for the
[public config files](https://github.com/search?q=path%3Aghostty%2Fconfig&type=code)
of many Ghostty users for examples and inspiration.
> [!NOTE]
>
> You'll see a lot of weird blank configurations like `font-family =`. This
> You may see strange looking blank configurations like `font-family =`. This
> is a valid syntax to specify the default behavior (no value). The
> `+show-config` outputs it so it's clear that key is defaulting and also
> to have something to attach the doc comment to.
You can also see and read all available configuration options in the source
[Config structure](https://github.com/ghostty-org/ghostty/blob/main/src/config/Config.zig).
The available keys are the keys verbatim, and their possible values are typically
documented in the comments. You also can search for the
[public config files](https://github.com/search?q=path%3Aghostty%2Fconfig&type=code)
of many Ghostty users for examples and inspiration.
> [!NOTE]
>
> Configuration can be reloaded on the fly with the `reload_config`

View File

@ -94,6 +94,16 @@ class TerminalController: BaseTerminalController {
}
}
override func fullscreenDidChange() {
super.fullscreenDidChange()
// When our fullscreen state changes, we resync our appearance because some
// properties change when fullscreen or not.
guard let focusedSurface else { return }
syncAppearance(focusedSurface.derivedConfig)
}
//MARK: - Methods
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
@ -204,7 +214,13 @@ class TerminalController: BaseTerminalController {
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (surfaceConfig.backgroundOpacity < 1) {
// Window transparency only takes effect if our window is not native fullscreen.
// In native fullscreen we disable transparency/opacity because the background
// becomes gray and widgets show through.
if (!window.styleMask.contains(.fullScreen) &&
surfaceConfig.backgroundOpacity < 1
) {
window.isOpaque = false
// This is weird, but we don't use ".clear" because this creates a look that

View File

@ -376,7 +376,13 @@ pub fn init(
// We want a config pointer for everything so we get that either
// based on our conditional state or the original config.
const config: *const configpkg.Config = if (config_) |*c| c else config_original;
const config: *const configpkg.Config = if (config_) |*c| config: {
// We want to preserve our original working directory. We
// don't need to dupe memory here because termio will derive
// it. We preserve this so directory inheritance works.
c.@"working-directory" = config_original.@"working-directory";
break :config c;
} else config_original;
// Get our configuration
var derived_config = try DerivedConfig.init(alloc, config);
@ -837,21 +843,28 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
}, .unlocked);
},
.color_change => |change| try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
.kind = switch (change.kind) {
.background => .background,
.foreground => .foreground,
.cursor => .cursor,
.palette => |v| @enumFromInt(v),
.color_change => |change| {
// On any color change, we have to report for mode 2031
// if it is enabled.
self.reportColorScheme(false);
// Notify our apprt
try self.rt_app.performAction(
.{ .surface = self },
.color_change,
.{
.kind = switch (change.kind) {
.background => .background,
.foreground => .foreground,
.cursor => .cursor,
.palette => |v| @enumFromInt(v),
},
.r = change.color.r,
.g = change.color.g,
.b = change.color.b,
},
.r = change.color.r,
.g = change.color.g,
.b = change.color.b,
},
),
);
},
.set_mouse_shape => |shape| {
log.debug("changing mouse shape: {}", .{shape});
@ -915,7 +928,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.renderer_health => |health| self.updateRendererHealth(health),
.report_color_scheme => try self.reportColorScheme(),
.report_color_scheme => |force| self.reportColorScheme(force),
.present_surface => try self.presentSurface(),
@ -952,8 +965,18 @@ fn passwordInput(self: *Surface, v: bool) !void {
try self.queueRender();
}
/// Sends a DSR response for the current color scheme to the pty.
fn reportColorScheme(self: *Surface) !void {
/// Sends a DSR response for the current color scheme to the pty. If
/// force is false then we only send the response if the terminal mode
/// 2031 is enabled.
fn reportColorScheme(self: *Surface, force: bool) void {
if (!force) {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
if (!self.renderer_state.terminal.modes.get(.report_color_scheme)) {
return;
}
}
const output = switch (self.config_conditional_state.theme) {
.light => "\x1B[?997;2n",
.dark => "\x1B[?997;1n",
@ -3660,12 +3683,7 @@ pub fn colorSchemeCallback(self: *Surface, scheme: apprt.ColorScheme) !void {
self.notifyConfigConditionalState();
// If mode 2031 is on, then we report the change live.
const report = report: {
self.renderer_state.mutex.lock();
defer self.renderer_state.mutex.unlock();
break :report self.renderer_state.terminal.modes.get(.report_color_scheme);
};
if (report) try self.reportColorScheme();
self.reportColorScheme(false);
}
pub fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Coordinate {

View File

@ -85,26 +85,38 @@ pub const App = struct {
};
core_app: *CoreApp,
config: *const Config,
opts: Options,
keymap: input.Keymap,
/// The configuration for the app. This is owned by this structure.
config: Config,
/// The keymap state is used for global keybinds only. Each surface
/// also has its own keymap state for focused keybinds.
keymap_state: input.Keymap.State,
pub fn init(core_app: *CoreApp, config: *const Config, opts: Options) !App {
pub fn init(
core_app: *CoreApp,
config: *const Config,
opts: Options,
) !App {
// We have to clone the config.
const alloc = core_app.alloc;
var config_clone = try config.clone(alloc);
errdefer config_clone.deinit();
return .{
.core_app = core_app,
.config = config,
.config = config_clone,
.opts = opts,
.keymap = try input.Keymap.init(),
.keymap_state = .{},
};
}
pub fn terminate(self: App) void {
pub fn terminate(self: *App) void {
self.keymap.deinit();
self.config.deinit();
}
/// Returns true if there are any global keybinds in the configuration.
@ -370,11 +382,11 @@ pub const App = struct {
}
}
pub fn wakeup(self: App) void {
pub fn wakeup(self: *const App) void {
self.opts.wakeup(self.opts.userdata);
}
pub fn wait(self: App) !void {
pub fn wait(self: *const App) !void {
_ = self;
}
@ -450,6 +462,19 @@ pub const App = struct {
},
},
.config_change => switch (target) {
.surface => {},
// For app updates, we update our core config. We need to
// clone it because the caller owns the param.
.app => if (value.config.clone(self.core_app.alloc)) |config| {
self.config.deinit();
self.config = config;
} else |err| {
log.err("error updating app config err={}", .{err});
},
},
else => {},
}
}
@ -573,7 +598,7 @@ pub const Surface = struct {
errdefer app.core_app.deleteSurface(self);
// Shallow copy the config so that we can modify it.
var config = try apprt.surface.newConfig(app.core_app, app.config);
var config = try apprt.surface.newConfig(app.core_app, &app.config);
defer config.deinit();
// If we have a working directory from the options then we set it.
@ -1831,7 +1856,7 @@ pub const CAPI = struct {
// This is only supported on macOS
if (comptime builtin.target.os.tag != .macos) return;
const config = app.config;
const config = &app.config;
// Do nothing if we don't have background transparency enabled
if (config.@"background-opacity" >= 1.0) return;

View File

@ -724,7 +724,7 @@ pub const Surface = struct {
/// Set the shape of the cursor.
fn setMouseShape(self: *Surface, shape: terminal.MouseShape) !void {
if ((comptime builtin.target.isDarwin()) and
!internal_os.macosVersionAtLeast(13, 0, 0))
!internal_os.macos.isAtLeastVersion(13, 0, 0))
{
// We only set our cursor if we're NOT on Mac, or if we are then the
// macOS version is >= 13 (Ventura). On prior versions, glfw crashes

View File

@ -14,6 +14,7 @@ const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const build_config = @import("../../build_config.zig");
const apprt = @import("../../apprt.zig");
const configpkg = @import("../../config.zig");
const input = @import("../../input.zig");
@ -99,9 +100,13 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
c.gtk_get_micro_version(),
});
// Disabling Vulkan can improve startup times by hundreds of
// milliseconds on some systems. We don't use Vulkan so we can just
// disable it.
if (version.atLeast(4, 16, 0)) {
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE
_ = internal_os.setenv("GDK_DISABLE", "gles-api");
// From gtk 4.16, GDK_DEBUG is split into GDK_DEBUG and GDK_DISABLE.
// For the remainder of "why" see the 4.14 comment below.
_ = internal_os.setenv("GDK_DISABLE", "gles-api,vulkan");
_ = internal_os.setenv("GDK_DEBUG", "opengl");
} else if (version.atLeast(4, 14, 0)) {
// We need to export GDK_DEBUG to run on Wayland after GTK 4.14.
@ -110,11 +115,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
// reassess...
//
// Upstream issue: https://gitlab.gnome.org/GNOME/gtk/-/issues/6589
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles");
_ = internal_os.setenv("GDK_DEBUG", "opengl,gl-disable-gles,vulkan-disable");
} else {
_ = internal_os.setenv("GDK_DEBUG", "vulkan-disable");
}
if (version.atLeast(4, 14, 0)) {
// We need to export GSK_RENDERER to opengl because GTK uses ngl by default after 4.14
// We need to export GSK_RENDERER to opengl because GTK uses ngl by
// default after 4.14
_ = internal_os.setenv("GSK_RENDERER", "opengl");
}
@ -181,7 +189,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
}
}
const default_id = "com.mitchellh.ghostty";
const default_id = comptime build_config.bundle_id;
break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id;
};
@ -377,22 +385,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
if (config.@"initial-window")
c.g_application_activate(gapp);
// Register for dbus events
if (c.g_application_get_dbus_connection(gapp)) |dbus_connection| {
_ = c.g_dbus_connection_signal_subscribe(
dbus_connection,
null,
"org.freedesktop.portal.Settings",
"SettingChanged",
"/org/freedesktop/portal/desktop",
"org.freedesktop.appearance",
c.G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE,
&gtkNotifyColorScheme,
core_app,
null,
);
}
// Internally, GTK ensures that only one instance of this provider exists in the provider list
// for the display.
const css_provider = c.gtk_css_provider_new();
@ -401,12 +393,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
@ptrCast(css_provider),
c.GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 3,
);
loadRuntimeCss(core_app.alloc, &config, css_provider) catch |err| switch (err) {
error.OutOfMemory => log.warn(
"out of memory loading runtime CSS, no runtime CSS applied",
.{},
),
};
return .{
.core_app = core_app,
@ -462,7 +448,7 @@ pub fn performAction(
.equalize_splits => self.equalizeSplits(target),
.goto_split => self.gotoSplit(target, value),
.open_config => try configpkg.edit.open(self.core_app.alloc),
.config_change => self.configChange(value.config),
.config_change => self.configChange(target, value.config),
.reload_config => try self.reloadConfig(target, value),
.inspector => self.controlInspector(target, value),
.desktop_notification => self.showDesktopNotification(target, value),
@ -818,18 +804,38 @@ fn showDesktopNotification(
c.g_application_send_notification(g_app, n.body.ptr, notification);
}
fn configChange(self: *App, new_config: *const Config) void {
_ = new_config;
fn configChange(
self: *App,
target: apprt.Target,
new_config: *const Config,
) void {
switch (target) {
// We don't do anything for surface config change events. There
// is nothing to sync with regards to a surface today.
.surface => {},
self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
.app => {
// We clone (to take ownership) and update our configuration.
if (new_config.clone(self.core_app.alloc)) |config_clone| {
self.config.deinit();
self.config = config_clone;
} else |err| {
log.warn("error cloning configuration err={}", .{err});
}
if (adwaita.enabled(&self.config)) {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
self.syncConfigChanges() catch |err| {
log.warn("error handling configuration changes err={}", .{err});
};
// App changes needs to show a toast that our configuration
// has reloaded.
if (adwaita.enabled(&self.config)) {
if (self.core_app.focusedSurface()) |core_surface| {
const surface = core_surface.rt_surface;
if (surface.container.window()) |window| window.onConfigReloaded();
}
}
},
}
}
@ -870,7 +876,7 @@ fn syncConfigChanges(self: *App) !void {
// Load our runtime CSS. If this fails then our window is just stuck
// with the old CSS but we don't want to fail the entire sync operation.
loadRuntimeCss(self.core_app.alloc, &self.config, self.css_provider) catch |err| switch (err) {
self.loadRuntimeCss() catch |err| switch (err) {
error.OutOfMemory => log.warn(
"out of memory loading runtime CSS, no runtime CSS applied",
.{},
@ -934,15 +940,14 @@ fn syncActionAccelerator(
}
fn loadRuntimeCss(
alloc: Allocator,
config: *const Config,
provider: *c.GtkCssProvider,
self: *const App,
) Allocator.Error!void {
var stack_alloc = std.heap.stackFallback(4096, alloc);
var stack_alloc = std.heap.stackFallback(4096, self.core_app.alloc);
var buf = std.ArrayList(u8).init(stack_alloc.get());
defer buf.deinit();
const writer = buf.writer();
const config: *const Config = &self.config;
const window_theme = config.@"window-theme";
const unfocused_fill: Config.Color = config.@"unfocused-split-fill" orelse config.background;
const headerbar_background = config.background;
@ -1005,7 +1010,7 @@ fn loadRuntimeCss(
// Clears any previously loaded CSS from this provider
c.gtk_css_provider_load_from_data(
provider,
self.css_provider,
buf.items.ptr,
@intCast(buf.items.len),
);
@ -1054,11 +1059,17 @@ pub fn run(self: *App) !void {
self.transient_cgroup_base = path;
} else log.debug("cgroup isolation disabled config={}", .{self.config.@"linux-cgroup"});
// Setup our D-Bus connection for listening to settings changes.
self.initDbus();
// Setup our menu items
self.initActions();
self.initMenu();
self.initContextMenu();
// Setup our initial color scheme
self.colorSchemeEvent(self.getColorScheme());
// On startup, we want to check for configuration errors right away
// so we can show our error window. We also need to setup other initial
// state.
@ -1092,6 +1103,26 @@ pub fn run(self: *App) !void {
}
}
fn initDbus(self: *App) void {
const dbus = c.g_application_get_dbus_connection(@ptrCast(self.app)) orelse {
log.warn("unable to get dbus connection, not setting up events", .{});
return;
};
_ = c.g_dbus_connection_signal_subscribe(
dbus,
null,
"org.freedesktop.portal.Settings",
"SettingChanged",
"/org/freedesktop/portal/desktop",
"org.freedesktop.appearance",
c.G_DBUS_SIGNAL_FLAGS_MATCH_ARG0_NAMESPACE,
&gtkNotifyColorScheme,
self,
null,
);
}
// This timeout function is started when no surfaces are open. It can be
// cancelled if a new surface is opened before the timer expires.
pub fn gtkQuitTimerExpired(ud: ?*anyopaque) callconv(.C) c.gboolean {
@ -1372,7 +1403,7 @@ fn gtkNotifyColorScheme(
parameters: ?*c.GVariant,
user_data: ?*anyopaque,
) callconv(.C) void {
const core_app: *CoreApp = @ptrCast(@alignCast(user_data orelse {
const self: *App = @ptrCast(@alignCast(user_data orelse {
log.err("style change notification: userdata is null", .{});
return;
}));
@ -1404,9 +1435,20 @@ fn gtkNotifyColorScheme(
else
.light;
for (core_app.surfaces.items) |surface| {
surface.core_surface.colorSchemeCallback(color_scheme) catch |err| {
log.err("unable to tell surface about color scheme change: {}", .{err});
self.colorSchemeEvent(color_scheme);
}
fn colorSchemeEvent(
self: *App,
scheme: apprt.ColorScheme,
) void {
self.core_app.colorSchemeEvent(self, scheme) catch |err| {
log.err("error updating app color scheme err={}", .{err});
};
for (self.core_app.surfaces.items) |surface| {
surface.core_surface.colorSchemeCallback(scheme) catch |err| {
log.err("unable to tell surface about color scheme change err={}", .{err});
};
}
}

View File

@ -3,6 +3,7 @@ const ConfigErrors = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const Config = configpkg.Config;
@ -53,7 +54,7 @@ fn init(self: *ConfigErrors, app: *App) !void {
c.gtk_window_set_title(gtk_window, "Configuration Errors");
c.gtk_window_set_default_size(gtk_window, 600, 275);
c.gtk_window_set_resizable(gtk_window, 0);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
_ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(&gtkDestroy), self, null, c.G_CONNECT_DEFAULT);
// Set some state

View File

@ -5,6 +5,7 @@ const Surface = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const build_config = @import("../../build_config.zig");
const configpkg = @import("../../config.zig");
const apprt = @import("../../apprt.zig");
const font = @import("../../font/main.zig");
@ -1149,7 +1150,7 @@ pub fn showDesktopNotification(
defer c.g_object_unref(notification);
c.g_notification_set_body(notification, body.ptr);
const icon = c.g_themed_icon_new("com.mitchellh.ghostty");
const icon = c.g_themed_icon_new(build_config.bundle_id);
defer c.g_object_unref(icon);
c.g_notification_set_icon(notification, icon);

View File

@ -103,7 +103,7 @@ pub fn init(self: *Window, app: *App) !void {
// to disable this so that terminal programs can capture F10 (such as htop)
c.gtk_window_set_handle_menubar_accel(gtk_window, 0);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
// Apply class to color headerbar if window-theme is set to `ghostty` and
// GTK version is before 4.16. The conditional is because above 4.16

View File

@ -2,6 +2,7 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
const build_config = @import("../../build_config.zig");
const App = @import("App.zig");
const Surface = @import("Surface.zig");
const TerminalWindow = @import("Window.zig");
@ -141,7 +142,7 @@ const Window = struct {
self.window = gtk_window;
c.gtk_window_set_title(gtk_window, "Ghostty: Terminal Inspector");
c.gtk_window_set_default_size(gtk_window, 1000, 600);
c.gtk_window_set_icon_name(gtk_window, "com.mitchellh.ghostty");
c.gtk_window_set_icon_name(gtk_window, build_config.bundle_id);
// Initialize our imgui widget
try self.imgui_widget.init();

View File

@ -58,8 +58,10 @@ pub const Message = union(enum) {
/// Health status change for the renderer.
renderer_health: renderer.Health,
/// Report the color scheme
report_color_scheme: void,
/// Report the color scheme. The bool parameter is whether to force or not.
/// If force is true, the color scheme should be reported even if mode
/// 2031 is not set.
report_color_scheme: bool,
/// Tell the surface to present itself to the user. This may require raising
/// a window and switching tabs.

View File

@ -53,7 +53,7 @@ fn writeFishCompletions(writer: anytype) !void {
if (std.mem.startsWith(u8, field.name, "font-family"))
try writer.writeAll(" -f -a \"(ghostty +list-fonts | grep '^[A-Z]')\"")
else if (std.mem.eql(u8, "theme", field.name))
try writer.writeAll(" -f -a \"(ghostty +list-themes)\"")
try writer.writeAll(" -f -a \"(ghostty +list-themes | sed -E 's/^(.*) \\(.*\\$/\\1/')\"")
else if (std.mem.eql(u8, "working-directory", field.name))
try writer.writeAll(" -f -k -a \"(__fish_complete_directories)\"")
else {

View File

@ -103,6 +103,20 @@ pub const app_runtime: apprt.Runtime = config.app_runtime;
pub const font_backend: font.Backend = config.font_backend;
pub const renderer: rendererpkg.Impl = config.renderer;
/// The bundle ID for the app. This is used in many places and is currently
/// hardcoded here. We could make this configurable in the future if there
/// is a reason to do so.
///
/// On macOS, this must match the App bundle ID. We can get that dynamically
/// via an API but I don't want to pay the cost of that at runtime.
///
/// On GTK, this should match the various folders with resources.
///
/// There are many places that don't use this variable so simply swapping
/// this variable is NOT ENOUGH to change the bundle ID. I just wanted to
/// avoid it in Zig coe as much as possible.
pub const bundle_id = "com.mitchellh.ghostty";
/// True if we should have "slow" runtime safety checks. The initial motivation
/// for this was terminal page/pagelist integrity checks. These were VERY
/// slow but very thorough. But they made it so slow that the terminal couldn't

View File

@ -104,7 +104,7 @@ pub fn parse(
try dst._diagnostics.append(arena_alloc, .{
.key = try arena_alloc.dupeZ(u8, arg),
.message = "invalid field",
.location = diags.Location.fromIter(iter),
.location = try diags.Location.fromIter(iter, arena_alloc),
});
continue;
@ -145,7 +145,7 @@ pub fn parse(
try dst._diagnostics.append(arena_alloc, .{
.key = try arena_alloc.dupeZ(u8, key),
.message = message,
.location = diags.Location.fromIter(iter),
.location = try diags.Location.fromIter(iter, arena_alloc),
});
};
}
@ -1140,7 +1140,7 @@ pub fn ArgsIterator(comptime Iterator: type) type {
}
/// Returns a location for a diagnostic message.
pub fn location(self: *const Self) ?diags.Location {
pub fn location(self: *const Self, _: Allocator) error{}!?diags.Location {
return .{ .cli = self.index };
}
};
@ -1262,12 +1262,15 @@ pub fn LineIterator(comptime ReaderType: type) type {
}
/// Returns a location for a diagnostic message.
pub fn location(self: *const Self) ?diags.Location {
pub fn location(
self: *const Self,
alloc: Allocator,
) Allocator.Error!?diags.Location {
// If we have no filepath then we have no location.
if (self.filepath.len == 0) return null;
return .{ .file = .{
.path = self.filepath,
.path = try alloc.dupe(u8, self.filepath),
.line = self.line,
} };
}

View File

@ -34,6 +34,14 @@ pub const Diagnostic = struct {
try writer.print("{s}", .{self.message});
}
pub fn clone(self: *const Diagnostic, alloc: Allocator) Allocator.Error!Diagnostic {
return .{
.location = try self.location.clone(alloc),
.key = try alloc.dupeZ(u8, self.key),
.message = try alloc.dupeZ(u8, self.message),
};
}
};
/// The possible locations for a diagnostic message. This is used
@ -48,7 +56,7 @@ pub const Location = union(enum) {
pub const Key = @typeInfo(Location).Union.tag_type.?;
pub fn fromIter(iter: anytype) Location {
pub fn fromIter(iter: anytype, alloc: Allocator) Allocator.Error!Location {
const Iter = t: {
const T = @TypeOf(iter);
break :t switch (@typeInfo(T)) {
@ -59,7 +67,20 @@ pub const Location = union(enum) {
};
if (!@hasDecl(Iter, "location")) return .none;
return iter.location() orelse .none;
return (try iter.location(alloc)) orelse .none;
}
pub fn clone(self: *const Location, alloc: Allocator) Allocator.Error!Location {
return switch (self.*) {
.none,
.cli,
=> self.*,
.file => |v| .{ .file = .{
.path = try alloc.dupe(u8, v.path),
.line = v.line,
} },
};
}
};
@ -88,11 +109,45 @@ pub const DiagnosticList = struct {
// We specifically want precompute for libghostty.
.lib => true,
};
const Precompute = if (precompute_enabled) struct {
messages: std.ArrayListUnmanaged([:0]const u8) = .{},
pub fn clone(
self: *const Precompute,
alloc: Allocator,
) Allocator.Error!Precompute {
var result: Precompute = .{};
try result.messages.ensureTotalCapacity(alloc, self.messages.items.len);
for (self.messages.items) |msg| {
result.messages.appendAssumeCapacity(
try alloc.dupeZ(u8, msg),
);
}
return result;
}
} else void;
const precompute_init: Precompute = if (precompute_enabled) .{} else {};
pub fn clone(
self: *const DiagnosticList,
alloc: Allocator,
) Allocator.Error!DiagnosticList {
var result: DiagnosticList = .{};
try result.list.ensureTotalCapacity(alloc, self.list.items.len);
for (self.list.items) |*diag| result.list.appendAssumeCapacity(
try diag.clone(alloc),
);
if (comptime precompute_enabled) {
result.precompute = try self.precompute.clone(alloc);
}
return result;
}
pub fn append(
self: *DiagnosticList,
alloc: Allocator,

View File

@ -527,6 +527,10 @@ palette: Palette = .{},
/// The opacity level (opposite of transparency) of the background. A value of
/// 1 is fully opaque and a value of 0 is fully transparent. A value less than 0
/// or greater than 1 will be clamped to the nearest valid value.
///
/// On macOS, background opacity is disabled when the terminal enters native
/// fullscreen. This is because the background becomes gray and it can cause
/// widgets to show through which isn't generally desirable.
@"background-opacity": f64 = 1.0,
/// A positive value enables blurring of the background when background-opacity
@ -1793,6 +1797,10 @@ _diagnostics: cli.DiagnosticList = .{},
/// determine if a conditional configuration matches or not.
_conditional_state: conditional.State = .{},
/// The conditional keys that are used at any point during the configuration
/// loading. This is used to speed up the conditional evaluation process.
_conditional_set: std.EnumSet(conditional.Key) = .{},
/// The steps we can use to reload the configuration after it has been loaded
/// without reopening the files. This is used in very specific cases such
/// as loadTheme which has more details on why.
@ -1809,9 +1817,10 @@ pub fn deinit(self: *Config) void {
/// Load the configuration according to the default rules:
///
/// 1. Defaults
/// 2. XDG Config File
/// 3. CLI flags
/// 4. Recursively defined configuration files
/// 2. XDG config dir
/// 3. "Application Support" directory (macOS only)
/// 4. CLI flags
/// 5. Recursively defined configuration files
///
pub fn load(alloc_gpa: Allocator) !Config {
var result = try default(alloc_gpa);
@ -2394,25 +2403,37 @@ pub fn loadFile(self: *Config, alloc: Allocator, path: []const u8) !void {
try self.expandPaths(std.fs.path.dirname(path).?);
}
/// Load the configuration from the default configuration file. The default
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
const config_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(config_path);
self.loadFile(alloc, config_path) catch |err| switch (err) {
/// Load optional configuration file from `path`. All errors are ignored.
pub fn loadOptionalFile(self: *Config, alloc: Allocator, path: []const u8) void {
self.loadFile(alloc, path) catch |err| switch (err) {
error.FileNotFound => std.log.info(
"homedir config not found, not loading path={s}",
.{config_path},
"optional config file not found, not loading path={s}",
.{path},
),
else => std.log.warn(
"error reading config file, not loading err={} path={s}",
.{ err, config_path },
"error reading optional config file, not loading err={} path={s}",
.{ err, path },
),
};
}
/// Load configurations from the default configuration files. The default
/// configuration file is at `$XDG_CONFIG_HOME/ghostty/config`.
///
/// On macOS, `$HOME/Library/Application Support/$CFBundleIdentifier/config`
/// is also loaded.
pub fn loadDefaultFiles(self: *Config, alloc: Allocator) !void {
const xdg_path = try internal_os.xdg.config(alloc, .{ .subdir = "ghostty/config" });
defer alloc.free(xdg_path);
self.loadOptionalFile(alloc, xdg_path);
if (comptime builtin.os.tag == .macos) {
const app_support_path = try internal_os.macos.appSupportDir(alloc, "config");
defer alloc.free(app_support_path);
self.loadOptionalFile(alloc, app_support_path);
}
}
/// Load and parse the CLI args.
pub fn loadCliArgs(self: *Config, alloc_gpa: Allocator) !void {
switch (builtin.os.tag) {
@ -2610,6 +2631,10 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
/// on the new state. The caller must free the old configuration if they
/// wish.
///
/// This returns null if the conditional state would result in no changes
/// to the configuration. In this case, the caller can continue to use
/// the existing configuration or clone if they want a copy.
///
/// This doesn't re-read any files, it just re-applies the same
/// configuration with the new conditional state. Importantly, this means
/// that if you change the conditional state and the user in the interim
@ -2618,7 +2643,30 @@ pub fn loadRecursiveFiles(self: *Config, alloc_gpa: Allocator) !void {
pub fn changeConditionalState(
self: *const Config,
new: conditional.State,
) !Config {
) !?Config {
// If the conditional state between the old and new is the same,
// then we don't need to do anything.
relevant: {
inline for (@typeInfo(conditional.Key).Enum.fields) |field| {
const key: conditional.Key = @field(conditional.Key, field.name);
// Conditional set contains the keys that this config uses. So we
// only continue if we use this key.
if (self._conditional_set.contains(key) and !equalField(
@TypeOf(@field(self._conditional_state, field.name)),
@field(self._conditional_state, field.name),
@field(new, field.name),
)) {
break :relevant;
}
}
// If we got here, then we didn't find any differences between
// the old and new conditional state that would affect the
// configuration.
return null;
}
// Create our new configuration
const alloc_gpa = self._arena.?.child_allocator;
var new_config = try self.cloneEmpty(alloc_gpa);
@ -2765,6 +2813,9 @@ pub fn finalize(self: *Config) !void {
// This setting doesn't make sense with different light/dark themes
// because it'll force the theme based on the Ghostty theme.
if (self.@"window-theme" == .auto) self.@"window-theme" = .system;
// Mark that we use a conditional theme
self._conditional_set.insert(.theme);
}
}
@ -2941,7 +2992,7 @@ pub fn parseManuallyHook(
if (command.items.len == 0) {
try self._diagnostics.append(alloc, .{
.location = cli.Location.fromIter(iter),
.location = try cli.Location.fromIter(iter, alloc),
.message = try std.fmt.allocPrintZ(
alloc,
"missing command after {s}",
@ -2995,22 +3046,47 @@ pub fn cloneEmpty(
/// Create a copy of this configuration.
///
/// This will not re-read referenced configuration files except for the
/// theme, but the config-file values will be preserved.
/// This will not re-read referenced configuration files and operates
/// purely in-memory.
pub fn clone(
self: *const Config,
alloc_gpa: Allocator,
) !Config {
// Create a new config with a new arena
var new_config = try self.cloneEmpty(alloc_gpa);
errdefer new_config.deinit();
) Allocator.Error!Config {
// Start with an empty config
var result = try self.cloneEmpty(alloc_gpa);
errdefer result.deinit();
const alloc_arena = result._arena.?.allocator();
// Replay all of our steps to rebuild the configuration
var it = Replay.iterator(self._replay_steps.items, &new_config);
try new_config.loadIter(alloc_gpa, &it);
try new_config.finalize();
// Copy our values
inline for (@typeInfo(Config).Struct.fields) |field| {
if (!@hasField(Key, field.name)) continue;
@field(result, field.name) = try cloneValue(
alloc_arena,
field.type,
@field(self, field.name),
);
}
return new_config;
// Copy our diagnostics
result._diagnostics = try self._diagnostics.clone(alloc_arena);
// Preserve our replay steps. We copy them exactly to also preserve
// the exact conditionals required for some steps.
try result._replay_steps.ensureTotalCapacity(
alloc_arena,
self._replay_steps.items.len,
);
for (self._replay_steps.items) |item| {
result._replay_steps.appendAssumeCapacity(
try item.clone(alloc_arena),
);
}
assert(result._replay_steps.items.len == self._replay_steps.items.len);
// Copy the conditional set
result._conditional_set = self._conditional_set;
return result;
}
fn cloneValue(
@ -3204,6 +3280,24 @@ const Replay = struct {
conditions: []const Conditional,
arg: []const u8,
},
fn clone(
self: Step,
alloc: Allocator,
) Allocator.Error!Step {
return switch (self) {
.arg => |v| .{ .arg = try alloc.dupe(u8, v) },
.expand => |v| .{ .expand = try alloc.dupe(u8, v) },
.conditional_arg => |v| conditional: {
var conds = try alloc.alloc(Conditional, v.conditions.len);
for (v.conditions, 0..) |cond, i| conds[i] = try cond.clone(alloc);
break :conditional .{ .conditional_arg = .{
.conditions = conds,
.arg = try alloc.dupe(u8, v.arg),
} };
},
};
}
};
const Iterator = struct {
@ -4523,17 +4617,33 @@ pub const RepeatableLink = struct {
}
/// Deep copy of the struct. Required by Config.
pub fn clone(self: *const Self, alloc: Allocator) error{}!Self {
_ = self;
_ = alloc;
return .{};
pub fn clone(
self: *const Self,
alloc: Allocator,
) Allocator.Error!Self {
// Note: we don't do any errdefers below since the allocation
// is expected to be arena allocated.
var list = try std.ArrayListUnmanaged(inputpkg.Link).initCapacity(
alloc,
self.links.items.len,
);
for (self.links.items) |item| {
const copy = try item.clone(alloc);
list.appendAssumeCapacity(copy);
}
return .{ .links = list };
}
/// Compare if two of our value are requal. Required by Config.
pub fn equal(self: Self, other: Self) bool {
_ = self;
_ = other;
return true;
const itemsA = self.links.items;
const itemsB = other.links.items;
if (itemsA.len != itemsB.len) return false;
for (itemsA, itemsB) |*a, *b| {
if (!a.equal(b)) return false;
} else return true;
}
/// Used by Formatter
@ -5221,20 +5331,107 @@ test "clone preserves conditional state" {
var a = try Config.default(alloc);
defer a.deinit();
var b = try a.changeConditionalState(.{ .theme = .dark });
defer b.deinit();
try testing.expectEqual(.dark, b._conditional_state.theme);
var dest = try b.clone(alloc);
a._conditional_state.theme = .dark;
try testing.expectEqual(.dark, a._conditional_state.theme);
var dest = try a.clone(alloc);
defer dest.deinit();
// Should have no changes
var it = b.changeIterator(&dest);
var it = a.changeIterator(&dest);
try testing.expectEqual(@as(?Key, null), it.next());
// Should have the same conditional state
try testing.expectEqual(.dark, dest._conditional_state.theme);
}
test "clone can then change conditional state" {
// This tests a particular bug sequence where:
// 1. Load light
// 2. Convert to dark
// 3. Clone dark
// 4. Convert to light
// 5. Config is still dark (bug)
const testing = std.testing;
const alloc = testing.allocator;
var arena = ArenaAllocator.init(alloc);
defer arena.deinit();
const alloc_arena = arena.allocator();
// Setup our test theme
var td = try internal_os.TempDir.init();
defer td.deinit();
{
var file = try td.dir.createFile("theme_light", .{});
defer file.close();
try file.writer().writeAll(@embedFile("testdata/theme_light"));
}
{
var file = try td.dir.createFile("theme_dark", .{});
defer file.close();
try file.writer().writeAll(@embedFile("testdata/theme_dark"));
}
var light_buf: [std.fs.max_path_bytes]u8 = undefined;
const light = try td.dir.realpath("theme_light", &light_buf);
var dark_buf: [std.fs.max_path_bytes]u8 = undefined;
const dark = try td.dir.realpath("theme_dark", &dark_buf);
var cfg_light = try Config.default(alloc);
defer cfg_light.deinit();
var it: TestIterator = .{ .data = &.{
try std.fmt.allocPrint(
alloc_arena,
"--theme=light:{s},dark:{s}",
.{ light, dark },
),
} };
try cfg_light.loadIter(alloc, &it);
try cfg_light.finalize();
var cfg_dark = (try cfg_light.changeConditionalState(.{ .theme = .dark })).?;
defer cfg_dark.deinit();
try testing.expectEqual(Color{
.r = 0xEE,
.g = 0xEE,
.b = 0xEE,
}, cfg_dark.background);
var cfg_clone = try cfg_dark.clone(alloc);
defer cfg_clone.deinit();
try testing.expectEqual(Color{
.r = 0xEE,
.g = 0xEE,
.b = 0xEE,
}, cfg_clone.background);
var cfg_light2 = (try cfg_clone.changeConditionalState(.{ .theme = .light })).?;
defer cfg_light2.deinit();
try testing.expectEqual(Color{
.r = 0xFF,
.g = 0xFF,
.b = 0xFF,
}, cfg_light2.background);
}
test "clone preserves conditional set" {
const testing = std.testing;
const alloc = testing.allocator;
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--theme=light:foo,dark:bar",
"--window-theme=auto",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
var clone1 = try cfg.clone(alloc);
defer clone1.deinit();
try testing.expect(clone1._conditional_set.contains(.theme));
}
test "changed" {
const testing = std.testing;
const alloc = testing.allocator;
@ -5249,6 +5446,44 @@ test "changed" {
try testing.expect(!source.changed(&dest, .@"font-size"));
}
test "changeConditionalState ignores irrelevant changes" {
const testing = std.testing;
const alloc = testing.allocator;
{
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--theme=foo",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
try testing.expect(try cfg.changeConditionalState(
.{ .theme = .dark },
) == null);
}
}
test "changeConditionalState applies relevant changes" {
const testing = std.testing;
const alloc = testing.allocator;
{
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--theme=light:foo,dark:bar",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
var cfg2 = (try cfg.changeConditionalState(.{ .theme = .dark })).?;
defer cfg2.deinit();
try testing.expect(cfg2._conditional_set.contains(.theme));
}
}
test "theme loading" {
const testing = std.testing;
const alloc = testing.allocator;
@ -5280,6 +5515,9 @@ test "theme loading" {
.g = 0x3A,
.b = 0xBC,
}, cfg.background);
// Not a conditional theme
try testing.expect(!cfg._conditional_set.contains(.theme));
}
test "theme loading preserves conditional state" {
@ -5428,7 +5666,7 @@ test "theme loading correct light/dark" {
try cfg.loadIter(alloc, &it);
try cfg.finalize();
var new = try cfg.changeConditionalState(.{ .theme = .dark });
var new = (try cfg.changeConditionalState(.{ .theme = .dark })).?;
defer new.deinit();
try testing.expectEqual(Color{
.r = 0xEE,
@ -5455,3 +5693,22 @@ test "theme specifying light/dark changes window-theme from auto" {
try testing.expect(cfg.@"window-theme" == .system);
}
}
test "theme specifying light/dark sets theme usage in conditional state" {
const testing = std.testing;
const alloc = testing.allocator;
{
var cfg = try Config.default(alloc);
defer cfg.deinit();
var it: TestIterator = .{ .data = &.{
"--theme=light:foo,dark:bar",
"--window-theme=auto",
} };
try cfg.loadIter(alloc, &it);
try cfg.finalize();
try testing.expect(cfg.@"window-theme" == .system);
try testing.expect(cfg._conditional_set.contains(.theme));
}
}

View File

@ -61,6 +61,17 @@ pub const Conditional = struct {
value: []const u8,
pub const Op = enum { eq, ne };
pub fn clone(
self: Conditional,
alloc: Allocator,
) Allocator.Error!Conditional {
return .{
.key = self.key,
.op = self.op,
.value = try alloc.dupe(u8, self.value),
};
}
};
test "conditional enum match" {

View File

@ -4,6 +4,8 @@
//! action types.
const Link = @This();
const std = @import("std");
const Allocator = std.mem.Allocator;
const oni = @import("oniguruma");
const Mods = @import("key.zig").Mods;
@ -59,3 +61,19 @@ pub fn oniRegex(self: *const Link) !oni.Regex {
null,
);
}
/// Deep clone the link.
pub fn clone(self: *const Link, alloc: Allocator) Allocator.Error!Link {
return .{
.regex = try alloc.dupe(u8, self.regex),
.action = self.action,
.highlight = self.highlight,
};
}
/// Check if two links are equal.
pub fn equal(self: *const Link, other: *const Link) bool {
return std.meta.eql(self.action, other.action) and
std.meta.eql(self.highlight, other.highlight) and
std.mem.eql(u8, self.regex, other.regex);
}

View File

@ -141,7 +141,7 @@ fn logFn(
// Initialize a logger. This is slow to do on every operation
// but we shouldn't be logging too much.
const logger = macos.os.Log.create("com.mitchellh.ghostty", @tagName(scope));
const logger = macos.os.Log.create(build_config.bundle_id, @tagName(scope));
defer logger.release();
logger.log(std.heap.c_allocator, mac_level, format, args);
}

118
src/os/macos.zig Normal file
View File

@ -0,0 +1,118 @@
const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const assert = std.debug.assert;
const objc = @import("objc");
const Allocator = std.mem.Allocator;
/// Verifies that the running macOS system version is at least the given version.
pub fn isAtLeastVersion(major: i64, minor: i64, patch: i64) bool {
comptime assert(builtin.target.isDarwin());
const NSProcessInfo = objc.getClass("NSProcessInfo").?;
const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{});
return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{
NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch },
});
}
pub const AppSupportDirError = Allocator.Error || error{AppleAPIFailed};
/// Return the path to the application support directory for Ghostty
/// with the given sub path joined. This allocates the result using the
/// given allocator.
pub fn appSupportDir(
alloc: Allocator,
sub_path: []const u8,
) AppSupportDirError![]u8 {
comptime assert(builtin.target.isDarwin());
const NSFileManager = objc.getClass("NSFileManager").?;
const manager = NSFileManager.msgSend(
objc.Object,
objc.sel("defaultManager"),
.{},
);
const url = manager.msgSend(
objc.Object,
objc.sel("URLForDirectory:inDomain:appropriateForURL:create:error:"),
.{
NSSearchPathDirectory.NSApplicationSupportDirectory,
NSSearchPathDomainMask.NSUserDomainMask,
@as(?*anyopaque, null),
true,
@as(?*anyopaque, null),
},
);
// I don't think this is possible but just in case.
if (url.value == null) return error.AppleAPIFailed;
// Get the UTF-8 string from the URL.
const path = url.getProperty(objc.Object, "path");
const c_str = path.getProperty(?[*:0]const u8, "UTF8String") orelse
return error.AppleAPIFailed;
const app_support_dir = std.mem.sliceTo(c_str, 0);
return try std.fs.path.join(alloc, &.{
app_support_dir,
build_config.bundle_id,
sub_path,
});
}
pub const SetQosClassError = error{
// The thread can't have its QoS class changed usually because
// a different pthread API was called that makes it an invalid
// target.
ThreadIncompatible,
};
/// Set the QoS class of the running thread.
///
/// https://developer.apple.com/documentation/apple-silicon/tuning-your-code-s-performance-for-apple-silicon?preferredLanguage=occ
pub fn setQosClass(class: QosClass) !void {
return switch (std.posix.errno(pthread_set_qos_class_self_np(
class,
0,
))) {
.SUCCESS => {},
.PERM => error.ThreadIncompatible,
// EPERM is the only known error that can happen based on
// the man pages for pthread_set_qos_class_self_np. I haven't
// checked the XNU source code to see if there are other
// possible errors.
else => @panic("unexpected pthread_set_qos_class_self_np error"),
};
}
/// https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html#//apple_ref/doc/uid/TP40013929-CH35-SW1
pub const QosClass = enum(c_uint) {
user_interactive = 0x21,
user_initiated = 0x19,
default = 0x15,
utility = 0x11,
background = 0x09,
unspecified = 0x00,
};
extern "c" fn pthread_set_qos_class_self_np(
qos_class: QosClass,
relative_priority: c_int,
) c_int;
pub const NSOperatingSystemVersion = extern struct {
major: i64,
minor: i64,
patch: i64,
};
pub const NSSearchPathDirectory = enum(c_ulong) {
NSApplicationSupportDirectory = 14,
};
pub const NSSearchPathDomainMask = enum(c_ulong) {
NSUserDomainMask = 1,
};

View File

@ -1,21 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const objc = @import("objc");
/// Verifies that the running macOS system version is at least the given version.
pub fn macosVersionAtLeast(major: i64, minor: i64, patch: i64) bool {
assert(builtin.target.isDarwin());
const NSProcessInfo = objc.getClass("NSProcessInfo").?;
const info = NSProcessInfo.msgSend(objc.Object, objc.sel("processInfo"), .{});
return info.msgSend(bool, objc.sel("isOperatingSystemAtLeastVersion:"), .{
NSOperatingSystemVersion{ .major = major, .minor = minor, .patch = patch },
});
}
pub const NSOperatingSystemVersion = extern struct {
major: i64,
minor: i64,
patch: i64,
};

View File

@ -8,7 +8,6 @@ const file = @import("file.zig");
const flatpak = @import("flatpak.zig");
const homedir = @import("homedir.zig");
const locale = @import("locale.zig");
const macos_version = @import("macos_version.zig");
const mouse = @import("mouse.zig");
const openpkg = @import("open.zig");
const pipepkg = @import("pipe.zig");
@ -21,6 +20,7 @@ pub const hostname = @import("hostname.zig");
pub const passwd = @import("passwd.zig");
pub const xdg = @import("xdg.zig");
pub const windows = @import("windows.zig");
pub const macos = @import("macos.zig");
// Functions and types
pub const CFReleaseThread = @import("cf_release_thread.zig");
@ -37,7 +37,6 @@ pub const freeTmpDir = file.freeTmpDir;
pub const isFlatpak = flatpak.isFlatpak;
pub const home = homedir.home;
pub const ensureLocale = locale.ensureLocale;
pub const macosVersionAtLeast = macos_version.macosVersionAtLeast;
pub const clickInterval = mouse.clickInterval;
pub const open = openpkg.open;
pub const pipe = pipepkg.pipe;

View File

@ -4,8 +4,10 @@ pub const Thread = @This();
const std = @import("std");
const builtin = @import("builtin");
const assert = std.debug.assert;
const xev = @import("xev");
const crash = @import("../crash/main.zig");
const internal_os = @import("../os/main.zig");
const renderer = @import("../renderer.zig");
const apprt = @import("../apprt.zig");
const configpkg = @import("../config.zig");
@ -92,6 +94,10 @@ flags: packed struct {
/// This is true when the view is visible. This is used to determine
/// if we should be rendering or not.
visible: bool = true,
/// This is true when the view is focused. This defaults to true
/// and it is up to the apprt to set the correct value.
focused: bool = true,
} = .{},
pub const DerivedConfig = struct {
@ -199,6 +205,9 @@ fn threadMain_(self: *Thread) !void {
};
defer crash.sentry.thread_state = null;
// Setup our thread QoS
self.setQosClass();
// Run our loop start/end callbacks if the renderer cares.
const has_loop = @hasDecl(renderer.Renderer, "loopEnter");
if (has_loop) try self.renderer.loopEnter(self);
@ -237,6 +246,36 @@ fn threadMain_(self: *Thread) !void {
_ = try self.loop.run(.until_done);
}
fn setQosClass(self: *const Thread) void {
// Thread QoS classes are only relevant on macOS.
if (comptime !builtin.target.isDarwin()) return;
const class: internal_os.macos.QosClass = class: {
// If we aren't visible (our view is fully occluded) then we
// always drop our rendering priority down because it's just
// mostly wasted work.
//
// The renderer itself should be doing this as well (for example
// Metal will stop our DisplayLink) but this also helps with
// general forced updates and CPU usage i.e. a rebuild cells call.
if (!self.flags.visible) break :class .utility;
// If we're not focused, but we're visible, then we set a higher
// than default priority because framerates still matter but it isn't
// as important as when we're focused.
if (!self.flags.focused) break :class .user_initiated;
// We are focused and visible, we are the definition of user interactive.
break :class .user_interactive;
};
if (internal_os.macos.setQosClass(class)) {
log.debug("thread QoS class set class={}", .{class});
} else |err| {
log.warn("error setting QoS class err={}", .{err});
}
}
fn startDrawTimer(self: *Thread) void {
// If our renderer doesn't support animations then we never run this.
if (!@hasDecl(renderer.Renderer, "hasAnimations")) return;
@ -273,10 +312,16 @@ fn drainMailbox(self: *Thread) !void {
switch (message) {
.crash => @panic("crash request, crashing intentionally"),
.visible => |v| {
.visible => |v| visible: {
// If our state didn't change we do nothing.
if (self.flags.visible == v) break :visible;
// Set our visible state
self.flags.visible = v;
// Visibility affects our QoS class
self.setQosClass();
// If we became visible then we immediately trigger a draw.
// We don't need to update frame data because that should
// still be happening.
@ -293,7 +338,16 @@ fn drainMailbox(self: *Thread) !void {
// check the visible state themselves to control their behavior.
},
.focus => |v| {
.focus => |v| focus: {
// If our state didn't change we do nothing.
if (self.flags.focused == v) break :focus;
// Set our state
self.flags.focused = v;
// Focus affects our QoS class
self.setQosClass();
// Set it on the renderer
try self.renderer.setFocus(v);

View File

@ -1955,13 +1955,9 @@ pub fn deleteChars(self: *Terminal, count_req: usize) void {
}
pub fn eraseChars(self: *Terminal, count_req: usize) void {
const count = @max(count_req, 1);
// Our last index is at most the end of the number of chars we have
// in the current line.
const end = end: {
const count = end: {
const remaining = self.cols - self.screen.cursor.x;
var end = @min(remaining, count);
var end = @min(remaining, @max(count_req, 1));
// If our last cell is a wide char then we need to also clear the
// cell beyond it since we can't just split a wide char.
@ -1979,7 +1975,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void {
// protected modes. We need to figure out how to make `clearCells` or at
// least `clearUnprotectedCells` handle boundary conditions...
self.screen.splitCellBoundary(self.screen.cursor.x);
self.screen.splitCellBoundary(end);
self.screen.splitCellBoundary(self.screen.cursor.x + count);
// Reset our row's soft-wrap.
self.screen.cursorResetWrap();
@ -1997,7 +1993,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void {
self.screen.clearCells(
&self.screen.cursor.page_pin.node.data,
self.screen.cursor.page_row,
cells[0..end],
cells[0..count],
);
return;
}
@ -2005,7 +2001,7 @@ pub fn eraseChars(self: *Terminal, count_req: usize) void {
self.screen.clearUnprotectedCells(
&self.screen.cursor.page_pin.node.data,
self.screen.cursor.page_row,
cells[0..end],
cells[0..count],
);
}
@ -6104,6 +6100,36 @@ test "Terminal: eraseChars wide char boundary conditions" {
}
}
test "Terminal: eraseChars wide char splits proper cell boundaries" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 1, .cols = 30 });
defer t.deinit(alloc);
// This is a test for a bug: https://github.com/ghostty-org/ghostty/issues/2817
// To explain the setup:
// (1) We need our wide characters starting on an even (1-based) column.
// (2) We need our cursor to be in the middle somewhere.
// (3) We need our count to be less than our cursor X and on a split cell.
// The bug was that we split the wrong cell boundaries.
try t.printString("x食べて下さい");
{
const str = try t.plainString(alloc);
defer testing.allocator.free(str);
try testing.expectEqualStrings("x食べて下さい", str);
}
t.setCursorPos(1, 6); // At:
t.eraseChars(4); // Delete:
t.screen.cursor.page_pin.node.data.assertIntegrity();
{
const str = try t.plainString(alloc);
defer testing.allocator.free(str);
try testing.expectEqualStrings("x食べ さい", str);
}
}
test "Terminal: eraseChars wide char wrap boundary conditions" {
const alloc = testing.allocator;
var t = try init(alloc, .{ .rows = 3, .cols = 8 });

View File

@ -843,6 +843,7 @@ const Subprocess = struct {
// Don't leak these environment variables to child processes.
if (comptime build_config.app_runtime == .gtk) {
env.remove("GDK_DEBUG");
env.remove("GDK_DISABLE");
env.remove("GSK_RENDERER");
}

View File

@ -200,9 +200,9 @@ pub fn init(self: *Termio, alloc: Allocator, opts: termio.Options) !void {
.default_cursor_style = opts.config.cursor_style,
.default_cursor_blink = opts.config.cursor_blink,
.default_cursor_color = default_cursor_color,
.cursor_color = default_cursor_color,
.foreground_color = opts.config.foreground.toTerminalRGB(),
.background_color = opts.config.background.toTerminalRGB(),
.cursor_color = null,
.foreground_color = null,
.background_color = null,
};
};

View File

@ -78,7 +78,7 @@ pub fn setup(
try setupXdgDataDirs(alloc_arena, resource_dir, env);
break :shell .{
.shell = .elvish,
.command = command,
.command = try alloc_arena.dupe(u8, command),
};
}
@ -86,7 +86,7 @@ pub fn setup(
try setupXdgDataDirs(alloc_arena, resource_dir, env);
break :shell .{
.shell = .fish,
.command = command,
.command = try alloc_arena.dupe(u8, command),
};
}
@ -94,7 +94,7 @@ pub fn setup(
try setupZsh(resource_dir, env);
break :shell .{
.shell = .zsh,
.command = command,
.command = try alloc_arena.dupe(u8, command),
};
}

View File

@ -52,20 +52,20 @@ pub const StreamHandler = struct {
default_cursor_blink: ?bool,
default_cursor_color: ?terminal.color.RGB,
/// Actual cursor color. This can be changed with OSC 12.
/// Actual cursor color. This can be changed with OSC 12. If unset, falls
/// back to the default cursor color.
cursor_color: ?terminal.color.RGB,
/// The default foreground and background color are those set by the user's
/// config file. These can be overridden by terminal applications using OSC
/// 10 and OSC 11, respectively.
/// config file.
default_foreground_color: terminal.color.RGB,
default_background_color: terminal.color.RGB,
/// The actual foreground and background color. Normally this will be the
/// same as the default foreground and background color, unless changed by a
/// terminal application.
foreground_color: terminal.color.RGB,
background_color: terminal.color.RGB,
/// The foreground and background color as set by an OSC 10 or OSC 11
/// sequence. If unset then the respective color falls back to the default
/// value.
foreground_color: ?terminal.color.RGB,
background_color: ?terminal.color.RGB,
/// The response to use for ENQ requests. The memory is owned by
/// whoever owns StreamHandler.
@ -126,6 +126,9 @@ pub const StreamHandler = struct {
if (self.default_cursor) self.setCursorStyle(.default) catch |err| {
log.warn("failed to set default cursor style: {}", .{err});
};
// The config could have changed any of our colors so update mode 2031
self.surfaceMessageWriter(.{ .report_color_scheme = false });
}
inline fn surfaceMessageWriter(
@ -767,7 +770,7 @@ pub const StreamHandler = struct {
self.messageWriter(msg);
},
.color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = {} }),
.color_scheme => self.surfaceMessageWriter(.{ .report_color_scheme = true }),
}
}
@ -892,6 +895,9 @@ pub const StreamHandler = struct {
) !void {
self.terminal.fullReset();
try self.setMouseShape(.text);
// Reset resets our palette so we report it for mode 2031.
self.surfaceMessageWriter(.{ .report_color_scheme = false });
}
pub fn queryKittyKeyboard(self: *StreamHandler) !void {
@ -1191,9 +1197,12 @@ pub const StreamHandler = struct {
const color = switch (kind) {
.palette => |i| self.terminal.color_palette.colors[i],
.foreground => self.foreground_color,
.background => self.background_color,
.cursor => self.cursor_color orelse self.foreground_color,
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse
self.default_cursor_color orelse
self.foreground_color orelse
self.default_foreground_color,
};
var msg: termio.Message = .{ .write_small = .{} };
@ -1336,34 +1345,35 @@ pub const StreamHandler = struct {
}
},
.foreground => {
self.foreground_color = self.default_foreground_color;
self.foreground_color = null;
_ = self.renderer_mailbox.push(.{
.foreground_color = self.foreground_color,
.foreground_color = self.default_foreground_color,
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .foreground,
.color = self.foreground_color,
.color = self.default_foreground_color,
} });
},
.background => {
self.background_color = self.default_background_color;
self.background_color = null;
_ = self.renderer_mailbox.push(.{
.background_color = self.background_color,
.background_color = self.default_background_color,
}, .{ .forever = {} });
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .background,
.color = self.background_color,
.color = self.default_background_color,
} });
},
.cursor => {
self.cursor_color = self.default_cursor_color;
self.cursor_color = null;
_ = self.renderer_mailbox.push(.{
.cursor_color = self.cursor_color,
.cursor_color = self.default_cursor_color,
}, .{ .forever = {} });
if (self.cursor_color) |color| {
if (self.default_cursor_color) |color| {
self.surfaceMessageWriter(.{ .color_change = .{
.kind = .cursor,
.color = color,
@ -1408,7 +1418,7 @@ pub const StreamHandler = struct {
var buf = std.ArrayList(u8).init(self.alloc);
defer buf.deinit();
const writer = buf.writer();
try writer.writeAll("\x1b[21");
try writer.writeAll("\x1b]21");
for (request.list.items) |item| {
switch (item) {
@ -1416,16 +1426,16 @@ pub const StreamHandler = struct {
const color: terminal.color.RGB = switch (key) {
.palette => |palette| self.terminal.color_palette.colors[palette],
.special => |special| switch (special) {
.foreground => self.foreground_color,
.background => self.background_color,
.cursor => self.cursor_color,
.foreground => self.foreground_color orelse self.default_foreground_color,
.background => self.background_color orelse self.default_background_color,
.cursor => self.cursor_color orelse self.default_cursor_color,
else => {
log.warn("ignoring unsupported kitty color protocol key: {}", .{key});
continue;
},
},
} orelse {
log.warn("no color configured for {}", .{key});
try writer.print(";{}=", .{key});
continue;
};
@ -1479,15 +1489,15 @@ pub const StreamHandler = struct {
.special => |special| {
const msg: renderer.Message = switch (special) {
.foreground => msg: {
self.foreground_color = self.default_foreground_color;
self.foreground_color = null;
break :msg .{ .foreground_color = self.default_foreground_color };
},
.background => msg: {
self.background_color = self.default_background_color;
self.background_color = null;
break :msg .{ .background_color = self.default_background_color };
},
.cursor => msg: {
self.cursor_color = self.default_cursor_color;
self.cursor_color = null;
break :msg .{ .cursor_color = self.default_cursor_color };
},
else => {