//! Surface represents a single terminal "surface". A terminal surface is //! a minimal "widget" where the terminal is drawn and responds to events //! such as keyboard and mouse. Each surface also creates and owns its pty //! session. //! //! The word "surface" is used because it is left to the higher level //! application runtime to determine if the surface is a window, a tab, //! a split, a preview pane in a larger window, etc. This struct doesn't care: //! it just draws and responds to events. The events come from the application //! runtime so the runtime can determine when and how those are delivered //! (i.e. with focus, without focus, and so on). const Surface = @This(); const apprt = @import("apprt.zig"); pub const Mailbox = apprt.surface.Mailbox; pub const Message = apprt.surface.Message; const std = @import("std"); const builtin = @import("builtin"); const assert = std.debug.assert; const Allocator = std.mem.Allocator; const ArenaAllocator = std.heap.ArenaAllocator; const renderer = @import("renderer.zig"); const termio = @import("termio.zig"); const objc = @import("objc"); const imgui = @import("imgui"); const Pty = @import("Pty.zig"); const font = @import("font/main.zig"); const Command = @import("Command.zig"); const trace = @import("tracy").trace; const terminal = @import("terminal/main.zig"); const configpkg = @import("config.zig"); const input = @import("input.zig"); const DevMode = @import("DevMode.zig"); const App = @import("App.zig"); const internal_os = @import("os/main.zig"); const log = std.log.scoped(.surface); // The renderer implementation to use. const Renderer = renderer.Renderer; /// Allocator alloc: Allocator, /// The mailbox for sending messages to the main app thread. app_mailbox: App.Mailbox, /// The windowing system surface rt_surface: *apprt.runtime.Surface, /// The font structures font_lib: font.Library, font_group: *font.GroupCache, font_size: font.face.DesiredSize, /// Imgui context imgui_ctx: if (DevMode.enabled) *imgui.Context else void, /// The renderer for this surface. renderer: Renderer, /// The render state renderer_state: renderer.State, /// The renderer thread manager renderer_thread: renderer.Thread, /// The actual thread renderer_thr: std.Thread, /// Mouse state. mouse: Mouse, /// The terminal IO handler. io: termio.Impl, io_thread: termio.Thread, io_thr: std.Thread, /// All the cached sizes since we need them at various times. screen_size: renderer.ScreenSize, grid_size: renderer.GridSize, cell_size: renderer.CellSize, /// Explicit padding due to configuration padding: renderer.Padding, /// The configuration derived from the main config. We "derive" it so that /// we don't have a shared pointer hanging around that we need to worry about /// the lifetime of. This makes updating config at runtime easier. config: DerivedConfig, /// Set to true for a single GLFW key/char callback cycle to cause the /// char callback to ignore. GLFW seems to always do key followed by char /// callbacks so we abuse that here. This is to solve an issue where commands /// like such as "control-v" will write a "v" even if they're intercepted. ignore_char: bool = false, /// Mouse state for the surface. const Mouse = struct { /// The last tracked mouse button state by button. click_state: [input.MouseButton.max]input.MouseButtonState = .{.release} ** input.MouseButton.max, /// The last mods state when the last mouse button (whatever it was) was /// pressed or release. mods: input.Mods = .{}, /// The point at which the left mouse click happened. This is in screen /// coordinates so that scrolling preserves the location. left_click_point: terminal.point.ScreenPoint = .{}, /// The starting xpos/ypos of the left click. Note that if scrolling occurs, /// these will point to different "cells", but the xpos/ypos will stay /// stable during scrolling relative to the surface. left_click_xpos: f64 = 0, left_click_ypos: f64 = 0, /// The count of clicks to count double and triple clicks and so on. /// The left click time was the last time the left click was done. This /// is always set on the first left click. left_click_count: u8 = 0, left_click_time: std.time.Instant = undefined, /// The last x/y sent for mouse reports. event_point: terminal.point.Viewport = .{}, }; /// The configuration that a surface has, this is copied from the main /// Config struct usually to prevent sharing a single value. const DerivedConfig = struct { arena: ArenaAllocator, /// For docs for these, see the associated config they are derived from. original_font_size: u8, keybind: configpkg.Keybinds, clipboard_read: bool, clipboard_write: bool, clipboard_trim_trailing_spaces: bool, mouse_interval: u64, pub fn init(alloc_gpa: Allocator, config: *const configpkg.Config) !DerivedConfig { var arena = ArenaAllocator.init(alloc_gpa); errdefer arena.deinit(); const alloc = arena.allocator(); return .{ .original_font_size = config.@"font-size", .keybind = try config.keybind.clone(alloc), .clipboard_read = config.@"clipboard-read", .clipboard_write = config.@"clipboard-write", .clipboard_trim_trailing_spaces = config.@"clipboard-trim-trailing-spaces", .mouse_interval = config.@"click-repeat-interval" * 1_000_000, // 500ms // Assignments happen sequentially so we have to do this last // so that the memory is captured from allocs above. .arena = arena, }; } pub fn deinit(self: *DerivedConfig) void { self.arena.deinit(); } }; /// Create a new surface. This must be called from the main thread. The /// pointer to the memory for the surface must be provided and must be /// stable due to interfacing with various callbacks. pub fn init( self: *Surface, alloc: Allocator, config: *const configpkg.Config, app_mailbox: App.Mailbox, rt_surface: *apprt.runtime.Surface, ) !void { // Initialize our renderer with our initialized surface. try Renderer.surfaceInit(rt_surface); // Determine our DPI configurations so we can properly configure // font points to pixels and handle other high-DPI scaling factors. const content_scale = try rt_surface.getContentScale(); const x_dpi = content_scale.x * font.face.default_dpi; const y_dpi = content_scale.y * font.face.default_dpi; log.debug("xscale={} yscale={} xdpi={} ydpi={}", .{ content_scale.x, content_scale.y, x_dpi, y_dpi, }); // The font size we desire along with the DPI determined for the surface const font_size: font.face.DesiredSize = .{ .points = config.@"font-size", .xdpi = @floatToInt(u16, x_dpi), .ydpi = @floatToInt(u16, y_dpi), }; // Find all the fonts for this surface // // Future: we can share the font group amongst all surfaces to save // some new surface init time and some memory. This will require making // thread-safe changes to font structs. var font_lib = try font.Library.init(); errdefer font_lib.deinit(); var font_group = try alloc.create(font.GroupCache); errdefer alloc.destroy(font_group); font_group.* = try font.GroupCache.init(alloc, group: { var group = try font.Group.init(alloc, font_lib, font_size); errdefer group.deinit(); // Search for fonts if (font.Discover != void) { var disco = font.Discover.init(); group.discover = disco; if (config.@"font-family") |family| { var disco_it = try disco.discover(.{ .family = family, .size = font_size.points, }); defer disco_it.deinit(); if (try disco_it.next()) |face| { log.info("font regular: {s}", .{try face.name()}); try group.addFace(alloc, .regular, face); } } if (config.@"font-family-bold") |family| { var disco_it = try disco.discover(.{ .family = family, .size = font_size.points, .bold = true, }); defer disco_it.deinit(); if (try disco_it.next()) |face| { log.info("font bold: {s}", .{try face.name()}); try group.addFace(alloc, .bold, face); } } if (config.@"font-family-italic") |family| { var disco_it = try disco.discover(.{ .family = family, .size = font_size.points, .italic = true, }); defer disco_it.deinit(); if (try disco_it.next()) |face| { log.info("font italic: {s}", .{try face.name()}); try group.addFace(alloc, .italic, face); } } if (config.@"font-family-bold-italic") |family| { var disco_it = try disco.discover(.{ .family = family, .size = font_size.points, .bold = true, .italic = true, }); defer disco_it.deinit(); if (try disco_it.next()) |face| { log.info("font bold+italic: {s}", .{try face.name()}); try group.addFace(alloc, .bold_italic, face); } } } // Our built-in font will be used as a backup try group.addFace( alloc, .regular, font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)), ); try group.addFace( alloc, .bold, font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)), ); // Emoji fallback. We don't include this on Mac since Mac is expected // to always have the Apple Emoji available. if (builtin.os.tag != .macos or font.Discover == void) { try group.addFace( alloc, .regular, font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)), ); try group.addFace( alloc, .regular, font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)), ); } // If we're on Mac, then we try to use the Apple Emoji font for Emoji. if (builtin.os.tag == .macos and font.Discover != void) { var disco = font.Discover.init(); defer disco.deinit(); var disco_it = try disco.discover(.{ .family = "Apple Color Emoji", .size = font_size.points, }); defer disco_it.deinit(); if (try disco_it.next()) |face| { log.debug("font emoji: {s}", .{try face.name()}); try group.addFace(alloc, .regular, face); } } break :group group; }); errdefer font_group.deinit(alloc); // Pre-calculate our initial cell size ourselves. const cell_size = try renderer.CellSize.init(alloc, font_group); // Convert our padding from points to pixels const padding_x = (@intToFloat(f32, config.@"window-padding-x") * x_dpi) / 72; const padding_y = (@intToFloat(f32, config.@"window-padding-y") * y_dpi) / 72; const padding: renderer.Padding = .{ .top = padding_y, .bottom = padding_y, .right = padding_x, .left = padding_x, }; // Create our terminal grid with the initial size var renderer_impl = try Renderer.init(alloc, .{ .config = try Renderer.DerivedConfig.init(alloc, config), .font_group = font_group, .padding = .{ .explicit = padding, .balance = config.@"window-padding-balance", }, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, }); errdefer renderer_impl.deinit(); // Calculate our grid size based on known dimensions. const surface_size = try rt_surface.getSize(); const screen_size: renderer.ScreenSize = .{ .width = surface_size.width, .height = surface_size.height, }; const grid_size = renderer.GridSize.init( screen_size.subPadding(padding), cell_size, ); // The mutex used to protect our renderer state. var mutex = try alloc.create(std.Thread.Mutex); mutex.* = .{}; errdefer alloc.destroy(mutex); // Create the renderer thread var render_thread = try renderer.Thread.init( alloc, rt_surface, &self.renderer, &self.renderer_state, app_mailbox, ); errdefer render_thread.deinit(); // Start our IO implementation var io = try termio.Impl.init(alloc, .{ .grid_size = grid_size, .screen_size = screen_size, .full_config = config, .config = try termio.Impl.DerivedConfig.init(alloc, config), .renderer_state = &self.renderer_state, .renderer_wakeup = render_thread.wakeup, .renderer_mailbox = render_thread.mailbox, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, }); errdefer io.deinit(); // Create the IO thread var io_thread = try termio.Thread.init(alloc, &self.io); errdefer io_thread.deinit(); // True if this surface is hosting devmode. We only host devmode on // the first surface since imgui is not threadsafe. We need to do some // work to make DevMode work with multiple threads. const host_devmode = DevMode.enabled and DevMode.instance.surface == null; self.* = .{ .alloc = alloc, .app_mailbox = app_mailbox, .rt_surface = rt_surface, .font_lib = font_lib, .font_group = font_group, .font_size = font_size, .renderer = renderer_impl, .renderer_thread = render_thread, .renderer_state = .{ .mutex = mutex, .cursor = .{ .style = .blinking_block, .visible = true, }, .terminal = &self.io.terminal, .devmode = if (!host_devmode) null else &DevMode.instance, }, .renderer_thr = undefined, .mouse = .{}, .io = io, .io_thread = io_thread, .io_thr = undefined, .screen_size = screen_size, .grid_size = grid_size, .cell_size = cell_size, .padding = padding, .config = try DerivedConfig.init(alloc, config), .imgui_ctx = if (!DevMode.enabled) {} else try imgui.Context.create(), }; errdefer if (DevMode.enabled) self.imgui_ctx.destroy(); // Set a minimum size that is cols=10 h=4. This matches Mac's Terminal.app // but is otherwise somewhat arbitrary. try rt_surface.setSizeLimits(.{ .width = @floatToInt(u32, cell_size.width * 10), .height = @floatToInt(u32, cell_size.height * 4), }, null); // Call our size callback which handles all our retina setup // Note: this shouldn't be necessary and when we clean up the surface // init stuff we should get rid of this. But this is required because // sizeCallback does retina-aware stuff we don't do here and don't want // to duplicate. try self.sizeCallback(surface_size); // Load imgui. This must be done LAST because it has to be done after // all our GLFW setup is complete. if (DevMode.enabled and DevMode.instance.surface == null) { const dev_io = try imgui.IO.get(); dev_io.cval().IniFilename = "ghostty_dev_mode.ini"; // Add our built-in fonts so it looks slightly better const dev_atlas = @ptrCast(*imgui.FontAtlas, dev_io.cval().Fonts); dev_atlas.addFontFromMemoryTTF( face_ttf, @intToFloat(f32, font_size.pixels()), ); // Default dark style const style = try imgui.Style.get(); style.colorsDark(); // Add our surface to the instance if it isn't set. DevMode.instance.surface = self; // Let our renderer setup try renderer_impl.initDevMode(rt_surface); } // Give the renderer one more opportunity to finalize any surface // setup on the main thread prior to spinning up the rendering thread. try renderer_impl.finalizeSurfaceInit(rt_surface); // Start our renderer thread self.renderer_thr = try std.Thread.spawn( .{}, renderer.Thread.threadMain, .{&self.renderer_thread}, ); self.renderer_thr.setName("renderer") catch {}; // Start our IO thread self.io_thr = try std.Thread.spawn( .{}, termio.Thread.threadMain, .{&self.io_thread}, ); self.io_thr.setName("io") catch {}; } pub fn deinit(self: *Surface) void { // 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 self.renderer.threadEnter(self.rt_surface) catch unreachable; // If we are devmode-owning, clean that up. if (DevMode.enabled and DevMode.instance.surface == self) { // Let our renderer clean up self.renderer.deinitDevMode(); // Clear the surface DevMode.instance.surface = null; // Uninitialize imgui self.imgui_ctx.destroy(); } } // Stop our IO thread { self.io_thread.stop.notify() catch |err| log.err("error notifying io thread to stop, may stall err={}", .{err}); self.io_thr.join(); } // We need to deinit AFTER everything is stopped, since there are // shared values between the two threads. self.renderer_thread.deinit(); self.renderer.deinit(); self.io_thread.deinit(); self.io.deinit(); self.font_group.deinit(self.alloc); self.font_lib.deinit(); self.alloc.destroy(self.font_group); self.alloc.destroy(self.renderer_state.mutex); self.config.deinit(); log.info("surface closed addr={x}", .{@ptrToInt(self)}); } /// Close this surface. This will trigger the runtime to start the /// close process, which should ultimately deinitialize this surface. pub fn close(self: *Surface) void { self.rt_surface.close(); } /// Called from the app thread to handle mailbox messages to our specific /// surface. pub fn handleMessage(self: *Surface, msg: Message) !void { switch (msg) { .change_config => |config| try self.changeConfig(config), .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.rt_surface.setTitle(slice); }, .cell_size => |size| try self.setCellSize(size), .clipboard_read => |kind| try self.clipboardRead(kind), .clipboard_write => |req| switch (req) { .small => |v| try self.clipboardWrite(v.data[0..v.len]), .stable => |v| try self.clipboardWrite(v), .alloc => |v| { defer v.alloc.free(v.data); try self.clipboardWrite(v.data); }, }, .close => self.close(), } } /// Update our configuration at runtime. fn changeConfig(self: *Surface, config: *const configpkg.Config) !void { // Update our new derived config immediately const derived = DerivedConfig.init(self.alloc, config) catch |err| { // If the derivation fails then we just log and return. We don't // hard fail in this case because we don't want to error the surface // when config fails we just want to keep using the old config. log.err("error updating configuration err={}", .{err}); return; }; self.config.deinit(); self.config = derived; // We need to store our configs in a heap-allocated pointer so that // our messages aren't huge. var renderer_config_ptr = try self.alloc.create(Renderer.DerivedConfig); errdefer self.alloc.destroy(renderer_config_ptr); var termio_config_ptr = try self.alloc.create(termio.Impl.DerivedConfig); errdefer self.alloc.destroy(termio_config_ptr); // Update our derived configurations for the renderer and termio, // then send them a message to update. renderer_config_ptr.* = try Renderer.DerivedConfig.init(self.alloc, config); errdefer renderer_config_ptr.deinit(); termio_config_ptr.* = try termio.Impl.DerivedConfig.init(self.alloc, config); errdefer termio_config_ptr.deinit(); _ = self.renderer_thread.mailbox.push(.{ .change_config = .{ .alloc = self.alloc, .ptr = renderer_config_ptr, }, }, .{ .forever = {} }); _ = self.io_thread.mailbox.push(.{ .change_config = .{ .alloc = self.alloc, .ptr = termio_config_ptr, }, }, .{ .forever = {} }); // With mailbox messages sent, we have to wake them up so they process it. self.queueRender() catch |err| { log.warn("failed to notify renderer of config change err={}", .{err}); }; self.io_thread.wakeup.notify() catch |err| { log.warn("failed to notify io thread of config change err={}", .{err}); }; } /// Returns the x/y coordinate of where the IME (Input Method Editor) /// keyboard should be rendered. pub fn imePoint(self: *const Surface) apprt.IMEPos { self.renderer_state.mutex.lock(); const cursor = self.renderer_state.terminal.screen.cursor; self.renderer_state.mutex.unlock(); // TODO: need to handle when scrolling and the cursor is not // in the visible portion of the screen. // Our sizes are all scaled so we need to send the unscaled values back. const content_scale = self.rt_surface.getContentScale() catch .{ .x = 1, .y = 1 }; const x: f64 = x: { // Simple x * cell width gives the top-left corner var x: f64 = @floatCast(f64, @intToFloat(f32, cursor.x) * self.cell_size.width); // We want the midpoint x += self.cell_size.width / 2; // And scale it x /= content_scale.x; break :x x; }; const y: f64 = y: { // Simple x * cell width gives the top-left corner var y: f64 = @floatCast(f64, @intToFloat(f32, cursor.y) * self.cell_size.height); // We want the bottom y += self.cell_size.height; // And scale it y /= content_scale.y; break :y y; }; return .{ .x = x, .y = y }; } fn clipboardRead(self: *const Surface, kind: u8) !void { if (!self.config.clipboard_read) { log.info("application attempted to read clipboard, but 'clipboard-read' setting is off", .{}); return; } const data = self.rt_surface.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; // Even if the clipboard data is empty we reply, since presumably // the client app is expecting a reply. We first allocate our buffer. // This must hold the base64 encoded data PLUS the OSC code surrounding it. const enc = std.base64.standard.Encoder; const size = enc.calcSize(data.len); var buf = try self.alloc.alloc(u8, size + 9); // const for OSC defer self.alloc.free(buf); // Wrap our data with the OSC code const prefix = try std.fmt.bufPrint(buf, "\x1b]52;{c};", .{kind}); assert(prefix.len == 7); buf[buf.len - 2] = '\x1b'; buf[buf.len - 1] = '\\'; // Do the base64 encoding const encoded = enc.encode(buf[prefix.len..], data); assert(encoded.len == size); _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, buf, ), .{ .forever = {} }); self.io_thread.wakeup.notify() catch {}; } fn clipboardWrite(self: *const Surface, data: []const u8) !void { if (!self.config.clipboard_write) { log.info("application attempted to write clipboard, but 'clipboard-write' setting is off", .{}); return; } const dec = std.base64.standard.Decoder; // Build buffer const size = try dec.calcSizeForSlice(data); var buf = try self.alloc.allocSentinel(u8, size, 0); defer self.alloc.free(buf); buf[buf.len] = 0; // Decode try dec.decode(buf, data); assert(buf[buf.len] == 0); self.rt_surface.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; } /// Change the cell size for the terminal grid. This can happen as /// a result of changing the font size at runtime. fn setCellSize(self: *Surface, size: renderer.CellSize) !void { // Update our new cell size for future calcs self.cell_size = size; // Update our grid_size self.grid_size = renderer.GridSize.init( self.screen_size.subPadding(self.padding), self.cell_size, ); // Notify the terminal _ = self.io_thread.mailbox.push(.{ .resize = .{ .grid_size = self.grid_size, .screen_size = self.screen_size, .padding = self.padding, }, }, .{ .forever = {} }); self.io_thread.wakeup.notify() catch {}; } /// Change the font size. /// /// This can only be called from the main thread. pub fn setFontSize(self: *Surface, size: font.face.DesiredSize) void { // Update our font size so future changes work self.font_size = size; // Notify our render thread of the font size. This triggers everything else. _ = self.renderer_thread.mailbox.push(.{ .font_size = size, }, .{ .forever = {} }); // Schedule render which also drains our mailbox self.queueRender() catch unreachable; } /// 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. fn queueRender(self: *const Surface) !void { try self.renderer_thread.wakeup.notify(); } pub fn sizeCallback(self: *Surface, size: apprt.SurfaceSize) !void { const tracy = trace(@src()); defer tracy.end(); // TODO: if our screen size didn't change, then we should avoid the // overhead of inter-thread communication // Save our screen size self.screen_size = .{ .width = size.width, .height = size.height, }; // Recalculate our grid size self.grid_size = renderer.GridSize.init( self.screen_size.subPadding(self.padding), self.cell_size, ); if (self.grid_size.columns < 5 and (self.padding.left > 0 or self.padding.right > 0)) { log.warn("WARNING: very small terminal grid detected with padding " ++ "set. Is your padding reasonable?", .{}); } if (self.grid_size.rows < 2 and (self.padding.top > 0 or self.padding.bottom > 0)) { log.warn("WARNING: very small terminal grid detected with padding " ++ "set. Is your padding reasonable?", .{}); } // Mail the renderer _ = self.renderer_thread.mailbox.push(.{ .screen_size = self.screen_size, }, .{ .forever = {} }); try self.queueRender(); // Mail the IO thread _ = self.io_thread.mailbox.push(.{ .resize = .{ .grid_size = self.grid_size, .screen_size = self.screen_size, .padding = self.padding, }, }, .{ .forever = {} }); try self.io_thread.wakeup.notify(); } pub fn charCallback(self: *Surface, codepoint: u21) !void { const tracy = trace(@src()); defer tracy.end(); // Dev Mode if (DevMode.enabled and DevMode.instance.visible) { // If the event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureKeyboard) { try self.queueRender(); } } else |_| {} } // Ignore if requested. See field docs for more information. if (self.ignore_char) { self.ignore_char = false; return; } // Critical area { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // Clear the selction if we have one. if (self.io.terminal.selection != null) { self.io.terminal.selection = null; try self.queueRender(); } // We want to scroll to the bottom // TODO: detect if we're at the bottom to avoid the render call here. try self.io.terminal.scrollViewport(.{ .bottom = {} }); } // Ask our IO thread to write the data var data: termio.Message.WriteReq.Small.Array = undefined; const len = try std.unicode.utf8Encode(codepoint, &data); _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = len, }, }, .{ .forever = {} }); // After sending all our messages we have to notify our IO thread try self.io_thread.wakeup.notify(); } pub fn keyCallback( self: *Surface, action: input.Action, key: input.Key, mods: input.Mods, ) !void { const tracy = trace(@src()); defer tracy.end(); // Dev Mode if (DevMode.enabled and DevMode.instance.visible) { // If the event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureKeyboard) { try self.queueRender(); } } else |_| {} } // Reset the ignore char setting. If we didn't handle the char // by here, we aren't going to get it so we just reset this. self.ignore_char = false; if (action == .press or action == .repeat) { const trigger: input.Binding.Trigger = .{ .mods = mods, .key = key, }; //log.warn("BINDING TRIGGER={}", .{trigger}); if (self.config.keybind.set.get(trigger)) |binding_action| { //log.warn("BINDING ACTION={}", .{binding_action}); switch (binding_action) { .unbind => unreachable, .ignore => {}, .reload_config => { _ = self.app_mailbox.push(.{ .reload_config = {}, }, .{ .instant = {} }); }, .csi => |data| { _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[", }, .{ .forever = {} }); _ = self.io_thread.mailbox.push(.{ .write_stable = data, }, .{ .forever = {} }); try self.io_thread.wakeup.notify(); // CSI triggers a scroll. { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); self.scrollToBottom() catch |err| { log.warn("error scrolling to bottom err={}", .{err}); }; } }, .cursor_key => |ck| { // We send a different sequence depending on if we're // in cursor keys mode. We're in "normal" mode if cursor // keys mdoe is NOT set. const normal = normal: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // With the lock held, we must scroll to the bottom. // We always scroll to the bottom for these inputs. self.scrollToBottom() catch |err| { log.warn("error scrolling to bottom err={}", .{err}); }; break :normal !self.io.terminal.modes.cursor_keys; }; if (normal) { _ = self.io_thread.mailbox.push(.{ .write_stable = ck.normal, }, .{ .forever = {} }); } else { _ = self.io_thread.mailbox.push(.{ .write_stable = ck.application, }, .{ .forever = {} }); } try self.io_thread.wakeup.notify(); }, .copy_to_clipboard => { // We can read from the renderer state without holding // the lock because only we will write to this field. if (self.io.terminal.selection) |sel| { var buf = self.io.terminal.screen.selectionString( self.alloc, sel, self.config.clipboard_trim_trailing_spaces, ) catch |err| { log.err("error reading selection string err={}", .{err}); return; }; defer self.alloc.free(buf); self.rt_surface.setClipboardString(buf) catch |err| { log.err("error setting clipboard string err={}", .{err}); return; }; } }, .paste_from_clipboard => { const data = self.rt_surface.getClipboardString() catch |err| { log.warn("error reading clipboard: {}", .{err}); return; }; if (data.len > 0) { const bracketed = bracketed: { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // With the lock held, we must scroll to the bottom. // We always scroll to the bottom for these inputs. self.scrollToBottom() catch |err| { log.warn("error scrolling to bottom err={}", .{err}); }; break :bracketed self.io.terminal.modes.bracketed_paste; }; if (bracketed) { _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[200~", }, .{ .forever = {} }); } _ = self.io_thread.mailbox.push(try termio.Message.writeReq( self.alloc, data, ), .{ .forever = {} }); if (bracketed) { _ = self.io_thread.mailbox.push(.{ .write_stable = "\x1B[201~", }, .{ .forever = {} }); } try self.io_thread.wakeup.notify(); } }, .increase_font_size => |delta| { log.debug("increase font size={}", .{delta}); var size = self.font_size; size.points +|= delta; self.setFontSize(size); }, .decrease_font_size => |delta| { log.debug("decrease font size={}", .{delta}); var size = self.font_size; size.points = @max(1, size.points -| delta); self.setFontSize(size); }, .reset_font_size => { log.debug("reset font size", .{}); var size = self.font_size; size.points = self.config.original_font_size; self.setFontSize(size); }, .clear_screen => { _ = self.io_thread.mailbox.push(.{ .clear_screen = .{ .history = true }, }, .{ .forever = {} }); try self.io_thread.wakeup.notify(); }, .toggle_dev_mode => if (DevMode.enabled) { DevMode.instance.visible = !DevMode.instance.visible; try self.queueRender(); } else log.warn("dev mode was not compiled into this binary", .{}), .new_window => { _ = self.app_mailbox.push(.{ .new_window = .{ .parent = self, }, }, .{ .instant = {} }); }, .new_tab => { if (@hasDecl(apprt.Surface, "newTab")) { try self.rt_surface.newTab(); } else log.warn("runtime doesn't implement newTab", .{}); }, .previous_tab => { if (@hasDecl(apprt.Surface, "gotoPreviousTab")) { self.rt_surface.gotoPreviousTab(); } else log.warn("runtime doesn't implement gotoPreviousTab", .{}); }, .next_tab => { if (@hasDecl(apprt.Surface, "gotoNextTab")) { self.rt_surface.gotoNextTab(); } else log.warn("runtime doesn't implement gotoNextTab", .{}); }, .goto_tab => |n| { if (@hasDecl(apprt.Surface, "gotoTab")) { self.rt_surface.gotoTab(n); } else log.warn("runtime doesn't implement gotoTab", .{}); }, .new_split => |direction| { if (@hasDecl(apprt.Surface, "newSplit")) { try self.rt_surface.newSplit(direction); } else log.warn("runtime doesn't implement newSplit", .{}); }, .goto_split => |direction| { if (@hasDecl(apprt.Surface, "gotoSplit")) { self.rt_surface.gotoSplit(direction); } else log.warn("runtime doesn't implement gotoSplit", .{}); }, .close_surface => self.close(), .close_window => { _ = self.app_mailbox.push(.{ .close = self }, .{ .instant = {} }); }, .quit => { _ = self.app_mailbox.push(.{ .quit = {}, }, .{ .instant = {} }); }, } // Bindings always result in us ignoring the char if printable self.ignore_char = true; // No matter what, if there is a binding then we are done. return; } // Handle non-printables const char: u8 = char: { const mods_int = @bitCast(u8, mods); const ctrl_only = @bitCast(u8, input.Mods{ .ctrl = true }); // If we're only pressing control, check if this is a character // we convert to a non-printable. if (mods_int == ctrl_only) { const val: u8 = switch (key) { .a => 0x01, .b => 0x02, .c => 0x03, .d => 0x04, .e => 0x05, .f => 0x06, .g => 0x07, .h => 0x08, .i => 0x09, .j => 0x0A, .k => 0x0B, .l => 0x0C, .m => 0x0D, .n => 0x0E, .o => 0x0F, .p => 0x10, .q => 0x11, .r => 0x12, .s => 0x13, .t => 0x14, .u => 0x15, .v => 0x16, .w => 0x17, .x => 0x18, .y => 0x19, .z => 0x1A, else => 0, }; if (val > 0) break :char val; } // Otherwise, we don't care what modifiers we press we do this. break :char @as(u8, switch (key) { .backspace => 0x7F, .enter => '\r', .tab => '\t', .escape => 0x1B, else => 0, }); }; if (char > 0) { // Ask our IO thread to write the data var data: termio.Message.WriteReq.Small.Array = undefined; data[0] = @intCast(u8, char); _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = 1, }, }, .{ .forever = {} }); // After sending all our messages we have to notify our IO thread try self.io_thread.wakeup.notify(); // Control charactesr trigger a scroll { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); self.scrollToBottom() catch |err| { log.warn("error scrolling to bottom err={}", .{err}); }; } } } } pub fn focusCallback(self: *Surface, focused: bool) !void { // Notify our render thread of the new state _ = self.renderer_thread.mailbox.push(.{ .focus = focused, }, .{ .forever = {} }); // Schedule render which also drains our mailbox try self.queueRender(); // Notify the app about focus in/out if it is requesting it { self.renderer_state.mutex.lock(); const focus_event = self.io.terminal.modes.focus_event; self.renderer_state.mutex.unlock(); if (focus_event) { const seq = if (focused) "\x1b[I" else "\x1b[O"; _ = self.io_thread.mailbox.push(.{ .write_stable = seq, }, .{ .forever = {} }); try self.io_thread.wakeup.notify(); } } } pub fn refreshCallback(self: *Surface) !void { // The point of this callback is to schedule a render, so do that. try self.queueRender(); } pub fn scrollCallback(self: *Surface, xoff: f64, yoff: f64) !void { const tracy = trace(@src()); defer tracy.end(); // If our dev mode surface is visible then we always schedule a render on // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureMouse) return; } else |_| {} } // log.info("SCROLL: {} {}", .{ xoff, yoff }); // Positive is up const y_sign: isize = if (yoff > 0) -1 else 1; const y_delta_unsigned: usize = if (yoff == 0) 0 else @max(@divFloor(self.grid_size.rows, 15), 1); const y_delta: isize = y_sign * @intCast(isize, y_delta_unsigned); // Positive is right const x_sign: isize = if (xoff < 0) -1 else 1; const x_delta_unsigned: usize = if (xoff == 0) 0 else 1; const x_delta: isize = x_sign * @intCast(isize, x_delta_unsigned); log.info("scroll: delta_y={} delta_x={}", .{ y_delta, x_delta }); { self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // If we're in alternate screen with alternate scroll enabled, then // we convert to cursor keys. This only happens if we're: // (1) alt screen (2) no explicit mouse reporting and (3) alt // scroll mode enabled. if (self.io.terminal.active_screen == .alternate and self.io.terminal.modes.mouse_event == .none and self.io.terminal.modes.mouse_alternate_scroll) { if (y_delta_unsigned > 0) { const seq = if (y_delta < 0) "\x1bOA" else "\x1bOB"; for (0..y_delta_unsigned) |_| { _ = self.io_thread.mailbox.push(.{ .write_stable = seq, }, .{ .forever = {} }); } } if (x_delta_unsigned > 0) { const seq = if (x_delta < 0) "\x1bOC" else "\x1bOD"; for (0..x_delta_unsigned) |_| { _ = self.io_thread.mailbox.push(.{ .write_stable = seq, }, .{ .forever = {} }); } } // After sending all our messages we have to notify our IO thread try self.io_thread.wakeup.notify(); return; } // We have mouse events, are not in an alternate scroll buffer, // or have alternate scroll disabled. In this case, we just run // the normal logic. // Modify our viewport, this requires a lock since it affects rendering try self.io.terminal.scrollViewport(.{ .delta = y_delta }); // If we're scrolling up or down, then send a mouse event. This requires // a lock since we read terminal state. if (yoff != 0) { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(if (yoff < 0) .five else .four, .press, self.mouse.mods, pos); } if (xoff != 0) { const pos = try self.rt_surface.getCursorPos(); try self.mouseReport(if (xoff > 0) .six else .seven, .press, self.mouse.mods, pos); } } try self.queueRender(); } /// The type of action to report for a mouse event. const MouseReportAction = enum { press, release, motion }; fn mouseReport( self: *Surface, button: ?input.MouseButton, action: MouseReportAction, mods: input.Mods, pos: apprt.CursorPos, ) !void { // TODO: posToViewport currently clamps to the surface boundary, // do we want to not report mouse events at all outside the surface? // Depending on the event, we may do nothing at all. switch (self.io.terminal.modes.mouse_event) { .none => return, // X10 only reports clicks with mouse button 1, 2, 3. We verify // the button later. .x10 => if (action != .press or button == null or !(button.? == .left or button.? == .right or button.? == .middle)) return, // Doesn't report motion .normal => if (action == .motion) return, // Button must be pressed .button => if (button == null) return, // Everything .any => {}, } // This format reports X/Y const viewport_point = self.posToViewport(pos.x, pos.y); // Record our new point self.mouse.event_point = viewport_point; // Get the code we'll actually write const button_code: u8 = code: { var acc: u8 = 0; // Determine our initial button value if (button == null) { // Null button means motion without a button pressed acc = 3; } else if (action == .release and self.io.terminal.modes.mouse_format != .sgr) { // Release is 3. It is NOT 3 in SGR mode because SGR can tell // the application what button was released. acc = 3; } else { acc = switch (button.?) { .left => 0, .right => 1, .middle => 2, .four => 64, .five => 65, else => return, // unsupported }; } // X10 doesn't have modifiers if (self.io.terminal.modes.mouse_event != .x10) { if (mods.shift) acc += 4; if (mods.super) acc += 8; if (mods.ctrl) acc += 16; } // Motion adds another bit if (action == .motion) acc += 32; break :code acc; }; switch (self.io.terminal.modes.mouse_format) { .x10 => { if (viewport_point.x > 222 or viewport_point.y > 222) { log.info("X10 mouse format can only encode X/Y up to 223", .{}); return; } // + 1 below is because our x/y is 0-indexed and the protocol wants 1 var data: termio.Message.WriteReq.Small.Array = undefined; assert(data.len >= 6); data[0] = '\x1b'; data[1] = '['; data[2] = 'M'; data[3] = 32 + button_code; data[4] = 32 + @intCast(u8, viewport_point.x) + 1; data[5] = 32 + @intCast(u8, viewport_point.y) + 1; // Ask our IO thread to write the data _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = 6, }, }, .{ .forever = {} }); }, .utf8 => { // Maximum of 12 because at most we have 2 fully UTF-8 encoded chars var data: termio.Message.WriteReq.Small.Array = undefined; assert(data.len >= 12); data[0] = '\x1b'; data[1] = '['; data[2] = 'M'; // The button code will always fit in a single u8 data[3] = 32 + button_code; // UTF-8 encode the x/y var i: usize = 4; i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.x + 1), data[i..]); i += try std.unicode.utf8Encode(@intCast(u21, 32 + viewport_point.y + 1), data[i..]); // Ask our IO thread to write the data _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = @intCast(u8, i), }, }, .{ .forever = {} }); }, .sgr => { // Final character to send in the CSI const final: u8 = if (action == .release) 'm' else 'M'; // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, viewport_point.x + 1, viewport_point.y + 1, final, }); // Ask our IO thread to write the data _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = @intCast(u8, resp.len), }, }, .{ .forever = {} }); }, .urxvt => { // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[{d};{d};{d}M", .{ 32 + button_code, viewport_point.x + 1, viewport_point.y + 1, }); // Ask our IO thread to write the data _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = @intCast(u8, resp.len), }, }, .{ .forever = {} }); }, .sgr_pixels => { // Final character to send in the CSI const final: u8 = if (action == .release) 'm' else 'M'; // Response always is at least 4 chars, so this leaves the // remainder for numbers which are very large... var data: termio.Message.WriteReq.Small.Array = undefined; const resp = try std.fmt.bufPrint(&data, "\x1B[<{d};{d};{d}{c}", .{ button_code, pos.x, pos.y, final, }); // Ask our IO thread to write the data _ = self.io_thread.mailbox.push(.{ .write_small = .{ .data = data, .len = @intCast(u8, resp.len), }, }, .{ .forever = {} }); }, } // After sending all our messages we have to notify our IO thread try self.io_thread.wakeup.notify(); } pub fn mouseButtonCallback( self: *Surface, action: input.MouseButtonState, button: input.MouseButton, mods: input.Mods, ) !void { const tracy = trace(@src()); defer tracy.end(); // If our dev mode surface is visible then we always schedule a render on // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureMouse) return; } else |_| {} } // Always record our latest mouse state self.mouse.click_state[@intCast(usize, @enumToInt(button))] = action; self.mouse.mods = @bitCast(input.Mods, mods); self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // Report mouse events if enabled if (self.io.terminal.modes.mouse_event != .none) { const pos = try self.rt_surface.getCursorPos(); const report_action: MouseReportAction = switch (action) { .press => .press, .release => .release, }; try self.mouseReport( button, report_action, self.mouse.mods, pos, ); // If we're doing mouse reporting, we do not support any other // selection or highlighting. return; } // For left button clicks we always record some information for // selection/highlighting purposes. if (button == .left and action == .press) { const pos = try self.rt_surface.getCursorPos(); // If we move our cursor too much between clicks then we reset // the multi-click state. if (self.mouse.left_click_count > 0) { const max_distance = self.cell_size.width; const distance = @sqrt( std.math.pow(f64, pos.x - self.mouse.left_click_xpos, 2) + std.math.pow(f64, pos.y - self.mouse.left_click_ypos, 2), ); if (distance > max_distance) self.mouse.left_click_count = 0; } // Store it const point = self.posToViewport(pos.x, pos.y); self.mouse.left_click_point = point.toScreen(&self.io.terminal.screen); self.mouse.left_click_xpos = pos.x; self.mouse.left_click_ypos = pos.y; // Setup our click counter and timer if (std.time.Instant.now()) |now| { // If we have mouse clicks, then we check if the time elapsed // is less than and our interval and if so, increase the count. if (self.mouse.left_click_count > 0) { const since = now.since(self.mouse.left_click_time); if (since > self.config.mouse_interval) { self.mouse.left_click_count = 0; } } self.mouse.left_click_time = now; self.mouse.left_click_count += 1; // We only support up to triple-clicks. if (self.mouse.left_click_count > 3) self.mouse.left_click_count = 1; } else |err| { self.mouse.left_click_count = 1; log.err("error reading time, mouse multi-click won't work err={}", .{err}); } switch (self.mouse.left_click_count) { // First mouse click, clear selection 1 => if (self.io.terminal.selection != null) { self.io.terminal.selection = null; try self.queueRender(); }, // Double click, select the word under our mouse 2 => { const sel_ = self.io.terminal.screen.selectWord(self.mouse.left_click_point); if (sel_) |sel| { self.io.terminal.selection = sel; try self.queueRender(); } }, // Triple click, select the line under our mouse 3 => { const sel_ = self.io.terminal.screen.selectLine(self.mouse.left_click_point); if (sel_) |sel| { self.io.terminal.selection = sel; try self.queueRender(); } }, // We should be bounded by 1 to 3 else => unreachable, } } } pub fn cursorPosCallback( self: *Surface, pos: apprt.CursorPos, ) !void { const tracy = trace(@src()); defer tracy.end(); // If our dev mode surface is visible then we always schedule a render on // cursor move because the cursor might touch our surfaces. if (DevMode.enabled and DevMode.instance.visible) { try self.queueRender(); // If the mouse event was handled by imgui, ignore it. if (imgui.IO.get()) |io| { if (io.cval().WantCaptureMouse) return; } else |_| {} } // We are reading/writing state for the remainder self.renderer_state.mutex.lock(); defer self.renderer_state.mutex.unlock(); // Do a mouse report if (self.io.terminal.modes.mouse_event != .none) { // We use the first mouse button we find pressed in order to report // since the spec (afaict) does not say... const button: ?input.MouseButton = button: for (self.mouse.click_state, 0..) |state, i| { if (state == .press) break :button @intToEnum(input.MouseButton, i); } else null; try self.mouseReport(button, .motion, self.mouse.mods, pos); // If we're doing mouse motion tracking, we do not support text // selection. return; } // If the cursor isn't clicked currently, it doesn't matter if (self.mouse.click_state[@enumToInt(input.MouseButton.left)] != .press) return; // All roads lead to requiring a re-render at this pont. try self.queueRender(); // Convert to pixels from screen coords const xpos = pos.x; const ypos = pos.y; // Convert to points const viewport_point = self.posToViewport(xpos, ypos); const screen_point = viewport_point.toScreen(&self.io.terminal.screen); // Handle dragging depending on click count switch (self.mouse.left_click_count) { 1 => self.dragLeftClickSingle(screen_point, xpos), 2 => self.dragLeftClickDouble(screen_point), 3 => self.dragLeftClickTriple(screen_point), else => unreachable, } } /// Double-click dragging moves the selection one "word" at a time. fn dragLeftClickDouble( self: *Surface, screen_point: terminal.point.ScreenPoint, ) void { // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectWord(screen_point) orelse return; // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectWord(self.mouse.left_click_point) orelse { self.io.terminal.selection = word; return; }; // Grow our selection if (screen_point.before(self.mouse.left_click_point)) { sel.start = word.start; } else { sel.end = word.end; } self.io.terminal.selection = sel; } /// Triple-click dragging moves the selection one "line" at a time. fn dragLeftClickTriple( self: *Surface, screen_point: terminal.point.ScreenPoint, ) void { // Get the word under our current point. If there isn't a word, do nothing. const word = self.io.terminal.screen.selectLine(screen_point) orelse return; // Get our selection to grow it. If we don't have a selection, start it now. // We may not have a selection if we started our dbl-click in an area // that had no data, then we dragged our mouse into an area with data. var sel = self.io.terminal.screen.selectLine(self.mouse.left_click_point) orelse { self.io.terminal.selection = word; return; }; // Grow our selection if (screen_point.before(self.mouse.left_click_point)) { sel.start = word.start; } else { sel.end = word.end; } self.io.terminal.selection = sel; } fn dragLeftClickSingle( self: *Surface, screen_point: terminal.point.ScreenPoint, xpos: f64, ) void { // NOTE(mitchellh): This logic super sucks. There has to be an easier way // to calculate this, but this is good for a v1. Selection isn't THAT // common so its not like this performance heavy code is running that // often. // TODO: unit test this, this logic sucks // If we were selecting, and we switched directions, then we restart // calculations because it forces us to reconsider if the first cell is // selected. if (self.io.terminal.selection) |sel| { const reset: bool = if (sel.end.before(sel.start)) sel.start.before(screen_point) else screen_point.before(sel.start); if (reset) self.io.terminal.selection = null; } // Our logic for determing if the starting cell is selected: // // - The "xboundary" is 60% the width of a cell from the left. We choose // 60% somewhat arbitrarily based on feeling. // - If we started our click left of xboundary, backwards selections // can NEVER select the current char. // - If we started our click right of xboundary, backwards selections // ALWAYS selected the current char, but we must move the cursor // left of the xboundary. // - Inverted logic for forwards selections. // // the boundary point at which we consider selection or non-selection const cell_xboundary = self.cell_size.width * 0.6; // first xpos of the clicked cell const cell_xstart = @intToFloat(f32, self.mouse.left_click_point.x) * self.cell_size.width; const cell_start_xpos = self.mouse.left_click_xpos - cell_xstart; // If this is the same cell, then we only start the selection if weve // moved past the boundary point the opposite direction from where we // started. if (std.meta.eql(screen_point, self.mouse.left_click_point)) { const cell_xpos = xpos - cell_xstart; const selected: bool = if (cell_start_xpos < cell_xboundary) cell_xpos >= cell_xboundary else cell_xpos < cell_xboundary; self.io.terminal.selection = if (selected) .{ .start = screen_point, .end = screen_point, } else null; return; } // If this is a different cell and we haven't started selection, // we determine the starting cell first. if (self.io.terminal.selection == null) { // - If we're moving to a point before the start, then we select // the starting cell if we started after the boundary, else // we start selection of the prior cell. // - Inverse logic for a point after the start. const click_point = self.mouse.left_click_point; const start: terminal.point.ScreenPoint = if (screen_point.before(click_point)) start: { if (self.mouse.left_click_xpos > cell_xboundary) { break :start click_point; } else { break :start if (click_point.x > 0) terminal.point.ScreenPoint{ .y = click_point.y, .x = click_point.x - 1, } else terminal.point.ScreenPoint{ .x = self.io.terminal.screen.cols - 1, .y = click_point.y -| 1, }; } } else start: { if (self.mouse.left_click_xpos < cell_xboundary) { break :start click_point; } else { break :start if (click_point.x < self.io.terminal.screen.cols - 1) terminal.point.ScreenPoint{ .y = click_point.y, .x = click_point.x + 1, } else terminal.point.ScreenPoint{ .y = click_point.y + 1, .x = 0, }; } }; self.io.terminal.selection = .{ .start = start, .end = screen_point }; return; } // TODO: detect if selection point is passed the point where we've // actually written data before and disallow it. // We moved! Set the selection end point. The start point should be // set earlier. assert(self.io.terminal.selection != null); self.io.terminal.selection.?.end = screen_point; } fn posToViewport(self: Surface, xpos: f64, ypos: f64) terminal.point.Viewport { // xpos and ypos can be negative if while dragging, the user moves the // mouse off the surface. Likewise, they can be larger than our surface // width if the user drags out of the surface positively. return .{ .x = if (xpos < 0) 0 else x: { // Our cell is the mouse divided by cell width const cell_width = @floatCast(f64, self.cell_size.width); const x = @floatToInt(usize, xpos / cell_width); // Can be off the screen if the user drags it out, so max // it out on our available columns break :x @min(x, self.grid_size.columns - 1); }, .y = if (ypos < 0) 0 else y: { const cell_height = @floatCast(f64, self.cell_size.height); const y = @floatToInt(usize, ypos / cell_height); break :y @min(y, self.grid_size.rows - 1); }, }; } /// Scroll to the bottom of the viewport. /// /// Precondition: the render_state mutex must be held. fn scrollToBottom(self: *Surface) !void { try self.io.terminal.scrollViewport(.{ .bottom = {} }); try self.queueRender(); } const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf");