Replace vaxis with zterm #1

Merged
yves-biener merged 20 commits from own-tty-visuals into main 2024-11-13 19:52:55 +01:00
9 changed files with 559 additions and 96 deletions
Showing only changes of commit 14aab9ef50 - Show all commits

90
src/app.zig Normal file
View File

@@ -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);
}
};
}

View File

@@ -1,5 +1,5 @@
//! Events which are defined by the library. They might be extended by user //! 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 std = @import("std");
const terminal = @import("terminal.zig"); 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 // 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 // `Layout`s may react to the event but should continue throwing the message up
// to the application event loop. // to the application event loop.
pub const ApplicationEvent = union(enum) { const ApplicationEvent = union(enum) {
none, none,
quit,
err: []const u8, err: []const u8,
}; };
// System events which contain information about events triggered from outside // System events which contain information about events triggered from outside
// of the application which impact the application. E.g. the terminal window // of the application which impact the application. E.g. the terminal window
// size has changed, etc. // size has changed, etc.
pub const SystemEvent = union(enum) { const SystemEvent = union(enum) {
resize: terminal.Size, 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)) { if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
@compileError("Both types for merging tagged unions need to be of type `union(enum)`."); @compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
} }

View File

@@ -1,7 +1,7 @@
//! Dynamic dispatch for layout implementations. //! Dynamic dispatch for layout implementations.
//! Each layout should at last implement these functions: //! Each layout should at last implement these functions:
//! - handle(this: *@This(), event: Event) Event {} //! - handle(this: *@This(), event: Event) anyerror!*std.ArrayList(Event) {}
//! - content(this: *@This()) *std.ArrayList(u8) {} //! - content(this: *@This()) anyerror!*std.ArrayList(u8) {}
//! - deinit(this: *@This()) void {} //! - deinit(this: *@This()) void {}
//! //!
//! Create a `Layout` using `createFrom(object: anytype)` and use them through //! Create a `Layout` using `createFrom(object: anytype)` and use them through
@@ -14,18 +14,17 @@
const std = @import("std"); const std = @import("std");
const lib_event = @import("event.zig"); const lib_event = @import("event.zig");
pub fn Layout(comptime E: type) type { pub fn Layout(comptime Event: type) type {
if (!lib_event.isTaggedUnion(E)) { if (!lib_event.isTaggedUnion(Event)) {
@compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
} }
return struct { return struct {
pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E);
const LayoutType = @This(); const LayoutType = @This();
const Ptr = usize; const Ptr = usize;
const VTable = struct { const VTable = struct {
handle: *const fn (this: *LayoutType, event: Event) Event, handle: *const fn (this: *LayoutType, event: Event) anyerror!*std.ArrayList(Event),
content: *const fn (this: *LayoutType) *std.ArrayList(u8), content: *const fn (this: *LayoutType) anyerror!*std.ArrayList(u8),
deinit: *const fn (this: *LayoutType) void, deinit: *const fn (this: *LayoutType) void,
}; };
@@ -33,13 +32,13 @@ pub fn Layout(comptime E: type) type {
vtable: *const VTable = undefined, vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Widget`. // Handle the provided `Event` for this `Widget`.
pub fn handle(this: *LayoutType, event: Event) Event { pub fn handle(this: *LayoutType, event: Event) !*std.ArrayList(Event) {
return this.vtable.handle(this, event); return try this.vtable.handle(this, event);
} }
// Return the entire content of this `Widget`. // Return the entire content of this `Widget`.
pub fn content(this: *LayoutType) *std.ArrayList(u8) { pub fn content(this: *LayoutType) !*std.ArrayList(u8) {
return this.vtable.content(this); return try this.vtable.content(this);
} }
pub fn deinit(this: *LayoutType) void { pub fn deinit(this: *LayoutType) void {
@@ -53,16 +52,16 @@ pub fn Layout(comptime E: type) type {
.vtable = &.{ .vtable = &.{
.handle = struct { .handle = struct {
// Handle the provided `Event` for this `Widget`. // 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); const layout: @TypeOf(object) = @ptrFromInt(this.object);
return layout.handle(event); return try layout.handle(event);
} }
}.handle, }.handle,
.content = struct { .content = struct {
// Return the entire content of this `Widget`. // 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); const layout: @TypeOf(object) = @ptrFromInt(this.object);
return layout.content(); return try layout.content();
} }
}.content, }.content,
.deinit = struct { .deinit = struct {
@@ -76,6 +75,6 @@ pub fn Layout(comptime E: type) type {
} }
// import and export of `Layout` implementations // import and export of `Layout` implementations
pub const Pane = @import("layout/Pane.zig").Layout(E); pub const Pane = @import("layout/Pane.zig").Layout(Event);
}; };
} }

View File

@@ -2,37 +2,42 @@ const std = @import("std");
const lib_event = @import("../event.zig"); const lib_event = @import("../event.zig");
const widget = @import("../widget.zig"); const widget = @import("../widget.zig");
pub fn Layout(comptime E: type) type { pub fn Layout(comptime Event: type) type {
if (!lib_event.isTaggedUnion(E)) { if (!lib_event.isTaggedUnion(Event)) {
@compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
} }
return struct { return struct {
pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); w: widget.Widget(Event) = undefined,
events: std.ArrayList(Event) = undefined,
w: widget.Widget(E) = undefined,
c: std.ArrayList(u8) = 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 .{ return .{
.w = w, .w = w,
.events = std.ArrayList(Event).init(allocator),
.c = std.ArrayList(u8).init(allocator), .c = std.ArrayList(u8).init(allocator),
}; };
} }
pub fn deinit(this: *@This()) void { pub fn deinit(this: *@This()) void {
this.w.deinit(); this.w.deinit();
this.events.deinit();
this.c.deinit(); this.c.deinit();
this.* = undefined; this.* = undefined;
} }
pub fn handle(this: *@This(), event: Event) Event { pub fn handle(this: *@This(), event: Event) !*std.ArrayList(Event) {
return this.w.handle(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) { pub fn content(this: *@This()) !*std.ArrayList(u8) {
const widget_content = this.w.content(); const widget_content = try this.w.content();
this.c.clearRetainingCapacity(); this.c.clearRetainingCapacity();
this.c.appendSlice(widget_content.items) catch @panic("OOM"); try this.c.appendSlice(widget_content.items);
return &this.c; return &this.c;
} }
}; };

View File

@@ -1,18 +1,15 @@
const std = @import("std"); const std = @import("std");
const terminal = @import("terminal.zig");
const zlog = @import("zlog"); const zlog = @import("zlog");
const terminal = @import("terminal.zig"); const App = @import("app.zig").App(union(enum) {});
const UserEvent = union(enum) {};
const Widget = @import("widget.zig").Widget(UserEvent);
const Layout = @import("layout.zig").Layout(UserEvent);
pub const std_options = zlog.std_options; pub const std_options = zlog.std_options;
const log = std.log.scoped(.main); const log = std.log.scoped(.main);
pub fn main() !void { pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer { defer {
const deinit_status = gpa.deinit(); const deinit_status = gpa.deinit();
@@ -23,18 +20,46 @@ pub fn main() !void {
} }
const allocator = gpa.allocator(); 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 = App.Widget.RawText.init(allocator);
var rawText = Widget.RawText.init(allocator); const widget = App.Widget.createFrom(&rawText);
const widget = Widget.createFrom(&rawText); var layout = App.Layout.Pane.init(allocator, widget);
var layout = Layout.Pane.init(allocator, widget);
defer layout.deinit(); defer layout.deinit();
// single 'draw' loop try app.start();
_ = layout.handle(.none); defer app.stop() catch unreachable;
log.debug("Layout result: {s}", .{layout.content().items});
// 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? // how would I draw?
// use array for screen contents? <-> support partial re-draws // use array for screen contents? <-> support partial re-draws
// support widget type drawing similar to the already existing widgets // support widget type drawing similar to the already existing widgets
@@ -43,3 +68,7 @@ pub fn main() !void {
// - contents of corresponding locations // - contents of corresponding locations
// resize event // resize event
} }
test {
_ = @import("queue.zig");
}

322
src/queue.zig Normal file
View File

@@ -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();
}

View File

@@ -1,8 +1,5 @@
const std = @import("std"); const std = @import("std");
const posix = std.posix;
const fmt = std.fmt;
const log = std.log.scoped(.terminal); const log = std.log.scoped(.terminal);
pub const Size = struct { pub const Size = struct {
@@ -26,20 +23,44 @@ pub const ReportMode = enum {
/// Gets number of rows and columns in the terminal /// Gets number of rows and columns in the terminal
pub fn getTerminalSize() Size { pub fn getTerminalSize() Size {
var ws: posix.winsize = undefined; var ws: std.posix.winsize = undefined;
_ = posix.system.ioctl(posix.STDERR_FILENO, posix.T.IOCGWINSZ, &ws); _ = std.posix.system.ioctl(std.posix.STDERR_FILENO, std.posix.T.IOCGWINSZ, &ws);
return .{ .cols = ws.ws_col, .rows = ws.ws_row }; 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 { pub fn getCursorPosition() !Position {
// Needs Raw mode (no wait for \n) to work properly cause // Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it. // 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; var buf: [64]u8 = undefined;
// format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R" // 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])) { if (!isCursorPosition(buf[0..len])) {
return error.InvalidValueReturned; return error.InvalidValueReturned;
@@ -73,8 +94,8 @@ pub fn getCursorPosition() !Position {
} }
return .{ return .{
.row = try fmt.parseInt(u16, row[0..ridx], 10), .row = try std.fmt.parseInt(u16, row[0..ridx], 10),
.col = try fmt.parseInt(u16, col[0..cidx], 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 /// `bak`: pointer to store termios struct backup before
/// altering, this is used to disable raw mode. /// altering, this is used to disable raw mode.
pub fn enableRawMode(bak: *posix.termios) !void { pub fn enableRawMode(bak: *std.posix.termios) !void {
var termios = try posix.tcgetattr(posix.STDIN_FILENO); var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO);
bak.* = termios; bak.* = termios;
termios.iflag.IXON = false; termios.iflag.IXON = false;
@@ -114,18 +135,18 @@ pub fn enableRawMode(bak: *posix.termios) !void {
termios.lflag.IEXTEN = false; termios.lflag.IEXTEN = false;
termios.lflag.ISIG = false; termios.lflag.ISIG = false;
try posix.tcsetattr( try std.posix.tcsetattr(
posix.STDIN_FILENO, std.posix.STDIN_FILENO,
posix.TCSA.FLUSH, .FLUSH,
termios, termios,
); );
} }
/// Reverts `enableRawMode` to restore initial functionality. /// Reverts `enableRawMode` to restore initial functionality.
pub fn disableRawMode(bak: *posix.termios) !void { pub fn disableRawMode(bak: *std.posix.termios) !void {
try posix.tcsetattr( try std.posix.tcsetattr(
posix.STDIN_FILENO, std.posix.STDIN_FILENO,
posix.TCSA.FLUSH, .FLUSH,
bak.*, bak.*,
); );
} }
@@ -134,12 +155,12 @@ pub fn disableRawMode(bak: *posix.termios) !void {
pub fn canSynchornizeOutput() !bool { pub fn canSynchornizeOutput() !bool {
// Needs Raw mode (no wait for \n) to work properly cause // Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it. // 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; var buf: [64]u8 = undefined;
// format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y" // 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) { if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
return false; return false;
} }

View File

@@ -1,6 +1,6 @@
//! Dynamic dispatch for widget implementations. //! Dynamic dispatch for widget implementations.
//! Each widget should at last implement these functions: //! 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) {} //! - content(this: *@This()) *std.ArrayList(u8) {}
//! - deinit(this: *@This()) void {} //! - deinit(this: *@This()) void {}
//! //!
@@ -13,18 +13,17 @@
const std = @import("std"); const std = @import("std");
const lib_event = @import("event.zig"); const lib_event = @import("event.zig");
pub fn Widget(comptime E: type) type { pub fn Widget(comptime Event: type) type {
if (!lib_event.isTaggedUnion(E)) { if (!lib_event.isTaggedUnion(Event)) {
@compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
} }
return struct { return struct {
pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E);
const WidgetType = @This(); const WidgetType = @This();
const Ptr = usize; const Ptr = usize;
const VTable = struct { const VTable = struct {
handle: *const fn (this: *WidgetType, event: Event) Event, handle: *const fn (this: *WidgetType, event: Event) ?Event,
content: *const fn (this: *WidgetType) *std.ArrayList(u8), content: *const fn (this: *WidgetType) anyerror!*std.ArrayList(u8),
deinit: *const fn (this: *WidgetType) void, deinit: *const fn (this: *WidgetType) void,
}; };
@@ -32,13 +31,13 @@ pub fn Widget(comptime E: type) type {
vtable: *const VTable = undefined, vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Widget`. // 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 this.vtable.handle(this, event);
} }
// Return the entire content of this `Widget`. // Return the entire content of this `Widget`.
pub fn content(this: *WidgetType) *std.ArrayList(u8) { pub fn content(this: *WidgetType) !*std.ArrayList(u8) {
return this.vtable.content(this); return try this.vtable.content(this);
} }
pub fn deinit(this: *WidgetType) void { pub fn deinit(this: *WidgetType) void {
@@ -52,16 +51,16 @@ pub fn Widget(comptime E: type) type {
.vtable = &.{ .vtable = &.{
.handle = struct { .handle = struct {
// Handle the provided `Event` for this `Widget`. // 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); const widget: @TypeOf(object) = @ptrFromInt(this.object);
return widget.handle(event); return widget.handle(event);
} }
}.handle, }.handle,
.content = struct { .content = struct {
// Return the entire content of this `Widget`. // 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); const widget: @TypeOf(object) = @ptrFromInt(this.object);
return widget.content(); return try widget.content();
} }
}.content, }.content,
.deinit = struct { .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`) // 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 Header = @import("widget/Header.zig");
// pub const ViewPort = @import("widget/ViewPort.zig"); // pub const ViewPort = @import("widget/ViewPort.zig");
// pub const PopupMenu = @import("widget/PopupMenu.zig"); // pub const PopupMenu = @import("widget/PopupMenu.zig");

View File

@@ -1,21 +1,15 @@
const std = @import("std"); const std = @import("std");
const lib_event = @import("../event.zig"); const lib_event = @import("../event.zig");
pub fn Widget(comptime E: type) type { pub fn Widget(comptime Event: type) type {
if (!lib_event.isTaggedUnion(E)) { if (!lib_event.isTaggedUnion(Event)) {
@compileError("Provided user event `E` for `Layout(comptime E: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
} }
return struct { return struct {
pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E);
c: std.ArrayList(u8) = undefined, c: std.ArrayList(u8) = undefined,
pub fn init(allocator: std.mem.Allocator) @This() { pub fn init(allocator: std.mem.Allocator) @This() {
var c = std.ArrayList(u8).init(allocator); return .{ .c = std.ArrayList(u8).init(allocator) };
c.appendSlice("This is a simple test") catch @panic("OOM");
return .{
.c = c,
};
} }
pub fn deinit(this: *@This()) void { pub fn deinit(this: *@This()) void {
@@ -23,14 +17,16 @@ pub fn Widget(comptime E: type) type {
this.* = undefined; this.* = undefined;
} }
pub fn handle(this: *@This(), event: Event) Event { pub fn handle(this: *@This(), event: Event) ?Event {
// ignore the event for now // ignore the event for now
_ = this; _ = this;
_ = event; _ = 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; return &this.c;
} }
}; };