Compare commits

..

20 commits

Author SHA1 Message Date
9a47d18ac6
wip: macos 2024-07-10 22:41:49 +02:00
36a276a10b
wip: macos 2024-07-10 22:41:43 +02:00
7b7b84ad21
wip: macos 2024-07-10 22:40:36 +02:00
Tim Culverhouse
40e51ad054 fix(underline): use correct style value in switch 2024-07-09 13:05:37 -05:00
Tim Culverhouse
dff7681c30 widgets(terminal): set working_directory at spawn
Fixes #49.

If a user has passed an initial working directory, set that as the
set that as the terminal's working directory when we spawn the widget.
2024-07-05 07:37:16 -05:00
ippsav
09a4de63e5 fix: missing tty.deinit calls 2024-07-05 04:52:33 -07:00
CJ van den Berg
72d96638a4 fix(Parser): prevent index out of bounds error in skipUntilST 2024-07-03 17:30:21 -07:00
CJ van den Berg
b82f4e14b4 feat(windows): parse escape seqences in windows input stream 2024-07-03 17:30:21 -07:00
Rylee Lyman
ca85cbf3b2 fix: use u64 for render_dur so that *Vaxis becomes align(8)
on macOS, `@alignOf(std.c.max_align_t)` is 8, while `@alignOf(i128)`
is 16. since we moved to using `std.time.Timer`, render duration
should always be an unsigned quantity.

this change is motivated by my seamstress project, which wants to use
a Vaxis struct (well, something with a Vaxis struct as a field) as a
Lua userdata. it turns out that Lua won't allocate at an alignment
greater than `@alignOf(std.c.max_align_t)` even if you ask it to.
2024-07-03 04:23:12 -07:00
Tim Culverhouse
2605613019 vaxis: conditionally rely on terminal wrap to reposition cursor
If the text was printed with a wrap, and we can determine this from the
`print` method in Window, then we rely on the terminal for wrapping.
This can help with primary screen text reflowing on resize
2024-07-02 11:27:46 -05:00
Tim Culverhouse
1a52178c1f log: prefix all log scopes with "vaxis" 2024-07-02 08:05:57 -05:00
CJ van den Berg
1e5d39d9b1 fix(windows): unbreak escape sequence handling 2024-07-01 11:55:46 -07:00
CJ van den Berg
c4b5372253 fix(windows): unbreak shifted characters 2024-07-01 11:55:46 -07:00
CJ van den Berg
99942da4e1 fix(windows): fix parsing of UTF-16 codepoints in eventFromRecord 2024-07-01 11:55:46 -07:00
Tim Culverhouse
49ed160268 loop: prevent stopping a stopped loop 2024-07-01 11:17:44 -05:00
Tim Culverhouse
b68864c3ba parser: return early if ss3 contains an escape
Signed-off-by: Tim Culverhouse <tim@timculverhouse.com>
2024-06-30 16:40:22 -05:00
Tim Culverhouse
763d2a14a3 fix: correct param order for mode 2048 response 2024-06-30 11:53:16 -05:00
Tim Culverhouse
edaeb17f3d feat: implement mode 2048 in band resize reports
Implement mode 2048 for in-band window resize reports.

Reference: https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83
2024-06-30 11:45:43 -05:00
Jari Vetoniemi
9b78bb8a78 aio: update to latest, windows, code reuse
Update to latest aio, which has minor changes such as the thread pool
argument and special aio.ReadTty operation.

In future the aio.ReadTty might have option to translate to vt escape
sequences, but for vaxis it will use the direct mode.

