mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
terminal: implement support for ConEmu OSC 9;4 progress report
ConTerm and iTerm2 have both taken OSC 9 to implement different things: iTerm2 uses it to implement desktop notifications ConTerm uses it to implement lots of different instructions Terminals such as Kitty or GhosTTY have implemented OSC 9 like iTerm2 did meanwhile other projects such as Windows Terminal, systemd and flatpak have adopted the ConTerm implementation as a way to state the current progress of a task
This commit is contained in:
@ -158,6 +158,12 @@ pub const Command = union(enum) {
|
||||
/// End a hyperlink (OSC 8)
|
||||
hyperlink_end: void,
|
||||
|
||||
/// Set progress state (OSC 9;4)
|
||||
progress: struct {
|
||||
state: ProgressState,
|
||||
progress: ?u8 = null,
|
||||
},
|
||||
|
||||
pub const ColorKind = union(enum) {
|
||||
palette: u8,
|
||||
foreground,
|
||||
@ -173,6 +179,14 @@ pub const Command = union(enum) {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const ProgressState = union(enum) {
|
||||
remove,
|
||||
set,
|
||||
err,
|
||||
indeterminate,
|
||||
pause,
|
||||
};
|
||||
};
|
||||
|
||||
/// The terminator used to end an OSC command. For OSC commands that demand
|
||||
@ -322,6 +336,20 @@ pub const Parser = struct {
|
||||
// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||
kitty_color_protocol_key,
|
||||
kitty_color_protocol_value,
|
||||
|
||||
// OSC 9 is used by ConEmu and iTerm2 for different things
|
||||
// iTerm2 uses it to post a notification
|
||||
// https://iterm2.com/documentation-escape-codes.html
|
||||
// ConEmu uses it to implement many custom functions
|
||||
// https://conemu.github.io/en/AnsiEscapeCodes.html#OSC_Operating_system_commands
|
||||
// Some Linux applications (namely systemd and flatpak) have adopted the ConEmu implementation
|
||||
// but this causes bogus notifications on iTerm2 compatible terminal emulators
|
||||
osc_9,
|
||||
|
||||
// ConEmu specific substates
|
||||
conemu_progress_start,
|
||||
conemu_progress_state,
|
||||
conemu_progress,
|
||||
};
|
||||
|
||||
/// This must be called to clean up any allocated memory.
|
||||
@ -735,18 +763,79 @@ pub const Parser = struct {
|
||||
|
||||
.@"9" => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .show_desktop_notification = .{
|
||||
.title = "",
|
||||
.body = undefined,
|
||||
} };
|
||||
|
||||
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
|
||||
self.buf_start = self.buf_idx;
|
||||
self.state = .string;
|
||||
self.state = .osc_9;
|
||||
},
|
||||
else => self.state = .invalid,
|
||||
},
|
||||
|
||||
.osc_9 => switch (c) {
|
||||
'4' => {
|
||||
self.state = .conemu_progress_start;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_start => switch (c) {
|
||||
';' => {
|
||||
self.command = .{ .progress = .{
|
||||
.state = undefined,
|
||||
} };
|
||||
self.state = .conemu_progress_state;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress_state => switch (c) {
|
||||
'0' => {
|
||||
self.command.progress.state = .remove;
|
||||
self.state = .conemu_progress;
|
||||
self.complete = true;
|
||||
},
|
||||
'1' => {
|
||||
self.command.progress.state = .set;
|
||||
self.command.progress.progress = 0;
|
||||
self.state = .conemu_progress;
|
||||
},
|
||||
'2' => {
|
||||
self.command.progress.state = .err;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress;
|
||||
},
|
||||
'3' => {
|
||||
self.command.progress.state = .indeterminate;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress;
|
||||
},
|
||||
'4' => {
|
||||
self.command.progress.state = .pause;
|
||||
self.complete = true;
|
||||
self.state = .conemu_progress;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.conemu_progress => switch (c) {
|
||||
';' => {
|
||||
if (self.command.progress.progress) |progress| {
|
||||
if (progress > 0)
|
||||
self.showDesktopNotification();
|
||||
}
|
||||
},
|
||||
'0'...'9' => {
|
||||
if (self.command.progress.state == .set and self.command.progress.progress.? < 100) {
|
||||
if (self.command.progress.progress.? >= 10)
|
||||
self.command.progress.progress = 100
|
||||
else {
|
||||
const d = std.fmt.charToDigit(c, 10) catch 0;
|
||||
self.command.progress.progress = @min(100, (self.command.progress.progress.? * 10) + d);
|
||||
}
|
||||
}
|
||||
self.complete = true;
|
||||
},
|
||||
else => self.showDesktopNotification(),
|
||||
},
|
||||
|
||||
.query_fg_color => switch (c) {
|
||||
'?' => {
|
||||
self.command = .{ .report_color = .{ .kind = .foreground } };
|
||||
@ -901,6 +990,16 @@ pub const Parser = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn showDesktopNotification(self: *Parser) void {
|
||||
self.command = .{ .show_desktop_notification = .{
|
||||
.title = "",
|
||||
.body = undefined,
|
||||
} };
|
||||
|
||||
self.temp_state = .{ .str = &self.command.show_desktop_notification.body };
|
||||
self.state = .string;
|
||||
}
|
||||
|
||||
fn prepAllocableString(self: *Parser) void {
|
||||
assert(self.buf_dynamic == null);
|
||||
|
||||
@ -1518,6 +1617,20 @@ test "OSC: show desktop notification" {
|
||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world");
|
||||
}
|
||||
|
||||
test "OSC: set progress" {
|
||||
const testing = std.testing;
|
||||
|
||||
var p: Parser = .{};
|
||||
|
||||
const input = "9;4;1;100";
|
||||
for (input) |ch| p.next(ch);
|
||||
|
||||
const cmd = p.end('\x1b').?;
|
||||
try testing.expect(cmd == .progress);
|
||||
try testing.expect(cmd.progress.state == .set);
|
||||
try testing.expect(cmd.progress.progress == 100);
|
||||
}
|
||||
|
||||
test "OSC: show desktop notification with title" {
|
||||
const testing = std.testing;
|
||||
|
||||
|
@ -1447,6 +1447,10 @@ pub fn Stream(comptime Handler: type) type {
|
||||
return;
|
||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
|
||||
.progress => {
|
||||
log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||
},
|
||||
}
|
||||
|
||||
// Fall through for when we don't have a handler.
|
||||
|
Reference in New Issue
Block a user