diff --git a/build.zig.zon b/build.zig.zon index 444ec791..8aff3dfa 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -9,8 +9,8 @@ .hash = "zigimg-0.1.0-8_eo2nWlEgCddu8EGLOM_RkYshx3sC8tWv-yYA4-htS6", }, .zg = .{ - .url = "git+https://codeberg.org/atman/zg#0b05141b033043c5f7bcd72048a48eef6531ea6c", - .hash = "zg-0.14.0-oGqU3KEFswIffnDu8eAE2XlhzwcfgjwtM6akIc5L7cEV", + .url = "https://codeberg.org/atman/zg/archive/v0.14.1.tar.gz", + .hash = "zg-0.14.1-oGqU3IQ_tALZIiBN026_NTaPJqU-Upm8P_C7QED2Rzm8", }, }, .paths = .{ diff --git a/src/InternalScreen.zig b/src/InternalScreen.zig index 8db2bdb4..fce4b4c8 100644 --- a/src/InternalScreen.zig +++ b/src/InternalScreen.zig @@ -36,11 +36,11 @@ pub const InternalCell = struct { } }; -arena: *std.heap.ArenaAllocator = undefined, +arena: *std.heap.ArenaAllocator, width: u16 = 0, height: u16 = 0, -buf: []InternalCell = undefined, +buf: []InternalCell, cursor_row: u16 = 0, cursor_col: u16 = 0, diff --git a/src/Loop.zig b/src/Loop.zig index cf8e578a..99228395 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -24,6 +24,7 @@ pub fn Loop(comptime T: type) type { queue: Queue(T, 512) = .{}, thread: ?std.Thread = null, should_quit: bool = false, + winch_eventfd: std.posix.fd_t = -1, /// Initialize the event loop. This is an intrusive init so that we have /// a stable pointer to register signal callbacks with posix TTYs @@ -32,6 +33,9 @@ pub fn Loop(comptime T: type) type { .windows => {}, else => { if (!builtin.is_test) { + if (self.winch_eventfd < 0) { + self.winch_eventfd = try std.posix.eventfd(0, 0); + } const handler: Tty.SignalHandler = .{ .context = self, .callback = Self.winsizeCallback, @@ -98,10 +102,9 @@ pub fn Loop(comptime T: type) type { // We will be receiving winsize updates in-band if (self.vaxis.state.in_band_resize) return; - const winsize = Tty.getWinsize(self.tty.fd) catch return; - if (@hasField(Event, "winsize")) { - self.postEvent(.{ .winsize = winsize }); - } + if (self.winch_eventfd < 0) return; + // notify the event loop that a winsize signal was received + _ = std.posix.write(self.winch_eventfd, &[8]u8{ 0, 0, 0, 0, 0, 0, 0, 1 }) catch {}; } /// read input from the tty. This is run in a separate thread @@ -124,21 +127,53 @@ pub fn Loop(comptime T: type) type { } }, else => { - // get our initial winsize - const winsize = try Tty.getWinsize(self.tty.fd); - if (@hasField(Event, "winsize")) { - self.postEvent(.{ .winsize = winsize }); + { + // get our initial winsize + const winsize = try Tty.getWinsize(self.tty.fd); + if (@hasField(Event, "winsize")) { + self.postEvent(.{ .winsize = winsize }); + } } var parser: Parser = .{ .grapheme_data = grapheme_data, }; + // initialize poll fds + var fds: [2]std.posix.pollfd = .{ + .{ + .fd = self.tty.fd, + .events = std.posix.POLL.IN, + .revents = 0, + }, + .{ + .fd = self.winch_eventfd, + .events = std.posix.POLL.IN, + .revents = 0, + }, + }; // initialize the read buffer var buf: [1024]u8 = undefined; var read_start: usize = 0; // read loop read_loop: while (!self.should_quit) { + if (@hasField(Event, "winsize")) { + // self.init might be called after start, so we need to check + fds[1].fd = self.winch_eventfd; + _ = try std.posix.poll(&fds, -1); + if (fds[1].revents & std.posix.POLL.IN != 0) { + fds[1].revents = 0; + var tmp: [8]u8 = undefined; + _ = try std.posix.read(self.winch_eventfd, &tmp); + + const winsize = try Tty.getWinsize(self.tty.fd); + self.postEvent(.{ .winsize = winsize }); + } + + if (fds[0].revents & std.posix.POLL.IN == 0) continue :read_loop; + fds[0].revents = 0; + } + const n = try self.tty.read(buf[read_start..]); var seq_start: usize = 0; while (seq_start < n) { @@ -289,6 +324,11 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); } }, + .mouse_leave => { + if (@hasField(Event, "mouse_leave")) { + return self.postEvent(.mouse_leave); + } + }, .focus_in => { if (@hasField(Event, "focus_in")) { return self.postEvent(.focus_in); diff --git a/src/Parser.zig b/src/Parser.zig index ebada128..b97538d3 100644 --- a/src/Parser.zig +++ b/src/Parser.zig @@ -25,6 +25,7 @@ const mouse_bits = struct { const shift: u8 = 0b00000100; const alt: u8 = 0b00001000; const ctrl: u8 = 0b00010000; + const leave: u16 = 0b100000000; }; // the state of the parser @@ -116,7 +117,7 @@ inline fn parseGround(input: []const u8, data: *const Graphemes) !Result { // Check if we have a multi-codepoint grapheme var code = cp.code; - var g_state: Graphemes.State = .{}; + var g_state: Graphemes.IterState = .{}; var prev_cp = code; while (iter.next()) |next_cp| { if (Graphemes.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) { @@ -679,6 +680,9 @@ inline fn parseMouse(input: []const u8, full_input: []const u8) Result { return null_event; } + if (button_mask & mouse_bits.leave > 0) + return .{ .event = .mouse_leave, .n = if (xterm) 6 else input.len }; + const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons); const motion = button_mask & mouse_bits.motion > 0; const shift = button_mask & mouse_bits.shift > 0; diff --git a/src/Screen.zig b/src/Screen.zig index d1b50b44..1b7370ea 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -5,7 +5,6 @@ const Cell = @import("Cell.zig"); const Shape = @import("Mouse.zig").Shape; const Image = @import("Image.zig"); const Winsize = @import("main.zig").Winsize; -const Unicode = @import("Unicode.zig"); const Method = @import("gwidth.zig").Method; const Screen = @This(); @@ -22,14 +21,12 @@ cursor_row: u16 = 0, cursor_col: u16 = 0, cursor_vis: bool = false, -unicode: *const Unicode = undefined, - width_method: Method = .wcwidth, mouse_shape: Shape = .default, cursor_shape: Cell.CursorShape = .default, -pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) std.mem.Allocator.Error!Screen { +pub fn init(alloc: std.mem.Allocator, winsize: Winsize) std.mem.Allocator.Error!Screen { const w = winsize.cols; const h = winsize.rows; const self = Screen{ @@ -38,7 +35,6 @@ pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) .height = h, .width_pix = winsize.x_pixel, .height_pix = winsize.y_pixel, - .unicode = unicode, }; const base_cell: Cell = .{}; @memset(self.buf, base_cell); diff --git a/src/Vaxis.zig b/src/Vaxis.zig index 13646e27..058bf8d1 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -80,6 +80,10 @@ sgr: enum { legacy, } = .standard, +/// Enable workarounds for escape sequence handling issues/bugs in terminals +/// So far this just enables a UL escape sequence workaround for conpty +enable_workarounds: bool = true, + state: struct { /// if we are in the alt screen alt_screen: bool = false, @@ -104,7 +108,7 @@ pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { return .{ .opts = opts, .screen = .{}, - .screen_last = try .init(alloc, 80, 24), + .screen_last = try .init(alloc, 0, 0), .unicode = try Unicode.init(alloc), }; } @@ -189,7 +193,7 @@ pub fn resize( ) !void { log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); self.screen.deinit(alloc); - self.screen = try Screen.init(alloc, winsize, &self.unicode); + self.screen = try Screen.init(alloc, winsize); self.screen.width_method = self.caps.unicode; // try self.screen.int(alloc, winsize.cols, winsize.rows); // we only init our current screen. This has the effect of redrawing @@ -217,6 +221,7 @@ pub fn window(self: *Vaxis) Window { .width = self.screen.width, .height = self.screen.height, .screen = &self.screen, + .unicode = &self.unicode, }; } @@ -574,7 +579,9 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void { } }, .rgb => |rgb| { - switch (self.sgr) { + if (self.enable_workarounds) + try tty.print(ctlseqs.ul_rgb_conpty, .{ rgb[0], rgb[1], rgb[2] }) + else switch (self.sgr) { .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), } @@ -1237,9 +1244,13 @@ pub fn prettyPrint(self: *Vaxis, tty: AnyWriter) !void { } }, .rgb => |rgb| { - switch (self.sgr) { + if (self.enable_workarounds) + try tty.print(ctlseqs.ul_rgb_conpty, .{ rgb[0], rgb[1], rgb[2] }) + else switch (self.sgr) { .standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), - .legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), + .legacy => { + try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }); + }, } }, } diff --git a/src/Window.zig b/src/Window.zig index 6f0f2ae2..016b2bec 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -25,6 +25,7 @@ width: u16, height: u16, screen: *Screen, +unicode: *const Unicode, /// Creates a new window with offset relative to parent and size clamped to the /// parent's size. Windows do not retain a reference to their parent and are @@ -49,6 +50,7 @@ fn initChild( .width = @min(width, max_width), .height = @min(height, max_height), .screen = self.screen, + .unicode = self.unicode, }; } @@ -205,7 +207,7 @@ pub fn clear(self: Window) void { /// returns the width of the grapheme. This depends on the terminal capabilities pub fn gwidth(self: Window, str: []const u8) u16 { - return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data); + return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data); } /// fills the window with the provided cell @@ -293,7 +295,7 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) PrintR .grapheme => { var col: u16 = opts.col_offset; const overflow: bool = blk: for (segments) |segment| { - var iter = self.screen.unicode.graphemeIterator(segment.text); + var iter = self.unicode.graphemeIterator(segment.text); while (iter.next()) |grapheme| { if (col >= self.width) { row += 1; @@ -376,7 +378,7 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) PrintR col = 0; } - var grapheme_iterator = self.screen.unicode.graphemeIterator(word); + var grapheme_iterator = self.unicode.graphemeIterator(word); while (grapheme_iterator.next()) |grapheme| { soft_wrapped = false; if (row >= self.height) { @@ -415,7 +417,7 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) PrintR .none => { var col: u16 = opts.col_offset; const overflow: bool = blk: for (segments) |segment| { - var iter = self.screen.unicode.graphemeIterator(segment.text); + var iter = self.unicode.graphemeIterator(segment.text); while (iter.next()) |grapheme| { if (col >= self.width) break :blk true; const s = grapheme.bytes(segment.text); @@ -487,6 +489,7 @@ test "Window size set" { .width = 20, .height = 20, .screen = undefined, + .unicode = undefined, }; const ch = parent.initChild(1, 1, null, null); @@ -503,6 +506,7 @@ test "Window size set too big" { .width = 20, .height = 20, .screen = undefined, + .unicode = undefined, }; const ch = parent.initChild(0, 0, 21, 21); @@ -519,6 +523,7 @@ test "Window size set too big with offset" { .width = 20, .height = 20, .screen = undefined, + .unicode = undefined, }; const ch = parent.initChild(10, 10, 21, 21); @@ -535,6 +540,7 @@ test "Window size nested offsets" { .width = 20, .height = 20, .screen = undefined, + .unicode = undefined, }; const ch = parent.initChild(10, 10, 21, 21); @@ -551,6 +557,7 @@ test "Window offsets" { .width = 20, .height = 20, .screen = undefined, + .unicode = undefined, }; const ch = parent.initChild(10, 10, 21, 21); @@ -565,7 +572,7 @@ test "print: grapheme" { const alloc = std.testing.allocator_instance.allocator(); const unicode = try Unicode.init(alloc); defer unicode.deinit(alloc); - var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode }; + var screen: Screen = .{ .width_method = .unicode }; const win: Window = .{ .x_off = 0, .y_off = 0, @@ -574,6 +581,7 @@ test "print: grapheme" { .width = 4, .height = 2, .screen = &screen, + .unicode = &unicode, }; const opts: PrintOptions = .{ .commit = false, @@ -633,7 +641,6 @@ test "print: word" { defer unicode.deinit(alloc); var screen: Screen = .{ .width_method = .unicode, - .unicode = &unicode, }; const win: Window = .{ .x_off = 0, @@ -643,6 +650,7 @@ test "print: word" { .width = 4, .height = 2, .screen = &screen, + .unicode = &unicode, }; const opts: PrintOptions = .{ .commit = false, diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig index 73a869bf..e6736058 100644 --- a/src/ctlseqs.zig +++ b/src/ctlseqs.zig @@ -95,6 +95,7 @@ pub const ul_indexed_legacy = "\x1b[58;5;{d}m"; pub const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m"; pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m"; pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m"; +pub const ul_rgb_conpty = "\x1b[58:2::{d}:{d}:{d}m"; // Underlines pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported diff --git a/src/event.zig b/src/event.zig index efa62f2a..8f1cf152 100644 --- a/src/event.zig +++ b/src/event.zig @@ -8,6 +8,7 @@ pub const Event = union(enum) { key_press: Key, key_release: Key, mouse: Mouse, + mouse_leave, focus_in, focus_out, paste_start, // bracketed paste start diff --git a/src/vxfw/Border.zig b/src/vxfw/Border.zig index f29684ce..cca5a3ef 100644 --- a/src/vxfw/Border.zig +++ b/src/vxfw/Border.zig @@ -5,10 +5,23 @@ const Allocator = std.mem.Allocator; const vxfw = @import("vxfw.zig"); +pub const BorderLabel = struct { + text: []const u8, + alignment: enum { + top_left, + top_center, + top_right, + bottom_left, + bottom_center, + bottom_right, + }, +}; + const Border = @This(); child: vxfw.Widget, style: vaxis.Style = .{}, +labels: []const BorderLabel = &[_]BorderLabel{}, pub fn widget(self: *const Border) vxfw.Widget { return .{ @@ -65,6 +78,35 @@ pub fn draw(self: *const Border, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Sur surf.writeCell(0, row, .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style }); surf.writeCell(right_edge, row, .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style }); } + + // Add border labels + for (self.labels) |label| { + const text_len: u16 = @intCast(ctx.stringWidth(label.text)); + if (text_len == 0) continue; + + const text_row: u16 = switch (label.alignment) { + .top_left, .top_center, .top_right => 0, + .bottom_left, .bottom_center, .bottom_right => bottom_edge, + }; + + var text_col: u16 = switch (label.alignment) { + .top_left, .bottom_left => 1, + .top_center, .bottom_center => @max((size.width - text_len) / 2, 1), + .top_right, .bottom_right => @max(size.width - 1 - text_len, 1), + }; + + var iter = ctx.graphemeIterator(label.text); + while (iter.next()) |grapheme| { + const text = grapheme.bytes(label.text); + const width: u16 = @intCast(ctx.stringWidth(text)); + surf.writeCell(text_col, text_row, .{ + .char = .{ .grapheme = text, .width = @intCast(width) }, + .style = self.style, + }); + text_col += width; + } + } + return surf; } diff --git a/src/widgets/View.zig b/src/widgets/View.zig index e9a294e7..b051aeeb 100644 --- a/src/widgets/View.zig +++ b/src/widgets/View.zig @@ -18,6 +18,8 @@ alloc: mem.Allocator, /// Underlying Screen screen: Screen, +unicode: *const Unicode, + /// View Initialization Config pub const Config = struct { width: u16, @@ -26,19 +28,15 @@ pub const Config = struct { /// Initialize a new View pub fn init(alloc: mem.Allocator, unicode: *const Unicode, config: Config) mem.Allocator.Error!View { - const screen = try Screen.init( - alloc, - .{ + return .{ + .alloc = alloc, + .screen = try Screen.init(alloc, .{ .cols = config.width, .rows = config.height, .x_pixel = 0, .y_pixel = 0, - }, - unicode, - ); - return .{ - .alloc = alloc, - .screen = screen, + }), + .unicode = unicode, }; } @@ -51,6 +49,7 @@ pub fn window(self: *View) Window { .width = self.screen.width, .height = self.screen.height, .screen = &self.screen, + .unicode = self.unicode, }; } @@ -142,7 +141,7 @@ pub fn clear(self: View) void { /// Returns the width of the grapheme. This depends on the terminal capabilities pub fn gwidth(self: View, str: []const u8) u16 { - return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data); + return gw.gwidth(str, self.screen.width_method, &self.unicode.width_data); } /// Fills the View with the provided cell diff --git a/src/widgets/terminal/Terminal.zig b/src/widgets/terminal/Terminal.zig index dafdfb11..1c4d2841 100644 --- a/src/widgets/terminal/Terminal.zig +++ b/src/widgets/terminal/Terminal.zig @@ -24,8 +24,6 @@ pub const Event = union(enum) { pwd_change: []const u8, }; -const grapheme = @import("grapheme"); - const posix = std.posix; const log = std.log.scoped(.terminal); @@ -291,7 +289,7 @@ fn run(self: *Terminal) !void { switch (event) { .print => |str| { - var iter = grapheme.Iterator.init(str, &self.unicode.width_data.g_data); + var iter = self.unicode.graphemeIterator(str); while (iter.next()) |g| { const gr = g.bytes(str); // TODO: use actual instead of .unicode