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)
|
/// End a hyperlink (OSC 8)
|
||||||
hyperlink_end: void,
|
hyperlink_end: void,
|
||||||
|
|
||||||
|
/// Set progress state (OSC 9;4)
|
||||||
|
progress: struct {
|
||||||
|
state: ProgressState,
|
||||||
|
progress: ?u8 = null,
|
||||||
|
},
|
||||||
|
|
||||||
pub const ColorKind = union(enum) {
|
pub const ColorKind = union(enum) {
|
||||||
palette: u8,
|
palette: u8,
|
||||||
foreground,
|
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
|
/// 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
|
// https://sw.kovidgoyal.net/kitty/color-stack/#id1
|
||||||
kitty_color_protocol_key,
|
kitty_color_protocol_key,
|
||||||
kitty_color_protocol_value,
|
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.
|
/// This must be called to clean up any allocated memory.
|
||||||
@ -735,18 +763,79 @@ pub const Parser = struct {
|
|||||||
|
|
||||||
.@"9" => switch (c) {
|
.@"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.buf_start = self.buf_idx;
|
||||||
self.state = .string;
|
self.state = .osc_9;
|
||||||
},
|
},
|
||||||
else => self.state = .invalid,
|
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) {
|
.query_fg_color => switch (c) {
|
||||||
'?' => {
|
'?' => {
|
||||||
self.command = .{ .report_color = .{ .kind = .foreground } };
|
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 {
|
fn prepAllocableString(self: *Parser) void {
|
||||||
assert(self.buf_dynamic == null);
|
assert(self.buf_dynamic == null);
|
||||||
|
|
||||||
@ -1518,6 +1617,20 @@ test "OSC: show desktop notification" {
|
|||||||
try testing.expectEqualStrings(cmd.show_desktop_notification.body, "Hello world");
|
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" {
|
test "OSC: show desktop notification with title" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
|
||||||
|
@ -1447,6 +1447,10 @@ pub fn Stream(comptime Handler: type) type {
|
|||||||
return;
|
return;
|
||||||
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
} else log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
.progress => {
|
||||||
|
log.warn("unimplemented OSC callback: {}", .{cmd});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall through for when we don't have a handler.
|
// Fall through for when we don't have a handler.
|
||||||
|
Reference in New Issue
Block a user