I was not really able to test the windows at all actually as wine did
not seem to play nice with any vaxis example, but it compiles and ...
runs?
2024-06-30 08:44:04 -07:00
Rylee Lyman
9c2d18d5a2 fix: don't call the callback synchronously on watcher init
This makes `xev.TtyWatcher` behave according to my expectations:
namely that the callback will only file after the function which
registers it has returned.
2024-06-30 08:40:58 -07:00
22 changed files with 577 additions and 520 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,6 +6,9 @@ style: Style = .{},
link: Hyperlink = .{},
image: ?Image.Placement = null,
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
pub const Segment = struct {

View file

@ -6,8 +6,6 @@ const zigimg = @import("zigimg");
const Window = @import("Window.zig");
const log = std.log.scoped(.image);
const Image = @This();
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 CursorShape = Cell.CursorShape;
const log = std.log.scoped(.internal_screen);
const log = std.log.scoped(.vaxis);
const InternalScreen = @This();

View file

@ -6,17 +6,18 @@ const grapheme = @import("grapheme");
const GraphemeCache = @import("GraphemeCache.zig");
const Parser = @import("Parser.zig");
const Queue = @import("queue.zig").Queue;
const Tty = @import("main.zig").Tty;
const vaxis = @import("main.zig");
const Tty = vaxis.Tty;
const Vaxis = @import("Vaxis.zig");
const log = std.log.scoped(.vaxis);
pub fn Loop(comptime T: type) type {
return struct {
const Self = @This();
const Event = T;
const log = std.log.scoped(.loop);
tty: *Tty,
vaxis: *Vaxis,
@ -51,6 +52,8 @@ pub fn Loop(comptime T: type) type {
/// stops reading from the tty.
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;
// trigger a read
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
@ -90,6 +93,8 @@ pub fn Loop(comptime T: type) type {
pub fn winsizeCallback(ptr: *anyopaque) void {
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;
if (@hasField(Event, "winsize")) {
@ -108,40 +113,12 @@ pub fn Loop(comptime T: type) type {
switch (builtin.os.tag) {
.windows => {
var parser: Parser = .{
.grapheme_data = grapheme_data,
};
while (!self.should_quit) {
const event = try self.tty.nextEvent();
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 => {},
}
const event = try self.tty.nextEvent(&parser, paste_allocator);
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
}
},
else => {
@ -178,102 +155,7 @@ pub fn Loop(comptime T: type) type {
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);
},
.winsize => unreachable, // handled elsewhere for posix
}
try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
}
}
},
@ -281,3 +163,145 @@ pub fn Loop(comptime T: type) type {
}
};
}
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) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
return 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);
}
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 });
}
},
.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| {
if (@hasField(Event, "mouse")) {
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
return self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
return self.postEvent(.focus_out);
}
},
.paste_start => {
if (@hasField(Event, "paste_start")) {
return self.postEvent(.paste_start);
}
},
.paste_end => {
if (@hasField(Event, "paste_end")) {
return self.postEvent(.paste_end);
}
},
.paste => |text| {
if (@hasField(Event, "paste")) {
return self.postEvent(.{ .paste = text });
} else {
if (paste_allocator) |_|
paste_allocator.?.free(text);
}
},
.color_report => |report| {
if (@hasField(Event, "color_report")) {
return self.postEvent(.{ .color_report = report });
}
},
.color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) {
return 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 => |winsize| {
vx.state.in_band_resize = true;
if (@hasField(Event, "winsize")) {
self.postEvent(.{ .winsize = winsize });
}
},
}
},
}
}

View file

