From 07124dba64096a36a84b76cab05a937ea3e2aeac Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Wed, 1 Oct 2025 23:48:08 -0500 Subject: [PATCH 1/2] core: add 'command finished' notifications Fixes #8991 Uses OSC 133 esc sequences to keep track of how long commands take to execute. If the user chooses, commands that take longer than a user specified limit will trigger a notification. The user can choose between a bell notification or a desktop notification. --- include/ghostty.h | 10 +++ src/Surface.zig | 44 +++++++++++ src/apprt/action.zig | 22 ++++++ src/apprt/gtk/class/application.zig | 31 +++++--- src/apprt/gtk/class/split_tree.zig | 2 +- src/apprt/gtk/class/surface.zig | 118 +++++++++++++++++++++++++++- src/apprt/gtk/class/tab.zig | 2 +- src/apprt/gtk/ext/actions.zig | 51 ++++++++++-- src/apprt/gtk/ui/1.2/surface.blp | 5 ++ src/apprt/surface.zig | 8 ++ src/config/Config.zig | 93 ++++++++++++++++++++++ src/termio/stream_handler.zig | 5 ++ 12 files changed, 366 insertions(+), 25 deletions(-) diff --git a/include/ghostty.h b/include/ghostty.h index 3f1e0c9d9..48836ee96 100644 --- a/include/ghostty.h +++ b/include/ghostty.h @@ -733,6 +733,14 @@ typedef struct { int8_t progress; } ghostty_action_progress_report_s; +// apprt.action.CommandFinished.C +typedef struct { + // -1 if no exit code was reported, otherwise 0-255 + int16_t exit_code; + // number of nanoseconds that command was running for + uint64_t duration; +} ghostty_action_command_finished_s; + // apprt.Action.Key typedef enum { GHOSTTY_ACTION_QUIT, @@ -788,6 +796,7 @@ typedef enum { GHOSTTY_ACTION_SHOW_CHILD_EXITED, GHOSTTY_ACTION_PROGRESS_REPORT, GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD, + GHOSTTY_ACTION_COMMAND_FINISHED, } ghostty_action_tag_e; typedef union { @@ -819,6 +828,7 @@ typedef union { ghostty_action_close_tab_mode_e close_tab_mode; ghostty_surface_message_childexited_s child_exited; ghostty_action_progress_report_s progress_report; + ghostty_action_command_finished_s command_finished; } ghostty_action_u; typedef struct { diff --git a/src/Surface.zig b/src/Surface.zig index 03974dfc6..637af80cb 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -33,6 +33,7 @@ const font = @import("font/main.zig"); const Command = @import("Command.zig"); const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); +const Duration = configpkg.Config.Duration; const input = @import("input.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); @@ -147,6 +148,13 @@ focused: bool = true, /// Used to determine whether to continuously scroll. selection_scroll_active: bool = false, +/// Used to send notifications that long running commands have finished. +/// Requires that shell integration be active. Should represent a nanosecond +/// precision timestamp. It does not necessarily need to correspond to the +/// actual time, but we must be able to compare two subsequent timestamps to get +/// the wall clock time that has elapsed between timestamps. +command_timer: ?i128 = null, + /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key /// input can be forwarded to the OS for further processing if it @@ -280,6 +288,9 @@ const DerivedConfig = struct { links: []Link, link_previews: configpkg.LinkPreviews, scroll_to_bottom: configpkg.Config.ScrollToBottom, + notify_on_command_finish: configpkg.Config.NotifyOnCommandFinish, + notify_on_command_finish_action: configpkg.Config.NotifyOnCommandFinishAction, + notify_on_command_finish_after: Duration, const Link = struct { regex: oni.Regex, @@ -350,6 +361,9 @@ const DerivedConfig = struct { .links = links, .link_previews = config.@"link-previews", .scroll_to_bottom = config.@"scroll-to-bottom", + .notify_on_command_finish = config.@"notify-on-command-finish", + .notify_on_command_finish_action = config.@"notify-on-command-finish-action", + .notify_on_command_finish_after = config.@"notify-on-command-finish-after", // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. @@ -984,6 +998,36 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { self.selection_scroll_active = active; try self.selectionScrollTick(); }, + + .start_command_timer => { + self.command_timer = std.time.nanoTimestamp(); + }, + + .stop_command_timer => |v| timer: { + const end = std.time.nanoTimestamp(); + const start = self.command_timer orelse break :timer; + self.command_timer = null; + + const difference = end - start; + + // skip obviously silly results + if (difference < 0) break :timer; + if (difference > std.math.maxInt(u64)) break :timer; + + const duration: Duration = .{ .duration = @intCast(difference) }; + log.debug("command took {}", .{duration}); + + _ = self.rt_app.performAction( + .{ .surface = self }, + .command_finished, + .{ + .exit_code = v, + .duration = duration, + }, + ) catch |err| { + log.warn("apprt failed to notify command finish={}", .{err}); + }; + }, } } diff --git a/src/apprt/action.zig b/src/apprt/action.zig index b7dc80e03..b356ff32f 100644 --- a/src/apprt/action.zig +++ b/src/apprt/action.zig @@ -295,6 +295,9 @@ pub const Action = union(Key) { /// Show the on-screen keyboard. show_on_screen_keyboard, + /// A command has finished, + command_finished: CommandFinished, + /// Sync with: ghostty_action_tag_e pub const Key = enum(c_int) { quit, @@ -350,6 +353,7 @@ pub const Action = union(Key) { show_child_exited, progress_report, show_on_screen_keyboard, + command_finished, }; /// Sync with: ghostty_action_u @@ -741,3 +745,21 @@ pub const CloseTabMode = enum(c_int) { /// Close all other tabs. other, }; + +pub const CommandFinished = struct { + exit_code: ?u8, + duration: configpkg.Config.Duration, + + /// sync with ghostty_action_command_finished_s in ghostty.h + pub const C = extern struct { + exit_code: i16, + duration: u64, + }; + + pub fn cval(self: CommandFinished) C { + return .{ + .exit_code = self.exit_code orelse -1, + .duration = self.duration.duration, + }; + } +}; diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index f7ed0d38c..90c72681d 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -713,6 +713,7 @@ pub const Application = extern struct { .toggle_command_palette => return Action.toggleCommandPalette(target), .toggle_split_zoom => return Action.toggleSplitZoom(target), .show_on_screen_keyboard => return Action.showOnScreenKeyboard(target), + .command_finished => return Action.commandFinished(target, value), // Unimplemented .secure_input, @@ -1824,13 +1825,13 @@ const Action = struct { target: apprt.Target, n: apprt.action.DesktopNotification, ) void { - // TODO: We should move the surface target to a function call - // on Surface and emit a signal that embedders can connect to. This - // will let us handle notifications differently depending on where - // a surface is presented. At the time of writing this, we always - // want to show the notification AND the logic below was directly - // ported from "legacy" GTK so this is fine, but I want to leave this - // note so we can do it one day. + switch (target) { + .app => {}, + .surface => |v| { + v.rt_surface.gobj().sendDesktopNotification(n.title, n.body); + return; + }, + } // Set a default title if we don't already have one const t = switch (n.title.len) { @@ -1845,14 +1846,9 @@ const Action = struct { const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); defer icon.unref(); notification.setIcon(icon.as(gio.Icon)); - - const pointer = glib.Variant.newUint64(switch (target) { - .app => 0, - .surface => |v| @intFromPtr(v), - }); notification.setDefaultActionAndTargetValue( "app.present-surface", - pointer, + glib.Variant.newUint64(0), ); // We set the notification ID to the body content. If the content is the @@ -2457,6 +2453,15 @@ const Action = struct { }, } } + + pub fn commandFinished(target: apprt.Target, value: apprt.Action.Value(.command_finished)) bool { + switch (target) { + .app => return false, + .surface => |surface| { + return surface.rt_surface.gobj().commandFinished(value); + }, + } + } }; /// This sets various GTK-related environment variables as necessary diff --git a/src/apprt/gtk/class/split_tree.zig b/src/apprt/gtk/class/split_tree.zig index 755b51e9a..977a7eab2 100644 --- a/src/apprt/gtk/class/split_tree.zig +++ b/src/apprt/gtk/class/split_tree.zig @@ -198,7 +198,7 @@ pub const SplitTree = extern struct { .init("zoom", actionZoom, null), }; - ext.actions.addAsGroup(Self, self, "split-tree", &actions); + _ = ext.actions.addAsGroup(Self, self, "split-tree", &actions); } /// Create a new split in the given direction from the currently diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index 344bf8f21..401e542e4 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -32,6 +32,7 @@ const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog; const Window = @import("window.zig").Window; const WeakRef = @import("../weak_ref.zig").WeakRef; const InspectorWindow = @import("inspector_window.zig").InspectorWindow; +const i18n = @import("../../../os/i18n.zig"); const log = std.log.scoped(.gtk_ghostty_surface); @@ -545,6 +546,8 @@ pub const Surface = extern struct { // unfocused-split-* options is_split: bool = false, + action_group: ?*gio.SimpleActionGroup = null, + // Template binds child_exited_overlay: *ChildExited, context_menu: *gtk.PopoverMenu, @@ -809,6 +812,63 @@ pub const Surface = extern struct { ); } + pub fn commandFinished(self: *Self, value: apprt.Action.Value(.command_finished)) bool { + const app = Application.default(); + const alloc = app.allocator(); + const priv: *Private = self.private(); + + const notify_next_command_finish = notify: { + const simple_action_group = priv.action_group orelse break :notify false; + const action_group = simple_action_group.as(gio.ActionGroup); + const state = action_group.getActionState("notify-on-next-command-finish") orelse break :notify false; + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) break :notify false; + const notify = state.getBoolean() != 0; + action_group.changeActionState("notify-on-next-command-finish", glib.Variant.newBoolean(@intFromBool(false))); + break :notify notify; + }; + + const config = priv.config orelse return false; + + const cfg = config.get(); + + if (!notify_next_command_finish) { + if (cfg.@"notify-on-command-finish" == .never) return true; + if (cfg.@"notify-on-command-finish" == .unfocused and self.getFocused()) return true; + } + + const action = cfg.@"notify-on-command-finish-action"; + + if (action.bell) self.setBellRinging(true); + + if (action.notify) notify: { + const title_ = title: { + const exit_code = value.exit_code orelse break :title i18n._("Command Finished"); + if (exit_code == 0) break :title i18n._("Command Succeeded"); + break :title i18n._("Command Failed"); + }; + const title = std.mem.span(title_); + const body = body: { + const exit_code = value.exit_code orelse break :body std.fmt.allocPrintZ( + alloc, + "Command took {}.", + .{value.duration.round(std.time.ns_per_ms)}, + ) catch break :notify; + break :body std.fmt.allocPrintZ( + alloc, + "Command took {} and exited with code {d}.", + .{ value.duration.round(std.time.ns_per_ms), exit_code }, + ) catch break :notify; + }; + defer alloc.free(body); + + self.sendDesktopNotification(title, body); + } + + return true; + } + /// Key press event (press or release). /// /// At a high level, we want to construct an `input.KeyEvent` and @@ -1404,6 +1464,34 @@ pub const Surface = extern struct { _ = priv.gl_area.as(gtk.Widget).grabFocus(); } + pub fn sendDesktopNotification(self: *Self, title: [:0]const u8, body: [:0]const u8) void { + const app = Application.default(); + + const t = switch (title.len) { + 0 => "Ghostty", + else => title, + }; + + const notification = gio.Notification.new(t); + defer notification.unref(); + notification.setBody(body); + + const icon = gio.ThemedIcon.new("com.mitchellh.ghostty"); + defer icon.unref(); + notification.setIcon(icon.as(gio.Icon)); + + const pointer = glib.Variant.newUint64(@intFromPtr(self)); + notification.setDefaultActionAndTargetValue( + "app.present-surface", + pointer, + ); + + // We set the notification ID to the body content. If the content is the + // same, this notification may replace a previous notification + const gio_app = app.as(gio.Application); + gio_app.sendNotification(body, notification); + } + //--------------------------------------------------------------- // Virtual Methods @@ -1460,11 +1548,23 @@ pub const Surface = extern struct { } fn initActionMap(self: *Self) void { + const priv: *Private = self.private(); + const actions = [_]ext.actions.Action(Self){ - .init("prompt-title", actionPromptTitle, null), + .init( + "prompt-title", + actionPromptTitle, + null, + ), + .initStateful( + "notify-on-next-command-finish", + actionNotifyOnNextCommandFinish, + null, + glib.Variant.newBoolean(@intFromBool(false)), + ), }; - ext.actions.addAsGroup(Self, self, "surface", &actions); + priv.action_group = ext.actions.addAsGroup(Self, self, "surface", &actions); } fn dispose(self: *Self) callconv(.c) void { @@ -1966,6 +2066,20 @@ pub const Surface = extern struct { }; } + pub fn actionNotifyOnNextCommandFinish( + action: *gio.SimpleAction, + _: ?*glib.Variant, + _: *Self, + ) callconv(.c) void { + const state = action.as(gio.Action).getState() orelse glib.Variant.newBoolean(@intFromBool(false)); + defer state.unref(); + const bool_variant_type = glib.ext.VariantType.newFor(bool); + defer bool_variant_type.free(); + if (state.isOfType(bool_variant_type) == 0) return; + const value = state.getBoolean() != 0; + action.setState(glib.Variant.newBoolean(@intFromBool(!value))); + } + fn childExitedClose( _: *ChildExited, self: *Self, diff --git a/src/apprt/gtk/class/tab.zig b/src/apprt/gtk/class/tab.zig index d8f9b97f8..373507507 100644 --- a/src/apprt/gtk/class/tab.zig +++ b/src/apprt/gtk/class/tab.zig @@ -206,7 +206,7 @@ pub const Tab = extern struct { .init("ring-bell", actionRingBell, null), }; - ext.actions.addAsGroup(Self, self, "tab", &actions); + _ = ext.actions.addAsGroup(Self, self, "tab", &actions); } //--------------------------------------------------------------- diff --git a/src/apprt/gtk/ext/actions.zig b/src/apprt/gtk/ext/actions.zig index 8499e7de8..344c08e05 100644 --- a/src/apprt/gtk/ext/actions.zig +++ b/src/apprt/gtk/ext/actions.zig @@ -40,14 +40,21 @@ test "gActionNameIsValid" { /// Function to create a structure for describing an action. pub fn Action(comptime T: type) type { return struct { + const Self = @This(); pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void; name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType, + state: ?*glib.Variant = null, - /// Function to initialize a new action so that we can comptime check the name. - pub fn init(comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType) @This() { + /// Function to initialize a new action so that we can comptime check + /// the name. + pub fn init( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + ) Self { comptime assert(gActionNameIsValid(name)); return .{ @@ -56,6 +63,23 @@ pub fn Action(comptime T: type) type { .parameter_type = parameter_type, }; } + + /// Function to initialize a new stateful action so that we can comptime + /// check the name. + pub fn initStateful( + comptime name: [:0]const u8, + callback: Callback, + parameter_type: ?*const glib.VariantType, + state: *glib.Variant, + ) Self { + comptime assert(gActionNameIsValid(name)); + return .{ + .name = name, + .callback = callback, + .parameter_type = parameter_type, + .state = state, + }; + } }; } @@ -68,10 +92,19 @@ pub fn add(comptime T: type, self: *T, actions: []const Action(T)) void { pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void { for (actions) |entry| { assert(gActionNameIsValid(entry.name)); - const action = gio.SimpleAction.new( - entry.name, - entry.parameter_type, - ); + const action = action: { + if (entry.state) |state| { + break :action gio.SimpleAction.newStateful( + entry.name, + entry.parameter_type, + state, + ); + } + break :action gio.SimpleAction.new( + entry.name, + entry.parameter_type, + ); + }; defer action.unref(); _ = gio.SimpleAction.signals.activate.connect( action, @@ -85,7 +118,7 @@ pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []cons } /// Add actions to a widget that doesn't implement ActionGroup directly. -pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) void { +pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actions: []const Action(T)) *gio.SimpleActionGroup { comptime assert(gActionNameIsValid(name)); // Collect our actions into a group since we're just a plain widget that @@ -99,6 +132,8 @@ pub fn addAsGroup(comptime T: type, self: *T, comptime name: [:0]const u8, actio name, group.as(gio.ActionGroup), ); + + return group; } test "adding actions to an object" { @@ -138,7 +173,7 @@ test "adding actions to an object" { .init("test", callbacks.callback, i32_variant_type), }; - addAsGroup(gtk.Box, box, "test", &actions); + _ = addAsGroup(gtk.Box, box, "test", &actions); } const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31)); diff --git a/src/apprt/gtk/ui/1.2/surface.blp b/src/apprt/gtk/ui/1.2/surface.blp index 7ed78ecb3..84e00ac4a 100644 --- a/src/apprt/gtk/ui/1.2/surface.blp +++ b/src/apprt/gtk/ui/1.2/surface.blp @@ -203,6 +203,11 @@ menu context_menu_model { label: _("Paste"); action: "win.paste"; } + + item { + label: _("Notify on Next Command Finish"); + action: "surface.notify-on-next-command-finish"; + } } section { diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index e4effe128..70866c609 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -96,6 +96,14 @@ pub const Message = union(enum) { /// Report the progress of an action using a GUI element progress_report: terminal.osc.Command.ProgressReport, + /// A command has started in the shell, start a timer. + start_command_timer, + + /// A command has finished in the shell, stop the timer and send out + /// notifications as appropriate. The optional u8 is the exit code + /// of the command. + stop_command_timer: ?u8, + pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/config/Config.zig b/src/config/Config.zig index b7a9699c8..f33936e00 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1004,6 +1004,82 @@ command: ?Command = null, /// manually. @"initial-command": ?Command = null, +/// Controls when command finished notifications are sent. There are +/// three options: +/// +/// * `never` - Never send notifications (the default). +/// * `unfocused` - Only send notifications if the surface that the command is +/// running in is not focused. +/// * `always` - Always send notifications. +/// +/// Command finished notifications requires that either shell integration is +/// enabled, or that your shell sends OSC 133 escape sequences to mark the start +/// and end of commands. +/// +/// On GTK, there is a context menu item that will enable command finished +/// notifications for a single command, overriding the `never` and `unfocused` +/// options. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish": NotifyOnCommandFinish = .never, + +/// If command finished notifications are enabled, this controls how the user is +/// notified. +/// +/// Available options: +/// +/// * `bell` - enabled by default +/// * `notify` - disabled by default +/// +/// Options can be combined by listing them as a comma separated list. Options +/// can be negated by prefixing them with `no-`. For example `no-bell,notify`. +/// +/// GTK only. +/// +/// Available since 1.3.0. +@"notify-on-command-finish-action": NotifyOnCommandFinishAction = .{ + .bell = true, + .notify = false, +}, + +/// If command finished notifications are enabled, this controls how long a +/// command must have been running before a notification will be sent. The +/// default is five seconds. +/// +/// 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. +/// +/// The maximum value is `584y 49w 23h 34m 33s 709ms 551µs 615ns`. Any +/// value larger than this will be clamped to the maximum value. +/// +/// GTK only. +/// +/// Available since 1.3.0 +@"notify-on-command-finish-after": Duration = .{ .duration = 5 * std.time.ns_per_s }, + /// Extra environment variables to pass to commands launched in a terminal /// surface. The format is `env=KEY=VALUE`. /// @@ -8165,6 +8241,10 @@ pub const Duration = struct { return .{ .duration = self.duration / to * to }; } + pub fn lte(self: Duration, other: Duration) bool { + return self.duration <= other.duration; + } + pub fn parseCLI(input: ?[]const u8) !Duration { var remaining = input orelse return error.ValueRequired; @@ -8378,6 +8458,19 @@ pub const ScrollToBottom = packed struct { pub const default: ScrollToBottom = .{}; }; +/// See notify-on-command-finish +pub const NotifyOnCommandFinish = enum { + never, + unfocused, + always, +}; + +/// See notify-on-command-finish-action +pub const NotifyOnCommandFinishAction = packed struct { + bell: bool = true, + notify: bool = false, +}; + test "parse duration" { inline for (Duration.units) |unit| { var buf: [16]u8 = undefined; diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index 2d90831f2..b2b2af3d0 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1054,6 +1054,11 @@ pub const StreamHandler = struct { pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); + self.surfaceMessageWriter(.start_command_timer); + } + + pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { + self.surfaceMessageWriter(.{ .stop_command_timer = exit_code }); } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void { From 1c23ebc6f16941237a567d3c1c93ec9c6d4d24d2 Mon Sep 17 00:00:00 2001 From: "Jeffrey C. Ollie" Date: Thu, 2 Oct 2025 12:57:53 -0500 Subject: [PATCH 2/2] address review comments --- src/Surface.zig | 18 ++++++------------ src/apprt/surface.zig | 4 ++-- src/termio/stream_handler.zig | 4 ++-- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 637af80cb..3b4bf872f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -153,7 +153,7 @@ selection_scroll_active: bool = false, /// precision timestamp. It does not necessarily need to correspond to the /// actual time, but we must be able to compare two subsequent timestamps to get /// the wall clock time that has elapsed between timestamps. -command_timer: ?i128 = null, +command_timer: ?std.time.Instant = null, /// The effect of an input event. This can be used by callers to take /// the appropriate action after an input event. For example, key @@ -999,22 +999,16 @@ pub fn handleMessage(self: *Surface, msg: Message) !void { try self.selectionScrollTick(); }, - .start_command_timer => { - self.command_timer = std.time.nanoTimestamp(); + .start_command => { + self.command_timer = try .now(); }, - .stop_command_timer => |v| timer: { - const end = std.time.nanoTimestamp(); + .stop_command => |v| timer: { + const end: std.time.Instant = try .now(); const start = self.command_timer orelse break :timer; self.command_timer = null; - const difference = end - start; - - // skip obviously silly results - if (difference < 0) break :timer; - if (difference > std.math.maxInt(u64)) break :timer; - - const duration: Duration = .{ .duration = @intCast(difference) }; + const duration: Duration = .{ .duration = end.since(start) }; log.debug("command took {}", .{duration}); _ = self.rt_app.performAction( diff --git a/src/apprt/surface.zig b/src/apprt/surface.zig index 70866c609..a46732c16 100644 --- a/src/apprt/surface.zig +++ b/src/apprt/surface.zig @@ -97,12 +97,12 @@ pub const Message = union(enum) { progress_report: terminal.osc.Command.ProgressReport, /// A command has started in the shell, start a timer. - start_command_timer, + start_command, /// A command has finished in the shell, stop the timer and send out /// notifications as appropriate. The optional u8 is the exit code /// of the command. - stop_command_timer: ?u8, + stop_command: ?u8, pub const ReportTitleStyle = enum { csi_21_t, diff --git a/src/termio/stream_handler.zig b/src/termio/stream_handler.zig index b2b2af3d0..9a7e8b416 100644 --- a/src/termio/stream_handler.zig +++ b/src/termio/stream_handler.zig @@ -1054,11 +1054,11 @@ pub const StreamHandler = struct { pub inline fn endOfInput(self: *StreamHandler) !void { self.terminal.markSemanticPrompt(.command); - self.surfaceMessageWriter(.start_command_timer); + self.surfaceMessageWriter(.start_command); } pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void { - self.surfaceMessageWriter(.{ .stop_command_timer = exit_code }); + self.surfaceMessageWriter(.{ .stop_command = exit_code }); } pub fn reportPwd(self: *StreamHandler, url: []const u8) !void {