mod(tui): create own terminal interface framework
This commit is contained in:
90
src/app.zig
Normal file
90
src/app.zig
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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)`.");
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
59
src/main.zig
59
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");
|
||||
}
|
||||
|
||||
322
src/queue.zig
Normal file
322
src/queue.zig
Normal 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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user