Replace vaxis with zterm (#1)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 32s
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:
10
README.md
10
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
|
||||
|
||||
13
build.zig
13
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
|
||||
|
||||
@@ -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",
|
||||
|
||||
186
src/main.zig
186
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
@@ -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, .{});
|
||||
}
|
||||
}
|
||||
@@ -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, .{});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user