diff --git a/macos/Sources/Features/Terminal/TerminalManager.swift b/macos/Sources/Features/Terminal/TerminalManager.swift index e105a3711..fc4183864 100644 --- a/macos/Sources/Features/Terminal/TerminalManager.swift +++ b/macos/Sources/Features/Terminal/TerminalManager.swift @@ -67,6 +67,11 @@ class TerminalManager { Self.lastCascadePoint = window.cascadeTopLeft(from: Self.lastCascadePoint) } + if (ghostty.windowFullscreen) { + // NOTE: this doesn't properly handle non-native fullscreen yet + c.window?.toggleFullScreen(nil) + } + c.showWindow(self) } diff --git a/macos/Sources/Ghostty/AppState.swift b/macos/Sources/Ghostty/AppState.swift index 1cac36ae2..f54c80ca3 100644 --- a/macos/Sources/Ghostty/AppState.swift +++ b/macos/Sources/Ghostty/AppState.swift @@ -105,6 +105,15 @@ extension Ghostty { _ = ghostty_config_get(config, &v, key, UInt(key.count)) return v } + + /// Whether to open new windows in fullscreen. + var windowFullscreen: Bool { + guard let config = self.config else { return true } + var v = false + let key = "fullscreen" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return v + } /// The background opacity. var backgroundOpacity: Double { diff --git a/src/Surface.zig b/src/Surface.zig index 80e8db79c..903737a98 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -155,6 +155,7 @@ const DerivedConfig = struct { window_padding_x: u32, window_padding_y: u32, window_padding_balance: bool, + title: ?[:0]const u8, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); @@ -180,6 +181,7 @@ const DerivedConfig = struct { .window_padding_x = config.@"window-padding-x", .window_padding_y = config.@"window-padding-y", .window_padding_balance = config.@"window-padding-balance", + .title = config.title, // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -544,6 +546,8 @@ pub fn init( log.warn("unable to set initial window size: {s}", .{err}); }; } + + if (config.title) |title| try rt_surface.setTitle(title); } pub fn deinit(self: *Surface) void { @@ -663,6 +667,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .change_config => |config| try self.changeConfig(config), .set_title => |*v| { + // We ignore the message in case the title was set via config. + if (self.config.title != null) { + log.debug("ignoring title change request since static title is set via config", .{}); + return; + } + // The ptrCast just gets sliceTo to return the proper type. // We know that our title should end in 0. const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 68a2d1d0d..2456d2ada 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -28,6 +28,7 @@ const UnsafePasteWindow = @import("UnsafePasteWindow.zig"); const c = @import("c.zig"); const inspector = @import("inspector.zig"); const key = @import("key.zig"); +const testing = std.testing; const log = std.log.scoped(.gtk); @@ -103,9 +104,17 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { // Our app ID determines uniqueness and maps to our desktop file. // We append "-debug" to the ID if we're in debug mode so that we // can develop Ghostty in Ghostty. - const app_id: [:0]const u8 = comptime app_id: { - var id = "com.mitchellh.ghostty"; - break :app_id if (builtin.mode == .Debug) id ++ "-debug" else id; + const app_id: [:0]const u8 = app_id: { + if (config.class) |class| { + if (isValidAppId(class)) { + break :app_id class; + } else { + log.warn("invalid 'class' in config, ignoring", .{}); + } + } + + const default_id = "com.mitchellh.ghostty"; + break :app_id if (builtin.mode == .Debug) default_id ++ "-debug" else default_id; }; // Create our GTK Application which encapsulates our process. @@ -159,7 +168,6 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .config = config, .ctx = ctx, .cursor_none = cursor_none, - // If we are NOT the primary instance, then we never want to run. // This means that another instance of the GTK app is running and // our "activate" call above will open a window. @@ -490,3 +498,32 @@ fn initMenu(self: *App) void { self.menu = menu; } + +fn isValidAppId(app_id: [:0]const u8) bool { + if (app_id.len > 255 or app_id.len == 0) return false; + if (app_id[0] == '.') return false; + if (app_id[app_id.len - 1] == '.') return false; + + var hasDot = false; + for (app_id) |char| { + switch (char) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {}, + '.' => hasDot = true, + else => return false, + } + } + if (!hasDot) return false; + + return true; +} + +test "isValidAppId" { + try testing.expect(isValidAppId("foo.bar")); + try testing.expect(isValidAppId("foo.bar.baz")); + try testing.expect(!isValidAppId("foo")); + try testing.expect(!isValidAppId("foo.bar?")); + try testing.expect(!isValidAppId("foo.")); + try testing.expect(!isValidAppId(".foo")); + try testing.expect(!isValidAppId("")); + try testing.expect(!isValidAppId("foo" ** 86)); +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 1103806c3..9a36f826f 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -123,6 +123,9 @@ pub fn init(self: *Window, app: *App) !void { } c.gtk_box_append(@ptrCast(box), notebook_widget); + // If we are in fullscreen mode, new windows start fullscreen. + if (app.config.fullscreen) c.gtk_window_fullscreen(self.window); + // All of our events _ = c.g_signal_connect_data(window, "close-request", c.G_CALLBACK(>kCloseRequest), self, null, c.G_CONNECT_DEFAULT); _ = c.g_signal_connect_data(window, "destroy", c.G_CALLBACK(>kDestroy), self, null, c.G_CONNECT_DEFAULT); diff --git a/src/config/Config.zig b/src/config/Config.zig index b7db59ae4..814c621ea 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -278,6 +278,30 @@ command: ?[]const u8 = null, /// indicate that it is a login shell, depending on the OS). @"command-arg": RepeatableString = .{}, +/// Start new windows in fullscreen. This setting applies to new +/// windows and does not apply to tabs, splits, etc. However, this +/// setting will apply to all new windows, not just the first one. +/// +/// On macOS, this always creates the window in native fullscreen. +/// Non-native fullscreen is not currently supported with this +/// setting. +fullscreen: bool = false, + +/// The title Ghostty will use for the window. This will force the title +/// of the window to be this title at all times and Ghostty will ignore any +/// set title escape sequences programs (such as Neovim) may send. +title: ?[:0]const u8 = null, + +/// The setting that will change the application class value. This value is +/// often used with Linux window managers to change behavior (such as +/// floating vs tiled). If you don't know what this is, don't set it. +/// +/// The class name must follow the GTK requirements defined here: +/// https://docs.gtk.org/gio/type_func.Application.id_is_valid.html +/// +/// This only affects GTK builds. +class: ?[:0]const u8 = null, + /// The directory to change to after starting the command. /// /// This setting is secondary to the "window-inherit-working-directory"