Merge branch 'main' of github.com:mitchellh/ghostty

This commit is contained in:
Krzysztof Wolicki
2024-01-06 05:42:55 +01:00
15 changed files with 132 additions and 73 deletions

View File

@ -377,6 +377,12 @@ version when I get closer to generally releasing this to ease downstream
packagers. You can find binary releases of nightly builds on the
[Zig downloads page](https://ziglang.org/download/).
Under some conditions, the very latest Zig nightly may not work (for example,
when Zig introduces breaking changes that Ghostty or our dependencies haven't
been upated for). To be sure what Zig version will work, see the `build.zig`
file which has a constant `required_zig`. Ghostty plans to pin to Zig 0.12
once it is released, which will make all of this much easier.
With Zig and necessary dependencies installed, a binary can be built using
`zig build`:

View File

@ -251,6 +251,10 @@ pub fn build(b: *std.Build) !void {
);
}
// Building with LTO on Windows is broken.
// https://github.com/ziglang/zig/issues/15958
if (target.isWindows()) exe.want_lto = false;
// If we're installing, we get the install step so we can add
// additional dependencies to it.
const install_step = if (app_runtime != .none) step: {

View File

@ -57,6 +57,9 @@ class AppDelegate: NSObject,
/// The dock menu
private var dockMenu: NSMenu = NSMenu()
/// This is only true before application has become active.
private var applicationHasBecomeActive: Bool = false
/// The ghostty global state. Only one per process.
let ghostty: Ghostty.AppState = Ghostty.AppState()
@ -104,13 +107,6 @@ class AppDelegate: NSObject,
// Initial config loading
configDidReload(ghostty)
// Let's launch our first window. We only do this if we have no other windows. It
// is possible to have other windows if we're opening a URL since `application(_:openFile:)`
// is called before this.
if (terminalManager.windows.count == 0) {
terminalManager.newWindow()
}
// Register our service provider. This must happen after everything
// else is initialized.
NSApp.servicesProvider = ServiceProvider()
@ -132,6 +128,19 @@ class AppDelegate: NSObject,
center.delegate = self
}
func applicationDidBecomeActive(_ notification: Notification) {
guard !applicationHasBecomeActive else { return }
applicationHasBecomeActive = true
// Let's launch our first window. We only do this if we have no other windows. It
// is possible to have other windows in a few scenarios:
// - if we're opening a URL since `application(_:openFile:)` is called before this.
// - if we're restoring from persisted state
if terminalManager.windows.count == 0 {
terminalManager.newWindow()
}
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return ghostty.shouldQuitAfterLastWindowClosed
}

View File

@ -164,6 +164,23 @@ class TerminalController: NSWindowController, NSWindowDelegate,
// covered in thie GitHub issue: https://github.com/mitchellh/ghostty/pull/376
window.colorSpace = NSColorSpace.sRGB
// If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree {
if let initialSize = leaf.surface.initialSize {
// Setup our frame. We need to first subtract the views frame so that we can
// just get the chrome frame so that we only affect the surface view size.
var frame = window.frame
frame.size.width -= leaf.surface.frame.size.width
frame.size.height -= leaf.surface.frame.size.height
frame.size.width += initialSize.width
frame.size.height += initialSize.height
// We have no tabs and we are not a split, so set the initial size of the window.
window.setFrame(frame, display: true)
}
}
// Center the window to start, we'll move the window frame automatically
// when cascading.
window.center()

View File

