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) | ✅ |
| Unicode Core (DEC 2027) | ✅ |
| Color Mode Updates (DEC 2031) | ✅ |
| [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) | ✅ |
| Images (kitty) | ✅ |
## Usage

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,17 +6,18 @@ const grapheme = @import("grapheme");
const GraphemeCache = @import("GraphemeCache.zig");
const Parser = @import("Parser.zig");
const Queue = @import("queue.zig").Queue;
const Tty = @import("main.zig").Tty;
const vaxis = @import("main.zig");
const Tty = vaxis.Tty;
const Vaxis = @import("Vaxis.zig");
const log = std.log.scoped(.vaxis);
pub fn Loop(comptime T: type) type {
return struct {
const Self = @This();
const Event = T;
const log = std.log.scoped(.loop);
tty: *Tty,
vaxis: *Vaxis,
@ -51,6 +52,8 @@ pub fn Loop(comptime T: type) type {
/// stops reading from the tty.
pub fn stop(self: *Self) void {
// If we don't have a thread, we have nothing to stop
if (self.thread == null) return;
self.should_quit = true;
// trigger a read
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
@ -90,6 +93,8 @@ pub fn Loop(comptime T: type) type {
pub fn winsizeCallback(ptr: *anyopaque) void {
const self: *Self = @ptrCast(@alignCast(ptr));
// We will be receiving winsize updates in-band
if (self.vaxis.state.in_band_resize) return;
const winsize = Tty.getWinsize(self.tty.fd) catch return;
if (@hasField(Event, "winsize")) {
@ -108,40 +113,12 @@ pub fn Loop(comptime T: type) type {
switch (builtin.os.tag) {
.windows => {
var parser: Parser = .{
.grapheme_data = grapheme_data,
};
while (!self.should_quit) {
const event = try self.tty.nextEvent();
switch (event) {
.winsize => |ws| {
if (@hasField(Event, "winsize")) {
self.postEvent(.{ .winsize = ws });
}
},
.key_press => |key| {
if (@hasField(Event, "key_press")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
self.postEvent(.{ .key_press = mut_key });
}
},
.key_release => |*key| {
if (@hasField(Event, "key_release")) {
// HACK: yuck. there has to be a better way
var mut_key = key;
if (key.text) |text| {
mut_key.text = cache.put(text);
}
self.postEvent(.{ .key_release = mut_key });
}
},
.cap_da1 => {
std.Thread.Futex.wake(&self.vaxis.query_futex, 10);
},
.mouse => {}, // Unsupported currently
else => {},
}
const event = try self.tty.nextEvent(&parser, paste_allocator);
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
}
},
else => {
@ -178,7 +155,24 @@ pub fn Loop(comptime T: type) type {
seq_start += result.n;
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) {
.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
@ -186,7 +180,7 @@ pub fn Loop(comptime T: type) type {
if (key.text) |text| {
mut_key.text = cache.put(text);
}
self.postEvent(.{ .key_press = mut_key });
return self.postEvent(.{ .key_press = mut_key });
}
},
.key_release => |*key| {
@ -196,37 +190,66 @@ pub fn Loop(comptime T: type) type {
if (key.text) |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| {
if (@hasField(Event, "mouse")) {
self.postEvent(.{ .mouse = self.vaxis.translateMouse(mouse) });
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
}
},
.focus_in => {
if (@hasField(Event, "focus_in")) {
self.postEvent(.focus_in);
return self.postEvent(.focus_in);
}
},
.focus_out => {
if (@hasField(Event, "focus_out")) {
self.postEvent(.focus_out);
return self.postEvent(.focus_out);
}
},
.paste_start => {
if (@hasField(Event, "paste_start")) {
self.postEvent(.paste_start);
return self.postEvent(.paste_start);
}
},
.paste_end => {
if (@hasField(Event, "paste_end")) {
self.postEvent(.paste_end);
return self.postEvent(.paste_end);
}
},
.paste => |text| {
if (@hasField(Event, "paste")) {
self.postEvent(.{ .paste = text });
return self.postEvent(.{ .paste = text });
} else {
if (paste_allocator) |_|
paste_allocator.?.free(text);
@ -234,50 +257,51 @@ pub fn Loop(comptime T: type) type {
},
.color_report => |report| {
if (@hasField(Event, "color_report")) {
self.postEvent(.{ .color_report = report });
return self.postEvent(.{ .color_report = report });
}
},
.color_scheme => |scheme| {
if (@hasField(Event, "color_scheme")) {
self.postEvent(.{ .color_scheme = scheme });
return self.postEvent(.{ .color_scheme = scheme });
}
},
.cap_kitty_keyboard => {
log.info("kitty keyboard capability detected", .{});
self.vaxis.caps.kitty_keyboard = true;
vx.caps.kitty_keyboard = true;
},
.cap_kitty_graphics => {
if (!self.vaxis.caps.kitty_graphics) {
if (!vx.caps.kitty_graphics) {
log.info("kitty graphics capability detected", .{});
self.vaxis.caps.kitty_graphics = true;
vx.caps.kitty_graphics = true;
}
},
.cap_rgb => {
log.info("rgb capability detected", .{});
self.vaxis.caps.rgb = true;
vx.caps.rgb = true;
},
.cap_unicode => {
log.info("unicode capability detected", .{});
self.vaxis.caps.unicode = .unicode;
self.vaxis.screen.width_method = .unicode;
vx.caps.unicode = .unicode;
vx.screen.width_method = .unicode;
},
.cap_sgr_pixels => {
log.info("pixel mouse capability detected", .{});
self.vaxis.caps.sgr_pixels = true;
vx.caps.sgr_pixels = true;
},
.cap_color_scheme_updates => {
log.info("color_scheme_updates capability detected", .{});
self.vaxis.caps.color_scheme_updates = true;
vx.caps.color_scheme_updates = true;
},
.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 code_point = @import("code_point");
const grapheme = @import("grapheme");
const Winsize = @import("main.zig").Winsize;
const log = std.log.scoped(.parser);
const log = std.log.scoped(.vaxis_parser);
const Parser = @This();
@ -138,8 +139,17 @@ inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Re
}
inline fn parseSs3(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const key: Key = switch (input[2]) {
0x1B => return .{
.event = null,
.n = 2,
},
'A' => .{ .codepoint = Key.up },
'B' => .{ .codepoint = Key.down },
'C' => .{ .codepoint = Key.right },
@ -166,7 +176,12 @@ inline fn parseSs3(input: []const u8) Result {
}
inline fn parseApc(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
@ -187,11 +202,22 @@ inline fn parseApc(input: []const u8) Result {
/// Skips sequences until we see an ST (String Terminator, ESC \)
inline fn skipUntilST(input: []const u8) Result {
std.debug.assert(input.len >= 3);
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
const end = std.mem.indexOfScalarPos(u8, input, 2, 0x1b) orelse return .{
.event = null,
.n = 0,
};
if (input.len < end + 1 + 1) {
return .{
.event = null,
.n = 0,
};
}
const sequence = input[0 .. end + 1 + 1];
return .{
.event = null,
@ -201,6 +227,12 @@ inline fn skipUntilST(input: []const u8) Result {
/// Parses an OSC sequence
inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Result {
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
var bel_terminated: bool = false;
// end is the index of the terminating byte(s) (either the last byte of an
// ST or BEL)
@ -288,7 +320,13 @@ inline fn parseOsc(input: []const u8, paste_allocator: ?std.mem.Allocator) !Resu
}
inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
// We start iterating at index 2 to get past te '['
if (input.len < 3) {
return .{
.event = null,
.n = 0,
};
}
// We start iterating at index 2 to get past the '['
const sequence = for (input[2..], 2..) |b, i| {
switch (b) {
0x40...0xFF => break input[0 .. i + 1],
@ -470,6 +508,32 @@ inline fn parseCsi(input: []const u8, text_buf: []u8) Result {
else => return null_event,
}
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (std.mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const height_char = iter.next() orelse return null_event;
const width_char = iter.next() orelse return null_event;
const height_pix = iter.next() orelse "0";
const width_pix = iter.next() orelse "0";
const winsize: Winsize = .{
.rows = std.fmt.parseUnsigned(usize, height_char, 10) catch return null_event,
.cols = std.fmt.parseUnsigned(usize, width_char, 10) catch return null_event,
.x_pixel = std.fmt.parseUnsigned(usize, width_pix, 10) catch return null_event,
.y_pixel = std.fmt.parseUnsigned(usize, height_pix, 10) catch return null_event,
};
return .{
.event = .{ .winsize = winsize },
.n = sequence.len,
};
}
return null_event;
},
'u' => {
// Kitty keyboard
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u

View file

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

View file

@ -68,7 +68,7 @@ unicode: Unicode,
// statistics
renders: usize = 0,
render_dur: i128 = 0,
render_dur: u64 = 0,
render_timer: std.time.Timer,
sgr: enum {
@ -85,6 +85,7 @@ state: struct {
mouse: bool = false,
pixel_mouse: bool = false,
color_scheme_updates: bool = false,
in_band_resize: bool = false,
cursor: struct {
row: usize = 0,
col: usize = 0,
@ -151,6 +152,10 @@ pub fn resetState(self: *Vaxis, tty: AnyWriter) !void {
try tty.writeAll(ctlseqs.color_scheme_reset);
self.state.color_scheme_updates = false;
}
if (self.state.in_band_resize) {
try tty.writeAll(ctlseqs.in_band_resize_reset);
self.state.in_band_resize = false;
}
}
/// resize allocates a slice of cells equal to the number of cells
@ -244,6 +249,7 @@ pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
try tty.writeAll(ctlseqs.decrqm_sgr_pixels ++
ctlseqs.decrqm_unicode ++
ctlseqs.decrqm_color_scheme ++
ctlseqs.in_band_resize_set ++
ctlseqs.xtversion ++
ctlseqs.csi_u_query ++
ctlseqs.kitty_graphics_query ++
@ -360,6 +366,8 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
if (col >= self.screen.width) {
row += 1;
col = 0;
// 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
@ -488,7 +496,7 @@ pub fn render(self: *Vaxis, tty: AnyWriter) !void {
}
// underline color
if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
switch (cell.style.bg) {
switch (cell.style.ul) {
.default => try tty.writeAll(ctlseqs.ul_reset),
.index => |idx| {
switch (self.sgr) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,6 +17,23 @@ pid: ?std.posix.pid_t = null,
env_map: *const std.process.EnvMap,
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 {
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();
if (pid == 0) {
// we are the child
_ = std.os.linux.setsid();
// set the controlling terminal
std.posix.close(self.pty.pty);
std.debug.print("inside child\n", .{});
if (c.setsid() != 0) return error.SetSid;
std.debug.print("setting up io for tty\n", .{});
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
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
const err = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp);
_ = err catch {};
_ = std.posix.execvpeZ(argv_buf.ptr[0].?, argv_buf.ptr, envp) catch null;
//return error.ExecFailed;
// const err = std.posix.execvpeZ("/bin/zsh", "/bin/zsh", envp);
// _ = err catch {};
}
// we are the parent
self.pid = @intCast(pid);
_ = posix.waitpid(pid, 0);
std.debug.print("waitpid in parent\n", .{});
if (!Terminal.global_sigchild_installed) {
Terminal.global_sigchild_installed = true;
var act = posix.Sigaction{

View file

@ -7,6 +7,25 @@ const Winsize = @import("../../main.zig").Winsize;
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,
tty: posix.fd_t,
@ -14,6 +33,7 @@ tty: posix.fd_t,
pub fn init() !Pty {
switch (builtin.os.tag) {
.linux => return openPtyLinux(),
.macos => return openPtyMacos(),
else => @compileError("unsupported os"),
}
}
@ -32,10 +52,75 @@ pub fn setSize(self: Pty, ws: Winsize) !void {
.ws_xpixel = @truncate(ws.x_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;
}
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 {
const p = try posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
errdefer posix.close(p);

View file

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

View file

@ -94,8 +94,12 @@ pub fn init(
unicode: *const vaxis.Unicode,
opts: Options,
) !Terminal {
if (opts.initial_working_directory) |pwd| {
if (!std.fs.path.isAbsolute(pwd)) return error.InvalidWorkingDirectory;
}
const pty = try Pty.init();
try pty.setSize(opts.winsize);
std.debug.print("set size done\n", .{});
const cmd: Command = .{
.argv = argv,
.env_map = env,
@ -155,11 +159,22 @@ pub fn deinit(self: *Terminal) void {
}
pub fn spawn(self: *Terminal) !void {
std.debug.print("inside spawn", .{});
if (self.thread != null) return;
self.back_screen = &self.back_screen_pri;
try self.cmd.spawn(self.allocator);
self.working_directory.clearRetainingCapacity();
if (self.cmd.working_directory) |pwd| {
try self.working_directory.appendSlice(pwd);
} else {
const pwd = std.fs.cwd();
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
try self.working_directory.appendSlice(out_path);
}
{
// add to our global list
global_vt_mutex.lock();
@ -259,6 +274,7 @@ fn anyReader(self: *const Terminal) std.io.AnyReader {
/// process the output from the command on the pty
fn run(self: *Terminal) !void {
std.debug.print("inside run\n", .{});
var parser: Parser = .{
.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());
while (!self.should_quit) {
std.debug.print("inside while loop\n", .{});
const event = try parser.parseReader(&reader);
self.back_mutex.lock();
defer self.back_mutex.unlock();
@ -275,8 +292,10 @@ fn run(self: *Terminal) !void {
if (!self.dirty and self.event_queue.tryPush(.redraw))
self.dirty = true;
std.debug.print("before switch event\n", .{});
switch (event) {
.print => |str| {
std.debug.print("inside print event\n", .{});
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
while (iter.next()) |g| {
const gr = g.bytes(str);
@ -287,6 +306,7 @@ fn run(self: *Terminal) !void {
},
.c0 => |b| try self.handleC0(b),
.escape => |esc| {
std.debug.print("inside escape event\n", .{});
const final = esc[esc.len - 1];
switch (final) {
'B' => {}, // TODO: handle charsets
@ -700,6 +720,7 @@ fn run(self: *Terminal) !void {
}
inline fn handleC0(self: *Terminal, b: ansi.C0) !void {
std.debug.print("inside handlec0\n", .{});
switch (b) {
.NUL, .SOH, .STX => {},
.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 {
std.debug.print("inside setmode\n", .{});
switch (mode) {
7 => self.mode.autowrap = 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 Key = @import("../Key.zig");
const Mouse = @import("../Mouse.zig");
const Parser = @import("../Parser.zig");
const windows = std.os.windows;
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());
}
pub fn nextEvent(self: *Tty) !Event {
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
// We use a loop so we can ignore certain events
var ansi_buf: [128]u8 = undefined;
var ansi_idx: usize = 0;
var escape_st: bool = false;
var state: EventState = .{};
while (true) {
var event_count: u32 = 0;
var input_record: INPUT_RECORD = undefined;
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
return windows.unexpectedError(windows.kernel32.GetLastError());
switch (input_record.EventType) {
0x0001 => { // Key event
const event = input_record.Event.KeyEvent;
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
return ev;
}
}
}
const base_layout: u21 = switch (event.wVirtualKeyCode) {
0x00 => { // delivered when we get an escape sequence
ansi_buf[ansi_idx] = event.uChar.AsciiChar;
ansi_idx += 1;
if (ansi_idx <= 2) {
continue;
pub const EventState = struct {
ansi_buf: [128]u8 = undefined,
ansi_idx: usize = 0,
utf16_buf: [2]u16 = undefined,
utf16_half: bool = false,
};
pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
switch (record.EventType) {
0x0001 => { // Key event
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,
0x09 => Key.tab,
@ -257,16 +265,25 @@ pub fn nextEvent(self: *Tty) !Event {
0xdc => '\\',
0xdd => ']',
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 text: ?[]const u8 = null;
switch (event.uChar.UnicodeChar) {
0x00...0x1F => {},
else => |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];
},
}
@ -286,7 +303,7 @@ pub fn nextEvent(self: *Tty) !Event {
0x0002 => { // Mouse event
// 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
// is wheel_down
@ -349,7 +366,7 @@ pub fn nextEvent(self: *Tty) !Event {
},
else => {
std.log.warn("unknown mouse event: {}", .{event});
continue;
return null;
},
};
@ -391,14 +408,14 @@ pub fn nextEvent(self: *Tty) !Event {
};
},
0x0010 => { // Focus events
switch (input_record.Event.FocusEvent.bSetFocus) {
switch (record.Event.FocusEvent.bSetFocus) {
0 => return .focus_out,
else => return .focus_in,
}
},
else => {},
}
}
return null;
}
fn translateMods(mods: u32) Key.Modifiers {

View file

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