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) | ✅ | | 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

@ -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,7 +155,24 @@ pub fn Loop(comptime T: type) type {
seq_start += result.n; seq_start += result.n;
const event = result.event orelse continue; const event = result.event orelse continue;
try handleEventGeneric(self, self.vaxis, &cache, Event, event, paste_allocator);
}
}
},
}
}
};
}
pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Event: type, event: anytype, paste_allocator: ?std.mem.Allocator) !void {
switch (builtin.os.tag) {
.windows => {
switch (event) { switch (event) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
return self.postEvent(.{ .winsize = ws });
}
},
.key_press => |key| { .key_press => |key| {
if (@hasField(Event, "key_press")) { if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way // HACK: yuck. there has to be a better way
@ -186,7 +180,7 @@ pub fn Loop(comptime T: type) type {
if (key.text) |text| { if (key.text) |text| {
mut_key.text = cache.put(text); mut_key.text = cache.put(text);
} }
self.postEvent(.{ .key_press = mut_key }); return self.postEvent(.{ .key_press = mut_key });
} }
}, },
.key_release => |*key| { .key_release => |*key| {
@ -196,37 +190,66 @@ pub fn Loop(comptime T: type) type {
if (key.text) |text| { if (key.text) |text| {
mut_key.text = cache.put(text); mut_key.text = cache.put(text);
} }
self.postEvent(.{ .key_release = mut_key }); 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| { .mouse => |mouse| {
if (@hasField(Event, "mouse")) { if (@hasField(Event, "mouse")) {
self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) }); return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
} }
}, },
.focus_in => { .focus_in => {
if (@hasField(Event, "focus_in")) { if (@hasField(Event, "focus_in")) {
self.postEvent(.focus_in); return self.postEvent(.focus_in);
} }
}, },
.focus_out => { .focus_out => {
if (@hasField(Event, "focus_out")) { if (@hasField(Event, "focus_out")) {
self.postEvent(.focus_out); return self.postEvent(.focus_out);
} }
}, },
.paste_start => { .paste_start => {
if (@hasField(Event, "paste_start")) { if (@hasField(Event, "paste_start")) {
self.postEvent(.paste_start); return self.postEvent(.paste_start);
} }
}, },
.paste_end => { .paste_end => {
if (@hasField(Event, "paste_end")) { if (@hasField(Event, "paste_end")) {
self.postEvent(.paste_end); return self.postEvent(.paste_end);
} }
}, },
.paste => |text| { .paste => |text| {
if (@hasField(Event, "paste")) { if (@hasField(Event, "paste")) {
self.postEvent(.{ .paste = text }); return self.postEvent(.{ .paste = text });
} else { } else {
if (paste_allocator) |_| if (paste_allocator) |_|
paste_allocator.?.free(text); paste_allocator.?.free(text);
@ -234,50 +257,51 @@ pub fn Loop(comptime T: type) type {
}, },
.color_report => |report| { .color_report => |report| {
if (@hasField(Event, "color_report")) { if (@hasField(Event, "color_report")) {
self.postEvent(.{ .color_report = report }); return self.postEvent(.{ .color_report = report });
} }
}, },
.color_scheme => |scheme| { .color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) { if (@hasField(Event, "color_scheme")) {
self.postEvent(.{ .color_scheme = scheme }); return self.postEvent(.{ .color_scheme = scheme });
} }
}, },
.cap_kitty_keyboard => { .cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{}); log.info("kitty keyboard capability detected", .{});
self.vaxis.caps.kitty_keyboard = true; vx.caps.kitty_keyboard = true;
}, },
.cap_kitty_graphics => { .cap_kitty_graphics => {
if (!self.vaxis.caps.kitty_graphics) { if (!vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{}); log.info("kitty graphics capability detected", .{});
self.vaxis.caps.kitty_graphics = true; vx.caps.kitty_graphics = true;
} }
}, },
.cap_rgb => { .cap_rgb => {
log.info("rgb capability detected", .{}); log.info("rgb capability detected", .{});
self.vaxis.caps.rgb = true; vx.caps.rgb = true;
}, },
.cap_unicode => { .cap_unicode => {
log.info("unicode capability detected", .{}); log.info("unicode capability detected", .{});
self.vaxis.caps.unicode = .unicode; vx.caps.unicode = .unicode;
self.vaxis.screen.width_method = .unicode; vx.screen.width_method = .unicode;
}, },
.cap_sgr_pixels => { .cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{}); log.info("pixel mouse capability detected", .{});
self.vaxis.caps.sgr_pixels = true; vx.caps.sgr_pixels = true;
}, },
.cap_color_scheme_updates => { .cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{}); log.info("color_scheme_updates capability detected", .{});
self.vaxis.caps.color_scheme_updates = true; vx.caps.color_scheme_updates = true;
}, },
.cap_da1 => { .cap_da1 => {
std.Thread.Futex.wake(&self.vaxis.query_futex, 10); std.Thread.Futex.wake(&vx.query_futex, 10);
}, },
.winsize => unreachable, // handled elsewhere for posix .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,6 +366,8 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
if (col >= self.screen.width) { if (col >= self.screen.width) {
row += 1; row += 1;
col = 0; col = 0;
// Rely on terminal wrapping to reposition into next row instead of forcing it
if (!cell.wrapped)
reposition = true; reposition = true;
} }
// If cell is the same as our last frame, we don't need to do // If cell is the same as our last frame, we don't need to do
@ -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,10 +47,12 @@ pub fn Loop(comptime T: type) type {
// keep on stack // keep on stack
var ctx: Context = .{ .loop = self, .tty = tty }; var ctx: Context = .{ .loop = self, .tty = tty };
if (builtin.target.os.tag != .windows) {
if (@hasField(Event, "winsize")) { if (@hasField(Event, "winsize")) {
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
try vaxis.Tty.notifyWinsize(handler); try vaxis.Tty.notifyWinsize(handler);
} }
}
while (true) { while (true) {
try coro.io.single(aio.WaitEventSource{ .source = &self.source }); try coro.io.single(aio.WaitEventSource{ .source = &self.source });
@ -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

@ -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

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

View file

@ -6,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,57 +118,64 @@ 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; };
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 = codepoint,
.mods = translateMods(event.dwControlKeyState),
.text = self.buf[0..n],
};
switch (event.bKeyDown) {
0 => return .{ .key_release = key },
else => return .{ .key_press = key },
} }
switch (ansi_buf[1]) {
'[' => { // CSI, read until 0x40 to 0xFF
switch (event.uChar.AsciiChar) {
0x40...0xFF => {
return .cap_da1;
},
else => continue,
}
},
']' => { // OSC, read until ESC \ or BEL
switch (event.uChar.AsciiChar) {
0x07 => {
return .cap_da1;
},
0x1B => {
escape_st = true;
continue;
},
'\\' => {
if (escape_st) {
return .cap_da1;
}
continue;
},
else => continue,
}
},
else => continue,
} }
const base_layout: u16 = switch (event.wVirtualKeyCode) {
0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
break :blk event.uChar.UnicodeChar;
state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
state.ansi_idx += 1;
if (state.ansi_idx <= 2) return null;
const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
return if (result.n == 0) null else evt: {
state.ansi_idx = 0;
break :evt result.event;
};
}, },
0x08 => Key.backspace, 0x08 => Key.backspace,
0x09 => Key.tab, 0x09 => Key.tab,
@ -257,16 +265,25 @@ pub fn nextEvent(self: *Tty) !Event {
0xdc => '\\', 0xdc => '\\',
0xdd => ']', 0xdd => ']',
0xde => '\'', 0xde => '\'',
else => continue, else => return null,
}; };
if (std.unicode.utf16IsHighSurrogate(base_layout)) {
state.utf16_buf[0] = base_layout;
state.utf16_half = true;
return null;
}
if (std.unicode.utf16IsLowSurrogate(base_layout)) {
return null;
}
var codepoint: u21 = base_layout; var codepoint: u21 = base_layout;
var text: ?[]const u8 = null; var text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) { switch (event.uChar.UnicodeChar) {
0x00...0x1F => {}, 0x00...0x1F => {},
else => |cp| { else => |cp| {
codepoint = cp; codepoint = cp;
const n = try std.unicode.utf8Encode(cp, &self.buf); const n = try std.unicode.utf8Encode(codepoint, &self.buf);
text = self.buf[0..n]; text = self.buf[0..n];
}, },
} }
@ -286,7 +303,7 @@ pub fn nextEvent(self: *Tty) !Event {
0x0002 => { // Mouse event 0x0002 => { // Mouse event
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str // see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
const event = input_record.Event.MouseEvent; const event = record.Event.MouseEvent;
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative // High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
// is wheel_down // is wheel_down
@ -349,7 +366,7 @@ pub fn nextEvent(self: *Tty) !Event {
}, },
else => { else => {
std.log.warn("unknown mouse event: {}", .{event}); std.log.warn("unknown mouse event: {}", .{event});
continue; return null;
}, },
}; };
@ -391,14 +408,14 @@ pub fn nextEvent(self: *Tty) !Event {
}; };
}, },
0x0010 => { // Focus events 0x0010 => { // Focus events
switch (input_record.Event.FocusEvent.bSetFocus) { switch (record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out, 0 => return .focus_out,
else => return .focus_in, else => return .focus_in,
} }
}, },
else => {}, else => {},
} }
} return null;
} }
fn translateMods(mods: u32) Key.Modifiers { fn translateMods(mods: u32) Key.Modifiers {

View file

@ -9,7 +9,7 @@ const Key = @import("Key.zig");
const Mouse = @import("Mouse.zig"); const Mouse = @import("Mouse.zig");
const Color = @import("Cell.zig").Color; const Color = @import("Cell.zig").Color;
const log = std.log.scoped(.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 {