From 0330b3a2f54583fca864bada3c8373be8bd7eb11 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sat, 2 Nov 2024 17:52:44 +0100 Subject: [PATCH] add: layout and widget dynamic dispatch with interface definitions --- README.md | 7 ++ src/event.zig | 86 +++++++++++++++++++++ src/layout.zig | 81 ++++++++++++++++++++ src/layout/Pane.zig | 39 ++++++++++ src/main.zig | 145 ++++++----------------------------- src/terminal.zig | 159 +++++++++++++++++++++++++++++++++++++++ src/widget.zig | 118 +++++++++++++++++------------ src/widget/Header.zig | 127 ------------------------------- src/widget/PopupMenu.zig | 80 -------------------- src/widget/RawText.zig | 37 +++++++++ src/widget/ViewPort.zig | 103 ------------------------- 11 files changed, 502 insertions(+), 480 deletions(-) create mode 100644 src/event.zig create mode 100644 src/layout.zig create mode 100644 src/layout/Pane.zig create mode 100644 src/terminal.zig delete mode 100644 src/widget/Header.zig delete mode 100644 src/widget/PopupMenu.zig create mode 100644 src/widget/RawText.zig delete mode 100644 src/widget/ViewPort.zig diff --git a/README.md b/README.md index 859e9a7..24733a9 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,13 @@ It contains information about me and my projects as well as blog entries about s - they are instead showed locally, which might cause issues with the docker container running in the background - very likely it is `tui-website` which causes this issue - not entirely as inputs are not passed through correctly to the below running application (i.e. `diffnav` via `serve git diff`) + - fex however works as expected - [ ] Improve navigation - [ ] Have clickable/navigatable links inside of the tui application - [ ] Launch simple http server alongside tui application + +--- + +## Branch: `own-tty-visuals` + +- [ ] How can I support to run a sub-process inside of a given pane / layout? diff --git a/src/event.zig b/src/event.zig new file mode 100644 index 0000000..8c49dd4 --- /dev/null +++ b/src/event.zig @@ -0,0 +1,86 @@ +//! Events which are defined by the library. They might be extended by user +//! events. +const std = @import("std"); +const terminal = @import("terminal.zig"); + +// Application events which contain information about default application +// parameter. Either `none` or `err`, where `none` represents no event with no +// 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) { + none, + 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) { + resize: terminal.Size, +}; + +pub const BuiltinEvent = MergeTaggedUnions(SystemEvent, ApplicationEvent); + +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)`."); + } + const a_fields = @typeInfo(A).Union.fields; + const a_fields_tag = @typeInfo(A).Union.tag_type.?; + const a_enum_fields = @typeInfo(a_fields_tag).Enum.fields; + const b_fields = @typeInfo(B).Union.fields; + const b_fields_tag = @typeInfo(B).Union.tag_type.?; + const b_enum_fields = @typeInfo(b_fields_tag).Enum.fields; + var fields: [a_fields.len + b_fields.len]std.builtin.Type.UnionField = undefined; + var enum_fields: [a_fields.len + b_fields.len]std.builtin.Type.EnumField = undefined; + var i: usize = 0; + for (a_fields, a_enum_fields) |field, enum_field| { + fields[i] = field; + var enum_f = enum_field; + enum_f.value = i; + enum_fields[i] = enum_f; + i += 1; + } + for (b_fields, b_enum_fields) |field, enum_field| { + fields[i] = field; + var enum_f = enum_field; + enum_f.value = i; + enum_fields[i] = enum_f; + i += 1; + } + + const log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i); + + const EventType = @Type(.{ .Int = .{ + .signedness = .unsigned, + .bits = log2_i, + } }); + + const Event = @Type(.{ .Enum = .{ + .tag_type = EventType, + .fields = enum_fields[0..], + .decls = &.{}, + .is_exhaustive = true, + } }); + + return @Type(.{ .Union = .{ + .layout = .auto, + .tag_type = Event, + .fields = fields[0..], + .decls = &.{}, + } }); +} + +// Determine at `comptime` wether the provided type `E` is an `union(enum)`. +pub fn isTaggedUnion(comptime E: type) bool { + switch (@typeInfo(E)) { + .Union => |u| { + if (u.tag_type) |_| {} else { + return false; + } + }, + else => return false, + } + return true; +} diff --git a/src/layout.zig b/src/layout.zig new file mode 100644 index 0000000..57a1be1 --- /dev/null +++ b/src/layout.zig @@ -0,0 +1,81 @@ +//! 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) {} +//! - deinit(this: *@This()) void {} +//! +//! Create a `Layout` using `createFrom(object: anytype)` and use them through +//! the defined interface. The layout will take care of calling the correct +//! implementation of the corresponding underlying type. +//! +//! Each `Layout` is responsible for clearing the allocated memory of the used +//! widgets when deallocated. This means that `deinit()` will also deallocate +//! every used widget too. +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)`."); + } + 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), + deinit: *const fn (this: *LayoutType) void, + }; + + object: Ptr = undefined, + 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); + } + + // Return the entire content of this `Widget`. + pub fn content(this: *LayoutType) *std.ArrayList(u8) { + return this.vtable.content(this); + } + + pub fn deinit(this: *LayoutType) void { + this.vtable.deinit(this); + this.* = undefined; + } + + pub fn createFrom(object: anytype) LayoutType { + return LayoutType{ + .object = @intFromPtr(object), + .vtable = &.{ + .handle = struct { + // Handle the provided `Event` for this `Widget`. + fn handle(this: *LayoutType, event: Event) Event { + const layout: @TypeOf(object) = @ptrFromInt(this.object); + return layout.handle(event); + } + }.handle, + .content = struct { + // Return the entire content of this `Widget`. + fn content(this: *LayoutType) *std.ArrayList(u8) { + const layout: @TypeOf(object) = @ptrFromInt(this.object); + return layout.content(); + } + }.content, + .deinit = struct { + fn deinit(this: *LayoutType) void { + const layout: @TypeOf(object) = @ptrFromInt(this.object); + layout.deinit(); + } + }.deinit, + }, + }; + } + + // import and export of `Layout` implementations + pub const Pane = @import("layout/Pane.zig").Layout(E); + }; +} diff --git a/src/layout/Pane.zig b/src/layout/Pane.zig new file mode 100644 index 0000000..629dd59 --- /dev/null +++ b/src/layout/Pane.zig @@ -0,0 +1,39 @@ +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)`."); + } + return struct { + pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); + + w: widget.Widget(E) = undefined, + c: std.ArrayList(u8) = undefined, + + pub fn init(allocator: std.mem.Allocator, w: widget.Widget(E)) @This() { + return .{ + .w = w, + .c = std.ArrayList(u8).init(allocator), + }; + } + + pub fn deinit(this: *@This()) void { + this.w.deinit(); + this.c.deinit(); + this.* = undefined; + } + + pub fn handle(this: *@This(), event: Event) Event { + return this.w.handle(event); + } + + pub fn content(this: *@This()) *std.ArrayList(u8) { + const widget_content = this.w.content(); + this.c.clearRetainingCapacity(); + this.c.appendSlice(widget_content.items) catch @panic("OOM"); + return &this.c; + } + }; +} diff --git a/src/main.zig b/src/main.zig index 0fdd2ce..d0d6dc8 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,142 +1,45 @@ const std = @import("std"); -const vaxis = @import("vaxis"); const zlog = @import("zlog"); -const widget = @import("widget.zig"); +const terminal = @import("terminal.zig"); -const TextInput = vaxis.widgets.TextInput; -const Event = widget.Event; +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; +const log = std.log.scoped(.main); pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; defer { const deinit_status = gpa.deinit(); - //fail test; can't try in defer as defer is executed after we return + // fail test; can't try in defer as defer is executed after we return if (deinit_status == .leak) { std.log.err("memory leak", .{}); } } - const alloc = gpa.allocator(); + const allocator = gpa.allocator(); - // Initialize a tty - var tty = try vaxis.Tty.init(); - defer tty.deinit(); + const size = terminal.getTerminalSize(); - // Initialize Vaxis - var vx = try vaxis.init(alloc, .{}); - // deinit takes an optional allocator. If your program is exiting, you can - // choose to pass a null allocator to save some exit time. - defer vx.deinit(alloc, tty.anyWriter()); + 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); + defer layout.deinit(); - // The event loop requires an intrusive init. We create an instance with - // stable pointers to Vaxis and our TTY, then init the instance. Doing so - // installs a signal handler for SIGWINCH on posix TTYs - // - // This event loop is thread safe. It reads the tty in a separate thread - var loop: vaxis.Loop(Event) = .{ - .tty = &tty, - .vaxis = &vx, - }; - try loop.init(); + // single 'draw' loop + _ = layout.handle(.none); + log.debug("Layout result: {s}", .{layout.content().items}); - // Start the read loop. This puts the terminal in raw mode and begins - // reading user input - try loop.start(); - defer loop.stop(); - - // Optionally enter the alternate screen - try vx.enterAltScreen(tty.anyWriter()); - - var header = try widget.Header.init(alloc, &vx.unicode); - defer header.deinit(); - - var view_port = widget.ViewPort.init(alloc, &vx.unicode); - defer view_port.deinit(); - - var active_menu = false; - var menu = widget.PopupMenu.init(alloc, &vx.unicode); - defer menu.deinit(); - - // Sends queries to terminal to detect certain features. This should always - // be called after entering the alt screen, if you are using the alt screen - try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); - - loop.postEvent(.{ .path = "./doc/home.md" }); - - while (true) { - const event = loop.nextEvent(); - // update widgets - header.update(event); - view_port.update(event); - if (active_menu) { - if (menu.update(event)) |e| { - _ = loop.tryPostEvent(e); - active_menu = false; - } - } - - switch (event) { - .key_press => |key| { - if (active_menu) { - if (key.matches(vaxis.Key.escape, .{})) { - active_menu = false; - } - } - - if (key.matches('c', .{ .ctrl = true })) { - break; - } else if (key.matches(vaxis.Key.space, .{})) { - active_menu = true; - } else if (key.matches('l', .{ .ctrl = true })) { - vx.queueRefresh(); - } - }, - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), - else => {}, - } - - var root_window = vx.window(); - root_window.clear(); - // FIXME: this should not be necessary to clear the contents - try vx.render(tty.anyWriter()); // re-draw after clear! - - header.draw(root_window.child(.{ - .x_off = 0, - .y_off = 0, - .height = .{ .limit = 3 }, - .border = .{ .where = .all }, - })); - - // should be 120 characters wide and centered horizontally - var view_port_x_off: usize = undefined; - var limit: usize = 120; - if (root_window.width / 2 -| 60 > 0) { - view_port_x_off = root_window.width / 2 -| 60; - } else { - view_port_x_off = 1; - limit = root_window.width - 1; - } - view_port.draw(root_window.child(.{ - .x_off = view_port_x_off, - .y_off = 3, - .width = .{ .limit = limit }, - })); - - if (active_menu) { - menu.draw(root_window.child(.{ - .x_off = root_window.width / 2 -| 25, - .y_off = root_window.height / 2 -| 10, - .width = .{ .limit = 50 }, - .height = .{ .limit = 20 }, - .border = .{ .where = .all }, - })); - } - - // Render the screen. Using a buffered writer will offer much better - // performance, but is not required - try vx.render(tty.anyWriter()); - } + // how would I draw? + // use array for screen contents? <-> support partial re-draws + // support widget type drawing similar to the already existing widgets + // determine the corresponding capabilities of the terminal? + // support layouts + // - contents of corresponding locations + // resize event } diff --git a/src/terminal.zig b/src/terminal.zig new file mode 100644 index 0000000..f49c94b --- /dev/null +++ b/src/terminal.zig @@ -0,0 +1,159 @@ +const std = @import("std"); + +const posix = std.posix; +const fmt = std.fmt; + +const log = std.log.scoped(.terminal); + +pub const Size = struct { + cols: u16, + rows: u16, +}; + +pub const Position = struct { + col: u16, + row: u16, +}; + +// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html +pub const ReportMode = enum { + not_recognized, + set, + reset, + permanently_set, + permanently_reset, +}; + +/// 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); + return .{ .cols = ws.ws_col, .rows = ws.ws_row }; +} + +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"); + + var buf: [64]u8 = undefined; + + // format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R" + const len = try posix.read(posix.STDIN_FILENO, &buf); + + if (!isCursorPosition(buf[0..len])) { + return error.InvalidValueReturned; + } + + var row: [8]u8 = undefined; + var col: [8]u8 = undefined; + + var ridx: u3 = 0; + var cidx: u3 = 0; + + var is_parsing_cols = false; + for (2..(len - 1)) |i| { + const b = buf[i]; + if (b == ';') { + is_parsing_cols = true; + continue; + } + + if (b == 'R') { + break; + } + + if (is_parsing_cols) { + col[cidx] = buf[i]; + cidx += 1; + } else { + row[ridx] = buf[i]; + ridx += 1; + } + } + + return .{ + .row = try fmt.parseInt(u16, row[0..ridx], 10), + .col = try fmt.parseInt(u16, col[0..cidx], 10), + }; +} + +/// Function is more of a heuristic as opposed +/// to an exact check. +pub fn isCursorPosition(buf: []u8) bool { + if (buf.len < 6) { + return false; + } + + if (buf[0] != 27 or buf[1] != '[') { + return false; + } + + return true; +} + +/// Sets the following +/// - IXON: disables start/stop output flow (reads CTRL-S, CTRL-Q) +/// - ICRNL: disables CR to NL translation (reads CTRL-M) +/// - IEXTEN: disable implementation defined functions (reads CTRL-V, CTRL-O) +/// - ECHO: user input is not printed to terminal +/// - ICANON: read runs for every input (no waiting for `\n`) +/// - ISIG: disable QUIT, ISIG, SUSP. +/// +/// `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); + bak.* = termios; + + termios.iflag.IXON = false; + termios.iflag.ICRNL = false; + + termios.lflag.ECHO = false; + termios.lflag.ICANON = false; + termios.lflag.IEXTEN = false; + termios.lflag.ISIG = false; + + try posix.tcsetattr( + posix.STDIN_FILENO, + posix.TCSA.FLUSH, + termios, + ); +} + +/// Reverts `enableRawMode` to restore initial functionality. +pub fn disableRawMode(bak: *posix.termios) !void { + try posix.tcsetattr( + posix.STDIN_FILENO, + posix.TCSA.FLUSH, + bak.*, + ); +} + +// Ref +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"); + + var buf: [64]u8 = undefined; + + // format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y" + const len = try posix.read(posix.STDIN_FILENO, &buf); + if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) { + return false; + } + + // Check value of n + return getReportMode(buf[8]) == .reset; +} + +fn getReportMode(ps: u8) ReportMode { + return switch (ps) { + '1' => ReportMode.set, + '2' => ReportMode.reset, + '3' => ReportMode.permanently_set, + '4' => ReportMode.permanently_reset, + else => ReportMode.not_recognized, + }; +} diff --git a/src/widget.zig b/src/widget.zig index 5c8b8a7..e4fc387 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -1,63 +1,83 @@ -//! Dynamic dispatch for widget implementations -//! Each widget should at last implement these two methods: -//! - update(this: *@This(), event: Event) void {} -//! - draw(this: *@This(), win: vaxis.Window) void {} +//! Dynamic dispatch for widget implementations. +//! Each widget should at last implement these functions: +//! - handle(this: *@This(), event: Event) Event {} +//! - content(this: *@This()) *std.ArrayList(u8) {} +//! - deinit(this: *@This()) void {} //! //! Create a `Widget` using `createFrom(object: anytype)` and use them through //! the defined interface. The widget will take care of calling the correct //! implementation of the corresponding underlying type. +//! +//! Each `Widget` may cache its content and should if the contents will not +//! change for a long time. +const std = @import("std"); +const lib_event = @import("event.zig"); -const vaxis = @import("vaxis"); +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)`."); + } + return struct { + pub const Event = lib_event.MergeTaggedUnions(lib_event.BuiltinEvent, E); + const WidgetType = @This(); + const Ptr = usize; -pub const Event = union(enum) { - key_press: vaxis.Key, - winsize: vaxis.Winsize, - path: []const u8, -}; + const VTable = struct { + handle: *const fn (this: *WidgetType, event: Event) Event, + content: *const fn (this: *WidgetType) *std.ArrayList(u8), + deinit: *const fn (this: *WidgetType) void, + }; -const Ptr = usize; + object: Ptr = undefined, + vtable: *const VTable = undefined, -object: Ptr = undefined, -vtable: *const VTable = undefined, + // Handle the provided `Event` for this `Widget`. + pub fn handle(this: *WidgetType, event: Event) Event { + return this.vtable.handle(this, event); + } -const VTable = struct { - update: *const fn (this: *@This(), event: Event) void, - draw: *const fn (this: *@This(), win: vaxis.Window) void, -}; + // Return the entire content of this `Widget`. + pub fn content(this: *WidgetType) *std.ArrayList(u8) { + return this.vtable.content(this); + } -/// Update loop for a given widget to react to the provided `Event`. It may -/// change its internal state, update variables, react to user input, etc. -pub fn update(this: *@This(), event: Event) void { - this.vtable.update(this, event); -} + pub fn deinit(this: *WidgetType) void { + this.vtable.deinit(this); + this.* = undefined; + } -/// Draw a given widget using the provided `vaxis.Window`. The window controls -/// the dimension one widget may take on the screen. The widget itself has no -/// control over this. -pub fn draw(this: *@This(), win: vaxis.Window) void { - this.vtable.draw(this, win); -} + pub fn createFrom(object: anytype) WidgetType { + return WidgetType{ + .object = @intFromPtr(object), + .vtable = &.{ + .handle = struct { + // Handle the provided `Event` for this `Widget`. + 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) { + const widget: @TypeOf(object) = @ptrFromInt(this.object); + return widget.content(); + } + }.content, + .deinit = struct { + fn deinit(this: *WidgetType) void { + const widget: @TypeOf(object) = @ptrFromInt(this.object); + widget.deinit(); + } + }.deinit, + }, + }; + } -pub fn createFrom(object: anytype) @This() { - return @This(){ - .object = @intFromPtr(object), - .vtable = &.{ - .update = struct { - fn update(this: *@This(), event: Event) void { - const widget: @TypeOf(object) = @ptrFromInt(this.object); - widget.update(event); - } - }.update, - .draw = struct { - fn draw(this: *@This(), win: vaxis.Window) void { - const widget: @TypeOf(object) = @ptrFromInt(this.object); - widget.draw(win); - } - }.draw, - }, + // TODO: import and export of `Widget` implementations (with corresponding intialization using `Event`) + pub const RawText = @import("widget/RawText.zig").Widget(E); + // pub const Header = @import("widget/Header.zig"); + // pub const ViewPort = @import("widget/ViewPort.zig"); + // pub const PopupMenu = @import("widget/PopupMenu.zig"); }; } - -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/Header.zig b/src/widget/Header.zig deleted file mode 100644 index c8b1732..0000000 --- a/src/widget/Header.zig +++ /dev/null @@ -1,127 +0,0 @@ -//! Header widget, which shows the name of the website and the main navigation entries -const std = @import("std"); -const vaxis = @import("vaxis"); - -const widget = @import("../widget.zig"); - -const Event = widget.Event; - -allocator: std.mem.Allocator = undefined, -unicode: *const vaxis.Unicode = undefined, -path: ?[]const u8 = undefined, -view: ?vaxis.widgets.View = undefined, - -pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) !@This() { - return .{ - .allocator = allocator, - .unicode = unicode, - .path = null, - .view = null, - }; -} - -pub fn deinit(this: *@This()) void { - if (this.view) |*view| { - view.*.deinit(); - } - if (this.path) |*path| { - this.allocator.free(path.*); - } - this.* = undefined; -} - -fn fillView(this: *@This()) void { - this.view.?.clear(); - - const msg = "Yves Biener"; - for (msg, 0..) |_, i| { - const cell: vaxis.Cell = .{ - // each cell takes a _grapheme_ as opposed to a single - // codepoint. This allows Vaxis to handle emoji properly, - // particularly with terminals that the Unicode Core extension - // (IE Mode 2027) - .char = .{ .grapheme = msg[i .. i + 1] }, - .style = .{ - .fg = .{ .index = 6 }, - .bold = true, - }, - }; - this.view.?.writeCell(i + 1, 0, cell); - } - - if (this.path) |path| { - for (0..path.len, this.view.?.screen.width / 2 - path.len / 2..) |i, col| { - const cell: vaxis.Cell = .{ - .char = .{ .grapheme = path[i .. i + 1] }, - .style = .{ - .ul_style = .single, - }, - }; - this.view.?.writeCell(col, 0, cell); - } - } - - // github - { - const cell: vaxis.Cell = .{ - .link = .{ .uri = "https://github.com/yves-biener" }, - .char = .{ .grapheme = "github" }, - .style = .{ - .fg = .{ .index = 3 }, - .ul_style = .single, - }, - }; - this.view.?.writeCell(this.view.?.screen.width - 9, 0, cell); - } - // mail - { - const cell: vaxis.Cell = .{ - .link = .{ .uri = "mailto:yves.biener@gmx.de" }, - .char = .{ .grapheme = "mail" }, - .style = .{ - .fg = .{ .index = 3 }, - .ul_style = .single, - }, - }; - this.view.?.writeCell(this.view.?.screen.width - 16, 0, cell); - } -} - -/// Update loop for a given widget to react to the provided `Event`. It may -/// change its internal state, update variables, react to user input, etc. -pub fn update(this: *@This(), event: Event) void { - switch (event) { - .winsize => |ws| { - if (this.view) |*view| { - if (ws.cols != view.screen.width) { - view.*.deinit(); - this.view = vaxis.widgets.View.init(this.allocator, this.unicode, .{ .width = ws.cols, .height = ws.rows }) catch @panic("OOM"); - this.fillView(); - } - } else { - this.view = vaxis.widgets.View.init(this.allocator, this.unicode, .{ .width = ws.cols, .height = ws.rows }) catch @panic("OOM"); - this.fillView(); - } - }, - .path => |path| { - // TODO: try to remove the necessary amount of allocations - if (this.path) |*p| { - this.allocator.free(p.*); - } - const p = this.allocator.alloc(u8, path.len) catch @panic("OOM"); - @memcpy(p, path); - this.path = p; - this.fillView(); - }, - else => {}, - } -} - -/// Draw a given widget using the provided `vaxis.Window`. The window controls -/// the dimension one widget may take on the screen. The widget itself has no -/// control over this. -pub fn draw(this: *@This(), win: vaxis.Window) void { - if (this.view) |*view| { - view.*.draw(win, .{}); - } -} diff --git a/src/widget/PopupMenu.zig b/src/widget/PopupMenu.zig deleted file mode 100644 index 3643033..0000000 --- a/src/widget/PopupMenu.zig +++ /dev/null @@ -1,80 +0,0 @@ -//! Pop-up Menu widget to show the available keybindings - -const std = @import("std"); -const vaxis = @import("vaxis"); - -const converter = @import("node2buffer.zig").Markdown; -const widget = @import("../widget.zig"); - -const Event = widget.Event; - -allocator: std.mem.Allocator = undefined, -unicode: *const vaxis.Unicode = undefined, - -pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) @This() { - return .{ - .allocator = allocator, - .unicode = unicode, - }; -} - -pub fn deinit(this: *@This()) void { - this.* = undefined; -} - -/// Update loop for a given widget to react to the provided `Event`. It may -/// change its internal state, update variables, react to user input, etc. -pub fn update(this: *@This(), event: Event) ?Event { - _ = this; - switch (event) { - .key_press => |key| { - if (key.matches('a', .{})) { - // About - return .{ .path = "./doc/about.md" }; - } - if (key.matches('h', .{})) { - // Home - return .{ .path = "./doc/home.md" }; - } - if (key.matches('t', .{})) { - // test - return .{ .path = "./doc/test.md" }; - } - }, - else => {}, - } - return null; -} - -/// Draw a given widget using the provided `vaxis.Window`. The window controls -/// the dimension one widget may take on the screen. The widget itself has no -/// control over this. -pub fn draw(this: *@This(), win: vaxis.Window) void { - var view = vaxis.widgets.View.init(this.allocator, this.unicode, .{ .width = win.width, .height = win.height }) catch @panic("OOM"); - defer view.deinit(); - - const msg = - \\# Goto - \\ - \\**a** about - \\**h** home - \\**t** test - ; - var cells = std.ArrayList(vaxis.Cell).init(this.allocator); - defer cells.deinit(); - - converter.toBuffer(msg, this.allocator, &cells); - - var col: usize = 0; - var row: usize = 0; - for (cells.items) |cell| { - view.writeCell(col, row, cell); - if (std.mem.eql(u8, cell.char.grapheme, "\n")) { - col = 0; - row += 1; - } else { - col += 1; - } - } - view.draw(win, .{}); -} diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig new file mode 100644 index 0000000..42836fd --- /dev/null +++ b/src/widget/RawText.zig @@ -0,0 +1,37 @@ +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)`."); + } + 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, + }; + } + + pub fn deinit(this: *@This()) void { + this.c.deinit(); + this.* = undefined; + } + + pub fn handle(this: *@This(), event: Event) Event { + // ignore the event for now + _ = this; + _ = event; + return .none; + } + + pub fn content(this: *@This()) *std.ArrayList(u8) { + return &this.c; + } + }; +} diff --git a/src/widget/ViewPort.zig b/src/widget/ViewPort.zig deleted file mode 100644 index 05e7f54..0000000 --- a/src/widget/ViewPort.zig +++ /dev/null @@ -1,103 +0,0 @@ -//! ViewPort widget, which show the content of a file as a pager with corresponding navigation of the view port - -const std = @import("std"); -const vaxis = @import("vaxis"); -const Zmd = @import("zmd").Zmd; - -const converter = @import("node2buffer.zig").Markdown; -const widget = @import("../widget.zig"); - -const Event = widget.Event; - -allocator: std.mem.Allocator = undefined, -unicode: *const vaxis.Unicode = undefined, -contents: ?[]const u8 = null, -buffer: std.ArrayList(vaxis.Cell) = undefined, -view: vaxis.widgets.ScrollView = undefined, - -pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) @This() { - return .{ - .allocator = allocator, - .unicode = unicode, - .contents = null, - .buffer = std.ArrayList(vaxis.Cell).init(allocator), - .view = .{ .vertical_scrollbar = null }, - }; -} - -pub fn deinit(this: *@This()) void { - if (this.contents) |*content| { - this.allocator.free(content.*); - } - this.buffer.deinit(); - this.* = undefined; -} - -/// Update loop for a given widget to react to the provided `Event`. It may -/// change its internal state, update variables, react to user input, etc. -pub fn update(this: *@This(), event: Event) void { - // FIXME: limit scroll-able area to corresponding contents! - switch (event) { - .key_press => |key| { - if (key.matches(vaxis.Key.up, .{}) or key.matches('k', .{})) { - this.view.scroll.y -|= 1; - } else if (key.matches(vaxis.Key.page_up, .{}) or key.matches('u', .{ .ctrl = true })) { - this.view.scroll.y -|= 32; - } else if (key.matches(vaxis.Key.down, .{}) or key.matches('j', .{})) { - this.view.scroll.y +|= 1; - } else if (key.matches(vaxis.Key.page_down, .{}) or key.matches('d', .{ .ctrl = true })) { - this.view.scroll.y +|= 32; - } else if (key.matches(vaxis.Key.end, .{}) or key.matches('G', .{})) { - this.view.scroll.y = std.math.maxInt(usize); - } else if (key.matches(vaxis.Key.home, .{}) or key.matches('g', .{})) { - this.view.scroll.y = 0; - } - }, - .path => |path| { - const file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch |err| { - // TODO: in case of an error show an error-page or an error notification? - std.log.err("could not open file: {s} due to {any}", .{ path, err }); - return; - }; - defer file.close(); - if (this.contents) |*content| { - this.allocator.free(content.*); - } - this.contents = file.readToEndAlloc(this.allocator, 4096) catch @panic("could not read to end"); - - // TODO: support typst files as parser and display driver -> as I'll be using this file format anyway with my personal note system - // - I should leverage the typst compiler! (i.e. maybe use the html export, once that is available?) - - this.buffer.clearRetainingCapacity(); - converter.toBuffer(this.contents.?, this.allocator, &this.buffer); - }, - else => {}, - } -} - -/// Draw a given widget using the provided `vaxis.Window`. The window controls -/// the dimension one widget may take on the screen. The widget itself has no -/// control over this. -pub fn draw(this: *@This(), win: vaxis.Window) void { - // TODO: this is not very performant and should be improved - var rows: usize = 1; - for (this.buffer.items) |cell| { - if (std.mem.eql(u8, cell.char.grapheme, "\n")) { - rows += 1; - } - } - this.view.draw(win, .{ .cols = win.width, .rows = rows }); // needed for scroll bar scaling and display - - const Pos = struct { x: usize = 0, y: usize = 0 }; - var pos: Pos = .{}; - for (this.buffer.items) |cell| { - // NOTE: do not print newline characters, but instead go to the next row - if (std.mem.eql(u8, cell.char.grapheme, "\n")) { - pos.x = 0; - pos.y += 1; - } else { - this.view.writeCell(win, pos.x, pos.y, cell); - pos.x += 1; - } - } -}