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:
Jan200101
2024-12-24 21:50:57 +01:00
parent b1756b93b8
commit 1482e94692
2 changed files with 124 additions and 7 deletions

View File

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

View File

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