mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-09 19:36:45 +00:00
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.
This commit is contained in:
@@ -733,6 +733,14 @@ typedef struct {
|
|||||||
int8_t progress;
|
int8_t progress;
|
||||||
} ghostty_action_progress_report_s;
|
} 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
|
// apprt.Action.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
GHOSTTY_ACTION_QUIT,
|
GHOSTTY_ACTION_QUIT,
|
||||||
@@ -788,6 +796,7 @@ typedef enum {
|
|||||||
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
GHOSTTY_ACTION_SHOW_CHILD_EXITED,
|
||||||
GHOSTTY_ACTION_PROGRESS_REPORT,
|
GHOSTTY_ACTION_PROGRESS_REPORT,
|
||||||
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
GHOSTTY_ACTION_SHOW_ON_SCREEN_KEYBOARD,
|
||||||
|
GHOSTTY_ACTION_COMMAND_FINISHED,
|
||||||
} ghostty_action_tag_e;
|
} ghostty_action_tag_e;
|
||||||
|
|
||||||
typedef union {
|
typedef union {
|
||||||
@@ -819,6 +828,7 @@ typedef union {
|
|||||||
ghostty_action_close_tab_mode_e close_tab_mode;
|
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||||
ghostty_surface_message_childexited_s child_exited;
|
ghostty_surface_message_childexited_s child_exited;
|
||||||
ghostty_action_progress_report_s progress_report;
|
ghostty_action_progress_report_s progress_report;
|
||||||
|
ghostty_action_command_finished_s command_finished;
|
||||||
} ghostty_action_u;
|
} ghostty_action_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
@@ -33,6 +33,7 @@ const font = @import("font/main.zig");
|
|||||||
const Command = @import("Command.zig");
|
const Command = @import("Command.zig");
|
||||||
const terminal = @import("terminal/main.zig");
|
const terminal = @import("terminal/main.zig");
|
||||||
const configpkg = @import("config.zig");
|
const configpkg = @import("config.zig");
|
||||||
|
const Duration = configpkg.Config.Duration;
|
||||||
const input = @import("input.zig");
|
const input = @import("input.zig");
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const internal_os = @import("os/main.zig");
|
const internal_os = @import("os/main.zig");
|
||||||
@@ -147,6 +148,13 @@ focused: bool = true,
|
|||||||
/// Used to determine whether to continuously scroll.
|
/// Used to determine whether to continuously scroll.
|
||||||
selection_scroll_active: bool = false,
|
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 effect of an input event. This can be used by callers to take
|
||||||
/// the appropriate action after an input event. For example, key
|
/// the appropriate action after an input event. For example, key
|
||||||
/// input can be forwarded to the OS for further processing if it
|
/// input can be forwarded to the OS for further processing if it
|
||||||
@@ -280,6 +288,9 @@ const DerivedConfig = struct {
|
|||||||
links: []Link,
|
links: []Link,
|
||||||
link_previews: configpkg.LinkPreviews,
|
link_previews: configpkg.LinkPreviews,
|
||||||
scroll_to_bottom: configpkg.Config.ScrollToBottom,
|
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 {
|
const Link = struct {
|
||||||
regex: oni.Regex,
|
regex: oni.Regex,
|
||||||
@@ -350,6 +361,9 @@ const DerivedConfig = struct {
|
|||||||
.links = links,
|
.links = links,
|
||||||
.link_previews = config.@"link-previews",
|
.link_previews = config.@"link-previews",
|
||||||
.scroll_to_bottom = config.@"scroll-to-bottom",
|
.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
|
// Assignments happen sequentially so we have to do this last
|
||||||
// so that the memory is captured from allocs above.
|
// 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;
|
self.selection_scroll_active = active;
|
||||||
try self.selectionScrollTick();
|
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});
|
||||||
|
};
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -295,6 +295,9 @@ pub const Action = union(Key) {
|
|||||||
/// Show the on-screen keyboard.
|
/// Show the on-screen keyboard.
|
||||||
show_on_screen_keyboard,
|
show_on_screen_keyboard,
|
||||||
|
|
||||||
|
/// A command has finished,
|
||||||
|
command_finished: CommandFinished,
|
||||||
|
|
||||||
/// Sync with: ghostty_action_tag_e
|
/// Sync with: ghostty_action_tag_e
|
||||||
pub const Key = enum(c_int) {
|
pub const Key = enum(c_int) {
|
||||||
quit,
|
quit,
|
||||||
@@ -350,6 +353,7 @@ pub const Action = union(Key) {
|
|||||||
show_child_exited,
|
show_child_exited,
|
||||||
progress_report,
|
progress_report,
|
||||||
show_on_screen_keyboard,
|
show_on_screen_keyboard,
|
||||||
|
command_finished,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Sync with: ghostty_action_u
|
/// Sync with: ghostty_action_u
|
||||||
@@ -741,3 +745,21 @@ pub const CloseTabMode = enum(c_int) {
|
|||||||
/// Close all other tabs.
|
/// Close all other tabs.
|
||||||
other,
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@@ -713,6 +713,7 @@ pub const Application = extern struct {
|
|||||||
.toggle_command_palette => return Action.toggleCommandPalette(target),
|
.toggle_command_palette => return Action.toggleCommandPalette(target),
|
||||||
.toggle_split_zoom => return Action.toggleSplitZoom(target),
|
.toggle_split_zoom => return Action.toggleSplitZoom(target),
|
||||||
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
|
.show_on_screen_keyboard => return Action.showOnScreenKeyboard(target),
|
||||||
|
.command_finished => return Action.commandFinished(target, value),
|
||||||
|
|
||||||
// Unimplemented
|
// Unimplemented
|
||||||
.secure_input,
|
.secure_input,
|
||||||
@@ -1824,13 +1825,13 @@ const Action = struct {
|
|||||||
target: apprt.Target,
|
target: apprt.Target,
|
||||||
n: apprt.action.DesktopNotification,
|
n: apprt.action.DesktopNotification,
|
||||||
) void {
|
) void {
|
||||||
// TODO: We should move the surface target to a function call
|
switch (target) {
|
||||||
// on Surface and emit a signal that embedders can connect to. This
|
.app => {},
|
||||||
// will let us handle notifications differently depending on where
|
.surface => |v| {
|
||||||
// a surface is presented. At the time of writing this, we always
|
v.rt_surface.gobj().sendDesktopNotification(n.title, n.body);
|
||||||
// want to show the notification AND the logic below was directly
|
return;
|
||||||
// ported from "legacy" GTK so this is fine, but I want to leave this
|
},
|
||||||
// note so we can do it one day.
|
}
|
||||||
|
|
||||||
// Set a default title if we don't already have one
|
// Set a default title if we don't already have one
|
||||||
const t = switch (n.title.len) {
|
const t = switch (n.title.len) {
|
||||||
@@ -1845,14 +1846,9 @@ const Action = struct {
|
|||||||
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
|
const icon = gio.ThemedIcon.new("com.mitchellh.ghostty");
|
||||||
defer icon.unref();
|
defer icon.unref();
|
||||||
notification.setIcon(icon.as(gio.Icon));
|
notification.setIcon(icon.as(gio.Icon));
|
||||||
|
|
||||||
const pointer = glib.Variant.newUint64(switch (target) {
|
|
||||||
.app => 0,
|
|
||||||
.surface => |v| @intFromPtr(v),
|
|
||||||
});
|
|
||||||
notification.setDefaultActionAndTargetValue(
|
notification.setDefaultActionAndTargetValue(
|
||||||
"app.present-surface",
|
"app.present-surface",
|
||||||
pointer,
|
glib.Variant.newUint64(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
// We set the notification ID to the body content. If the content is the
|
// 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
|
/// This sets various GTK-related environment variables as necessary
|
||||||
|
@@ -198,7 +198,7 @@ pub const SplitTree = extern struct {
|
|||||||
.init("zoom", actionZoom, null),
|
.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
|
/// Create a new split in the given direction from the currently
|
||||||
|
@@ -32,6 +32,7 @@ const TitleDialog = @import("surface_title_dialog.zig").SurfaceTitleDialog;
|
|||||||
const Window = @import("window.zig").Window;
|
const Window = @import("window.zig").Window;
|
||||||
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
const WeakRef = @import("../weak_ref.zig").WeakRef;
|
||||||
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
const InspectorWindow = @import("inspector_window.zig").InspectorWindow;
|
||||||
|
const i18n = @import("../../../os/i18n.zig");
|
||||||
|
|
||||||
const log = std.log.scoped(.gtk_ghostty_surface);
|
const log = std.log.scoped(.gtk_ghostty_surface);
|
||||||
|
|
||||||
@@ -545,6 +546,8 @@ pub const Surface = extern struct {
|
|||||||
// unfocused-split-* options
|
// unfocused-split-* options
|
||||||
is_split: bool = false,
|
is_split: bool = false,
|
||||||
|
|
||||||
|
action_group: ?*gio.SimpleActionGroup = null,
|
||||||
|
|
||||||
// Template binds
|
// Template binds
|
||||||
child_exited_overlay: *ChildExited,
|
child_exited_overlay: *ChildExited,
|
||||||
context_menu: *gtk.PopoverMenu,
|
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).
|
/// Key press event (press or release).
|
||||||
///
|
///
|
||||||
/// At a high level, we want to construct an `input.KeyEvent` and
|
/// 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();
|
_ = 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
|
// Virtual Methods
|
||||||
|
|
||||||
@@ -1460,11 +1548,23 @@ pub const Surface = extern struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn initActionMap(self: *Self) void {
|
fn initActionMap(self: *Self) void {
|
||||||
|
const priv: *Private = self.private();
|
||||||
|
|
||||||
const actions = [_]ext.actions.Action(Self){
|
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 {
|
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(
|
fn childExitedClose(
|
||||||
_: *ChildExited,
|
_: *ChildExited,
|
||||||
self: *Self,
|
self: *Self,
|
||||||
|
@@ -206,7 +206,7 @@ pub const Tab = extern struct {
|
|||||||
.init("ring-bell", actionRingBell, null),
|
.init("ring-bell", actionRingBell, null),
|
||||||
};
|
};
|
||||||
|
|
||||||
ext.actions.addAsGroup(Self, self, "tab", &actions);
|
_ = ext.actions.addAsGroup(Self, self, "tab", &actions);
|
||||||
}
|
}
|
||||||
|
|
||||||
//---------------------------------------------------------------
|
//---------------------------------------------------------------
|
||||||
|
@@ -40,14 +40,21 @@ test "gActionNameIsValid" {
|
|||||||
/// Function to create a structure for describing an action.
|
/// Function to create a structure for describing an action.
|
||||||
pub fn Action(comptime T: type) type {
|
pub fn Action(comptime T: type) type {
|
||||||
return struct {
|
return struct {
|
||||||
|
const Self = @This();
|
||||||
pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void;
|
pub const Callback = *const fn (*gio.SimpleAction, ?*glib.Variant, *T) callconv(.c) void;
|
||||||
|
|
||||||
name: [:0]const u8,
|
name: [:0]const u8,
|
||||||
callback: Callback,
|
callback: Callback,
|
||||||
parameter_type: ?*const glib.VariantType,
|
parameter_type: ?*const glib.VariantType,
|
||||||
|
state: ?*glib.Variant = null,
|
||||||
|
|
||||||
/// Function to initialize a new action so that we can comptime check the name.
|
/// Function to initialize a new action so that we can comptime check
|
||||||
pub fn init(comptime name: [:0]const u8, callback: Callback, parameter_type: ?*const glib.VariantType) @This() {
|
/// the name.
|
||||||
|
pub fn init(
|
||||||
|
comptime name: [:0]const u8,
|
||||||
|
callback: Callback,
|
||||||
|
parameter_type: ?*const glib.VariantType,
|
||||||
|
) Self {
|
||||||
comptime assert(gActionNameIsValid(name));
|
comptime assert(gActionNameIsValid(name));
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -56,6 +63,23 @@ pub fn Action(comptime T: type) type {
|
|||||||
.parameter_type = parameter_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 {
|
pub fn addToMap(comptime T: type, self: *T, map: *gio.ActionMap, actions: []const Action(T)) void {
|
||||||
for (actions) |entry| {
|
for (actions) |entry| {
|
||||||
assert(gActionNameIsValid(entry.name));
|
assert(gActionNameIsValid(entry.name));
|
||||||
const action = gio.SimpleAction.new(
|
const action = action: {
|
||||||
entry.name,
|
if (entry.state) |state| {
|
||||||
entry.parameter_type,
|
break :action gio.SimpleAction.newStateful(
|
||||||
);
|
entry.name,
|
||||||
|
entry.parameter_type,
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break :action gio.SimpleAction.new(
|
||||||
|
entry.name,
|
||||||
|
entry.parameter_type,
|
||||||
|
);
|
||||||
|
};
|
||||||
defer action.unref();
|
defer action.unref();
|
||||||
_ = gio.SimpleAction.signals.activate.connect(
|
_ = gio.SimpleAction.signals.activate.connect(
|
||||||
action,
|
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.
|
/// 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));
|
comptime assert(gActionNameIsValid(name));
|
||||||
|
|
||||||
// Collect our actions into a group since we're just a plain widget that
|
// 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,
|
name,
|
||||||
group.as(gio.ActionGroup),
|
group.as(gio.ActionGroup),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return group;
|
||||||
}
|
}
|
||||||
|
|
||||||
test "adding actions to an object" {
|
test "adding actions to an object" {
|
||||||
@@ -138,7 +173,7 @@ test "adding actions to an object" {
|
|||||||
.init("test", callbacks.callback, i32_variant_type),
|
.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));
|
const expected = std.crypto.random.intRangeAtMost(i32, 1, std.math.maxInt(u31));
|
||||||
|
@@ -203,6 +203,11 @@ menu context_menu_model {
|
|||||||
label: _("Paste");
|
label: _("Paste");
|
||||||
action: "win.paste";
|
action: "win.paste";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
label: _("Notify on Next Command Finish");
|
||||||
|
action: "surface.notify-on-next-command-finish";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
|
@@ -96,6 +96,14 @@ pub const Message = union(enum) {
|
|||||||
/// Report the progress of an action using a GUI element
|
/// Report the progress of an action using a GUI element
|
||||||
progress_report: terminal.osc.Command.ProgressReport,
|
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 {
|
pub const ReportTitleStyle = enum {
|
||||||
csi_21_t,
|
csi_21_t,
|
||||||
|
|
||||||
|
@@ -1004,6 +1004,82 @@ command: ?Command = null,
|
|||||||
/// manually.
|
/// manually.
|
||||||
@"initial-command": ?Command = null,
|
@"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
|
/// Extra environment variables to pass to commands launched in a terminal
|
||||||
/// surface. The format is `env=KEY=VALUE`.
|
/// surface. The format is `env=KEY=VALUE`.
|
||||||
///
|
///
|
||||||
@@ -8165,6 +8241,10 @@ pub const Duration = struct {
|
|||||||
return .{ .duration = self.duration / to * to };
|
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 {
|
pub fn parseCLI(input: ?[]const u8) !Duration {
|
||||||
var remaining = input orelse return error.ValueRequired;
|
var remaining = input orelse return error.ValueRequired;
|
||||||
|
|
||||||
@@ -8378,6 +8458,19 @@ pub const ScrollToBottom = packed struct {
|
|||||||
pub const default: ScrollToBottom = .{};
|
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" {
|
test "parse duration" {
|
||||||
inline for (Duration.units) |unit| {
|
inline for (Duration.units) |unit| {
|
||||||
var buf: [16]u8 = undefined;
|
var buf: [16]u8 = undefined;
|
||||||
|
@@ -1054,6 +1054,11 @@ pub const StreamHandler = struct {
|
|||||||
|
|
||||||
pub inline fn endOfInput(self: *StreamHandler) !void {
|
pub inline fn endOfInput(self: *StreamHandler) !void {
|
||||||
self.terminal.markSemanticPrompt(.command);
|
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 {
|
pub fn reportPwd(self: *StreamHandler, url: []const u8) !void {
|
||||||
|
Reference in New Issue
Block a user