mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
feat: add handling logic to store and retrieve closed tabs
This commit is contained in:
@ -345,6 +345,7 @@ class AppDelegate: NSObject,
|
||||
|
||||
syncMenuShortcut(config, action: "new_window", menuItem: self.menuNewWindow)
|
||||
syncMenuShortcut(config, action: "new_tab", menuItem: self.menuNewTab)
|
||||
syncMenuShortcut(config, action: "reopen_last_tab", menuItem: self.menuReopenLastTab)
|
||||
syncMenuShortcut(config, action: "close_surface", menuItem: self.menuClose)
|
||||
syncMenuShortcut(config, action: "close_window", menuItem: self.menuCloseWindow)
|
||||
syncMenuShortcut(config, action: "close_all_windows", menuItem: self.menuCloseAllWindows)
|
||||
|
@ -107,7 +107,7 @@ extension Ghostty {
|
||||
deinit {
|
||||
// This will force the didSet callbacks to run which free.
|
||||
self.app = nil
|
||||
|
||||
|
||||
#if os(macOS)
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
#endif
|
||||
@ -197,6 +197,13 @@ extension Ghostty {
|
||||
}
|
||||
}
|
||||
|
||||
func reopenLastTab(surface: ghostty_surface_t) {
|
||||
let action = "reopen_last_tab"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
logger.warning("action failed action=\(action)")
|
||||
}
|
||||
}
|
||||
|
||||
func newWindow(surface: ghostty_surface_t) {
|
||||
let action = "new_window"
|
||||
if (!ghostty_surface_binding_action(surface, action, UInt(action.count))) {
|
||||
@ -460,6 +467,9 @@ extension Ghostty {
|
||||
case GHOSTTY_ACTION_NEW_TAB:
|
||||
newTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_REOPEN_LAST_TAB:
|
||||
reopenLastTab(app, target: target)
|
||||
|
||||
case GHOSTTY_ACTION_NEW_SPLIT:
|
||||
newSplit(app, target: target, direction: action.action.new_split)
|
||||
|
||||
|
@ -24,6 +24,7 @@ const objc = @import("objc");
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
const SurfaceList = std.ArrayListUnmanaged(*apprt.Surface);
|
||||
const LastClosedTabs = @import("terminal/closedtabs.zig").LastClosedTabs;
|
||||
|
||||
/// General purpose allocator
|
||||
alloc: Allocator,
|
||||
@ -31,6 +32,9 @@ alloc: Allocator,
|
||||
/// The list of surfaces that are currently active.
|
||||
surfaces: SurfaceList,
|
||||
|
||||
/// Storage for recently closed tabs
|
||||
last_closed_tabs: LastClosedTabs = .{},
|
||||
|
||||
/// This is true if the app that Ghostty is in is focused. This may
|
||||
/// mean that no surfaces (terminals) are focused but the app is still
|
||||
/// focused, i.e. may an about window. On macOS, this concept is known
|
||||
@ -101,6 +105,7 @@ pub fn create(
|
||||
.quit = false,
|
||||
.font_grid_set = font_grid_set,
|
||||
.config_conditional_state = .{},
|
||||
.last_closed_tabs = .{},
|
||||
};
|
||||
errdefer app.surfaces.deinit(alloc);
|
||||
|
||||
@ -112,6 +117,9 @@ pub fn destroy(self: *App) void {
|
||||
for (self.surfaces.items) |surface| surface.deinit();
|
||||
self.surfaces.deinit(self.alloc);
|
||||
|
||||
// Clean up our last closed tabs
|
||||
self.last_closed_tabs.deinit(self.alloc);
|
||||
|
||||
// Clean up our font group cache
|
||||
// We should have zero items in the grid set at this point because
|
||||
// destroy only gets called when the app is shutting down and this
|
||||
@ -444,7 +452,6 @@ pub fn performAction(
|
||||
.close_all_windows => try rt_app.performAction(.app, .close_all_windows, {}),
|
||||
.toggle_quick_terminal => try rt_app.performAction(.app, .toggle_quick_terminal, {}),
|
||||
.toggle_visibility => try rt_app.performAction(.app, .toggle_visibility, {}),
|
||||
.reopen_last_tab => try rt_app.performAction(.app, .reopen_last_tab, {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -724,6 +724,25 @@ pub fn deinit(self: *Surface) void {
|
||||
/// Close this surface. This will trigger the runtime to start the
|
||||
/// close process, which should ultimately deinitialize this surface.
|
||||
pub fn close(self: *Surface) void {
|
||||
// Save tab data before closing
|
||||
const cwd = self.io.terminal.getPwd();
|
||||
const cwd_copy = if (cwd) |c| self.alloc.dupe(u8, c) catch null else null;
|
||||
|
||||
const title = self.rt_surface.getTitle();
|
||||
const title_copy = if (title) |t| self.alloc.dupe(u8, t) catch null else null;
|
||||
|
||||
// Save to last closed tabs
|
||||
self.app.last_closed_tabs.push(.{
|
||||
.title = title_copy,
|
||||
.cwd = cwd_copy,
|
||||
}, self.alloc);
|
||||
|
||||
log.debug("closing tab - pwd: {s}, title: {s}", .{
|
||||
cwd_copy orelse "(null)",
|
||||
title_copy orelse "(null)",
|
||||
});
|
||||
|
||||
log.debug("close from surface ptr={X}", .{@intFromPtr(self)});
|
||||
self.rt_surface.close(self.needsConfirmQuit());
|
||||
}
|
||||
|
||||
@ -4007,6 +4026,12 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
||||
{},
|
||||
),
|
||||
|
||||
.reopen_last_tab => try self.rt_app.performAction(
|
||||
.{ .surface = self },
|
||||
.reopen_last_tab,
|
||||
{},
|
||||
),
|
||||
|
||||
inline .previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
|
@ -22,8 +22,7 @@ const CoreApp = @import("../App.zig");
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const Config = @import("../config.zig").Config;
|
||||
|
||||
const LastClosedTabs = @import("../terminal/closedtabs.zig").LastClosedTabs;
|
||||
const LastClosedTab = @import("../terminal/closedtabs.zig").LastClosedTab;
|
||||
|
||||
// Get native API access on certain platforms so we can do more customization.
|
||||
const glfwNative = glfw.Native(.{
|
||||
@ -40,9 +39,6 @@ pub const App = struct {
|
||||
/// Mac-specific state.
|
||||
darwin: if (Darwin.enabled) Darwin else void,
|
||||
|
||||
/// Store information about the last closed tabs
|
||||
last_closed_tabs: LastClosedTabs = .{},
|
||||
|
||||
pub const Options = struct {};
|
||||
|
||||
pub fn init(core_app: *CoreApp, _: Options) !App {
|
||||
@ -110,7 +106,6 @@ pub const App = struct {
|
||||
}
|
||||
|
||||
pub fn terminate(self: *App) void {
|
||||
self.last_closed_tabs.deinit(self.app.alloc);
|
||||
self.config.deinit();
|
||||
glfw.terminate();
|
||||
}
|
||||
@ -165,7 +160,10 @@ pub const App = struct {
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.reopen_last_tab => try self.reopenLastTab(),
|
||||
.reopen_last_tab => try self.reopenLastTab(switch (target) {
|
||||
.app => null,
|
||||
.surface => |v| v,
|
||||
}),
|
||||
|
||||
.size_limit => switch (target) {
|
||||
.app => {},
|
||||
@ -325,26 +323,8 @@ pub const App = struct {
|
||||
win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
|
||||
}
|
||||
|
||||
/// Log that a reopen last tab action was triggered
|
||||
fn reopenLastTab(self: *App) !void {
|
||||
fn addTab(self: *App, parent: *CoreSurface, window: *Surface) !void {
|
||||
_ = self;
|
||||
std.log.debug("Reopen last tab action triggered", .{});
|
||||
}
|
||||
|
||||
/// Create a new tab in the parent surface.
|
||||
fn newTab(self: *App, parent_: ?*CoreSurface) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = parent_ orelse {
|
||||
_ = try self.newSurface(null);
|
||||
return;
|
||||
};
|
||||
|
||||
// Create the new window
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
// Add the new window the parent window
|
||||
const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?;
|
||||
@ -370,6 +350,66 @@ pub const App = struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Log that a reopen last tab action was triggered
|
||||
fn reopenLastTab(self: *App, parent_: ?*CoreSurface) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent: *CoreSurface = parent_ orelse {
|
||||
log.warn("No parent surface found", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
// Get the last closed tab from the app-level storage
|
||||
const last_tab: *LastClosedTab = parent.app.last_closed_tabs.getLast() orelse {
|
||||
log.warn("No last closed tab found", .{});
|
||||
return;
|
||||
};
|
||||
|
||||
// Create a new tab
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
// Set the working directory and title if available
|
||||
if (last_tab.cwd) |cwd| {
|
||||
try window.core_surface.io.terminal.setPwd(cwd);
|
||||
}
|
||||
if (last_tab.title) |title| {
|
||||
// Ensure we have a null-terminated string for the title
|
||||
const title_z = try window.core_surface.alloc.dupeZ(u8, title);
|
||||
errdefer window.core_surface.alloc.free(title_z);
|
||||
try window.core_surface.rt_surface.setTitle(title_z);
|
||||
}
|
||||
|
||||
log.debug("Reopening last tab - pwd: {s}, title: {s}", .{
|
||||
last_tab.cwd orelse "(null)",
|
||||
last_tab.title orelse "(null)",
|
||||
});
|
||||
|
||||
try self.addTab(parent, window);
|
||||
}
|
||||
|
||||
/// Create a new tab in the parent surface.
|
||||
fn newTab(self: *App, parent_: ?*CoreSurface) !void {
|
||||
if (!Darwin.enabled) {
|
||||
log.warn("tabbing is not supported on this platform", .{});
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = parent_ orelse {
|
||||
_ = try self.newSurface(null);
|
||||
return;
|
||||
};
|
||||
|
||||
log.debug("New tab: {?}", .{parent});
|
||||
|
||||
// Create the new window
|
||||
const window = try self.newSurface(parent);
|
||||
|
||||
try self.addTab(parent, window);
|
||||
}
|
||||
|
||||
fn newSurface(self: *App, parent_: ?*CoreSurface) !*Surface {
|
||||
// Grab a surface allocation because we're going to need it.
|
||||
var surface = try self.app.alloc.create(Surface);
|
||||
@ -610,26 +650,6 @@ pub const Surface = struct {
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Surface) void {
|
||||
// Save the closing tab information
|
||||
const title = if (self.title_text) |t|
|
||||
self.core_surface.alloc.dupe(u8, t) catch null
|
||||
else
|
||||
null;
|
||||
errdefer if (title) |t| self.core_surface.alloc.free(t);
|
||||
|
||||
const cwd = self.core_surface.alloc.dupe(u8, "~") catch null;
|
||||
errdefer if (cwd) |c| self.core_surface.alloc.free(c);
|
||||
|
||||
self.app.last_closed_tabs.push(.{
|
||||
.title = title,
|
||||
.cwd = cwd,
|
||||
}, self.core_surface.alloc);
|
||||
|
||||
log.debug("all last closed tab: {?}", .{self.app.last_closed_tabs.this});
|
||||
log.debug("last closed tab: {?}", .{self.app.last_closed_tabs.getLast()});
|
||||
|
||||
if (self.title_text) |t| self.core_surface.alloc.free(t);
|
||||
|
||||
// Remove ourselves from the list of known surfaces in the app.
|
||||
self.app.app.deleteSurface(self);
|
||||
|
||||
|
@ -459,6 +459,7 @@ pub fn performAction(
|
||||
.toggle_fullscreen => self.toggleFullscreen(target, value),
|
||||
|
||||
.new_tab => try self.newTab(target),
|
||||
.reopen_last_tab => try self.reopenLastTab(target),
|
||||
.goto_tab => self.gotoTab(target, value),
|
||||
.move_tab => self.moveTab(target, value),
|
||||
.new_split => try self.newSplit(target, value),
|
||||
@ -514,6 +515,23 @@ fn newTab(_: *App, target: apprt.Target) !void {
|
||||
}
|
||||
}
|
||||
|
||||
fn reopenLastTab(_: *App, target: apprt.Target) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
.surface => |v| {
|
||||
const window = v.rt_surface.container.window() orelse {
|
||||
log.info(
|
||||
"reopen_last_tab invalid for container={s}",
|
||||
.{@tagName(v.rt_surface.container)},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
window.reopenLastTab();
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn gotoTab(_: *App, target: apprt.Target, tab: apprt.action.GotoTab) void {
|
||||
switch (target) {
|
||||
.app => {},
|
||||
@ -927,6 +945,7 @@ fn syncActionAccelerators(self: *App) !void {
|
||||
try self.syncActionAccelerator("win.close", .{ .close_surface = {} });
|
||||
try self.syncActionAccelerator("win.new_window", .{ .new_window = {} });
|
||||
try self.syncActionAccelerator("win.new_tab", .{ .new_tab = {} });
|
||||
try self.syncActionAccelerator("win.reopen_last_tab", .{ .reopen_last_tab = {} });
|
||||
try self.syncActionAccelerator("win.split_right", .{ .new_split = .right });
|
||||
try self.syncActionAccelerator("win.split_down", .{ .new_split = .down });
|
||||
try self.syncActionAccelerator("win.split_left", .{ .new_split = .left });
|
||||
@ -1591,6 +1610,7 @@ fn initMenu(self: *App) void {
|
||||
c.g_menu_append_section(menu, null, @ptrCast(@alignCast(section)));
|
||||
c.g_menu_append(section, "New Window", "win.new_window");
|
||||
c.g_menu_append(section, "New Tab", "win.new_tab");
|
||||
c.g_menu_append(section, "Reopen Last Tab", "win.reopen_last_tab");
|
||||
c.g_menu_append(section, "Split Right", "win.split_right");
|
||||
c.g_menu_append(section, "Split Down", "win.split_down");
|
||||
c.g_menu_append(section, "Close Window", "win.close");
|
||||
|
@ -129,6 +129,7 @@ pub fn init(self: *Window, app: *App) !void {
|
||||
const tab_overview = c.adw_tab_overview_new();
|
||||
c.adw_tab_overview_set_view(@ptrCast(tab_overview), self.notebook.adw_tab_view);
|
||||
c.adw_tab_overview_set_enable_new_tab(@ptrCast(tab_overview), 1);
|
||||
c.adw_tab_overview_set_enable_reopen_last_tab(@ptrCast(tab_overview), 1);
|
||||
_ = c.g_signal_connect_data(
|
||||
tab_overview,
|
||||
"create-tab",
|
||||
@ -382,6 +383,7 @@ fn initActions(self: *Window) void {
|
||||
.{ "close", >kActionClose },
|
||||
.{ "new_window", >kActionNewWindow },
|
||||
.{ "new_tab", >kActionNewTab },
|
||||
.{ "reopen_last_tab", >kActionReopenLastTab },
|
||||
.{ "split_right", >kActionSplitRight },
|
||||
.{ "split_down", >kActionSplitDown },
|
||||
.{ "split_left", >kActionSplitLeft },
|
||||
@ -436,6 +438,12 @@ pub fn newTab(self: *Window, parent: ?*CoreSurface) !void {
|
||||
// does not (cursor doesn't blink) unless reactivated by refocusing.
|
||||
}
|
||||
|
||||
pub fn reopenLastTab(self: *Window) void {
|
||||
_ = self;
|
||||
|
||||
log.debug("reopen last tab", .{});
|
||||
}
|
||||
|
||||
/// Close the tab for the given notebook page. This will automatically
|
||||
/// handle closing the window if there are no more tabs.
|
||||
pub fn closeTab(self: *Window, tab: *Tab) void {
|
||||
@ -810,6 +818,15 @@ fn gtkActionNewTab(
|
||||
gtkTabNewClick(undefined, ud);
|
||||
}
|
||||
|
||||
fn gtkActionReopenLastTab(
|
||||
_: *c.GSimpleAction,
|
||||
_: *c.GVariant,
|
||||
ud: ?*anyopaque,
|
||||
) callconv(.C) void {
|
||||
const self: *Window = @ptrCast(@alignCast(ud orelse return));
|
||||
self.reopenLastTab();
|
||||
}
|
||||
|
||||
fn gtkActionSplitRight(
|
||||
_: *c.GSimpleAction,
|
||||
_: *c.GVariant,
|
||||
|
@ -628,7 +628,6 @@ pub const Action = union(enum) {
|
||||
.quit,
|
||||
.toggle_quick_terminal,
|
||||
.toggle_visibility,
|
||||
.reopen_last_tab,
|
||||
=> .app,
|
||||
|
||||
// These are app but can be special-cased in a surface context.
|
||||
@ -673,6 +672,7 @@ pub const Action = union(enum) {
|
||||
// come from. For example `new_window` needs to be sourced to
|
||||
// a surface so inheritance can be done correctly.
|
||||
.new_tab,
|
||||
.reopen_last_tab,
|
||||
.previous_tab,
|
||||
.next_tab,
|
||||
.last_tab,
|
||||
@ -896,6 +896,7 @@ pub const Key = enum(c_int) {
|
||||
paste_from_clipboard,
|
||||
new_tab,
|
||||
new_window,
|
||||
reopen_last_tab,
|
||||
};
|
||||
|
||||
/// Trigger is the associated key state that can trigger an action.
|
||||
|
Reference in New Issue
Block a user