diff --git a/examples/text_input.zig b/examples/text_input.zig index f4a756d..168ee1b 100644 --- a/examples/text_input.zig +++ b/examples/text_input.zig @@ -29,24 +29,32 @@ pub fn main() !void { } const alloc = gpa.allocator(); + // Initalize a tty + var tty = try vaxis.Tty.init(); + defer tty.deinit(); + + // Use a buffered writer for better performance. There are a lot of writes + // in the render loop and this can have a significant savings + var buffered_writer = tty.bufferedWriter(); + const writer = buffered_writer.writer().any(); + // Initialize Vaxis var vx = try vaxis.init(alloc, .{}); - // deinit takes an optional allocator. If your program is exiting, you can - // choose to pass a null allocator to save some exit time. - defer vx.deinit(alloc); + defer vx.deinit(tty.anyWriter(), alloc); - // create our event loop var loop: vaxis.Loop(Event) = .{ .vaxis = &vx, + .tty = &tty, }; + try loop.init(); // Start the read loop. This puts the terminal in raw mode and begins // reading user input - try loop.run(); + try loop.start(); defer loop.stop(); // Optionally enter the alternate screen - try vx.enterAltScreen(); + try vx.enterAltScreen(writer); // We'll adjust the color index every keypress for the border var color_idx: u8 = 0; @@ -58,9 +66,11 @@ pub fn main() !void { // Sends queries to terminal to detect certain features. This should // _always_ be called, but is left to the application to decide when - try vx.queryTerminal(); + // try vx.queryTerminal(); - try vx.setMouseMode(true); + try vx.setMouseMode(writer, true); + + try buffered_writer.flush(); // The main event loop. Vaxis provides a thread safe, blocking, buffered // queue which can serve as the primary event queue for an application @@ -81,12 +91,12 @@ pub fn main() !void { } else if (key.matches('l', .{ .ctrl = true })) { vx.queueRefresh(); } else if (key.matches('n', .{ .ctrl = true })) { - try vx.notify("vaxis", "hello from vaxis"); + try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis"); loop.stop(); var child = std.process.Child.init(&.{"nvim"}, alloc); _ = try child.spawnAndWait(); - try loop.run(); - try vx.enterAltScreen(); + try loop.start(); + try vx.enterAltScreen(tty.anyWriter()); vx.queueRefresh(); } else if (key.matches(vaxis.Key.enter, .{})) { text_input.clearAndFree(); @@ -110,7 +120,7 @@ pub fn main() !void { // more than one byte will incur an allocation on the first render // after it is drawn. Thereafter, it will not allocate unless the // screen is resized - .winsize => |ws| try vx.resize(alloc, ws), + .winsize => |ws| try vx.resize(alloc, ws, tty.anyWriter()), else => {}, } @@ -140,6 +150,7 @@ pub fn main() !void { text_input.draw(child); // Render the screen - try vx.render(); + try vx.render(writer); + try buffered_writer.flush(); } } diff --git a/src/Loop.zig b/src/Loop.zig index a676003..85efd22 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -1,28 +1,49 @@ const std = @import("std"); +const builtin = @import("builtin"); +const grapheme = @import("grapheme"); + +const GraphemeCache = @import("GraphemeCache.zig"); +const Parser = @import("Parser.zig"); const Queue = @import("queue.zig").Queue; -const Tty = @import("Tty.zig"); +const tty = @import("tty.zig"); +const Tty = tty.Tty; const Vaxis = @import("Vaxis.zig"); pub fn Loop(comptime T: type) type { return struct { const Self = @This(); + const Event = T; + const log = std.log.scoped(.loop); - queue: Queue(T, 512) = .{}, - - thread: ?std.Thread = null, - + tty: *Tty, vaxis: *Vaxis, + queue: Queue(T, 512) = .{}, + thread: ?std.Thread = null, + should_quit: bool = false, + + /// Initialize the event loop. This is an intrusive init so that we have + /// a stable pointer to register signal callbacks with posix TTYs + pub fn init(self: *Self) !void { + switch (builtin.os.tag) { + .windows => @compileError("windows not supported"), + else => { + const handler: Tty.SignalHandler = .{ + .context = self, + .callback = Self.winsizeCallback, + }; + try Tty.notifyWinsize(handler); + }, + } + } + /// spawns the input thread to read input from the tty - pub fn run(self: *Self) !void { + pub fn start(self: *Self) !void { if (self.thread) |_| return; - if (self.vaxis.tty == null) self.vaxis.tty = try Tty.init(); - self.thread = try std.Thread.spawn(.{}, Tty.run, .{ - &self.vaxis.tty.?, - T, + self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{ self, &self.vaxis.unicode.grapheme_data, self.vaxis.opts.system_clipboard_allocator, @@ -31,16 +52,13 @@ pub fn Loop(comptime T: type) type { /// stops reading from the tty and returns it to it's initial state pub fn stop(self: *Self) void { - if (self.vaxis.tty) |*tty| { - // stop the read loop, then join the thread - tty.stop(); - if (self.thread) |thread| { - thread.join(); - self.thread = null; - } - // once thread is closed we can deinit the tty - tty.deinit(); - self.vaxis.tty = null; + self.should_quit = true; + // trigger a read + self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; + + if (self.thread) |thread| { + thread.join(); + self.thread = null; } } @@ -69,5 +87,155 @@ pub fn Loop(comptime T: type) type { pub fn tryPostEvent(self: *Self, event: T) bool { return self.queue.tryPush(event); } + + pub fn winsizeCallback(ptr: *anyopaque) void { + const self: *Self = @ptrCast(@alignCast(ptr)); + + const winsize = Tty.getWinsize(self.tty.fd) catch return; + if (@hasField(Event, "winsize")) { + self.postEvent(.{ .winsize = winsize }); + } + } + + /// read input from the tty. This is run in a separate thread + fn ttyRun( + self: *Self, + grapheme_data: *const grapheme.GraphemeData, + paste_allocator: ?std.mem.Allocator, + ) !void { + // get our initial winsize + const winsize = try Tty.getWinsize(self.tty.fd); + if (@hasField(Event, "winsize")) { + self.postEvent(.{ .winsize = winsize }); + } + + // initialize a grapheme cache + var cache: GraphemeCache = .{}; + + var parser: Parser = .{ + .grapheme_data = grapheme_data, + }; + + // initialize the read buffer + var buf: [1024]u8 = undefined; + var read_start: usize = 0; + // read loop + while (!self.should_quit) { + const n = try self.tty.read(buf[read_start..]); + var seq_start: usize = 0; + while (seq_start < n) { + const result = try parser.parse(buf[seq_start..n], paste_allocator); + if (result.n == 0) { + // copy the read to the beginning. We don't use memcpy because + // this could be overlapping, and it's also rare + const initial_start = seq_start; + while (seq_start < n) : (seq_start += 1) { + buf[seq_start - initial_start] = buf[seq_start]; + } + read_start = seq_start - initial_start + 1; + continue; + } + read_start = 0; + seq_start += result.n; + + const event = result.event orelse continue; + switch (event) { + .key_press => |key| { + if (@hasField(Event, "key_press")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_press = mut_key }); + } + }, + .key_release => |*key| { + if (@hasField(Event, "key_release")) { + // HACK: yuck. there has to be a better way + var mut_key = key; + if (key.text) |text| { + mut_key.text = cache.put(text); + } + self.postEvent(.{ .key_release = mut_key }); + } + }, + .mouse => |mouse| { + if (@hasField(Event, "mouse")) { + self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); + } + }, + .focus_in => { + if (@hasField(Event, "focus_in")) { + self.postEvent(.focus_in); + } + }, + .focus_out => { + if (@hasField(Event, "focus_out")) { + self.postEvent(.focus_out); + } + }, + .paste_start => { + if (@hasField(Event, "paste_start")) { + self.postEvent(.paste_start); + } + }, + .paste_end => { + if (@hasField(Event, "paste_end")) { + self.postEvent(.paste_end); + } + }, + .paste => |text| { + if (@hasField(Event, "paste")) { + self.postEvent(.{ .paste = text }); + } else { + if (paste_allocator) |_| + paste_allocator.?.free(text); + } + }, + .color_report => |report| { + if (@hasField(Event, "color_report")) { + self.postEvent(.{ .color_report = report }); + } + }, + .color_scheme => |scheme| { + if (@hasField(Event, "color_scheme")) { + self.postEvent(.{ .color_scheme = scheme }); + } + }, + .cap_kitty_keyboard => { + log.info("kitty keyboard capability detected", .{}); + self.vaxis.caps.kitty_keyboard = true; + }, + .cap_kitty_graphics => { + if (!self.vaxis.caps.kitty_graphics) { + log.info("kitty graphics capability detected", .{}); + self.vaxis.caps.kitty_graphics = true; + } + }, + .cap_rgb => { + log.info("rgb capability detected", .{}); + self.vaxis.caps.rgb = true; + }, + .cap_unicode => { + log.info("unicode capability detected", .{}); + self.vaxis.caps.unicode = .unicode; + self.vaxis.screen.width_method = .unicode; + }, + .cap_sgr_pixels => { + log.info("pixel mouse capability detected", .{}); + self.vaxis.caps.sgr_pixels = true; + }, + .cap_color_scheme_updates => { + log.info("color_scheme_updates capability detected", .{}); + self.vaxis.caps.color_scheme_updates = true; + }, + .cap_da1 => { + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); + }, + } + } + } + } }; } diff --git a/src/Screen.zig b/src/Screen.zig index 3bce6ba..362c477 100644 --- a/src/Screen.zig +++ b/src/Screen.zig @@ -4,7 +4,7 @@ const assert = std.debug.assert; const Cell = @import("Cell.zig"); const Shape = @import("Mouse.zig").Shape; const Image = @import("Image.zig"); -const Winsize = @import("Tty.zig").Winsize; +const Winsize = @import("tty.zig").Winsize; const Unicode = @import("Unicode.zig"); const Method = @import("gwidth.zig").Method; diff --git a/src/Vaxis.zig b/src/Vaxis.zig index bfbe3be..f095020 100644 --- a/src/Vaxis.zig +++ b/src/Vaxis.zig @@ -9,15 +9,16 @@ const InternalScreen = @import("InternalScreen.zig"); const Key = @import("Key.zig"); const Mouse = @import("Mouse.zig"); const Screen = @import("Screen.zig"); -const Tty = @import("Tty.zig"); +const tty = @import("tty.zig"); const Unicode = @import("Unicode.zig"); const Window = @import("Window.zig"); +const AnyWriter = std.io.AnyWriter; const Hyperlink = Cell.Hyperlink; const KittyFlags = Key.KittyFlags; const Shape = Mouse.Shape; const Style = Cell.Style; -const Winsize = Tty.Winsize; +const Winsize = tty.Winsize; const ctlseqs = @import("ctlseqs.zig"); const gwidth = @import("gwidth.zig"); @@ -43,8 +44,6 @@ pub const Options = struct { system_clipboard_allocator: ?std.mem.Allocator = null, }; -tty: ?Tty, - /// the screen we write to screen: Screen, /// The last screen we drew. We keep this so we can efficiently update on @@ -96,7 +95,6 @@ state: struct { pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { return .{ .opts = opts, - .tty = null, .screen = .{}, .screen_last = .{}, .render_timer = try std.time.Timer.start(), @@ -108,10 +106,11 @@ pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { /// passed, this will free resources associated with Vaxis. This is left as an /// optional so applications can choose to not free resources when the /// application will be exiting anyways -pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { - if (self.tty) |*tty| { - tty.deinit(); - } +pub fn deinit(self: *Vaxis, writer: AnyWriter, alloc: ?std.mem.Allocator) void { + self.resetState(writer) catch {}; + + // always show the cursor on exit + writer.writeAll(ctlseqs.show_cursor) catch {}; if (alloc) |a| { self.screen.deinit(a); self.screen_last.deinit(a); @@ -124,11 +123,32 @@ pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { self.unicode.deinit(); } +/// resets enabled features +pub fn resetState(self: *Vaxis, writer: AnyWriter) !void { + if (self.state.kitty_keyboard) { + try writer.writeAll(ctlseqs.csi_u_pop); + self.state.kitty_keyboard = false; + } + if (self.state.mouse) { + try self.setMouseMode(writer, false); + } + if (self.state.bracketed_paste) { + try self.setBracketedPaste(writer, false); + } + if (self.state.alt_screen) { + try self.exitAltScreen(writer); + } + if (self.state.color_scheme_updates) { + try writer.writeAll(ctlseqs.color_scheme_reset); + self.state.color_scheme_updates = false; + } +} + /// resize allocates a slice of cells equal to the number of cells /// required to display the screen (ie width x height). Any previous screen is /// freed when resizing. The cursor will be sent to it's home position and a /// hardware clear-below-cursor will be sent -pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void { +pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize, writer: AnyWriter) !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); @@ -138,18 +158,16 @@ pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void { // every cell self.screen_last.deinit(alloc); self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); - var tty = self.tty orelse return; if (self.state.alt_screen) - _ = try tty.write(ctlseqs.home) + try writer.writeAll(ctlseqs.home) else { - _ = try tty.buffered_writer.write("\r"); + try writer.writeByte('\r'); var i: usize = 0; while (i < self.state.cursor.row) : (i += 1) { - _ = try tty.buffered_writer.write(ctlseqs.ri); + try writer.writeAll(ctlseqs.ri); } } - _ = try tty.write(ctlseqs.erase_below_cursor); - try tty.flush(); + try writer.writeAll(ctlseqs.erase_below_cursor); } /// returns a Window comprising of the entire terminal screen @@ -165,23 +183,15 @@ pub fn window(self: *Vaxis) Window { /// enter the alternate screen. The alternate screen will automatically /// be exited if calling deinit while in the alt screen -pub fn enterAltScreen(self: *Vaxis) !void { - if (self.tty) |*tty| { - if (self.state.alt_screen) return; - _ = try tty.write(ctlseqs.smcup); - try tty.flush(); - self.state.alt_screen = true; - } +pub fn enterAltScreen(self: *Vaxis, writer: AnyWriter) !void { + try writer.writeAll(ctlseqs.smcup); + self.state.alt_screen = true; } /// exit the alternate screen -pub fn exitAltScreen(self: *Vaxis) !void { - if (self.tty) |*tty| { - if (!self.state.alt_screen) return; - _ = try tty.write(ctlseqs.rmcup); - try tty.flush(); - self.state.alt_screen = false; - } +pub fn exitAltScreen(self: *Vaxis, writer: AnyWriter) !void { + try writer.writeAll(ctlseqs.rmcup); + self.state.alt_screen = false; } /// write queries to the terminal to determine capabilities. Individual @@ -199,8 +209,7 @@ pub fn queryTerminal(self: *Vaxis) !void { /// write queries to the terminal to determine capabilities. This function /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if /// you are using Loop.run() -pub fn queryTerminalSend(self: *Vaxis) !void { - var tty = self.tty orelse return; +pub fn queryTerminalSend(_: Vaxis, writer: AnyWriter) !void { // TODO: re-enable this // const colorterm = std.posix.getenv("COLORTERM") orelse ""; @@ -216,29 +225,26 @@ pub fn queryTerminalSend(self: *Vaxis) !void { // doesn't hurt to blindly use them // _ = try tty.write(ctlseqs.decrqm_focus); // _ = try tty.write(ctlseqs.decrqm_sync); - _ = try tty.write(ctlseqs.decrqm_sgr_pixels); - _ = try tty.write(ctlseqs.decrqm_unicode); - _ = try tty.write(ctlseqs.decrqm_color_scheme); + try writer.writeAll(ctlseqs.decrqm_sgr_pixels); + try writer.writeAll(ctlseqs.decrqm_unicode); + try writer.writeAll(ctlseqs.decrqm_color_scheme); // TODO: XTVERSION has a DCS response. uncomment when we can parse // that // _ = try tty.write(ctlseqs.xtversion); - _ = try tty.write(ctlseqs.csi_u_query); - _ = try tty.write(ctlseqs.kitty_graphics_query); + try writer.writeAll(ctlseqs.csi_u_query); + try writer.writeAll(ctlseqs.kitty_graphics_query); // TODO: sixel geometry query interferes with F4 keys. // _ = try tty.write(ctlseqs.sixel_geometry_query); // TODO: XTGETTCAP queries ("RGB", "Smulx") - _ = try tty.write(ctlseqs.primary_device_attrs); - try tty.flush(); + try writer.writeAll(ctlseqs.primary_device_attrs); } /// Enable features detected by responses to queryTerminal. This function /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if /// you are using Loop.run() pub fn enableDetectedFeatures(self: *Vaxis) !void { - var tty = self.tty orelse return; - // Apply any environment variables if (std.posix.getenv("ASCIINEMA_REC")) |_| self.sgr = .legacy; @@ -270,8 +276,7 @@ pub fn queueRefresh(self: *Vaxis) void { } /// draws the screen to the terminal -pub fn render(self: *Vaxis) !void { - var tty = self.tty orelse return; +pub fn render(self: *Vaxis, writer: AnyWriter) !void { self.renders += 1; self.render_timer.reset(); defer { @@ -279,30 +284,29 @@ pub fn render(self: *Vaxis) !void { } defer self.refresh = false; - defer tty.flush() catch {}; // Set up sync before we write anything // TODO: optimize sync so we only sync _when we have changes_. This // requires a smarter buffered writer, we'll probably have to write // our own - _ = try tty.write(ctlseqs.sync_set); - defer _ = tty.write(ctlseqs.sync_reset) catch {}; + try writer.writeAll(ctlseqs.sync_set); + defer writer.writeAll(ctlseqs.sync_reset) catch {}; // Send the cursor to 0,0 // TODO: this needs to move after we optimize writes. We only do // this if we have an update to make. We also need to hide cursor // and then reshow it if needed - _ = try tty.write(ctlseqs.hide_cursor); + try writer.writeAll(ctlseqs.hide_cursor); if (self.state.alt_screen) - _ = try tty.write(ctlseqs.home) + try writer.writeAll(ctlseqs.home) else { - _ = try tty.write("\r"); + try writer.writeAll("\r"); var i: usize = 0; while (i < self.state.cursor.row) : (i += 1) { - _ = try tty.write(ctlseqs.ri); + try writer.writeAll(ctlseqs.ri); } } - _ = try tty.write(ctlseqs.sgr_reset); + try writer.writeAll(ctlseqs.sgr_reset); // initialize some variables var reposition: bool = false; @@ -317,7 +321,7 @@ pub fn render(self: *Vaxis) !void { // Clear all images if (self.caps.kitty_graphics) - _ = try tty.write(ctlseqs.kitty_graphics_clear); + try writer.writeAll(ctlseqs.kitty_graphics_clear); var i: usize = 0; while (i < self.screen.buf.len) { @@ -353,7 +357,7 @@ pub fn render(self: *Vaxis) !void { // Close any osc8 sequence we might be in before // repositioning if (link.uri.len > 0) { - _ = try tty.write(ctlseqs.osc8_clear); + try writer.writeAll(ctlseqs.osc8_clear); } continue; } @@ -369,76 +373,52 @@ pub fn render(self: *Vaxis) !void { if (reposition) { reposition = false; if (self.state.alt_screen) - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }) + try writer.print(ctlseqs.cup, .{ row + 1, col + 1 }) else { if (cursor_pos.row == row) { const n = col - cursor_pos.col; - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{n}); + try writer.print(ctlseqs.cuf, .{n}); } else { + try writer.writeByte('\r'); const n = row - cursor_pos.row; - var _i: usize = 0; - _ = try tty.buffered_writer.write("\r"); - while (_i < n) : (_i += 1) { - _ = try tty.buffered_writer.write("\n"); - } - if (col > 0) - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{col}); + try writer.writeByteNTimes('\n', n); } + if (col > 0) + try writer.print(ctlseqs.cuf, .{col}); } } if (cell.image) |img| { - try tty.buffered_writer.writer().print( + try writer.print( ctlseqs.kitty_graphics_preamble, .{img.img_id}, ); if (img.options.pixel_offset) |offset| { - try tty.buffered_writer.writer().print( + try writer.print( ",X={d},Y={d}", .{ offset.x, offset.y }, ); } if (img.options.clip_region) |clip| { if (clip.x) |x| - try tty.buffered_writer.writer().print( - ",x={d}", - .{x}, - ); + try writer.print(",x={d}", .{x}); if (clip.y) |y| - try tty.buffered_writer.writer().print( - ",y={d}", - .{y}, - ); + try writer.print(",y={d}", .{y}); if (clip.width) |width| - try tty.buffered_writer.writer().print( - ",w={d}", - .{width}, - ); + try writer.print(",w={d}", .{width}); if (clip.height) |height| - try tty.buffered_writer.writer().print( - ",h={d}", - .{height}, - ); + try writer.print(",h={d}", .{height}); } if (img.options.size) |size| { if (size.rows) |rows| - try tty.buffered_writer.writer().print( - ",r={d}", - .{rows}, - ); + try writer.print(",r={d}", .{rows}); if (size.cols) |cols| - try tty.buffered_writer.writer().print( - ",c={d}", - .{cols}, - ); + try writer.print(",c={d}", .{cols}); } if (img.options.z_index) |z| { - try tty.buffered_writer.writer().print( - ",z={d}", - .{z}, - ); + try writer.print(",z={d}", .{z}); } - try tty.buffered_writer.writer().writeAll(ctlseqs.kitty_graphics_closing); + try writer.writeAll(ctlseqs.kitty_graphics_closing); } // something is different, so let's loop through everything and @@ -446,69 +426,66 @@ pub fn render(self: *Vaxis) !void { // foreground if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { - const writer = tty.buffered_writer.writer(); switch (cell.style.fg) { - .default => _ = try tty.write(ctlseqs.fg_reset), + .default => try writer.writeAll(ctlseqs.fg_reset), .index => |idx| { switch (idx) { - 0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}), - 8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}), + 0...7 => try writer.print(ctlseqs.fg_base, .{idx}), + 8...15 => try writer.print(ctlseqs.fg_bright, .{idx - 8}), else => { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}), - .legacy => try std.fmt.format(writer, ctlseqs.fg_indexed_legacy, .{idx}), + .standard => try writer.print(ctlseqs.fg_indexed, .{idx}), + .legacy => try writer.print(ctlseqs.fg_indexed_legacy, .{idx}), } }, } }, .rgb => |rgb| { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), - .legacy => try std.fmt.format(writer, ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), + .standard => try writer.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), + .legacy => try writer.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), } }, } } // background if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { - const writer = tty.buffered_writer.writer(); switch (cell.style.bg) { - .default => _ = try tty.write(ctlseqs.bg_reset), + .default => try writer.writeAll(ctlseqs.bg_reset), .index => |idx| { switch (idx) { - 0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}), - 8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx - 8}), + 0...7 => try writer.print(ctlseqs.bg_base, .{idx}), + 8...15 => try writer.print(ctlseqs.bg_bright, .{idx - 8}), else => { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}), - .legacy => try std.fmt.format(writer, ctlseqs.bg_indexed_legacy, .{idx}), + .standard => try writer.print(ctlseqs.bg_indexed, .{idx}), + .legacy => try writer.print(ctlseqs.bg_indexed_legacy, .{idx}), } }, } }, .rgb => |rgb| { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), - .legacy => try std.fmt.format(writer, ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), + .standard => try writer.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), + .legacy => try writer.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), } }, } } // underline color if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { - const writer = tty.buffered_writer.writer(); switch (cell.style.bg) { - .default => _ = try tty.write(ctlseqs.ul_reset), + .default => try writer.writeAll(ctlseqs.ul_reset), .index => |idx| { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}), - .legacy => try std.fmt.format(writer, ctlseqs.ul_indexed_legacy, .{idx}), + .standard => try writer.print(ctlseqs.ul_indexed, .{idx}), + .legacy => try writer.print(ctlseqs.ul_indexed_legacy, .{idx}), } }, .rgb => |rgb| { switch (self.sgr) { - .standard => try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), - .legacy => try std.fmt.format(writer, ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), + .standard => try writer.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), + .legacy => try writer.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), } }, } @@ -523,7 +500,7 @@ pub fn render(self: *Vaxis) !void { .dotted => ctlseqs.ul_dotted, .dashed => ctlseqs.ul_dashed, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // bold if (cursor.bold != cell.style.bold) { @@ -531,9 +508,9 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.bold_set, false => ctlseqs.bold_dim_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); if (cell.style.dim) { - _ = try tty.write(ctlseqs.dim_set); + try writer.writeAll(ctlseqs.dim_set); } } // dim @@ -542,9 +519,9 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.dim_set, false => ctlseqs.bold_dim_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); if (cell.style.bold) { - _ = try tty.write(ctlseqs.bold_set); + try writer.writeAll(ctlseqs.bold_set); } } // dim @@ -553,7 +530,7 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.italic_set, false => ctlseqs.italic_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // dim if (cursor.blink != cell.style.blink) { @@ -561,7 +538,7 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.blink_set, false => ctlseqs.blink_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // reverse if (cursor.reverse != cell.style.reverse) { @@ -569,7 +546,7 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.reverse_set, false => ctlseqs.reverse_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // invisible if (cursor.invisible != cell.style.invisible) { @@ -577,7 +554,7 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.invisible_set, false => ctlseqs.invisible_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // strikethrough if (cursor.strikethrough != cell.style.strikethrough) { @@ -585,7 +562,7 @@ pub fn render(self: *Vaxis) !void { true => ctlseqs.strikethrough_set, false => ctlseqs.strikethrough_reset, }; - _ = try tty.write(seq); + try writer.writeAll(seq); } // url @@ -596,17 +573,15 @@ pub fn render(self: *Vaxis) !void { // a url ps = ""; } - const writer = tty.buffered_writer.writer(); - try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri }); + try writer.print(ctlseqs.osc8, .{ ps, cell.link.uri }); } - _ = try tty.write(cell.char.grapheme); + try writer.writeAll(cell.char.grapheme); cursor_pos.col = col + w; cursor_pos.row = row; } if (self.screen.cursor_vis) { if (self.state.alt_screen) { - try std.fmt.format( - tty.buffered_writer.writer(), + try writer.print( ctlseqs.cup, .{ self.screen.cursor_row + 1, @@ -615,39 +590,30 @@ pub fn render(self: *Vaxis) !void { ); } else { // TODO: position cursor relative to current location - _ = try tty.write("\r"); - var r: usize = 0; - if (self.screen.cursor_row >= cursor_pos.row) { - while (r < (self.screen.cursor_row - cursor_pos.row)) : (r += 1) { - _ = try tty.write("\n"); - } - } else { - while (r < (cursor_pos.row - self.screen.cursor_row)) : (r += 1) { - _ = try tty.write(ctlseqs.ri); - } - } - if (self.screen.cursor_col > 0) { - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{self.screen.cursor_col}); - } + try writer.writeByte('\r'); + if (self.screen.cursor_row >= cursor_pos.row) + try writer.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row) + else + try writer.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row); + if (self.screen.cursor_col > 0) + try writer.print(ctlseqs.cuf, .{self.screen.cursor_col}); } self.state.cursor.row = self.screen.cursor_row; self.state.cursor.col = self.screen.cursor_col; - _ = try tty.write(ctlseqs.show_cursor); + try writer.writeAll(ctlseqs.show_cursor); } else { self.state.cursor.row = cursor_pos.row; self.state.cursor.col = cursor_pos.col; } if (self.screen.mouse_shape != self.screen_last.mouse_shape) { - try std.fmt.format( - tty.buffered_writer.writer(), + try writer.print( ctlseqs.osc22_mouse_shape, .{@tagName(self.screen.mouse_shape)}, ); self.screen_last.mouse_shape = self.screen.mouse_shape; } if (self.screen.cursor_shape != self.screen_last.cursor_shape) { - try std.fmt.format( - tty.buffered_writer.writer(), + try writer.print( ctlseqs.cursor_shape, .{@intFromEnum(self.screen.cursor_shape)}, ); @@ -655,64 +621,35 @@ pub fn render(self: *Vaxis) !void { } } -fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void { - if (self.tty) |*tty| { - const flag_int: u5 = @bitCast(flags); - try std.fmt.format( - tty.buffered_writer.writer(), - ctlseqs.csi_u_push, - .{ - flag_int, - }, - ); - try tty.flush(); - self.state.kitty_keyboard = true; - } +fn enableKittyKeyboard(self: *Vaxis, writer: AnyWriter, flags: Key.KittyFlags) !void { + const flag_int: u5 = @bitCast(flags); + try writer.print(ctlseqs.csi_u_push, .{flag_int}); + self.state.kitty_keyboard = true; } /// send a system notification -pub fn notify(self: *Vaxis, title: ?[]const u8, body: []const u8) !void { - if (self.tty == null) return; - if (title) |t| { - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc777_notify, - .{ t, body }, - ); - } else { - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc9_notify, - .{body}, - ); - } - try self.tty.?.flush(); +pub fn notify(_: *Vaxis, writer: AnyWriter, title: ?[]const u8, body: []const u8) !void { + if (title) |t| + try writer.print(ctlseqs.osc777_notify, .{ t, body }) + else + try writer.print(ctlseqs.osc9_notify, .{body}); } /// sets the window title -pub fn setTitle(self: *Vaxis, title: []const u8) !void { - if (self.tty == null) return; - try std.fmt.format( - self.tty.?.buffered_writer.writer(), - ctlseqs.osc2_set_title, - .{title}, - ); - try self.tty.?.flush(); +pub fn setTitle(_: *Vaxis, writer: AnyWriter, title: []const u8) !void { + try writer.print(ctlseqs.osc2_set_title, .{title}); } // turn bracketed paste on or off. An event will be sent at the // beginning and end of a detected paste. All keystrokes between these // events were pasted -pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void { - if (self.tty) |*tty| { - const seq = if (enable) - ctlseqs.bp_set - else - ctlseqs.bp_reset; - _ = try tty.write(seq); - try tty.flush(); - self.state.bracketed_paste = enable; - } +pub fn setBracketedPaste(self: *Vaxis, writer: AnyWriter, enable: bool) !void { + const seq = if (enable) + ctlseqs.bp_set + else + ctlseqs.bp_reset; + try writer.writeAll(seq); + self.state.bracketed_paste = enable; } /// set the mouse shape @@ -721,22 +658,19 @@ pub fn setMouseShape(self: *Vaxis, shape: Shape) void { } /// Change the mouse reporting mode -pub fn setMouseMode(self: *Vaxis, enable: bool) !void { - if (self.tty) |*tty| { - if (enable) { - self.state.mouse = true; - if (self.caps.sgr_pixels) { - log.debug("enabling mouse mode: pixel coordinates", .{}); - self.state.pixel_mouse = true; - _ = try tty.write(ctlseqs.mouse_set_pixels); - } else { - log.debug("enabling mouse mode: cell coordinates", .{}); - _ = try tty.write(ctlseqs.mouse_set); - } +pub fn setMouseMode(self: *Vaxis, writer: AnyWriter, enable: bool) !void { + if (enable) { + self.state.mouse = true; + if (self.caps.sgr_pixels) { + log.debug("enabling mouse mode: pixel coordinates", .{}); + self.state.pixel_mouse = true; + try writer.writeAll(ctlseqs.mouse_set_pixels); } else { - _ = try tty.write(ctlseqs.mouse_reset); + log.debug("enabling mouse mode: cell coordinates", .{}); + try writer.writeAll(ctlseqs.mouse_set); } - try tty.flush(); + } else { + try writer.writeAll(ctlseqs.mouse_reset); } } @@ -775,14 +709,12 @@ pub fn translateMouse(self: Vaxis, mouse: Mouse) Mouse { pub fn loadImage( self: *Vaxis, alloc: std.mem.Allocator, + writer: AnyWriter, src: Image.Source, ) !Image { if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; - var tty = self.tty orelse return error.NoTTY; defer self.next_img_id += 1; - const writer = tty.buffered_writer.writer(); - var img = switch (src) { .path => |path| try zigimg.Image.fromFilePath(alloc, path), .mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes), @@ -798,8 +730,7 @@ pub fn loadImage( const id = self.next_img_id; if (encoded.len < 4096) { - try std.fmt.format( - writer, + try writer.print( "\x1b_Gf=100,i={d};{s}\x1b\\", .{ id, @@ -809,16 +740,14 @@ pub fn loadImage( } else { var n: usize = 4096; - try std.fmt.format( - writer, + try writer.print( "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", .{ id, encoded[0..n] }, ); while (n < encoded.len) : (n += 4096) { const end: usize = @min(n + 4096, encoded.len); const m: u2 = if (end == encoded.len) 0 else 1; - try std.fmt.format( - writer, + try writer.print( "\x1b_Gm={d};{s}\x1b\\", .{ m, @@ -827,7 +756,6 @@ pub fn loadImage( ); } } - try tty.buffered_writer.flush(); return .{ .id = id, .width = img.width, @@ -836,56 +764,43 @@ pub fn loadImage( } /// deletes an image from the terminal's memory -pub fn freeImage(self: Vaxis, id: u32) void { - var tty = self.tty orelse return; - const writer = tty.buffered_writer.writer(); - std.fmt.format(writer, "\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { +pub fn freeImage(_: Vaxis, writer: AnyWriter, id: u32) void { + writer.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { log.err("couldn't delete image {d}: {}", .{ id, err }); return; }; - tty.buffered_writer.flush() catch |err| { - log.err("couldn't flush writer: {}", .{err}); - }; } -pub fn copyToSystemClipboard(self: Vaxis, text: []const u8, encode_allocator: std.mem.Allocator) !void { - var tty = self.tty orelse return; +pub fn copyToSystemClipboard(_: Vaxis, writer: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void { const encoder = std.base64.standard.Encoder; const size = encoder.calcSize(text.len); const buf = try encode_allocator.alloc(u8, size); const b64 = encoder.encode(buf, text); defer encode_allocator.free(buf); - try std.fmt.format( - tty.buffered_writer.writer(), + try writer.print( ctlseqs.osc52_clipboard_copy, .{b64}, ); - try tty.flush(); } -pub fn requestSystemClipboard(self: Vaxis) !void { +pub fn requestSystemClipboard(self: Vaxis, writer: AnyWriter) !void { if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator; - var tty = self.tty orelse return; - try std.fmt.format( - tty.buffered_writer.writer(), + try writer.print( ctlseqs.osc52_clipboard_request, .{}, ); - try tty.flush(); } /// Request a color report from the terminal. Note: not all terminals support /// reporting colors. It is always safe to try, but you may not receive a /// response. -pub fn queryColor(self: Vaxis, kind: Cell.Color.Kind) !void { - var tty = self.tty orelse return; +pub fn queryColor(_: Vaxis, writer: AnyWriter, kind: Cell.Color.Kind) !void { switch (kind) { - .fg => _ = try tty.write(ctlseqs.osc10_query), - .bg => _ = try tty.write(ctlseqs.osc11_query), - .cursor => _ = try tty.write(ctlseqs.osc12_query), - .index => |idx| try tty.buffered_writer.writer().print(ctlseqs.osc4_query, .{idx}), + .fg => try writer.writeAll(ctlseqs.osc10_query), + .bg => try writer.writeAll(ctlseqs.osc11_query), + .cursor => try writer.writeAll(ctlseqs.osc12_query), + .index => |idx| try writer.print(ctlseqs.osc4_query, .{idx}), } - try tty.flush(); } /// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must @@ -893,10 +808,12 @@ pub fn queryColor(self: Vaxis, kind: Cell.Color.Kind) !void { /// capability. Support can be detected by checking the value of /// vaxis.caps.color_scheme_updates. The initial scheme will be reported when /// subscribing. -pub fn subscribeToColorSchemeUpdates(self: Vaxis) !void { - var tty = self.tty orelse return; - _ = try tty.write(ctlseqs.color_scheme_request); - _ = try tty.write(ctlseqs.color_scheme_set); - try tty.flush(); +pub fn subscribeToColorSchemeUpdates(self: Vaxis, writer: AnyWriter) !void { + try writer.writeAll(ctlseqs.color_scheme_request); + try writer.writeAll(ctlseqs.color_scheme_set); self.state.color_scheme_updates = true; } + +pub fn deviceStatusReport(_: Vaxis, writer: AnyWriter) !void { + try writer.writeAll(ctlseqs.device_status_report); +} diff --git a/src/main.zig b/src/main.zig index cb8225f..718ed46 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,9 +15,10 @@ pub const Mouse = @import("Mouse.zig"); pub const Screen = @import("Screen.zig"); pub const AllocatingScreen = @import("InternalScreen.zig"); pub const Parser = @import("Parser.zig"); -pub const Tty = @import("Tty.zig"); pub const Window = @import("Window.zig"); -pub const Winsize = Tty.Winsize; +pub const tty = @import("tty.zig"); +pub const Tty = tty.Tty; +pub const Winsize = tty.Winsize; pub const widgets = @import("widgets.zig"); pub const gwidth = @import("gwidth.zig"); @@ -36,8 +37,13 @@ pub const logo = ; test { - std.testing.refAllDecls(@This()); - std.testing.refAllDecls(widgets); + _ = @import("gwidth.zig"); + _ = @import("Cell.zig"); + _ = @import("Key.zig"); _ = @import("Parser.zig"); - _ = @import("Tty.zig"); + _ = @import("Window.zig"); + + _ = @import("gwidth.zig"); + _ = @import("queue.zig"); + _ = @import("widgets/TextInput.zig"); } diff --git a/src/tty.zig b/src/tty.zig new file mode 100644 index 0000000..10d2a16 --- /dev/null +++ b/src/tty.zig @@ -0,0 +1,181 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const posix = std.posix; + +const log = std.log.scoped(.tty); + +pub const Tty = switch (builtin.os.tag) { + .windows => @compileError("windows not supported, try wsl"), + else => PosixTty, +}; + +/// The size of the terminal screen +pub const Winsize = struct { + rows: usize, + cols: usize, + x_pixel: usize, + y_pixel: usize, +}; + +/// TTY implementation conforming to posix standards +pub const PosixTty = struct { + /// the original state of the terminal, prior to calling makeRaw + termios: posix.termios, + + /// The file descriptor of the tty + fd: posix.fd_t, + + pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, + }; + + /// global signal handlers + var handlers: [8]SignalHandler = undefined; + var handler_mutex: std.Thread.Mutex = .{}; + var handler_idx: usize = 0; + + /// initializes a Tty instance by opening /dev/tty and "making it raw". A + /// signal handler is installed for SIGWINCH. No callbacks are installed, be + /// sure to register a callback when initializing the event loop + pub fn init() !PosixTty { + // Open our tty + const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); + + // Set the termios of the tty + const termios = try makeRaw(fd); + + var act = posix.Sigaction{ + .handler = .{ .handler = PosixTty.handleWinch }, + .mask = switch (builtin.os.tag) { + .macos => 0, + .linux => posix.empty_sigset, + else => @compileError("os not supported"), + }, + .flags = 0, + }; + try posix.sigaction(posix.SIG.WINCH, &act, null); + + return .{ + .fd = fd, + .termios = termios, + }; + } + + /// release resources associated with the Tty return it to its original state + pub fn deinit(self: PosixTty) void { + posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { + log.err("couldn't restore terminal: {}", .{err}); + }; + if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos + posix.close(self.fd); + } + + /// Write bytes to the tty + pub fn write(self: *const PosixTty, bytes: []const u8) !usize { + return posix.write(self.fd, bytes); + } + + pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { + const self: *const PosixTty = @ptrCast(@alignCast(ptr)); + return posix.write(self.fd, bytes); + } + + pub fn anyWriter(self: *const PosixTty) std.io.AnyWriter { + return .{ + .context = self, + .writeFn = PosixTty.opaqueWrite, + }; + } + + pub fn read(self: *const PosixTty, buf: []u8) !usize { + return posix.read(self.fd, buf); + } + + pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { + const self: *const PosixTty = @ptrCast(@alignCast(ptr)); + return posix.read(self.fd, buf); + } + + pub fn anyReader(self: *const PosixTty) std.io.AnyReader { + return .{ + .context = self, + .readFn = PosixTty.opaqueRead, + }; + } + + /// Install a signal handler for winsize. A maximum of 8 handlers may be + /// installed + pub fn notifyWinsize(handler: SignalHandler) !void { + handler_mutex.lock(); + defer handler_mutex.unlock(); + if (handler_idx == handlers.len) return error.OutOfMemory; + handlers[handler_idx] = handler; + handler_idx += 1; + } + + fn handleWinch(_: c_int) callconv(.C) void { + handler_mutex.lock(); + defer handler_mutex.unlock(); + var i: usize = 0; + while (i < handler_idx) : (i += 1) { + const handler = handlers[i]; + handler.callback(handler.context); + } + } + + /// makeRaw enters the raw state for the terminal. + pub fn makeRaw(fd: posix.fd_t) !posix.termios { + const state = try posix.tcgetattr(fd); + var raw = state; + // see termios(3) + raw.iflag.IGNBRK = false; + raw.iflag.BRKINT = false; + raw.iflag.PARMRK = false; + raw.iflag.ISTRIP = false; + raw.iflag.INLCR = false; + raw.iflag.IGNCR = false; + raw.iflag.ICRNL = false; + raw.iflag.IXON = false; + + raw.oflag.OPOST = false; + + raw.lflag.ECHO = false; + raw.lflag.ECHONL = false; + raw.lflag.ICANON = false; + raw.lflag.ISIG = false; + raw.lflag.IEXTEN = false; + + raw.cflag.CSIZE = .CS8; + raw.cflag.PARENB = false; + + raw.cc[@intFromEnum(posix.V.MIN)] = 1; + raw.cc[@intFromEnum(posix.V.TIME)] = 0; + try posix.tcsetattr(fd, .FLUSH, raw); + return state; + } + + /// Get the window size from the kernel + pub fn getWinsize(fd: posix.fd_t) !Winsize { + var winsize = posix.winsize{ + .ws_row = 0, + .ws_col = 0, + .ws_xpixel = 0, + .ws_ypixel = 0, + }; + + const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); + if (posix.errno(err) == .SUCCESS) + return Winsize{ + .rows = winsize.ws_row, + .cols = winsize.ws_col, + .x_pixel = winsize.ws_xpixel, + .y_pixel = winsize.ws_ypixel, + }; + return error.IoctlError; + } + + pub fn bufferedWriter(self: *const PosixTty) std.io.BufferedWriter(4096, std.io.AnyWriter) { + return std.io.bufferedWriter(self.anyWriter()); + } +};