From 4ced2290b3b15fc3f64531bb44a038e6a400c346 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 14 Nov 2022 10:46:40 -0800 Subject: [PATCH] OSC handling, handle OSC change window title command --- TODO.md | 1 - src/App.zig | 25 ++++++++++++++++++++++++- src/Window.zig | 21 +++++++++++++++++++++ src/blocking_queue.zig | 2 +- src/terminal/stream.zig | 18 +++++++++++++++++- src/termio/Exec.zig | 25 +++++++++++++++++++++++++ src/termio/Options.zig | 4 ++++ src/window/message.zig | 35 +++++++++++++++++++++++++++++++++++ 8 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 src/window/message.zig diff --git a/TODO.md b/TODO.md index e06315afc..43f0aed42 100644 --- a/TODO.md +++ b/TODO.md @@ -41,7 +41,6 @@ Mac: * Set menubar * Preferences window -* Need to load and proper locale env vars Major Features: diff --git a/src/App.zig b/src/App.zig index f04f2ee6f..1a55538e2 100644 --- a/src/App.zig +++ b/src/App.zig @@ -106,10 +106,11 @@ fn drainMailbox(self: *App) !void { defer drain.deinit(); while (drain.next()) |message| { - log.debug("mailbox message={}", .{message}); + log.debug("mailbox message={s}", .{@tagName(message)}); switch (message) { .new_window => try self.newWindow(), .quit => try self.setQuit(), + .window_message => |msg| try self.windowMessage(msg.window, msg.message), } } } @@ -133,6 +134,22 @@ fn setQuit(self: *App) !void { } } +/// Handle a window message +fn windowMessage(self: *App, win: *Window, msg: Window.Message) !void { + // We want to ensure our window is still active. Window messages + // are quite rare and we normally don't have many windows so we do + // a simple linear search here. + for (self.windows.items) |window| { + if (window == win) { + try win.handleMessage(msg); + return; + } + } + + // Window was not found, it probably quit before we handled the message. + // Not a problem. +} + /// The message types that can be sent to the app thread. pub const Message = union(enum) { /// Create a new terminal window. @@ -140,4 +157,10 @@ pub const Message = union(enum) { /// Quit quit: void, + + /// A message for a specific window + window_message: struct { + window: *Window, + message: Window.Message, + }, }; diff --git a/src/Window.zig b/src/Window.zig index 8f9b1cced..6d8b87f68 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -5,6 +5,12 @@ //! writing, we support exactly one window. const Window = @This(); +// TODO: eventually, I want to extract Window.zig into the "window" package +// so we can also have alternate implementations (i.e. not glfw). +const message = @import("window/message.zig"); +pub const Mailbox = message.Mailbox; +pub const Message = message.Message; + const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; @@ -309,6 +315,7 @@ pub fn create(alloc: Allocator, app: *App, config: *const Config) !*Window { .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, .renderer_mailbox = render_thread.mailbox, + .window_mailbox = .{ .window = self, .app = app.mailbox }, }); errdefer io.deinit(); @@ -478,6 +485,20 @@ pub fn shouldClose(self: Window) bool { return self.window.shouldClose(); } +/// Called from the app thread to handle mailbox messages to our specific +/// window. +pub fn handleMessage(self: *Window, msg: Message) !void { + switch (msg) { + .set_title => |*v| { + // The ptrCast just gets sliceTo to return the proper type. + // We know that our title should end in 0. + const slice = std.mem.sliceTo(@ptrCast([*:0]const u8, v), 0); + log.debug("changing title \"{s}\"", .{slice}); + try self.window.setTitle(slice.ptr); + }, + } +} + /// This queues a render operation with the renderer thread. The render /// isn't guaranteed to happen immediately but it will happen as soon as /// practical. diff --git a/src/blocking_queue.zig b/src/blocking_queue.zig index 8014c37a1..b3ef09b8c 100644 --- a/src/blocking_queue.zig +++ b/src/blocking_queue.zig @@ -37,7 +37,7 @@ pub fn BlockingQueue( // The type we use for queue size types. We can optimize this // in the future to be the correct bit-size for our preallocated // size for this queue. - const Size = u32; + pub const Size = u32; // The bounds of this queue. We recast this to Size so we can do math. const bounds = @intCast(Size, capacity); diff --git a/src/terminal/stream.zig b/src/terminal/stream.zig index ee2cb7457..acdfe1c31 100644 --- a/src/terminal/stream.zig +++ b/src/terminal/stream.zig @@ -4,6 +4,7 @@ const Parser = @import("Parser.zig"); const ansi = @import("ansi.zig"); const charsets = @import("charsets.zig"); const csi = @import("csi.zig"); +const osc = @import("osc.zig"); const sgr = @import("sgr.zig"); const trace = @import("tracy").trace; @@ -59,7 +60,7 @@ pub fn Stream(comptime Handler: type) type { .execute => |code| try self.execute(code), .csi_dispatch => |csi_action| try self.csiDispatch(csi_action), .esc_dispatch => |esc| try self.escDispatch(esc), - .osc_dispatch => |cmd| log.warn("unhandled OSC: {}", .{cmd}), + .osc_dispatch => |cmd| try self.oscDispatch(cmd), .dcs_hook => |dcs| log.warn("unhandled DCS hook: {}", .{dcs}), .dcs_put => |code| log.warn("unhandled DCS put: {}", .{code}), .dcs_unhook => log.warn("unhandled DCS unhook", .{}), @@ -455,6 +456,21 @@ pub fn Stream(comptime Handler: type) type { } } + fn oscDispatch(self: *Self, cmd: osc.Command) !void { + switch (cmd) { + .change_window_title => |title| { + if (@hasDecl(T, "changeWindowTitle")) { + try self.handler.changeWindowTitle(title); + } else log.warn("unimplemented OSC callback: {}", .{cmd}); + }, + + else => if (@hasDecl(T, "oscUnimplemented")) + try self.handler.oscUnimplemented(cmd) + else + log.warn("unimplemented OSC command: {}", .{cmd}), + } + } + fn configureCharset( self: *Self, intermediates: []const u8, diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index ce3e34c93..8926ea671 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -7,6 +7,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const termio = @import("../termio.zig"); const Command = @import("../Command.zig"); +const Window = @import("../Window.zig"); const Pty = @import("../Pty.zig"); const SegmentedPool = @import("../segmented_pool.zig").SegmentedPool; const terminal = @import("../terminal/main.zig"); @@ -52,6 +53,9 @@ renderer_wakeup: libuv.Async, /// The mailbox for notifying the renderer of things. renderer_mailbox: *renderer.Thread.Mailbox, +/// The mailbox for communicating with the window. +window_mailbox: Window.Mailbox, + /// The cached grid size whenever a resize is called. grid_size: renderer.GridSize, @@ -117,6 +121,7 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { .renderer_state = opts.renderer_state, .renderer_wakeup = opts.renderer_wakeup, .renderer_mailbox = opts.renderer_mailbox, + .window_mailbox = opts.window_mailbox, .grid_size = opts.grid_size, .data = null, }; @@ -199,6 +204,7 @@ pub fn threadEnter(self: *Exec, loop: libuv.Loop) !ThreadData { .ev = ev_data_ptr, .terminal = &self.terminal, .grid_size = &self.grid_size, + .window_mailbox = self.window_mailbox, }, }, }; @@ -466,6 +472,7 @@ const StreamHandler = struct { alloc: Allocator, grid_size: *renderer.GridSize, terminal: *terminal.Terminal, + window_mailbox: Window.Mailbox, inline fn queueRender(self: *StreamHandler) !void { try self.ev.queueRender(); @@ -787,4 +794,22 @@ const StreamHandler = struct { ) !void { self.terminal.fullReset(); } + + //------------------------------------------------------------------------- + // OSC + + pub fn changeWindowTitle(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", .{}); + return; + } + + std.mem.copy(u8, &buf, title); + buf[title.len] = 0; + + _ = self.window_mailbox.push(.{ + .set_title = buf, + }, .{ .forever = {} }); + } }; diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 752516e77..f4bb3b692 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -3,6 +3,7 @@ const libuv = @import("libuv"); const renderer = @import("../renderer.zig"); const Config = @import("../config.zig").Config; +const Window = @import("../Window.zig"); /// The size of the terminal grid. grid_size: renderer.GridSize, @@ -25,3 +26,6 @@ renderer_wakeup: libuv.Async, /// The mailbox for renderer messages. renderer_mailbox: *renderer.Thread.Mailbox, + +/// The mailbox for sending the window messages. +window_mailbox: Window.Mailbox, diff --git a/src/window/message.zig b/src/window/message.zig new file mode 100644 index 000000000..8dd814092 --- /dev/null +++ b/src/window/message.zig @@ -0,0 +1,35 @@ +const App = @import("../App.zig"); +const Window = @import("../Window.zig"); + +/// The message types that can be sent to a single window. +pub const Message = union(enum) { + /// Set the title of the window. + /// TODO: we should change this to a "WriteReq" style structure in + /// the termio message so that we can more efficiently send strings + /// of any length + set_title: [256]u8, +}; + +/// A window mailbox. +pub const Mailbox = struct { + window: *Window, + app: *App.Mailbox, + + /// Send a message to the window. + pub fn push(self: Mailbox, msg: Message, timeout: App.Mailbox.Timeout) App.Mailbox.Size { + // Window message sending is actually implemented on the app + // thread, so we have to rewrap the message with our window + // pointer and send it to the app thread. + const result = self.app.push(.{ + .window_message = .{ + .window = self.window, + .message = msg, + }, + }, timeout); + + // Wake up our app loop + self.window.app.wakeup(); + + return result; + } +};