diff --git a/README.md b/README.md index 8518fbba7..8719a7bff 100644 --- a/README.md +++ b/README.md @@ -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`: diff --git a/build.zig b/build.zig index 0542c72e9..1e9d891ef 100644 --- a/build.zig +++ b/build.zig @@ -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: { diff --git a/macos/Sources/AppDelegate.swift b/macos/Sources/AppDelegate.swift index 1b6e5bd29..c582b4628 100644 --- a/macos/Sources/AppDelegate.swift +++ b/macos/Sources/AppDelegate.swift @@ -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 } diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 02cd2de9c..a96e56eb8 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -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() diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index 745bfdf24..65ba5b03a 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -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) { diff --git a/macos/Sources/Ghostty/SurfaceView.swift b/macos/Sources/Ghostty/SurfaceView.swift index 11ddb6dd5..bd4a2b0a5 100644 --- a/macos/Sources/Ghostty/SurfaceView.swift +++ b/macos/Sources/Ghostty/SurfaceView.swift @@ -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() diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 3d30d9ba5..46a982348 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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", >kActionQuit }, + .{ "open_config", >kActionOpenConfig }, .{ "reload_config", >kActionReloadConfig }, }; @@ -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"); } diff --git a/src/cli/args.zig b/src/cli/args.zig index 6444400a7..c226493f9 100644 --- a/src/cli/args.zig +++ b/src/cli/args.zig @@ -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()); +} diff --git a/src/config/Config.zig b/src/config/Config.zig index 5b816c676..025d09412 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -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 } }, diff --git a/src/config/edit.zig b/src/config/edit.zig index c673991df..38d9f2b7f 100644 --- a/src/config/edit.zig +++ b/src/config/edit.zig @@ -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, diff --git a/src/quirks.zig b/src/quirks.zig index a43001e5a..e521eeb56 100644 --- a/src/quirks.zig +++ b/src/quirks.zig @@ -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"); } diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index 00104ffa8..81518c6e6 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -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.*; } diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index de7b3ffbf..aea6991df 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -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 = .{}; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index be8a6678f..a694ebc19 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -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, diff --git a/src/termio/shell_integration.zig b/src/termio/shell_integration.zig index 08733f6ee..296c95db6 100644 --- a/src/termio/shell_integration.zig +++ b/src/termio/shell_integration.zig @@ -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 {