@ -6,8 +6,9 @@ const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig");
const code_point = @import("code_point");
const grapheme = @import("grapheme");
const Winsize = @import("main.zig").Winsize;
const log = std.log.scoped(.parser);
const log = std.log.scoped(.vaxis_parser);
const Parser = @This();
@ -138,8 +139,17 @@ inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Re
}
inline fn parseSs3(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const key: Key = switch (input[2]) {
0x1B => return .{
.event = null,
.n = 2,
},
'A' => .{ .codepoint = Key.up },
'B' => .{ .codepoint = Key.down },
'C' => .{ .codepoint = Key.right },
@ -166,7 +176,12 @@ inline fn parseSs3(input: []const u8) Result {
}
inline fn parseApc(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
@ -187,11 +202,22 @@ inline fn parseApc(input: []const u8) Result {
/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
if (input.len < end + 1 + 1) {
return .{
.event = null,
.n = 0,
};
}
const sequence = input[0 .. end + 1 + 1];
return .{
.event = null,
@ -201,6 +227,12 @@ inline fn skipUntilST(input: []const u8) Result {
/// Parses an OSC sequence
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;
// end is the index of the terminating byte(s) (either the last byte of an
// ST or BEL)
@ -288,7 +320,13 @@ inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Resu
}
inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
// We start iterating at index 2 to get past te '['
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
// We start iterating at index 2 to get past the '['
const sequence = for (input[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break input[0 .. i + 1],
@ -470,6 +508,32 @@ inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
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' => {
// Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u

View file

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

View file

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

View file

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

View file

@ -3,14 +3,9 @@ const std = @import("std");
const aio = @import("aio");
const coro = @import("coro");
const vaxis = @import("main.zig");
const handleEventGeneric = @import("Loop.zig").handleEventGeneric;
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 };
/// zig-aio based event loop
@ -52,9 +47,11 @@ pub fn Loop(comptime T: type) type {
// keep on stack
var ctx: Context = .{ .loop = self, .tty = tty };
if (@hasField(Event, "winsize")) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler);
if (builtin.target.os.tag != .windows) {
if (@hasField(Event, "winsize")) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler);
}
}
while (true) {
@ -74,7 +71,32 @@ pub fn Loop(comptime T: type) type {
};
}
fn ttyReaderInner(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event {
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
var cache: vaxis.GraphemeCache = .{};
@ -93,7 +115,7 @@ pub fn Loop(comptime T: type) type {
var buf: [4096]u8 = undefined;
var n: usize = undefined;
var read_start: usize = 0;
try coro.io.single(aio.Read{ .file = file, .buffer = buf[read_start..], .out_read = &n });
try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n });
var seq_start: usize = 0;
while (seq_start < n) {
const result = try parser.parse(buf[seq_start..n], paste_allocator);
@ -111,108 +133,16 @@ pub fn Loop(comptime T: type) type {
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);
}
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
}
try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
}
}
}
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
self.ttyReaderInner(vx, tty, paste_allocator) catch |err| {
return switch (builtin.target.os.tag) {
.windows => self.ttyReaderWindows(vx, tty),
else => self.ttyReaderPosix(vx, tty, paste_allocator),
} catch |err| {
if (err != error.Canceled) log.err("ttyReader: {}", .{err});
self.fatal = true;
};

View file

@ -20,6 +20,10 @@ pub const mouse_set = "\x1b[?1002;1003;1004;1006h";
pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h";
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
pub const sync_set = "\x1b[?2026h";
pub const sync_reset = "\x1b[?2026l";

View file

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

View file

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

View file

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

View file

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

View file

@ -165,6 +165,16 @@ pub fn spawn(self: *Terminal) !void {
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
global_vt_mutex.lock();

View file

@ -6,6 +6,7 @@ const std = @import("std");
const Event = @import("../event.zig").Event;
const Key = @import("../Key.zig");
const Mouse = @import("../Mouse.zig");
const Parser = @import("../Parser.zig");
const windows = std.os.windows;
stdin: windows.HANDLE,
@ -117,288 +118,304 @@ pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWr
return std.io.bufferedWriter(self.anyWriter());
}
pub fn nextEvent(self: *Tty) !Event {
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
// We use a loop so we can ignore certain events
var ansi_buf: [128]u8 = undefined;
var ansi_idx: usize = 0;
var escape_st: bool = false;
var state: EventState = .{};
while (true) {
var event_count: u32 = 0;
var input_record: INPUT_RECORD = undefined;
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
switch (input_record.EventType) {
0x0001 => { // Key event
const event = input_record.Event.KeyEvent;
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
return ev;
}
}
}
const base_layout: u21 = switch (event.wVirtualKeyCode) {
0x00 => { // delivered when we get an escape sequence
ansi_buf[ansi_idx] = event.uChar.AsciiChar;
ansi_idx += 1;
if (ansi_idx <= 2) {
continue;
}
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,
}
},
0x08 => Key.backspace,
0x09 => Key.tab,
0x0D => Key.enter,
0x13 => Key.pause,
0x14 => Key.caps_lock,
0x1B => Key.escape,
0x20 => Key.space,
0x21 => Key.page_up,
0x22 => Key.page_down,
0x23 => Key.end,
0x24 => Key.home,
0x25 => Key.left,
0x26 => Key.up,
0x27 => Key.right,
0x28 => Key.down,
0x2c => Key.print_screen,
0x2d => Key.insert,
0x2e => Key.delete,
0x30...0x39 => |k| k,
0x41...0x5a => |k| k + 0x20, // translate to lowercase
0x5b => Key.left_meta,
0x5c => Key.right_meta,
0x60 => Key.kp_0,
0x61 => Key.kp_1,
0x62 => Key.kp_2,
0x63 => Key.kp_3,
0x64 => Key.kp_4,
0x65 => Key.kp_5,
0x66 => Key.kp_6,
0x67 => Key.kp_7,
0x68 => Key.kp_8,
0x69 => Key.kp_9,
0x6a => Key.kp_multiply,
0x6b => Key.kp_add,
0x6c => Key.kp_separator,
0x6d => Key.kp_subtract,
0x6e => Key.kp_decimal,
0x6f => Key.kp_divide,
0x70 => Key.f1,
0x71 => Key.f2,
0x72 => Key.f3,
0x73 => Key.f4,
0x74 => Key.f5,
0x75 => Key.f6,
0x76 => Key.f8,
0x77 => Key.f8,
0x78 => Key.f9,
0x79 => Key.f10,
0x7a => Key.f11,
0x7b => Key.f12,
0x7c => Key.f13,
0x7d => Key.f14,
0x7e => Key.f15,
0x7f => Key.f16,
0x80 => Key.f17,
0x81 => Key.f18,
0x82 => Key.f19,
0x83 => Key.f20,
0x84 => Key.f21,
0x85 => Key.f22,
0x86 => Key.f23,
0x87 => Key.f24,
0x90 => Key.num_lock,
0x91 => Key.scroll_lock,
0xa0 => Key.left_shift,
0xa1 => Key.right_shift,
0xa2 => Key.left_control,
0xa3 => Key.right_control,
0xa4 => Key.left_alt,
0xa5 => Key.right_alt,
0xad => Key.mute_volume,
0xae => Key.lower_volume,
0xaf => Key.raise_volume,
0xb0 => Key.media_track_next,
0xb1 => Key.media_track_previous,
0xb2 => Key.media_stop,
0xb3 => Key.media_play_pause,
0xba => ';',
0xbb => '+',
0xbc => ',',
0xbd => '-',
0xbe => '.',
0xbf => '/',
0xc0 => '`',
0xdb => '[',
0xdc => '\\',
0xdd => ']',
0xde => '\'',
else => continue,
};
pub const EventState = struct {
ansi_buf: [128]u8 = undefined,
ansi_idx: usize = 0,
utf16_buf: [2]u16 = undefined,
utf16_half: bool = false,
};
var codepoint: u21 = base_layout;
var text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) {
0x00...0x1F => {},
else => |cp| {
codepoint = cp;
const n = try std.unicode.utf8Encode(cp, &self.buf);
text = self.buf[0..n];
},
}
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
const event = record.Event.KeyEvent;
if (state.utf16_half) half: {
state.utf16_half = false;
state.utf16_buf[1] = event.uChar.UnicodeChar;
const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
const key: Key = .{
.codepoint = codepoint,
.base_layout_codepoint = base_layout,
.base_layout_codepoint = codepoint,
.mods = translateMods(event.dwControlKeyState),
.text = text,
.text = self.buf[0..n],
};
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
}
},
0x0002 => { // Mouse event
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
}
const event = input_record.Event.MouseEvent;
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,
0x09 => Key.tab,
0x0D => Key.enter,
0x13 => Key.pause,
0x14 => Key.caps_lock,
0x1B => Key.escape,
0x20 => Key.space,
0x21 => Key.page_up,
0x22 => Key.page_down,
0x23 => Key.end,
0x24 => Key.home,
0x25 => Key.left,
0x26 => Key.up,
0x27 => Key.right,
0x28 => Key.down,
0x2c => Key.print_screen,
0x2d => Key.insert,
0x2e => Key.delete,
0x30...0x39 => |k| k,
0x41...0x5a => |k| k + 0x20, // translate to lowercase
0x5b => Key.left_meta,
0x5c => Key.right_meta,
0x60 => Key.kp_0,
0x61 => Key.kp_1,
0x62 => Key.kp_2,
0x63 => Key.kp_3,
0x64 => Key.kp_4,
0x65 => Key.kp_5,
0x66 => Key.kp_6,
0x67 => Key.kp_7,
0x68 => Key.kp_8,
0x69 => Key.kp_9,
0x6a => Key.kp_multiply,
0x6b => Key.kp_add,
0x6c => Key.kp_separator,
0x6d => Key.kp_subtract,
0x6e => Key.kp_decimal,
0x6f => Key.kp_divide,
0x70 => Key.f1,
0x71 => Key.f2,
0x72 => Key.f3,
0x73 => Key.f4,
0x74 => Key.f5,
0x75 => Key.f6,
0x76 => Key.f8,
0x77 => Key.f8,
0x78 => Key.f9,
0x79 => Key.f10,
0x7a => Key.f11,
0x7b => Key.f12,
0x7c => Key.f13,
0x7d => Key.f14,
0x7e => Key.f15,
0x7f => Key.f16,
0x80 => Key.f17,
0x81 => Key.f18,
0x82 => Key.f19,
0x83 => Key.f20,
0x84 => Key.f21,
0x85 => Key.f22,
0x86 => Key.f23,
0x87 => Key.f24,
0x90 => Key.num_lock,
0x91 => Key.scroll_lock,
0xa0 => Key.left_shift,
0xa1 => Key.right_shift,
0xa2 => Key.left_control,
0xa3 => Key.right_control,
0xa4 => Key.left_alt,
0xa5 => Key.right_alt,
0xad => Key.mute_volume,
0xae => Key.lower_volume,
0xaf => Key.raise_volume,
0xb0 => Key.media_track_next,
0xb1 => Key.media_track_previous,
0xb2 => Key.media_stop,
0xb3 => Key.media_play_pause,
0xba => ';',
0xbb => '+',
0xbc => ',',
0xbd => '-',
0xbe => '.',
0xbf => '/',
0xc0 => '`',
0xdb => '[',
0xdc => '\\',
0xdd => ']',
0xde => '\'',
else => return null,
};
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
// is wheel_down
// Low word represents button state
const mouse_wheel_direction: i16 = blk: {
const wheelu32: u32 = event.dwButtonState >> 16;
const wheelu16: u16 = @truncate(wheelu32);
break :blk @bitCast(wheelu16);
};
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;
}
const buttons: u16 = @truncate(event.dwButtonState);
// save the current state when we are done
defer self.last_mouse_button_press = buttons;
const button_xor = self.last_mouse_button_press ^ buttons;
var codepoint: u21 = base_layout;
var text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) {
0x00...0x1F => {},
else => |cp| {
codepoint = cp;
const n = try std.unicode.utf8Encode(codepoint, &self.buf);
text = self.buf[0..n];
},
}
var event_type: Mouse.Type = .press;
const btn: Mouse.Button = switch (button_xor) {
0x0000 => blk: {
// Check wheel event
if (event.dwEventFlags & 0x0004 > 0) {
if (mouse_wheel_direction > 0)
break :blk .wheel_up
else
break :blk .wheel_down;
}
const key: Key = .{
.codepoint = codepoint,
.base_layout_codepoint = base_layout,
.mods = translateMods(event.dwControlKeyState),
.text = text,
};
// If we have no change but one of the buttons is still pressed we have a
// drag event. Find out which button is held down
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
event_type = .drag;
if (buttons & 0x0001 > 0) break :blk .left;
if (buttons & 0x0002 > 0) break :blk .right;
if (buttons & 0x0004 > 0) break :blk .middle;
if (buttons & 0x0008 > 0) break :blk .button_8;
if (buttons & 0x0010 > 0) break :blk .button_9;
}
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
}
},
0x0002 => { // Mouse event
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
break :blk .none;
},
0x0001 => blk: {
if (buttons & 0x0001 == 0) event_type = .release;
break :blk .left;
},
0x0002 => blk: {
if (buttons & 0x0002 == 0) event_type = .release;
break :blk .right;
},
0x0004 => blk: {
if (buttons & 0x0004 == 0) event_type = .release;
break :blk .middle;
},
0x0008 => blk: {
if (buttons & 0x0008 == 0) event_type = .release;
break :blk .button_8;
},
0x0010 => blk: {
if (buttons & 0x0010 == 0) event_type = .release;
break :blk .button_9;
},
else => {
std.log.warn("unknown mouse event: {}", .{event});
continue;
},
};
const event = record.Event.MouseEvent;
const shift: u32 = 0x0010;
const alt: u32 = 0x0001 | 0x0002;
const ctrl: u32 = 0x0004 | 0x0008;
const mods: Mouse.Modifiers = .{
.shift = event.dwControlKeyState & shift > 0,
.alt = event.dwControlKeyState & alt > 0,
.ctrl = event.dwControlKeyState & ctrl > 0,
};
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
// is wheel_down
// Low word represents button state
const mouse_wheel_direction: i16 = blk: {
const wheelu32: u32 = event.dwButtonState >> 16;
const wheelu16: u16 = @truncate(wheelu32);
break :blk @bitCast(wheelu16);
};
const mouse: Mouse = .{
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
.mods = mods,
.type = event_type,
.button = btn,
};
return .{ .mouse = mouse };
},
0x0004 => { // Screen resize events
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for
// the size directly when we get this event
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
const window_rect = console_info.srWindow;
const width = window_rect.Right - window_rect.Left + 1;
const height = window_rect.Bottom - window_rect.Top + 1;
return .{
.winsize = .{
.cols = @intCast(width),
.rows = @intCast(height),
.x_pixel = 0,
.y_pixel = 0,
},
};
},
0x0010 => { // Focus events
switch (input_record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out,
else => return .focus_in,
}
},
else => {},
}
const buttons: u16 = @truncate(event.dwButtonState);
// save the current state when we are done
defer self.last_mouse_button_press = buttons;
const button_xor = self.last_mouse_button_press ^ buttons;
var event_type: Mouse.Type = .press;
const btn: Mouse.Button = switch (button_xor) {
0x0000 => blk: {
// Check wheel event
if (event.dwEventFlags & 0x0004 > 0) {
if (mouse_wheel_direction > 0)
break :blk .wheel_up
else
break :blk .wheel_down;
}
// If we have no change but one of the buttons is still pressed we have a
// drag event. Find out which button is held down
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
event_type = .drag;
if (buttons & 0x0001 > 0) break :blk .left;
if (buttons & 0x0002 > 0) break :blk .right;
if (buttons & 0x0004 > 0) break :blk .middle;
if (buttons & 0x0008 > 0) break :blk .button_8;
if (buttons & 0x0010 > 0) break :blk .button_9;
}
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
break :blk .none;
},
0x0001 => blk: {
if (buttons & 0x0001 == 0) event_type = .release;
break :blk .left;
},
0x0002 => blk: {
if (buttons & 0x0002 == 0) event_type = .release;
break :blk .right;
},
0x0004 => blk: {
if (buttons & 0x0004 == 0) event_type = .release;
break :blk .middle;
},
0x0008 => blk: {
if (buttons & 0x0008 == 0) event_type = .release;
break :blk .button_8;
},
0x0010 => blk: {
if (buttons & 0x0010 == 0) event_type = .release;
break :blk .button_9;
},
else => {
std.log.warn("unknown mouse event: {}", .{event});
return null;
},
};
const shift: u32 = 0x0010;
const alt: u32 = 0x0001 | 0x0002;
const ctrl: u32 = 0x0004 | 0x0008;
const mods: Mouse.Modifiers = .{
.shift = event.dwControlKeyState & shift > 0,
.alt = event.dwControlKeyState & alt > 0,
.ctrl = event.dwControlKeyState & ctrl > 0,
};
const mouse: Mouse = .{
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
.mods = mods,
.type = event_type,
.button = btn,
};
return .{ .mouse = mouse };
},
0x0004 => { // Screen resize events
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for
// the size directly when we get this event
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
return windows.unexpectedError(windows.kernel32.GetLastError());
}
const window_rect = console_info.srWindow;
const width = window_rect.Right - window_rect.Left + 1;
const height = window_rect.Bottom - window_rect.Top + 1;
return .{
.winsize = .{
.cols = @intCast(width),
.rows = @intCast(height),
.x_pixel = 0,
.y_pixel = 0,
},
};
},
0x0010 => { // Focus events
switch (record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out,
else => return .focus_in,
}
},
else => {},
}
return null;
}
fn translateMods(mods: u32) Key.Modifiers {

View file

@ -9,7 +9,7 @@ const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig");
const Color = @import("Cell.zig").Color;
const log = std.log.scoped(.tty_watcher);
const log = std.log.scoped(.vaxis_xev);
pub const Event = union(enum) {
key_press: Key,
@ -99,8 +99,6 @@ pub fn TtyWatcher(comptime Userdata: type) type {
.callback = Self.signalCallback,
};
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 {