mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-02 16:08:39 +00:00
add undo/redo keybindings, default them on macOS
This commit is contained in:
@@ -673,6 +673,8 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_CONFIG_CHANGE,
|
GHOSTTY_ACTION_CONFIG_CHANGE,
|
||||||
GHOSTTY_ACTION_CLOSE_WINDOW,
|
GHOSTTY_ACTION_CLOSE_WINDOW,
|
||||||
GHOSTTY_ACTION_RING_BELL,
|
GHOSTTY_ACTION_RING_BELL,
|
||||||
|
GHOSTTY_ACTION_UNDO,
|
||||||
|
GHOSTTY_ACTION_REDO,
|
||||||
GHOSTTY_ACTION_CHECK_FOR_UPDATES
|
GHOSTTY_ACTION_CHECK_FOR_UPDATES
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
|
@@ -398,11 +398,8 @@ class AppDelegate: NSObject,
|
|||||||
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
|
syncMenuShortcut(config, action: "new_split:down", menuItem: self.menuSplitDown)
|
||||||
syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp)
|
syncMenuShortcut(config, action: "new_split:up", menuItem: self.menuSplitUp)
|
||||||
|
|
||||||
// TODO: sync
|
syncMenuShortcut(config, action: "undo", menuItem: self.menuUndo)
|
||||||
menuUndo?.keyEquivalent = "z"
|
syncMenuShortcut(config, action: "redo", menuItem: self.menuRedo)
|
||||||
menuUndo?.keyEquivalentModifierMask = [.command]
|
|
||||||
menuRedo?.keyEquivalent = "z"
|
|
||||||
menuRedo?.keyEquivalentModifierMask = [.command, .shift]
|
|
||||||
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
syncMenuShortcut(config, action: "copy_to_clipboard", menuItem: self.menuCopy)
|
||||||
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
syncMenuShortcut(config, action: "paste_from_clipboard", menuItem: self.menuPaste)
|
||||||
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
syncMenuShortcut(config, action: "paste_from_selection", menuItem: self.menuPasteSelection)
|
||||||
|
@@ -428,8 +428,7 @@ class TerminalController: BaseTerminalController {
|
|||||||
undoManager.registerUndo(
|
undoManager.registerUndo(
|
||||||
withTarget: newController,
|
withTarget: newController,
|
||||||
expiresAfter: newController.undoExpiration) { target in
|
expiresAfter: newController.undoExpiration) { target in
|
||||||
// For redo, we close the tab again
|
target.closeTab(nil)
|
||||||
target.closeTabImmediately()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -459,8 +458,7 @@ class TerminalController: BaseTerminalController {
|
|||||||
undoManager.registerUndo(
|
undoManager.registerUndo(
|
||||||
withTarget: newController,
|
withTarget: newController,
|
||||||
expiresAfter: newController.undoExpiration) { target in
|
expiresAfter: newController.undoExpiration) { target in
|
||||||
// For redo, we close the window again
|
target.closeWindow(nil)
|
||||||
target.closeWindowImmediately()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -553,6 +553,12 @@ extension Ghostty {
|
|||||||
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
case GHOSTTY_ACTION_CHECK_FOR_UPDATES:
|
||||||
checkForUpdates(app)
|
checkForUpdates(app)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_UNDO:
|
||||||
|
return undo(app, target: target)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_REDO:
|
||||||
|
return redo(app, target: target)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
case GHOSTTY_ACTION_CLOSE_ALL_WINDOWS:
|
||||||
fallthrough
|
fallthrough
|
||||||
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
case GHOSTTY_ACTION_TOGGLE_TAB_OVERVIEW:
|
||||||
@@ -599,6 +605,48 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func undo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
|
||||||
|
let undoManager: UndoManager?
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
undoManager = (NSApp.delegate as? AppDelegate)?.undoManager
|
||||||
|
|
||||||
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
guard let surface = target.target.surface else { return false }
|
||||||
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||||
|
undoManager = surfaceView.undoManager
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let undoManager, undoManager.canUndo else { return false }
|
||||||
|
undoManager.undo()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func redo(_ app: ghostty_app_t, target: ghostty_target_s) -> Bool {
|
||||||
|
let undoManager: UndoManager?
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
undoManager = (NSApp.delegate as? AppDelegate)?.undoManager
|
||||||
|
|
||||||
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
guard let surface = target.target.surface else { return false }
|
||||||
|
guard let surfaceView = self.surfaceView(from: surface) else { return false }
|
||||||
|
undoManager = surfaceView.undoManager
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let undoManager, undoManager.canRedo else { return false }
|
||||||
|
undoManager.redo()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
private static func newWindow(_ app: ghostty_app_t, target: ghostty_target_s) {
|
||||||
switch (target.tag) {
|
switch (target.tag) {
|
||||||
case GHOSTTY_TARGET_APP:
|
case GHOSTTY_TARGET_APP:
|
||||||
|
@@ -4337,6 +4337,18 @@ pub fn performBindingAction(self: *Surface, action: input.Binding.Action) !bool
|
|||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
.undo => return try self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.undo,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
|
||||||
|
.redo => return try self.rt_app.performAction(
|
||||||
|
.{ .surface = self },
|
||||||
|
.redo,
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
|
||||||
.select_all => {
|
.select_all => {
|
||||||
const sel = self.io.terminal.screen.selectAll();
|
const sel = self.io.terminal.screen.selectAll();
|
||||||
if (sel) |s| {
|
if (sel) |s| {
|
||||||
|
@@ -258,6 +258,13 @@ pub const Action = union(Key) {
|
|||||||
/// it needs to ring the bell. This is usually a sound or visual effect.
|
/// it needs to ring the bell. This is usually a sound or visual effect.
|
||||||
ring_bell,
|
ring_bell,
|
||||||
|
|
||||||
|
/// Undo the last action. See the "undo" keybinding for more
|
||||||
|
/// details on what can and cannot be undone.
|
||||||
|
undo,
|
||||||
|
|
||||||
|
/// Redo the last undone action.
|
||||||
|
redo,
|
||||||
|
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
|
|
||||||
/// Sync with: ghostty_action_tag_e
|
/// Sync with: ghostty_action_tag_e
|
||||||
@@ -307,6 +314,8 @@ pub const Action = union(Key) {
|
|||||||
config_change,
|
config_change,
|
||||||
close_window,
|
close_window,
|
||||||
ring_bell,
|
ring_bell,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -26,7 +26,7 @@ pub fn genConfig(writer: anytype, cli: bool) !void {
|
|||||||
\\
|
\\
|
||||||
);
|
);
|
||||||
|
|
||||||
@setEvalBranchQuota(3000);
|
@setEvalBranchQuota(5000);
|
||||||
inline for (@typeInfo(Config).@"struct".fields) |field| {
|
inline for (@typeInfo(Config).@"struct".fields) |field| {
|
||||||
if (field.name[0] == '_') continue;
|
if (field.name[0] == '_') continue;
|
||||||
|
|
||||||
@@ -94,6 +94,7 @@ pub fn genKeybindActions(writer: anytype) !void {
|
|||||||
const info = @typeInfo(KeybindAction);
|
const info = @typeInfo(KeybindAction);
|
||||||
std.debug.assert(info == .@"union");
|
std.debug.assert(info == .@"union");
|
||||||
|
|
||||||
|
@setEvalBranchQuota(5000);
|
||||||
inline for (info.@"union".fields) |field| {
|
inline for (info.@"union".fields) |field| {
|
||||||
if (field.name[0] == '_') continue;
|
if (field.name[0] == '_') continue;
|
||||||
|
|
||||||
|
@@ -4898,6 +4898,18 @@ pub const Keybinds = struct {
|
|||||||
.{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } },
|
.{ .key = .{ .unicode = 'q' }, .mods = .{ .super = true } },
|
||||||
.{ .quit = {} },
|
.{ .quit = {} },
|
||||||
);
|
);
|
||||||
|
try self.set.putFlags(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true } },
|
||||||
|
.{ .undo = {} },
|
||||||
|
.{ .performable = true },
|
||||||
|
);
|
||||||
|
try self.set.putFlags(
|
||||||
|
alloc,
|
||||||
|
.{ .key = .{ .unicode = 'z' }, .mods = .{ .super = true, .shift = true } },
|
||||||
|
.{ .redo = {} },
|
||||||
|
.{ .performable = true },
|
||||||
|
);
|
||||||
try self.set.putFlags(
|
try self.set.putFlags(
|
||||||
alloc,
|
alloc,
|
||||||
.{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } },
|
.{ .key = .{ .unicode = 'k' }, .mods = .{ .super = true } },
|
||||||
|
@@ -655,6 +655,35 @@ pub const Action = union(enum) {
|
|||||||
/// Only implemented on macOS.
|
/// Only implemented on macOS.
|
||||||
check_for_updates,
|
check_for_updates,
|
||||||
|
|
||||||
|
/// Undo the last undoable action for the focused surface or terminal,
|
||||||
|
/// if possible. This can undo actions such as closing tabs or
|
||||||
|
/// windows.
|
||||||
|
///
|
||||||
|
/// Not every action in Ghostty can be undone or redone. The list
|
||||||
|
/// of actions support undo/redo is currently limited to:
|
||||||
|
///
|
||||||
|
/// - New window, close window
|
||||||
|
/// - New tab, close tab
|
||||||
|
/// - New split, close split
|
||||||
|
///
|
||||||
|
/// All actions are only undoable/redoable for a limited time.
|
||||||
|
/// For example, restoring a closed split can only be done for
|
||||||
|
/// some number of seconds since the split was closed. The exact
|
||||||
|
/// amount is configured with `TODO`.
|
||||||
|
///
|
||||||
|
/// The undo/redo actions being limited ensures that there is
|
||||||
|
/// bounded memory usage over time, closed surfaces don't continue running
|
||||||
|
/// in the background indefinitely, and the keybinds become available
|
||||||
|
/// for terminal applications to use.
|
||||||
|
///
|
||||||
|
/// Only implemented on macOS.
|
||||||
|
undo,
|
||||||
|
|
||||||
|
/// Redo the last undoable action for the focused surface or terminal,
|
||||||
|
/// if possible. See "undo" for more details on what can and cannot
|
||||||
|
/// be undone or redone.
|
||||||
|
redo,
|
||||||
|
|
||||||
/// Quit Ghostty.
|
/// Quit Ghostty.
|
||||||
quit,
|
quit,
|
||||||
|
|
||||||
@@ -991,6 +1020,8 @@ pub const Action = union(enum) {
|
|||||||
.toggle_secure_input,
|
.toggle_secure_input,
|
||||||
.toggle_command_palette,
|
.toggle_command_palette,
|
||||||
.reset_window_size,
|
.reset_window_size,
|
||||||
|
.undo,
|
||||||
|
.redo,
|
||||||
.crash,
|
.crash,
|
||||||
=> .surface,
|
=> .surface,
|
||||||
|
|
||||||
|
@@ -409,6 +409,18 @@ fn actionCommands(action: Action.Key) []const Command {
|
|||||||
.description = "Check for updates to the application.",
|
.description = "Check for updates to the application.",
|
||||||
}},
|
}},
|
||||||
|
|
||||||
|
.undo => comptime &.{.{
|
||||||
|
.action = .undo,
|
||||||
|
.title = "Undo",
|
||||||
|
.description = "Undo the last action.",
|
||||||
|
}},
|
||||||
|
|
||||||
|
.redo => comptime &.{.{
|
||||||
|
.action = .redo,
|
||||||
|
.title = "Redo",
|
||||||
|
.description = "Redo the last undone action.",
|
||||||
|
}},
|
||||||
|
|
||||||
.quit => comptime &.{.{
|
.quit => comptime &.{.{
|
||||||
.action = .quit,
|
.action = .quit,
|
||||||
.title = "Quit",
|
.title = "Quit",
|
||||||
|
Reference in New Issue
Block a user