mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
Merge branch 'main' of github.com:mitchellh/ghostty
This commit is contained in:
@ -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`:
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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 } },
|
||||
|
@ -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,
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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.*;
|
||||
}
|
||||
|
@ -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 = .{};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
Reference in New Issue
Block a user