@ -33,8 +33,8 @@ class TerminalManager {
}
}
// If we have no main window, just use the first window.
return windows.first
// If we have no main window, just use the last window.
return windows.last
}
init(_ ghostty: Ghostty.AppState) {

View File

@ -534,9 +534,6 @@ extension Ghostty {
override func viewDidMoveToWindow() {
// Set our background blur if requested
setWindowBackgroundBlur(window)
// Try to set the initial window size if we have one
setInitialWindowSize()
}
/// This function sets the window background to blur if it is configured on the surface.
@ -567,37 +564,6 @@ extension Ghostty {
// If we have a blur, set the blur
ghostty_set_window_background_blur(surface, Unmanaged.passUnretained(window).toOpaque())
}
/// Sets the initial window size requested by the Ghostty config.
///
/// This only works under certain conditions:
/// - The window must be "uninitialized"
/// - The window must have no tabs
/// - Ghostty must have requested an initial size
///
private func setInitialWindowSize() {
guard let initialSize = initialSize else { return }
// If we have tabs, then do not change the window size
guard let window = self.window else { return }
guard let windowControllerRaw = window.windowController else { return }
guard let windowController = windowControllerRaw as? TerminalController else { return }
guard case .leaf = windowController.surfaceTree else { return }
// If our window is full screen, we do not set the frame
guard !window.styleMask.contains(.fullScreen) else { return }
// Setup our frame. We need to first subtract the views frame so that we can
// just get the chrome frame so that we only affect the surface view size.
var frame = window.frame
frame.size.width -= self.frame.size.width
frame.size.height -= self.frame.size.height
frame.size.width += initialSize.width
frame.size.height += initialSize.height
// We have no tabs and we are not a split, so set the initial size of the window.
window.setFrame(frame, display: true)
}
override func becomeFirstResponder() -> Bool {
let result = super.becomeFirstResponder()

View File

@ -293,6 +293,7 @@ fn updateConfigErrors(self: *App) !void {
fn syncActionAccelerators(self: *App) !void {
try self.syncActionAccelerator("app.quit", .{ .quit = {} });
try self.syncActionAccelerator("app.open_config", .{ .open_config = {} });
try self.syncActionAccelerator("app.reload_config", .{ .reload_config = {} });
try self.syncActionAccelerator("app.toggle_inspector", .{ .inspector = .toggle });
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
@ -479,6 +480,17 @@ fn gtkActivate(app: *c.GtkApplication, ud: ?*anyopaque) callconv(.C) void {
}, .{ .forever = {} });
}
fn gtkActionOpenConfig(
_: *c.GSimpleAction,
_: *c.GVariant,
ud: ?*anyopaque,
) callconv(.C) void {
const self: *App = @ptrCast(@alignCast(ud orelse return));
_ = self.core_app.mailbox.push(.{
.open_config = {},
}, .{ .forever = {} });
}
fn gtkActionReloadConfig(
_: *c.GSimpleAction,
_: *c.GVariant,
@ -507,6 +519,7 @@ fn gtkActionQuit(
fn initActions(self: *App) void {
const actions = .{
.{ "quit", &gtkActionQuit },
.{ "open_config", &gtkActionOpenConfig },
.{ "reload_config", &gtkActionReloadConfig },
};
@ -545,6 +558,7 @@ fn initMenu(self: *App) void {
defer c.g_object_unref(section);
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
c.g_menu_append(section, "Terminal Inspector", "win.toggle_inspector");
c.g_menu_append(section, "Open Configuration", "app.open_config");
c.g_menu_append(section, "Reload Configuration", "app.reload_config");
c.g_menu_append(section, "About Ghostty", "win.about");
}

View File

@ -712,8 +712,8 @@ pub fn LineIterator(comptime ReaderType: type) type {
unreachable;
} orelse return null;
// Trim any whitespace around it
const trim = std.mem.trim(u8, entry, whitespace);
// Trim any whitespace (including CR) around it
const trim = std.mem.trim(u8, entry, whitespace ++ "\r");
if (trim.len != entry.len) {
std.mem.copyForwards(u8, entry, trim);
entry = entry[0..trim.len];
@ -833,3 +833,14 @@ test "LineIterator spaces around '='" {
try testing.expectEqual(@as(?[]const u8, null), iter.next());
try testing.expectEqual(@as(?[]const u8, null), iter.next());
}
test "LineIterator with CRLF line endings" {
const testing = std.testing;
var fbs = std.io.fixedBufferStream("A\r\nB = C\r\n");
var iter = lineIterator(fbs.reader());
try testing.expectEqualStrings("--A", iter.next().?);
try testing.expectEqualStrings("--B=C", iter.next().?);
try testing.expectEqual(@as(?[]const u8, null), iter.next());
try testing.expectEqual(@as(?[]const u8, null), iter.next());
}

View File

@ -946,11 +946,18 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
const alloc = result._arena.?.allocator();
// Add our default keybindings
// keybinds for opening and reloading config
try result.keybind.set.put(
alloc,
.{ .key = .space, .mods = .{ .super = true, .alt = true, .ctrl = true } },
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .reload_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = inputpkg.ctrlOrSuper(.{}) },
.{ .open_config = {} },
);
{
// On macOS we default to super but Linux ctrl+shift since
@ -1210,16 +1217,6 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .key = .q, .mods = .{ .super = true } },
.{ .quit = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = .{ .super = true, .shift = true } },
.{ .reload_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .comma, .mods = .{ .super = true } },
.{ .open_config = {} },
);
try result.keybind.set.put(
alloc,
.{ .key = .k, .mods = .{ .super = true } },

View File

@ -9,6 +9,11 @@ pub fn open(alloc_gpa: Allocator) !void {
const config_path = try internal_os.xdg.config(alloc_gpa, .{ .subdir = "ghostty/config" });
defer alloc_gpa.free(config_path);
// Create config directory recursively.
if (std.fs.path.dirname(config_path)) |config_dir| {
try std.fs.cwd().makePath(config_dir);
}
// Try to create file and go on if it already exists
_ = std.fs.createFileAbsolute(
config_path,

View File

@ -23,6 +23,7 @@ pub fn disableDefaultFontFeatures(face: *const font.Face) bool {
// looks really bad in terminal grids, so we want to disable ligatures
// by default for these faces.
return std.mem.eql(u8, name, "CodeNewRoman") or
std.mem.eql(u8, name, "CodeNewRoman Nerd Font") or
std.mem.eql(u8, name, "Menlo") or
std.mem.eql(u8, name, "Monaco");
}

View File

@ -1305,6 +1305,11 @@ pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
// Set our new minimum contrast
self.uniforms.min_contrast = config.min_contrast;
// Set our new colors
self.background_color = config.background;
self.foreground_color = config.foreground;
self.cursor_color = config.cursor_color;
self.config.deinit();
self.config = config.*;
}

View File

@ -1658,6 +1658,11 @@ pub fn changeConfig(self: *OpenGL, config: *DerivedConfig) !void {
self.font_shaper = font_shaper;
}
// Set our new colors
self.background_color = config.background;
self.foreground_color = config.foreground;
self.cursor_color = config.cursor_color;
// Update our uniforms
self.deferred_config = .{};

View File

@ -1090,14 +1090,22 @@ const Subprocess = struct {
break :args try args.toOwnedSlice();
}
// We run our shell wrapped in `/bin/sh` so that we don't have
// to parse the commadnd line ourselves if it has arguments.
// Additionally, some environments (NixOS, I found) use /bin/sh
// to setup some environment variables that are important to
// have set.
try args.append("/bin/sh");
if (internal_os.isFlatpak()) try args.append("-l");
try args.append("-c");
if (comptime builtin.os.tag == .windows) {
// We run our shell wrapped in `cmd.exe` so that we don't have
// to parse the command line ourselves if it has arguments.
try args.append("C:\\Windows\\System32\\cmd.exe");
try args.append("/C");
} else {
// We run our shell wrapped in `/bin/sh` so that we don't have
// to parse the command line ourselves if it has arguments.
// Additionally, some environments (NixOS, I found) use /bin/sh
// to setup some environment variables that are important to
// have set.
try args.append("/bin/sh");
if (internal_os.isFlatpak()) try args.append("-l");
try args.append("-c");
}
try args.append(opts.full_config.command orelse default_path);
break :args try args.toOwnedSlice();
};
@ -1129,6 +1137,7 @@ const Subprocess = struct {
const dir = opts.resources_dir orelse break :shell null;
break :shell try shell_integration.setup(
gpa,
dir,
path,
&env,

View File

@ -1,4 +1,5 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const EnvMap = std.process.EnvMap;
const config = @import("../config.zig");
@ -14,7 +15,13 @@ pub const Shell = enum {
/// integrated shell integration. This returns true if shell
/// integration was successful. False could mean many things:
/// the shell type wasn't detected, etc.
///
/// The allocator is only used for temporary values, so it should
/// be given a general purpose allocator. No allocated memory remains
/// after this function returns except anything allocated by the
/// EnvMap.
pub fn setup(
alloc: Allocator,
resource_dir: []const u8,
command_path: []const u8,
env: *EnvMap,
@ -28,7 +35,7 @@ pub fn setup(
const shell: Shell = shell: {
if (std.mem.eql(u8, "fish", exe)) {
try setupFish(resource_dir, env);
try setupFish(alloc, resource_dir, env);
break :shell .fish;
}
@ -51,6 +58,7 @@ pub fn setup(
/// Fish will automatically load configuration in XDG_DATA_DIRS
/// "fish/vendor_conf.d/*.fish".
fn setupFish(
alloc_gpa: Allocator,
resource_dir: []const u8,
env: *EnvMap,
) !void {
@ -71,17 +79,19 @@ fn setupFish(
if (env.get("XDG_DATA_DIRS")) |old| {
// We have an old value, We need to prepend our value to it.
// We use a 4K buffer to hold our XDG_DATA_DIR value. The stack
// on macOS is at least 512K and Linux is 8MB or more. So this
// should fit. If the user has a XDG_DATA_DIR value that is longer
// than this then it will fail... and we will cross that bridge
// when we actually get there. This avoids us needing an allocator.
var buf: [4096]u8 = undefined;
const prepended = try std.fmt.bufPrint(
&buf,
// We attempt to avoid allocating by using the stack up to 4K.
// Max stack size is considerably larger on macOS and Linux but
// 4K is a reasonable size for this for most cases. However, env
// vars can be significantly larger so if we have to we fall
// back to a heap allocated value.
var stack_alloc = std.heap.stackFallback(4096, alloc_gpa);
const alloc = stack_alloc.get();
const prepended = try std.fmt.allocPrint(
alloc,
"{s}{c}{s}",
.{ integ_dir, std.fs.path.delimiter, old },
);
defer alloc.free(prepended);
try env.put("XDG_DATA_DIRS", prepended);
} else {