Compare commits

..

3 commits

Author SHA1 Message Date
0f9824d08b
wip: macos 2024-07-10 22:40:29 +02:00
045955d5db
wip: macos 2024-07-10 22:39:45 +02:00
54c43bab5d
wip: macos 2024-07-04 14:53:40 +02:00
22 changed files with 520 additions and 577 deletions

View file

@ -32,7 +32,6 @@ Unix-likes.
| Synchronized Output (DEC 2026) | ✅ | | Synchronized Output (DEC 2026) | ✅ |
| Unicode Core (DEC 2027) | ✅ | | Unicode Core (DEC 2027) | ✅ |
| Color Mode Updates (DEC 2031) | ✅ | | Color Mode Updates (DEC 2031) | ✅ |
| [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) | ✅ |
| Images (kitty) | ✅ | | Images (kitty) | ✅ |
## Usage ## Usage

View file

@ -23,8 +23,8 @@
.lazy = true, .lazy = true,
}, },
.aio = .{ .aio = .{
.url = "git+https://github.com/Cloudef/zig-aio#407bb416136b61087cec2c561fa4b4103a44c5b1", .url = "git+https://github.com/Cloudef/zig-aio#be8e2b374bf223202090e282447fa4581029c2eb",
.hash = "12202405ca6dd40f314dba6472983fcbb388118ab7446d75065b1efb982d03f515d2", .hash = "122012a11b37a350395a32fdb514e57ff54a0f9d8d4ce09498b6c45ffb7211232920",
.lazy = true, .lazy = true,
}, },
}, },

View file

@ -41,7 +41,7 @@ fn audioTask(allocator: std.mem.Allocator) !void {
const sound = blk: { const sound = blk: {
var tpool: coro.ThreadPool = .{}; var tpool: coro.ThreadPool = .{};
try tpool.start(allocator, .{}); try tpool.start(allocator, 1);
defer tpool.deinit(); defer tpool.deinit();
break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" }); break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" });
}; };

View file

@ -25,7 +25,6 @@ pub fn main() !void {
defer user_list.deinit(); defer user_list.deinit();
var tty = try vaxis.Tty.init(); var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc, tty.anyWriter()); defer vx.deinit(alloc, tty.anyWriter());

View file

