this repo has no description
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

vaxis: add aio event loop and example

disabled by default, aio/coro might not be that mature yet.
however, appreciated if people give it a go and report issues.

authored by

Jari Vetoniemi and committed by
Tim Culverhouse
b84f9e58 935c5a54

+456
+11
build.zig
··· 4 4 const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true; 5 5 const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true; 6 6 const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true; 7 + const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false; 7 8 8 9 const options = b.addOptions(); 9 10 options.addOption(bool, "libxev", include_libxev); 10 11 options.addOption(bool, "images", include_images); 11 12 options.addOption(bool, "text_input", include_text_input); 13 + options.addOption(bool, "aio", include_aio); 12 14 13 15 const options_mod = options.createModule(); 14 16 ··· 30 32 .target = target, 31 33 }) else null; 32 34 const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{ 35 + .optimize = optimize, 36 + .target = target, 37 + }) else null; 38 + const aio_dep = if (include_aio) b.lazyDependency("aio", .{ 33 39 .optimize = optimize, 34 40 .target = target, 35 41 }) else null; ··· 46 52 if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg")); 47 53 if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer")); 48 54 if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev")); 55 + if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio")); 56 + if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro")); 49 57 vaxis_mod.addImport("build_options", options_mod); 50 58 51 59 // Examples ··· 59 67 vaxis, 60 68 vt, 61 69 xev, 70 + aio, 62 71 }; 63 72 const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input; 64 73 const example_step = b.step("example", "Run example"); ··· 73 82 }); 74 83 example.root_module.addImport("vaxis", vaxis_mod); 75 84 if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev")); 85 + if (aio_dep) |dep| example.root_module.addImport("aio", dep.module("aio")); 86 + if (aio_dep) |dep| example.root_module.addImport("coro", dep.module("coro")); 76 87 77 88 const example_run = b.addRunArtifact(example); 78 89 example_step.dependOn(&example_run.step);
+5
build.zig.zon
··· 22 22 .hash = "12207b7a5b538ffb7fb18f954ae17d2f8490b6e3778a9e30564ad82c58ee8da52361", 23 23 .lazy = true, 24 24 }, 25 + .aio = .{ 26 + .url = "git+https://github.com/Cloudef/zig-aio#be8e2b374bf223202090e282447fa4581029c2eb", 27 + .hash = "122012a11b37a350395a32fdb514e57ff54a0f9d8d4ce09498b6c45ffb7211232920", 28 + .lazy = true, 29 + }, 25 30 }, 26 31 .paths = .{ 27 32 "LICENSE",
+171
examples/aio.zig
··· 1 + const builtin = @import("builtin"); 2 + const std = @import("std"); 3 + const vaxis = @import("vaxis"); 4 + const aio = @import("aio"); 5 + const coro = @import("coro"); 6 + 7 + pub const panic = vaxis.panic_handler; 8 + 9 + const Event = union(enum) { 10 + key_press: vaxis.Key, 11 + winsize: vaxis.Winsize, 12 + }; 13 + 14 + const Loop = vaxis.aio.Loop(Event); 15 + 16 + const Video = enum { no_state, ready, end }; 17 + const Audio = enum { no_state, ready, end }; 18 + 19 + fn downloadTask(allocator: std.mem.Allocator, url: []const u8) ![]const u8 { 20 + var client: std.http.Client = .{ .allocator = allocator }; 21 + defer client.deinit(); 22 + var body = std.ArrayList(u8).init(allocator); 23 + _ = try client.fetch(.{ 24 + .location = .{ .url = url }, 25 + .response_storage = .{ .dynamic = &body }, 26 + .max_append_size = 1.6e+7, 27 + }); 28 + return try body.toOwnedSlice(); 29 + } 30 + 31 + fn audioTask(allocator: std.mem.Allocator) !void { 32 + errdefer coro.yield(Audio.end) catch {}; 33 + 34 + // var child = std.process.Child.init(&.{ "aplay", "-Dplug:default", "-q", "-f", "S16_LE", "-r", "8000" }, allocator); 35 + var child = std.process.Child.init(&.{ "mpv", "--audio-samplerate=16000", "--audio-channels=mono", "--audio-format=s16", "-" }, allocator); 36 + child.stdin_behavior = .Pipe; 37 + child.stdout_behavior = .Ignore; 38 + child.stderr_behavior = .Ignore; 39 + child.spawn() catch return; // no sound 40 + defer _ = child.kill() catch {}; 41 + 42 + const sound = blk: { 43 + var tpool: coro.ThreadPool = .{}; 44 + try tpool.start(allocator, 1); 45 + defer tpool.deinit(); 46 + break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" }); 47 + }; 48 + defer allocator.free(sound); 49 + 50 + try coro.yield(Audio.ready); 51 + 52 + var audio_off: usize = 0; 53 + while (audio_off < sound.len) { 54 + var written: usize = 0; 55 + try coro.io.single(aio.Write{ .file = child.stdin.?, .buffer = sound[audio_off..], .out_written = &written }); 56 + audio_off += written; 57 + } 58 + 59 + // the audio is already fed to the player and the defer 60 + // would kill the child stay here chilling 61 + coro.yield(Audio.end) catch {}; 62 + } 63 + 64 + fn videoTask(writer: std.io.AnyWriter) !void { 65 + defer coro.yield(Video.end) catch {}; 66 + 67 + var socket: std.posix.socket_t = undefined; 68 + try coro.io.single(aio.Socket{ 69 + .domain = std.posix.AF.INET, 70 + .flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC, 71 + .protocol = std.posix.IPPROTO.TCP, 72 + .out_socket = &socket, 73 + }); 74 + defer std.posix.close(socket); 75 + 76 + const address = std.net.Address.initIp4(.{ 44, 224, 41, 160 }, 1987); 77 + try coro.io.single(aio.Connect{ 78 + .socket = socket, 79 + .addr = &address.any, 80 + .addrlen = address.getOsSockLen(), 81 + }); 82 + 83 + try coro.yield(Video.ready); 84 + 85 + var buf: [1024]u8 = undefined; 86 + while (true) { 87 + var read: usize = 0; 88 + try coro.io.single(aio.Recv{ .socket = socket, .buffer = &buf, .out_read = &read }); 89 + if (read == 0) break; 90 + _ = try writer.write(buf[0..read]); 91 + } 92 + } 93 + 94 + fn loadingTask(vx: *vaxis.Vaxis, writer: std.io.AnyWriter) !void { 95 + var color_idx: u8 = 30; 96 + var dir: enum { up, down } = .up; 97 + 98 + while (true) { 99 + try coro.io.single(aio.Timeout{ .ns = 8 * std.time.ns_per_ms }); 100 + 101 + const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ color_idx, color_idx, color_idx } } }; 102 + const segment: vaxis.Segment = .{ .text = vaxis.logo, .style = style }; 103 + 104 + const win = vx.window(); 105 + win.clear(); 106 + 107 + var loc = vaxis.widgets.alignment.center(win, 28, 4); 108 + _ = try loc.printSegment(segment, .{ .wrap = .grapheme }); 109 + 110 + switch (dir) { 111 + .up => { 112 + color_idx += 1; 113 + if (color_idx == 255) dir = .down; 114 + }, 115 + .down => { 116 + color_idx -= 1; 117 + if (color_idx == 30) dir = .up; 118 + }, 119 + } 120 + 121 + try vx.render(writer); 122 + } 123 + } 124 + 125 + pub fn main() !void { 126 + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 127 + defer _ = gpa.deinit(); 128 + const allocator = gpa.allocator(); 129 + 130 + var tty = try vaxis.Tty.init(); 131 + defer tty.deinit(); 132 + 133 + var vx = try vaxis.init(allocator, .{}); 134 + defer vx.deinit(allocator, tty.anyWriter()); 135 + 136 + var scheduler = try coro.Scheduler.init(allocator, .{}); 137 + defer scheduler.deinit(); 138 + 139 + var loop = try Loop.init(); 140 + try loop.spawn(&scheduler, &vx, &tty, null, .{}); 141 + defer loop.deinit(&vx, &tty); 142 + 143 + try vx.enterAltScreen(tty.anyWriter()); 144 + try vx.queryTerminalSend(tty.anyWriter()); 145 + 146 + var buffered_tty_writer = tty.bufferedWriter(); 147 + const loading = try scheduler.spawn(loadingTask, .{ &vx, buffered_tty_writer.writer().any() }, .{}); 148 + const audio = try scheduler.spawn(audioTask, .{allocator}, .{}); 149 + const video = try scheduler.spawn(videoTask, .{buffered_tty_writer.writer().any()}, .{}); 150 + 151 + main: while (try scheduler.tick(.blocking) > 0) { 152 + while (try loop.popEvent()) |event| switch (event) { 153 + .key_press => |key| { 154 + if (key.matches('c', .{ .ctrl = true })) { 155 + break :main; 156 + } 157 + }, 158 + .winsize => |ws| try vx.resize(allocator, buffered_tty_writer.writer().any(), ws), 159 + }; 160 + 161 + if (audio.state(Video) == .ready and video.state(Audio) == .ready) { 162 + loading.cancel(); 163 + audio.wakeup(); 164 + video.wakeup(); 165 + } else if (audio.state(Audio) == .end and video.state(Video) == .end) { 166 + break :main; 167 + } 168 + 169 + try buffered_tty_writer.flush(); 170 + } 171 + }
+268
src/aio.zig
··· 1 + const builtin = @import("builtin"); 2 + const std = @import("std"); 3 + const aio = @import("aio"); 4 + const coro = @import("coro"); 5 + const vaxis = @import("main.zig"); 6 + const log = std.log.scoped(.vaxis_aio); 7 + 8 + comptime { 9 + if (builtin.target.os.tag == .windows) { 10 + @compileError("Windows is not supported right now"); 11 + } 12 + } 13 + 14 + const Yield = enum { no_state, took_event }; 15 + 16 + /// zig-aio based event loop 17 + /// <https://github.com/Cloudef/zig-aio> 18 + pub fn Loop(comptime T: type) type { 19 + return struct { 20 + const Event = T; 21 + 22 + winsize_task: ?coro.Task.Generic2(winsizeTask) = null, 23 + reader_task: ?coro.Task.Generic2(ttyReaderTask) = null, 24 + queue: std.BoundedArray(T, 512) = .{}, 25 + source: aio.EventSource, 26 + fatal: bool = false, 27 + 28 + pub fn init() !@This() { 29 + return .{ .source = try aio.EventSource.init() }; 30 + } 31 + 32 + pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void { 33 + vx.deviceStatusReport(tty.anyWriter()) catch {}; 34 + if (self.winsize_task) |task| task.cancel(); 35 + if (self.reader_task) |task| task.cancel(); 36 + self.source.deinit(); 37 + self.* = undefined; 38 + } 39 + 40 + fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void { 41 + const Context = struct { 42 + loop: *@TypeOf(self.*), 43 + tty: *vaxis.Tty, 44 + winsize: ?vaxis.Winsize = null, 45 + fn cb(ptr: *anyopaque) void { 46 + std.debug.assert(coro.current() == null); 47 + const ctx: *@This() = @ptrCast(@alignCast(ptr)); 48 + ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return; 49 + ctx.loop.source.notify(); 50 + } 51 + }; 52 + 53 + // keep on stack 54 + var ctx: Context = .{ .loop = self, .tty = tty }; 55 + if (@hasField(Event, "winsize")) { 56 + const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb }; 57 + try vaxis.Tty.notifyWinsize(handler); 58 + } 59 + 60 + while (true) { 61 + try coro.io.single(aio.WaitEventSource{ .source = &self.source }); 62 + if (ctx.winsize) |winsize| { 63 + if (!@hasField(Event, "winsize")) unreachable; 64 + ctx.loop.postEvent(.{ .winsize = winsize }) catch {}; 65 + ctx.winsize = null; 66 + } 67 + } 68 + } 69 + 70 + fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void { 71 + self.winsizeInner(tty) catch |err| { 72 + if (err != error.Canceled) log.err("winsize: {}", .{err}); 73 + self.fatal = true; 74 + }; 75 + } 76 + 77 + fn ttyReaderInner(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void { 78 + // initialize a grapheme cache 79 + var cache: vaxis.GraphemeCache = .{}; 80 + 81 + // get our initial winsize 82 + const winsize = try vaxis.Tty.getWinsize(tty.fd); 83 + if (@hasField(Event, "winsize")) { 84 + try self.postEvent(.{ .winsize = winsize }); 85 + } 86 + 87 + var parser: vaxis.Parser = .{ 88 + .grapheme_data = &vx.unicode.grapheme_data, 89 + }; 90 + 91 + const file: std.fs.File = .{ .handle = tty.fd }; 92 + while (true) { 93 + var buf: [4096]u8 = undefined; 94 + var n: usize = undefined; 95 + var read_start: usize = 0; 96 + try coro.io.single(aio.Read{ .file = file, .buffer = buf[read_start..], .out_read = &n }); 97 + var seq_start: usize = 0; 98 + while (seq_start < n) { 99 + const result = try parser.parse(buf[seq_start..n], paste_allocator); 100 + if (result.n == 0) { 101 + // copy the read to the beginning. We don't use memcpy because 102 + // this could be overlapping, and it's also rare 103 + const initial_start = seq_start; 104 + while (seq_start < n) : (seq_start += 1) { 105 + buf[seq_start - initial_start] = buf[seq_start]; 106 + } 107 + read_start = seq_start - initial_start + 1; 108 + continue; 109 + } 110 + read_start = 0; 111 + seq_start += result.n; 112 + 113 + const event = result.event orelse continue; 114 + switch (event) { 115 + .key_press => |key| { 116 + if (@hasField(Event, "key_press")) { 117 + // HACK: yuck. there has to be a better way 118 + var mut_key = key; 119 + if (key.text) |text| { 120 + mut_key.text = cache.put(text); 121 + } 122 + try self.postEvent(.{ .key_press = mut_key }); 123 + } 124 + }, 125 + .key_release => |*key| { 126 + if (@hasField(Event, "key_release")) { 127 + // HACK: yuck. there has to be a better way 128 + var mut_key = key; 129 + if (key.text) |text| { 130 + mut_key.text = cache.put(text); 131 + } 132 + try self.postEvent(.{ .key_release = mut_key }); 133 + } 134 + }, 135 + .mouse => |mouse| { 136 + if (@hasField(Event, "mouse")) { 137 + try self.postEvent(.{ .mouse = vx.translateMouse(mouse) }); 138 + } 139 + }, 140 + .focus_in => { 141 + if (@hasField(Event, "focus_in")) { 142 + try self.postEvent(.focus_in); 143 + } 144 + }, 145 + .focus_out => { 146 + if (@hasField(Event, "focus_out")) { 147 + try self.postEvent(.focus_out); 148 + } 149 + }, 150 + .paste_start => { 151 + if (@hasField(Event, "paste_start")) { 152 + try self.postEvent(.paste_start); 153 + } 154 + }, 155 + .paste_end => { 156 + if (@hasField(Event, "paste_end")) { 157 + try self.postEvent(.paste_end); 158 + } 159 + }, 160 + .paste => |text| { 161 + if (@hasField(Event, "paste")) { 162 + try self.postEvent(.{ .paste = text }); 163 + } else { 164 + if (paste_allocator) |_| 165 + paste_allocator.?.free(text); 166 + } 167 + }, 168 + .color_report => |report| { 169 + if (@hasField(Event, "color_report")) { 170 + try self.postEvent(.{ .color_report = report }); 171 + } 172 + }, 173 + .color_scheme => |scheme| { 174 + if (@hasField(Event, "color_scheme")) { 175 + try self.postEvent(.{ .color_scheme = scheme }); 176 + } 177 + }, 178 + .cap_kitty_keyboard => { 179 + log.info("kitty keyboard capability detected", .{}); 180 + vx.caps.kitty_keyboard = true; 181 + }, 182 + .cap_kitty_graphics => { 183 + if (!vx.caps.kitty_graphics) { 184 + log.info("kitty graphics capability detected", .{}); 185 + vx.caps.kitty_graphics = true; 186 + } 187 + }, 188 + .cap_rgb => { 189 + log.info("rgb capability detected", .{}); 190 + vx.caps.rgb = true; 191 + }, 192 + .cap_unicode => { 193 + log.info("unicode capability detected", .{}); 194 + vx.caps.unicode = .unicode; 195 + vx.screen.width_method = .unicode; 196 + }, 197 + .cap_sgr_pixels => { 198 + log.info("pixel mouse capability detected", .{}); 199 + vx.caps.sgr_pixels = true; 200 + }, 201 + .cap_color_scheme_updates => { 202 + log.info("color_scheme_updates capability detected", .{}); 203 + vx.caps.color_scheme_updates = true; 204 + }, 205 + .cap_da1 => { 206 + std.Thread.Futex.wake(&vx.query_futex, 10); 207 + }, 208 + .winsize => unreachable, // handled elsewhere for posix 209 + } 210 + } 211 + } 212 + } 213 + 214 + fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void { 215 + self.ttyReaderInner(vx, tty, paste_allocator) catch |err| { 216 + if (err != error.Canceled) log.err("ttyReader: {}", .{err}); 217 + self.fatal = true; 218 + }; 219 + } 220 + 221 + /// Spawns tasks to handle winsize signal and tty 222 + pub fn spawn( 223 + self: *@This(), 224 + scheduler: *coro.Scheduler, 225 + vx: *vaxis.Vaxis, 226 + tty: *vaxis.Tty, 227 + paste_allocator: ?std.mem.Allocator, 228 + spawn_options: coro.Scheduler.SpawnOptions, 229 + ) coro.Scheduler.SpawnError!void { 230 + if (self.reader_task) |_| unreachable; // programming error 231 + // This is required even if app doesn't care about winsize 232 + // It is because it consumes the EventSource, so it can wakeup the scheduler 233 + // Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update 234 + self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options); 235 + self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options); 236 + } 237 + 238 + pub const PopEventError = error{TtyCommunicationSevered}; 239 + 240 + /// Call this in a while loop in the main event handler until it returns null 241 + pub fn popEvent(self: *@This()) PopEventError!?T { 242 + if (self.fatal) return error.TtyCommunicationSevered; 243 + defer self.winsize_task.?.wakeupIf(Yield.took_event); 244 + defer self.reader_task.?.wakeupIf(Yield.took_event); 245 + return self.queue.popOrNull(); 246 + } 247 + 248 + pub const PostEventError = error{Overflow}; 249 + 250 + pub fn postEvent(self: *@This(), event: T) !void { 251 + if (coro.current()) |_| { 252 + while (true) { 253 + self.queue.insert(0, event) catch { 254 + // wait for the app to take event 255 + try coro.yield(Yield.took_event); 256 + continue; 257 + }; 258 + break; 259 + } 260 + } else { 261 + // queue can be full, app could handle this error by spinning the scheduler 262 + try self.queue.insert(0, event); 263 + } 264 + // wakes up the scheduler, so custom events update UI 265 + self.source.notify(); 266 + } 267 + }; 268 + }
+1
src/main.zig
··· 6 6 7 7 pub const Loop = @import("Loop.zig").Loop; 8 8 pub const xev = @import("xev.zig"); 9 + pub const aio = @import("aio.zig"); 9 10 10 11 pub const Queue = @import("queue.zig").Queue; 11 12 pub const Key = @import("Key.zig");