From 9515def4fbd8b62af2a35e26596542ca1ec0e393 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Wed, 13 Nov 2024 19:52:55 +0100 Subject: [PATCH] Replace vaxis with zterm (#1) Vaxis sadly cannot be used for applications which want to serve their contents via ssh to the (remote) user. Instead I wrote my own tui library [zterm](https://gitea.yves-biener.de/yves-biener/zterm), which this tui application now uses. This will serve two purposes: 1. implement the tui application and be servable through ssh (see [wish-serve](https://gitea.yves-biener.de/yves-biener/wish-serve)) 2. serve as documentation and showcase of an example application for **zterm** Reviewed-on: https://gitea.yves-biener.de/yves-biener/tui-website/pulls/1 --- README.md | 10 +- build.zig | 13 +- build.zig.zon | 12 +- src/main.zig | 186 +++++++++------------- src/widget.zig | 63 -------- src/widget/Header.zig | 127 --------------- src/widget/PopupMenu.zig | 80 ---------- src/widget/ViewPort.zig | 103 ------------ src/widget/node2buffer.zig | 314 ------------------------------------- 9 files changed, 91 insertions(+), 817 deletions(-) delete mode 100644 src/widget.zig delete mode 100644 src/widget/Header.zig delete mode 100644 src/widget/PopupMenu.zig delete mode 100644 src/widget/ViewPort.zig delete mode 100644 src/widget/node2buffer.zig diff --git a/README.md b/README.md index 859e9a7..e801e41 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,14 @@ This is my terminal based website. It is served as a tui application via ssh and It contains information about me and my projects as well as blog entries about something I feel like writing something about. +## zterm + +This tui application also serves as an example implementation of the [zterm](https://gitea.yves-biener.de/yves-biener/zterm) library. + +--- + ## Open tasks -- [ ] BUG: when served via `wish-serve` the corresponding outputs are not pushed through the ssh connection - - 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`) - [ ] Improve navigation - [ ] Have clickable/navigatable links inside of the tui application - [ ] Launch simple http server alongside tui application diff --git a/build.zig b/build.zig index 2534bdd..8f612a4 100644 --- a/build.zig +++ b/build.zig @@ -16,16 +16,12 @@ pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{}); // 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, }); @@ -36,9 +32,8 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); - 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("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 192c11c..0a8ab4f 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -24,8 +24,8 @@ // internet connectivity. .dependencies = .{ .zlog = .{ - .url = "git+https://gitea.yves-biener.de/yves-biener/zlog#73991389f6f4156e5ccbc6afd611bc34fe738a72", - .hash = "1220d86aec0bf263dd4df028e017f094f4c530cc2367a3cd031989bbe94f466da1e0", + .url = "git+https://gitea.yves-biener.de/yves-biener/zlog#87cd904c708bf47c0ecde51631328e7411563dcf", + .hash = "122055b153ca7493f2c5b34bcaa9dc40f7bfc2ead3d4880be023fbb99e3ec13d2827", }, .vaxis = .{ .url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa", @@ -35,6 +35,14 @@ .url = "git+https://github.com/jetzig-framework/zmd#90e52429cacd4fdc90fd615596fe584ae40ec8e9", .hash = "12202a4edefedd52478223a44cdc9a3b41d4bc5cf3670497a48377f45ff16a5e3363", }, + .zg = .{ + .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/main.zig b/src/main.zig index 0fdd2ce..57ee98d 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,142 +1,98 @@ const std = @import("std"); - -const vaxis = @import("vaxis"); const zlog = @import("zlog"); +const zterm = @import("zterm"); -const widget = @import("widget.zig"); - -const TextInput = vaxis.widgets.TextInput; -const Event = widget.Event; +const App = zterm.App( + union(enum) {}, + zterm.Renderer.Direct, + true, +); +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); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + errdefer |err| log.err("Application Error: {any}", .{err}); + + 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", .{}); + log.err("memory leak", .{}); } } - const alloc = gpa.allocator(); + const allocator = gpa.allocator(); - // Initialize a tty - var tty = try vaxis.Tty.init(); - defer tty.deinit(); + var app: App = .{}; + var renderer: App.Renderer = .{}; - // 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()); + 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; + }); + 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(); - - // 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" }); + try app.start(); + defer app.stop() catch unreachable; + // App.Event loop 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; - } - } + const event = app.nextEvent(); 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(); + .quit => break, + .key => |key| { + // ctrl+c to quit + if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) { + app.quit(); + break; // no need to render this frame anyway } }, - .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), + .err => |err| { + log.err("Received {any} with message: {s}", .{ err.err, err.msg }); + }, 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; + const events = try layout.handle(event); + for (events.items) |e| { + app.postEvent(e); } - 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()); + try layout.render(&renderer); } } diff --git a/src/widget.zig b/src/widget.zig deleted file mode 100644 index 5c8b8a7..0000000 --- a/src/widget.zig +++ /dev/null @@ -1,63 +0,0 @@ -//! 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 {} -//! -//! 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. - -const vaxis = @import("vaxis"); - -pub const Event = union(enum) { - key_press: vaxis.Key, - winsize: vaxis.Winsize, - path: []const u8, -}; - -const Ptr = usize; - -object: Ptr = undefined, -vtable: *const VTable = undefined, - -const VTable = struct { - update: *const fn (this: *@This(), event: Event) void, - draw: *const fn (this: *@This(), win: vaxis.Window) void, -}; - -/// 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); -} - -/// 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) @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, - }, - }; -} - -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/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; - } - } -} 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"); - } -};