@ -21,8 +21,6 @@ pub fn main() !void {
const alloc = gpa.allocator(); const alloc = gpa.allocator();
var tty = try vaxis.Tty.init(); var tty = try vaxis.Tty.init();
defer tty.deinit();
var vx = try vaxis.init(alloc, .{}); var vx = try vaxis.init(alloc, .{});
defer vx.deinit(alloc, tty.anyWriter()); defer vx.deinit(alloc, tty.anyWriter());

View file

@ -6,9 +6,6 @@ style: Style = .{},
link: Hyperlink = .{}, link: Hyperlink = .{},
image: ?Image.Placement = null, image: ?Image.Placement = null,
default: bool = false, default: bool = false,
/// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if
/// it should rely on the terminal's autowrap feature which can help with primary screen resizes
wrapped: bool = false,
/// Segment is a contiguous run of text that has a constant style /// Segment is a contiguous run of text that has a constant style
pub const Segment = struct { pub const Segment = struct {

View file

@ -6,6 +6,8 @@ const zigimg = @import("zigimg");
const Window = @import("Window.zig"); const Window = @import("Window.zig");
const log = std.log.scoped(.image);
const Image = @This(); const Image = @This();
const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};"; const transmit_opener = "\x1b_Gf=32,i={d},s={d},v={d},m={d};";

View file

@ -5,7 +5,7 @@ const Cell = @import("Cell.zig");
const MouseShape = @import("Mouse.zig").Shape; const MouseShape = @import("Mouse.zig").Shape;
const CursorShape = Cell.CursorShape; const CursorShape = Cell.CursorShape;
const log = std.log.scoped(.vaxis); const log = std.log.scoped(.internal_screen);
const InternalScreen = @This(); const InternalScreen = @This();

View file

@ -6,18 +6,17 @@ const grapheme = @import("grapheme");
const GraphemeCache = @import("GraphemeCache.zig"); const GraphemeCache = @import("GraphemeCache.zig");
const Parser = @import("Parser.zig"); const Parser = @import("Parser.zig");
const Queue = @import("queue.zig").Queue; const Queue = @import("queue.zig").Queue;
const vaxis = @import("main.zig"); const Tty = @import("main.zig").Tty;
const Tty = vaxis.Tty;
const Vaxis = @import("Vaxis.zig"); const Vaxis = @import("Vaxis.zig");
const log = std.log.scoped(.vaxis);
pub fn Loop(comptime T: type) type { pub fn Loop(comptime T: type) type {
return struct { return struct {
const Self = @This(); const Self = @This();
const Event = T; const Event = T;
const log = std.log.scoped(.loop);
tty: *Tty, tty: *Tty,
vaxis: *Vaxis, vaxis: *Vaxis,
@ -52,8 +51,6 @@ pub fn Loop(comptime T: type) type {
/// stops reading from the tty. /// stops reading from the tty.
pub fn stop(self: *Self) void { pub fn stop(self: *Self) void {
// If we don't have a thread, we have nothing to stop
if (self.thread == null) return;
self.should_quit = true; self.should_quit = true;
// trigger a read // trigger a read
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {}; self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
@ -93,8 +90,6 @@ pub fn Loop(comptime T: type) type {
pub fn winsizeCallback(ptr: *anyopaque) void { pub fn winsizeCallback(ptr: *anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ptr)); const self: *Self = @ptrCast(@alignCast(ptr));
// 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; const winsize = Tty.getWinsize(self.tty.fd) catch return;
if (@hasField(Event, "winsize")) { if (@hasField(Event, "winsize")) {
@ -113,12 +108,40 @@ pub fn Loop(comptime T: type) type {
switch (builtin.os.tag) { switch (builtin.os.tag) {
.windows => { .windows => {
var parser: Parser = .{
.grapheme_data = grapheme_data,
};
while (!self.should_quit) { while (!self.should_quit) {
const event = try self.tty.nextEvent(&parser, paste_allocator); const event = try self.tty.nextEvent();
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null); switch (event) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
self.postEvent(.{ .winsize = ws });
}
},
.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 });
}
},
.cap_da1 => {
std.Thread.Futex.wake(&self.vaxis.query_futex, 10);
},
.mouse => {}, // Unsupported currently
else => {},
}
} }
}, },
else => { else => {
@ -155,24 +178,7 @@ pub fn Loop(comptime T: type) type {
seq_start += result.n; seq_start += result.n;
const event = result.event orelse continue; const event = result.event orelse continue;
try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
}
}
},
}
}
};
}
pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void {
switch (builtin.os.tag) {
.windows => {
switch (event) { switch (event) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
return self.postEvent(.{ .winsize = ws });
}
},
.key_press => |key| { .key_press => |key| {
if (@hasField(Event, "key_press")) { if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way // HACK: yuck. there has to be a better way
@ -180,7 +186,7 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
if (key.text) |text| { if (key.text) |text| {
mut_key.text = cache.put(text); mut_key.text = cache.put(text);
} }
return self.postEvent(.{ .key_press = mut_key }); self.postEvent(.{ .key_press = mut_key });
} }
}, },
.key_release => |*key| { .key_release => |*key| {
@ -190,66 +196,37 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
if (key.text) |text| { if (key.text) |text| {
mut_key.text = cache.put(text); mut_key.text = cache.put(text);
} }
return self.postEvent(.{ .key_release = mut_key }); self.postEvent(.{ .key_release = mut_key });
}
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
},
.mouse => {}, // Unsupported currently
else => {},
}
},
else => {
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);
}
return 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);
}
return self.postEvent(.{ .key_release = mut_key });
} }
}, },
.mouse => |mouse| { .mouse => |mouse| {
if (@hasField(Event, "mouse")) { if (@hasField(Event, "mouse")) {
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) });
} }
}, },
.focus_in => { .focus_in => {
if (@hasField(Event, "focus_in")) { if (@hasField(Event, "focus_in")) {
return self.postEvent(.focus_in); self.postEvent(.focus_in);
} }
}, },
.focus_out => { .focus_out => {
if (@hasField(Event, "focus_out")) { if (@hasField(Event, "focus_out")) {
return self.postEvent(.focus_out); self.postEvent(.focus_out);
} }
}, },
.paste_start => { .paste_start => {
if (@hasField(Event, "paste_start")) { if (@hasField(Event, "paste_start")) {
return self.postEvent(.paste_start); self.postEvent(.paste_start);
} }
}, },
.paste_end => { .paste_end => {
if (@hasField(Event, "paste_end")) { if (@hasField(Event, "paste_end")) {
return self.postEvent(.paste_end); self.postEvent(.paste_end);
} }
}, },
.paste => |text| { .paste => |text| {
if (@hasField(Event, "paste")) { if (@hasField(Event, "paste")) {
return self.postEvent(.{ .paste = text }); self.postEvent(.{ .paste = text });
} else { } else {
if (paste_allocator) |_| if (paste_allocator) |_|
paste_allocator.?.free(text); paste_allocator.?.free(text);
@ -257,51 +234,50 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
}, },
.color_report => |report| { .color_report => |report| {
if (@hasField(Event, "color_report")) { if (@hasField(Event, "color_report")) {
return self.postEvent(.{ .color_report = report }); self.postEvent(.{ .color_report = report });
} }
}, },
.color_scheme => |scheme| { .color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) { if (@hasField(Event, "color_scheme")) {
return self.postEvent(.{ .color_scheme = scheme }); self.postEvent(.{ .color_scheme = scheme });
} }
}, },
.cap_kitty_keyboard => { .cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{}); log.info("kitty keyboard capability detected", .{});
vx.caps.kitty_keyboard = true; self.vaxis.caps.kitty_keyboard = true;
}, },
.cap_kitty_graphics => { .cap_kitty_graphics => {
if (!vx.caps.kitty_graphics) { if (!self.vaxis.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{}); log.info("kitty graphics capability detected", .{});
vx.caps.kitty_graphics = true; self.vaxis.caps.kitty_graphics = true;
} }
}, },
.cap_rgb => { .cap_rgb => {
log.info("rgb capability detected", .{}); log.info("rgb capability detected", .{});
vx.caps.rgb = true; self.vaxis.caps.rgb = true;
}, },
.cap_unicode => { .cap_unicode => {
log.info("unicode capability detected", .{}); log.info("unicode capability detected", .{});
vx.caps.unicode = .unicode; self.vaxis.caps.unicode = .unicode;
vx.screen.width_method = .unicode; self.vaxis.screen.width_method = .unicode;
}, },
.cap_sgr_pixels => { .cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{}); log.info("pixel mouse capability detected", .{});
vx.caps.sgr_pixels = true; self.vaxis.caps.sgr_pixels = true;
}, },
.cap_color_scheme_updates => { .cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{}); log.info("color_scheme_updates capability detected", .{});
vx.caps.color_scheme_updates = true; self.vaxis.caps.color_scheme_updates = true;
}, },
.cap_da1 => { .cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10); std.Thread.Futex.wake(&self.vaxis.query_futex, 10);
}, },
.winsize => |winsize| { .winsize => unreachable, // handled elsewhere for posix
vx.state.in_band_resize = true; }
if (@hasField(Event, "winsize")) { }
self.postEvent(.{ .winsize = winsize });
} }
}, },
} }
},
} }
};
} }

