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.
|
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
|
||||||
|
|||||||
13
build.zig
13
build.zig
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
186
src/main.zig
186
src/main.zig
@@ -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());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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