Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,17 @@ typedef struct {

// apprt.action.CommandFinished.C
typedef struct {
// -1 if no exit code was reported, otherwise 0-255
// The command line or null if the command line
// is unknown.
const char *command_line;
// The exit code of the command. The exit code will be a number between 0
// and 255 or -1 if no exit code was provided. 0 indicates that the command
// was successful. Any number from 1 to 255 indicates an application specific
// error code.
int16_t exit_code;
// number of nanoseconds that command was running for
// How long the command took in nanoseconds. Despite the duration being
// reported in nanoseconds the accuracy is probably only within a few
// milliseconds.
uint64_t duration;
} ghostty_action_command_finished_s;

Expand Down
51 changes: 47 additions & 4 deletions src/Surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,19 @@ selection_scroll_active: bool = false,
/// always enabled in this state.
readonly: 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
/// Timestamp used to send notifications that long running commands have
/// finished. Requires that the shell reports to Ghostty when a command stops
/// and start. The timestamp 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: ?std.time.Instant = null,

/// The command that is being executed, as reported by by the shell. If shell
/// does not report the command line this will always be null. This will never
/// be the `command` or `initial-command` that was used to start the shell.
/// Ghostty's shell integration does not supply the command line being executed.
command_line: ?[]const u8 = null,

/// Search state
search: ?Search = null,

Expand Down Expand Up @@ -807,6 +813,14 @@ pub fn deinit(self: *Surface) void {
self.alloc.destroy(v);
}

// If we're still storing a command line, deallocate it. This could happen
// if a shell sends a report that a command started containing a command
// line but doesn't send a report that the command finished before the shell
// exits.
if (self.command_line) |command_line| {
self.alloc.free(command_line);
}

// Clean up our keyboard state
for (self.keyboard.sequence_queued.items) |req| req.deinit();
self.keyboard.sequence_queued.deinit(self.alloc);
Expand Down Expand Up @@ -1111,10 +1125,32 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
try self.selectionScrollTick();
},

