From bb90104de3f71596b38b9b4c4aadb5d13be7f126 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 20:24:59 -0800 Subject: [PATCH 1/6] enable Mac native tabbing --- src/App.zig | 39 +++++++++++++++++++++++++++++++++++++++ src/Window.zig | 10 +++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/App.zig b/src/App.zig index a27e78ba0..a2a81b687 100644 --- a/src/App.zig +++ b/src/App.zig @@ -4,6 +4,7 @@ const App = @This(); const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const glfw = @import("glfw"); const Window = @import("Window.zig"); @@ -12,6 +13,8 @@ const Config = @import("config.zig").Config; const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue; const renderer = @import("renderer.zig"); const font = @import("font/main.zig"); +const macos = @import("macos"); +const objc = @import("objc"); const log = std.log.scoped(.app); @@ -36,6 +39,21 @@ mailbox: *Mailbox, /// Set to true once we're quitting. This never goes false again. quit: bool, +/// Mac settings +darwin: if (Darwin.enabled) Darwin else void, + +/// Mac-specific settings +pub const Darwin = struct { + pub const enabled = builtin.target.isDarwin(); + + tabbing_id: *macos.foundation.String, + + pub fn deinit(self: *Darwin) void { + self.tabbing_id.release(); + self.* = undefined; + } +}; + /// Initialize the main app instance. This creates the main window, sets /// up the renderer state, compiles the shaders, etc. This is the primary /// "startup" logic. @@ -52,9 +70,30 @@ pub fn create(alloc: Allocator, config: *const Config) !*App { .config = config, .mailbox = mailbox, .quit = false, + .darwin = if (Darwin.enabled) undefined else {}, }; errdefer app.windows.deinit(alloc); + // On Mac, we enable window tabbing + if (comptime builtin.target.isDarwin()) { + const NSWindow = objc.Class.getClass("NSWindow").?; + NSWindow.msgSend(void, objc.sel("setAllowsAutomaticWindowTabbing:"), .{true}); + + // Our tabbing ID allows all of our windows to group together + const tabbing_id = try macos.foundation.String.createWithBytes( + "dev.ghostty.window", + .utf8, + false, + ); + errdefer tabbing_id.release(); + + // Setup our Mac settings + app.darwin = .{ + .tabbing_id = tabbing_id, + }; + } + errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit(); + // Create the first window try app.newWindow(.{}); diff --git a/src/Window.zig b/src/Window.zig index affafad21..864453520 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -32,7 +32,7 @@ const App = @import("App.zig"); // Get native API access on certain platforms so we can do more customization. const glfwNative = glfw.Native(.{ - .cocoa = builtin.os.tag == .macos, + .cocoa = builtin.target.isDarwin(), }); const log = std.log.scoped(.window); @@ -132,6 +132,14 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { errdefer window.destroy(); try Renderer.windowInit(window); + // On Mac, enable tabbing + if (comptime builtin.target.isDarwin()) { + const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?); + nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic); + nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id); + } + // Determine our DPI configurations so we can properly configure // font points to pixels and handle other high-DPI scaling factors. const content_scale = try window.getContentScale(); From c6f7eab60143c8b6fd88d9e208cb9fefe18402ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 20:34:37 -0800 Subject: [PATCH 2/6] hide tab bar if last window is destroyed --- src/Window.zig | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/Window.zig b/src/Window.zig index 864453520..42adc2e63 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -507,6 +507,22 @@ pub fn destroy(self: *Window) void { self.io.deinit(); } + if (comptime builtin.target.isDarwin()) { + // If our tab bar is visible and we are going down to 1 window, + // hide the tab bar. + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); + const tabgroup = nswindow.getProperty(objc.Object, "tabGroup"); + if (tabgroup.getProperty(bool, "tabBarVisible")) { + const windows = tabgroup.getProperty(objc.Object, "windows"); + + // count check is 2 because our window will still be present + // in the tab bar since we haven't destroyed yet + if (windows.getProperty(usize, "count") == 2) { + nswindow.msgSend(void, objc.sel("toggleTabBar:"), .{nswindow.value}); + } + } + } + self.window.destroy(); // We can destroy the cursor right away. glfw will just revert any From 8ac90d33e6de115e6cdb6ef708ce792f3dce2279 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 21:17:41 -0800 Subject: [PATCH 3/6] new_tab action --- src/App.zig | 57 +++++++++++++++++++++++++++++++++++++------ src/Window.zig | 26 ++++++++++++++++++++ src/config.zig | 7 +++++- src/input/Binding.zig | 5 +++- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/src/App.zig b/src/App.zig index a2a81b687..fe542d8ee 100644 --- a/src/App.zig +++ b/src/App.zig @@ -95,7 +95,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*App { errdefer if (comptime builtin.target.isDarwin()) app.darwin.deinit(); // Create the first window - try app.newWindow(.{}); + _ = try app.newWindow(.{}); return app; } @@ -148,7 +148,8 @@ fn drainMailbox(self: *App) !void { while (drain.next()) |message| { log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { - .new_window => |msg| try self.newWindow(msg), + .new_window => |msg| _ = try self.newWindow(msg), + .new_tab => |msg| try self.newTab(msg), .quit => try self.setQuit(), .window_message => |msg| try self.windowMessage(msg.window, msg.message), } @@ -156,7 +157,7 @@ fn drainMailbox(self: *App) !void { } /// Create a new window -fn newWindow(self: *App, msg: Message.NewWindow) !void { +fn newWindow(self: *App, msg: Message.NewWindow) !*Window { var window = try Window.create(self.alloc, self, self.config); errdefer window.destroy(); try self.windows.append(self.alloc, window); @@ -164,6 +165,33 @@ fn newWindow(self: *App, msg: Message.NewWindow) !void { // Set initial font size if given if (msg.font_size) |size| window.setFontSize(size); + + return window; +} + +/// Create a new tab in the parent window +fn newTab(self: *App, msg: Message.NewWindow) !void { + if (comptime !builtin.target.isDarwin()) { + log.warn("tabbing is not supported on this platform", .{}); + return; + } + + const parent = msg.parent orelse { + log.warn("parent must be set in new_tab message", .{}); + return; + }; + + // If the parent was closed prior to us handling the message, we do nothing. + if (!self.hasWindow(parent)) { + log.warn("new_tab parent is gone, not launching a new tab", .{}); + return; + } + + // Create the new window + const window = try self.newWindow(msg); + + // Add the window to our parent tab group + parent.addWindow(window); } /// Start quitting @@ -182,22 +210,32 @@ fn windowMessage(self: *App, win: *Window, msg: Window.Message) !void { // We want to ensure our window is still active. Window messages // are quite rare and we normally don't have many windows so we do // a simple linear search here. - for (self.windows.items) |window| { - if (window == win) { - try win.handleMessage(msg); - return; - } + if (self.hasWindow(win)) { + try win.handleMessage(msg); } // Window was not found, it probably quit before we handled the message. // Not a problem. } +fn hasWindow(self: *App, win: *Window) bool { + for (self.windows.items) |window| { + if (window == win) return true; + } + + return false; +} + /// The message types that can be sent to the app thread. pub const Message = union(enum) { /// Create a new terminal window. new_window: NewWindow, + /// Create a new tab within the tab group of the focused window. + /// This does nothing if we're on a platform or using a window + /// environment that doesn't support tabs. + new_tab: NewWindow, + /// Quit quit: void, @@ -208,6 +246,9 @@ pub const Message = union(enum) { }, const NewWindow = struct { + /// The parent window, only used for new tabs. + parent: ?*Window = null, + /// The font size to create the window with or null to default to /// the configuration amount. font_size: ?font.face.DesiredSize = null, diff --git a/src/Window.zig b/src/Window.zig index 42adc2e63..f98bc7b8f 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -542,6 +542,18 @@ pub fn shouldClose(self: Window) bool { return self.window.shouldClose(); } +/// Add a window to the tab group of this window. +pub fn addWindow(self: Window, other: *Window) void { + assert(builtin.target.isDarwin()); + + const NSWindowOrderingMode = enum(isize) { below = -1, out = 0, above = 1 }; + const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(self.window).?); + nswindow.msgSend(void, objc.sel("addTabbedWindow:ordered:"), .{ + objc.Object.fromId(glfwNative.getCocoaWindow(other.window).?), + NSWindowOrderingMode.above, + }); +} + /// Called from the app thread to handle mailbox messages to our specific /// window. pub fn handleMessage(self: *Window, msg: Message) !void { @@ -953,6 +965,20 @@ fn keyCallback( win.app.wakeup(); }, + .new_tab => { + _ = win.app.mailbox.push(.{ + .new_tab = .{ + .parent = win, + + .font_size = if (win.config.@"window-inherit-font-size") + win.font_size + else + null, + }, + }, .{ .instant = {} }); + win.app.wakeup(); + }, + .close_window => win.window.setShouldClose(true), .quit => { diff --git a/src/config.zig b/src/config.zig index 74b5e93d3..bac96785e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -217,7 +217,12 @@ pub const Config = struct { .{ .key = .w, .mods = .{ .super = true } }, .{ .close_window = {} }, ); - if (builtin.os.tag == .macos) { + if (comptime builtin.target.isDarwin()) { + try result.keybind.set.put( + alloc, + .{ .key = .t, .mods = .{ .super = true } }, + .{ .new_tab = {} }, + ); try result.keybind.set.put( alloc, .{ .key = .q, .mods = .{ .super = true } }, diff --git a/src/input/Binding.zig b/src/input/Binding.zig index 8b909f7ad..e0629bd36 100644 --- a/src/input/Binding.zig +++ b/src/input/Binding.zig @@ -144,7 +144,10 @@ pub const Action = union(enum) { /// Open a new window new_window: void, - /// Close the current window + /// Open a new tab + new_tab: void, + + /// Close the current window or tab close_window: void, /// Quit ghostty From b4d59012253d1de2a9511be71cc966007071112e Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 21:18:37 -0800 Subject: [PATCH 4/6] update some docs --- src/config.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config.zig b/src/config.zig index bac96785e..74926d65e 100644 --- a/src/config.zig +++ b/src/config.zig @@ -115,7 +115,7 @@ pub const Config = struct { /// to balance the padding given a certain viewport size and grid cell size. @"window-padding-balance": bool = true, - /// If true, new windows will inherit the font size of the previously + /// If true, new windows and tabs will inherit the font size of the previously /// focused window. If no window was previously focused, the default /// font size will be used. If this is false, the default font size /// specified in the configuration "font-size" will be used. From 357ad43656bd2ee27ecfac9c33a7751d815c6e70 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 21:20:04 -0800 Subject: [PATCH 5/6] app: deinit darwin info --- src/App.zig | 1 + src/Window.zig | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/src/App.zig b/src/App.zig index fe542d8ee..60e0292a1 100644 --- a/src/App.zig +++ b/src/App.zig @@ -104,6 +104,7 @@ pub fn destroy(self: *App) void { // Clean up all our windows for (self.windows.items) |window| window.destroy(); self.windows.deinit(self.alloc); + if (comptime builtin.target.isDarwin()) self.darwin.deinit(); self.mailbox.destroy(self.alloc); self.alloc.destroy(self); } diff --git a/src/Window.zig b/src/Window.zig index f98bc7b8f..1d6beda32 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -136,7 +136,12 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { if (comptime builtin.target.isDarwin()) { const NSWindowTabbingMode = enum(usize) { automatic = 0, preferred = 1, disallowed = 2 }; const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?); + + // Tabbing mode enables tabbing at all nswindow.setProperty("tabbingMode", NSWindowTabbingMode.automatic); + + // All windows within a tab bar must have a matching tabbing ID. + // The app sets this up for us. nswindow.setProperty("tabbingIdentifier", app.darwin.tabbing_id); } From 30f8b55ed41298f94946c0cb6eaae28e75258543 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 16 Nov 2022 21:21:00 -0800 Subject: [PATCH 6/6] update TODO --- README.md | 4 ++-- TODO.md | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ce48d6ea4..531f3e848 100644 --- a/README.md +++ b/README.md @@ -63,8 +63,8 @@ only terminal emulator that has a Metal renderer that supports ligatures. ### Richer Windowing Features -We support multi-window, but do not yet support tabs or panes. This is -a must-have feature so this will come in time. +We support multi-window and tabbing on Mac. We will support panes/splits +in the future and we'll continue to improve multi-window features. ### Native Platform Experiences diff --git a/TODO.md b/TODO.md index 43f0aed42..0c73ef1c4 100644 --- a/TODO.md +++ b/TODO.md @@ -45,7 +45,6 @@ Mac: Major Features: * Reloadable configuration -* Tabs (macOS only for UI reasons) * Bell * Sixels: https://saitoha.github.io/libsixel/ * Kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/