feat(gui): clipboard, title, cursor, attention

This commit is contained in:
CJ van den Berg 2026-03-30 21:17:27 +02:00
parent 273be78055
commit b1b50b7ff0
Signed by: neurocyte
GPG key ID: 8EB1E1BB660E3FB9
2 changed files with 118 additions and 5 deletions

View file

@ -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.

View file

@ -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();
}