diff --git a/build.zig b/build.zig index f05cf3a..8f612a4 100644 --- a/build.zig +++ b/build.zig @@ -15,29 +15,16 @@ pub fn build(b: *std.Build) void { // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - // build options to customize the log message formatting - const benchmark = b.option(bool, "benchmark", "Create a benchmark build (default: false)") orelse false; - - const options = b.addOptions(); - options.addOption(bool, "benchmark", benchmark); - - const options_module = options.createModule(); - // Dependencies - const vaxis_dep = b.dependency("vaxis", .{ - .optimize = optimize, - .target = target, - }); - const zlog_dep = b.dependency("zlog", .{ + const zlog = b.dependency("zlog", .{ .optimize = optimize, .target = target, .timestamp = true, }); - const zmd_dep = b.dependency("zmd", .{ + const zterm = b.dependency("zterm", .{ .optimize = optimize, .target = target, }); - const zg_dep = b.dependency("zg", .{}); const exe = b.addExecutable(.{ .name = "tui-website", @@ -45,11 +32,8 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - exe.root_module.addImport("build_options", options_module); - exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis")); - exe.root_module.addImport("zlog", zlog_dep.module("zlog")); - exe.root_module.addImport("zmd", zmd_dep.module("zmd")); - exe.root_module.addImport("code_point", zg_dep.module("code_point")); + exe.root_module.addImport("zlog", zlog.module("zlog")); + exe.root_module.addImport("zterm", zterm.module("zterm")); // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default diff --git a/build.zig.zon b/build.zig.zon index 92c3069..0a8ab4f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -39,6 +39,10 @@ .url = "https://codeberg.org/dude_the_builder/zg/archive/v0.13.2.tar.gz", .hash = "122055beff332830a391e9895c044d33b15ea21063779557024b46169fb1984c6e40", }, + .zterm = .{ + .url = "git+https://gitea.yves-biener.de/yves-biener/zterm#0cc0ed10d20feadd053aa2c573b73cd8d67edf71", + .hash = "122072281f3dab8b8ce7ce407def708010b5282b9e31d9c998346c9a0094f3b8648f", + }, }, .paths = .{ "build.zig", diff --git a/src/app.zig b/src/app.zig deleted file mode 100644 index baf65e1..0000000 --- a/src/app.zig +++ /dev/null @@ -1,202 +0,0 @@ -//! 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 Key = @import("key.zig"); -const SystemEvent = @import("event.zig").SystemEvent; -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. -/// -/// _R_ is the type function for the `Renderer` to use. The parameter boolean -/// will be set to the _fullscreen_ value at compile time. The corresponding -/// `Renderer` type is accessable through the generated type of this function. -/// -/// _fullscreen_ will be used to configure the `App` and the `Renderer` to -/// respect the corresponding configuration whether to render a fullscreen tui -/// or an inline tui. -/// -/// # Example -/// -/// Create an `App` which renders using the `PlainRenderer` in fullscreen with -/// an empty user Event: -/// -/// ```zig -/// const App = @import("app.zig").App( -/// union(enum) {}, -/// @import("render.zig").PlainRenderer, -/// true, -/// ); -/// // later on use -/// var app: App = .{}; -/// var renderer: App.Renderer = .{}; -/// ``` -pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) 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(SystemEvent, E); - pub const Layout = @import("layout.zig").Layout(Event); - pub const Widget = @import("widget.zig").Widget(Event); - pub const Renderer = R(fullscreen); - - queue: Queue(Event, 256) = .{}, - thread: ?std.Thread = null, - quit_event: std.Thread.ResetEvent = .{}, - termios: ?std.posix.termios = null, - attached_handler: bool = false, - - pub const SignalHandler = struct { - context: *anyopaque, - callback: *const fn (context: *anyopaque) void, - }; - - pub fn start(this: *@This()) !void { - if (this.thread) |_| return; - - if (!this.attached_handler) { - var winch_act = std.posix.Sigaction{ - .handler = .{ .handler = @This().handleWinch }, - .mask = std.posix.empty_sigset, - .flags = 0, - }; - std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null) catch @panic("could not attach signal WINCH"); - - try registerWinch(.{ - .context = this, - .callback = @This().winsizeCallback, - }); - this.attached_handler = true; - } - - this.quit_event.reset(); - this.thread = try std.Thread.spawn(.{}, @This().run, .{this}); - - var termios: std.posix.termios = undefined; - try terminal.enableRawMode(&termios); - if (this.termios) |_| {} else { - this.termios = termios; - } - if (fullscreen) { - try terminal.saveScreen(); - try terminal.enterAltScreen(); - } - } - - pub fn interrupt(this: *@This()) !void { - this.quit_event.set(); - if (fullscreen) { - try terminal.existAltScreen(); - try terminal.restoreScreen(); - } - if (this.thread) |thread| { - thread.join(); - this.thread = null; - } - } - - pub fn stop(this: *@This()) !void { - try this.interrupt(); - if (this.termios) |*termios| { - try terminal.disableRawMode(termios); - if (fullscreen) { - try terminal.existAltScreen(); - try terminal.restoreScreen(); - } - } - this.termios = null; - } - - /// Quit the application loop. - /// This will stop the internal input thread and post a **.quit** `Event`. - pub fn quit(this: *@This()) void { - this.quit_event.set(); - this.postEvent(.quit); - } - - /// 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 winsizeCallback(ptr: *anyopaque) void { - const this: *@This() = @ptrCast(@alignCast(ptr)); - this.postEvent(.{ .resize = terminal.getTerminalSize() }); - } - - var winch_handler: ?SignalHandler = null; - - fn registerWinch(handler: SignalHandler) !void { - if (winch_handler) |_| { - @panic("Cannot register another WINCH handler."); - } - winch_handler = handler; - } - - fn handleWinch(_: c_int) callconv(.C) void { - if (winch_handler) |handler| { - handler.callback(handler.context); - } - } - - fn run(this: *@This()) !void { - // send initial terminal size - // changes are handled by the winch signal handler - // see `App.start` and `App.registerWinch` for details - this.postEvent(.{ .resize = terminal.getTerminalSize() }); - - // thread to read user inputs - var buf: [256]u8 = undefined; - while (true) { - // FIX: I still think that there is a race condition (I'm just waiting 'long' enough) - this.quit_event.timedWait(20 * std.time.ns_per_ms) catch { - const read_bytes = try terminal.read(buf[0..]); - // escape key presses - if (buf[0] == 0x1b and read_bytes > 1) { - switch (buf[1]) { - // TODO: parse corresponding codes - else => {}, - } - } else { - const b = buf[0]; - const key: Key = switch (b) { - 0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } }, - 0x08 => .{ .cp = Key.backspace }, - 0x09 => .{ .cp = Key.tab }, - 0x0a, 0x0d => .{ .cp = Key.enter }, - 0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } }, - 0x1b => escape: { - std.debug.assert(read_bytes == 1); - break :escape .{ .cp = Key.escape }; - }, - 0x7f => .{ .cp = Key.backspace }, - else => { - var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] }; - while (iter.next()) |cp| { - this.postEvent(.{ .key = .{ .cp = cp.code } }); - } - continue; - }, - }; - this.postEvent(.{ .key = key }); - } - continue; - }; - break; - } - } - }; -} diff --git a/src/ctlseqs.zig b/src/ctlseqs.zig deleted file mode 100644 index 4e55448..0000000 --- a/src/ctlseqs.zig +++ /dev/null @@ -1,140 +0,0 @@ -// Queries -pub const primary_device_attrs = "\x1b[c"; -pub const tertiary_device_attrs = "\x1b[=c"; -pub const device_status_report = "\x1b[5n"; -pub const xtversion = "\x1b[>0q"; -pub const decrqm_focus = "\x1b[?1004$p"; -pub const decrqm_sgr_pixels = "\x1b[?1016$p"; -pub const decrqm_sync = "\x1b[?2026$p"; -pub const decrqm_unicode = "\x1b[?2027$p"; -pub const decrqm_color_scheme = "\x1b[?2031$p"; -pub const csi_u_query = "\x1b[?u"; -pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\"; -pub const sixel_geometry_query = "\x1b[?2;1;0S"; - -// mouse. We try for button motion and any motion. terminals will enable the -// last one we tried (any motion). This was added because zellij doesn't -// support any motion currently -// See: https://github.com/zellij-org/zellij/issues/1679 -pub const mouse_set = "\x1b[?1002;1003;1004;1006h"; -pub const mouse_set_pixels = "\x1b[?1002;1003;1004;1016h"; -pub const mouse_reset = "\x1b[?1002;1003;1004;1006;1016l"; - -// in-band window size reports -pub const in_band_resize_set = "\x1b[?2048h"; -pub const in_band_resize_reset = "\x1b[?2048l"; - -// sync -pub const sync_set = "\x1b[?2026h"; -pub const sync_reset = "\x1b[?2026l"; - -// unicode -pub const unicode_set = "\x1b[?2027h"; -pub const unicode_reset = "\x1b[?2027l"; - -// bracketed paste -pub const bp_set = "\x1b[?2004h"; -pub const bp_reset = "\x1b[?2004l"; - -// color scheme updates -pub const color_scheme_request = "\x1b[?996n"; -pub const color_scheme_set = "\x1b[?2031h"; -pub const color_scheme_reset = "\x1b[?2031l"; - -// Key encoding -pub const csi_u_push = "\x1b[>{d}u"; -pub const csi_u_pop = "\x1b[ |u| { - if (u.tag_type) |_| {} else { - return false; - } - }, - else => return false, - } - return true; -} diff --git a/src/key.zig b/src/key.zig deleted file mode 100644 index eec5f8c..0000000 --- a/src/key.zig +++ /dev/null @@ -1,149 +0,0 @@ -//! Keybindings and Modifiers for user input detection and selection. -const std = @import("std"); - -pub const Modifier = struct { - shift: bool = false, - alt: bool = false, - ctrl: bool = false, -}; - -cp: u21, -mod: Modifier = .{}, - -/// Compare _this_ `Key` with an _other_ `Key`. -/// -/// # Example -/// -/// Configure `ctrl+c` to quit the application (done in main event loop of the application): -/// -/// ```zig -/// switch (event) { -/// .quit => break, -/// .key => |key| { -/// // ctrl+c to quit -/// if (terminal.Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { -/// app.quit.set(); -/// } -/// }, -/// else => {}, -/// } -/// ``` -pub fn matches(this: @This(), other: @This()) bool { - return std.meta.eql(this, other); -} - -// codepoints for keys -pub const tab: u21 = 0x09; -pub const enter: u21 = 0x0D; -pub const escape: u21 = 0x1B; -pub const space: u21 = 0x20; -pub const backspace: u21 = 0x7F; - -// kitty key encodings (re-used here) -pub const insert: u21 = 57348; -pub const delete: u21 = 57349; -pub const left: u21 = 57350; -pub const right: u21 = 57351; -pub const up: u21 = 57352; -pub const down: u21 = 57353; -pub const page_up: u21 = 57354; -pub const page_down: u21 = 57355; -pub const home: u21 = 57356; -pub const end: u21 = 57357; -pub const caps_lock: u21 = 57358; -pub const scroll_lock: u21 = 57359; -pub const num_lock: u21 = 57360; -pub const print_screen: u21 = 57361; -pub const pause: u21 = 57362; -pub const menu: u21 = 57363; -pub const f1: u21 = 57364; -pub const f2: u21 = 57365; -pub const f3: u21 = 57366; -pub const f4: u21 = 57367; -pub const f5: u21 = 57368; -pub const f6: u21 = 57369; -pub const f7: u21 = 57370; -pub const f8: u21 = 57371; -pub const f9: u21 = 57372; -pub const f10: u21 = 57373; -pub const f11: u21 = 57374; -pub const f12: u21 = 57375; -pub const f13: u21 = 57376; -pub const f14: u21 = 57377; -pub const f15: u21 = 57378; -pub const @"f16": u21 = 57379; -pub const f17: u21 = 57380; -pub const f18: u21 = 57381; -pub const f19: u21 = 57382; -pub const f20: u21 = 57383; -pub const f21: u21 = 57384; -pub const f22: u21 = 57385; -pub const f23: u21 = 57386; -pub const f24: u21 = 57387; -pub const f25: u21 = 57388; -pub const f26: u21 = 57389; -pub const f27: u21 = 57390; -pub const f28: u21 = 57391; -pub const f29: u21 = 57392; -pub const f30: u21 = 57393; -pub const f31: u21 = 57394; -pub const @"f32": u21 = 57395; -pub const f33: u21 = 57396; -pub const f34: u21 = 57397; -pub const f35: u21 = 57398; -pub const kp_0: u21 = 57399; -pub const kp_1: u21 = 57400; -pub const kp_2: u21 = 57401; -pub const kp_3: u21 = 57402; -pub const kp_4: u21 = 57403; -pub const kp_5: u21 = 57404; -pub const kp_6: u21 = 57405; -pub const kp_7: u21 = 57406; -pub const kp_8: u21 = 57407; -pub const kp_9: u21 = 57408; -pub const kp_decimal: u21 = 57409; -pub const kp_divide: u21 = 57410; -pub const kp_multiply: u21 = 57411; -pub const kp_subtract: u21 = 57412; -pub const kp_add: u21 = 57413; -pub const kp_enter: u21 = 57414; -pub const kp_equal: u21 = 57415; -pub const kp_separator: u21 = 57416; -pub const kp_left: u21 = 57417; -pub const kp_right: u21 = 57418; -pub const kp_up: u21 = 57419; -pub const kp_down: u21 = 57420; -pub const kp_page_up: u21 = 57421; -pub const kp_page_down: u21 = 57422; -pub const kp_home: u21 = 57423; -pub const kp_end: u21 = 57424; -pub const kp_insert: u21 = 57425; -pub const kp_delete: u21 = 57426; -pub const kp_begin: u21 = 57427; -pub const media_play: u21 = 57428; -pub const media_pause: u21 = 57429; -pub const media_play_pause: u21 = 57430; -pub const media_reverse: u21 = 57431; -pub const media_stop: u21 = 57432; -pub const media_fast_forward: u21 = 57433; -pub const media_rewind: u21 = 57434; -pub const media_track_next: u21 = 57435; -pub const media_track_previous: u21 = 57436; -pub const media_record: u21 = 57437; -pub const lower_volume: u21 = 57438; -pub const raise_volume: u21 = 57439; -pub const mute_volume: u21 = 57440; -pub const left_shift: u21 = 57441; -pub const left_control: u21 = 57442; -pub const left_alt: u21 = 57443; -pub const left_super: u21 = 57444; -pub const left_hyper: u21 = 57445; -pub const left_meta: u21 = 57446; -pub const right_shift: u21 = 57447; -pub const right_control: u21 = 57448; -pub const right_alt: u21 = 57449; -pub const right_super: u21 = 57450; -pub const right_hyper: u21 = 57451; -pub const right_meta: u21 = 57452; -pub const iso_level_3_shift: u21 = 57453; -pub const iso_level_5_shift: u21 = 57454; diff --git a/src/layout.zig b/src/layout.zig deleted file mode 100644 index 478b856..0000000 --- a/src/layout.zig +++ /dev/null @@ -1,85 +0,0 @@ -//! Dynamic dispatch for layout implementations. -//! Each layout should at last implement these functions: -//! - 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 -//! 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 isTaggedUnion = @import("event.zig").isTaggedUnion; - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Events = std.ArrayList(Event); - return struct { - const LayoutType = @This(); - const Ptr = usize; - - const VTable = struct { - handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events, - content: *const fn (this: *LayoutType) anyerror!*std.ArrayList(u8), - deinit: *const fn (this: *LayoutType) void, - }; - - object: Ptr = undefined, - vtable: *const VTable = undefined, - - // Handle the provided `Event` for this `Layout`. - pub fn handle(this: *LayoutType, event: Event) !*Events { - return try this.vtable.handle(this, event); - } - - // Return the entire content of this `Layout`. - pub fn content(this: *LayoutType) !*std.ArrayList(u8) { - return try 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 `Layout`. - fn handle(this: *LayoutType, event: Event) !*Events { - const layout: @TypeOf(object) = @ptrFromInt(this.object); - return try layout.handle(event); - } - }.handle, - .content = struct { - // Return the entire content of this `Layout`. - fn content(this: *LayoutType) !*std.ArrayList(u8) { - const layout: @TypeOf(object) = @ptrFromInt(this.object); - return try 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(Event); - pub const HStack = @import("layout/HStack.zig").Layout(Event); - pub const VStack = @import("layout/VStack.zig").Layout(Event); - pub const Padding = @import("layout/Padding.zig").Layout(Event); - pub const Framing = @import("layout/Framing.zig").Layout(Event); - }; -} diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig deleted file mode 100644 index 9ea7abb..0000000 --- a/src/layout/Framing.zig +++ /dev/null @@ -1,100 +0,0 @@ -//! Framing layout for a nested `Layout`s or `Widget`s. -//! -//! # Example -//! ... -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Element = union(enum) { - layout: @import("../layout.zig").Layout(Event), - widget: @import("../widget.zig").Widget(Event), - }; - const Events = std.ArrayList(Event); - const Contents = std.ArrayList(u8); - return struct { - size: terminal.Size = undefined, - contents: Contents = undefined, - element: Element = undefined, - events: Events = undefined, - - pub fn init(allocator: std.mem.Allocator, element: Element) @This() { - return .{ - .contents = Contents.init(allocator), - .element = element, - .events = Events.init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.contents.deinit(); - this.events.deinit(); - switch ((&this.element).*) { - .layout => |*layout| { - layout.deinit(); - }, - .widget => |*widget| { - widget.deinit(); - }, - } - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - // order is important - switch (event) { - .resize => |size| { - this.size = size; - // adjust size according to the containing elements - const sub_event = event; - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(sub_event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(sub_event)) |e| { - try this.events.append(e); - } - }, - } - }, - else => { - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - }, - } - return &this.events; - } - - pub fn content(this: *@This()) !*Contents { - this.contents.clearRetainingCapacity(); - // TODO: padding contents accordingly - switch ((&this.element).*) { - .layout => |*layout| { - const layout_content = try layout.content(); - try this.contents.appendSlice(layout_content.items); - }, - .widget => |*widget| { - try this.contents.appendSlice(try widget.content()); - }, - } - return &this.contents; - } - }; -} diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig deleted file mode 100644 index 87aeea6..0000000 --- a/src/layout/HStack.zig +++ /dev/null @@ -1,133 +0,0 @@ -//! Horizontal Stacking layout for nested `Layout`s and/or `Widget`s. -//! -//! # Example -//! ... -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Widget = @import("../widget.zig").Widget(Event); - const Lay = @import("../layout.zig").Layout(Event); - const Element = union(enum) { - layout: Lay, - widget: Widget, - }; - const Elements = std.ArrayList(Element); - const Events = std.ArrayList(Event); - const Contents = std.ArrayList(u8); - return struct { - // TODO: current focused `Element`? - size: terminal.Size = undefined, - contents: Contents = undefined, - elements: Elements = undefined, - events: Events = undefined, - - pub fn init(allocator: std.mem.Allocator, children: anytype) @This() { - const ArgsType = @TypeOf(children); - const args_type_info = @typeInfo(ArgsType); - if (args_type_info != .Struct) { - @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); - } - const fields_info = args_type_info.Struct.fields; - var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("OOM"); - inline for (comptime fields_info) |field| { - const child = @field(children, field.name); - const ChildType = @TypeOf(child); - if (ChildType == Widget) { - elements.append(.{ .widget = child }) catch {}; - continue; - } - if (ChildType == Lay) { - elements.append(.{ .layout = child }) catch {}; - continue; - } - @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType)); - } - return .{ - .contents = Contents.init(allocator), - .elements = elements, - .events = Events.init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.events.deinit(); - this.contents.deinit(); - for (this.elements.items) |*element| { - switch (element.*) { - .layout => |*layout| { - layout.deinit(); - }, - .widget => |*widget| { - widget.deinit(); - }, - } - } - this.elements.deinit(); - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - // order is important - switch (event) { - .resize => |size| { - this.size = size; - // adjust size according to the containing elements - for (this.elements.items) |*element| { - const sub_event = event; - switch (element.*) { - .layout => |*layout| { - const events = try layout.handle(sub_event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(sub_event)) |e| { - try this.events.append(e); - } - }, - } - } - }, - else => { - for (this.elements.items) |*element| { - switch (element.*) { - .layout => |*layout| { - const events = try layout.handle(event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - } - }, - } - return &this.events; - } - - pub fn content(this: *@This()) !*Contents { - this.contents.clearRetainingCapacity(); - // TODO: concat contents accordingly to create a vertical stack - for (this.elements.items) |*element| { - switch (element.*) { - .layout => |*layout| { - const layout_content = try layout.content(); - try this.contents.appendSlice(layout_content.items); - }, - .widget => |*widget| { - try this.contents.appendSlice(try widget.content()); - }, - } - } - return &this.contents; - } - }; -} diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig deleted file mode 100644 index ea32fa3..0000000 --- a/src/layout/Padding.zig +++ /dev/null @@ -1,100 +0,0 @@ -//! Padding layout for a nested `Layout`s or `Widget`s. -//! -//! # Example -//! ... -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Element = union(enum) { - layout: @import("../layout.zig").Layout(Event), - widget: @import("../widget.zig").Widget(Event), - }; - const Events = std.ArrayList(Event); - const Contents = std.ArrayList(u8); - return struct { - size: terminal.Size = undefined, - contents: Contents = undefined, - element: Element = undefined, - events: Events = undefined, - - pub fn init(allocator: std.mem.Allocator, element: Element) @This() { - return .{ - .contents = Contents.init(allocator), - .element = element, - .events = Events.init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.contents.deinit(); - this.events.deinit(); - switch ((&this.element).*) { - .layout => |*layout| { - layout.deinit(); - }, - .widget => |*widget| { - widget.deinit(); - }, - } - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - // order is important - switch (event) { - .resize => |size| { - this.size = size; - // adjust size according to the containing elements - const sub_event = event; - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(sub_event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(sub_event)) |e| { - try this.events.append(e); - } - }, - } - }, - else => { - switch ((&this.element).*) { - .layout => |*layout| { - const events = try layout.handle(event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - }, - } - return &this.events; - } - - pub fn content(this: *@This()) !*Contents { - this.contents.clearRetainingCapacity(); - // TODO: padding contents accordingly - switch ((&this.element).*) { - .layout => |*layout| { - const layout_content = try layout.content(); - try this.contents.appendSlice(layout_content.items); - }, - .widget => |*widget| { - try this.contents.appendSlice(try widget.content()); - }, - } - return &this.contents; - } - }; -} diff --git a/src/layout/Pane.zig b/src/layout/Pane.zig deleted file mode 100644 index 1d3678c..0000000 --- a/src/layout/Pane.zig +++ /dev/null @@ -1,66 +0,0 @@ -// TODO: remove this `Layout` -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Widget = @import("../widget.zig").Widget(Event); - const Events = std.ArrayList(Event); - const Contents = std.ArrayList(u8); - return struct { - widget: Widget = undefined, - events: Events = undefined, - contents: Contents = undefined, - - pub fn init(allocator: std.mem.Allocator, widget: Widget) @This() { - return .{ - .widget = widget, - .events = Events.init(allocator), - .contents = Contents.init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.widget.deinit(); - this.events.deinit(); - this.contents.deinit(); - this.* = undefined; - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - switch (event) { - .resize => |size| { - const widget_event: Event = .{ - .resize = .{ - .cols = size.cols, - .rows = size.rows -| 2, // remove top and bottom rows - }, - }; - if (this.widget.handle(widget_event)) |e| { - try this.events.append(e); - } - }, - else => { - if (this.widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - return &this.events; - } - - pub fn content(this: *@This()) !*std.ArrayList(u8) { - this.contents.clearRetainingCapacity(); - try this.contents.appendSlice("\n"); - try this.contents.appendSlice(try this.widget.content()); - try this.contents.appendSlice("\n"); - return &this.c; - } - }; -} diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig deleted file mode 100644 index 4332e24..0000000 --- a/src/layout/VStack.zig +++ /dev/null @@ -1,148 +0,0 @@ -//! Vertical Stacking layout for nested `Layout`s and/or `Widget`s. -//! -//! # Example -//! ... -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -const log = std.log.scoped(.layout_vstack); - -pub fn Layout(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Widget = @import("../widget.zig").Widget(Event); - const Lay = @import("../layout.zig").Layout(Event); - const Element = union(enum) { - layout: Lay, - widget: Widget, - }; - const Elements = std.ArrayList(Element); - const Events = std.ArrayList(Event); - const Contents = std.ArrayList(u8); - return struct { - // TODO: current focused `Element`? - size: terminal.Size = undefined, - contents: Contents = undefined, - elements: Elements = undefined, - events: Events = undefined, - - pub fn init(allocator: std.mem.Allocator, children: anytype) @This() { - const ArgsType = @TypeOf(children); - const args_type_info = @typeInfo(ArgsType); - if (args_type_info != .Struct) { - @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); - } - const fields_info = args_type_info.Struct.fields; - var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("OOM"); - inline for (comptime fields_info) |field| { - const child = @field(children, field.name); - const ChildType = @TypeOf(child); - if (ChildType == Widget) { - elements.append(.{ .widget = child }) catch {}; - continue; - } - if (ChildType == Lay) { - elements.append(.{ .layout = child }) catch {}; - continue; - } - @compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType)); - } - return .{ - .contents = Contents.init(allocator), - .elements = elements, - .events = Events.init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.events.deinit(); - this.contents.deinit(); - for (this.elements.items) |*element| { - switch (element.*) { - .layout => |*layout| { - layout.deinit(); - }, - .widget => |*widget| { - widget.deinit(); - }, - } - } - this.elements.deinit(); - } - - pub fn handle(this: *@This(), event: Event) !*Events { - this.events.clearRetainingCapacity(); - // order is important - switch (event) { - .resize => |size| { - this.size = size; - log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows }); - const len: u16 = @truncate(this.elements.items.len); - const rows = size.rows / len; - // adjust size according to the containing elements - for (this.elements.items) |*element| { - const sub_event: Event = .{ - .resize = .{ - .cols = size.cols, - .rows = rows, - }, - }; - switch (element.*) { - .layout => |*layout| { - const events = try layout.handle(sub_event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(sub_event)) |e| { - try this.events.append(e); - } - }, - } - } - }, - else => { - for (this.elements.items) |*element| { - switch (element.*) { - .layout => |*layout| { - const events = try layout.handle(event); - try this.events.appendSlice(events.items); - }, - .widget => |*widget| { - if (widget.handle(event)) |e| { - try this.events.append(e); - } - }, - } - } - }, - } - return &this.events; - } - - pub fn content(this: *@This()) !*Contents { - this.contents.clearRetainingCapacity(); - // TODO: concat contents accordingly to create a horizontal stack - for (this.elements.items, 1..) |*element, i| { - switch (element.*) { - .layout => |*layout| { - const layout_content = try layout.content(); - try this.contents.appendSlice(layout_content.items); - }, - .widget => |*widget| { - const widget_content = try widget.content(); - try this.contents.appendSlice(widget_content); - }, - } - // TODO: support clear positioning of content on the tui screen - if (i != this.elements.items.len) { - try this.contents.appendSlice("\n"); // NOTE: this assumes that the previous content fills all the provided size.rows accordingly with content, such that a newline introduces the start of the next content - } - } - return &this.contents; - } - }; -} diff --git a/src/main.zig b/src/main.zig index f668fc9..57ee98d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,14 +1,15 @@ -const build_options = @import("build_options"); const std = @import("std"); -const terminal = @import("terminal.zig"); const zlog = @import("zlog"); +const zterm = @import("zterm"); -const App = @import("app.zig").App( +const App = zterm.App( union(enum) {}, - @import("render.zig").PlainRenderer, + zterm.Renderer.Direct, true, ); -const Key = @import("key.zig"); +const Key = zterm.Key; +const Layout = App.Layout; +const Widget = App.Widget; pub const std_options = zlog.std_options; const log = std.log.scoped(.default); @@ -29,38 +30,46 @@ pub fn main() !void { var app: App = .{}; var renderer: App.Renderer = .{}; - const file = try std.fs.cwd().openFile("./src/main.zig", .{}); - var rawText = App.Widget.RawText.init(allocator, file); - file.close(); - - const doc = try std.fs.cwd().openFile("./doc/test.md", .{}); - var docText = App.Widget.RawText.init(allocator, doc); - doc.close(); - - var framing = App.Layout.Framing.init(allocator, .{ - .widget = App.Widget.createFrom(&rawText), + var layout = Layout.createFrom(vstack: { + var vstack = Layout.VStack.init(allocator, .{ + Layout.createFrom(framing: { + var framing = Layout.Framing.init(allocator, .{ + .title = .{ + .str = "Welcome to my terminal website", + .style = .{ + .ul = .{ .index = 6 }, + .ul_style = .single, + }, + }, + }, .{ + .widget = Widget.createFrom(header: { + const doc = try std.fs.cwd().openFile("./doc/home.md", .{}); + defer doc.close(); + var header = Widget.RawText.init(allocator, doc); + break :header &header; + }), + }); + break :framing &framing; + }), + Layout.createFrom(margin: { + var margin = Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ + .widget = Widget.createFrom(body: { + const doc = try std.fs.cwd().openFile("./doc/test.md", .{}); + defer doc.close(); + var body = Widget.RawText.init(allocator, doc); + break :body &body; + }), + }); + break :margin &margin; + }), + }); + break :vstack &vstack; }); - var hstack = App.Layout.HStack.init(allocator, .{ - App.Layout.createFrom(&framing), - }); - var vstack = App.Layout.VStack.init(allocator, .{ - App.Widget.createFrom(&docText), - App.Layout.createFrom(&hstack), - }); - var layout = App.Layout.createFrom(&vstack); defer layout.deinit(); try app.start(); defer app.stop() catch unreachable; - // Benchmarking - var instants: std.ArrayList(u64) = undefined; - var benchmark_thread: std.Thread = undefined; - if (comptime build_options.benchmark) { - instants = try std.ArrayList(u64).initCapacity(allocator, 1024); - benchmark_thread = try std.Thread.spawn(.{}, benchmark, .{&app}); - } - // App.Event loop while (true) { const event = app.nextEvent(); @@ -71,19 +80,7 @@ pub fn main() !void { // ctrl+c to quit if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { app.quit(); - } - if (Key.matches(key, .{ .cp = 'n', .mod = .{ .ctrl = true } })) { - try app.interrupt(); - defer app.start() catch @panic("could not start app event loop"); - var child = std.process.Child.init(&.{"hx"}, allocator); - _ = child.spawnAndWait() catch |err| { - app.postEvent(.{ - .err = .{ - .err = err, - .msg = "Spawning Helix failed", - }, - }); - }; + break; // no need to render this frame anyway } }, .err => |err| { @@ -92,45 +89,10 @@ pub fn main() !void { else => {}, } - var start_instant: std.time.Instant = undefined; - if (comptime build_options.benchmark) { - start_instant = try std.time.Instant.now(); - } - - // NOTE: this currently re-renders the screen for every key-press -> which might be a bit of an overkill const events = try layout.handle(event); for (events.items) |e| { app.postEvent(e); } - try renderer.render(try layout.content()); - - if (comptime build_options.benchmark) { - const end_instant = try std.time.Instant.now(); - try instants.append(end_instant.since(start_instant)); - } - } - - if (comptime build_options.benchmark) { - benchmark_thread.join(); - // print benchmark results - for (instants.items, 1..) |epoch, i| { - _ = i; - const epoch_float: f64 = @floatFromInt(epoch); - std.log.info("{d:.3}", .{epoch_float / std.time.ns_per_ms}); - } - instants.deinit(); + try layout.render(&renderer); } } - -fn benchmark(app: *App) void { - std.time.sleep(1 * std.time.ns_per_s); - for (0..512) |_| { - app.postEvent(.{ .key = .{ .cp = 'j' } }); - app.postEvent(.{ .key = .{ .cp = 'k' } }); - } - app.quit(); -} - -test { - _ = @import("queue.zig"); -} diff --git a/src/queue.zig b/src/queue.zig deleted file mode 100644 index a364e57..0000000 --- a/src/queue.zig +++ /dev/null @@ -1,322 +0,0 @@ -// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License) -// with slight modifications -const std = @import("std"); -const assert = std.debug.assert; - -/// Thread safe. Fixed size. Blocking push and pop. -pub fn Queue(comptime T: type, comptime size: usize) type { - return struct { - buf: [size]T = undefined, - - read_index: usize = 0, - write_index: usize = 0, - - mutex: std.Thread.Mutex = .{}, - // blocks when the buffer is full - not_full: std.Thread.Condition = .{}, - // ...or empty - not_empty: std.Thread.Condition = .{}, - - const QueueType = @This(); - - /// Pop an item from the queue. Blocks until an item is available. - pub fn pop(this: *QueueType) T { - this.mutex.lock(); - defer this.mutex.unlock(); - while (this.isEmptyLH()) { - this.not_empty.wait(&this.mutex); - } - assert(!this.isEmptyLH()); - if (this.isFullLH()) { - // If we are full, wake up a push that might be - // waiting here. - this.not_full.signal(); - } - - const result = this.buf[this.mask(this.read_index)]; - this.read_index = this.mask2(this.read_index + 1); - return result; - } - - /// Push an item into the queue. Blocks until an item has been - /// put in the queue. - pub fn push(this: *QueueType, item: T) void { - this.mutex.lock(); - defer this.mutex.unlock(); - while (this.isFullLH()) { - this.not_full.wait(&this.mutex); - } - if (this.isEmptyLH()) { - // If we were empty, wake up a pop if it was waiting. - this.not_empty.signal(); - } - assert(!this.isFullLH()); - - this.buf[this.mask(this.write_index)] = item; - this.write_index = this.mask2(this.write_index + 1); - } - - /// Push an item into the queue. Returns true when the item - /// was successfully placed in the queue, false if the queue - /// was full. - pub fn tryPush(this: *QueueType, item: T) bool { - this.mutex.lock(); - if (this.isFullLH()) { - this.mutex.unlock(); - return false; - } - this.mutex.unlock(); - this.push(item); - return true; - } - - /// Pop an item from the queue. Returns null when no item is - /// available. - pub fn tryPop(this: *QueueType) ?T { - this.mutex.lock(); - if (this.isEmptyLH()) { - this.mutex.unlock(); - return null; - } - this.mutex.unlock(); - return this.pop(); - } - - /// Poll the queue. This call blocks until events are in the queue - pub fn poll(this: *QueueType) void { - this.mutex.lock(); - defer this.mutex.unlock(); - while (this.isEmptyLH()) { - this.not_empty.wait(&this.mutex); - } - assert(!this.isEmptyLH()); - } - - fn isEmptyLH(this: QueueType) bool { - return this.write_index == this.read_index; - } - - fn isFullLH(this: QueueType) bool { - return this.mask2(this.write_index + this.buf.len) == - this.read_index; - } - - /// Returns `true` if the queue is empty and `false` otherwise. - pub fn isEmpty(this: *QueueType) bool { - this.mutex.lock(); - defer this.mutex.unlock(); - return this.isEmptyLH(); - } - - /// Returns `true` if the queue is full and `false` otherwise. - pub fn isFull(this: *QueueType) bool { - this.mutex.lock(); - defer this.mutex.unlock(); - return this.isFullLH(); - } - - /// Returns the length - fn len(this: QueueType) usize { - const wrap_offset = 2 * this.buf.len * - @intFromBool(this.write_index < this.read_index); - const adjusted_write_index = this.write_index + wrap_offset; - return adjusted_write_index - this.read_index; - } - - /// Returns `index` modulo the length of the backing slice. - fn mask(this: QueueType, index: usize) usize { - return index % this.buf.len; - } - - /// Returns `index` modulo twice the length of the backing slice. - fn mask2(this: QueueType, index: usize) usize { - return index % (2 * this.buf.len); - } - }; -} - -const testing = std.testing; -const cfg = Thread.SpawnConfig{ .allocator = testing.allocator }; -test "Queue: simple push / pop" { - var queue: Queue(u8, 16) = .{}; - queue.push(1); - queue.push(2); - const pop = queue.pop(); - try testing.expectEqual(1, pop); - try testing.expectEqual(2, queue.pop()); -} - -const Thread = std.Thread; -fn testPushPop(q: *Queue(u8, 2)) !void { - q.push(3); - try testing.expectEqual(2, q.pop()); -} - -test "Fill, wait to push, pop once in another thread" { - var queue: Queue(u8, 2) = .{}; - queue.push(1); - queue.push(2); - const t = try Thread.spawn(cfg, testPushPop, .{&queue}); - try testing.expectEqual(false, queue.tryPush(3)); - try testing.expectEqual(1, queue.pop()); - t.join(); - try testing.expectEqual(3, queue.pop()); - try testing.expectEqual(null, queue.tryPop()); -} - -fn testPush(q: *Queue(u8, 2)) void { - q.push(0); - q.push(1); - q.push(2); - q.push(3); - q.push(4); -} - -test "Try to pop, fill from another thread" { - var queue: Queue(u8, 2) = .{}; - const thread = try Thread.spawn(cfg, testPush, .{&queue}); - for (0..5) |idx| { - try testing.expectEqual(@as(u8, @intCast(idx)), queue.pop()); - } - thread.join(); -} - -fn sleepyPop(q: *Queue(u8, 2)) !void { - // First we wait for the queue to be full. - while (!q.isFull()) - try Thread.yield(); - - // Then we spuriously wake it up, because that's a thing that can - // happen. - q.not_full.signal(); - q.not_empty.signal(); - - // Then give the other thread a good chance of waking up. It's not - // clear that yield guarantees the other thread will be scheduled, - // so we'll throw a sleep in here just to be sure. The queue is - // still full and the push in the other thread is still blocked - // waiting for space. - try Thread.yield(); - std.time.sleep(std.time.ns_per_s); - // Finally, let that other thread go. - try std.testing.expectEqual(1, q.pop()); - - // This won't continue until the other thread has had a chance to - // put at least one item in the queue. - while (!q.isFull()) - try Thread.yield(); - // But we want to ensure that there's a second push waiting, so - // here's another sleep. - std.time.sleep(std.time.ns_per_s / 2); - - // Another spurious wake... - q.not_full.signal(); - q.not_empty.signal(); - // And another chance for the other thread to see that it's - // spurious and go back to sleep. - try Thread.yield(); - std.time.sleep(std.time.ns_per_s / 2); - - // Pop that thing and we're done. - try std.testing.expectEqual(2, q.pop()); -} - -test "Fill, block, fill, block" { - // Fill the queue, block while trying to write another item, have - // a background thread unblock us, then block while trying to - // write yet another thing. Have the background thread unblock - // that too (after some time) then drain the queue. This test - // fails if the while loop in `push` is turned into an `if`. - - var queue: Queue(u8, 2) = .{}; - const thread = try Thread.spawn(cfg, sleepyPop, .{&queue}); - queue.push(1); - queue.push(2); - const now = std.time.milliTimestamp(); - queue.push(3); // This one should block. - const then = std.time.milliTimestamp(); - - // Just to make sure the sleeps are yielding to this thread, make - // sure it took at least 900ms to do the push. - try std.testing.expect(then - now > 900); - - // This should block again, waiting for the other thread. - queue.push(4); - - // And once that push has gone through, the other thread's done. - thread.join(); - try std.testing.expectEqual(3, queue.pop()); - try std.testing.expectEqual(4, queue.pop()); -} - -fn sleepyPush(q: *Queue(u8, 1)) !void { - // Try to ensure the other thread has already started trying to pop. - try Thread.yield(); - std.time.sleep(std.time.ns_per_s / 2); - - // Spurious wake - q.not_full.signal(); - q.not_empty.signal(); - - try Thread.yield(); - std.time.sleep(std.time.ns_per_s / 2); - - // Stick something in the queue so it can be popped. - q.push(1); - // Ensure it's been popped. - while (!q.isEmpty()) - try Thread.yield(); - // Give the other thread time to block again. - try Thread.yield(); - std.time.sleep(std.time.ns_per_s / 2); - - // Spurious wake - q.not_full.signal(); - q.not_empty.signal(); - - q.push(2); -} - -test "Drain, block, drain, block" { - // This is like fill/block/fill/block, but on the pop end. This - // test should fail if the `while` loop in `pop` is turned into an - // `if`. - - var queue: Queue(u8, 1) = .{}; - const thread = try Thread.spawn(cfg, sleepyPush, .{&queue}); - try std.testing.expectEqual(1, queue.pop()); - try std.testing.expectEqual(2, queue.pop()); - thread.join(); -} - -fn readerThread(q: *Queue(u8, 1)) !void { - try testing.expectEqual(1, q.pop()); -} - -test "2 readers" { - // 2 threads read, one thread writes - var queue: Queue(u8, 1) = .{}; - const t1 = try Thread.spawn(cfg, readerThread, .{&queue}); - const t2 = try Thread.spawn(cfg, readerThread, .{&queue}); - try Thread.yield(); - std.time.sleep(std.time.ns_per_s / 2); - queue.push(1); - queue.push(1); - t1.join(); - t2.join(); -} - -fn writerThread(q: *Queue(u8, 1)) !void { - q.push(1); -} - -test "2 writers" { - var queue: Queue(u8, 1) = .{}; - const t1 = try Thread.spawn(cfg, writerThread, .{&queue}); - const t2 = try Thread.spawn(cfg, writerThread, .{&queue}); - - try testing.expectEqual(1, queue.pop()); - try testing.expectEqual(1, queue.pop()); - t1.join(); - t2.join(); -} diff --git a/src/render.zig b/src/render.zig deleted file mode 100644 index 58022ea..0000000 --- a/src/render.zig +++ /dev/null @@ -1,52 +0,0 @@ -//! Renderer which holds the screen to compare with the previous screen for efficient rendering. -const std = @import("std"); -const terminal = @import("terminal.zig"); - -pub fn BufferedRenderer(comptime fullscreen: bool) type { - return struct { - refresh: bool = false, - size: terminal.Size = undefined, - screen: std.ArrayList(u8) = undefined, - - pub fn init(allocator: std.mem.Allocator) @This() { - return .{ - .fullscreen = fullscreen, - .screen = std.ArrayList(u8).init(allocator), - }; - } - - pub fn deinit(this: *@This()) void { - this.screen.deinit(); - this.* = undefined; - } - - pub fn resize(this: *@This(), size: terminal.Size) void { - // TODO: are there size changes which impact the corresponding rendered content? - // -> can I even be sure nothing needs to be re-rendered? - this.size = size; - this.refresh = true; - } - - pub fn render(this: *@This(), content: *std.ArrayList(u8)) !void { - // TODO: put the corresponding screen to the terminal - // -> determine diff between screen and new content and only update the corresponding characters of the terminal - _ = this; - _ = content; - @panic("Not yet implemented."); - } - }; -} - -pub fn PlainRenderer(comptime fullscreen: bool) type { - return struct { - pub fn render(this: *@This(), content: *std.ArrayList(u8)) !void { - _ = this; - if (fullscreen) { - try terminal.clearScreen(); - try terminal.setCursorPositionHome(); - } - // TODO: how would I clear the screen in case of a non fullscreen application (i.e. to clear to the start of the command) - _ = try terminal.write(content.items); - } - }; -} diff --git a/src/style.zig b/src/style.zig deleted file mode 100644 index fb46c9d..0000000 --- a/src/style.zig +++ /dev/null @@ -1,285 +0,0 @@ -//! Helper function collection to provide ascii encodings for styling outputs. -//! Stylings are implemented such that they can be nested in anyway to support -//! multiple styles (i.e. bold and italic). -//! -//! Stylings however also include highlighting for specific terminal capabilities. -//! For example url highlighting. - -// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License) -// with slight modifications -const std = @import("std"); -const ctlseqs = @import("ctlseqs.zig"); - -pub const Underline = enum { - off, - single, - double, - curly, - dotted, - dashed, -}; - -pub const Color = union(enum) { - default, - index: u8, - rgb: [3]u8, - - pub fn eql(a: @This(), b: @This()) bool { - switch (a) { - .default => return b == .default, - .index => |a_idx| { - switch (b) { - .index => |b_idx| return a_idx == b_idx, - else => return false, - } - }, - .rgb => |a_rgb| { - switch (b) { - .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and - a_rgb[1] == b_rgb[1] and - a_rgb[2] == b_rgb[2], - else => return false, - } - }, - } - } - - pub fn rgbFromUint(val: u24) Color { - const r_bits = val & 0b11111111_00000000_00000000; - const g_bits = val & 0b00000000_11111111_00000000; - const b_bits = val & 0b00000000_00000000_11111111; - const rgb = [_]u8{ - @truncate(r_bits >> 16), - @truncate(g_bits >> 8), - @truncate(b_bits), - }; - return .{ .rgb = rgb }; - } - - /// parse an XParseColor-style rgb specification into an rgb Color. The spec - /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always - /// be the same as the low two bits. - pub fn rgbFromSpec(spec: []const u8) !Color { - var iter = std.mem.splitScalar(u8, spec, ':'); - const prefix = iter.next() orelse return error.InvalidColorSpec; - if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; - - const spec_str = iter.next() orelse return error.InvalidColorSpec; - - var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); - - const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (r_raw.len != 4) return error.InvalidColorSpec; - - const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (g_raw.len != 4) return error.InvalidColorSpec; - - const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (b_raw.len != 4) return error.InvalidColorSpec; - - const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); - const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); - const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); - - return .{ - .rgb = [_]u8{ r, g, b }, - }; - } - - test "rgbFromSpec" { - const spec = "rgb:aaaa/bbbb/cccc"; - const actual = try rgbFromSpec(spec); - switch (actual) { - .rgb => |rgb| { - try std.testing.expectEqual(0xAA, rgb[0]); - try std.testing.expectEqual(0xBB, rgb[1]); - try std.testing.expectEqual(0xCC, rgb[2]); - }, - else => try std.testing.expect(false), - } - } -}; - -fg: Color = .default, -bg: Color = .default, -ul: Color = .default, -ul_style: Underline = .off, - -bold: bool = false, -dim: bool = false, -italic: bool = false, -blink: bool = false, -reverse: bool = false, -invisible: bool = false, -strikethrough: bool = false, - -fn start(this: @This(), writer: anytype) !void { - // foreground - switch (this.fg) { - .default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), - .index => |idx| { - switch (idx) { - 0...7 => { - try std.fmt.format(writer, ctlseqs.fg_base, .{idx}); - }, - 8...15 => { - try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}); - }, - else => { - try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}); - }, - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - // background - switch (this.bg) { - .default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), - .index => |idx| { - switch (idx) { - 0...7 => { - try std.fmt.format(writer, ctlseqs.bg_base, .{idx}); - }, - 8...15 => { - try std.fmt.format(writer, ctlseqs.bg_bright, .{idx}); - }, - else => { - try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}); - }, - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - // underline color - switch (this.ul) { - .default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), - .index => |idx| { - try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - // underline style - switch (this.ul_style) { - .off => try std.fmt.format(writer, ctlseqs.ul_off, .{}), - .single => try std.fmt.format(writer, ctlseqs.ul_single, .{}), - .double => try std.fmt.format(writer, ctlseqs.ul_double, .{}), - .curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}), - .dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}), - .dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}), - } - // bold - switch (this.bold) { - true => try std.fmt.format(writer, ctlseqs.bold_set, .{}), - false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - } - // dim - switch (this.dim) { - true => try std.fmt.format(writer, ctlseqs.dim_set, .{}), - false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - } - // italic - switch (this.italic) { - true => try std.fmt.format(writer, ctlseqs.italic_set, .{}), - false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), - } - // blink - switch (this.blink) { - true => try std.fmt.format(writer, ctlseqs.blink_set, .{}), - false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), - } - // reverse - switch (this.reverse) { - true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}), - false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), - } - // invisible - switch (this.invisible) { - true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}), - false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), - } - // strikethrough - switch (this.strikethrough) { - true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}), - false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), - } -} - -fn end(this: @This(), writer: anytype) !void { - // foreground - switch (this.fg) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), - } - // background - switch (this.bg) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), - } - // underline color - switch (this.ul) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), - } - // underline style - switch (this.ul_style) { - .off => {}, - else => try std.fmt.format(writer, ctlseqs.ul_off, .{}), - } - // bold - switch (this.bold) { - true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - false => {}, - } - // dim - switch (this.dim) { - true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - false => {}, - } - // italic - switch (this.italic) { - true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), - false => {}, - } - // blink - switch (this.blink) { - true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), - false => {}, - } - // reverse - switch (this.reverse) { - true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), - false => {}, - } - // invisible - switch (this.invisible) { - true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), - false => {}, - } - // strikethrough - switch (this.strikethrough) { - true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), - false => {}, - } -} - -pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void { - try this.start(writer); - try std.fmt.format(writer, content, args); - try this.end(writer); -} - -pub fn value(this: @This(), writer: anytype, content: []const u8) !void { - try this.start(writer); - _ = try writer.write(content); - try this.end(writer); -} - -// TODO: implement helper functions for terminal capabilities: -// - links / url display (osc 8) -// - show / hide cursor? diff --git a/src/terminal.zig b/src/terminal.zig deleted file mode 100644 index 5ac995e..0000000 --- a/src/terminal.zig +++ /dev/null @@ -1,207 +0,0 @@ -const std = @import("std"); -const Key = @import("key.zig"); -pub const code_point = @import("code_point"); - -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: std.posix.winsize = undefined; - _ = std.posix.system.ioctl(std.posix.STDIN_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.STDIN_FILENO, "\x1b[?47h"); -} - -pub fn restoreScreen() !void { - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?47l"); -} - -pub fn enterAltScreen() !void { - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049h"); -} - -pub fn existAltScreen() !void { - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049l"); -} - -pub fn clearScreen() !void { - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[2J"); -} - -pub fn setCursorPositionHome() !void { - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[H"); -} - -pub fn read(buf: []u8) !usize { - return try std.posix.read(std.posix.STDIN_FILENO, buf); -} - -pub fn write(buf: []const u8) !usize { - return try std.posix.write(std.posix.STDIN_FILENO, buf); -} - -pub fn getCursorPosition() !Position { - // Needs Raw mode (no wait for \n) to work properly cause - // control sequence will not be written without it. - _ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n"); - - var buf: [64]u8 = undefined; - - // format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R" - const len = try std.posix.read(std.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 std.fmt.parseInt(u16, row[0..ridx], 10), - .col = try std.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: *std.posix.termios) !void { - var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO); - bak.* = termios; - - // termios flags used by termios(3) - termios.iflag.IGNBRK = false; - termios.iflag.BRKINT = false; - termios.iflag.PARMRK = false; - termios.iflag.ISTRIP = false; - termios.iflag.INLCR = false; - termios.iflag.IGNCR = false; - termios.iflag.ICRNL = false; - termios.iflag.IXON = false; - - // messes with output -> not used - // termios.oflag.OPOST = false; - - termios.lflag.ECHO = false; - termios.lflag.ECHONL = false; - termios.lflag.ICANON = false; - termios.lflag.ISIG = false; - termios.lflag.IEXTEN = false; - - termios.cflag.CSIZE = .CS8; - termios.cflag.PARENB = false; - - termios.cc[@intFromEnum(std.posix.V.MIN)] = 1; - termios.cc[@intFromEnum(std.posix.V.TIME)] = 0; - - try std.posix.tcsetattr( - std.posix.STDIN_FILENO, - .FLUSH, - termios, - ); -} - -/// Reverts `enableRawMode` to restore initial functionality. -pub fn disableRawMode(bak: *std.posix.termios) !void { - try std.posix.tcsetattr( - std.posix.STDIN_FILENO, - .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 std.posix.write(std.posix.STDIN_FILENO, "\x1b[?2026$p"); - - var buf: [64]u8 = undefined; - - // format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y" - 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; - } - - // 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 deleted file mode 100644 index d99a1f3..0000000 --- a/src/widget.zig +++ /dev/null @@ -1,78 +0,0 @@ -//! Dynamic dispatch for widget implementations. -//! Each widget should at last implement these functions: -//! - handle(this: *@This(), event: Event) ?Event {} -//! - content(this: *@This()) ![]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 isTaggedUnion = @import("event.zig").isTaggedUnion; - -pub fn Widget(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - return struct { - const WidgetType = @This(); - const Ptr = usize; - - const VTable = struct { - handle: *const fn (this: *WidgetType, event: Event) ?Event, - content: *const fn (this: *WidgetType) anyerror![]u8, - deinit: *const fn (this: *WidgetType) void, - }; - - 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); - } - - // Return the entire content of this `Widget`. - pub fn content(this: *WidgetType) ![]u8 { - return try this.vtable.content(this); - } - - pub fn deinit(this: *WidgetType) void { - this.vtable.deinit(this); - this.* = undefined; - } - - 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) ![]u8 { - const widget: @TypeOf(object) = @ptrFromInt(this.object); - return try widget.content(); - } - }.content, - .deinit = struct { - fn deinit(this: *WidgetType) void { - const widget: @TypeOf(object) = @ptrFromInt(this.object); - widget.deinit(); - } - }.deinit, - }, - }; - } - - // TODO: import and export of `Widget` implementations (with corresponding intialization using `Event`) - pub const RawText = @import("widget/RawText.zig").Widget(Event); - }; -} diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig deleted file mode 100644 index ff2144e..0000000 --- a/src/widget/RawText.zig +++ /dev/null @@ -1,95 +0,0 @@ -const std = @import("std"); -const terminal = @import("../terminal.zig"); -const Style = @import("../style.zig"); - -const isTaggedUnion = @import("../event.zig").isTaggedUnion; -const Error = @import("../event.zig").Error; -const Key = @import("../key.zig"); - -const log = std.log.scoped(.widget_rawtext); - -pub fn Widget(comptime Event: type) type { - if (!isTaggedUnion(Event)) { - @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); - } - const Contents = std.ArrayList(u8); - return struct { - contents: Contents = undefined, - line_index: std.ArrayList(usize) = undefined, - line: usize = 0, - size: terminal.Size = undefined, - - pub fn init(allocator: std.mem.Allocator, file: std.fs.File) @This() { - var contents = Contents.init(allocator); - var line_index = std.ArrayList(usize).init(allocator); - file.reader().readAllArrayList(&contents, std.math.maxInt(usize)) catch {}; - line_index.append(0) catch {}; - for (contents.items, 0..) |item, i| { - if (item == '\n') { - line_index.append(i + 1) catch {}; - } - } - return .{ - .contents = contents, - .line_index = line_index, - }; - } - - pub fn deinit(this: *@This()) void { - this.contents.deinit(); - this.line_index.deinit(); - this.* = undefined; - } - - pub fn handle(this: *@This(), event: Event) ?Event { - switch (event) { - // store the received size - .resize => |size| { - this.size = size; - log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows }); - if (this.line > this.line_index.items.len -| 1 -| size.rows) { - this.line = this.line_index.items.len -| 1 -| size.rows; - } - }, - .key => |key| { - if (key.matches(.{ .cp = 'g' })) { - // top - this.line = 0; - } - if (key.matches(.{ .cp = 'G' })) { - // bottom - this.line = this.line_index.items.len -| 1 -| this.size.rows; - } - if (key.matches(.{ .cp = 'j' })) { - // down - if (this.line < this.line_index.items.len -| 1 -| this.size.rows) { - this.line +|= 1; - } - } - if (key.matches(.{ .cp = 'k' })) { - // up - this.line -|= 1; - } - }, - else => {}, - } - return null; - } - - pub fn content(this: *@This()) ![]u8 { - if (this.size.rows >= this.line_index.items.len) { - return this.contents.items; - } else { - // more rows than we can display - const i = this.line_index.items[this.line]; - log.debug("i := {d} this.line := {d}", .{ i, this.line }); - const e = this.size.rows + this.line; - if (e >= this.line_index.items.len) { - return this.contents.items[i..]; - } - const x = this.line_index.items[e] - 1; - return this.contents.items[i..x]; - } - } - }; -} diff --git a/src/widget/node2buffer.zig b/src/widget/node2buffer.zig deleted file mode 100644 index dfa225c..0000000 --- a/src/widget/node2buffer.zig +++ /dev/null @@ -1,314 +0,0 @@ -///! Transform a given file type into a buffer which can be used by any `vaxis.widgets.View` -///! -///! Use the `toBuffer` method of any struct to convert the file type described by the -///! struct to convert the file contents accordingly. -const std = @import("std"); -const vaxis = @import("vaxis"); - -/// Markdown tronsformation to convert a markdown file as a `std.ArrayList(vaxis.cell)` -pub const Markdown = struct { - const zmd = @import("zmd"); - - const digits = "0123456789"; - - pub fn toBuffer( - input: []const u8, - allocator: std.mem.Allocator, - array: *std.ArrayList(vaxis.Cell), - ) void { - var z = zmd.Zmd.init(allocator); - defer z.deinit(); - - z.parse(input) catch @panic("failed to parse markdown contents"); - convert( - z.nodes.items[0], - allocator, - input, - array, - .{}, - null, - ) catch @panic("failed to transform parsed markdown to cell array"); - } - - fn convert( - node: *zmd.Node, - allocator: std.mem.Allocator, - input: []const u8, - array: *std.ArrayList(vaxis.Cell), - sty: vaxis.Cell.Style, - start: ?usize, - ) !void { - var next_start: ?usize = start; - var style = sty; - - // determine general styling changes - switch (node.token.element.type) { - .bold => { - style.bold = true; - next_start = node.token.start; - style.fg = .{ .index = 5 }; - }, - .italic => { - style.italic = true; - next_start = node.token.start; - style.fg = .{ .index = 2 }; - }, - .block => { - style.fg = .{ .index = 252 }; - style.dim = true; - next_start = node.token.start; - }, - .code => { - next_start = node.token.start; - style.fg = .{ .index = 6 }; - }, - .href => { - next_start = node.token.start; - style.fg = .{ .index = 8 }; - }, - .link => { - style.fg = .{ .index = 3 }; - style.ul_style = .single; - }, - .list_item => { - style.fg = .{ .index = 1 }; - }, - else => {}, - } - - // determine content that needs to be displayed - const content = value: { - switch (node.token.element.type) { - // NOTE: do not support ordered lists? (as it only accepts `1.` and not `2.`, etc.) - .text, .list_item => break :value input[node.token.start..node.token.end], - .link => break :value input[node.token.start + 1 .. node.token.start + 1 + node.title.?.len], - .code_close => { - if (next_start) |s| { - next_start = null; - break :value input[s + 1 .. node.token.end - 1]; - } - break :value ""; - }, - .bold_close, .italic_close, .block_close, .title_close, .href_close => { - if (next_start) |s| { - next_start = null; - break :value input[s..node.token.end]; - } - break :value ""; - }, - else => { - break :value ""; - }, - } - }; - - // display content - switch (node.token.element.type) { - .linebreak, .paragraph => { - try array.append(.{ - .char = .{ .grapheme = "\n" }, - .style = style, - }); - style.ul_style = .off; - }, - .h1 => { - style.ul_style = .single; - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - }, - .h2 => { - style.ul_style = .single; - for (0..2) |_| { - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - } - }, - .h3 => { - style.ul_style = .single; - for (0..3) |_| { - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - } - }, - .h4 => { - style.ul_style = .single; - for (0..4) |_| { - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - } - }, - .h5 => { - style.ul_style = .single; - for (0..5) |_| { - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - } - }, - .h6 => { - style.ul_style = .single; - for (0..6) |_| { - try array.append(.{ - .char = .{ .grapheme = "#" }, - .style = style, - }); - } - }, - .link => { - const uri = input[node.token.start + 1 + node.title.?.len + 2 .. node.token.start + 1 + node.title.?.len + 2 + node.href.?.len]; - for (content, 0..) |_, i| { - try array.append(.{ - .link = .{ .uri = uri }, - .char = .{ .grapheme = content[i .. i + 1] }, - .style = style, - }); - } - }, - .block_close => { - // generate a `block` i.e. - // 01 | ... - // 02 | ... - // 03 | ... - // ... - // 10 | ... - try array.append(.{ - .char = .{ .grapheme = "\n" }, - }); - var rows: usize = 0; - var c: usize = 0; - // TODO: would be cool to not have to re-iterate over the contents - for (content, 0..) |char, i| { - if (char == '\n') { - // NOTE: start after the ``` - if (c == 0) { - c = i + 1; - } - rows += 1; - } - } - rows = rows -| 1; - const pad = vaxis.widgets.LineNumbers.numDigits(rows); - for (1..rows + 1) |r| { - try array.append(.{ - .char = .{ .grapheme = " " }, - .style = style, - }); - try array.append(.{ - .char = .{ .grapheme = " " }, - .style = style, - }); - for (1..pad + 1) |i| { - const digit = vaxis.widgets.LineNumbers.extractDigit(r, pad - i); - try array.append(.{ - .char = .{ .grapheme = digits[digit .. digit + 1] }, - .style = style, - }); - } - try array.append(.{ - .char = .{ .grapheme = " " }, - .style = style, - }); - try array.append(.{ - .char = .{ .grapheme = "│" }, - .style = style, - }); - try array.append(.{ - .char = .{ .grapheme = " " }, - .style = style, - }); - for (c..content.len) |c_i| { - if (r == rows and content[c_i] == '\n') { - break; - } - try array.append(.{ - .char = .{ .grapheme = content[c_i .. c_i + 1] }, - .style = style, - }); - if (content[c_i] == '\n') { - c = c_i + 1; - break; - } - } - } - }, - .list_item => { - try array.append(.{ - .char = .{ .grapheme = "\n" }, - }); - for (content, 0..) |_, i| { - try array.append(.{ - .char = .{ .grapheme = content[i .. i + 1] }, - .style = style, - }); - } - }, - else => { - for (content, 0..) |_, i| { - try array.append(.{ - .char = .{ .grapheme = content[i .. i + 1] }, - .style = style, - }); - } - }, - } - - // close styling after creating the corresponding cells - switch (node.token.element.type) { - .bold_close => { - style.bold = false; - style.fg = .default; - }, - .italic_close => { - style.italic = false; - style.fg = .default; - }, - .block_close => { - style.fg = .default; - }, - .code_close => { - style.fg = .default; - }, - .href_close => { - style.fg = .default; - }, - .list_item => { - style.fg = .default; - }, - else => {}, - } - - // run conversion for all childrens - for (node.children.items) |child_node| { - try convert(child_node, allocator, input, array, style, next_start); - } - } -}; - -pub const Typst = struct { - pub fn toBuffer( - input: []const u8, - allocator: std.mem.Allocator, - array: *std.ArrayList(vaxis.Cell), - ) void { - // TODO: leverage the compiler to create corresponding file? - // -> would enable functions to be executed, however this is not possible currently - // -> this would only work if I serve the corresponding pdf's (maybe I can have a download server for these?) - // NOTE: currently the typst compiler does not allow such a usage from the outside - // -> I would need to wait for html export I guess - - // TODO: use pret parsing to parse typst file contents and transform them into `vaxis.Cell`s accordingly - _ = input; - _ = allocator; - _ = array; - @panic("Typst parsing not yet implemented"); - } -};