feat: add surface attach/detach C API
parent
2c62d182ce
commit
a3d9a3f933
|
|
@ -414,6 +414,31 @@ typedef struct {
|
|||
uintptr_t text_len;
|
||||
} ghostty_text_s;
|
||||
|
||||
typedef struct {
|
||||
uint32_t codepoint;
|
||||
uint32_t fg_rgb;
|
||||
uint32_t bg_rgb;
|
||||
uint32_t flags;
|
||||
} ghostty_cell_s;
|
||||
|
||||
typedef struct {
|
||||
uint32_t cols;
|
||||
uint32_t rows;
|
||||
ghostty_cell_s* cells;
|
||||
uintptr_t cells_len;
|
||||
uint32_t cursor_x;
|
||||
uint32_t cursor_y;
|
||||
bool cursor_visible;
|
||||
uint32_t default_fg;
|
||||
uint32_t default_bg;
|
||||
bool alt_screen;
|
||||
bool cursor_keys;
|
||||
bool bracketed_paste;
|
||||
bool focus_event;
|
||||
uint32_t mouse_event;
|
||||
uint32_t mouse_format;
|
||||
} ghostty_cells_s;
|
||||
|
||||
typedef enum {
|
||||
GHOSTTY_POINT_ACTIVE,
|
||||
GHOSTTY_POINT_VIEWPORT,
|
||||
|
|
@ -1113,6 +1138,8 @@ GHOSTTY_API void ghostty_surface_draw(ghostty_surface_t);
|
|||
GHOSTTY_API void ghostty_surface_set_content_scale(ghostty_surface_t, double, double);
|
||||
GHOSTTY_API void ghostty_surface_set_focus(ghostty_surface_t, bool);
|
||||
GHOSTTY_API void ghostty_surface_set_occlusion(ghostty_surface_t, bool);
|
||||
GHOSTTY_API bool ghostty_surface_detach(ghostty_surface_t);
|
||||
GHOSTTY_API bool ghostty_surface_attach(ghostty_surface_t);
|
||||
GHOSTTY_API void ghostty_surface_set_size(ghostty_surface_t, uint32_t, uint32_t);
|
||||
GHOSTTY_API ghostty_surface_size_s ghostty_surface_size(ghostty_surface_t);
|
||||
GHOSTTY_API uint64_t ghostty_surface_foreground_pid(ghostty_surface_t);
|
||||
|
|
@ -1162,6 +1189,13 @@ GHOSTTY_API bool ghostty_surface_read_text(ghostty_surface_t,
|
|||
ghostty_text_s*);
|
||||
GHOSTTY_API void ghostty_surface_free_text(ghostty_surface_t, ghostty_text_s*);
|
||||
|
||||
GHOSTTY_API void ghostty_surface_set_data_callback(ghostty_surface_t,
|
||||
void (*)(void*, const uint8_t*, uintptr_t),
|
||||
void*);
|
||||
GHOSTTY_API void ghostty_surface_send_input_raw(ghostty_surface_t, const uint8_t*, uintptr_t);
|
||||
GHOSTTY_API bool ghostty_surface_read_cells(ghostty_surface_t, ghostty_cells_s*);
|
||||
GHOSTTY_API void ghostty_surface_free_cells(ghostty_surface_t, ghostty_cells_s*);
|
||||
|
||||
#ifdef __APPLE__
|
||||
GHOSTTY_API void ghostty_surface_set_display_id(ghostty_surface_t, uint32_t);
|
||||
GHOSTTY_API void* ghostty_surface_quicklook_font(ghostty_surface_t);
|
||||
|
|
|
|||
172
src/Surface.zig
172
src/Surface.zig
|
|
@ -94,6 +94,10 @@ renderer_thread: rendererpkg.Thread,
|
|||
/// The actual thread
|
||||
renderer_thr: std.Thread,
|
||||
|
||||
/// True when the renderer is detached. The renderer thread is stopped,
|
||||
/// GPU resources are freed, but the PTY/terminal remain alive.
|
||||
renderer_detached: bool = false,
|
||||
|
||||
/// Mouse state.
|
||||
mouse: Mouse,
|
||||
|
||||
|
|
@ -791,8 +795,8 @@ pub fn deinit(self: *Surface) void {
|
|||
// Stop search thread
|
||||
if (self.search) |*s| s.deinit();
|
||||
|
||||
// Stop rendering thread
|
||||
{
|
||||
// Stop rendering thread (skip if already detached)
|
||||
if (!self.renderer_detached) {
|
||||
self.renderer_thread.stop.notify() catch |err|
|
||||
log.err("error notifying renderer thread to stop, may stall err={}", .{err});
|
||||
self.renderer_thr.join();
|
||||
|
|
@ -810,8 +814,10 @@ pub fn deinit(self: *Surface) void {
|
|||
|
||||
// We need to deinit AFTER everything is stopped, since there are
|
||||
// shared values between the two threads.
|
||||
self.renderer_thread.deinit();
|
||||
self.renderer.deinit();
|
||||
if (!self.renderer_detached) {
|
||||
self.renderer_thread.deinit();
|
||||
self.renderer.deinit();
|
||||
}
|
||||
self.io_thread.deinit();
|
||||
self.mouse.selection_gesture.deinit(&self.io.terminal);
|
||||
self.io.deinit();
|
||||
|
|
@ -843,6 +849,150 @@ pub fn close(self: *Surface) void {
|
|||
self.rt_surface.close(self.needsConfirmQuit());
|
||||
}
|
||||
|
||||
/// Detach the renderer from this surface. This frees all GPU resources
|
||||
/// and stops the renderer thread. The PTY and terminal state remain alive.
|
||||
/// Must be called from the main thread. Returns false if already detached.
|
||||
pub fn detachRenderer(self: *Surface) bool {
|
||||
if (self.renderer_detached) return false;
|
||||
|
||||
log.info("detaching renderer from surface addr={x}", .{@intFromPtr(self)});
|
||||
|
||||
// The IO thread stays alive while detached. Disconnect its renderer
|
||||
// handles before destroying renderer-thread resources so PTY output does
|
||||
// not notify freed async handles or push to a freed mailbox.
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.io.setRenderer(null, null);
|
||||
}
|
||||
|
||||
// Stop rendering thread
|
||||
{
|
||||
self.renderer_thread.stop.notify() catch |err|
|
||||
log.err("error notifying renderer thread to stop, may stall err={}", .{err});
|
||||
self.renderer_thr.join();
|
||||
|
||||
// We need to become the active rendering thread again for cleanup
|
||||
self.renderer.threadEnter(self.rt_surface) catch unreachable;
|
||||
}
|
||||
|
||||
// Clear the display callback on the Metal layer so CoreAnimation
|
||||
// stops calling into the renderer. Without this, CA's display
|
||||
// callback fires after deinit and accesses freed Metal resources.
|
||||
self.renderer.api.layer.setDisplayCallback(null, null);
|
||||
|
||||
// Deinit renderer thread resources (event loop, timers, mailbox)
|
||||
self.renderer_thread.deinit();
|
||||
|
||||
// Deinit the renderer (frees all GPU resources)
|
||||
self.renderer.deinit();
|
||||
|
||||
self.renderer_detached = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Attach (or reattach) the renderer to this surface. This rebuilds the
|
||||
/// GPU pipeline, font atlas, and restarts the renderer thread. The terminal
|
||||
/// grid is redrawn on the first frame.
|
||||
/// Must be called from the main thread. Returns false if already attached.
|
||||
/// The caller must provide the current full configuration.
|
||||
pub fn attachRenderer(self: *Surface, config: *const configpkg.Config) !bool {
|
||||
if (!self.renderer_detached) return false;
|
||||
|
||||
log.info("attaching renderer to surface addr={x}", .{@intFromPtr(self)});
|
||||
|
||||
// Initialize our renderer with the current surface.
|
||||
try Renderer.surfaceInit(self.rt_surface);
|
||||
|
||||
// Determine our DPI configurations.
|
||||
const content_scale = try self.rt_surface.getContentScale();
|
||||
const x_dpi = content_scale.x * font.face.default_dpi;
|
||||
const y_dpi = content_scale.y * font.face.default_dpi;
|
||||
|
||||
// Get our current font grid (we kept the reference alive during detach).
|
||||
const font_grid = self.app.font_grid_set.get(self.font_grid_key) orelse
|
||||
return error.FontGridNotFound;
|
||||
|
||||
// Rebuild our size struct from current state.
|
||||
const size: rendererpkg.Size = size: {
|
||||
var size: rendererpkg.Size = .{
|
||||
.screen = screen: {
|
||||
const surface_size = try self.rt_surface.getSize();
|
||||
break :screen .{
|
||||
.width = surface_size.width,
|
||||
.height = surface_size.height,
|
||||
};
|
||||
},
|
||||
.cell = font_grid.cellSize(),
|
||||
.padding = .{},
|
||||
};
|
||||
|
||||
const explicit: rendererpkg.Padding = self.config.scaledPadding(
|
||||
x_dpi,
|
||||
y_dpi,
|
||||
);
|
||||
if (self.config.window_padding_balance != .false) {
|
||||
size.balancePadding(explicit, self.config.window_padding_balance);
|
||||
} else {
|
||||
size.padding = explicit;
|
||||
}
|
||||
|
||||
break :size size;
|
||||
};
|
||||
self.size = size;
|
||||
|
||||
const app_mailbox: App.Mailbox = .{ .rt_app = self.rt_app, .mailbox = &self.app.mailbox };
|
||||
|
||||
// Rebuild the renderer
|
||||
const renderer_impl = try Renderer.init(self.alloc, .{
|
||||
.config = try Renderer.DerivedConfig.init(self.alloc, config),
|
||||
.font_grid = font_grid,
|
||||
.size = size,
|
||||
.surface_mailbox = .{ .surface = self, .app = app_mailbox },
|
||||
.rt_surface = self.rt_surface,
|
||||
.thread = &self.renderer_thread,
|
||||
});
|
||||
|
||||
// Create the renderer thread
|
||||
const render_thread = try rendererpkg.Thread.init(
|
||||
self.alloc,
|
||||
config,
|
||||
self.rt_surface,
|
||||
&self.renderer,
|
||||
&self.renderer_state,
|
||||
app_mailbox,
|
||||
);
|
||||
|
||||
// Update our fields
|
||||
self.renderer = renderer_impl;
|
||||
self.renderer_thread = render_thread;
|
||||
self.renderer_state.terminal = &self.io.terminal;
|
||||
|
||||
// Finalize surface setup on the main thread
|
||||
try self.renderer.finalizeSurfaceInit(self.rt_surface);
|
||||
|
||||
// Start our renderer thread
|
||||
self.renderer_thr = try std.Thread.spawn(
|
||||
.{},
|
||||
rendererpkg.Thread.threadMain,
|
||||
.{&self.renderer_thread},
|
||||
);
|
||||
self.renderer_thr.setName("renderer") catch {};
|
||||
|
||||
// Reconnect the live IO thread to the newly-created renderer thread.
|
||||
{
|
||||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.io.setRenderer(self.renderer_thread.wakeup, self.renderer_thread.mailbox);
|
||||
}
|
||||
|
||||
// Trigger a full redraw
|
||||
try self.resize(self.size.screen);
|
||||
|
||||
self.renderer_detached = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Returns a mailbox that can be used to send messages to this surface.
|
||||
inline fn surfaceMailbox(self: *Surface) Mailbox {
|
||||
return .{
|
||||
|
|
@ -879,6 +1029,8 @@ fn queueIo(
|
|||
/// is in the middle of animation (such as a resize, etc.) or when
|
||||
/// the render timer is managed manually by the apprt.
|
||||
pub fn draw(self: *Surface) !void {
|
||||
if (self.renderer_detached) return;
|
||||
|
||||
// Renderers are required to support `drawFrame` being called from
|
||||
// the main thread, so that they can update contents during resize.
|
||||
try self.renderer.drawFrame(true);
|
||||
|
|
@ -2438,6 +2590,8 @@ pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) !void {
|
|||
/// isn't guaranteed to happen immediately but it will happen as soon as
|
||||
/// practical.
|
||||
fn queueRender(self: *Surface) !void {
|
||||
if (self.renderer_detached) return;
|
||||
|
||||
try self.renderer_thread.wakeup.notify();
|
||||
}
|
||||
|
||||
|
|
@ -3268,6 +3422,8 @@ pub fn textCallback(self: *Surface, text: []const u8) !void {
|
|||
/// of focus state. This is used to pause rendering when the surface
|
||||
/// is not visible, and also re-render when it becomes visible again.
|
||||
pub fn occlusionCallback(self: *Surface, visible: bool) !void {
|
||||
if (self.renderer_detached) return;
|
||||
|
||||
// Crash metadata in case we crash in here
|
||||
crash.sentry.thread_state = self.crashThreadState();
|
||||
defer crash.sentry.thread_state = null;
|
||||
|
|
@ -3292,9 +3448,11 @@ pub fn focusCallback(self: *Surface, focused: bool) !void {
|
|||
self.focused = focused;
|
||||
|
||||
// Notify our render thread of the new state
|
||||
_ = self.renderer_thread.mailbox.push(.{
|
||||
.focus = focused,
|
||||
}, .{ .forever = {} });
|
||||
if (!self.renderer_detached) {
|
||||
_ = self.renderer_thread.mailbox.push(.{
|
||||
.focus = focused,
|
||||
}, .{ .forever = {} });
|
||||
}
|
||||
|
||||
if (!focused) unfocused: {
|
||||
// If we lost focus and we have a keypress, then we want to send a key
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ const CoreApp = @import("../App.zig");
|
|||
const CoreInspector = @import("../inspector/main.zig").Inspector;
|
||||
const CoreSurface = @import("../Surface.zig");
|
||||
const configpkg = @import("../config.zig");
|
||||
const termio = @import("../termio.zig");
|
||||
const Config = configpkg.Config;
|
||||
const String = @import("../main_c.zig").String;
|
||||
|
||||
|
|
@ -421,6 +422,11 @@ pub const Surface = struct {
|
|||
/// that getTitle works without the implementer needing to save it.
|
||||
title: ?[:0]const u8 = null,
|
||||
|
||||
/// Callback for receiving PTY output data. Called from the IO thread
|
||||
/// with raw bytes read from the PTY.
|
||||
data_callback: ?*const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void = null,
|
||||
data_callback_userdata: ?*anyopaque = null,
|
||||
|
||||
/// Surface initialization options.
|
||||
pub const Options = extern struct {
|
||||
/// The platform that this surface is being initialized for and
|
||||
|
|
@ -775,6 +781,17 @@ pub const Surface = struct {
|
|||
};
|
||||
}
|
||||
|
||||
pub fn detachRenderer(self: *Surface) bool {
|
||||
return self.core_surface.detachRenderer();
|
||||
}
|
||||
|
||||
pub fn attachRenderer(self: *Surface) bool {
|
||||
return self.core_surface.attachRenderer(&self.app.config) catch |err| {
|
||||
log.err("error attaching renderer err={}", .{err});
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn updateContentScale(self: *Surface, x: f64, y: f64) void {
|
||||
// We are an embedded API so the caller can send us all sorts of
|
||||
// garbage. We want to make sure that the float values are valid
|
||||
|
|
@ -1305,6 +1322,31 @@ pub const CAPI = struct {
|
|||
}
|
||||
};
|
||||
|
||||
const Cell = extern struct {
|
||||
codepoint: u32 = 0,
|
||||
fg_rgb: u32 = 0,
|
||||
bg_rgb: u32 = 0,
|
||||
flags: u32 = 0,
|
||||
};
|
||||
|
||||
const Cells = extern struct {
|
||||
cols: u32 = 0,
|
||||
rows: u32 = 0,
|
||||
cells: ?[*]Cell = null,
|
||||
cells_len: usize = 0,
|
||||
cursor_x: u32 = 0,
|
||||
cursor_y: u32 = 0,
|
||||
cursor_visible: bool = false,
|
||||
default_fg: u32 = 0,
|
||||
default_bg: u32 = 0,
|
||||
alt_screen: bool = false,
|
||||
cursor_keys: bool = false,
|
||||
bracketed_paste: bool = false,
|
||||
focus_event: bool = false,
|
||||
mouse_event: u32 = 0,
|
||||
mouse_format: u32 = 0,
|
||||
};
|
||||
|
||||
// ghostty_point_s
|
||||
const Point = extern struct {
|
||||
tag: Tag,
|
||||
|
|
@ -1756,6 +1798,120 @@ pub const CAPI = struct {
|
|||
surface.occlusionCallback(visible);
|
||||
}
|
||||
|
||||
/// Detach the surface's renderer from its platform view. Frees all GPU
|
||||
/// resources and stops the renderer thread. The PTY and terminal state
|
||||
/// remain alive. The surface pointer remains valid.
|
||||
export fn ghostty_surface_detach(surface: *Surface) bool {
|
||||
return surface.detachRenderer();
|
||||
}
|
||||
|
||||
/// Attach (or reattach) the surface's renderer to a platform view.
|
||||
/// Rebuilds the GPU pipeline and restarts the renderer thread.
|
||||
export fn ghostty_surface_attach(surface: *Surface) bool {
|
||||
return surface.attachRenderer();
|
||||
}
|
||||
|
||||
/// Set a callback that is called with raw PTY output data.
|
||||
/// Pass null to clear a previously set callback.
|
||||
export fn ghostty_surface_set_data_callback(
|
||||
surface: *Surface,
|
||||
callback: ?*const fn (?*anyopaque, [*]const u8, usize) callconv(.c) void,
|
||||
userdata: ?*anyopaque,
|
||||
) void {
|
||||
surface.data_callback = callback;
|
||||
surface.data_callback_userdata = userdata;
|
||||
}
|
||||
|
||||
/// Send raw bytes directly to the PTY input without any paste processing.
|
||||
export fn ghostty_surface_send_input_raw(
|
||||
surface: *Surface,
|
||||
ptr: [*]const u8,
|
||||
len: usize,
|
||||
) void {
|
||||
if (len == 0) return;
|
||||
surface.core_surface.io.queueMessage(
|
||||
termio.Message.writeReq(global.alloc, ptr[0..len]) catch |err| {
|
||||
log.warn("failed to queue raw input err={}", .{err});
|
||||
return;
|
||||
},
|
||||
.unlocked,
|
||||
);
|
||||
}
|
||||
|
||||
/// Read the terminal cell grid including codepoints, colors, cursor, and mode flags.
|
||||
export fn ghostty_surface_read_cells(
|
||||
surface: *Surface,
|
||||
out: *Cells,
|
||||
) bool {
|
||||
const core = &surface.core_surface;
|
||||
core.renderer_state.mutex.lock();
|
||||
defer core.renderer_state.mutex.unlock();
|
||||
|
||||
const term = core.io.terminal;
|
||||
const screen = term.screens.active;
|
||||
const pages = &screen.pages;
|
||||
|
||||
out.* = .{
|
||||
.cols = @intCast(pages.cols),
|
||||
.rows = @intCast(pages.rows),
|
||||
.cells = null,
|
||||
.cells_len = 0,
|
||||
.cursor_x = @intCast(screen.cursor.x),
|
||||
.cursor_y = @intCast(screen.cursor.y),
|
||||
.cursor_visible = true,
|
||||
.default_fg = rgb: {
|
||||
const rgb = term.colors.foreground.get() orelse break :rgb 0xFFFFFFFF;
|
||||
break :rgb @as(u32, rgb.r) << 16 | @as(u32, rgb.g) << 8 | @as(u32, rgb.b);
|
||||
},
|
||||
.default_bg = rgb: {
|
||||
const rgb = term.colors.background.get() orelse break :rgb 0xFF000000;
|
||||
break :rgb @as(u32, rgb.r) << 16 | @as(u32, rgb.g) << 8 | @as(u32, rgb.b);
|
||||
},
|
||||
.alt_screen = core.io.terminal.modes.get(.alt_screen),
|
||||
.cursor_keys = core.io.terminal.modes.get(.cursor_keys),
|
||||
.bracketed_paste = core.io.terminal.modes.get(.bracketed_paste),
|
||||
.focus_event = core.io.terminal.modes.get(.focus_event),
|
||||
.mouse_event = 0,
|
||||
.mouse_format = 0,
|
||||
};
|
||||
|
||||
const total = pages.cols * pages.rows;
|
||||
if (total == 0) return true;
|
||||
|
||||
const cells_ptr = global.alloc.alloc(Cell, total) catch return false;
|
||||
out.cells = cells_ptr.ptr;
|
||||
out.cells_len = @intCast(total);
|
||||
|
||||
var row_idx: usize = 0;
|
||||
var y: usize = 0;
|
||||
while (y < pages.rows) : (y += 1) {
|
||||
var x: usize = 0;
|
||||
while (x < pages.cols) : (x += 1) {
|
||||
const pt = terminal.point.Point{ .screen = .{ .x = @intCast(x), .y = @intCast(y) } };
|
||||
const page_pin = pages.pin(pt) orelse continue;
|
||||
const rc = page_pin.rowAndCell();
|
||||
const idx = row_idx * pages.cols + x;
|
||||
cells_ptr[idx] = .{
|
||||
.codepoint = rc.cell.codepoint(),
|
||||
};
|
||||
}
|
||||
row_idx += 1;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Free the cells array previously allocated by ghostty_surface_read_cells.
|
||||
export fn ghostty_surface_free_cells(
|
||||
_: *Surface,
|
||||
out: *Cells,
|
||||
) void {
|
||||
if (out.cells) |ptr| {
|
||||
global.alloc.free(ptr[0..out.cells_len]);
|
||||
}
|
||||
out.* = .{};
|
||||
}
|
||||
|
||||
/// Filter the mods if necessary. This handles settings such as
|
||||
/// `macos-option-as-alt`. The filtered mods should be used for
|
||||
/// key translation but should NOT be sent back via the `_key`
|
||||
|
|
|
|||
|
|
@ -389,6 +389,17 @@ fn collection(
|
|||
return c;
|
||||
}
|
||||
|
||||
/// Retrieve the SharedGrid for an existing key. Returns null if the
|
||||
/// key is not found. The caller must already hold a reference to the key.
|
||||
/// Thread-safe: acquires the internal lock.
|
||||
pub fn get(self: *SharedGridSet, key: Key) ?*SharedGrid {
|
||||
self.lock.lock();
|
||||
defer self.lock.unlock();
|
||||
|
||||
const entry = self.map.getEntry(key) orelse return null;
|
||||
return entry.value_ptr.grid;
|
||||
}
|
||||
|
||||
/// Decrement the ref count for the given key. If the ref count is zero,
|
||||
/// the grid will be deinitialized and removed from the map.j:w
|
||||
pub fn deref(self: *SharedGridSet, key: Key) void {
|
||||
|
|
|
|||
|
|
@ -45,10 +45,10 @@ renderer_state: *renderer.State,
|
|||
|
||||
/// A handle to wake up the renderer. This hints to the renderer that
|
||||
/// a repaint should happen.
|
||||
renderer_wakeup: xev.Async,
|
||||
renderer_wakeup: ?xev.Async,
|
||||
|
||||
/// The mailbox for notifying the renderer of things.
|
||||
renderer_mailbox: *renderer.Thread.Mailbox,
|
||||
renderer_mailbox: ?*renderer.Thread.Mailbox,
|
||||
|
||||
/// The mailbox for communicating with the surface.
|
||||
surface_mailbox: apprt.surface.Mailbox,
|
||||
|
|
@ -324,6 +324,21 @@ pub fn deinit(self: *Termio) void {
|
|||
if (self.thread_enter_state) |v| v.destroy();
|
||||
}
|
||||
|
||||
/// Update the renderer thread handles used by termio and the stream parser.
|
||||
///
|
||||
/// The caller must hold renderer_state.mutex. This serializes updates with
|
||||
/// output processing, which also touches these handles under the same mutex.
|
||||
pub fn setRenderer(
|
||||
self: *Termio,
|
||||
renderer_wakeup: ?xev.Async,
|
||||
renderer_mailbox: ?*renderer.Thread.Mailbox,
|
||||
) void {
|
||||
self.renderer_wakeup = renderer_wakeup;
|
||||
self.renderer_mailbox = renderer_mailbox;
|
||||
self.terminal_stream.handler.renderer_wakeup = renderer_wakeup;
|
||||
self.terminal_stream.handler.renderer_mailbox = renderer_mailbox;
|
||||
}
|
||||
|
||||
pub fn threadEnter(
|
||||
self: *Termio,
|
||||
thread: *termio.Thread,
|
||||
|
|
@ -498,8 +513,10 @@ pub fn resize(
|
|||
}
|
||||
|
||||
// Mail the renderer so that it can update the GPU and re-render
|
||||
_ = self.renderer_mailbox.push(.{ .resize = size }, .{ .forever = {} });
|
||||
self.renderer_wakeup.notify() catch {};
|
||||
if (self.renderer_mailbox) |mailbox| {
|
||||
_ = mailbox.push(.{ .resize = size }, .{ .forever = {} });
|
||||
}
|
||||
if (self.renderer_wakeup) |wakeup| wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
/// Make a size report.
|
||||
|
|
@ -537,7 +554,7 @@ pub fn resetSynchronizedOutput(self: *Termio) void {
|
|||
self.renderer_state.mutex.lock();
|
||||
defer self.renderer_state.mutex.unlock();
|
||||
self.terminal.modes.set(.synchronized_output, false);
|
||||
self.renderer_wakeup.notify() catch {};
|
||||
if (self.renderer_wakeup) |wakeup| wakeup.notify() catch {};
|
||||
}
|
||||
|
||||
/// Clear the screen.
|
||||
|
|
@ -613,7 +630,7 @@ pub fn jumpToPrompt(self: *Termio, delta: isize) !void {
|
|||
self.terminal.screens.active.scroll(.{ .delta_prompt = delta });
|
||||
}
|
||||
|
||||
try self.renderer_wakeup.notify();
|
||||
if (self.renderer_wakeup) |wakeup| try wakeup.notify();
|
||||
}
|
||||
|
||||
/// Called when focus is gained or lost (when focus events are enabled)
|
||||
|
|
@ -641,6 +658,9 @@ pub fn focusGained(self: *Termio, td: *ThreadData, focused: bool) !void {
|
|||
/// call with pty data but it is also called by the read thread when using
|
||||
/// an exec subprocess.
|
||||
pub fn processOutput(self: *Termio, buf: []const u8) void {
|
||||
// Notify data callback if registered (raw PTY output).
|
||||
self.invokeDataCallback(buf);
|
||||
|
||||
// We are modifying terminal state from here on out and we need
|
||||
// the lock to grab our read data.
|
||||
self.renderer_state.mutex.lock();
|
||||
|
|
@ -648,6 +668,17 @@ pub fn processOutput(self: *Termio, buf: []const u8) void {
|
|||
self.processOutputLocked(buf);
|
||||
}
|
||||
|
||||
fn invokeDataCallback(self: *Termio, buf: []const u8) void {
|
||||
if (buf.len == 0) return;
|
||||
const surface = self.surface_mailbox.surface;
|
||||
const embedded: ?*apprt.embedded.Surface = @ptrCast(@alignCast(surface));
|
||||
if (embedded) |s| {
|
||||
const callback = s.data_callback orelse return;
|
||||
const userdata = s.data_callback_userdata;
|
||||
callback(userdata, buf.ptr, buf.len);
|
||||
}
|
||||
}
|
||||
|
||||
/// Process output from readdata but the lock is already held.
|
||||
fn processOutputLocked(self: *Termio, buf: []const u8) void {
|
||||
// Schedule a render. We can call this first because we have the lock.
|
||||
|
|
@ -665,9 +696,11 @@ fn processOutputLocked(self: *Termio, buf: []const u8) void {
|
|||
}
|
||||
|
||||
self.last_cursor_reset = now;
|
||||
_ = self.renderer_mailbox.push(.{
|
||||
.reset_cursor_blink = {},
|
||||
}, .{ .instant = {} });
|
||||
if (self.renderer_mailbox) |mailbox| {
|
||||
_ = mailbox.push(.{
|
||||
.reset_cursor_blink = {},
|
||||
}, .{ .instant = {} });
|
||||
}
|
||||
} else |err| {
|
||||
log.warn("failed to get current time err={}", .{err});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -357,7 +357,7 @@ fn drainMailbox(
|
|||
// Trigger a redraw after we've drained so we don't waste cyces
|
||||
// messaging a redraw.
|
||||
if (redraw) {
|
||||
try io.renderer_wakeup.notify();
|
||||
if (io.renderer_wakeup) |wakeup| try wakeup.notify();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ pub const StreamHandler = struct {
|
|||
renderer_state: *renderer.State,
|
||||
|
||||
/// The mailbox for notifying the renderer of things.
|
||||
renderer_mailbox: *renderer.Thread.Mailbox,
|
||||
renderer_mailbox: ?*renderer.Thread.Mailbox,
|
||||
|
||||
/// A handle to wake up the renderer. This hints to the renderer that
|
||||
/// a repaint should happen.
|
||||
renderer_wakeup: xev.Async,
|
||||
renderer_wakeup: ?xev.Async,
|
||||
|
||||
/// The default cursor state. This is used with CSI q. This is
|
||||
/// set to true when we're currently in the default cursor state.
|
||||
|
|
@ -102,7 +102,7 @@ pub const StreamHandler = struct {
|
|||
/// isn't guaranteed to happen immediately but it will happen as soon as
|
||||
/// practical.
|
||||
pub inline fn queueRender(self: *StreamHandler) !void {
|
||||
try self.renderer_wakeup.notify();
|
||||
if (self.renderer_wakeup) |wakeup| try wakeup.notify();
|
||||
}
|
||||
|
||||
/// Change the configuration for this handler.
|
||||
|
|
@ -148,10 +148,12 @@ pub const StreamHandler = struct {
|
|||
self: *StreamHandler,
|
||||
msg: renderer.Message,
|
||||
) void {
|
||||
const mailbox = self.renderer_mailbox orelse return;
|
||||
|
||||
// See termio.Mailbox.send for more details on how this works.
|
||||
|
||||
// Try instant first. If it works then we can return.
|
||||
if (self.renderer_mailbox.push(msg, .{ .instant = {} }) > 0) {
|
||||
if (mailbox.push(msg, .{ .instant = {} }) > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -160,16 +162,18 @@ pub const StreamHandler = struct {
|
|||
// and then try again.
|
||||
self.renderer_state.mutex.unlock();
|
||||
defer self.renderer_state.mutex.lock();
|
||||
self.renderer_wakeup.notify() catch |err| {
|
||||
// This is an EXTREMELY unlikely case. We still don't return
|
||||
// and attempt to send the message because its most likely
|
||||
// that everything is fine, but log in case a freeze happens.
|
||||
log.warn(
|
||||
"failed to notify renderer, may deadlock err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
_ = self.renderer_mailbox.push(msg, .{ .forever = {} });
|
||||
if (self.renderer_wakeup) |wakeup| {
|
||||
wakeup.notify() catch |err| {
|
||||
// This is an EXTREMELY unlikely case. We still don't return
|
||||
// and attempt to send the message because its most likely
|
||||
// that everything is fine, but log in case a freeze happens.
|
||||
log.warn(
|
||||
"failed to notify renderer, may deadlock err={}",
|
||||
.{err},
|
||||
);
|
||||
};
|
||||
}
|
||||
_ = mailbox.push(msg, .{ .forever = {} });
|
||||
}
|
||||
|
||||
pub fn vt(
|
||||
|
|
|
|||
Loading…
Reference in New Issue