From 9e9ad67d3c39175b770a01dde7d548592a0713b7 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 11:49:25 +0000 Subject: [PATCH 01/18] feat: add support for fullscreen, title and class values --- src/Surface.zig | 32 +++++++++++++++++--------------- src/apprt/gtk/App.zig | 8 +++----- src/config/Config.zig | 14 ++++++++++++++ 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 0e4bb47a4..b82de8875 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -154,6 +154,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); @@ -178,6 +179,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. @@ -542,6 +544,10 @@ pub fn init( log.warn("unable to set initial window size: {s}", .{err}); }; } + + if (config.fullscreen) { + rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); + } } pub fn deinit(self: *Surface) void { @@ -663,9 +669,13 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .set_title => |*v| { // 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); - log.debug("changing title \"{s}\"", .{slice}); - try self.rt_surface.setTitle(slice); + if (self.config.title) |title| { + try self.rt_surface.setTitle(title); + } else { + const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); + log.debug("changing title \"{s}\"", .{slice}); + try self.rt_surface.setTitle(slice); + } }, .set_mouse_shape => |shape| { @@ -676,12 +686,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .cell_size => |size| try self.setCellSize(size), .clipboard_read => |kind| { + _ = kind; if (!self.config.clipboard_read) { log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); return; } - - try self.startClipboardRequest(.standard, .{ .osc_52 = kind }); }, .clipboard_write => |req| switch (req) { @@ -1857,8 +1866,7 @@ pub fn mouseButtonCallback( .clipboard => .standard, .false => unreachable, }; - - try self.startClipboardRequest(clipboard, .{ .paste = {} }); + _ = clipboard; } } } @@ -1950,7 +1958,6 @@ fn dragLeftClickDouble( // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectWord(screen_point) orelse return; - // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { @@ -1959,9 +1966,7 @@ fn dragLeftClickDouble( }; // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) { - sel.start = word.start; - } else { + if (screen_point.before(self.mouse.left_click_point)) {} else { sel.end = word.end; } self.setSelection(sel); @@ -1975,7 +1980,6 @@ fn dragLeftClickTriple( // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectLine(screen_point) orelse return; - // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { @@ -1984,9 +1988,7 @@ fn dragLeftClickTriple( }; // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) { - sel.start = word.start; - } else { + if (screen_point.before(self.mouse.left_click_point)) {} else { sel.end = word.end; } self.setSelection(sel); diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 68a2d1d0d..cbda211ab 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -103,11 +103,10 @@ 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: { + var id = config.class orelse "com.mitchellh.ghostty"; + break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else id; }; - // Create our GTK Application which encapsulates our process. log.debug("creating GTK application id={s} single-instance={}", .{ app_id, @@ -159,7 +158,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. diff --git a/src/config/Config.zig b/src/config/Config.zig index fbb92e5ba..8e42e7daa 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -278,6 +278,20 @@ command: ?[]const u8 = null, /// indicate that it is a login shell, depending on the OS). @"command-arg": RepeatableString = .{}, +/// The setting to tell Ghostty to start in fullscreen mode. +/// By default with will start windowed. +fullscreen: bool = false, + +/// The setting that will change the application class value. +/// This is usefull if you want to have multiple instances of Ghostty +/// running with separate classes making it possible to have unique behavior for each of them. +class: ?[:0]const u8 = null, + +/// The setting that will tell Ghostty which title to display. +/// By default Ghostty will output the current directory or what application is running as the title, +/// but with this setting it will force Ghostty to output that title independent of what is happening in terminal. +title: ?[:0]const u8 = null, + /// The directory to change to after starting the command. /// /// This setting is secondary to the "window-inherit-working-directory" From 17c6496855ada7b900cc8e7f822953644742a91b Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 11:58:44 +0000 Subject: [PATCH 02/18] fix: match main --- src/Surface.zig | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index b82de8875..1cf536d3f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -686,11 +686,12 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .cell_size => |size| try self.setCellSize(size), .clipboard_read => |kind| { - _ = kind; if (!self.config.clipboard_read) { log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); return; } + + try self.startClipboardRequest(.standard, .{ .osc_52 = kind }); }, .clipboard_write => |req| switch (req) { @@ -1866,7 +1867,8 @@ pub fn mouseButtonCallback( .clipboard => .standard, .false => unreachable, }; - _ = clipboard; + + try self.startClipboardRequest(clipboard, .{ .paste = {} }); } } } @@ -1966,7 +1968,9 @@ fn dragLeftClickDouble( }; // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) {} else { + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { sel.end = word.end; } self.setSelection(sel); @@ -1980,6 +1984,7 @@ fn dragLeftClickTriple( // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectLine(screen_point) orelse return; + // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { @@ -1988,7 +1993,9 @@ fn dragLeftClickTriple( }; // Grow our selection - if (screen_point.before(self.mouse.left_click_point)) {} else { + if (screen_point.before(self.mouse.left_click_point)) { + sel.start = word.start; + } else { sel.end = word.end; } self.setSelection(sel); From d45415c8bf044d8561cbf368138b822610bf85ad Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:01:11 +0000 Subject: [PATCH 03/18] more match fixes --- src/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Surface.zig b/src/Surface.zig index 1cf536d3f..a6cf70087 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -1960,6 +1960,7 @@ fn dragLeftClickDouble( // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectWord(screen_point) orelse return; + // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { From aa62d78fc2a49ba2bd7246dd29173ffb2b7f3165 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 12:03:08 +0000 Subject: [PATCH 04/18] chore: add log to setTitle --- src/Surface.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Surface.zig b/src/Surface.zig index a6cf70087..096b89a22 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -670,6 +670,7 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { // The ptrCast just gets sliceTo to return the proper type. // We know that our title should end in 0. if (self.config.title) |title| { + log.debug("setting title \"{s}\"", .{title}); try self.rt_surface.setTitle(title); } else { const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); From dff3ce6eb56cd297d105e79b3a6f1bc3f1159314 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 13:26:51 +0000 Subject: [PATCH 05/18] chore: add targets --- src/Surface.zig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 096b89a22..521fc22e5 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -546,7 +546,9 @@ pub fn init( } if (config.fullscreen) { - rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); + if (builtin.target.isDarwin() or builtin.os.tag == .linux) { + rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); + } } } From 19afbb90a7d41ae3f2d74fb49010c7b6cd7d258d Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 14:06:33 +0000 Subject: [PATCH 06/18] fix: glfw build --- src/Surface.zig | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index 521fc22e5..3b0319af7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -17,6 +17,7 @@ pub const Message = apprt.surface.Message; const std = @import("std"); const builtin = @import("builtin"); +const build_config = @import("build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; @@ -546,7 +547,7 @@ pub fn init( } if (config.fullscreen) { - if (builtin.target.isDarwin() or builtin.os.tag == .linux) { + if (build_config.app_runtime == .gtk or build_config.app_runtime == .none) { rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); } } From 467b840bcf19435023c99678be3a0c88089b3184 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Sun, 5 Nov 2023 15:19:29 +0000 Subject: [PATCH 07/18] glfw: send warning for fullscreen --- src/Surface.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 3b0319af7..24810d933 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -547,9 +547,9 @@ pub fn init( } if (config.fullscreen) { - if (build_config.app_runtime == .gtk or build_config.app_runtime == .none) { + if (@hasDecl(apprt.Surface, "toggleFullscreen")) { rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); - } + } else log.warn("runtime doesn't implement toggleFullscreen", .{}); } } From 094f8effa3d5504692e6e89462eb2bb2ede1e7ec Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:19:09 +0000 Subject: [PATCH 08/18] fix: begin implementation of suggested changes --- src/Surface.zig | 21 +++++----- src/apprt/gtk/App.zig | 82 +++++++++++++++++++++++++++++++++++++++- src/apprt/gtk/Window.zig | 4 ++ 3 files changed, 93 insertions(+), 14 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 24810d933..a450051c0 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -546,10 +546,8 @@ pub fn init( }; } - if (config.fullscreen) { - if (@hasDecl(apprt.Surface, "toggleFullscreen")) { - rt_surface.toggleFullscreen(config.@"macos-non-native-fullscreen"); - } else log.warn("runtime doesn't implement toggleFullscreen", .{}); + if (config.title) |title| { + try self.rt_surface.setTitle(title); } } @@ -670,16 +668,15 @@ 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) |_| { + return; + } // The ptrCast just gets sliceTo to return the proper type. // We know that our title should end in 0. - if (self.config.title) |title| { - log.debug("setting title \"{s}\"", .{title}); - try self.rt_surface.setTitle(title); - } else { - const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); - log.debug("changing title \"{s}\"", .{slice}); - try self.rt_surface.setTitle(slice); - } + const slice = std.mem.sliceTo(@as([*:0]const u8, @ptrCast(v)), 0); + log.debug("changing title \"{s}\"", .{slice}); + try self.rt_surface.setTitle(slice); }, .set_mouse_shape => |shape| { diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index cbda211ab..7c830aef9 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,10 +104,36 @@ 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 default_id = "com.mitchellh.ghostty"; const app_id: [:0]const u8 = app_id: { - var id = config.class orelse "com.mitchellh.ghostty"; - break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else id; + if (config.class) |class| { + break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else isValidGtkId(class) catch |err| switch (err) { + error.InvalidChar => { + log.warn("Invalid char found in class. Setting app id to default value", .{}); + break :app_id default_id; + }, + error.InvalidLength => { + log.warn("Class name value is over 255 chars in length. Setting app id to default value", .{}); + break :app_id default_id; + }, + error.NoDotInId => { + log.warn("Class name has no dot char. Setting app id to default value", .{}); + break :app_id default_id; + }, + error.StartWithDot => { + log.warn("Class name start with dot. Setting app id to default value", .{}); + break :app_id default_id; + }, + error.EndsWithDot => { + log.warn("Class name ends with dot. Setting app id to default value", .{}); + break :app_id default_id; + }, + }; + } else { + break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else default_id; + } }; + // Create our GTK Application which encapsulates our process. log.debug("creating GTK application id={s} single-instance={}", .{ app_id, @@ -488,3 +515,54 @@ fn initMenu(self: *App) void { self.menu = menu; } + +fn isValidGtkId(app_id: [:0]const u8) ![:0]const u8 { + var position: u8 = 0; + var hasDot = false; + + if (app_id.len > 255 or app_id.len == 0) + return error.InvalidLength; + + if (app_id[position] == '.') + return error.StartWithDot; + + if (app_id[app_id.len - 1] == '.') + return error.EndsWithDot; + + while (true) { + const char = app_id[position]; + + switch (char) { + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => { + position += 1; + continue; + }, + '.' => { + hasDot = true; + position += 1; + continue; + }, + 0 => { + if (position != app_id.len) return error.InvalidChar; + break; + }, + else => return error.InvalidChar, + } + } + + if (!hasDot) return error.NoDotInId; + + return app_id; +} + +test "ValidGtkClassName" { + try testing.expectEqualStrings("foo.bar", try isValidGtkId("foo.bar")); + try testing.expectEqualStrings("foo.bar.baz", try isValidGtkId("foo.bar.baz")); + + try testing.expectError(error.NoDotInId, isValidGtkId("foo")); + try testing.expectError(error.InvalidChar, isValidGtkId("foo.bar?")); + try testing.expectError(error.EndsWithDot, isValidGtkId("foo.")); + try testing.expectError(error.StartWithDot, isValidGtkId(".foo")); + try testing.expectError(error.InvalidLength, isValidGtkId("")); + try testing.expectError(error.InvalidLength, isValidGtkId("foo" ** 86)); +} diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index 1103806c3..b4a4327c9 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -123,6 +123,10 @@ pub fn init(self: *Window, app: *App) !void { } c.gtk_box_append(@ptrCast(box), notebook_widget); + 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); From 8d526112a121f91c0df8777cf0a0cb4fabb7268a Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:20:52 +0000 Subject: [PATCH 09/18] chore: update comment --- src/config/Config.zig | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 8e42e7daa..592b61ab3 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -285,6 +285,7 @@ fullscreen: bool = false, /// The setting that will change the application class value. /// This is usefull if you want to have multiple instances of Ghostty /// running with separate classes making it possible to have unique behavior for each of them. +/// This is currently only supported on Gtk builds. class: ?[:0]const u8 = null, /// The setting that will tell Ghostty which title to display. From 7fa712ab2bbabf44f8b4a7545e4ccca39d396efa Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:32:25 +0000 Subject: [PATCH 10/18] chore: more comment changes --- src/Surface.zig | 1 - src/config/Config.zig | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Surface.zig b/src/Surface.zig index a450051c0..5396c2068 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -17,7 +17,6 @@ pub const Message = apprt.surface.Message; const std = @import("std"); const builtin = @import("builtin"); -const build_config = @import("build_config.zig"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; diff --git a/src/config/Config.zig b/src/config/Config.zig index 592b61ab3..fb30d94bb 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -286,6 +286,7 @@ fullscreen: bool = false, /// This is usefull if you want to have multiple instances of Ghostty /// running with separate classes making it possible to have unique behavior for each of them. /// This is currently only supported on Gtk builds. +/// The class name must follow the GTK requirements defined here: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html class: ?[:0]const u8 = null, /// The setting that will tell Ghostty which title to display. From 232527c9dc17f3d543c9cc45d741168c1e76aea8 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 11:59:16 +0000 Subject: [PATCH 11/18] fix: fullscreen on new windows --- src/Surface.zig | 2 ++ src/apprt/gtk/App.zig | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/src/Surface.zig b/src/Surface.zig index 5396c2068..60f29ccc6 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -155,6 +155,7 @@ const DerivedConfig = struct { window_padding_y: u32, window_padding_balance: bool, title: ?[:0]const u8, + fullscreen: bool, 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_y = config.@"window-padding-y", .window_padding_balance = config.@"window-padding-balance", .title = config.title, + .fullscreen = config.fullscreen, // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 7c830aef9..72cf1a730 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -325,6 +325,15 @@ pub fn redrawInspector(self: *App, surface: *Surface) void { pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { const alloc = self.core_app.alloc; + // If we are in fullscreen mode and have a parent surface we force the disable of this setting. + // This prevents that new windows get created in fullscreen mode. + // This also prevents that the settings always gets set to false everytime a new window is created. + if (self.config.fullscreen) { + if (parent_) |_| { + self.config.fullscreen = false; + } + } + // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal // compared to the steady-state terminal operation so we use heap From 60717cde14cd7ddc2ca2904022ed10b323b5066c Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:03:44 +0000 Subject: [PATCH 12/18] chore: update fullscreen implementation --- src/apprt/gtk/App.zig | 11 +---------- src/apprt/gtk/Window.zig | 3 +++ src/config/Config.zig | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 72cf1a730..a23d8ed22 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -113,7 +113,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { break :app_id default_id; }, error.InvalidLength => { - log.warn("Class name value is over 255 chars in length. Setting app id to default value", .{}); + log.warn("Class name value doesn't have valid length (> 0 or 255 <=). Setting app id to default value", .{}); break :app_id default_id; }, error.NoDotInId => { @@ -325,15 +325,6 @@ pub fn redrawInspector(self: *App, surface: *Surface) void { pub fn newWindow(self: *App, parent_: ?*CoreSurface) !void { const alloc = self.core_app.alloc; - // If we are in fullscreen mode and have a parent surface we force the disable of this setting. - // This prevents that new windows get created in fullscreen mode. - // This also prevents that the settings always gets set to false everytime a new window is created. - if (self.config.fullscreen) { - if (parent_) |_| { - self.config.fullscreen = false; - } - } - // Allocate a fixed pointer for our window. We try to minimize // allocations but windows and other GUI requirements are so minimal // compared to the steady-state terminal operation so we use heap diff --git a/src/apprt/gtk/Window.zig b/src/apprt/gtk/Window.zig index b4a4327c9..28f3c9f6d 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -125,6 +125,9 @@ pub fn init(self: *Window, app: *App) !void { if (app.config.fullscreen) { c.gtk_window_fullscreen(self.window); + + // We disable this because we just want to start the first window in fullscreen. + app.config.fullscreen = false; } // All of our events diff --git a/src/config/Config.zig b/src/config/Config.zig index fb30d94bb..644f665a5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -291,7 +291,7 @@ class: ?[:0]const u8 = null, /// The setting that will tell Ghostty which title to display. /// By default Ghostty will output the current directory or what application is running as the title, -/// but with this setting it will force Ghostty to output that title independent of what is happening in terminal. +/// but with this setting it will force Ghostty to output that title independent of what is happening in the terminal. title: ?[:0]const u8 = null, /// The directory to change to after starting the command. From d2d5d4ba82eeab5c3227b18b5898f08ef0819551 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:27:36 +0000 Subject: [PATCH 13/18] chore: cleanup --- src/Surface.zig | 2 -- src/apprt/embedded.zig | 2 ++ src/config/Config.zig | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 60f29ccc6..5396c2068 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -155,7 +155,6 @@ const DerivedConfig = struct { window_padding_y: u32, window_padding_balance: bool, title: ?[:0]const u8, - fullscreen: bool, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); @@ -181,7 +180,6 @@ const DerivedConfig = struct { .window_padding_y = config.@"window-padding-y", .window_padding_balance = config.@"window-padding-balance", .title = config.title, - .fullscreen = config.fullscreen, // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index b7ae1c6ee..a38defa34 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -101,6 +101,8 @@ pub const App = struct { /// Called when the cell size changes. set_cell_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, + + fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, }; /// Special values for the goto_tab callback. diff --git a/src/config/Config.zig b/src/config/Config.zig index 644f665a5..8e9b720fe 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -280,6 +280,7 @@ command: ?[]const u8 = null, /// The setting to tell Ghostty to start in fullscreen mode. /// By default with will start windowed. +/// New windows created when this is set to true will not start in fullscreen. fullscreen: bool = false, /// The setting that will change the application class value. From e9535a8d3e2d806192c4018607bf08157617af76 Mon Sep 17 00:00:00 2001 From: Raiden1411 <67233402+Raiden1411@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:28:48 +0000 Subject: [PATCH 14/18] chore: cleanup --- src/apprt/embedded.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/apprt/embedded.zig b/src/apprt/embedded.zig index a38defa34..b7ae1c6ee 100644 --- a/src/apprt/embedded.zig +++ b/src/apprt/embedded.zig @@ -101,8 +101,6 @@ pub const App = struct { /// Called when the cell size changes. set_cell_size: ?*const fn (SurfaceUD, u32, u32) callconv(.C) void = null, - - fullscreen: ?*const fn (SurfaceUD, configpkg.NonNativeFullscreen) callconv(.C) void = null, }; /// Special values for the goto_tab callback. From 3564dd5e7e77f74a0eb2742bf615cfef0ce65d15 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Nov 2023 08:47:09 -0800 Subject: [PATCH 15/18] stylistic changes --- src/Surface.zig | 8 ++-- src/apprt/gtk/App.zig | 93 ++++++++++++---------------------------- src/apprt/gtk/Window.zig | 8 +--- src/config/Config.zig | 29 +++++++------ 4 files changed, 49 insertions(+), 89 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 5396c2068..b00219b6f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -545,9 +545,7 @@ pub fn init( }; } - if (config.title) |title| { - try self.rt_surface.setTitle(title); - } + if (config.title) |title| try rt_surface.setTitle(title); } pub fn deinit(self: *Surface) void { @@ -668,9 +666,11 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { .set_title => |*v| { // We ignore the message in case the title was set via config. - if (self.config.title) |_| { + 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 a23d8ed22..84582484e 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -107,31 +107,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { const default_id = "com.mitchellh.ghostty"; const app_id: [:0]const u8 = app_id: { if (config.class) |class| { - break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else isValidGtkId(class) catch |err| switch (err) { - error.InvalidChar => { - log.warn("Invalid char found in class. Setting app id to default value", .{}); - break :app_id default_id; - }, - error.InvalidLength => { - log.warn("Class name value doesn't have valid length (> 0 or 255 <=). Setting app id to default value", .{}); - break :app_id default_id; - }, - error.NoDotInId => { - log.warn("Class name has no dot char. Setting app id to default value", .{}); - break :app_id default_id; - }, - error.StartWithDot => { - log.warn("Class name start with dot. Setting app id to default value", .{}); - break :app_id default_id; - }, - error.EndsWithDot => { - log.warn("Class name ends with dot. Setting app id to default value", .{}); - break :app_id default_id; - }, - }; - } else { - break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else default_id; + if (isValidAppId(class)) { + break :app_id class; + } else { + log.warn("invalid 'class' in config, ignoring", .{}); + } } + + break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else default_id; }; // Create our GTK Application which encapsulates our process. @@ -516,53 +499,31 @@ fn initMenu(self: *App) void { self.menu = menu; } -fn isValidGtkId(app_id: [:0]const u8) ![:0]const u8 { - var position: u8 = 0; +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; - - if (app_id.len > 255 or app_id.len == 0) - return error.InvalidLength; - - if (app_id[position] == '.') - return error.StartWithDot; - - if (app_id[app_id.len - 1] == '.') - return error.EndsWithDot; - - while (true) { - const char = app_id[position]; - + for (app_id) |char| { switch (char) { - 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => { - position += 1; - continue; - }, - '.' => { - hasDot = true; - position += 1; - continue; - }, - 0 => { - if (position != app_id.len) return error.InvalidChar; - break; - }, - else => return error.InvalidChar, + 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {}, + '.' => hasDot = true, + 0 => return false, } } + if (!hasDot) return false; - if (!hasDot) return error.NoDotInId; - - return app_id; + return true; } -test "ValidGtkClassName" { - try testing.expectEqualStrings("foo.bar", try isValidGtkId("foo.bar")); - try testing.expectEqualStrings("foo.bar.baz", try isValidGtkId("foo.bar.baz")); - - try testing.expectError(error.NoDotInId, isValidGtkId("foo")); - try testing.expectError(error.InvalidChar, isValidGtkId("foo.bar?")); - try testing.expectError(error.EndsWithDot, isValidGtkId("foo.")); - try testing.expectError(error.StartWithDot, isValidGtkId(".foo")); - try testing.expectError(error.InvalidLength, isValidGtkId("")); - try testing.expectError(error.InvalidLength, isValidGtkId("foo" ** 86)); +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 28f3c9f6d..9a36f826f 100644 --- a/src/apprt/gtk/Window.zig +++ b/src/apprt/gtk/Window.zig @@ -123,12 +123,8 @@ pub fn init(self: *Window, app: *App) !void { } c.gtk_box_append(@ptrCast(box), notebook_widget); - if (app.config.fullscreen) { - c.gtk_window_fullscreen(self.window); - - // We disable this because we just want to start the first window in fullscreen. - app.config.fullscreen = false; - } + // 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); diff --git a/src/config/Config.zig b/src/config/Config.zig index 8e9b720fe..54f3d6c3f 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -278,23 +278,26 @@ command: ?[]const u8 = null, /// indicate that it is a login shell, depending on the OS). @"command-arg": RepeatableString = .{}, -/// The setting to tell Ghostty to start in fullscreen mode. -/// By default with will start windowed. -/// New windows created when this is set to true will not start in fullscreen. +/// 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. fullscreen: bool = false, -/// The setting that will change the application class value. -/// This is usefull if you want to have multiple instances of Ghostty -/// running with separate classes making it possible to have unique behavior for each of them. -/// This is currently only supported on Gtk builds. -/// The class name must follow the GTK requirements defined here: https://docs.gtk.org/gio/type_func.Application.id_is_valid.html -class: ?[:0]const u8 = null, - -/// The setting that will tell Ghostty which title to display. -/// By default Ghostty will output the current directory or what application is running as the title, -/// but with this setting it will force Ghostty to output that title independent of what is happening in the terminal. +/// 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" From efe9721c9805644f36b7313089e14e819155a0ac Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Nov 2023 08:52:36 -0800 Subject: [PATCH 16/18] macos: honor fullscreen setting --- macos/Sources/Features/Terminal/TerminalManager.swift | 5 +++++ macos/Sources/Ghostty/AppState.swift | 9 +++++++++ src/config/Config.zig | 4 ++++ 3 files changed, 18 insertions(+) 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 4f637e3a9..68d63b39c 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/config/Config.zig b/src/config/Config.zig index 54f3d6c3f..b1512457c 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -281,6 +281,10 @@ command: ?[]const u8 = null, /// 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 From 364a14d7a2ce3ea331b31624370eafcd25f1b4dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Nov 2023 08:54:23 -0800 Subject: [PATCH 17/18] apprt/gtk: fix valid id check --- src/apprt/gtk/App.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 84582484e..fd7441e76 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -509,7 +509,7 @@ fn isValidAppId(app_id: [:0]const u8) bool { switch (char) { 'a'...'z', 'A'...'Z', '0'...'9', '_', '-' => {}, '.' => hasDot = true, - 0 => return false, + else => return false, } } if (!hasDot) return false; From f338c095068768875a933ff710addbd1e45b96f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 6 Nov 2023 08:55:13 -0800 Subject: [PATCH 18/18] apprt/gtk: move default id into scope --- src/apprt/gtk/App.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index fd7441e76..2456d2ada 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -104,7 +104,6 @@ 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 default_id = "com.mitchellh.ghostty"; const app_id: [:0]const u8 = app_id: { if (config.class) |class| { if (isValidAppId(class)) { @@ -114,7 +113,8 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { } } - break :app_id if (builtin.mode == .Debug) "com.mitchellh.ghostty-debug" else default_id; + 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.