mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
Merge branch 'ghostty-org:main' into dolphin-action
This commit is contained in:
39
README.md
39
README.md
@ -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`
|
||||
|
@ -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
|
||||
|
@ -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,7 +843,13 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
}, .unlocked);
|
||||
},
|
||||
|
||||
.color_change => |change| try self.rt_app.performAction(
|
||||
.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,
|
||||
.{
|
||||
@ -851,7 +863,8 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
|
||||
.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 {
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
>kNotifyColorScheme,
|
||||
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,19 +804,39 @@ 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 => {},
|
||||
|
||||
.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});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reloadConfig(
|
||||
@ -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,
|
||||
>kNotifyColorScheme,
|
||||
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});
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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(>kDestroy), self, null, c.G_CONNECT_DEFAULT);
|
||||
|
||||
// Set some state
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
} };
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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" {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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
118
src/os/macos.zig
Normal 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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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;
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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 });
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 => {
|
||||
|
Reference in New Issue
Block a user