View file

@ -6,9 +6,8 @@ const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig"); const Mouse = @import("Mouse.zig");
const code_point = @import("code_point"); const code_point = @import("code_point");
const grapheme = @import("grapheme"); const grapheme = @import("grapheme");
const Winsize = @import("main.zig").Winsize;
const log = std.log.scoped(.vaxis_parser); const log = std.log.scoped(.parser);
const Parser = @This(); const Parser = @This();
@ -139,17 +138,8 @@ inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Re
} }
inline fn parseSs3(input: []const u8) Result { inline fn parseSs3(input: []const u8) Result {
if (input.len < 3) { std.debug.assert(input.len >= 3);
return .{
.event = null,
.n = 0,
};
}
const key: Key = switch (input[2]) { const key: Key = switch (input[2]) {
0x1B => return .{
.event = null,
.n = 2,
},
'A' => .{ .codepoint = Key.up }, 'A' => .{ .codepoint = Key.up },
'B' => .{ .codepoint = Key.down }, 'B' => .{ .codepoint = Key.down },
'C' => .{ .codepoint = Key.right }, 'C' => .{ .codepoint = Key.right },
@ -176,12 +166,7 @@ inline fn parseSs3(input: []const u8) Result {
} }
inline fn parseApc(input: []const u8) Result { inline fn parseApc(input: []const u8) Result {
if (input.len < 3) { std.debug.assert(input.len >= 3);
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{ const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null, .event = null,
.n = 0, .n = 0,
@ -202,22 +187,11 @@ inline fn parseApc(input: []const u8) Result {
/// Skips sequences until we see an ST (String Terminator, ESC \) /// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(input: []const u8) Result { inline fn skipUntilST(input: []const u8) Result {
if (input.len < 3) { std.debug.assert(input.len >= 3);
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{ const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null, .event = null,
.n = 0, .n = 0,
}; };
if (input.len < end + 1 + 1) {
return .{
.event = null,
.n = 0,
};
}
const sequence = input[0 .. end + 1 + 1]; const sequence = input[0 .. end + 1 + 1];
return .{ return .{
.event = null, .event = null,
@ -227,12 +201,6 @@ inline fn skipUntilST(input: []const u8) Result {
/// Parses an OSC sequence /// Parses an OSC sequence
inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Result { inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
var bel_terminated: bool = false; var bel_terminated: bool = false;
// end is the index of the terminating byte(s) (either the last byte of an // end is the index of the terminating byte(s) (either the last byte of an
// ST or BEL) // ST or BEL)
@ -320,13 +288,7 @@ inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Resu
} }
inline fn parseCsi(input: []const u8, text_buf: []u8) Result { inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
if (input.len < 3) { // We start iterating at index 2 to get past te '['
return .{
.event = null,
.n = 0,
};
}
// We start iterating at index 2 to get past the '['
const sequence = for (input[2..], 2..) |b, i| { const sequence = for (input[2..], 2..) |b, i| {
switch (b) { switch (b) {
0x40...0xFF => break input[0 .. i + 1], 0x40...0xFF => break input[0 .. i + 1],
@ -508,32 +470,6 @@ inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
else => return null_event, else => return null_event,
} }
}, },
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (std.mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const height_char = iter.next() orelse return null_event;
const width_char = iter.next() orelse return null_event;
const height_pix = iter.next() orelse "0";
const width_pix = iter.next() orelse "0";
const winsize: Winsize = .{
.rows = std.fmt.parseUnsigned(usize, height_char, 10) catch return null_event,
.cols = std.fmt.parseUnsigned(usize, width_char, 10) catch return null_event,
.x_pixel = std.fmt.parseUnsigned(usize, width_pix, 10) catch return null_event,
.y_pixel = std.fmt.parseUnsigned(usize, height_pix, 10) catch return null_event,
};
return .{
.event = .{ .winsize = winsize },
.n = sequence.len,
};
}
return null_event;
},
'u' => { 'u' => {
// Kitty keyboard // Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u

View file

@ -8,6 +8,8 @@ const Winsize = @import("main.zig").Winsize;
const Unicode = @import("Unicode.zig"); const Unicode = @import("Unicode.zig");
const Method = @import("gwidth.zig").Method; const Method = @import("gwidth.zig").Method;
const log = std.log.scoped(.screen);
const Screen = @This(); const Screen = @This();
width: usize = 0, width: usize = 0,

View file

@ -68,7 +68,7 @@ unicode: Unicode,
// statistics // statistics
renders: usize = 0, renders: usize = 0,
render_dur: u64 = 0, render_dur: i128 = 0,
render_timer: std.time.Timer, render_timer: std.time.Timer,
sgr: enum { sgr: enum {
@ -85,7 +85,6 @@ state: struct {
mouse: bool = false, mouse: bool = false,
pixel_mouse: bool = false, pixel_mouse: bool = false,
color_scheme_updates: bool = false, color_scheme_updates: bool = false,
in_band_resize: bool = false,
cursor: struct { cursor: struct {
row: usize = 0, row: usize = 0,
col: usize = 0, col: usize = 0,
@ -152,10 +151,6 @@ pub fn resetState(self: *Vaxis, tty: AnyWriter) !void {
try tty.writeAll(ctlseqs.color_scheme_reset); try tty.writeAll(ctlseqs.color_scheme_reset);
self.state.color_scheme_updates = false; self.state.color_scheme_updates = false;
} }
if (self.state.in_band_resize) {
try tty.writeAll(ctlseqs.in_band_resize_reset);
self.state.in_band_resize = false;
}
} }
/// resize allocates a slice of cells equal to the number of cells /// resize allocates a slice of cells equal to the number of cells
@ -249,7 +244,6 @@ pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++ try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++
ctlseqs.decrqm_unicode ++ ctlseqs.decrqm_unicode ++
ctlseqs.decrqm_color_scheme ++ ctlseqs.decrqm_color_scheme ++
ctlseqs.in_band_resize_set ++
ctlseqs.xtversion ++ ctlseqs.xtversion ++
ctlseqs.csi_u_query ++ ctlseqs.csi_u_query ++
ctlseqs.kitty_graphics_query ++ ctlseqs.kitty_graphics_query ++
@ -366,8 +360,6 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
if (col >= self.screen.width) { if (col >= self.screen.width) {
row += 1; row += 1;
col = 0; col = 0;
// Rely on terminal wrapping to reposition into next row instead of forcing it
if (!cell.wrapped)
reposition = true; reposition = true;
} }
// If cell is the same as our last frame, we don't need to do // If cell is the same as our last frame, we don't need to do
@ -496,7 +488,7 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
} }
// underline color // underline color
if (!Cell.Color.eql(cursor.ul, cell.style.ul)) { if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
switch (cell.style.ul) { switch (cell.style.bg) {
.default => try tty.writeAll(ctlseqs.ul_reset), .default => try tty.writeAll(ctlseqs.ul_reset),
.index => |idx| { .index => |idx| {
switch (self.sgr) { switch (self.sgr) {

View file

@ -7,6 +7,8 @@ const Segment = @import("Cell.zig").Segment;
const Unicode = @import("Unicode.zig"); const Unicode = @import("Unicode.zig");
const gw = @import("gwidth.zig"); const gw = @import("gwidth.zig");
const log = std.log.scoped(.window);
const Window = @This(); const Window = @This();
pub const Size = union(enum) { pub const Size = union(enum) {
@ -309,7 +311,6 @@ pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !Print
}, },
.style = segment.style, .style = segment.style,
.link = segment.link, .link = segment.link,
.wrapped = col + w >= self.width,
}); });
col += w; col += w;
} }

View file

@ -3,9 +3,14 @@ const std = @import("std");
const aio = @import("aio"); const aio = @import("aio");
const coro = @import("coro"); const coro = @import("coro");
const vaxis = @import("main.zig"); const vaxis = @import("main.zig");
const handleEventGeneric = @import("Loop.zig").handleEventGeneric;
const log = std.log.scoped(.vaxis_aio); const log = std.log.scoped(.vaxis_aio);
comptime {
if (builtin.target.os.tag == .windows) {
@compileError("Windows is not supported right now");
}
}
const Yield = enum { no_state, took_event }; const Yield = enum { no_state, took_event };
/// zig-aio based event loop /// zig-aio based event loop
@ -47,12 +52,10 @@ pub fn Loop(comptime T: type) type {
// keep on stack // keep on stack
var ctx: Context = .{ .loop = self, .tty = tty }; var ctx: Context = .{ .loop = self, .tty = tty };
if (builtin.target.os.tag != .windows) {
if (@hasField(Event, "winsize")) { if (@hasField(Event, "winsize")) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler); try vaxis.Tty.notifyWinsize(handler);
} }
}
while (true) { while (true) {
try coro.io.single(aio.WaitEventSource{ .source = &self.source }); try coro.io.single(aio.WaitEventSource{ .source = &self.source });
@ -71,32 +74,7 @@ pub fn Loop(comptime T: type) type {
}; };
} }
fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event { fn ttyReaderInner(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
var state: vaxis.Tty.EventState = .{};
while (true) {
var bytes_read: usize = 0;
var input_record: vaxis.Tty.INPUT_RECORD = undefined;
try coro.io.single(aio.ReadTty{
.tty = .{ .handle = tty.stdin },
.buffer = std.mem.asBytes(&input_record),
.out_read = &bytes_read,
});
if (try tty.eventFromRecord(&input_record, &state)) |ev| {
return ev;
}
}
}
fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void {
var cache: vaxis.GraphemeCache = .{};
while (true) {
const event = try windowsReadEvent(tty);
try handleEventGeneric(self, vx, &cache, Event, event, null);
}
}
fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
// initialize a grapheme cache // initialize a grapheme cache
var cache: vaxis.GraphemeCache = .{}; var cache: vaxis.GraphemeCache = .{};
@ -115,7 +93,7 @@ pub fn Loop(comptime T: type) type {
var buf: [4096]u8 = undefined; var buf: [4096]u8 = undefined;
var n: usize = undefined; var n: usize = undefined;
var read_start: usize = 0; var read_start: usize = 0;
try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n }); try coro.io.single(aio.Read{ .file = file, .buffer = buf[read_start..], .out_read = &n });
var seq_start: usize = 0; var seq_start: usize = 0;
while (seq_start < n) { while (seq_start < n) {
const result = try parser.parse(buf[seq_start..n], paste_allocator); const result = try parser.parse(buf[seq_start..n], paste_allocator);
@ -133,16 +111,108 @@ pub fn Loop(comptime T: type) type {
seq_start += result.n; seq_start += result.n;
const event = result.event orelse continue; const event = result.event orelse continue;
try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator); 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);
}
try 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);
}
try self.postEvent(.{ .key_release = mut_key });
}
},
.mouse => |mouse| {
if (@hasField(Event, "mouse")) {
try self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
try self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
try self.postEvent(.focus_out);
}
},
.paste_start => {
if (@hasField(Event, "paste_start")) {
try self.postEvent(.paste_start);
}
},
.paste_end => {
if (@hasField(Event, "paste_end")) {
try self.postEvent(.paste_end);
}
},
.paste => |text| {
if (@hasField(Event, "paste")) {
try self.postEvent(.{ .paste = text });
} else {
if (paste_allocator) |_|
paste_allocator.?.free(text);
}
},
.color_report => |report| {
if (@hasField(Event, "color_report")) {
try self.postEvent(.{ .color_report = report });
}
},
.color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) {
try self.postEvent(.{ .color_scheme = scheme });
}
},
.cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{});
vx.caps.kitty_keyboard = true;
},
.cap_kitty_graphics => {
if (!vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{});
vx.caps.kitty_graphics = true;
}
},
.cap_rgb => {
log.info("rgb capability detected", .{});
vx.caps.rgb = true;
},
.cap_unicode => {
log.info("unicode capability detected", .{});
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
},
.cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{});
vx.caps.sgr_pixels = true;
},
.cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{});
vx.caps.color_scheme_updates = true;
},
.cap_da1 => {
std.Thread.Futex.wake(&vx.query_futex, 10);
},
.winsize => unreachable, // handled elsewhere for posix
}
} }
} }
} }
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
return switch (builtin.target.os.tag) { self.ttyReaderInner(vx, tty, paste_allocator) catch |err| {
.windows => self.ttyReaderWindows(vx, tty),
else => self.ttyReaderPosix(vx, tty, paste_allocator),
} catch |err| {
if (err != error.Canceled) log.err("ttyReader: {}", .{err}); if (err != error.Canceled) log.err("ttyReader: {}", .{err});
self.fatal = true; self.fatal = true;
}; };

