mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
@ -63,8 +63,8 @@ only terminal emulator that has a Metal renderer that supports ligatures.
|
|||||||
|
|
||||||
### Richer Windowing Features
|
### Richer Windowing Features
|
||||||
|
|
||||||
We support multi-window, but do not yet support tabs or panes. This is
|
We support multi-window and tabbing on Mac. We will support panes/splits
|
||||||
a must-have feature so this will come in time.
|
in the future and we'll continue to improve multi-window features.
|
||||||
|
|
||||||
### Native Platform Experiences
|
### Native Platform Experiences
|
||||||
|
|
||||||
|
1
TODO.md
1
TODO.md
@ -45,7 +45,6 @@ Mac:
|
|||||||
Major Features:
|
Major Features:
|
||||||
|
|
||||||
* Reloadable configuration
|
* Reloadable configuration
|
||||||
* Tabs (macOS only for UI reasons)
|
|
||||||
* Bell
|
* Bell
|
||||||
* Sixels: https://saitoha.github.io/libsixel/
|
* Sixels: https://saitoha.github.io/libsixel/
|
||||||
* Kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
* Kitty keyboard protocol: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
|
||||||
|
95
src/App.zig
95
src/App.zig
@ -4,6 +4,7 @@
|
|||||||
const App = @This();
|
const App = @This();
|
||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
const builtin = @import("builtin");
|
||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
const glfw = @import("glfw");
|
const glfw = @import("glfw");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
@ -12,6 +13,8 @@ const Config = @import("config.zig").Config;
|
|||||||
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||||
const renderer = @import("renderer.zig");
|
const renderer = @import("renderer.zig");
|
||||||
const font = @import("font/main.zig");
|
const font = @import("font/main.zig");
|
||||||
|
const macos = @import("macos");
|
||||||
|
const objc = @import("objc");
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
@ -36,6 +39,21 @@ mailbox: *Mailbox,
|
|||||||
/// Set to true once we're quitting. This never goes false again.
|
/// Set to true once we're quitting. This never goes false again.
|
||||||
quit: bool,
|
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
|
/// Initialize the main app instance. This creates the main window, sets
|
||||||
/// up the renderer state, compiles the shaders, etc. This is the primary
|
/// up the renderer state, compiles the shaders, etc. This is the primary
|
||||||
/// "startup" logic.
|
/// "startup" logic.
|
||||||
@ -52,11 +70,32 @@ pub fn create(alloc: Allocator, config: *const Config) !*App {
|
|||||||
.config = config,
|
.config = config,
|
||||||
.mailbox = mailbox,
|
.mailbox = mailbox,
|
||||||
.quit = false,
|
.quit = false,
|
||||||
|
.darwin = if (Darwin.enabled) undefined else {},
|
||||||
};
|
};
|
||||||
errdefer app.windows.deinit(alloc);
|
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
|
// Create the first window
|
||||||
try app.newWindow(.{});
|
_ = try app.newWindow(.{});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@ -65,6 +104,7 @@ pub fn destroy(self: *App) void {
|
|||||||
// Clean up all our windows
|
// Clean up all our windows
|
||||||
for (self.windows.items) |window| window.destroy();
|
for (self.windows.items) |window| window.destroy();
|
||||||
self.windows.deinit(self.alloc);
|
self.windows.deinit(self.alloc);
|
||||||
|
if (comptime builtin.target.isDarwin()) self.darwin.deinit();
|
||||||
self.mailbox.destroy(self.alloc);
|
self.mailbox.destroy(self.alloc);
|
||||||
self.alloc.destroy(self);
|
self.alloc.destroy(self);
|
||||||
}
|
}
|
||||||
@ -109,7 +149,8 @@ fn drainMailbox(self: *App) !void {
|
|||||||
while (drain.next()) |message| {
|
while (drain.next()) |message| {
|
||||||
log.debug("mailbox message={s}", .{@tagName(message)});
|
log.debug("mailbox message={s}", .{@tagName(message)});
|
||||||
switch (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(),
|
.quit => try self.setQuit(),
|
||||||
.window_message => |msg| try self.windowMessage(msg.window, msg.message),
|
.window_message => |msg| try self.windowMessage(msg.window, msg.message),
|
||||||
}
|
}
|
||||||
@ -117,7 +158,7 @@ fn drainMailbox(self: *App) !void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new window
|
/// 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);
|
var window = try Window.create(self.alloc, self, self.config);
|
||||||
errdefer window.destroy();
|
errdefer window.destroy();
|
||||||
try self.windows.append(self.alloc, window);
|
try self.windows.append(self.alloc, window);
|
||||||
@ -125,6 +166,33 @@ fn newWindow(self: *App, msg: Message.NewWindow) !void {
|
|||||||
|
|
||||||
// Set initial font size if given
|
// Set initial font size if given
|
||||||
if (msg.font_size) |size| window.setFontSize(size);
|
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
|
/// Start quitting
|
||||||
@ -143,22 +211,32 @@ fn windowMessage(self: *App, win: *Window, msg: Window.Message) !void {
|
|||||||
// We want to ensure our window is still active. Window messages
|
// 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
|
// are quite rare and we normally don't have many windows so we do
|
||||||
// a simple linear search here.
|
// a simple linear search here.
|
||||||
for (self.windows.items) |window| {
|
if (self.hasWindow(win)) {
|
||||||
if (window == win) {
|
|
||||||
try win.handleMessage(msg);
|
try win.handleMessage(msg);
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Window was not found, it probably quit before we handled the message.
|
// Window was not found, it probably quit before we handled the message.
|
||||||
// Not a problem.
|
// 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.
|
/// The message types that can be sent to the app thread.
|
||||||
pub const Message = union(enum) {
|
pub const Message = union(enum) {
|
||||||
/// Create a new terminal window.
|
/// Create a new terminal window.
|
||||||
new_window: NewWindow,
|
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
|
||||||
quit: void,
|
quit: void,
|
||||||
|
|
||||||
@ -169,6 +247,9 @@ pub const Message = union(enum) {
|
|||||||
},
|
},
|
||||||
|
|
||||||
const NewWindow = struct {
|
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 font size to create the window with or null to default to
|
||||||
/// the configuration amount.
|
/// the configuration amount.
|
||||||
font_size: ?font.face.DesiredSize = null,
|
font_size: ?font.face.DesiredSize = null,
|
||||||
|
@ -32,7 +32,7 @@ const App = @import("App.zig");
|
|||||||
|
|
||||||
// Get native API access on certain platforms so we can do more customization.
|
// Get native API access on certain platforms so we can do more customization.
|
||||||
const glfwNative = glfw.Native(.{
|
const glfwNative = glfw.Native(.{
|
||||||
.cocoa = builtin.os.tag == .macos,
|
.cocoa = builtin.target.isDarwin(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const log = std.log.scoped(.window);
|
const log = std.log.scoped(.window);
|
||||||
@ -132,6 +132,19 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
|
|||||||
errdefer window.destroy();
|
errdefer window.destroy();
|
||||||
try Renderer.windowInit(window);
|
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).?);
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine our DPI configurations so we can properly configure
|
// Determine our DPI configurations so we can properly configure
|
||||||
// font points to pixels and handle other high-DPI scaling factors.
|
// font points to pixels and handle other high-DPI scaling factors.
|
||||||
const content_scale = try window.getContentScale();
|
const content_scale = try window.getContentScale();
|
||||||
@ -499,6 +512,22 @@ pub fn destroy(self: *Window) void {
|
|||||||
self.io.deinit();
|
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();
|
self.window.destroy();
|
||||||
|
|
||||||
// We can destroy the cursor right away. glfw will just revert any
|
// We can destroy the cursor right away. glfw will just revert any
|
||||||
@ -518,6 +547,18 @@ pub fn shouldClose(self: Window) bool {
|
|||||||
return self.window.shouldClose();
|
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
|
/// Called from the app thread to handle mailbox messages to our specific
|
||||||
/// window.
|
/// window.
|
||||||
pub fn handleMessage(self: *Window, msg: Message) !void {
|
pub fn handleMessage(self: *Window, msg: Message) !void {
|
||||||
@ -929,6 +970,20 @@ fn keyCallback(
|
|||||||
win.app.wakeup();
|
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),
|
.close_window => win.window.setShouldClose(true),
|
||||||
|
|
||||||
.quit => {
|
.quit => {
|
||||||
|
@ -115,7 +115,7 @@ pub const Config = struct {
|
|||||||
/// to balance the padding given a certain viewport size and grid cell size.
|
/// to balance the padding given a certain viewport size and grid cell size.
|
||||||
@"window-padding-balance": bool = true,
|
@"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
|
/// focused window. If no window was previously focused, the default
|
||||||
/// font size will be used. If this is false, the default font size
|
/// font size will be used. If this is false, the default font size
|
||||||
/// specified in the configuration "font-size" will be used.
|
/// specified in the configuration "font-size" will be used.
|
||||||
@ -217,7 +217,12 @@ pub const Config = struct {
|
|||||||
.{ .key = .w, .mods = .{ .super = true } },
|
.{ .key = .w, .mods = .{ .super = true } },
|
||||||
.{ .close_window = {} },
|
.{ .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(
|
try result.keybind.set.put(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .q, .mods = .{ .super = true } },
|
.{ .key = .q, .mods = .{ .super = true } },
|
||||||
|
@ -144,7 +144,10 @@ pub const Action = union(enum) {
|
|||||||
/// Open a new window
|
/// Open a new window
|
||||||
new_window: void,
|
new_window: void,
|
||||||
|
|
||||||
/// Close the current window
|
/// Open a new tab
|
||||||
|
new_tab: void,
|
||||||
|
|
||||||
|
/// Close the current window or tab
|
||||||
close_window: void,
|
close_window: void,
|
||||||
|
|
||||||
/// Quit ghostty
|
/// Quit ghostty
|
||||||
|
Reference in New Issue
Block a user