macos: configurable undo timeout

This commit is contained in:
Mitchell Hashimoto
2025-06-06 12:21:05 -07:00
parent 3e02c0cbd5
commit 49cc88f0d3
4 changed files with 75 additions and 4 deletions

View File

@@ -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,

View File

@@ -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<Int8>? = nil

View File

@@ -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,

View File

@@ -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);