From 1482e946928f3cddfba54557a461cdab9f7b74f4 Mon Sep 17 00:00:00 2001 From: Jan200101 Date: Tue, 24 Dec 2024 21:50:57 +0100 Subject: [PATCH] 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 --- src/terminal/osc.zig | 127 +++++++++++++++++++++++++++++++++++++--- src/terminal/stream.zig | 4 ++ 2 files changed, 124 insertions(+), 7 deletions(-) diff --git a/src/terminal/osc.zig b/src/terminal/osc.zig index 34bc46745..049dac91f 100644 --- a/src/terminal/osc.zig +++ b/src/terminal/osc.zig @@ -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; diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index b8d60a13f..8e8be90b1 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -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.