Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add keybinding to reopen last closed tab #3822

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,10 @@ typedef struct {
bool soft;
} ghostty_action_reload_config_s;

// apprt.action.ReopenLastTab
typedef struct {
} ghostty_action_reopen_last_tab_s;

// apprt.Action.Key
typedef enum {
GHOSTTY_ACTION_NEW_WINDOW,
Expand Down Expand Up @@ -594,6 +598,7 @@ typedef enum {
GHOSTTY_ACTION_COLOR_CHANGE,
GHOSTTY_ACTION_RELOAD_CONFIG,
GHOSTTY_ACTION_CONFIG_CHANGE,
GHOSTTY_ACTION_REOPEN_LAST_TAB,
} ghostty_action_tag_e;

typedef union {
Expand All @@ -620,6 +625,7 @@ typedef union {
ghostty_action_color_change_s color_change;
ghostty_action_reload_config_s reload_config;
ghostty_action_config_change_s config_change;
ghostty_action_reopen_last_tab_s reopen_last_tab;
} ghostty_action_u;

typedef struct {
Expand Down
1 change: 1 addition & 0 deletions macos/Sources/App/macOS/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion macos/Sources/Ghostty/Ghostty.App.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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))) {
Expand Down Expand Up @@ -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)

Expand Down
8 changes: 8 additions & 0 deletions src/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@ 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,

/// 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
Expand Down Expand Up @@ -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);

Expand All @@ -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
Expand Down
29 changes: 28 additions & 1 deletion src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -724,7 +724,28 @@ 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 {
self.rt_surface.close(self.needsConfirmQuit());
// 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;
errdefer if (cwd_copy) |c| self.alloc.free(c);

const title = self.rt_surface.getTitle();
const title_copy = if (title) |t| self.alloc.dupe(u8, t) catch null else null;
errdefer if (title_copy) |t| self.alloc.free(t);

// Save terminal contents including scrollback buffer
const contents = self.io.terminal.screen.dumpStringAlloc(self.alloc, .{ .screen = .{} }) catch null;
errdefer if (contents) |c| self.alloc.free(c);

// Save to last closed tabs
self.app.last_closed_tabs.push(.{
.title = title_copy,
.cwd = cwd_copy,
.contents = contents,
});

// Close the surface
self.rt_surface.close(true);
}

/// Forces the surface to render. This is useful for when the surface
Expand Down Expand Up @@ -4007,6 +4028,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,
Expand Down
4 changes: 4 additions & 0 deletions src/apprt/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ pub const Action = union(Key) {
/// for changes.
config_change: ConfigChange,

/// Reopen the most recently closed tab.
reopen_last_tab,

/// Sync with: ghostty_action_tag_e
pub const Key = enum(c_int) {
new_window,
Expand Down Expand Up @@ -254,6 +257,7 @@ pub const Action = union(Key) {
color_change,
reload_config,
config_change,
reopen_last_tab,
};

/// Sync with: ghostty_action_u
Expand Down
89 changes: 75 additions & 14 deletions src/apprt/glfw.zig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const CoreApp = @import("../App.zig");
const CoreSurface = @import("../Surface.zig");
const configpkg = @import("../config.zig");
const Config = @import("../config.zig").Config;
const LastClosedTab = @import("../terminal/closedtabs.zig").LastClosedTab;
const sgr = @import("../terminal/sgr.zig");

// Get native API access on certain platforms so we can do more customization.
const glfwNative = glfw.Native(.{
Expand Down Expand Up @@ -159,6 +161,11 @@ pub const App = struct {
.surface => |v| v,
}),

.reopen_last_tab => try self.reopenLastTab(switch (target) {
.app => null,
.surface => |v| v,
}),

.size_limit => switch (target) {
.app => {},
.surface => |surface| try surface.rt_surface.setSizeLimits(.{
Expand Down Expand Up @@ -317,20 +324,8 @@ pub const App = struct {
win.setMonitor(monitor, 0, 0, video_mode.getWidth(), video_mode.getHeight(), 0);
}

/// 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);
fn addTab(self: *App, parent: *CoreSurface, window: *Surface) !void {
_ = self;

// Add the new window the parent window
const parent_win = glfwNative.getCocoaWindow(parent.rt_surface.window).?;
Expand All @@ -356,6 +351,71 @@ 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;
};

const last_tab_opt = parent.app.last_closed_tabs.pop() orelse {
log.warn("No last closed tab found", .{});
return;
};

// Make a mutable copy
var last_tab = last_tab_opt;
defer last_tab.deinit(parent.app.alloc);

const window = try self.newSurface(parent);

if (last_tab.cwd) |cwd| {
try window.core_surface.io.terminal.setPwd(cwd);
}

if (last_tab.title) |title| {
const title_z = try window.core_surface.alloc.dupeZ(u8, title);
defer window.core_surface.alloc.free(title_z);
try window.core_surface.rt_surface.setTitle(title_z);
}

if (last_tab.contents) |contents| {
try window.core_surface.io.terminal.printString(contents);
}

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);
Expand Down Expand Up @@ -596,6 +656,7 @@ pub const Surface = struct {
}

pub fn deinit(self: *Surface) void {
// Free the title text if it exists
if (self.title_text) |t| self.core_surface.alloc.free(t);

// Remove ourselves from the list of known surfaces in the app.
Expand Down
20 changes: 20 additions & 0 deletions src/apprt/gtk/App.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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 => {},
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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");
Expand Down
17 changes: 17 additions & 0 deletions src/apprt/gtk/Window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -382,6 +383,7 @@ fn initActions(self: *Window) void {
.{ "close", &gtkActionClose },
.{ "new_window", &gtkActionNewWindow },
.{ "new_tab", &gtkActionNewTab },
.{ "reopen_last_tab", &gtkActionReopenLastTab },
.{ "split_right", &gtkActionSplitRight },
.{ "split_down", &gtkActionSplitDown },
.{ "split_left", &gtkActionSplitLeft },
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions src/config/Config.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2046,6 +2046,13 @@ pub fn default(alloc_gpa: Allocator) Allocator.Error!Config {
.{ .open_config = {} },
);

// keybind for reopening last closed tab
try result.keybind.set.put(
alloc,
.{ .key = .{ .translated = .t }, .mods = inputpkg.ctrlOrSuper(.{ .shift = true }) },
.{ .reopen_last_tab = {} },
);

{
// On macOS we default to super but Linux ctrl+shift since
// ctrl+c is to kill the process.
Expand Down
Loading
Loading