terminal: some osc types

pull/9342/head
Mitchell Hashimoto 2025-10-24 10:53:57 -07:00
parent bce1164ae6
commit 4d028dac1f
No known key found for this signature in database
GPG Key ID: 523D5DC389D273BC
4 changed files with 209 additions and 73 deletions

View File

@ -1,9 +1,11 @@
const std = @import("std");
const enumpkg = @import("enum.zig");
const types = @import("types.zig");
const unionpkg = @import("union.zig");
pub const allocator = @import("allocator.zig");
pub const Enum = enumpkg.Enum;
pub const String = types.String;
pub const Struct = @import("struct.zig").Struct;
pub const Target = @import("target.zig").Target;
pub const TaggedUnion = unionpkg.TaggedUnion;

13
src/lib/types.zig Normal file
View File

@ -0,0 +1,13 @@
pub const String = extern struct {
ptr: [*]const u8,
len: usize,
pub fn init(zig: anytype) String {
return switch (@TypeOf(zig)) {
[]u8, []const u8 => .{
.ptr = zig.ptr,
.len = zig.len,
},
};
}
};

View File

@ -113,6 +113,16 @@ pub const Action = union(Key) {
end_hyperlink,
active_status_display: ansi.StatusDisplay,
decaln,
window_title: WindowTitle,
report_pwd: ReportPwd,
show_desktop_notification: ShowDesktopNotification,
progress_report: osc.Command.ProgressReport,
start_hyperlink: StartHyperlink,
clipboard_contents: ClipboardContents,
prompt_start: PromptStart,
prompt_continuation: PromptContinuation,
end_of_command: EndOfCommand,
mouse_shape: MouseShape,
pub const Key = lib.Enum(
lib_target,
@ -200,6 +210,16 @@ pub const Action = union(Key) {
"end_hyperlink",
"active_status_display",
"decaln",
"window_title",
"report_pwd",
"show_desktop_notification",
"progress_report",
"start_hyperlink",
"clipboard_contents",
"prompt_start",
"prompt_continuation",
"end_of_command",
"mouse_shape",
},
);
@ -288,6 +308,118 @@ pub const Action = union(Key) {
return @intCast(self.flags.int());
}
};
pub const WindowTitle = struct {
title: []const u8,
pub const C = lib.String;
pub fn cval(self: WindowTitle) WindowTitle.C {
return .init(self.title);
}
};
pub const ReportPwd = struct {
url: []const u8,
pub const C = lib.String;
pub fn cval(self: ReportPwd) ReportPwd.C {
return .init(self.url);
}
};
pub const ShowDesktopNotification = struct {
title: []const u8,
body: []const u8,
pub const C = extern struct {
title: lib.String,
body: lib.String,
};
pub fn cval(self: ShowDesktopNotification) ShowDesktopNotification.C {
return .{
.title = .init(self.title),
.body = .init(self.body),
};
}
};
pub const StartHyperlink = struct {
uri: []const u8,
id: ?[]const u8,
pub const C = extern struct {
uri: lib.String,
id: lib.String,
};
pub fn cval(self: StartHyperlink) StartHyperlink.C {
return .{
.uri = .init(self.uri),
.id = .init(self.id orelse ""),
};
}
};
pub const ClipboardContents = struct {
kind: u8,
data: []const u8,
pub const C = extern struct {
kind: u8,
data: lib.String,
};
pub fn cval(self: ClipboardContents) ClipboardContents.C {
return .{
.kind = self.kind,
.data = .init(self.data),
};
}
};
pub const PromptStart = struct {
aid: ?[]const u8,
redraw: bool,
pub const C = extern struct {
aid: lib.String,
redraw: bool,
};
pub fn cval(self: PromptStart) PromptStart.C {
return .{
.aid = .init(self.aid orelse ""),
.redraw = self.redraw,
};
}
};
pub const PromptContinuation = struct {
aid: ?[]const u8,
pub const C = lib.String;
pub fn cval(self: PromptContinuation) PromptContinuation.C {
return .init(self.aid orelse "");
}
};
pub const EndOfCommand = struct {
exit_code: ?u8,
pub const C = extern struct {
exit_code: i16,
};
pub fn cval(self: EndOfCommand) EndOfCommand.C {
return .{
.exit_code = if (self.exit_code) |code| @intCast(code) else -1,
};
}
};
};
/// Returns a type that can process a stream of tty control characters.
@ -1710,15 +1842,12 @@ pub fn Stream(comptime Handler: type) type {
inline fn oscDispatch(self: *Self, cmd: osc.Command) !void {
switch (cmd) {
.change_window_title => |title| {
if (@hasDecl(T, "changeWindowTitle")) {
if (!std.unicode.utf8ValidateSlice(title)) {
log.warn("change title request: invalid utf-8, ignoring request", .{});
return;
}
try self.handler.changeWindowTitle(title);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.window_title, .{ .title = title });
},
.change_window_icon => |icon| {
@ -1726,54 +1855,43 @@ pub fn Stream(comptime Handler: type) type {
},
.clipboard_contents => |clip| {
if (@hasDecl(T, "clipboardContents")) {
try self.handler.clipboardContents(clip.kind, clip.data);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.clipboard_contents, .{
.kind = clip.kind,
.data = clip.data,
});
},
.prompt_start => |v| {
if (@hasDecl(T, "promptStart")) {
switch (v.kind) {
.primary, .right => try self.handler.promptStart(v.aid, v.redraw),
.continuation, .secondary => try self.handler.promptContinuation(v.aid),
.primary, .right => try self.handler.vt(.prompt_start, .{
.aid = v.aid,
.redraw = v.redraw,
}),
.continuation, .secondary => try self.handler.vt(.prompt_continuation, .{
.aid = v.aid,
}),
}
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
},
.prompt_end => {
try self.handler.vt(.prompt_end, {});
},
.prompt_end => try self.handler.vt(.prompt_end, {}),
.end_of_input => {
try self.handler.vt(.end_of_input, {});
},
.end_of_input => try self.handler.vt(.end_of_input, {}),
.end_of_command => |end| {
if (@hasDecl(T, "endOfCommand")) {
try self.handler.endOfCommand(end.exit_code);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.end_of_command, .{ .exit_code = end.exit_code });
},
.report_pwd => |v| {
if (@hasDecl(T, "reportPwd")) {
try self.handler.reportPwd(v.value);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.report_pwd, .{ .url = v.value });
},
.mouse_shape => |v| {
if (@hasDecl(T, "setMouseShape")) {
const shape = MouseShape.fromString(v.value) orelse {
log.warn("unknown cursor shape: {s}", .{v.value});
return;
};
try self.handler.setMouseShape(shape);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.mouse_shape, shape);
},
.color_operation => |v| {
@ -1795,17 +1913,17 @@ pub fn Stream(comptime Handler: type) type {
},
.show_desktop_notification => |v| {
if (@hasDecl(T, "showDesktopNotification")) {
try self.handler.showDesktopNotification(v.title, v.body);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.show_desktop_notification, .{
.title = v.title,
.body = v.body,
});
},
.hyperlink_start => |v| {
if (@hasDecl(T, "startHyperlink")) {
try self.handler.startHyperlink(v.uri, v.id);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.start_hyperlink, .{
.uri = v.uri,
.id = v.id,
});
},
.hyperlink_end => {
@ -1813,10 +1931,7 @@ pub fn Stream(comptime Handler: type) type {
},
.conemu_progress_report => |v| {
if (@hasDecl(T, "handleProgressReport")) {
try self.handler.handleProgressReport(v);
return;
} else log.warn("unimplemented OSC callback: {}", .{cmd});
try self.handler.vt(.progress_report, v);
},
.conemu_sleep,
@ -2643,20 +2758,16 @@ test "stream: change window title with invalid utf-8" {
const H = struct {
seen: bool = false,
pub fn changeWindowTitle(self: *@This(), title: []const u8) !void {
_ = title;
self.seen = true;
}
pub fn vt(
self: *@This(),
comptime action: anytype,
value: anytype,
) !void {
_ = self;
_ = action;
_ = value;
switch (action) {
.window_title => self.seen = true,
else => {},
}
}
};

View File

@ -306,6 +306,16 @@ pub const StreamHandler = struct {
.end_hyperlink => try self.endHyperlink(),
.active_status_display => self.terminal.status_display = value,
.decaln => try self.decaln(),
.window_title => try self.windowTitle(value.title),
.report_pwd => try self.reportPwd(value.url),
.show_desktop_notification => try self.showDesktopNotification(value.title, value.body),
.progress_report => self.progressReport(value),
.start_hyperlink => try self.startHyperlink(value.uri, value.id),
.clipboard_contents => try self.clipboardContents(value.kind, value.data),
.prompt_start => self.promptStart(value.aid, value.redraw),
.prompt_continuation => self.promptContinuation(value.aid),
.end_of_command => self.endOfCommand(value.exit_code),
.mouse_shape => try self.setMouseShape(value),
.dcs_hook => try self.dcsHook(value),
.dcs_put => try self.dcsPut(value),
.dcs_unhook => try self.dcsUnhook(),
@ -714,7 +724,7 @@ pub const StreamHandler = struct {
}
}
pub inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void {
inline fn startHyperlink(self: *StreamHandler, uri: []const u8, id: ?[]const u8) !void {
try self.terminal.screen.startHyperlink(uri, id);
}
@ -902,7 +912,7 @@ pub const StreamHandler = struct {
//-------------------------------------------------------------------------
// OSC
pub fn changeWindowTitle(self: *StreamHandler, title: []const u8) !void {
fn windowTitle(self: *StreamHandler, title: []const u8) !void {
var buf: [256]u8 = undefined;
if (title.len >= buf.len) {
log.warn("change title requested larger than our buffer size, ignoring", .{});
@ -933,7 +943,7 @@ pub const StreamHandler = struct {
self.surfaceMessageWriter(.{ .set_title = buf });
}
pub inline fn setMouseShape(
inline fn setMouseShape(
self: *StreamHandler,
shape: terminal.MouseShape,
) !void {
@ -945,7 +955,7 @@ pub const StreamHandler = struct {
self.surfaceMessageWriter(.{ .set_mouse_shape = shape });
}
pub fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
fn clipboardContents(self: *StreamHandler, kind: u8, data: []const u8) !void {
// Note: we ignore the "kind" field and always use the standard clipboard.
// iTerm also appears to do this but other terminals seem to only allow
// certain. Let's investigate more.
@ -975,13 +985,13 @@ pub const StreamHandler = struct {
});
}
pub inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) !void {
inline fn promptStart(self: *StreamHandler, aid: ?[]const u8, redraw: bool) void {
_ = aid;
self.terminal.markSemanticPrompt(.prompt);
self.terminal.flags.shell_redraws_prompt = redraw;
}
pub inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) !void {
inline fn promptContinuation(self: *StreamHandler, aid: ?[]const u8) void {
_ = aid;
self.terminal.markSemanticPrompt(.prompt_continuation);
}
@ -995,11 +1005,11 @@ pub const StreamHandler = struct {
self.surfaceMessageWriter(.start_command);
}
pub inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) !void {
inline fn endOfCommand(self: *StreamHandler, exit_code: ?u8) void {
self.surfaceMessageWriter(.{ .stop_command = exit_code });
}
pub fn reportPwd(self: *StreamHandler, url: []const u8) !void {
fn reportPwd(self: *StreamHandler, url: []const u8) !void {
// Special handling for the empty URL. We treat the empty URL
// as resetting the pwd as if we never saw a pwd. I can't find any
// other terminal that does this but it seems like a reasonable
@ -1013,7 +1023,7 @@ pub const StreamHandler = struct {
// If we haven't seen a title, we're using the pwd as our title.
// Set it to blank which will reset our title behavior.
if (!self.seen_title) {
try self.changeWindowTitle("");
try self.windowTitle("");
assert(!self.seen_title);
}
@ -1093,7 +1103,7 @@ pub const StreamHandler = struct {
// If we haven't seen a title, use our pwd as the title.
if (!self.seen_title) {
try self.changeWindowTitle(path);
try self.windowTitle(path);
self.seen_title = false;
}
}
@ -1347,7 +1357,7 @@ pub const StreamHandler = struct {
}
}
pub fn showDesktopNotification(
fn showDesktopNotification(
self: *StreamHandler,
title: []const u8,
body: []const u8,
@ -1500,7 +1510,7 @@ pub const StreamHandler = struct {
}
/// Display a GUI progress report.
pub fn handleProgressReport(self: *StreamHandler, report: terminal.osc.Command.ProgressReport) error{}!void {
fn progressReport(self: *StreamHandler, report: terminal.osc.Command.ProgressReport) void {
self.surfaceMessageWriter(.{ .progress_report = report });
}
};