this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(vaxis): move tty out of vaxis

Refactor to move the tty out of the vaxis struct. All vaxis writes now
take an io.AnyWriter

+569 -286
+24 -13
examples/text_input.zig
··· 29 29 } 30 30 const alloc = gpa.allocator(); 31 31 32 + // Initalize a tty 33 + var tty = try vaxis.Tty.init(); 34 + defer tty.deinit(); 35 + 36 + // Use a buffered writer for better performance. There are a lot of writes 37 + // in the render loop and this can have a significant savings 38 + var buffered_writer = tty.bufferedWriter(); 39 + const writer = buffered_writer.writer().any(); 40 + 32 41 // Initialize Vaxis 33 42 var vx = try vaxis.init(alloc, .{}); 34 - // deinit takes an optional allocator. If your program is exiting, you can 35 - // choose to pass a null allocator to save some exit time. 36 - defer vx.deinit(alloc); 43 + defer vx.deinit(tty.anyWriter(), alloc); 37 44 38 - // create our event loop 39 45 var loop: vaxis.Loop(Event) = .{ 40 46 .vaxis = &vx, 47 + .tty = &tty, 41 48 }; 49 + try loop.init(); 42 50 43 51 // Start the read loop. This puts the terminal in raw mode and begins 44 52 // reading user input 45 - try loop.run(); 53 + try loop.start(); 46 54 defer loop.stop(); 47 55 48 56 // Optionally enter the alternate screen 49 - try vx.enterAltScreen(); 57 + try vx.enterAltScreen(writer); 50 58 51 59 // We'll adjust the color index every keypress for the border 52 60 var color_idx: u8 = 0; ··· 58 66 59 67 // Sends queries to terminal to detect certain features. This should 60 68 // _always_ be called, but is left to the application to decide when 61 - try vx.queryTerminal(); 69 + // try vx.queryTerminal(); 62 70 63 - try vx.setMouseMode(true); 71 + try vx.setMouseMode(writer, true); 72 + 73 + try buffered_writer.flush(); 64 74 65 75 // The main event loop. Vaxis provides a thread safe, blocking, buffered 66 76 // queue which can serve as the primary event queue for an application ··· 81 91 } else if (key.matches('l', .{ .ctrl = true })) { 82 92 vx.queueRefresh(); 83 93 } else if (key.matches('n', .{ .ctrl = true })) { 84 - try vx.notify("vaxis", "hello from vaxis"); 94 + try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis"); 85 95 loop.stop(); 86 96 var child = std.process.Child.init(&.{"nvim"}, alloc); 87 97 _ = try child.spawnAndWait(); 88 - try loop.run(); 89 - try vx.enterAltScreen(); 98 + try loop.start(); 99 + try vx.enterAltScreen(tty.anyWriter()); 90 100 vx.queueRefresh(); 91 101 } else if (key.matches(vaxis.Key.enter, .{})) { 92 102 text_input.clearAndFree(); ··· 110 120 // more than one byte will incur an allocation on the first render 111 121 // after it is drawn. Thereafter, it will not allocate unless the 112 122 // screen is resized 113 - .winsize => |ws| try vx.resize(alloc, ws), 123 + .winsize => |ws| try vx.resize(alloc, ws, tty.anyWriter()), 114 124 else => {}, 115 125 } 116 126 ··· 140 150 text_input.draw(child); 141 151 142 152 // Render the screen 143 - try vx.render(); 153 + try vx.render(writer); 154 + try buffered_writer.flush(); 144 155 } 145 156 }
+186 -18
src/Loop.zig
··· 1 1 const std = @import("std"); 2 + const builtin = @import("builtin"); 2 3 4 + const grapheme = @import("grapheme"); 5 + 6 + const GraphemeCache = @import("GraphemeCache.zig"); 7 + const Parser = @import("Parser.zig"); 3 8 const Queue = @import("queue.zig").Queue; 4 - const Tty = @import("Tty.zig"); 9 + const tty = @import("tty.zig"); 10 + const Tty = tty.Tty; 5 11 const Vaxis = @import("Vaxis.zig"); 6 12 7 13 pub fn Loop(comptime T: type) type { 8 14 return struct { 9 15 const Self = @This(); 10 16 17 + const Event = T; 18 + 11 19 const log = std.log.scoped(.loop); 12 20 13 - queue: Queue(T, 512) = .{}, 21 + tty: *Tty, 22 + vaxis: *Vaxis, 14 23 24 + queue: Queue(T, 512) = .{}, 15 25 thread: ?std.Thread = null, 26 + should_quit: bool = false, 16 27 17 - vaxis: *Vaxis, 28 + /// Initialize the event loop. This is an intrusive init so that we have 29 + /// a stable pointer to register signal callbacks with posix TTYs 30 + pub fn init(self: *Self) !void { 31 + switch (builtin.os.tag) { 32 + .windows => @compileError("windows not supported"), 33 + else => { 34 + const handler: Tty.SignalHandler = .{ 35 + .context = self, 36 + .callback = Self.winsizeCallback, 37 + }; 38 + try Tty.notifyWinsize(handler); 39 + }, 40 + } 41 + } 18 42 19 43 /// spawns the input thread to read input from the tty 20 - pub fn run(self: *Self) !void { 44 + pub fn start(self: *Self) !void { 21 45 if (self.thread) |_| return; 22 - if (self.vaxis.tty == null) self.vaxis.tty = try Tty.init(); 23 - self.thread = try std.Thread.spawn(.{}, Tty.run, .{ 24 - &self.vaxis.tty.?, 25 - T, 46 + self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{ 26 47 self, 27 48 &self.vaxis.unicode.grapheme_data, 28 49 self.vaxis.opts.system_clipboard_allocator, ··· 31 52 32 53 /// stops reading from the tty and returns it to it's initial state 33 54 pub fn stop(self: *Self) void { 34 - if (self.vaxis.tty) |*tty| { 35 - // stop the read loop, then join the thread 36 - tty.stop(); 37 - if (self.thread) |thread| { 38 - thread.join(); 39 - self.thread = null; 40 - } 41 - // once thread is closed we can deinit the tty 42 - tty.deinit(); 43 - self.vaxis.tty = null; 55 + self.should_quit = true; 56 + // trigger a read 57 + self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; 58 + 59 + if (self.thread) |thread| { 60 + thread.join(); 61 + self.thread = null; 44 62 } 45 63 } 46 64 ··· 68 86 69 87 pub fn tryPostEvent(self: *Self, event: T) bool { 70 88 return self.queue.tryPush(event); 89 + } 90 + 91 + pub fn winsizeCallback(ptr: *anyopaque) void { 92 + const self: *Self = @ptrCast(@alignCast(ptr)); 93 + 94 + const winsize = Tty.getWinsize(self.tty.fd) catch return; 95 + if (@hasField(Event, "winsize")) { 96 + self.postEvent(.{ .winsize = winsize }); 97 + } 98 + } 99 + 100 + /// read input from the tty. This is run in a separate thread 101 + fn ttyRun( 102 + self: *Self, 103 + grapheme_data: *const grapheme.GraphemeData, 104 + paste_allocator: ?std.mem.Allocator, 105 + ) !void { 106 + // get our initial winsize 107 + const winsize = try Tty.getWinsize(self.tty.fd); 108 + if (@hasField(Event, "winsize")) { 109 + self.postEvent(.{ .winsize = winsize }); 110 + } 111 + 112 + // initialize a grapheme cache 113 + var cache: GraphemeCache = .{}; 114 + 115 + var parser: Parser = .{ 116 + .grapheme_data = grapheme_data, 117 + }; 118 + 119 + // initialize the read buffer 120 + var buf: [1024]u8 = undefined; 121 + var read_start: usize = 0; 122 + // read loop 123 + while (!self.should_quit) { 124 + const n = try self.tty.read(buf[read_start..]); 125 + var seq_start: usize = 0; 126 + while (seq_start < n) { 127 + const result = try parser.parse(buf[seq_start..n], paste_allocator); 128 + if (result.n == 0) { 129 + // copy the read to the beginning. We don't use memcpy because 130 + // this could be overlapping, and it's also rare 131 + const initial_start = seq_start; 132 + while (seq_start < n) : (seq_start += 1) { 133 + buf[seq_start - initial_start] = buf[seq_start]; 134 + } 135 + read_start = seq_start - initial_start + 1; 136 + continue; 137 + } 138 + read_start = 0; 139 + seq_start += result.n; 140 + 141 + const event = result.event orelse continue; 142 + switch (event) { 143 + .key_press => |key| { 144 + if (@hasField(Event, "key_press")) { 145 + // HACK: yuck. there has to be a better way 146 + var mut_key = key; 147 + if (key.text) |text| { 148 + mut_key.text = cache.put(text); 149 + } 150 + self.postEvent(.{ .key_press = mut_key }); 151 + } 152 + }, 153 + .key_release => |*key| { 154 + if (@hasField(Event, "key_release")) { 155 + // HACK: yuck. there has to be a better way 156 + var mut_key = key; 157 + if (key.text) |text| { 158 + mut_key.text = cache.put(text); 159 + } 160 + self.postEvent(.{ .key_release = mut_key }); 161 + } 162 + }, 163 + .mouse => |mouse| { 164 + if (@hasField(Event, "mouse")) { 165 + self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); 166 + } 167 + }, 168 + .focus_in => { 169 + if (@hasField(Event, "focus_in")) { 170 + self.postEvent(.focus_in); 171 + } 172 + }, 173 + .focus_out => { 174 + if (@hasField(Event, "focus_out")) { 175 + self.postEvent(.focus_out); 176 + } 177 + }, 178 + .paste_start => { 179 + if (@hasField(Event, "paste_start")) { 180 + self.postEvent(.paste_start); 181 + } 182 + }, 183 + .paste_end => { 184 + if (@hasField(Event, "paste_end")) { 185 + self.postEvent(.paste_end); 186 + } 187 + }, 188 + .paste => |text| { 189 + if (@hasField(Event, "paste")) { 190 + self.postEvent(.{ .paste = text }); 191 + } else { 192 + if (paste_allocator) |_| 193 + paste_allocator.?.free(text); 194 + } 195 + }, 196 + .color_report => |report| { 197 + if (@hasField(Event, "color_report")) { 198 + self.postEvent(.{ .color_report = report }); 199 + } 200 + }, 201 + .color_scheme => |scheme| { 202 + if (@hasField(Event, "color_scheme")) { 203 + self.postEvent(.{ .color_scheme = scheme }); 204 + } 205 + }, 206 + .cap_kitty_keyboard => { 207 + log.info("kitty keyboard capability detected", .{}); 208 + self.vaxis.caps.kitty_keyboard = true; 209 + }, 210 + .cap_kitty_graphics => { 211 + if (!self.vaxis.caps.kitty_graphics) { 212 + log.info("kitty graphics capability detected", .{}); 213 + self.vaxis.caps.kitty_graphics = true; 214 + } 215 + }, 216 + .cap_rgb => { 217 + log.info("rgb capability detected", .{}); 218 + self.vaxis.caps.rgb = true; 219 + }, 220 + .cap_unicode => { 221 + log.info("unicode capability detected", .{}); 222 + self.vaxis.caps.unicode = .unicode; 223 + self.vaxis.screen.width_method = .unicode; 224 + }, 225 + .cap_sgr_pixels => { 226 + log.info("pixel mouse capability detected", .{}); 227 + self.vaxis.caps.sgr_pixels = true; 228 + }, 229 + .cap_color_scheme_updates => { 230 + log.info("color_scheme_updates capability detected", .{}); 231 + self.vaxis.caps.color_scheme_updates = true; 232 + }, 233 + .cap_da1 => { 234 + std.Thread.Futex.wake(&self.vaxis.query_futex, 10); 235 + }, 236 + } 237 + } 238 + } 71 239 } 72 240 }; 73 241 }
+1 -1
src/Screen.zig
··· 4 4 const Cell = @import("Cell.zig"); 5 5 const Shape = @import("Mouse.zig").Shape; 6 6 const Image = @import("Image.zig"); 7 - const Winsize = @import("Tty.zig").Winsize; 7 + const Winsize = @import("tty.zig").Winsize; 8 8 const Unicode = @import("Unicode.zig"); 9 9 const Method = @import("gwidth.zig").Method; 10 10
+166 -249
src/Vaxis.zig
··· 9 9 const Key = @import("Key.zig"); 10 10 const Mouse = @import("Mouse.zig"); 11 11 const Screen = @import("Screen.zig"); 12 - const Tty = @import("Tty.zig"); 12 + const tty = @import("tty.zig"); 13 13 const Unicode = @import("Unicode.zig"); 14 14 const Window = @import("Window.zig"); 15 15 16 + const AnyWriter = std.io.AnyWriter; 16 17 const Hyperlink = Cell.Hyperlink; 17 18 const KittyFlags = Key.KittyFlags; 18 19 const Shape = Mouse.Shape; 19 20 const Style = Cell.Style; 20 - const Winsize = Tty.Winsize; 21 + const Winsize = tty.Winsize; 21 22 22 23 const ctlseqs = @import("ctlseqs.zig"); 23 24 const gwidth = @import("gwidth.zig"); ··· 42 43 /// clipboard 43 44 system_clipboard_allocator: ?std.mem.Allocator = null, 44 45 }; 45 - 46 - tty: ?Tty, 47 46 48 47 /// the screen we write to 49 48 screen: Screen, ··· 96 95 pub fn init(alloc: std.mem.Allocator, opts: Options) !Vaxis { 97 96 return .{ 98 97 .opts = opts, 99 - .tty = null, 100 98 .screen = .{}, 101 99 .screen_last = .{}, 102 100 .render_timer = try std.time.Timer.start(), ··· 108 106 /// passed, this will free resources associated with Vaxis. This is left as an 109 107 /// optional so applications can choose to not free resources when the 110 108 /// application will be exiting anyways 111 - pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator) void { 112 - if (self.tty) |*tty| { 113 - tty.deinit(); 114 - } 109 + pub fn deinit(self: *Vaxis, writer: AnyWriter, alloc: ?std.mem.Allocator) void { 110 + self.resetState(writer) catch {}; 111 + 112 + // always show the cursor on exit 113 + writer.writeAll(ctlseqs.show_cursor) catch {}; 115 114 if (alloc) |a| { 116 115 self.screen.deinit(a); 117 116 self.screen_last.deinit(a); ··· 124 123 self.unicode.deinit(); 125 124 } 126 125 126 + /// resets enabled features 127 + pub fn resetState(self: *Vaxis, writer: AnyWriter) !void { 128 + if (self.state.kitty_keyboard) { 129 + try writer.writeAll(ctlseqs.csi_u_pop); 130 + self.state.kitty_keyboard = false; 131 + } 132 + if (self.state.mouse) { 133 + try self.setMouseMode(writer, false); 134 + } 135 + if (self.state.bracketed_paste) { 136 + try self.setBracketedPaste(writer, false); 137 + } 138 + if (self.state.alt_screen) { 139 + try self.exitAltScreen(writer); 140 + } 141 + if (self.state.color_scheme_updates) { 142 + try writer.writeAll(ctlseqs.color_scheme_reset); 143 + self.state.color_scheme_updates = false; 144 + } 145 + } 146 + 127 147 /// resize allocates a slice of cells equal to the number of cells 128 148 /// required to display the screen (ie width x height). Any previous screen is 129 149 /// freed when resizing. The cursor will be sent to it's home position and a 130 150 /// hardware clear-below-cursor will be sent 131 - pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize) !void { 151 + pub fn resize(self: *Vaxis, alloc: std.mem.Allocator, winsize: Winsize, writer: AnyWriter) !void { 132 152 log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows }); 133 153 self.screen.deinit(alloc); 134 154 self.screen = try Screen.init(alloc, winsize, &self.unicode); ··· 138 158 // every cell 139 159 self.screen_last.deinit(alloc); 140 160 self.screen_last = try InternalScreen.init(alloc, winsize.cols, winsize.rows); 141 - var tty = self.tty orelse return; 142 161 if (self.state.alt_screen) 143 - _ = try tty.write(ctlseqs.home) 162 + try writer.writeAll(ctlseqs.home) 144 163 else { 145 - _ = try tty.buffered_writer.write("\r"); 164 + try writer.writeByte('\r'); 146 165 var i: usize = 0; 147 166 while (i < self.state.cursor.row) : (i += 1) { 148 - _ = try tty.buffered_writer.write(ctlseqs.ri); 167 + try writer.writeAll(ctlseqs.ri); 149 168 } 150 169 } 151 - _ = try tty.write(ctlseqs.erase_below_cursor); 152 - try tty.flush(); 170 + try writer.writeAll(ctlseqs.erase_below_cursor); 153 171 } 154 172 155 173 /// returns a Window comprising of the entire terminal screen ··· 165 183 166 184 /// enter the alternate screen. The alternate screen will automatically 167 185 /// be exited if calling deinit while in the alt screen 168 - pub fn enterAltScreen(self: *Vaxis) !void { 169 - if (self.tty) |*tty| { 170 - if (self.state.alt_screen) return; 171 - _ = try tty.write(ctlseqs.smcup); 172 - try tty.flush(); 173 - self.state.alt_screen = true; 174 - } 186 + pub fn enterAltScreen(self: *Vaxis, writer: AnyWriter) !void { 187 + try writer.writeAll(ctlseqs.smcup); 188 + self.state.alt_screen = true; 175 189 } 176 190 177 191 /// exit the alternate screen 178 - pub fn exitAltScreen(self: *Vaxis) !void { 179 - if (self.tty) |*tty| { 180 - if (!self.state.alt_screen) return; 181 - _ = try tty.write(ctlseqs.rmcup); 182 - try tty.flush(); 183 - self.state.alt_screen = false; 184 - } 192 + pub fn exitAltScreen(self: *Vaxis, writer: AnyWriter) !void { 193 + try writer.writeAll(ctlseqs.rmcup); 194 + self.state.alt_screen = false; 185 195 } 186 196 187 197 /// write queries to the terminal to determine capabilities. Individual ··· 199 209 /// write queries to the terminal to determine capabilities. This function 200 210 /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 201 211 /// you are using Loop.run() 202 - pub fn queryTerminalSend(self: *Vaxis) !void { 203 - var tty = self.tty orelse return; 212 + pub fn queryTerminalSend(_: Vaxis, writer: AnyWriter) !void { 204 213 205 214 // TODO: re-enable this 206 215 // const colorterm = std.posix.getenv("COLORTERM") orelse ""; ··· 216 225 // doesn't hurt to blindly use them 217 226 // _ = try tty.write(ctlseqs.decrqm_focus); 218 227 // _ = try tty.write(ctlseqs.decrqm_sync); 219 - _ = try tty.write(ctlseqs.decrqm_sgr_pixels); 220 - _ = try tty.write(ctlseqs.decrqm_unicode); 221 - _ = try tty.write(ctlseqs.decrqm_color_scheme); 228 + try writer.writeAll(ctlseqs.decrqm_sgr_pixels); 229 + try writer.writeAll(ctlseqs.decrqm_unicode); 230 + try writer.writeAll(ctlseqs.decrqm_color_scheme); 222 231 // TODO: XTVERSION has a DCS response. uncomment when we can parse 223 232 // that 224 233 // _ = try tty.write(ctlseqs.xtversion); 225 - _ = try tty.write(ctlseqs.csi_u_query); 226 - _ = try tty.write(ctlseqs.kitty_graphics_query); 234 + try writer.writeAll(ctlseqs.csi_u_query); 235 + try writer.writeAll(ctlseqs.kitty_graphics_query); 227 236 // TODO: sixel geometry query interferes with F4 keys. 228 237 // _ = try tty.write(ctlseqs.sixel_geometry_query); 229 238 230 239 // TODO: XTGETTCAP queries ("RGB", "Smulx") 231 240 232 - _ = try tty.write(ctlseqs.primary_device_attrs); 233 - try tty.flush(); 241 + try writer.writeAll(ctlseqs.primary_device_attrs); 234 242 } 235 243 236 244 /// Enable features detected by responses to queryTerminal. This function 237 245 /// is only for use with a custom main loop. Call Vaxis.queryTerminal() if 238 246 /// you are using Loop.run() 239 247 pub fn enableDetectedFeatures(self: *Vaxis) !void { 240 - var tty = self.tty orelse return; 241 - 242 248 // Apply any environment variables 243 249 if (std.posix.getenv("ASCIINEMA_REC")) |_| 244 250 self.sgr = .legacy; ··· 270 276 } 271 277 272 278 /// draws the screen to the terminal 273 - pub fn render(self: *Vaxis) !void { 274 - var tty = self.tty orelse return; 279 + pub fn render(self: *Vaxis, writer: AnyWriter) !void { 275 280 self.renders += 1; 276 281 self.render_timer.reset(); 277 282 defer { ··· 279 284 } 280 285 281 286 defer self.refresh = false; 282 - defer tty.flush() catch {}; 283 287 284 288 // Set up sync before we write anything 285 289 // TODO: optimize sync so we only sync _when we have changes_. This 286 290 // requires a smarter buffered writer, we'll probably have to write 287 291 // our own 288 - _ = try tty.write(ctlseqs.sync_set); 289 - defer _ = tty.write(ctlseqs.sync_reset) catch {}; 292 + try writer.writeAll(ctlseqs.sync_set); 293 + defer writer.writeAll(ctlseqs.sync_reset) catch {}; 290 294 291 295 // Send the cursor to 0,0 292 296 // TODO: this needs to move after we optimize writes. We only do 293 297 // this if we have an update to make. We also need to hide cursor 294 298 // and then reshow it if needed 295 - _ = try tty.write(ctlseqs.hide_cursor); 299 + try writer.writeAll(ctlseqs.hide_cursor); 296 300 if (self.state.alt_screen) 297 - _ = try tty.write(ctlseqs.home) 301 + try writer.writeAll(ctlseqs.home) 298 302 else { 299 - _ = try tty.write("\r"); 303 + try writer.writeAll("\r"); 300 304 var i: usize = 0; 301 305 while (i < self.state.cursor.row) : (i += 1) { 302 - _ = try tty.write(ctlseqs.ri); 306 + try writer.writeAll(ctlseqs.ri); 303 307 } 304 308 } 305 - _ = try tty.write(ctlseqs.sgr_reset); 309 + try writer.writeAll(ctlseqs.sgr_reset); 306 310 307 311 // initialize some variables 308 312 var reposition: bool = false; ··· 317 321 318 322 // Clear all images 319 323 if (self.caps.kitty_graphics) 320 - _ = try tty.write(ctlseqs.kitty_graphics_clear); 324 + try writer.writeAll(ctlseqs.kitty_graphics_clear); 321 325 322 326 var i: usize = 0; 323 327 while (i < self.screen.buf.len) { ··· 353 357 // Close any osc8 sequence we might be in before 354 358 // repositioning 355 359 if (link.uri.len > 0) { 356 - _ = try tty.write(ctlseqs.osc8_clear); 360 + try writer.writeAll(ctlseqs.osc8_clear); 357 361 } 358 362 continue; 359 363 } ··· 369 373 if (reposition) { 370 374 reposition = false; 371 375 if (self.state.alt_screen) 372 - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cup, .{ row + 1, col + 1 }) 376 + try writer.print(ctlseqs.cup, .{ row + 1, col + 1 }) 373 377 else { 374 378 if (cursor_pos.row == row) { 375 379 const n = col - cursor_pos.col; 376 - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{n}); 380 + try writer.print(ctlseqs.cuf, .{n}); 377 381 } else { 382 + try writer.writeByte('\r'); 378 383 const n = row - cursor_pos.row; 379 - var _i: usize = 0; 380 - _ = try tty.buffered_writer.write("\r"); 381 - while (_i < n) : (_i += 1) { 382 - _ = try tty.buffered_writer.write("\n"); 383 - } 384 - if (col > 0) 385 - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{col}); 384 + try writer.writeByteNTimes('\n', n); 386 385 } 386 + if (col > 0) 387 + try writer.print(ctlseqs.cuf, .{col}); 387 388 } 388 389 } 389 390 390 391 if (cell.image) |img| { 391 - try tty.buffered_writer.writer().print( 392 + try writer.print( 392 393 ctlseqs.kitty_graphics_preamble, 393 394 .{img.img_id}, 394 395 ); 395 396 if (img.options.pixel_offset) |offset| { 396 - try tty.buffered_writer.writer().print( 397 + try writer.print( 397 398 ",X={d},Y={d}", 398 399 .{ offset.x, offset.y }, 399 400 ); 400 401 } 401 402 if (img.options.clip_region) |clip| { 402 403 if (clip.x) |x| 403 - try tty.buffered_writer.writer().print( 404 - ",x={d}", 405 - .{x}, 406 - ); 404 + try writer.print(",x={d}", .{x}); 407 405 if (clip.y) |y| 408 - try tty.buffered_writer.writer().print( 409 - ",y={d}", 410 - .{y}, 411 - ); 406 + try writer.print(",y={d}", .{y}); 412 407 if (clip.width) |width| 413 - try tty.buffered_writer.writer().print( 414 - ",w={d}", 415 - .{width}, 416 - ); 408 + try writer.print(",w={d}", .{width}); 417 409 if (clip.height) |height| 418 - try tty.buffered_writer.writer().print( 419 - ",h={d}", 420 - .{height}, 421 - ); 410 + try writer.print(",h={d}", .{height}); 422 411 } 423 412 if (img.options.size) |size| { 424 413 if (size.rows) |rows| 425 - try tty.buffered_writer.writer().print( 426 - ",r={d}", 427 - .{rows}, 428 - ); 414 + try writer.print(",r={d}", .{rows}); 429 415 if (size.cols) |cols| 430 - try tty.buffered_writer.writer().print( 431 - ",c={d}", 432 - .{cols}, 433 - ); 416 + try writer.print(",c={d}", .{cols}); 434 417 } 435 418 if (img.options.z_index) |z| { 436 - try tty.buffered_writer.writer().print( 437 - ",z={d}", 438 - .{z}, 439 - ); 419 + try writer.print(",z={d}", .{z}); 440 420 } 441 - try tty.buffered_writer.writer().writeAll(ctlseqs.kitty_graphics_closing); 421 + try writer.writeAll(ctlseqs.kitty_graphics_closing); 442 422 } 443 423 444 424 // something is different, so let's loop through everything and ··· 446 426 447 427 // foreground 448 428 if (!Cell.Color.eql(cursor.fg, cell.style.fg)) { 449 - const writer = tty.buffered_writer.writer(); 450 429 switch (cell.style.fg) { 451 - .default => _ = try tty.write(ctlseqs.fg_reset), 430 + .default => try writer.writeAll(ctlseqs.fg_reset), 452 431 .index => |idx| { 453 432 switch (idx) { 454 - 0...7 => try std.fmt.format(writer, ctlseqs.fg_base, .{idx}), 455 - 8...15 => try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}), 433 + 0...7 => try writer.print(ctlseqs.fg_base, .{idx}), 434 + 8...15 => try writer.print(ctlseqs.fg_bright, .{idx - 8}), 456 435 else => { 457 436 switch (self.sgr) { 458 - .standard => try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}), 459 - .legacy => try std.fmt.format(writer, ctlseqs.fg_indexed_legacy, .{idx}), 437 + .standard => try writer.print(ctlseqs.fg_indexed, .{idx}), 438 + .legacy => try writer.print(ctlseqs.fg_indexed_legacy, .{idx}), 460 439 } 461 440 }, 462 441 } 463 442 }, 464 443 .rgb => |rgb| { 465 444 switch (self.sgr) { 466 - .standard => try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 467 - .legacy => try std.fmt.format(writer, ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 445 + .standard => try writer.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 446 + .legacy => try writer.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 468 447 } 469 448 }, 470 449 } 471 450 } 472 451 // background 473 452 if (!Cell.Color.eql(cursor.bg, cell.style.bg)) { 474 - const writer = tty.buffered_writer.writer(); 475 453 switch (cell.style.bg) { 476 - .default => _ = try tty.write(ctlseqs.bg_reset), 454 + .default => try writer.writeAll(ctlseqs.bg_reset), 477 455 .index => |idx| { 478 456 switch (idx) { 479 - 0...7 => try std.fmt.format(writer, ctlseqs.bg_base, .{idx}), 480 - 8...15 => try std.fmt.format(writer, ctlseqs.bg_bright, .{idx - 8}), 457 + 0...7 => try writer.print(ctlseqs.bg_base, .{idx}), 458 + 8...15 => try writer.print(ctlseqs.bg_bright, .{idx - 8}), 481 459 else => { 482 460 switch (self.sgr) { 483 - .standard => try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}), 484 - .legacy => try std.fmt.format(writer, ctlseqs.bg_indexed_legacy, .{idx}), 461 + .standard => try writer.print(ctlseqs.bg_indexed, .{idx}), 462 + .legacy => try writer.print(ctlseqs.bg_indexed_legacy, .{idx}), 485 463 } 486 464 }, 487 465 } 488 466 }, 489 467 .rgb => |rgb| { 490 468 switch (self.sgr) { 491 - .standard => try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 492 - .legacy => try std.fmt.format(writer, ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 469 + .standard => try writer.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }), 470 + .legacy => try writer.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 493 471 } 494 472 }, 495 473 } 496 474 } 497 475 // underline color 498 476 if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { 499 - const writer = tty.buffered_writer.writer(); 500 477 switch (cell.style.bg) { 501 - .default => _ = try tty.write(ctlseqs.ul_reset), 478 + .default => try writer.writeAll(ctlseqs.ul_reset), 502 479 .index => |idx| { 503 480 switch (self.sgr) { 504 - .standard => try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}), 505 - .legacy => try std.fmt.format(writer, ctlseqs.ul_indexed_legacy, .{idx}), 481 + .standard => try writer.print(ctlseqs.ul_indexed, .{idx}), 482 + .legacy => try writer.print(ctlseqs.ul_indexed_legacy, .{idx}), 506 483 } 507 484 }, 508 485 .rgb => |rgb| { 509 486 switch (self.sgr) { 510 - .standard => try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), 511 - .legacy => try std.fmt.format(writer, ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 487 + .standard => try writer.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }), 488 + .legacy => try writer.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }), 512 489 } 513 490 }, 514 491 } ··· 523 500 .dotted => ctlseqs.ul_dotted, 524 501 .dashed => ctlseqs.ul_dashed, 525 502 }; 526 - _ = try tty.write(seq); 503 + try writer.writeAll(seq); 527 504 } 528 505 // bold 529 506 if (cursor.bold != cell.style.bold) { ··· 531 508 true => ctlseqs.bold_set, 532 509 false => ctlseqs.bold_dim_reset, 533 510 }; 534 - _ = try tty.write(seq); 511 + try writer.writeAll(seq); 535 512 if (cell.style.dim) { 536 - _ = try tty.write(ctlseqs.dim_set); 513 + try writer.writeAll(ctlseqs.dim_set); 537 514 } 538 515 } 539 516 // dim ··· 542 519 true => ctlseqs.dim_set, 543 520 false => ctlseqs.bold_dim_reset, 544 521 }; 545 - _ = try tty.write(seq); 522 + try writer.writeAll(seq); 546 523 if (cell.style.bold) { 547 - _ = try tty.write(ctlseqs.bold_set); 524 + try writer.writeAll(ctlseqs.bold_set); 548 525 } 549 526 } 550 527 // dim ··· 553 530 true => ctlseqs.italic_set, 554 531 false => ctlseqs.italic_reset, 555 532 }; 556 - _ = try tty.write(seq); 533 + try writer.writeAll(seq); 557 534 } 558 535 // dim 559 536 if (cursor.blink != cell.style.blink) { ··· 561 538 true => ctlseqs.blink_set, 562 539 false => ctlseqs.blink_reset, 563 540 }; 564 - _ = try tty.write(seq); 541 + try writer.writeAll(seq); 565 542 } 566 543 // reverse 567 544 if (cursor.reverse != cell.style.reverse) { ··· 569 546 true => ctlseqs.reverse_set, 570 547 false => ctlseqs.reverse_reset, 571 548 }; 572 - _ = try tty.write(seq); 549 + try writer.writeAll(seq); 573 550 } 574 551 // invisible 575 552 if (cursor.invisible != cell.style.invisible) { ··· 577 554 true => ctlseqs.invisible_set, 578 555 false => ctlseqs.invisible_reset, 579 556 }; 580 - _ = try tty.write(seq); 557 + try writer.writeAll(seq); 581 558 } 582 559 // strikethrough 583 560 if (cursor.strikethrough != cell.style.strikethrough) { ··· 585 562 true => ctlseqs.strikethrough_set, 586 563 false => ctlseqs.strikethrough_reset, 587 564 }; 588 - _ = try tty.write(seq); 565 + try writer.writeAll(seq); 589 566 } 590 567 591 568 // url ··· 596 573 // a url 597 574 ps = ""; 598 575 } 599 - const writer = tty.buffered_writer.writer(); 600 - try std.fmt.format(writer, ctlseqs.osc8, .{ ps, cell.link.uri }); 576 + try writer.print(ctlseqs.osc8, .{ ps, cell.link.uri }); 601 577 } 602 - _ = try tty.write(cell.char.grapheme); 578 + try writer.writeAll(cell.char.grapheme); 603 579 cursor_pos.col = col + w; 604 580 cursor_pos.row = row; 605 581 } 606 582 if (self.screen.cursor_vis) { 607 583 if (self.state.alt_screen) { 608 - try std.fmt.format( 609 - tty.buffered_writer.writer(), 584 + try writer.print( 610 585 ctlseqs.cup, 611 586 .{ 612 587 self.screen.cursor_row + 1, ··· 615 590 ); 616 591 } else { 617 592 // TODO: position cursor relative to current location 618 - _ = try tty.write("\r"); 619 - var r: usize = 0; 620 - if (self.screen.cursor_row >= cursor_pos.row) { 621 - while (r < (self.screen.cursor_row - cursor_pos.row)) : (r += 1) { 622 - _ = try tty.write("\n"); 623 - } 624 - } else { 625 - while (r < (cursor_pos.row - self.screen.cursor_row)) : (r += 1) { 626 - _ = try tty.write(ctlseqs.ri); 627 - } 628 - } 629 - if (self.screen.cursor_col > 0) { 630 - try std.fmt.format(tty.buffered_writer.writer(), ctlseqs.cuf, .{self.screen.cursor_col}); 631 - } 593 + try writer.writeByte('\r'); 594 + if (self.screen.cursor_row >= cursor_pos.row) 595 + try writer.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row) 596 + else 597 + try writer.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row); 598 + if (self.screen.cursor_col > 0) 599 + try writer.print(ctlseqs.cuf, .{self.screen.cursor_col}); 632 600 } 633 601 self.state.cursor.row = self.screen.cursor_row; 634 602 self.state.cursor.col = self.screen.cursor_col; 635 - _ = try tty.write(ctlseqs.show_cursor); 603 + try writer.writeAll(ctlseqs.show_cursor); 636 604 } else { 637 605 self.state.cursor.row = cursor_pos.row; 638 606 self.state.cursor.col = cursor_pos.col; 639 607 } 640 608 if (self.screen.mouse_shape != self.screen_last.mouse_shape) { 641 - try std.fmt.format( 642 - tty.buffered_writer.writer(), 609 + try writer.print( 643 610 ctlseqs.osc22_mouse_shape, 644 611 .{@tagName(self.screen.mouse_shape)}, 645 612 ); 646 613 self.screen_last.mouse_shape = self.screen.mouse_shape; 647 614 } 648 615 if (self.screen.cursor_shape != self.screen_last.cursor_shape) { 649 - try std.fmt.format( 650 - tty.buffered_writer.writer(), 616 + try writer.print( 651 617 ctlseqs.cursor_shape, 652 618 .{@intFromEnum(self.screen.cursor_shape)}, 653 619 ); ··· 655 621 } 656 622 } 657 623 658 - fn enableKittyKeyboard(self: *Vaxis, flags: Key.KittyFlags) !void { 659 - if (self.tty) |*tty| { 660 - const flag_int: u5 = @bitCast(flags); 661 - try std.fmt.format( 662 - tty.buffered_writer.writer(), 663 - ctlseqs.csi_u_push, 664 - .{ 665 - flag_int, 666 - }, 667 - ); 668 - try tty.flush(); 669 - self.state.kitty_keyboard = true; 670 - } 624 + fn enableKittyKeyboard(self: *Vaxis, writer: AnyWriter, flags: Key.KittyFlags) !void { 625 + const flag_int: u5 = @bitCast(flags); 626 + try writer.print(ctlseqs.csi_u_push, .{flag_int}); 627 + self.state.kitty_keyboard = true; 671 628 } 672 629 673 630 /// send a system notification 674 - pub fn notify(self: *Vaxis, title: ?[]const u8, body: []const u8) !void { 675 - if (self.tty == null) return; 676 - if (title) |t| { 677 - try std.fmt.format( 678 - self.tty.?.buffered_writer.writer(), 679 - ctlseqs.osc777_notify, 680 - .{ t, body }, 681 - ); 682 - } else { 683 - try std.fmt.format( 684 - self.tty.?.buffered_writer.writer(), 685 - ctlseqs.osc9_notify, 686 - .{body}, 687 - ); 688 - } 689 - try self.tty.?.flush(); 631 + pub fn notify(_: *Vaxis, writer: AnyWriter, title: ?[]const u8, body: []const u8) !void { 632 + if (title) |t| 633 + try writer.print(ctlseqs.osc777_notify, .{ t, body }) 634 + else 635 + try writer.print(ctlseqs.osc9_notify, .{body}); 690 636 } 691 637 692 638 /// sets the window title 693 - pub fn setTitle(self: *Vaxis, title: []const u8) !void { 694 - if (self.tty == null) return; 695 - try std.fmt.format( 696 - self.tty.?.buffered_writer.writer(), 697 - ctlseqs.osc2_set_title, 698 - .{title}, 699 - ); 700 - try self.tty.?.flush(); 639 + pub fn setTitle(_: *Vaxis, writer: AnyWriter, title: []const u8) !void { 640 + try writer.print(ctlseqs.osc2_set_title, .{title}); 701 641 } 702 642 703 643 // turn bracketed paste on or off. An event will be sent at the 704 644 // beginning and end of a detected paste. All keystrokes between these 705 645 // events were pasted 706 - pub fn setBracketedPaste(self: *Vaxis, enable: bool) !void { 707 - if (self.tty) |*tty| { 708 - const seq = if (enable) 709 - ctlseqs.bp_set 710 - else 711 - ctlseqs.bp_reset; 712 - _ = try tty.write(seq); 713 - try tty.flush(); 714 - self.state.bracketed_paste = enable; 715 - } 646 + pub fn setBracketedPaste(self: *Vaxis, writer: AnyWriter, enable: bool) !void { 647 + const seq = if (enable) 648 + ctlseqs.bp_set 649 + else 650 + ctlseqs.bp_reset; 651 + try writer.writeAll(seq); 652 + self.state.bracketed_paste = enable; 716 653 } 717 654 718 655 /// set the mouse shape ··· 721 658 } 722 659 723 660 /// Change the mouse reporting mode 724 - pub fn setMouseMode(self: *Vaxis, enable: bool) !void { 725 - if (self.tty) |*tty| { 726 - if (enable) { 727 - self.state.mouse = true; 728 - if (self.caps.sgr_pixels) { 729 - log.debug("enabling mouse mode: pixel coordinates", .{}); 730 - self.state.pixel_mouse = true; 731 - _ = try tty.write(ctlseqs.mouse_set_pixels); 732 - } else { 733 - log.debug("enabling mouse mode: cell coordinates", .{}); 734 - _ = try tty.write(ctlseqs.mouse_set); 735 - } 661 + pub fn setMouseMode(self: *Vaxis, writer: AnyWriter, enable: bool) !void { 662 + if (enable) { 663 + self.state.mouse = true; 664 + if (self.caps.sgr_pixels) { 665 + log.debug("enabling mouse mode: pixel coordinates", .{}); 666 + self.state.pixel_mouse = true; 667 + try writer.writeAll(ctlseqs.mouse_set_pixels); 736 668 } else { 737 - _ = try tty.write(ctlseqs.mouse_reset); 669 + log.debug("enabling mouse mode: cell coordinates", .{}); 670 + try writer.writeAll(ctlseqs.mouse_set); 738 671 } 739 - try tty.flush(); 672 + } else { 673 + try writer.writeAll(ctlseqs.mouse_reset); 740 674 } 741 675 } 742 676 ··· 775 709 pub fn loadImage( 776 710 self: *Vaxis, 777 711 alloc: std.mem.Allocator, 712 + writer: AnyWriter, 778 713 src: Image.Source, 779 714 ) !Image { 780 715 if (!self.caps.kitty_graphics) return error.NoGraphicsCapability; 781 - var tty = self.tty orelse return error.NoTTY; 782 716 defer self.next_img_id += 1; 783 - 784 - const writer = tty.buffered_writer.writer(); 785 717 786 718 var img = switch (src) { 787 719 .path => |path| try zigimg.Image.fromFilePath(alloc, path), ··· 798 730 const id = self.next_img_id; 799 731 800 732 if (encoded.len < 4096) { 801 - try std.fmt.format( 802 - writer, 733 + try writer.print( 803 734 "\x1b_Gf=100,i={d};{s}\x1b\\", 804 735 .{ 805 736 id, ··· 809 740 } else { 810 741 var n: usize = 4096; 811 742 812 - try std.fmt.format( 813 - writer, 743 + try writer.print( 814 744 "\x1b_Gf=100,i={d},m=1;{s}\x1b\\", 815 745 .{ id, encoded[0..n] }, 816 746 ); 817 747 while (n < encoded.len) : (n += 4096) { 818 748 const end: usize = @min(n + 4096, encoded.len); 819 749 const m: u2 = if (end == encoded.len) 0 else 1; 820 - try std.fmt.format( 821 - writer, 750 + try writer.print( 822 751 "\x1b_Gm={d};{s}\x1b\\", 823 752 .{ 824 753 m, ··· 827 756 ); 828 757 } 829 758 } 830 - try tty.buffered_writer.flush(); 831 759 return .{ 832 760 .id = id, 833 761 .width = img.width, ··· 836 764 } 837 765 838 766 /// deletes an image from the terminal's memory 839 - pub fn freeImage(self: Vaxis, id: u32) void { 840 - var tty = self.tty orelse return; 841 - const writer = tty.buffered_writer.writer(); 842 - std.fmt.format(writer, "\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { 767 + pub fn freeImage(_: Vaxis, writer: AnyWriter, id: u32) void { 768 + writer.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| { 843 769 log.err("couldn't delete image {d}: {}", .{ id, err }); 844 770 return; 845 - }; 846 - tty.buffered_writer.flush() catch |err| { 847 - log.err("couldn't flush writer: {}", .{err}); 848 771 }; 849 772 } 850 773 851 - pub fn copyToSystemClipboard(self: Vaxis, text: []const u8, encode_allocator: std.mem.Allocator) !void { 852 - var tty = self.tty orelse return; 774 + pub fn copyToSystemClipboard(_: Vaxis, writer: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void { 853 775 const encoder = std.base64.standard.Encoder; 854 776 const size = encoder.calcSize(text.len); 855 777 const buf = try encode_allocator.alloc(u8, size); 856 778 const b64 = encoder.encode(buf, text); 857 779 defer encode_allocator.free(buf); 858 - try std.fmt.format( 859 - tty.buffered_writer.writer(), 780 + try writer.print( 860 781 ctlseqs.osc52_clipboard_copy, 861 782 .{b64}, 862 783 ); 863 - try tty.flush(); 864 784 } 865 785 866 - pub fn requestSystemClipboard(self: Vaxis) !void { 786 + pub fn requestSystemClipboard(self: Vaxis, writer: AnyWriter) !void { 867 787 if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator; 868 - var tty = self.tty orelse return; 869 - try std.fmt.format( 870 - tty.buffered_writer.writer(), 788 + try writer.print( 871 789 ctlseqs.osc52_clipboard_request, 872 790 .{}, 873 791 ); 874 - try tty.flush(); 875 792 } 876 793 877 794 /// Request a color report from the terminal. Note: not all terminals support 878 795 /// reporting colors. It is always safe to try, but you may not receive a 879 796 /// response. 880 - pub fn queryColor(self: Vaxis, kind: Cell.Color.Kind) !void { 881 - var tty = self.tty orelse return; 797 + pub fn queryColor(_: Vaxis, writer: AnyWriter, kind: Cell.Color.Kind) !void { 882 798 switch (kind) { 883 - .fg => _ = try tty.write(ctlseqs.osc10_query), 884 - .bg => _ = try tty.write(ctlseqs.osc11_query), 885 - .cursor => _ = try tty.write(ctlseqs.osc12_query), 886 - .index => |idx| try tty.buffered_writer.writer().print(ctlseqs.osc4_query, .{idx}), 799 + .fg => try writer.writeAll(ctlseqs.osc10_query), 800 + .bg => try writer.writeAll(ctlseqs.osc11_query), 801 + .cursor => try writer.writeAll(ctlseqs.osc12_query), 802 + .index => |idx| try writer.print(ctlseqs.osc4_query, .{idx}), 887 803 } 888 - try tty.flush(); 889 804 } 890 805 891 806 /// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must ··· 893 808 /// capability. Support can be detected by checking the value of 894 809 /// vaxis.caps.color_scheme_updates. The initial scheme will be reported when 895 810 /// subscribing. 896 - pub fn subscribeToColorSchemeUpdates(self: Vaxis) !void { 897 - var tty = self.tty orelse return; 898 - _ = try tty.write(ctlseqs.color_scheme_request); 899 - _ = try tty.write(ctlseqs.color_scheme_set); 900 - try tty.flush(); 811 + pub fn subscribeToColorSchemeUpdates(self: Vaxis, writer: AnyWriter) !void { 812 + try writer.writeAll(ctlseqs.color_scheme_request); 813 + try writer.writeAll(ctlseqs.color_scheme_set); 901 814 self.state.color_scheme_updates = true; 902 815 } 816 + 817 + pub fn deviceStatusReport(_: Vaxis, writer: AnyWriter) !void { 818 + try writer.writeAll(ctlseqs.device_status_report); 819 + }
+11 -5
src/main.zig
··· 15 15 pub const Screen = @import("Screen.zig"); 16 16 pub const AllocatingScreen = @import("InternalScreen.zig"); 17 17 pub const Parser = @import("Parser.zig"); 18 - pub const Tty = @import("Tty.zig"); 19 18 pub const Window = @import("Window.zig"); 20 - pub const Winsize = Tty.Winsize; 19 + pub const tty = @import("tty.zig"); 20 + pub const Tty = tty.Tty; 21 + pub const Winsize = tty.Winsize; 21 22 22 23 pub const widgets = @import("widgets.zig"); 23 24 pub const gwidth = @import("gwidth.zig"); ··· 36 37 ; 37 38 38 39 test { 39 - std.testing.refAllDecls(@This()); 40 - std.testing.refAllDecls(widgets); 40 + _ = @import("gwidth.zig"); 41 + _ = @import("Cell.zig"); 42 + _ = @import("Key.zig"); 41 43 _ = @import("Parser.zig"); 42 - _ = @import("Tty.zig"); 44 + _ = @import("Window.zig"); 45 + 46 + _ = @import("gwidth.zig"); 47 + _ = @import("queue.zig"); 48 + _ = @import("widgets/TextInput.zig"); 43 49 }
+181
src/tty.zig
··· 1 + const std = @import("std"); 2 + const builtin = @import("builtin"); 3 + const posix = std.posix; 4 + 5 + const log = std.log.scoped(.tty); 6 + 7 + pub const Tty = switch (builtin.os.tag) { 8 + .windows => @compileError("windows not supported, try wsl"), 9 + else => PosixTty, 10 + }; 11 + 12 + /// The size of the terminal screen 13 + pub const Winsize = struct { 14 + rows: usize, 15 + cols: usize, 16 + x_pixel: usize, 17 + y_pixel: usize, 18 + }; 19 + 20 + /// TTY implementation conforming to posix standards 21 + pub const PosixTty = struct { 22 + /// the original state of the terminal, prior to calling makeRaw 23 + termios: posix.termios, 24 + 25 + /// The file descriptor of the tty 26 + fd: posix.fd_t, 27 + 28 + pub const SignalHandler = struct { 29 + context: *anyopaque, 30 + callback: *const fn (context: *anyopaque) void, 31 + }; 32 + 33 + /// global signal handlers 34 + var handlers: [8]SignalHandler = undefined; 35 + var handler_mutex: std.Thread.Mutex = .{}; 36 + var handler_idx: usize = 0; 37 + 38 + /// initializes a Tty instance by opening /dev/tty and "making it raw". A 39 + /// signal handler is installed for SIGWINCH. No callbacks are installed, be 40 + /// sure to register a callback when initializing the event loop 41 + pub fn init() !PosixTty { 42 + // Open our tty 43 + const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0); 44 + 45 + // Set the termios of the tty 46 + const termios = try makeRaw(fd); 47 + 48 + var act = posix.Sigaction{ 49 + .handler = .{ .handler = PosixTty.handleWinch }, 50 + .mask = switch (builtin.os.tag) { 51 + .macos => 0, 52 + .linux => posix.empty_sigset, 53 + else => @compileError("os not supported"), 54 + }, 55 + .flags = 0, 56 + }; 57 + try posix.sigaction(posix.SIG.WINCH, &act, null); 58 + 59 + return .{ 60 + .fd = fd, 61 + .termios = termios, 62 + }; 63 + } 64 + 65 + /// release resources associated with the Tty return it to its original state 66 + pub fn deinit(self: PosixTty) void { 67 + posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| { 68 + log.err("couldn't restore terminal: {}", .{err}); 69 + }; 70 + if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos 71 + posix.close(self.fd); 72 + } 73 + 74 + /// Write bytes to the tty 75 + pub fn write(self: *const PosixTty, bytes: []const u8) !usize { 76 + return posix.write(self.fd, bytes); 77 + } 78 + 79 + pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize { 80 + const self: *const PosixTty = @ptrCast(@alignCast(ptr)); 81 + return posix.write(self.fd, bytes); 82 + } 83 + 84 + pub fn anyWriter(self: *const PosixTty) std.io.AnyWriter { 85 + return .{ 86 + .context = self, 87 + .writeFn = PosixTty.opaqueWrite, 88 + }; 89 + } 90 + 91 + pub fn read(self: *const PosixTty, buf: []u8) !usize { 92 + return posix.read(self.fd, buf); 93 + } 94 + 95 + pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize { 96 + const self: *const PosixTty = @ptrCast(@alignCast(ptr)); 97 + return posix.read(self.fd, buf); 98 + } 99 + 100 + pub fn anyReader(self: *const PosixTty) std.io.AnyReader { 101 + return .{ 102 + .context = self, 103 + .readFn = PosixTty.opaqueRead, 104 + }; 105 + } 106 + 107 + /// Install a signal handler for winsize. A maximum of 8 handlers may be 108 + /// installed 109 + pub fn notifyWinsize(handler: SignalHandler) !void { 110 + handler_mutex.lock(); 111 + defer handler_mutex.unlock(); 112 + if (handler_idx == handlers.len) return error.OutOfMemory; 113 + handlers[handler_idx] = handler; 114 + handler_idx += 1; 115 + } 116 + 117 + fn handleWinch(_: c_int) callconv(.C) void { 118 + handler_mutex.lock(); 119 + defer handler_mutex.unlock(); 120 + var i: usize = 0; 121 + while (i < handler_idx) : (i += 1) { 122 + const handler = handlers[i]; 123 + handler.callback(handler.context); 124 + } 125 + } 126 + 127 + /// makeRaw enters the raw state for the terminal. 128 + pub fn makeRaw(fd: posix.fd_t) !posix.termios { 129 + const state = try posix.tcgetattr(fd); 130 + var raw = state; 131 + // see termios(3) 132 + raw.iflag.IGNBRK = false; 133 + raw.iflag.BRKINT = false; 134 + raw.iflag.PARMRK = false; 135 + raw.iflag.ISTRIP = false; 136 + raw.iflag.INLCR = false; 137 + raw.iflag.IGNCR = false; 138 + raw.iflag.ICRNL = false; 139 + raw.iflag.IXON = false; 140 + 141 + raw.oflag.OPOST = false; 142 + 143 + raw.lflag.ECHO = false; 144 + raw.lflag.ECHONL = false; 145 + raw.lflag.ICANON = false; 146 + raw.lflag.ISIG = false; 147 + raw.lflag.IEXTEN = false; 148 + 149 + raw.cflag.CSIZE = .CS8; 150 + raw.cflag.PARENB = false; 151 + 152 + raw.cc[@intFromEnum(posix.V.MIN)] = 1; 153 + raw.cc[@intFromEnum(posix.V.TIME)] = 0; 154 + try posix.tcsetattr(fd, .FLUSH, raw); 155 + return state; 156 + } 157 + 158 + /// Get the window size from the kernel 159 + pub fn getWinsize(fd: posix.fd_t) !Winsize { 160 + var winsize = posix.winsize{ 161 + .ws_row = 0, 162 + .ws_col = 0, 163 + .ws_xpixel = 0, 164 + .ws_ypixel = 0, 165 + }; 166 + 167 + const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 168 + if (posix.errno(err) == .SUCCESS) 169 + return Winsize{ 170 + .rows = winsize.ws_row, 171 + .cols = winsize.ws_col, 172 + .x_pixel = winsize.ws_xpixel, 173 + .y_pixel = winsize.ws_ypixel, 174 + }; 175 + return error.IoctlError; 176 + } 177 + 178 + pub fn bufferedWriter(self: *const PosixTty) std.io.BufferedWriter(4096, std.io.AnyWriter) { 179 + return std.io.bufferedWriter(self.anyWriter()); 180 + } 181 + };