View file

@ -20,10 +20,6 @@ pub const mouse_set = "\x1b[?1002;1003;1004;1006h";
pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h"; pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h";
pub const mouse_reset = "\x1b[?1002;1003;1004;1006;1016l"; pub const mouse_reset = "\x1b[?1002;1003;1004;1006;1016l";
// in-band window size reports
pub const in_band_resize_set = "\x1b[?2048h";
pub const in_band_resize_reset = "\x1b[?2048l";
// sync // sync
pub const sync_set = "\x1b[?2026h"; pub const sync_set = "\x1b[?2026h";
pub const sync_reset = "\x1b[?2026l"; pub const sync_reset = "\x1b[?2026l";

View file

@ -64,10 +64,6 @@ pub fn panic_handler(msg: []const u8, error_return_trace: ?*std.builtin.StackTra
std.builtin.default_panic(msg, error_return_trace, ret_addr); std.builtin.default_panic(msg, error_return_trace, ret_addr);
} }
pub const log_scopes = enum {
vaxis,
};
/// the vaxis logo. In PixelCode /// the vaxis logo. In PixelCode
pub const logo = pub const logo =
\\▄ ▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄ \\▄ ▄ ▄▄▄ ▄ ▄ ▄▄▄ ▄▄▄

View file

@ -3,6 +3,8 @@ const assert = std.debug.assert;
const atomic = std.atomic; const atomic = std.atomic;
const Condition = std.Thread.Condition; const Condition = std.Thread.Condition;
const log = std.log.scoped(.queue);
/// Thread safe. Fixed size. Blocking push and pop. /// Thread safe. Fixed size. Blocking push and pop.
pub fn Queue( pub fn Queue(
comptime T: type, comptime T: type,

View file

@ -6,6 +6,8 @@ const Window = @import("../Window.zig");
const GapBuffer = @import("gap_buffer").GapBuffer; const GapBuffer = @import("gap_buffer").GapBuffer;
const Unicode = @import("../Unicode.zig"); const Unicode = @import("../Unicode.zig");
const log = std.log.scoped(.text_input);
const TextInput = @This(); const TextInput = @This();
/// The events that this widget handles /// The events that this widget handles

View file

@ -4,7 +4,7 @@ const vaxis = @import("../../main.zig");
const ansi = @import("ansi.zig"); const ansi = @import("ansi.zig");
const log = std.log.scoped(.vaxis_terminal); const log = std.log.scoped(.terminal);
const Screen = @This(); const Screen = @This();

View file

@ -165,16 +165,6 @@ pub fn spawn(self: *Terminal) !void {
try self.cmd.spawn(self.allocator); try self.cmd.spawn(self.allocator);
self.working_directory.clearRetainingCapacity();
if (self.cmd.working_directory) |pwd| {
try self.working_directory.appendSlice(pwd);
} else {
const pwd = std.fs.cwd();
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
try self.working_directory.appendSlice(out_path);
}
{ {
// add to our global list // add to our global list
global_vt_mutex.lock(); global_vt_mutex.lock();

View file

@ -6,7 +6,6 @@ const std = @import("std");
const Event = @import("../event.zig").Event; const Event = @import("../event.zig").Event;
const Key = @import("../Key.zig"); const Key = @import("../Key.zig");
const Mouse = @import("../Mouse.zig"); const Mouse = @import("../Mouse.zig");
const Parser = @import("../Parser.zig");
const windows = std.os.windows; const windows = std.os.windows;
stdin: windows.HANDLE, stdin: windows.HANDLE,
@ -118,64 +117,57 @@ pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWr
return std.io.bufferedWriter(self.anyWriter()); return std.io.bufferedWriter(self.anyWriter());
} }
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event { pub fn nextEvent(self: *Tty) !Event {
// We use a loop so we can ignore certain events // We use a loop so we can ignore certain events
var state: EventState = .{}; var ansi_buf: [128]u8 = undefined;
var ansi_idx: usize = 0;
var escape_st: bool = false;
while (true) { while (true) {
var event_count: u32 = 0; var event_count: u32 = 0;
var input_record: INPUT_RECORD = undefined; var input_record: INPUT_RECORD = undefined;
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0) if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError()); return windows.unexpectedError(windows.kernel32.GetLastError());
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| { switch (input_record.EventType) {
return ev;
}
}
}
pub const EventState = struct {
ansi_buf: [128]u8 = undefined,
ansi_idx: usize = 0,
utf16_buf: [2]u16 = undefined,
utf16_half: bool = false,
};
pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
switch (record.EventType) {
0x0001 => { // Key event 0x0001 => { // Key event
const event = record.Event.KeyEvent; const event = input_record.Event.KeyEvent;
if (state.utf16_half) half: { const base_layout: u21 = switch (event.wVirtualKeyCode) {
state.utf16_half = false; 0x00 => { // delivered when we get an escape sequence
state.utf16_buf[1] = event.uChar.UnicodeChar; ansi_buf[ansi_idx] = event.uChar.AsciiChar;
const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half; ansi_idx += 1;
const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null; if (ansi_idx <= 2) {
continue;
const key: Key = .{
.codepoint = codepoint,
.base_layout_codepoint = codepoint,
.mods = translateMods(event.dwControlKeyState),
.text = self.buf[0..n],
};
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
} }
switch (ansi_buf[1]) {
'[' => { // CSI, read until 0x40 to 0xFF
switch (event.uChar.AsciiChar) {
0x40...0xFF => {
return .cap_da1;
},
else => continue,
}
},
']' => { // OSC, read until ESC \ or BEL
switch (event.uChar.AsciiChar) {
0x07 => {
return .cap_da1;
},
0x1B => {
escape_st = true;
continue;
},
'\\' => {
if (escape_st) {
return .cap_da1;
}
continue;
},
else => continue,
}
},
else => continue,
} }
const base_layout: u16 = switch (event.wVirtualKeyCode) {
0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
break :blk event.uChar.UnicodeChar;
state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
state.ansi_idx += 1;
if (state.ansi_idx <= 2) return null;
const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
return if (result.n == 0) null else evt: {
state.ansi_idx = 0;
break :evt result.event;
};
}, },
0x08 => Key.backspace, 0x08 => Key.backspace,
0x09 => Key.tab, 0x09 => Key.tab,
@ -265,25 +257,16 @@ pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventSta
0xdc => '\\', 0xdc => '\\',
0xdd => ']', 0xdd => ']',
0xde => '\'', 0xde => '\'',
else => return null, else => continue,
}; };
if (std.unicode.utf16IsHighSurrogate(base_layout)) {
state.utf16_buf[0] = base_layout;
state.utf16_half = true;
return null;
}
if (std.unicode.utf16IsLowSurrogate(base_layout)) {
return null;
}
var codepoint: u21 = base_layout; var codepoint: u21 = base_layout;
var text: ?[]const u8 = null; var text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) { switch (event.uChar.UnicodeChar) {
0x00...0x1F => {}, 0x00...0x1F => {},
else => |cp| { else => |cp| {
codepoint = cp; codepoint = cp;
const n = try std.unicode.utf8Encode(codepoint, &self.buf); const n = try std.unicode.utf8Encode(cp, &self.buf);
text = self.buf[0..n]; text = self.buf[0..n];
}, },
} }
@ -303,7 +286,7 @@ pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventSta
0x0002 => { // Mouse event 0x0002 => { // Mouse event
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
const event = record.Event.MouseEvent; const event = input_record.Event.MouseEvent;
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
// is wheel_down // is wheel_down
@ -366,7 +349,7 @@ pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventSta
}, },
else => { else => {
std.log.warn("unknown mouse event: {}", .{event}); std.log.warn("unknown mouse event: {}", .{event});
return null; continue;
}, },
}; };
@ -408,14 +391,14 @@ pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventSta
}; };
}, },
0x0010 => { // Focus events 0x0010 => { // Focus events
switch (record.Event.FocusEvent.bSetFocus) { switch (input_record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out, 0 => return .focus_out,
else => return .focus_in, else => return .focus_in,
} }
}, },
else => {}, else => {},
} }
return null; }
} }
fn translateMods(mods: u32) Key.Modifiers { fn translateMods(mods: u32) Key.Modifiers {

View file

@ -9,7 +9,7 @@ const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig"); const Mouse = @import("Mouse.zig");
const Color = @import("Cell.zig").Color; const Color = @import("Cell.zig").Color;
const log = std.log.scoped(.vaxis_xev); const log = std.log.scoped(.tty_watcher);
pub const Event = union(enum) { pub const Event = union(enum) {
key_press: Key, key_press: Key,
@ -99,6 +99,8 @@ pub fn TtyWatcher(comptime Userdata: type) type {
.callback = Self.signalCallback, .callback = Self.signalCallback,
}; };
try Tty.notifyWinsize(handler); try Tty.notifyWinsize(handler);
const winsize = try Tty.getWinsize(self.tty.fd);
_ = self.callback(self.ud, loop, self, .{ .winsize = winsize });
} }
fn signalCallback(ptr: *anyopaque) void { fn signalCallback(ptr: *anyopaque) void {