diff --git a/src/main.zig b/src/main.zig index 1784f98..2cb7acc 100644 --- a/src/main.zig +++ b/src/main.zig @@ -15,13 +15,12 @@ pub fn main() !void { var tty = try Tty.init(); defer tty.deinit(); - const pipe = try std.os.pipe(); // run our tty read loop in it's own thread - const read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &tty, pipe[0] }); + const read_thread = try std.Thread.spawn(.{}, Tty.run, .{ &tty, Event, eventCallback }); try read_thread.setName("tty"); - std.time.sleep(100_000_000_0); - _ = try std.os.write(pipe[1], "q"); + std.time.sleep(100_000_000_00); + tty.stop(); read_thread.join(); try stdout.print("Run `zig build test` to run the tests.\n", .{}); @@ -29,9 +28,14 @@ pub fn main() !void { try bw.flush(); // don't forget to flush! } +const Event = union(enum) { + key: u8, + mouse: u8, +}; + +fn eventCallback(_: Event) void {} + test "simple test" { - var list = std.ArrayList(i32).init(std.testing.allocator); - defer list.deinit(); // try commenting this out and see if zig detects the memory leak! - try list.append(42); - try std.testing.expectEqual(@as(i32, 42), list.pop()); + _ = @import("odditui.zig"); + _ = @import("queue.zig"); } diff --git a/src/odditui.zig b/src/odditui.zig new file mode 100644 index 0000000..da0257d --- /dev/null +++ b/src/odditui.zig @@ -0,0 +1,47 @@ +const std = @import("std"); + +const queue = @import("queue.zig"); + +/// App is the entrypoint for an odditui application. The provided type T should +/// be a tagged union which contains all of the events the application will +/// handle. Odditui will look for the following fields on the union and, if +/// found, emit them via the "nextEvent" method +pub fn App(comptime T: type) type { + return struct { + const Self = @This(); + + /// the event queue for this App + // + // TODO: is 512 ok? + queue: queue.Queue(T, 512) = .{}, + + /// Runtime options + const Options = struct {}; + + /// Initialize an App with runtime options + pub fn init(_: Options) Self { + return Self{}; + } + + /// returns the next available event, blocking until one is available + pub fn nextEvent(self: *Self) T { + return self.queue.pop(); + } + + /// posts an event into the event queue. Will block if there is not + /// capacity for the event + pub fn postEvent(self: *Self, event: T) void { + self.queue.push(event); + } + }; +} + +test "App: event queueing" { + const Event = union(enum) { + key, + }; + var app: App(Event) = App(Event).init(.{}); + app.postEvent(.key); + const event = app.nextEvent(); + try std.testing.expect(event == .key); +} diff --git a/src/queue.zig b/src/queue.zig new file mode 100644 index 0000000..ec4747d --- /dev/null +++ b/src/queue.zig @@ -0,0 +1,93 @@ +const std = @import("std"); +const assert = std.debug.assert; +const atomic = std.atomic; +const Futex = std.Thread.Futex; + +pub fn Queue( + comptime T: type, + comptime size: usize, +) type { + return struct { + buf: [size]T = undefined, + + read_index: usize = 0, + write_index: usize = 0, + + mutex: std.Thread.Mutex = .{}, + // blocks when the buffer is full or empty + futex: atomic.Value(u32) = atomic.Value(u32).init(0), + + const Self = @This(); + + pub fn pop(self: *Self) T { + self.mutex.lock(); + defer self.mutex.unlock(); + if (self.isEmpty()) { + // If we don't have any items, we unlock and wait + self.mutex.unlock(); + Futex.wait(&self.futex, 0); + // regain our lock + self.mutex.lock(); + } + if (self.isFull()) { + // If we are full, wake up the push + defer Futex.wake(&self.futex, 1); + } + const i = self.read_index; + self.read_index += 1; + self.read_index = self.read_index % self.buf.len; + return self.buf[i]; + } + + pub fn push(self: *Self, item: T) void { + self.mutex.lock(); + defer self.mutex.unlock(); + if (self.isFull()) { + self.mutex.unlock(); + Futex.wait(&self.futex, 0); + self.mutex.lock(); + } + if (self.isEmpty()) { + defer Futex.wake(&self.futex, 1); + } + const i = self.write_index; + self.write_index += 1; + self.write_index = self.write_index % self.buf.len; + self.buf[i] = item; + } + + /// Returns `true` if the ring buffer is empty and `false` otherwise. + pub fn isEmpty(self: Self) bool { + return self.write_index == self.read_index; + } + + /// Returns `true` if the ring buffer is full and `false` otherwise. + pub fn isFull(self: Self) bool { + return self.mask2(self.write_index + self.buf.len) == self.read_index; + } + + /// Returns the length + pub fn len(self: Self) usize { + const wrap_offset = 2 * self.buf.len * @intFromBool(self.write_index < self.read_index); + const adjusted_write_index = self.write_index + wrap_offset; + return adjusted_write_index - self.read_index; + } + + /// Returns `index` modulo the length of the backing slice. + pub fn mask(self: Self, index: usize) usize { + return index % self.buf.len; + } + + /// Returns `index` modulo twice the length of the backing slice. + pub fn mask2(self: Self, index: usize) usize { + return index % (2 * self.buf.len); + } + }; +} + +test "Queue: simple push / pop" { + var queue: Queue(u8, 16) = .{}; + queue.push(1); + const pop = queue.pop(); + try std.testing.expectEqual(1, pop); +} diff --git a/src/tty/Tty.zig b/src/tty/Tty.zig index 6eab9da..7ac5ac9 100644 --- a/src/tty/Tty.zig +++ b/src/tty/Tty.zig @@ -1,6 +1,5 @@ const std = @import("std"); const os = std.os; -const xev = @import("xev"); const log = std.log.scoped(.tty); @@ -12,6 +11,9 @@ termios: os.termios, /// The file descriptor we are using for I/O fd: os.fd_t, +/// the write end of a pipe to signal the tty should exit it's run loop +quit_fd: ?os.fd_t = null, + /// initializes a Tty instance by opening /dev/tty and "making it raw" pub fn init() !Tty { // Open our tty @@ -34,23 +36,38 @@ pub fn deinit(self: *Tty) void { os.close(self.fd); } +/// stops the run loop +pub fn stop(self: *Tty) void { + if (self.quit_fd) |fd| { + _ = std.os.write(fd, "q") catch {}; + } +} + /// read input from the tty -pub fn run(self: *Tty, quit: os.fd_t) !void { - defer os.close(quit); +pub fn run(self: *Tty, comptime T: type, comptime _: fn (ev: T) void) !void { + // create a pipe so we can signal to exit the run loop + const pipe = try os.pipe(); + defer os.close(pipe[0]); + defer os.close(pipe[1]); + + self.quit_fd = pipe[1]; + + var parser: Parser = .{}; + var buf: [1024]u8 = undefined; var pollfds: [2]std.os.pollfd = .{ .{ .fd = self.fd, .events = std.os.POLL.IN, .revents = undefined }, - .{ .fd = quit, .events = std.os.POLL.IN, .revents = undefined }, + .{ .fd = pipe[0], .events = std.os.POLL.IN, .revents = undefined }, }; while (true) { _ = try std.os.poll(&pollfds, -1); if (pollfds[1].revents & std.os.POLL.IN != 0) { - log.info("read thread got quit signal", .{}); + log.info("quitting read thread", .{}); return; } const n = try os.read(self.fd, &buf); - log.err("{s}", .{buf[0..n]}); + parser.parse(self, buf[0..n]); } } @@ -93,3 +110,38 @@ pub fn makeRaw(fd: os.fd_t) !os.termios { try os.tcsetattr(fd, .FLUSH, raw); return state; } + +/// parses vt input. Retains some state so we need an object for it +const Parser = struct { + const log = std.log.scoped(.parser); + + // the state of the parser + const State = enum { + ground, + escape, + csi, + osc, + dcs, + sos, + pm, + apc, + ss2, + ss3, + }; + + state: State = .ground, + + fn parse(self: *Parser, tty: *Tty, input: []u8) void { + _ = tty; // autofix + var i: usize = 0; + const start: usize = 0; + _ = start; // autofix + while (i < input.len) : (i += 1) { + const b = input[i]; + switch (self.state) { + .ground => Parser.log.err("0x{x}\r", .{b}), + else => {}, + } + } + } +};