diff --git a/src/app.zig b/src/app.zig new file mode 100644 index 0000000..8c986e0 --- /dev/null +++ b/src/app.zig @@ -0,0 +1,90 @@ +//! Application type for TUI-applications +const std = @import("std"); +const terminal = @import("terminal.zig"); +const mergeTaggedUnions = @import("event.zig").mergeTaggedUnions; +const isTaggedUnion = @import("event.zig").isTaggedUnion; + +const BuiltinEvent = @import("event.zig").BuiltinEvent; +const Queue = @import("queue.zig").Queue; + +const log = std.log.scoped(.app); + +// Create the App Type with the associated user events `E` which describes +// an tagged union for all the user events that can be send through the +// applications event loop. +pub fn App(comptime E: type) type { + if (!isTaggedUnion(E)) { + @compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`."); + } + return struct { + pub const Event = mergeTaggedUnions(BuiltinEvent, E); + pub const Layout = @import("layout.zig").Layout(Event); + pub const Widget = @import("widget.zig").Widget(Event); + + queue: Queue(Event, 256) = .{}, + thread: ?std.Thread = null, + quit: bool = false, + termios: ?std.posix.termios = null, + + // TODO: event loop function? + // layout handling? + + pub fn init() @This() { + return .{}; + } + + pub fn start(this: *@This()) !void { + if (this.thread) |_| return; + + this.thread = try std.Thread.spawn(.{}, @This().run, .{this}); + var termios: std.posix.termios = undefined; + try terminal.enableRawMode(&termios); + this.termios = termios; + try terminal.saveScreen(); + } + + pub fn stop(this: *@This()) !void { + if (this.termios) |*termios| { + try terminal.disableRawMode(termios); + try terminal.restoreScreen(); + } + this.quit = true; + if (this.thread) |thread| { + thread.join(); + this.thread = null; + } + } + + /// Returns the next available event, blocking until one is available. + pub fn nextEvent(this: *@This()) Event { + return this.queue.pop(); + } + + /// Post an event into the queue. Blocks if there is no capacity for the event. + pub fn postEvent(this: *@This(), event: Event) void { + this.queue.push(event); + } + + fn run(this: *@This()) !void { + // thread to read user inputs + const size = terminal.getTerminalSize(); + this.postEvent(.{ .resize = size }); + // read input in loop + const buf: [256]u8 = undefined; + _ = buf; + while (!this.quit) { + std.time.sleep(5 * std.time.ns_per_s); + break; + // try terminal.read(buf[0..]); + // TODO: send corresponding events with key_presses + // -> create corresponding event + // -> handle key inputs (modifier, op codes, etc.) + // -> I could take inspiration from `libvaxis` for this + } + // FIXME: here is a race-condition -> i.e. there could be events in + // the queue, but they will not be executed because the main loop + // will close! + this.postEvent(.quit); + } + }; +} diff --git a/src/event.zig b/src/event.zig index 8c49dd4..c6a5068 100644 --- a/src/event.zig +++ b/src/event.zig @@ -1,5 +1,5 @@ //! Events which are defined by the library. They might be extended by user -//! events. +//! events. See `App` for more details about user defined events. const std = @import("std"); const terminal = @import("terminal.zig"); @@ -8,21 +8,23 @@ const terminal = @import("terminal.zig"); // message, while `err` represents an error which is propagated. `Widget`s or // `Layout`s may react to the event but should continue throwing the message up // to the application event loop. -pub const ApplicationEvent = union(enum) { +const ApplicationEvent = union(enum) { none, + quit, err: []const u8, }; // System events which contain information about events triggered from outside // of the application which impact the application. E.g. the terminal window // size has changed, etc. -pub const SystemEvent = union(enum) { +const SystemEvent = union(enum) { resize: terminal.Size, + // key_press: terminal.Key, }; -pub const BuiltinEvent = MergeTaggedUnions(SystemEvent, ApplicationEvent); +pub const BuiltinEvent = mergeTaggedUnions(SystemEvent, ApplicationEvent); -pub fn MergeTaggedUnions(comptime A: type, comptime B: type) type { +pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { if (!isTaggedUnion(A) or !isTaggedUnion(B)) { @compileError("Both types for merging tagged unions need to be of type `union(enum)`."); } diff --git a/src/layout.zig b/src/layout.zig index 57a1be1..3cdc01f 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -1,7 +1,7 @@ //! Dynamic dispatch for layout implementations. //! Each layout should at last implement these functions: -//! - handle(this: *@This(), event: Event) Event {} -//! - content(this: *@This()) *std.ArrayList(u8) {} +//! - handle(this: *@This(), event: Event) anyerror!*std.ArrayList(Event) {} +//! - content(this: *@This()) anyerror!*std.ArrayList(u8) {} //! - deinit(this: *@This()) void {} //! //! Create a `Layout` using `createFrom(object: anytype)` and use them through @@ -14,18 +14,17 @@ const std = @import("std"); const lib_event = @import("event.zig"); -pub fn Layout(comptime E: type) type { - if (!lib_event.isTaggedUnion(E)) { - @compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); +pub fn Layout(comptime Event: type) type { + if (!lib_event.isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { - pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); const LayoutType = @This(); const Ptr = usize; const VTable = struct { - handle: *const fn (this: *LayoutType, event: Event) Event, - content: *const fn (this: *LayoutType) *std.ArrayList(u8), + handle: *const fn (this: *LayoutType, event: Event) anyerror!*std.ArrayList(Event), + content: *const fn (this: *LayoutType) anyerror!*std.ArrayList(u8), deinit: *const fn (this: *LayoutType) void, }; @@ -33,13 +32,13 @@ pub fn Layout(comptime E: type) type { vtable: *const VTable = undefined, // Handle the provided `Event` for this `Widget`. - pub fn handle(this: *LayoutType, event: Event) Event { - return this.vtable.handle(this, event); + pub fn handle(this: *LayoutType, event: Event) !*std.ArrayList(Event) { + return try this.vtable.handle(this, event); } // Return the entire content of this `Widget`. - pub fn content(this: *LayoutType) *std.ArrayList(u8) { - return this.vtable.content(this); + pub fn content(this: *LayoutType) !*std.ArrayList(u8) { + return try this.vtable.content(this); } pub fn deinit(this: *LayoutType) void { @@ -53,16 +52,16 @@ pub fn Layout(comptime E: type) type { .vtable = &.{ .handle = struct { // Handle the provided `Event` for this `Widget`. - fn handle(this: *LayoutType, event: Event) Event { + fn handle(this: *LayoutType, event: Event) !*std.ArrayList(Event) { const layout: @TypeOf(object) = @ptrFromInt(this.object); - return layout.handle(event); + return try layout.handle(event); } }.handle, .content = struct { // Return the entire content of this `Widget`. - fn content(this: *LayoutType) *std.ArrayList(u8) { + fn content(this: *LayoutType) !*std.ArrayList(u8) { const layout: @TypeOf(object) = @ptrFromInt(this.object); - return layout.content(); + return try layout.content(); } }.content, .deinit = struct { @@ -76,6 +75,6 @@ pub fn Layout(comptime E: type) type { } // import and export of `Layout` implementations - pub const Pane = @import("layout/Pane.zig").Layout(E); + pub const Pane = @import("layout/Pane.zig").Layout(Event); }; } diff --git a/src/layout/Pane.zig b/src/layout/Pane.zig index 629dd59..c7c0998 100644 --- a/src/layout/Pane.zig +++ b/src/layout/Pane.zig @@ -2,37 +2,42 @@ const std = @import("std"); const lib_event = @import("../event.zig"); const widget = @import("../widget.zig"); -pub fn Layout(comptime E: type) type { - if (!lib_event.isTaggedUnion(E)) { - @compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); +pub fn Layout(comptime Event: type) type { + if (!lib_event.isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { - pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); - - w: widget.Widget(E) = undefined, + w: widget.Widget(Event) = undefined, + events: std.ArrayList(Event) = undefined, c: std.ArrayList(u8) = undefined, - pub fn init(allocator: std.mem.Allocator, w: widget.Widget(E)) @This() { + pub fn init(allocator: std.mem.Allocator, w: widget.Widget(Event)) @This() { return .{ .w = w, + .events = std.ArrayList(Event).init(allocator), .c = std.ArrayList(u8).init(allocator), }; } pub fn deinit(this: *@This()) void { this.w.deinit(); + this.events.deinit(); this.c.deinit(); this.* = undefined; } - pub fn handle(this: *@This(), event: Event) Event { - return this.w.handle(event); + pub fn handle(this: *@This(), event: Event) !*std.ArrayList(Event) { + this.events.clearRetainingCapacity(); + if (this.w.handle(event)) |e| { + try this.events.append(e); + } + return &this.events; } - pub fn content(this: *@This()) *std.ArrayList(u8) { - const widget_content = this.w.content(); + pub fn content(this: *@This()) !*std.ArrayList(u8) { + const widget_content = try this.w.content(); this.c.clearRetainingCapacity(); - this.c.appendSlice(widget_content.items) catch @panic("OOM"); + try this.c.appendSlice(widget_content.items); return &this.c; } }; diff --git a/src/main.zig b/src/main.zig index d0d6dc8..b0baf99 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,18 +1,15 @@ const std = @import("std"); - +const terminal = @import("terminal.zig"); const zlog = @import("zlog"); -const terminal = @import("terminal.zig"); - -const UserEvent = union(enum) {}; - -const Widget = @import("widget.zig").Widget(UserEvent); -const Layout = @import("layout.zig").Layout(UserEvent); +const App = @import("app.zig").App(union(enum) {}); pub const std_options = zlog.std_options; const log = std.log.scoped(.main); pub fn main() !void { + errdefer |err| log.err("Application Error: {any}", .{err}); + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { const deinit_status = gpa.deinit(); @@ -23,18 +20,46 @@ pub fn main() !void { } const allocator = gpa.allocator(); - const size = terminal.getTerminalSize(); + var app = App.init(); - log.debug("Size := [x: {d}, y: {d}]", .{ size.cols, size.rows }); - var rawText = Widget.RawText.init(allocator); - const widget = Widget.createFrom(&rawText); - var layout = Layout.Pane.init(allocator, widget); + var rawText = App.Widget.RawText.init(allocator); + const widget = App.Widget.createFrom(&rawText); + var layout = App.Layout.Pane.init(allocator, widget); defer layout.deinit(); - // single 'draw' loop - _ = layout.handle(.none); - log.debug("Layout result: {s}", .{layout.content().items}); + try app.start(); + defer app.stop() catch unreachable; + // NOTE: necessary for fullscreen tui applications + try terminal.enterAltScreen(); + defer terminal.existAltScreen() catch unreachable; + + // App.Event loop + while (true) { + const event = app.nextEvent(); + + switch (event) { + .none => continue, + .quit => break, + .resize => |size| { + // NOTE: draw actions should not happen here (still here for testing) + // NOTE: clearing the screen and positioning the cursor is only necessary for full screen applications + // - in-line applications should use relative movements instead and should only clear lines (which they draw) + // - in-line applications should not enter the alt screen + try terminal.clearScreen(); + try terminal.setCursorPositionHome(); + log.debug("Size := [x: {d}, y: {d}]", .{ size.cols, size.rows }); + }, + else => {}, + } + const events = try layout.handle(event); + for (events.items) |e| { + app.postEvent(e); + } + log.debug("Layout result: {s}", .{(try layout.content()).items}); + } + // TODO: I could use the ascii codes in vaxis + // - see https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b // how would I draw? // use array for screen contents? <-> support partial re-draws // support widget type drawing similar to the already existing widgets @@ -43,3 +68,7 @@ pub fn main() !void { // - contents of corresponding locations // resize event } + +test { + _ = @import("queue.zig"); +} diff --git a/src/queue.zig b/src/queue.zig new file mode 100644 index 0000000..a364e57 --- /dev/null +++ b/src/queue.zig @@ -0,0 +1,322 @@ +// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License) +// with slight modifications +const std = @import("std"); +const assert = std.debug.assert; + +/// Thread safe. Fixed size. Blocking push and pop. +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 + not_full: std.Thread.Condition = .{}, + // ...or empty + not_empty: std.Thread.Condition = .{}, + + const QueueType = @This(); + + /// Pop an item from the queue. Blocks until an item is available. + pub fn pop(this: *QueueType) T { + this.mutex.lock(); + defer this.mutex.unlock(); + while (this.isEmptyLH()) { + this.not_empty.wait(&this.mutex); + } + assert(!this.isEmptyLH()); + if (this.isFullLH()) { + // If we are full, wake up a push that might be + // waiting here. + this.not_full.signal(); + } + + const result = this.buf[this.mask(this.read_index)]; + this.read_index = this.mask2(this.read_index + 1); + return result; + } + + /// Push an item into the queue. Blocks until an item has been + /// put in the queue. + pub fn push(this: *QueueType, item: T) void { + this.mutex.lock(); + defer this.mutex.unlock(); + while (this.isFullLH()) { + this.not_full.wait(&this.mutex); + } + if (this.isEmptyLH()) { + // If we were empty, wake up a pop if it was waiting. + this.not_empty.signal(); + } + assert(!this.isFullLH()); + + this.buf[this.mask(this.write_index)] = item; + this.write_index = this.mask2(this.write_index + 1); + } + + /// Push an item into the queue. Returns true when the item + /// was successfully placed in the queue, false if the queue + /// was full. + pub fn tryPush(this: *QueueType, item: T) bool { + this.mutex.lock(); + if (this.isFullLH()) { + this.mutex.unlock(); + return false; + } + this.mutex.unlock(); + this.push(item); + return true; + } + + /// Pop an item from the queue. Returns null when no item is + /// available. + pub fn tryPop(this: *QueueType) ?T { + this.mutex.lock(); + if (this.isEmptyLH()) { + this.mutex.unlock(); + return null; + } + this.mutex.unlock(); + return this.pop(); + } + + /// Poll the queue. This call blocks until events are in the queue + pub fn poll(this: *QueueType) void { + this.mutex.lock(); + defer this.mutex.unlock(); + while (this.isEmptyLH()) { + this.not_empty.wait(&this.mutex); + } + assert(!this.isEmptyLH()); + } + + fn isEmptyLH(this: QueueType) bool { + return this.write_index == this.read_index; + } + + fn isFullLH(this: QueueType) bool { + return this.mask2(this.write_index + this.buf.len) == + this.read_index; + } + + /// Returns `true` if the queue is empty and `false` otherwise. + pub fn isEmpty(this: *QueueType) bool { + this.mutex.lock(); + defer this.mutex.unlock(); + return this.isEmptyLH(); + } + + /// Returns `true` if the queue is full and `false` otherwise. + pub fn isFull(this: *QueueType) bool { + this.mutex.lock(); + defer this.mutex.unlock(); + return this.isFullLH(); + } + + /// Returns the length + fn len(this: QueueType) usize { + const wrap_offset = 2 * this.buf.len * + @intFromBool(this.write_index < this.read_index); + const adjusted_write_index = this.write_index + wrap_offset; + return adjusted_write_index - this.read_index; + } + + /// Returns `index` modulo the length of the backing slice. + fn mask(this: QueueType, index: usize) usize { + return index % this.buf.len; + } + + /// Returns `index` modulo twice the length of the backing slice. + fn mask2(this: QueueType, index: usize) usize { + return index % (2 * this.buf.len); + } + }; +} + +const testing = std.testing; +const cfg = Thread.SpawnConfig{ .allocator = testing.allocator }; +test "Queue: simple push / pop" { + var queue: Queue(u8, 16) = .{}; + queue.push(1); + queue.push(2); + const pop = queue.pop(); + try testing.expectEqual(1, pop); + try testing.expectEqual(2, queue.pop()); +} + +const Thread = std.Thread; +fn testPushPop(q: *Queue(u8, 2)) !void { + q.push(3); + try testing.expectEqual(2, q.pop()); +} + +test "Fill, wait to push, pop once in another thread" { + var queue: Queue(u8, 2) = .{}; + queue.push(1); + queue.push(2); + const t = try Thread.spawn(cfg, testPushPop, .{&queue}); + try testing.expectEqual(false, queue.tryPush(3)); + try testing.expectEqual(1, queue.pop()); + t.join(); + try testing.expectEqual(3, queue.pop()); + try testing.expectEqual(null, queue.tryPop()); +} + +fn testPush(q: *Queue(u8, 2)) void { + q.push(0); + q.push(1); + q.push(2); + q.push(3); + q.push(4); +} + +test "Try to pop, fill from another thread" { + var queue: Queue(u8, 2) = .{}; + const thread = try Thread.spawn(cfg, testPush, .{&queue}); + for (0..5) |idx| { + try testing.expectEqual(@as(u8, @intCast(idx)), queue.pop()); + } + thread.join(); +} + +fn sleepyPop(q: *Queue(u8, 2)) !void { + // First we wait for the queue to be full. + while (!q.isFull()) + try Thread.yield(); + + // Then we spuriously wake it up, because that's a thing that can + // happen. + q.not_full.signal(); + q.not_empty.signal(); + + // Then give the other thread a good chance of waking up. It's not + // clear that yield guarantees the other thread will be scheduled, + // so we'll throw a sleep in here just to be sure. The queue is + // still full and the push in the other thread is still blocked + // waiting for space. + try Thread.yield(); + std.time.sleep(std.time.ns_per_s); + // Finally, let that other thread go. + try std.testing.expectEqual(1, q.pop()); + + // This won't continue until the other thread has had a chance to + // put at least one item in the queue. + while (!q.isFull()) + try Thread.yield(); + // But we want to ensure that there's a second push waiting, so + // here's another sleep. + std.time.sleep(std.time.ns_per_s / 2); + + // Another spurious wake... + q.not_full.signal(); + q.not_empty.signal(); + // And another chance for the other thread to see that it's + // spurious and go back to sleep. + try Thread.yield(); + std.time.sleep(std.time.ns_per_s / 2); + + // Pop that thing and we're done. + try std.testing.expectEqual(2, q.pop()); +} + +test "Fill, block, fill, block" { + // Fill the queue, block while trying to write another item, have + // a background thread unblock us, then block while trying to + // write yet another thing. Have the background thread unblock + // that too (after some time) then drain the queue. This test + // fails if the while loop in `push` is turned into an `if`. + + var queue: Queue(u8, 2) = .{}; + const thread = try Thread.spawn(cfg, sleepyPop, .{&queue}); + queue.push(1); + queue.push(2); + const now = std.time.milliTimestamp(); + queue.push(3); // This one should block. + const then = std.time.milliTimestamp(); + + // Just to make sure the sleeps are yielding to this thread, make + // sure it took at least 900ms to do the push. + try std.testing.expect(then - now > 900); + + // This should block again, waiting for the other thread. + queue.push(4); + + // And once that push has gone through, the other thread's done. + thread.join(); + try std.testing.expectEqual(3, queue.pop()); + try std.testing.expectEqual(4, queue.pop()); +} + +fn sleepyPush(q: *Queue(u8, 1)) !void { + // Try to ensure the other thread has already started trying to pop. + try Thread.yield(); + std.time.sleep(std.time.ns_per_s / 2); + + // Spurious wake + q.not_full.signal(); + q.not_empty.signal(); + + try Thread.yield(); + std.time.sleep(std.time.ns_per_s / 2); + + // Stick something in the queue so it can be popped. + q.push(1); + // Ensure it's been popped. + while (!q.isEmpty()) + try Thread.yield(); + // Give the other thread time to block again. + try Thread.yield(); + std.time.sleep(std.time.ns_per_s / 2); + + // Spurious wake + q.not_full.signal(); + q.not_empty.signal(); + + q.push(2); +} + +test "Drain, block, drain, block" { + // This is like fill/block/fill/block, but on the pop end. This + // test should fail if the `while` loop in `pop` is turned into an + // `if`. + + var queue: Queue(u8, 1) = .{}; + const thread = try Thread.spawn(cfg, sleepyPush, .{&queue}); + try std.testing.expectEqual(1, queue.pop()); + try std.testing.expectEqual(2, queue.pop()); + thread.join(); +} + +fn readerThread(q: *Queue(u8, 1)) !void { + try testing.expectEqual(1, q.pop()); +} + +test "2 readers" { + // 2 threads read, one thread writes + var queue: Queue(u8, 1) = .{}; + const t1 = try Thread.spawn(cfg, readerThread, .{&queue}); + const t2 = try Thread.spawn(cfg, readerThread, .{&queue}); + try Thread.yield(); + std.time.sleep(std.time.ns_per_s / 2); + queue.push(1); + queue.push(1); + t1.join(); + t2.join(); +} + +fn writerThread(q: *Queue(u8, 1)) !void { + q.push(1); +} + +test "2 writers" { + var queue: Queue(u8, 1) = .{}; + const t1 = try Thread.spawn(cfg, writerThread, .{&queue}); + const t2 = try Thread.spawn(cfg, writerThread, .{&queue}); + + try testing.expectEqual(1, queue.pop()); + try testing.expectEqual(1, queue.pop()); + t1.join(); + t2.join(); +} diff --git a/src/terminal.zig b/src/terminal.zig index f49c94b..1a71f0d 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -1,8 +1,5 @@ const std = @import("std"); -const posix = std.posix; -const fmt = std.fmt; - const log = std.log.scoped(.terminal); pub const Size = struct { @@ -26,20 +23,44 @@ pub const ReportMode = enum { /// Gets number of rows and columns in the terminal pub fn getTerminalSize() Size { - var ws: posix.winsize = undefined; - _ = posix.system.ioctl(posix.STDERR_FILENO, posix.T.IOCGWINSZ, &ws); + var ws: std.posix.winsize = undefined; + _ = std.posix.system.ioctl(std.posix.STDERR_FILENO, std.posix.T.IOCGWINSZ, &ws); return .{ .cols = ws.ws_col, .rows = ws.ws_row }; } +pub fn saveScreen() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[?47h"); +} + +pub fn restoreScreen() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[?47l"); +} + +pub fn enterAltScreen() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[?1049h"); +} + +pub fn existAltScreen() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[?1049l"); +} + +pub fn clearScreen() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[2J"); +} + +pub fn setCursorPositionHome() !void { + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[H"); +} + pub fn getCursorPosition() !Position { // Needs Raw mode (no wait for \n) to work properly cause // control sequence will not be written without it. - _ = try posix.write(posix.STDERR_FILENO, "\x1b[6n"); + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[6n"); var buf: [64]u8 = undefined; // format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R" - const len = try posix.read(posix.STDIN_FILENO, &buf); + const len = try std.posix.read(std.posix.STDIN_FILENO, &buf); if (!isCursorPosition(buf[0..len])) { return error.InvalidValueReturned; @@ -73,8 +94,8 @@ pub fn getCursorPosition() !Position { } return .{ - .row = try fmt.parseInt(u16, row[0..ridx], 10), - .col = try fmt.parseInt(u16, col[0..cidx], 10), + .row = try std.fmt.parseInt(u16, row[0..ridx], 10), + .col = try std.fmt.parseInt(u16, col[0..cidx], 10), }; } @@ -102,8 +123,8 @@ pub fn isCursorPosition(buf: []u8) bool { /// /// `bak`: pointer to store termios struct backup before /// altering, this is used to disable raw mode. -pub fn enableRawMode(bak: *posix.termios) !void { - var termios = try posix.tcgetattr(posix.STDIN_FILENO); +pub fn enableRawMode(bak: *std.posix.termios) !void { + var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); bak.* = termios; termios.iflag.IXON = false; @@ -114,18 +135,18 @@ pub fn enableRawMode(bak: *posix.termios) !void { termios.lflag.IEXTEN = false; termios.lflag.ISIG = false; - try posix.tcsetattr( - posix.STDIN_FILENO, - posix.TCSA.FLUSH, + try std.posix.tcsetattr( + std.posix.STDIN_FILENO, + .FLUSH, termios, ); } /// Reverts `enableRawMode` to restore initial functionality. -pub fn disableRawMode(bak: *posix.termios) !void { - try posix.tcsetattr( - posix.STDIN_FILENO, - posix.TCSA.FLUSH, +pub fn disableRawMode(bak: *std.posix.termios) !void { + try std.posix.tcsetattr( + std.posix.STDIN_FILENO, + .FLUSH, bak.*, ); } @@ -134,12 +155,12 @@ pub fn disableRawMode(bak: *posix.termios) !void { pub fn canSynchornizeOutput() !bool { // Needs Raw mode (no wait for \n) to work properly cause // control sequence will not be written without it. - _ = try posix.write(posix.STDERR_FILENO, "\x1b[?2026$p"); + _ = try std.posix.write(std.posix.STDERR_FILENO, "\x1b[?2026$p"); var buf: [64]u8 = undefined; // format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y" - const len = try posix.read(posix.STDIN_FILENO, &buf); + const len = try std.posix.read(std.posix.STDIN_FILENO, &buf); if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) { return false; } diff --git a/src/widget.zig b/src/widget.zig index e4fc387..a9e8a65 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,6 +1,6 @@ //! Dynamic dispatch for widget implementations. //! Each widget should at last implement these functions: -//! - handle(this: *@This(), event: Event) Event {} +//! - handle(this: *@This(), event: Event) ?Event {} //! - content(this: *@This()) *std.ArrayList(u8) {} //! - deinit(this: *@This()) void {} //! @@ -13,18 +13,17 @@ const std = @import("std"); const lib_event = @import("event.zig"); -pub fn Widget(comptime E: type) type { - if (!lib_event.isTaggedUnion(E)) { - @compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); +pub fn Widget(comptime Event: type) type { + if (!lib_event.isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { - pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); const WidgetType = @This(); const Ptr = usize; const VTable = struct { - handle: *const fn (this: *WidgetType, event: Event) Event, - content: *const fn (this: *WidgetType) *std.ArrayList(u8), + handle: *const fn (this: *WidgetType, event: Event) ?Event, + content: *const fn (this: *WidgetType) anyerror!*std.ArrayList(u8), deinit: *const fn (this: *WidgetType) void, }; @@ -32,13 +31,13 @@ pub fn Widget(comptime E: type) type { vtable: *const VTable = undefined, // Handle the provided `Event` for this `Widget`. - pub fn handle(this: *WidgetType, event: Event) Event { + pub fn handle(this: *WidgetType, event: Event) ?Event { return this.vtable.handle(this, event); } // Return the entire content of this `Widget`. - pub fn content(this: *WidgetType) *std.ArrayList(u8) { - return this.vtable.content(this); + pub fn content(this: *WidgetType) !*std.ArrayList(u8) { + return try this.vtable.content(this); } pub fn deinit(this: *WidgetType) void { @@ -52,16 +51,16 @@ pub fn Widget(comptime E: type) type { .vtable = &.{ .handle = struct { // Handle the provided `Event` for this `Widget`. - fn handle(this: *WidgetType, event: Event) Event { + fn handle(this: *WidgetType, event: Event) ?Event { const widget: @TypeOf(object) = @ptrFromInt(this.object); return widget.handle(event); } }.handle, .content = struct { // Return the entire content of this `Widget`. - fn content(this: *WidgetType) *std.ArrayList(u8) { + fn content(this: *WidgetType) !*std.ArrayList(u8) { const widget: @TypeOf(object) = @ptrFromInt(this.object); - return widget.content(); + return try widget.content(); } }.content, .deinit = struct { @@ -75,7 +74,7 @@ pub fn Widget(comptime E: type) type { } // TODO: import and export of `Widget` implementations (with corresponding intialization using `Event`) - pub const RawText = @import("widget/RawText.zig").Widget(E); + pub const RawText = @import("widget/RawText.zig").Widget(Event); // pub const Header = @import("widget/Header.zig"); // pub const ViewPort = @import("widget/ViewPort.zig"); // pub const PopupMenu = @import("widget/PopupMenu.zig"); diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index 42836fd..5aba79c 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -1,21 +1,15 @@ const std = @import("std"); const lib_event = @import("../event.zig"); -pub fn Widget(comptime E: type) type { - if (!lib_event.isTaggedUnion(E)) { - @compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); +pub fn Widget(comptime Event: type) type { + if (!lib_event.isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); } return struct { - pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); - c: std.ArrayList(u8) = undefined, pub fn init(allocator: std.mem.Allocator) @This() { - var c = std.ArrayList(u8).init(allocator); - c.appendSlice("This is a simple test") catch @panic("OOM"); - return .{ - .c = c, - }; + return .{ .c = std.ArrayList(u8).init(allocator) }; } pub fn deinit(this: *@This()) void { @@ -23,14 +17,16 @@ pub fn Widget(comptime E: type) type { this.* = undefined; } - pub fn handle(this: *@This(), event: Event) Event { + pub fn handle(this: *@This(), event: Event) ?Event { // ignore the event for now _ = this; _ = event; - return .none; + return null; } - pub fn content(this: *@This()) *std.ArrayList(u8) { + pub fn content(this: *@This()) !*std.ArrayList(u8) { + this.c.clearRetainingCapacity(); + try this.c.appendSlice("This is a simple test"); return &this.c; } };