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
26 changed files with 727 additions and 530 deletions

View file

@ -32,6 +32,7 @@ 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#be8e2b374bf223202090e282447fa4581029c2eb", .url = "git+https://github.com/Cloudef/zig-aio#407bb416136b61087cec2c561fa4b4103a44c5b1",
.hash = "122012a11b37a350395a32fdb514e57ff54a0f9d8d4ce09498b6c45ffb7211232920", .hash = "12202405ca6dd40f314dba6472983fcbb388118ab7446d75065b1efb982d03f515d2",
.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, 1); try tpool.start(allocator, .{});
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,6 +25,7 @@ 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,6 +21,8 @@ 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

@ -49,6 +49,7 @@ pub fn main() !void {
}; };
const shell = env.get("SHELL") orelse "bash"; const shell = env.get("SHELL") orelse "bash";
const argv = [_][]const u8{shell}; const argv = [_][]const u8{shell};
std.debug.print("executing: {s}\n", .{shell});
var vt = try vaxis.widgets.Terminal.init( var vt = try vaxis.widgets.Terminal.init(
alloc, alloc,
&argv, &argv,
@ -61,9 +62,10 @@ pub fn main() !void {
var redraw: bool = false; var redraw: bool = false;
while (true) { while (true) {
std.time.sleep(8 * std.time.ns_per_ms); std.debug.print("inside while loop before resize\n", .{});
// try vt events first // try vt events first
while (vt.tryEvent()) |event| { while (vt.tryEvent()) |event| {
std.debug.print("inside stryEventloop \n", .{});
redraw = true; redraw = true;
switch (event) { switch (event) {
.bell => {}, .bell => {},
@ -74,6 +76,7 @@ pub fn main() !void {
} }
} }
while (loop.tryEvent()) |event| { while (loop.tryEvent()) |event| {
std.debug.print("inside loop.tryEvent\n", .{});
redraw = true; redraw = true;
switch (event) { switch (event) {
.key_press => |key| { .key_press => |key| {

View file

@ -6,6 +6,9 @@ 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,8 +6,6 @@ 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(.internal_screen); const log = std.log.scoped(.vaxis);
const InternalScreen = @This(); const InternalScreen = @This();

View file

@ -6,17 +6,18 @@ 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 Tty = @import("main.zig").Tty; const vaxis = @import("main.zig");
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,
@ -51,6 +52,8 @@ 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 {};
@ -90,6 +93,8 @@ 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")) {
@ -108,40 +113,12 @@ 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(); const event = try self.tty.nextEvent(&parser, paste_allocator);
switch (event) { try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
.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 => {
@ -178,102 +155,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;
switch (event) { try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
.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
}
} }
} }
}, },
@ -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 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(.parser); const log = std.log.scoped(.vaxis_parser);
const Parser = @This(); 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 { 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]) { 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 },
@ -166,7 +176,12 @@ inline fn parseSs3(input: []const u8) Result {
} }
inline fn parseApc(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 .{ const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null, .event = null,
.n = 0, .n = 0,
@ -187,11 +202,22 @@ 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 {
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 .{ 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,
@ -201,6 +227,12 @@ 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)
@ -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 { 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| { 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],
@ -470,6 +508,32 @@ 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,8 +8,6 @@ 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: i128 = 0, render_dur: u64 = 0,
render_timer: std.time.Timer, render_timer: std.time.Timer,
sgr: enum { sgr: enum {
@ -85,6 +85,7 @@ 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,
@ -151,6 +152,10 @@ 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
@ -244,6 +249,7 @@ 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 ++
@ -360,7 +366,9 @@ 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;
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 // If cell is the same as our last frame, we don't need to do
// anything // anything
@ -488,7 +496,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.bg) { switch (cell.style.ul) {
.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,8 +7,6 @@ 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) {
@ -311,6 +309,7 @@ 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,14 +3,9 @@ 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
@ -52,9 +47,11 @@ 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 (@hasField(Event, "winsize")) { if (builtin.target.os.tag != .windows) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; if (@hasField(Event, "winsize")) {
try vaxis.Tty.notifyWinsize(handler); const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler);
}
} }
while (true) { 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 // initialize a grapheme cache
var cache: vaxis.GraphemeCache = .{}; var cache: vaxis.GraphemeCache = .{};
@ -93,7 +115,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.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; 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);
@ -111,108 +133,16 @@ 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;
switch (event) { try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
.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 {
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}); if (err != error.Canceled) log.err("ttyReader: {}", .{err});
self.fatal = true; 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_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,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); 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,8 +3,6 @@ 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,8 +6,6 @@ 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

@ -17,6 +17,23 @@ pid: ?std.posix.pid_t = null,
env_map: *const std.process.EnvMap, env_map: *const std.process.EnvMap,
pty: Pty, pty: Pty,
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
// extern "c" fn setsid() std.c.pid_t;
const c = struct {
usingnamespace switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h");
@cInclude("unistd.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
};
pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void { pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
var arena_allocator = std.heap.ArenaAllocator.init(allocator); var arena_allocator = std.heap.ArenaAllocator.init(allocator);
@ -31,12 +48,26 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
const pid = try std.posix.fork(); const pid = try std.posix.fork();
if (pid == 0) { if (pid == 0) {
// we are the child std.posix.close(self.pty.pty);
_ = std.os.linux.setsid(); std.debug.print("inside child\n", .{});
if (c.setsid() != 0) return error.SetSid;
// set the controlling terminal std.debug.print("setting up io for tty\n", .{});
var u: c_uint = std.posix.STDIN_FILENO; var u: c_uint = std.posix.STDIN_FILENO;
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
if (c.ioctl(self.pty.tty, TIOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
// switch (builtin.os.tag) {
// .linux => {},
// .ios, .macos => {
// // Mac doesn't support dup3 so we use dup2. We purposely clear
// // CLO_ON_EXEC for this fd.
// const flags = try posix.fcntl(self.pty.tty, posix.F.GETFD, 0);
// if (flags & posix.FD_CLOEXEC != 0) {
// _ = try posix.fcntl(self.pty.tty, posix.F.SETFD, flags & ~@as(u32, posix.FD_CLOEXEC));
// }
// },
// else => @compileError("unsupported platform"),
// }
// set up io // set up io
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO); try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
@ -51,13 +82,17 @@ pub fn spawn(self: *Command, allocator: std.mem.Allocator) !void {
} }
// exec // exec
const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp); _ = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp) catch null;
_ = err catch {};
//return error.ExecFailed;
// const err = std.posix.execvpeZ("/bin/zsh", "/bin/zsh", envp);
// _ = err catch {};
} }
// we are the parent // we are the parent
self.pid = @intCast(pid); self.pid = @intCast(pid);
_ = posix.waitpid(pid, 0);
std.debug.print("waitpid in parent\n", .{});
if (!Terminal.global_sigchild_installed) { if (!Terminal.global_sigchild_installed) {
Terminal.global_sigchild_installed = true; Terminal.global_sigchild_installed = true;
var act = posix.Sigaction{ var act = posix.Sigaction{

View file

@ -7,6 +7,25 @@ const Winsize = @import("../../main.zig").Winsize;
const posix = std.posix; const posix = std.posix;
extern "c" fn setsid() std.c.pid_t;
const c = struct {
usingnamespace switch (builtin.os.tag) {
.macos => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("util.h"); // openpty()
}),
else => @cImport({
@cInclude("sys/ioctl.h"); // ioctl and constants
@cInclude("pty.h");
}),
};
};
const TIOCSCTTY = if (builtin.os.tag == .macos) 536900705 else c.TIOCSCTTY;
const TIOCSWINSZ = if (builtin.os.tag == .macos) 2148037735 else c.TIOCSWINSZ;
const TIOCGWINSZ = if (builtin.os.tag == .macos) 1074295912 else c.TIOCGWINSZ;
pty: posix.fd_t, pty: posix.fd_t,
tty: posix.fd_t, tty: posix.fd_t,
@ -14,6 +33,7 @@ tty: posix.fd_t,
pub fn init() !Pty { pub fn init() !Pty {
switch (builtin.os.tag) { switch (builtin.os.tag) {
.linux => return openPtyLinux(), .linux => return openPtyLinux(),
.macos => return openPtyMacos(),
else => @compileError("unsupported os"), else => @compileError("unsupported os"),
} }
} }
@ -32,10 +52,75 @@ pub fn setSize(self: Pty, ws: Winsize) !void {
.ws_xpixel = @truncate(ws.x_pixel), .ws_xpixel = @truncate(ws.x_pixel),
.ws_ypixel = @truncate(ws.y_pixel), .ws_ypixel = @truncate(ws.y_pixel),
}; };
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0) if (c.ioctl(self.pty, TIOCSWINSZ, @intFromPtr(&_ws)) != 0)
return error.SetWinsizeError; return error.SetWinsizeError;
} }
fn openPtyMacos2() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p);
// unlockpt
// var n: c_uint = 0;
if (c.ioctl(p, c.TIOCPTYUNLK) != 0) return error.IoctlError;
// ptsname
if (c.ioctl(p, c.TIOCPTYGRANT) != 0) return error.IoctlError;
var buf: [128]u8 = undefined;
// var buf2: [128]u8 = undefined;
if (c.ioctl(p, c.TIOCPTYGNAME, &buf) != 0) return error.IoctlError;
const sname = buf[0 .. 13 - 1];
// std.debug.print("sizeof buf: {d}", .{buf.len});
// const sname = try std.fmt.bufPrint(&buf2, "{s}", .{buf});
std.debug.print("slave name: {s}\n", .{sname});
// const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n});
// std.log.err("pts: {s}", .{sname});
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
std.debug.print("posix opened : {s}\n", .{sname});
var attrs: c.termios = undefined;
if (c.tcgetattr(p, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(p, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return .{
.pty = p,
.tty = t,
};
}
fn openPtyMacos() !Pty {
var master_fd: posix.fd_t = undefined;
var slave_fd: posix.fd_t = undefined;
if (c.openpty(
&master_fd,
&slave_fd,
null,
null,
null,
) < 0)
return error.OpenptyFailed;
errdefer {
_ = posix.system.close(master_fd);
_ = posix.system.close(slave_fd);
}
var attrs: c.termios = undefined;
if (c.tcgetattr(master_fd, &attrs) != 0)
return error.OpenptyFailed;
attrs.c_iflag |= c.IUTF8;
if (c.tcsetattr(master_fd, c.TCSANOW, &attrs) != 0)
return error.OpenptyFailed;
return .{
.pty = master_fd,
.tty = slave_fd,
};
}
fn openPtyLinux() !Pty { fn openPtyLinux() !Pty {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0); const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p); errdefer posix.close(p);

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(.terminal); const log = std.log.scoped(.vaxis_terminal);
const Screen = @This(); const Screen = @This();

View file

@ -94,8 +94,12 @@ pub fn init(
unicode: *const vaxis.Unicode, unicode: *const vaxis.Unicode,
opts: Options, opts: Options,
) !Terminal { ) !Terminal {
if (opts.initial_working_directory) |pwd| {
if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory;
}
const pty = try Pty.init(); const pty = try Pty.init();
try pty.setSize(opts.winsize); try pty.setSize(opts.winsize);
std.debug.print("set size done\n", .{});
const cmd: Command = .{ const cmd: Command = .{
.argv = argv, .argv = argv,
.env_map = env, .env_map = env,
@ -155,11 +159,22 @@ pub fn deinit(self: *Terminal) void {
} }
pub fn spawn(self: *Terminal) !void { pub fn spawn(self: *Terminal) !void {
std.debug.print("inside spawn", .{});
if (self.thread != null) return; if (self.thread != null) return;
self.back_screen = &self.back_screen_pri; self.back_screen = &self.back_screen_pri;
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();
@ -259,6 +274,7 @@ fn anyReader(self: *const Terminal) std.io.AnyReader {
/// process the output from the command on the pty /// process the output from the command on the pty
fn run(self: *Terminal) !void { fn run(self: *Terminal) !void {
std.debug.print("inside run\n", .{});
var parser: Parser = .{ var parser: Parser = .{
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128), .buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
}; };
@ -268,6 +284,7 @@ fn run(self: *Terminal) !void {
var reader = std.io.bufferedReader(self.anyReader()); var reader = std.io.bufferedReader(self.anyReader());
while (!self.should_quit) { while (!self.should_quit) {
std.debug.print("inside while loop\n", .{});
const event = try parser.parseReader(&reader); const event = try parser.parseReader(&reader);
self.back_mutex.lock(); self.back_mutex.lock();
defer self.back_mutex.unlock(); defer self.back_mutex.unlock();
@ -275,8 +292,10 @@ fn run(self: *Terminal) !void {
if (!self.dirty and self.event_queue.tryPush(.redraw)) if (!self.dirty and self.event_queue.tryPush(.redraw))
self.dirty = true; self.dirty = true;
std.debug.print("before switch event\n", .{});
switch (event) { switch (event) {
.print => |str| { .print => |str| {
std.debug.print("inside print event\n", .{});
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data); var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
while (iter.next()) |g| { while (iter.next()) |g| {
const gr = g.bytes(str); const gr = g.bytes(str);
@ -287,6 +306,7 @@ fn run(self: *Terminal) !void {
}, },
.c0 => |b| try self.handleC0(b), .c0 => |b| try self.handleC0(b),
.escape => |esc| { .escape => |esc| {
std.debug.print("inside escape event\n", .{});
const final = esc[esc.len - 1]; const final = esc[esc.len - 1];
switch (final) { switch (final) {
'B' => {}, // TODO: handle charsets 'B' => {}, // TODO: handle charsets
@ -700,6 +720,7 @@ fn run(self: *Terminal) !void {
} }
inline fn handleC0(self: *Terminal, b: ansi.C0) !void { inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
std.debug.print("inside handlec0\n", .{});
switch (b) { switch (b) {
.NUL, .SOH, .STX => {}, .NUL, .SOH, .STX => {},
.EOT => {}, // we send EOT to quit the read thread .EOT => {}, // we send EOT to quit the read thread
@ -716,6 +737,7 @@ inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
} }
pub fn setMode(self: *Terminal, mode: u16, val: bool) void { pub fn setMode(self: *Terminal, mode: u16, val: bool) void {
std.debug.print("inside setmode\n", .{});
switch (mode) { switch (mode) {
7 => self.mode.autowrap = val, 7 => self.mode.autowrap = val,
25 => self.mode.cursor = val, 25 => self.mode.cursor = val,

View file

@ -0,0 +1,5 @@
info(loop): pixel mouse capability detected
info(loop): unicode capability detected
info(loop): color_scheme_updates capability detected
info(loop): kitty keyboard capability detected
info(loop): kitty graphics capability detected

View file

@ -6,6 +6,7 @@ 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,
@ -117,288 +118,304 @@ 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) !Event { pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
// We use a loop so we can ignore certain events // We use a loop so we can ignore certain events
var ansi_buf: [128]u8 = undefined; var state: EventState = .{};
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());
switch (input_record.EventType) { if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
0x0001 => { // Key event return ev;
const event = input_record.Event.KeyEvent; }
}
}
const base_layout: u21 = switch (event.wVirtualKeyCode) { pub const EventState = struct {
0x00 => { // delivered when we get an escape sequence ansi_buf: [128]u8 = undefined,
ansi_buf[ansi_idx] = event.uChar.AsciiChar; ansi_idx: usize = 0,
ansi_idx += 1; utf16_buf: [2]u16 = undefined,
if (ansi_idx <= 2) { utf16_half: bool = false,
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,
};
var codepoint: u21 = base_layout; pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
var text: ?[]const u8 = null; switch (record.EventType) {
switch (event.uChar.UnicodeChar) { 0x0001 => { // Key event
0x00...0x1F => {}, const event = record.Event.KeyEvent;
else => |cp| {
codepoint = cp; if (state.utf16_half) half: {
const n = try std.unicode.utf8Encode(cp, &self.buf); state.utf16_half = false;
text = self.buf[0..n]; 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 = .{ const key: Key = .{
.codepoint = codepoint, .codepoint = codepoint,
.base_layout_codepoint = base_layout, .base_layout_codepoint = codepoint,
.mods = translateMods(event.dwControlKeyState), .mods = translateMods(event.dwControlKeyState),
.text = text, .text = self.buf[0..n],
}; };
switch (event.bKeyDown) { switch (event.bKeyDown) {
0 => return .{ .key_release = key }, 0 => return .{ .key_release = key },
else => return .{ .key_press = 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 if (std.unicode.utf16IsHighSurrogate(base_layout)) {
// is wheel_down state.utf16_buf[0] = base_layout;
// Low word represents button state state.utf16_half = true;
const mouse_wheel_direction: i16 = blk: { return null;
const wheelu32: u32 = event.dwButtonState >> 16; }
const wheelu16: u16 = @truncate(wheelu32); if (std.unicode.utf16IsLowSurrogate(base_layout)) {
break :blk @bitCast(wheelu16); return null;
}; }
const buttons: u16 = @truncate(event.dwButtonState); var codepoint: u21 = base_layout;
// save the current state when we are done var text: ?[]const u8 = null;
defer self.last_mouse_button_press = buttons; switch (event.uChar.UnicodeChar) {
const button_xor = self.last_mouse_button_press ^ buttons; 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 key: Key = .{
const btn: Mouse.Button = switch (button_xor) { .codepoint = codepoint,
0x0000 => blk: { .base_layout_codepoint = base_layout,
// Check wheel event .mods = translateMods(event.dwControlKeyState),
if (event.dwEventFlags & 0x0004 > 0) { .text = text,
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 switch (event.bKeyDown) {
// drag event. Find out which button is held down 0 => return .{ .key_release = key },
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) { else => return .{ .key_press = key },
event_type = .drag; }
if (buttons & 0x0001 > 0) break :blk .left; },
if (buttons & 0x0002 > 0) break :blk .right; 0x0002 => { // Mouse event
if (buttons & 0x0004 > 0) break :blk .middle; // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
if (buttons & 0x0008 > 0) break :blk .button_8;
if (buttons & 0x0010 > 0) break :blk .button_9;
}
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion; const event = record.Event.MouseEvent;
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 shift: u32 = 0x0010; // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
const alt: u32 = 0x0001 | 0x0002; // is wheel_down
const ctrl: u32 = 0x0004 | 0x0008; // Low word represents button state
const mods: Mouse.Modifiers = .{ const mouse_wheel_direction: i16 = blk: {
.shift = event.dwControlKeyState & shift > 0, const wheelu32: u32 = event.dwButtonState >> 16;
.alt = event.dwControlKeyState & alt > 0, const wheelu16: u16 = @truncate(wheelu32);
.ctrl = event.dwControlKeyState & ctrl > 0, break :blk @bitCast(wheelu16);
}; };
const mouse: Mouse = .{ const buttons: u16 = @truncate(event.dwButtonState);
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index // save the current state when we are done
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index defer self.last_mouse_button_press = buttons;
.mods = mods, const button_xor = self.last_mouse_button_press ^ buttons;
.type = event_type,
.button = btn, var event_type: Mouse.Type = .press;
}; const btn: Mouse.Button = switch (button_xor) {
return .{ .mouse = mouse }; 0x0000 => blk: {
}, // Check wheel event
0x0004 => { // Screen resize events if (event.dwEventFlags & 0x0004 > 0) {
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for if (mouse_wheel_direction > 0)
// the size directly when we get this event break :blk .wheel_up
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined; else
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) { break :blk .wheel_down;
return windows.unexpectedError(windows.kernel32.GetLastError()); }
}
const window_rect = console_info.srWindow; // If we have no change but one of the buttons is still pressed we have a
const width = window_rect.Right - window_rect.Left + 1; // drag event. Find out which button is held down
const height = window_rect.Bottom - window_rect.Top + 1; if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
return .{ event_type = .drag;
.winsize = .{ if (buttons & 0x0001 > 0) break :blk .left;
.cols = @intCast(width), if (buttons & 0x0002 > 0) break :blk .right;
.rows = @intCast(height), if (buttons & 0x0004 > 0) break :blk .middle;
.x_pixel = 0, if (buttons & 0x0008 > 0) break :blk .button_8;
.y_pixel = 0, if (buttons & 0x0010 > 0) break :blk .button_9;
}, }
};
}, if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
0x0010 => { // Focus events break :blk .none;
switch (input_record.Event.FocusEvent.bSetFocus) { },
0 => return .focus_out, 0x0001 => blk: {
else => return .focus_in, if (buttons & 0x0001 == 0) event_type = .release;
} break :blk .left;
}, },
else => {}, 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 { 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(.tty_watcher); const log = std.log.scoped(.vaxis_xev);
pub const Event = union(enum) { pub const Event = union(enum) {
key_press: Key, key_press: Key,
@ -99,8 +99,6 @@ 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 {