mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-17 09:16:11 +03:00
Multi-Window
This adds the ability to have multiple windows by introducing new keybind actions `new_window`, `close_window`, and `quit`. These are bound on macOS by default to the standard `cmd+n`, `cmd+w`, and `cmd+q`, respectively. The multi-window logic is absolutely minimal: we don't do any GPU data sharing between windows, so each window recreates a full font texture for example. We can continue to further optimize this in the future. DevMode is also pretty limited (arguably broken) with multi-window: DevMode only works on the _first_ window opened. If you close the first window, DevMode is no longer available. This is due to complexities around multi-threading and imgui. It is possible to fix this but I deferred it since it was a bit of a mess!
This commit is contained in:
@ -14,6 +14,10 @@ pub const Context = opaque {
|
|||||||
c.igDestroyContext(self.cval());
|
c.igDestroyContext(self.cval());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn setCurrent(self: *Context) void {
|
||||||
|
c.igSetCurrentContext(self.cval());
|
||||||
|
}
|
||||||
|
|
||||||
pub inline fn cval(self: *Context) *c.ImGuiContext {
|
pub inline fn cval(self: *Context) *c.ImGuiContext {
|
||||||
return @ptrCast(
|
return @ptrCast(
|
||||||
*c.ImGuiContext,
|
*c.ImGuiContext,
|
||||||
@ -25,4 +29,6 @@ pub const Context = opaque {
|
|||||||
test {
|
test {
|
||||||
var ctx = try Context.create();
|
var ctx = try Context.create();
|
||||||
defer ctx.destroy();
|
defer ctx.destroy();
|
||||||
|
|
||||||
|
ctx.setCurrent();
|
||||||
}
|
}
|
||||||
|
124
src/App.zig
124
src/App.zig
@ -9,46 +9,138 @@ const glfw = @import("glfw");
|
|||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const tracy = @import("tracy");
|
const tracy = @import("tracy");
|
||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
|
const BlockingQueue = @import("./blocking_queue.zig").BlockingQueue;
|
||||||
|
const renderer = @import("renderer.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
|
const WindowList = std.ArrayListUnmanaged(*Window);
|
||||||
|
|
||||||
|
/// The type used for sending messages to the app thread.
|
||||||
|
pub const Mailbox = BlockingQueue(Message, 64);
|
||||||
|
|
||||||
/// General purpose allocator
|
/// General purpose allocator
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
/// The primary window for the application. We currently support only
|
/// The list of windows that are currently open
|
||||||
/// single window operations.
|
windows: WindowList,
|
||||||
window: *Window,
|
|
||||||
|
|
||||||
// The configuration for the app.
|
// The configuration for the app.
|
||||||
config: *const Config,
|
config: *const Config,
|
||||||
|
|
||||||
|
/// The mailbox that can be used to send this thread messages. Note
|
||||||
|
/// this is a blocking queue so if it is full you will get errors (or block).
|
||||||
|
mailbox: *Mailbox,
|
||||||
|
|
||||||
|
/// Set to true once we're quitting. This never goes false again.
|
||||||
|
quit: bool,
|
||||||
|
|
||||||
/// 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.
|
||||||
pub fn init(alloc: Allocator, config: *const Config) !App {
|
pub fn create(alloc: Allocator, config: *const Config) !*App {
|
||||||
// Create the window
|
// The mailbox for messaging this thread
|
||||||
var window = try Window.create(alloc, config);
|
var mailbox = try Mailbox.create(alloc);
|
||||||
errdefer window.destroy();
|
errdefer mailbox.destroy(alloc);
|
||||||
|
|
||||||
return App{
|
var app = try alloc.create(App);
|
||||||
|
errdefer alloc.destroy(app);
|
||||||
|
app.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.window = window,
|
.windows = .{},
|
||||||
.config = config,
|
.config = config,
|
||||||
|
.mailbox = mailbox,
|
||||||
|
.quit = false,
|
||||||
};
|
};
|
||||||
|
errdefer app.windows.deinit(alloc);
|
||||||
|
|
||||||
|
// Create the first window
|
||||||
|
try app.newWindow();
|
||||||
|
|
||||||
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *App) void {
|
pub fn destroy(self: *App) void {
|
||||||
self.window.destroy();
|
// Clean up all our windows
|
||||||
self.* = undefined;
|
for (self.windows.items) |window| window.destroy();
|
||||||
|
self.windows.deinit(self.alloc);
|
||||||
|
self.mailbox.destroy(self.alloc);
|
||||||
|
self.alloc.destroy(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self: App) !void {
|
/// Wake up the app event loop. This should be called after any messages
|
||||||
while (!self.window.shouldClose()) {
|
/// are sent to the mailbox.
|
||||||
// Block for any glfw events. This may also be an "empty" event
|
pub fn wakeup(self: App) void {
|
||||||
// posted by the libuv watcher so that we trigger a libuv loop tick.
|
_ = self;
|
||||||
|
glfw.postEmptyEvent() catch {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the main event loop for the application. This blocks until the
|
||||||
|
/// application quits or every window is closed.
|
||||||
|
pub fn run(self: *App) !void {
|
||||||
|
while (!self.quit and self.windows.items.len > 0) {
|
||||||
|
// Block for any glfw events.
|
||||||
try glfw.waitEvents();
|
try glfw.waitEvents();
|
||||||
|
|
||||||
// Mark this so we're in a totally different "frame"
|
// Mark this so we're in a totally different "frame"
|
||||||
tracy.frameMark();
|
tracy.frameMark();
|
||||||
|
|
||||||
|
// If any windows are closing, destroy them
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < self.windows.items.len) {
|
||||||
|
const window = self.windows.items[i];
|
||||||
|
if (window.shouldClose()) {
|
||||||
|
window.destroy();
|
||||||
|
_ = self.windows.swapRemove(i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain our mailbox only if we're not quitting.
|
||||||
|
if (!self.quit) try self.drainMailbox();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drain the mailbox.
|
||||||
|
fn drainMailbox(self: *App) !void {
|
||||||
|
var drain = self.mailbox.drain();
|
||||||
|
defer drain.deinit();
|
||||||
|
|
||||||
|
while (drain.next()) |message| {
|
||||||
|
log.debug("mailbox message={}", .{message});
|
||||||
|
switch (message) {
|
||||||
|
.new_window => try self.newWindow(),
|
||||||
|
.quit => try self.setQuit(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new window
|
||||||
|
fn newWindow(self: *App) !void {
|
||||||
|
var window = try Window.create(self.alloc, self, self.config);
|
||||||
|
errdefer window.destroy();
|
||||||
|
try self.windows.append(self.alloc, window);
|
||||||
|
errdefer _ = self.windows.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start quitting
|
||||||
|
fn setQuit(self: *App) !void {
|
||||||
|
if (self.quit) return;
|
||||||
|
self.quit = true;
|
||||||
|
|
||||||
|
// Mark that all our windows should close
|
||||||
|
for (self.windows.items) |window| {
|
||||||
|
window.window.setShouldClose(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The message types that can be sent to the app thread.
|
||||||
|
pub const Message = union(enum) {
|
||||||
|
/// Create a new terminal window.
|
||||||
|
new_window: void,
|
||||||
|
|
||||||
|
/// Quit
|
||||||
|
quit: void,
|
||||||
|
};
|
||||||
|
@ -22,6 +22,7 @@ const terminal = @import("terminal/main.zig");
|
|||||||
const Config = @import("config.zig").Config;
|
const Config = @import("config.zig").Config;
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const DevMode = @import("DevMode.zig");
|
const DevMode = @import("DevMode.zig");
|
||||||
|
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(.{
|
||||||
@ -36,6 +37,9 @@ const Renderer = renderer.Renderer;
|
|||||||
/// Allocator
|
/// Allocator
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
|
/// The app that this window is a part of.
|
||||||
|
app: *App,
|
||||||
|
|
||||||
/// The font structures
|
/// The font structures
|
||||||
font_lib: font.Library,
|
font_lib: font.Library,
|
||||||
font_group: *font.GroupCache,
|
font_group: *font.GroupCache,
|
||||||
@ -107,7 +111,7 @@ const Mouse = struct {
|
|||||||
/// Create a new window. This allocates and returns a pointer because we
|
/// Create a new window. This allocates and returns a pointer because we
|
||||||
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
/// need a stable pointer for user data callbacks. Therefore, a stack-only
|
||||||
/// initialization is not currently possible.
|
/// initialization is not currently possible.
|
||||||
pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window {
|
||||||
var self = try alloc.create(Window);
|
var self = try alloc.create(Window);
|
||||||
errdefer alloc.destroy(self);
|
errdefer alloc.destroy(self);
|
||||||
|
|
||||||
@ -136,6 +140,10 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Find all the fonts for this window
|
// Find all the fonts for this window
|
||||||
|
//
|
||||||
|
// Future: we can share the font group amongst all windows to save
|
||||||
|
// some new window init time and some memory. This will require making
|
||||||
|
// thread-safe changes to font structs.
|
||||||
var font_lib = try font.Library.init();
|
var font_lib = try font.Library.init();
|
||||||
errdefer font_lib.deinit();
|
errdefer font_lib.deinit();
|
||||||
var font_group = try alloc.create(font.GroupCache);
|
var font_group = try alloc.create(font.GroupCache);
|
||||||
@ -247,6 +255,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
|
|
||||||
// Create our terminal grid with the initial window size
|
// Create our terminal grid with the initial window size
|
||||||
var renderer_impl = try Renderer.init(alloc, font_group);
|
var renderer_impl = try Renderer.init(alloc, font_group);
|
||||||
|
errdefer renderer_impl.deinit();
|
||||||
renderer_impl.background = .{
|
renderer_impl.background = .{
|
||||||
.r = config.background.r,
|
.r = config.background.r,
|
||||||
.g = config.background.g,
|
.g = config.background.g,
|
||||||
@ -307,8 +316,14 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
var io_thread = try termio.Thread.init(alloc, &self.io);
|
var io_thread = try termio.Thread.init(alloc, &self.io);
|
||||||
errdefer io_thread.deinit();
|
errdefer io_thread.deinit();
|
||||||
|
|
||||||
|
// True if this window is hosting devmode. We only host devmode on
|
||||||
|
// the first window since imgui is not threadsafe. We need to do some
|
||||||
|
// work to make DevMode work with multiple threads.
|
||||||
|
const host_devmode = DevMode.enabled and DevMode.instance.window == null;
|
||||||
|
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
|
.app = app,
|
||||||
.font_lib = font_lib,
|
.font_lib = font_lib,
|
||||||
.font_group = font_group,
|
.font_group = font_group,
|
||||||
.window = window,
|
.window = window,
|
||||||
@ -323,7 +338,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
.visible = true,
|
.visible = true,
|
||||||
},
|
},
|
||||||
.terminal = &self.io.terminal,
|
.terminal = &self.io.terminal,
|
||||||
.devmode = if (!DevMode.enabled) null else &DevMode.instance,
|
.devmode = if (!host_devmode) null else &DevMode.instance,
|
||||||
},
|
},
|
||||||
.renderer_thr = undefined,
|
.renderer_thr = undefined,
|
||||||
.mouse = .{},
|
.mouse = .{},
|
||||||
@ -333,7 +348,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
.grid_size = grid_size,
|
.grid_size = grid_size,
|
||||||
.config = config,
|
.config = config,
|
||||||
|
|
||||||
.imgui_ctx = if (!DevMode.enabled) void else try imgui.Context.create(),
|
.imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(),
|
||||||
};
|
};
|
||||||
errdefer if (DevMode.enabled) self.imgui_ctx.destroy();
|
errdefer if (DevMode.enabled) self.imgui_ctx.destroy();
|
||||||
|
|
||||||
@ -361,7 +376,7 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
|
|
||||||
// Load imgui. This must be done LAST because it has to be done after
|
// Load imgui. This must be done LAST because it has to be done after
|
||||||
// all our GLFW setup is complete.
|
// all our GLFW setup is complete.
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled and DevMode.instance.window == null) {
|
||||||
const dev_io = try imgui.IO.get();
|
const dev_io = try imgui.IO.get();
|
||||||
dev_io.cval().IniFilename = "ghostty_dev_mode.ini";
|
dev_io.cval().IniFilename = "ghostty_dev_mode.ini";
|
||||||
|
|
||||||
@ -376,13 +391,16 @@ pub fn create(alloc: Allocator, config: *const Config) !*Window {
|
|||||||
const style = try imgui.Style.get();
|
const style = try imgui.Style.get();
|
||||||
style.colorsDark();
|
style.colorsDark();
|
||||||
|
|
||||||
// Add our window to the instance
|
// Add our window to the instance if it isn't set.
|
||||||
DevMode.instance.window = self;
|
DevMode.instance.window = self;
|
||||||
|
|
||||||
|
// Let our renderer setup
|
||||||
|
try renderer_impl.initDevMode(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the renderer one more opportunity to finalize any window
|
// Give the renderer one more opportunity to finalize any window
|
||||||
// setup on the main thread prior to spinning up the rendering thread.
|
// setup on the main thread prior to spinning up the rendering thread.
|
||||||
try renderer_impl.finalizeInit(window);
|
try renderer_impl.finalizeWindowInit(window);
|
||||||
|
|
||||||
// Start our renderer thread
|
// Start our renderer thread
|
||||||
self.renderer_thr = try std.Thread.spawn(
|
self.renderer_thr = try std.Thread.spawn(
|
||||||
@ -412,18 +430,22 @@ pub fn destroy(self: *Window) void {
|
|||||||
self.renderer.threadEnter(self.window) catch unreachable;
|
self.renderer.threadEnter(self.window) catch unreachable;
|
||||||
self.renderer_thread.deinit();
|
self.renderer_thread.deinit();
|
||||||
|
|
||||||
|
// If we are devmode-owning, clean that up.
|
||||||
|
if (DevMode.enabled and DevMode.instance.window == self) {
|
||||||
|
// Let our renderer clean up
|
||||||
|
self.renderer.deinitDevMode();
|
||||||
|
|
||||||
|
// Clear the window
|
||||||
|
DevMode.instance.window = null;
|
||||||
|
|
||||||
|
// Uninitialize imgui
|
||||||
|
self.imgui_ctx.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
// Deinit our renderer
|
// Deinit our renderer
|
||||||
self.renderer.deinit();
|
self.renderer.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DevMode.enabled) {
|
|
||||||
// Clear the window
|
|
||||||
DevMode.instance.window = null;
|
|
||||||
|
|
||||||
// Uninitialize imgui
|
|
||||||
self.imgui_ctx.destroy();
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
// Stop our IO thread
|
// Stop our IO thread
|
||||||
self.io_thread.stop.send() catch |err|
|
self.io_thread.stop.send() catch |err|
|
||||||
@ -748,6 +770,22 @@ fn keyCallback(
|
|||||||
DevMode.instance.visible = !DevMode.instance.visible;
|
DevMode.instance.visible = !DevMode.instance.visible;
|
||||||
win.queueRender() catch unreachable;
|
win.queueRender() catch unreachable;
|
||||||
} else log.warn("dev mode was not compiled into this binary", .{}),
|
} else log.warn("dev mode was not compiled into this binary", .{}),
|
||||||
|
|
||||||
|
.new_window => {
|
||||||
|
_ = win.app.mailbox.push(.{
|
||||||
|
.new_window = {},
|
||||||
|
}, .{ .instant = {} });
|
||||||
|
win.app.wakeup();
|
||||||
|
},
|
||||||
|
|
||||||
|
.close_window => win.window.setShouldClose(true),
|
||||||
|
|
||||||
|
.quit => {
|
||||||
|
_ = win.app.mailbox.push(.{
|
||||||
|
.quit = {},
|
||||||
|
}, .{ .instant = {} });
|
||||||
|
win.app.wakeup();
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bindings always result in us ignoring the char if printable
|
// Bindings always result in us ignoring the char if printable
|
||||||
|
@ -157,6 +157,25 @@ pub const Config = struct {
|
|||||||
.{ .toggle_dev_mode = {} },
|
.{ .toggle_dev_mode = {} },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Windowing
|
||||||
|
try result.keybind.set.put(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .n, .mods = .{ .super = true } },
|
||||||
|
.{ .new_window = {} },
|
||||||
|
);
|
||||||
|
try result.keybind.set.put(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .w, .mods = .{ .super = true } },
|
||||||
|
.{ .close_window = {} },
|
||||||
|
);
|
||||||
|
if (builtin.os.tag == .macos) {
|
||||||
|
try result.keybind.set.put(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .q, .mods = .{ .super = true } },
|
||||||
|
.{ .quit = {} },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,6 +133,15 @@ pub const Action = union(enum) {
|
|||||||
|
|
||||||
/// Dev mode
|
/// Dev mode
|
||||||
toggle_dev_mode: void,
|
toggle_dev_mode: void,
|
||||||
|
|
||||||
|
/// Open a new window
|
||||||
|
new_window: void,
|
||||||
|
|
||||||
|
/// Close the current window
|
||||||
|
close_window: void,
|
||||||
|
|
||||||
|
/// Quit ghostty
|
||||||
|
quit: void,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Trigger is the associated key state that can trigger an action.
|
/// Trigger is the associated key state that can trigger an action.
|
||||||
|
55
src/main.zig
55
src/main.zig
@ -22,6 +22,9 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
std.log.info("renderer={}", .{renderer.Renderer});
|
std.log.info("renderer={}", .{renderer.Renderer});
|
||||||
|
|
||||||
|
// First things first, we fix our file descriptors
|
||||||
|
fixMaxFiles();
|
||||||
|
|
||||||
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
const GPA = std.heap.GeneralPurposeAllocator(.{});
|
||||||
var gpa: ?GPA = gpa: {
|
var gpa: ?GPA = gpa: {
|
||||||
// Use the libc allocator if it is available beacuse it is WAY
|
// Use the libc allocator if it is available beacuse it is WAY
|
||||||
@ -128,8 +131,8 @@ pub fn main() !void {
|
|||||||
defer glfw.terminate();
|
defer glfw.terminate();
|
||||||
|
|
||||||
// Run our app
|
// Run our app
|
||||||
var app = try App.init(alloc, &config);
|
var app = try App.create(alloc, &config);
|
||||||
defer app.deinit();
|
defer app.destroy();
|
||||||
try app.run();
|
try app.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -188,6 +191,54 @@ fn glfwErrorCallback(code: glfw.Error, desc: [:0]const u8) void {
|
|||||||
std.log.warn("glfw error={} message={s}", .{ code, desc });
|
std.log.warn("glfw error={} message={s}", .{ code, desc });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This maximizes the number of file descriptors we can have open. We
|
||||||
|
/// need to do this because each window consumes at least a handful of fds.
|
||||||
|
/// This is extracted from the Zig compiler source code.
|
||||||
|
fn fixMaxFiles() void {
|
||||||
|
if (!@hasDecl(std.os.system, "rlimit")) return;
|
||||||
|
const posix = std.os;
|
||||||
|
|
||||||
|
var lim = posix.getrlimit(.NOFILE) catch {
|
||||||
|
std.log.warn("failed to query file handle limit, may limit max windows", .{});
|
||||||
|
return; // Oh well; we tried.
|
||||||
|
};
|
||||||
|
if (comptime builtin.target.isDarwin()) {
|
||||||
|
// On Darwin, `NOFILE` is bounded by a hardcoded value `OPEN_MAX`.
|
||||||
|
// According to the man pages for setrlimit():
|
||||||
|
// setrlimit() now returns with errno set to EINVAL in places that historically succeeded.
|
||||||
|
// It no longer accepts "rlim_cur = RLIM.INFINITY" for RLIM.NOFILE.
|
||||||
|
// Use "rlim_cur = min(OPEN_MAX, rlim_max)".
|
||||||
|
lim.max = std.math.min(std.os.darwin.OPEN_MAX, lim.max);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're already at the max, we're done.
|
||||||
|
if (lim.cur >= lim.max) {
|
||||||
|
std.log.debug("file handle limit already maximized value={}", .{lim.cur});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do a binary search for the limit.
|
||||||
|
var min: posix.rlim_t = lim.cur;
|
||||||
|
var max: posix.rlim_t = 1 << 20;
|
||||||
|
// But if there's a defined upper bound, don't search, just set it.
|
||||||
|
if (lim.max != posix.RLIM.INFINITY) {
|
||||||
|
min = lim.max;
|
||||||
|
max = lim.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
lim.cur = min + @divTrunc(max - min, 2); // on freebsd rlim_t is signed
|
||||||
|
if (posix.setrlimit(.NOFILE, lim)) |_| {
|
||||||
|
min = lim.cur;
|
||||||
|
} else |_| {
|
||||||
|
max = lim.cur;
|
||||||
|
}
|
||||||
|
if (min + 1 >= max) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std.log.debug("file handle limit raised value={}", .{lim.cur});
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = @import("Atlas.zig");
|
_ = @import("Atlas.zig");
|
||||||
_ = @import("Pty.zig");
|
_ = @import("Pty.zig");
|
||||||
|
@ -240,11 +240,6 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Metal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Metal) void {
|
pub fn deinit(self: *Metal) void {
|
||||||
if (DevMode.enabled) {
|
|
||||||
imgui.ImplMetal.shutdown();
|
|
||||||
imgui.ImplGlfw.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.cells.deinit(self.alloc);
|
self.cells.deinit(self.alloc);
|
||||||
|
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
@ -255,7 +250,7 @@ pub fn deinit(self: *Metal) void {
|
|||||||
|
|
||||||
/// This is called just prior to spinning up the renderer thread for
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
pub fn finalizeWindowInit(self: *const Metal, window: glfw.Window) !void {
|
||||||
// Set our window backing layer to be our swapchain
|
// Set our window backing layer to be our swapchain
|
||||||
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?);
|
const nswindow = objc.Object.fromId(glfwNative.getCocoaWindow(window).?);
|
||||||
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
const contentView = objc.Object.fromId(nswindow.getProperty(?*anyopaque, "contentView").?);
|
||||||
@ -268,7 +263,10 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
|||||||
const layer = contentView.getProperty(objc.Object, "layer");
|
const layer = contentView.getProperty(objc.Object, "layer");
|
||||||
const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor");
|
const scaleFactor = nswindow.getProperty(macos.graphics.c.CGFloat, "backingScaleFactor");
|
||||||
layer.setProperty("contentsScale", scaleFactor);
|
layer.setProperty("contentsScale", scaleFactor);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is called if this renderer runs DevMode.
|
||||||
|
pub fn initDevMode(self: *const Metal, window: glfw.Window) !void {
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled) {
|
||||||
// Initialize for our window
|
// Initialize for our window
|
||||||
assert(imgui.ImplGlfw.initForOther(
|
assert(imgui.ImplGlfw.initForOther(
|
||||||
@ -279,6 +277,16 @@ pub fn finalizeInit(self: *const Metal, window: glfw.Window) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This is called if this renderer runs DevMode.
|
||||||
|
pub fn deinitDevMode(self: *const Metal) void {
|
||||||
|
_ = self;
|
||||||
|
|
||||||
|
if (DevMode.enabled) {
|
||||||
|
imgui.ImplMetal.shutdown();
|
||||||
|
imgui.ImplGlfw.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
pub fn threadEnter(self: *const Metal, window: glfw.Window) !void {
|
pub fn threadEnter(self: *const Metal, window: glfw.Window) !void {
|
||||||
_ = self;
|
_ = self;
|
||||||
|
@ -300,11 +300,6 @@ pub fn init(alloc: Allocator, font_group: *font.GroupCache) !OpenGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *OpenGL) void {
|
pub fn deinit(self: *OpenGL) void {
|
||||||
if (DevMode.enabled) {
|
|
||||||
imgui.ImplOpenGL3.shutdown();
|
|
||||||
imgui.ImplGlfw.shutdown();
|
|
||||||
}
|
|
||||||
|
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
self.alloc.free(self.font_shaper.cell_buf);
|
self.alloc.free(self.font_shaper.cell_buf);
|
||||||
|
|
||||||
@ -387,7 +382,15 @@ pub fn windowInit(window: glfw.Window) !void {
|
|||||||
|
|
||||||
/// This is called just prior to spinning up the renderer thread for
|
/// This is called just prior to spinning up the renderer thread for
|
||||||
/// final main thread setup requirements.
|
/// final main thread setup requirements.
|
||||||
pub fn finalizeInit(self: *const OpenGL, window: glfw.Window) !void {
|
pub fn finalizeWindowInit(self: *const OpenGL, window: glfw.Window) !void {
|
||||||
|
_ = self;
|
||||||
|
_ = window;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This is called if this renderer runs DevMode.
|
||||||
|
pub fn initDevMode(self: *const OpenGL, window: glfw.Window) !void {
|
||||||
|
_ = self;
|
||||||
|
|
||||||
if (DevMode.enabled) {
|
if (DevMode.enabled) {
|
||||||
// Initialize for our window
|
// Initialize for our window
|
||||||
assert(imgui.ImplGlfw.initForOpenGL(
|
assert(imgui.ImplGlfw.initForOpenGL(
|
||||||
@ -396,9 +399,16 @@ pub fn finalizeInit(self: *const OpenGL, window: glfw.Window) !void {
|
|||||||
));
|
));
|
||||||
assert(imgui.ImplOpenGL3.init("#version 330 core"));
|
assert(imgui.ImplOpenGL3.init("#version 330 core"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Call thread exit to clean up our context
|
/// This is called if this renderer runs DevMode.
|
||||||
self.threadExit();
|
pub fn deinitDevMode(self: *const OpenGL) void {
|
||||||
|
_ = self;
|
||||||
|
|
||||||
|
if (DevMode.enabled) {
|
||||||
|
imgui.ImplOpenGL3.shutdown();
|
||||||
|
imgui.ImplGlfw.shutdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback called by renderer.Thread when it begins.
|
/// Callback called by renderer.Thread when it begins.
|
||||||
|
@ -65,7 +65,11 @@ pub fn init(
|
|||||||
|
|
||||||
// Create our event loop.
|
// Create our event loop.
|
||||||
var loop = try libuv.Loop.init(alloc);
|
var loop = try libuv.Loop.init(alloc);
|
||||||
errdefer loop.deinit(alloc);
|
errdefer {
|
||||||
|
// Run the loop once to close any of our handles
|
||||||
|
_ = loop.run(.nowait) catch 0;
|
||||||
|
loop.deinit(alloc);
|
||||||
|
}
|
||||||
loop.setData(allocPtr);
|
loop.setData(allocPtr);
|
||||||
|
|
||||||
// This async handle is used to "wake up" the renderer and force a render.
|
// This async handle is used to "wake up" the renderer and force a render.
|
||||||
|
@ -15,6 +15,12 @@ const renderer = @import("../renderer.zig");
|
|||||||
|
|
||||||
const log = std.log.scoped(.io_exec);
|
const log = std.log.scoped(.io_exec);
|
||||||
|
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("errno.h");
|
||||||
|
@cInclude("signal.h");
|
||||||
|
@cInclude("unistd.h");
|
||||||
|
});
|
||||||
|
|
||||||
/// Allocator
|
/// Allocator
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
|
|
||||||
@ -78,8 +84,8 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
|||||||
.env = &env,
|
.env = &env,
|
||||||
.cwd = opts.config.@"working-directory",
|
.cwd = opts.config.@"working-directory",
|
||||||
.pre_exec = (struct {
|
.pre_exec = (struct {
|
||||||
fn callback(c: *Command) void {
|
fn callback(cmd: *Command) void {
|
||||||
const p = c.getData(Pty) orelse unreachable;
|
const p = cmd.getData(Pty) orelse unreachable;
|
||||||
p.childPreExec() catch |err|
|
p.childPreExec() catch |err|
|
||||||
log.err("error initializing child: {}", .{err});
|
log.err("error initializing child: {}", .{err});
|
||||||
}
|
}
|
||||||
@ -113,9 +119,9 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Exec) void {
|
pub fn deinit(self: *Exec) void {
|
||||||
// Deinitialize the pty. This closes the pty handles. This should
|
// Kill our command
|
||||||
// cause a close in the our subprocess so just wait for that.
|
self.killCommand() catch |err|
|
||||||
self.pty.deinit();
|
log.err("error sending SIGHUP to command, may hang: {}", .{err});
|
||||||
_ = self.command.wait() catch |err|
|
_ = self.command.wait() catch |err|
|
||||||
log.err("error waiting for command to exit: {}", .{err});
|
log.err("error waiting for command to exit: {}", .{err});
|
||||||
|
|
||||||
@ -123,6 +129,42 @@ pub fn deinit(self: *Exec) void {
|
|||||||
self.terminal.deinit(self.alloc);
|
self.terminal.deinit(self.alloc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Kill the underlying subprocess. This closes the pty file handle and
|
||||||
|
/// sends a SIGHUP to the child process. This doesn't wait for the child
|
||||||
|
/// process to be exited.
|
||||||
|
fn killCommand(self: *Exec) !void {
|
||||||
|
// Close our PTY
|
||||||
|
self.pty.deinit();
|
||||||
|
|
||||||
|
// We need to get our process group ID and send a SIGHUP to it.
|
||||||
|
if (self.command.pid) |pid| {
|
||||||
|
const pgid_: ?c.pid_t = pgid: {
|
||||||
|
const pgid = c.getpgid(pid);
|
||||||
|
|
||||||
|
// Don't know why it would be zero but its not a valid pid
|
||||||
|
if (pgid == 0) break :pgid null;
|
||||||
|
|
||||||
|
// If the pid doesn't exist then... okay.
|
||||||
|
if (pgid == c.ESRCH) break :pgid null;
|
||||||
|
|
||||||
|
// If we have an error...
|
||||||
|
if (pgid < 0) {
|
||||||
|
log.warn("error getting pgid for kill", .{});
|
||||||
|
break :pgid null;
|
||||||
|
}
|
||||||
|
|
||||||
|
break :pgid pgid;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pgid_) |pgid| {
|
||||||
|
if (c.killpg(pgid, c.SIGHUP) < 0) {
|
||||||
|
log.warn("error killing process group pgid={}", .{pgid});
|
||||||
|
return error.KillFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData {
|
pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData {
|
||||||
assert(self.data == null);
|
assert(self.data == null);
|
||||||
|
|
||||||
@ -367,15 +409,15 @@ fn ttyRead(t: *libuv.Tty, n: isize, buf: []const u8) void {
|
|||||||
var i: usize = 0;
|
var i: usize = 0;
|
||||||
const end = @intCast(usize, n);
|
const end = @intCast(usize, n);
|
||||||
if (ev.terminal_stream.parser.state == .ground) {
|
if (ev.terminal_stream.parser.state == .ground) {
|
||||||
for (buf[i..end]) |c| {
|
for (buf[i..end]) |ch| {
|
||||||
switch (terminal.parse_table.table[c][@enumToInt(terminal.Parser.State.ground)].action) {
|
switch (terminal.parse_table.table[ch][@enumToInt(terminal.Parser.State.ground)].action) {
|
||||||
// Print, call directly.
|
// Print, call directly.
|
||||||
.print => ev.terminal_stream.handler.print(@intCast(u21, c)) catch |err|
|
.print => ev.terminal_stream.handler.print(@intCast(u21, ch)) catch |err|
|
||||||
log.err("error processing terminal data: {}", .{err}),
|
log.err("error processing terminal data: {}", .{err}),
|
||||||
|
|
||||||
// C0 execute, let our stream handle this one but otherwise
|
// C0 execute, let our stream handle this one but otherwise
|
||||||
// continue since we're guaranteed to be back in ground.
|
// continue since we're guaranteed to be back in ground.
|
||||||
.execute => ev.terminal_stream.execute(c) catch |err|
|
.execute => ev.terminal_stream.execute(ch) catch |err|
|
||||||
log.err("error processing terminal data: {}", .{err}),
|
log.err("error processing terminal data: {}", .{err}),
|
||||||
|
|
||||||
// Otherwise, break out and go the slow path until we're
|
// Otherwise, break out and go the slow path until we're
|
||||||
@ -413,8 +455,8 @@ const StreamHandler = struct {
|
|||||||
try self.ev.queueWrite(data);
|
try self.ev.queueWrite(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn print(self: *StreamHandler, c: u21) !void {
|
pub fn print(self: *StreamHandler, ch: u21) !void {
|
||||||
try self.terminal.print(c);
|
try self.terminal.print(ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn bell(self: StreamHandler) !void {
|
pub fn bell(self: StreamHandler) !void {
|
||||||
|
@ -49,7 +49,11 @@ pub fn init(
|
|||||||
|
|
||||||
// Create our event loop.
|
// Create our event loop.
|
||||||
var loop = try libuv.Loop.init(alloc);
|
var loop = try libuv.Loop.init(alloc);
|
||||||
errdefer loop.deinit(alloc);
|
errdefer {
|
||||||
|
// Run the loop once to close any of our handles
|
||||||
|
_ = loop.run(.nowait) catch 0;
|
||||||
|
loop.deinit(alloc);
|
||||||
|
}
|
||||||
loop.setData(allocPtr);
|
loop.setData(allocPtr);
|
||||||
|
|
||||||
// This async handle is used to "wake up" the renderer and force a render.
|
// This async handle is used to "wake up" the renderer and force a render.
|
||||||
|
Reference in New Issue
Block a user