diff --git a/src/gui/wio/app.zig b/src/gui/wio/app.zig index 385f0683..39786561 100644 --- a/src/gui/wio/app.zig +++ b/src/gui/wio/app.zig @@ -42,6 +42,26 @@ var font_name_len: usize = 0; var font_dirty: std.atomic.Value(bool) = .init(true); var stop_requested: std.atomic.Value(bool) = .init(false); +// Window title (written from TUI thread, applied by wio thread) +var title_mutex: std.Thread.Mutex = .{}; +var title_buf: [512]u8 = undefined; +var title_len: usize = 0; +var title_dirty: std.atomic.Value(bool) = .init(false); + +// Clipboard write (heap-allocated, transferred to wio thread) +var clipboard_mutex: std.Thread.Mutex = .{}; +var clipboard_write: ?[]u8 = null; + +// Clipboard read request +var clipboard_read_pending: std.atomic.Value(bool) = .init(false); + +// Mouse cursor (stored as wio.Cursor tag value) +var pending_cursor: std.atomic.Value(u8) = .init(@intFromEnum(wio.Cursor.arrow)); +var cursor_dirty: std.atomic.Value(bool) = .init(false); + +// Window attention request +var attention_pending: std.atomic.Value(bool) = .init(false); + // Current font — written and read only from the wio thread (after gpu.init). var wio_font: gpu.Font = .{ .cell_size = .{ .x = 8, .y = 16 } }; @@ -136,6 +156,53 @@ pub fn setFontFace(name: []const u8) void { requestRender(); } +pub fn setWindowTitle(title: []const u8) void { + title_mutex.lock(); + defer title_mutex.unlock(); + const copy_len = @min(title.len, title_buf.len); + @memcpy(title_buf[0..copy_len], title[0..copy_len]); + title_len = copy_len; + title_dirty.store(true, .release); + wio.cancelWait(); +} + +pub fn setClipboard(text: []const u8) void { + const allocator = gpa.allocator(); + const copy = allocator.dupe(u8, text) catch return; + clipboard_mutex.lock(); + defer clipboard_mutex.unlock(); + if (clipboard_write) |old| allocator.free(old); + clipboard_write = copy; + wio.cancelWait(); +} + +pub fn requestClipboard() void { + clipboard_read_pending.store(true, .release); + wio.cancelWait(); +} + +pub fn setMouseCursor(shape: vaxis.Mouse.Shape) void { + const cursor: wio.Cursor = switch (shape) { + .default => .arrow, + .text => .text, + .pointer => .hand, + .help => .arrow, + .progress => .arrow_busy, + .wait => .busy, + .@"ew-resize" => .size_ew, + .@"ns-resize" => .size_ns, + .cell => .crosshair, + }; + pending_cursor.store(@intFromEnum(cursor), .release); + cursor_dirty.store(true, .release); + wio.cancelWait(); +} + +pub fn requestAttention() void { + attention_pending.store(true, .release); + wio.cancelWait(); +} + // ── Internal helpers (wio thread only) ──────────────────────────────────── // Reload wio_font from current settings. Called only from the wio thread. @@ -324,10 +391,42 @@ fn wioLoop() void { const row_cell: i32 = @intCast(@divTrunc(@as(i32, @intCast(mouse_pos.y)), wio_font.cell_size.y)); tui_pid.send(.{ "RDR", "B", @as(u8, 1), btn_id, col_cell, row_cell, @as(i32, 0), @as(i32, 0) }) catch {}; }, + .focused => window.enableTextInput(.{}), + .unfocused => window.disableTextInput(), else => {}, } } + // Apply pending cross-thread requests from the TUI thread. + if (title_dirty.swap(false, .acq_rel)) { + title_mutex.lock(); + const t = title_buf[0..title_len]; + title_mutex.unlock(); + window.setTitle(t); + } + { + clipboard_mutex.lock(); + const pending = clipboard_write; + clipboard_write = null; + clipboard_mutex.unlock(); + if (pending) |text| { + defer allocator.free(text); + window.setClipboardText(text); + } + } + if (clipboard_read_pending.swap(false, .acq_rel)) { + if (window.getClipboardText(allocator)) |text| { + defer allocator.free(text); + tui_pid.send(.{ "RDR", "system_clipboard", text }) catch {}; + } + } + if (cursor_dirty.swap(false, .acq_rel)) { + window.setCursor(@enumFromInt(pending_cursor.load(.acquire))); + } + if (attention_pending.swap(false, .acq_rel)) { + window.requestAttention(); + } + // Paint if the tui pushed new screen data. // Take ownership of the snap (set screen_snap = null under the mutex) // so the TUI thread cannot free the backing memory while we use it. diff --git a/src/renderer/gui/renderer.zig b/src/renderer/gui/renderer.zig index fef116f7..41ca35fe 100644 --- a/src/renderer/gui/renderer.zig +++ b/src/renderer/gui/renderer.zig @@ -325,6 +325,18 @@ pub fn process_renderer_event(self: *Self, msg: []const u8) Error!void { } } + { + var text: []const u8 = undefined; + if (try cbor.match(msg, .{ + cbor.any, + "system_clipboard", + cbor.extract(&text), + })) { + if (self.dispatch_event) |f| f(self.handler_ctx, try self.fmtmsg(.{ "system_clipboard", text })); + return; + } + } + return error.UnexpectedRendererEvent; } @@ -335,7 +347,7 @@ pub fn set_sgr_pixel_mode_support(self: *Self, enable: bool) void { pub fn set_terminal_title(self: *Self, text: []const u8) void { _ = self; - _ = text; + app.setWindowTitle(text); } pub fn set_terminal_style(self: *Self, style_: Style) void { @@ -398,23 +410,26 @@ pub fn request_windows_clipboard(self: *Self) void { pub fn request_mouse_cursor(self: *Self, shape: MouseCursorShape, push_or_pop: bool) void { _ = self; - _ = shape; _ = push_or_pop; + app.setMouseCursor(shape); } pub fn request_mouse_cursor_text(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; + app.setMouseCursor(.text); } pub fn request_mouse_cursor_pointer(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; + app.setMouseCursor(.pointer); } pub fn request_mouse_cursor_default(self: *Self, push_or_pop: bool) void { _ = self; _ = push_or_pop; + app.setMouseCursor(.default); } pub fn cursor_enable(self: *Self, y: i32, x: i32, shape: CursorShape) !void { @@ -440,11 +455,10 @@ pub fn show_multi_cursor_yx(self: *Self, y: i32, x: i32) !void { pub fn copy_to_system_clipboard(self: *Self, text: []const u8) void { _ = self; - _ = text; - // TODO: implement + app.setClipboard(text); } pub fn request_system_clipboard(self: *Self) void { _ = self; - // TODO: implement + app.requestClipboard(); }