diff --git a/macos/Sources/Features/Terminal/BaseTerminalController.swift b/macos/Sources/Features/Terminal/BaseTerminalController.swift index e34a44941..6ea56f693 100644 --- a/macos/Sources/Features/Terminal/BaseTerminalController.swift +++ b/macos/Sources/Features/Terminal/BaseTerminalController.swift @@ -77,7 +77,7 @@ class BaseTerminalController: NSWindowController, /// The time that undo/redo operations that contain running ptys are valid for. var undoExpiration: Duration { - .seconds(5) + ghostty.config.undoTimeout } /// The undo manager for this controller is the undo manager of the window, diff --git a/macos/Sources/Ghostty/Ghostty.Config.swift b/macos/Sources/Ghostty/Ghostty.Config.swift index 3acb93c25..fcbea2a12 100644 --- a/macos/Sources/Ghostty/Ghostty.Config.swift +++ b/macos/Sources/Ghostty/Ghostty.Config.swift @@ -506,6 +506,14 @@ extension Ghostty { return v; } + var undoTimeout: Duration { + guard let config = self.config else { return .seconds(5) } + var v: UInt = 0 + let key = "undo-timeout" + _ = ghostty_config_get(config, &v, key, UInt(key.count)) + return .milliseconds(v) + } + var autoUpdate: AutoUpdate? { guard let config = self.config else { return nil } var v: UnsafePointer? = nil diff --git a/macos/Sources/Helpers/ExpiringUndoManager.swift b/macos/Sources/Helpers/ExpiringUndoManager.swift index 3eda56182..9a9349cf3 100644 --- a/macos/Sources/Helpers/ExpiringUndoManager.swift +++ b/macos/Sources/Helpers/ExpiringUndoManager.swift @@ -29,6 +29,9 @@ class ExpiringUndoManager: UndoManager { expiresAfter duration: Duration, handler: @escaping (TargetType) -> Void ) { + // Ignore instantly expiring undos + guard duration.timeInterval > 0 else { return } + let expiringTarget = ExpiringTarget( target, expiresAfter: duration, diff --git a/src/config/Config.zig b/src/config/Config.zig index fdbde692d..e1d5b548e 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1705,6 +1705,52 @@ keybind: Keybinds = .{}, /// window is ever created. Only implemented on Linux and macOS. @"initial-window": bool = true, +/// The duration that undo operations remain available. After this +/// time, the operation will be removed from the undo stack and +/// cannot be undone. +/// +/// The default value is 5 seconds. +/// +/// This timeout applies per operation, meaning that if you perform +/// multiple operations, each operation will have its own timeout. +/// New operations do not reset the timeout of previous operations. +/// +/// A timeout of zero will effectively disable undo operations. It is +/// not possible to set an infinite timeout, but you can set a very +/// large timeout to effectively disable the timeout (on the order of years). +/// This is highly discouraged, as it will cause the undo stack to grow +/// indefinitely, memory usage to grow unbounded, and terminal sessions +/// to never actually quit. +/// +/// The duration is specified as a series of numbers followed by time units. +/// Whitespace is allowed between numbers and units. Each number and unit will +/// be added together to form the total duration. +/// +/// The allowed time units are as follows: +/// +/// * `y` - 365 SI days, or 8760 hours, or 31536000 seconds. No adjustments +/// are made for leap years or leap seconds. +/// * `d` - one SI day, or 86400 seconds. +/// * `h` - one hour, or 3600 seconds. +/// * `m` - one minute, or 60 seconds. +/// * `s` - one second. +/// * `ms` - one millisecond, or 0.001 second. +/// * `us` or `µs` - one microsecond, or 0.000001 second. +/// * `ns` - one nanosecond, or 0.000000001 second. +/// +/// Examples: +/// * `1h30m` +/// * `45s` +/// +/// Units can be repeated and will be added together. This means that +/// `1h1h` is equivalent to `2h`. This is confusing and should be avoided. +/// A future update may disallow this. +/// +/// This configuration is only supported on macOS. Linux doesn't +/// support undo operations at all so this configuration has no +/// effect. +@"undo-timeout": Duration = .{ .duration = 5 * std.time.ns_per_s }, + /// The position of the "quick" terminal window. To learn more about the /// quick terminal, see the documentation for the `toggle_quick_terminal` /// binding action. @@ -6583,7 +6629,7 @@ pub const Duration = struct { if (remaining.len == 0) break; // Find the longest number - const number = number: { + const number: u64 = number: { var prev_number: ?u64 = null; var prev_remaining: ?[]const u8 = null; for (1..remaining.len + 1) |index| { @@ -6597,8 +6643,17 @@ pub const Duration = struct { break :number prev_number; } orelse return error.InvalidValue; - // A number without a unit is invalid - if (remaining.len == 0) return error.InvalidValue; + // A number without a unit is invalid unless the number is + // exactly zero. In that case, the unit is unambiguous since + // its all the same. + if (remaining.len == 0) { + if (number == 0) { + value = 0; + break; + } + + return error.InvalidValue; + } // Find the longest matching unit. Needs to be the longest matching // to distinguish 'm' from 'ms'. @@ -6808,6 +6863,11 @@ test "parse duration" { try std.testing.expectEqual(unit.factor, d.duration); } + { + const d = try Duration.parseCLI("0"); + try std.testing.expectEqual(@as(u64, 0), d.duration); + } + { const d = try Duration.parseCLI("100ns"); try std.testing.expectEqual(@as(u64, 100), d.duration);