Replace vaxis with zterm (#1)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 32s

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: #1
This commit was merged in pull request #1.
This commit is contained in:
2024-11-13 19:52:55 +01:00
parent c62fc6fb43
commit 9515def4fb
9 changed files with 91 additions and 817 deletions

View File

@@ -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. 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 ## 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 - [ ] Improve navigation
- [ ] Have clickable/navigatable links inside of the tui application - [ ] Have clickable/navigatable links inside of the tui application
- [ ] Launch simple http server alongside tui application - [ ] Launch simple http server alongside tui application

View File

@@ -16,16 +16,12 @@ pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{}); const optimize = b.standardOptimizeOption(.{});
// Dependencies // Dependencies
const vaxis_dep = b.dependency("vaxis", .{ const zlog = b.dependency("zlog", .{
.optimize = optimize,
.target = target,
});
const zlog_dep = b.dependency("zlog", .{
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
.timestamp = true, .timestamp = true,
}); });
const zmd_dep = b.dependency("zmd", .{ const zterm = b.dependency("zterm", .{
.optimize = optimize, .optimize = optimize,
.target = target, .target = target,
}); });
@@ -36,9 +32,8 @@ pub fn build(b: *std.Build) void {
.target = target, .target = target,
.optimize = optimize, .optimize = optimize,
}); });
exe.root_module.addImport("vaxis", vaxis_dep.module("vaxis")); exe.root_module.addImport("zlog", zlog.module("zlog"));
exe.root_module.addImport("zlog", zlog_dep.module("zlog")); exe.root_module.addImport("zterm", zterm.module("zterm"));
exe.root_module.addImport("zmd", zmd_dep.module("zmd"));
// This declares intent for the executable to be installed into the // This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default // standard location when the user invokes the "install" step (the default

View File

@@ -24,8 +24,8 @@
// internet connectivity. // internet connectivity.
.dependencies = .{ .dependencies = .{
.zlog = .{ .zlog = .{
.url = "git+https://gitea.yves-biener.de/yves-biener/zlog#73991389f6f4156e5ccbc6afd611bc34fe738a72", .url = "git+https://gitea.yves-biener.de/yves-biener/zlog#87cd904c708bf47c0ecde51631328e7411563dcf",
.hash = "1220d86aec0bf263dd4df028e017f094f4c530cc2367a3cd031989bbe94f466da1e0", .hash = "122055b153ca7493f2c5b34bcaa9dc40f7bfc2ead3d4880be023fbb99e3ec13d2827",
}, },
.vaxis = .{ .vaxis = .{
.url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa", .url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa",
@@ -35,6 +35,14 @@
.url = "git+https://github.com/jetzig-framework/zmd#90e52429cacd4fdc90fd615596fe584ae40ec8e9", .url = "git+https://github.com/jetzig-framework/zmd#90e52429cacd4fdc90fd615596fe584ae40ec8e9",
.hash = "12202a4edefedd52478223a44cdc9a3b41d4bc5cf3670497a48377f45ff16a5e3363", .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 = .{ .paths = .{
"build.zig", "build.zig",

View File

@@ -1,142 +1,98 @@
const std = @import("std"); const std = @import("std");
const vaxis = @import("vaxis");
const zlog = @import("zlog"); const zlog = @import("zlog");
const zterm = @import("zterm");
const widget = @import("widget.zig"); const App = zterm.App(
union(enum) {},
const TextInput = vaxis.widgets.TextInput; zterm.Renderer.Direct,
const Event = widget.Event; true,
);
const Key = zterm.Key;
const Layout = App.Layout;
const Widget = App.Widget;
pub const std_options = zlog.std_options; pub const std_options = zlog.std_options;
const log = std.log.scoped(.default);
pub fn main() !void { pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){}; errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer { defer {
const deinit_status = gpa.deinit(); 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) { 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 app: App = .{};
var tty = try vaxis.Tty.init(); var renderer: App.Renderer = .{};
defer tty.deinit();
// Initialize Vaxis var layout = Layout.createFrom(vstack: {
var vx = try vaxis.init(alloc, .{}); var vstack = Layout.VStack.init(allocator, .{
// deinit takes an optional allocator. If your program is exiting, you can Layout.createFrom(framing: {
// choose to pass a null allocator to save some exit time. var framing = Layout.Framing.init(allocator, .{
defer vx.deinit(alloc, tty.anyWriter()); .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 try app.start();
// stable pointers to Vaxis and our TTY, then init the instance. Doing so defer app.stop() catch unreachable;
// 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" });
// App.Event loop
while (true) { while (true) {
const event = loop.nextEvent(); const event = app.nextEvent();
// update widgets
header.update(event);
view_port.update(event);
if (active_menu) {
if (menu.update(event)) |e| {
_ = loop.tryPostEvent(e);
active_menu = false;
}
}
switch (event) { switch (event) {
.key_press => |key| { .quit => break,
if (active_menu) { .key => |key| {
if (key.matches(vaxis.Key.escape, .{})) { // ctrl+c to quit
active_menu = false; if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
} app.quit();
} break; // no need to render this frame anyway
if (key.matches('c', .{ .ctrl = true })) {
break;
} else if (key.matches(vaxis.Key.space, .{})) {
active_menu = true;
} else if (key.matches('l', .{ .ctrl = true })) {
vx.queueRefresh();
} }
}, },
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), .err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
else => {}, else => {},
} }
var root_window = vx.window(); const events = try layout.handle(event);
root_window.clear(); for (events.items) |e| {
// FIXME: this should not be necessary to clear the contents app.postEvent(e);
try vx.render(tty.anyWriter()); // re-draw after clear!
header.draw(root_window.child(.{
.x_off = 0,
.y_off = 0,
.height = .{ .limit = 3 },
.border = .{ .where = .all },
}));
// should be 120 characters wide and centered horizontally
var view_port_x_off: usize = undefined;
var limit: usize = 120;
if (root_window.width / 2 -| 60 > 0) {
view_port_x_off = root_window.width / 2 -| 60;
} else {
view_port_x_off = 1;
limit = root_window.width - 1;
} }
view_port.draw(root_window.child(.{ try layout.render(&renderer);
.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());
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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