.start_command => {
// A command has started executing.
.start_command => |start_command| {
defer start_command.deinit();

self.command_timer = try .now();
if (start_command.command_line) |new_command_line| {

// Deallocate old command line if we are setting a new one. We
// don't deallocate the command line unconditionally because
// there are situations where the shell sends a bare command
// started report _after_ it has already sent a command started
// report with the command line but before it sends a command
// finished report.
if (self.command_line) |old_command_line| {
self.alloc.free(old_command_line);
self.command_line = null;
}

// Create our own copy of the command line, the copy that
// comes in the message could be deallocated once we are done
// processing the message.
self.command_line = self.alloc.dupe(u8, new_command_line.slice()) catch null;
}
},

// A command has finished executing.
.stop_command => |v| timer: {
const end: std.time.Instant = try .now();
const start = self.command_timer orelse break :timer;
Expand All @@ -1127,12 +1163,19 @@ pub fn handleMessage(self: *Surface, msg: Message) !void {
.{ .surface = self },
.command_finished,
.{
.command_line = self.command_line,
.exit_code = v,
.duration = duration,
},
) catch |err| {
log.warn("apprt failed to notify command finish={}", .{err});
};

// Free up memory as the command line should never be used again.
if (self.command_line) |old_command_line| {
self.alloc.free(old_command_line);
self.command_line = null;
}
},

.search_total => |v| {
Expand Down
26 changes: 24 additions & 2 deletions src/apprt/action.zig
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const input = @import("../input.zig");
const renderer = @import("../renderer.zig");
const terminal = @import("../terminal/main.zig");
const CoreSurface = @import("../Surface.zig");
const lib = @import("../lib/main.zig");

/// The target for an action. This is generally the thing that had focus
/// while the action was made but the concept of "focus" is not guaranteed
Expand Down Expand Up @@ -436,8 +437,8 @@ pub const Action = union(Key) {
// At the time of writing, we don't promise ABI compatibility
// so we can change this but I want to be aware of it.
assert(@sizeOf(CValue) == switch (@sizeOf(usize)) {
4 => 16,
8 => 24,
4 => 20,
8 => 32,
else => unreachable,
});
}
Expand Down Expand Up @@ -855,17 +856,38 @@ pub const CloseTabMode = enum(c_int) {
};

pub const CommandFinished = struct {
/// The command line, as reported by the shell, or null if the command line
/// is unknown.
command_line: ?[]const u8,
/// The exit code, as reported by the shell. The exit code will be a number
/// between 0 and 255 or null if no exit code was provided. 0 indicates
/// that the command was successful. Any number from 1 to 255 indicates an
/// application specific error code.
exit_code: ?u8,
/// How long the command took in nanoseconds. Despite the duration being
/// reported in nanoseconds the accuracy is probably only within a few
/// milliseconds.
duration: configpkg.Config.Duration,

/// sync with ghostty_action_command_finished_s in ghostty.h
pub const C = extern struct {
/// The command line, as reported by the shell, or null if the command
/// line is unknown.
command_line: lib.String,
/// The exit code, as reported by the shell. The exit code will be a
/// number between 0 and 255 or null if no exit code was provided. 0
/// indicates that the command was successful. Any number from 1 to 255
/// indicates an application specific error code.
exit_code: i16,
// How long the command took in nanoseconds. Despite the duration being
// reported in nanoseconds the accuracy is probably only within a few
// milliseconds.
duration: u64,
};

pub fn cval(self: CommandFinished) C {
return .{
.command_line = .init(self.command_line orelse ""),
.exit_code = self.exit_code orelse -1,
.duration = self.duration.duration,
};
Expand Down
53 changes: 36 additions & 17 deletions src/apprt/gtk/class/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1097,26 +1097,45 @@ pub const Surface = extern struct {
if (action.bell) self.setBellRinging(true);

if (action.notify) notify: {
const title_ = title: {
const title = std.mem.span(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.allocPrintSentinel(
alloc,
"Command took {f}.",
.{value.duration.round(std.time.ns_per_ms)},
0,
) catch break :notify;
break :body std.fmt.allocPrintSentinel(
alloc,
"Command took {f} and exited with code {d}.",
.{ value.duration.round(std.time.ns_per_ms), exit_code },
0,
) catch break :notify;
};
});

var buf: std.Io.Writer.Allocating = .init(alloc);
defer buf.deinit();
const writer = &buf.writer;

writer.writeAll("Command ") catch break :notify;

if (value.command_line) |command_line| command_line: {
// Don't bother with a zero length command line.
if (command_line.len == 0) break :command_line;
// The defacto standard for "hiding" a command line from being
// saved in history or other places is to prefix it with a
// space. Honor that here.
if (command_line[0] == ' ') {
writer.writeAll("«hidden» ") catch break :notify;
break :command_line;
}
writer.writeAll("“") catch break :notify;
writer.writeAll(command_line) catch break :notify;
writer.writeAll("” ") catch break :notify;
}

writer.print(
"took {f}",
.{value.duration.round(std.time.ns_per_ms)},
) catch break :notify;

if (value.exit_code) |exit_code| {
writer.print(" and exited with code {d}", .{exit_code}) catch break :notify;
}

writer.writeByte('.') catch break :notify;

const body = buf.toOwnedSliceSentinel(0) catch break :notify;
defer alloc.free(body);

self.sendDesktopNotification(title, body);
Expand Down
30 changes: 25 additions & 5 deletions src/apprt/surface.zig
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,13 @@ 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,
/// A command has started in the shell. Start a timer and store the command
/// line (if provided) for later display.
start_command: StartCommand,

/// 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.
/// 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: ?u8,

/// The scrollbar state changed for the surface.
Expand Down Expand Up @@ -129,6 +130,25 @@ pub const Message = union(enum) {
.none => void,
};
};

pub const StartCommand = struct {
command_line: ?WriteReq,

/// Return the command line, or null if no command line was supplied.
pub fn init(alloc: std.mem.Allocator, command_line: []const u8) StartCommand {
if (command_line.len == 0) return .{
.command_line = null,
};

return .{
.command_line = WriteReq.init(alloc, command_line) catch null,
};
}

pub fn deinit(self: *const StartCommand) void {
if (self.command_line) |command_line| command_line.deinit();
}
};
};

/// A surface mailbox.
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.zig
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub const String = extern struct {
.ptr = zig.ptr,
.len = zig.len,
},
else => |v| @compileLog(v),
};
}
};
Loading