feat(content): routing to provided .blog paths
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m7s

This is a starting point to dynamically loading `markdown` files as blog
entries. With this change the *doc/about.md* and *doc/blog.md* files
are no longer builded into the executable and instead read from the
filesystem during runtime (along with the new test file *doc/test.md*).
This commit is contained in:
2025-11-01 22:36:48 +01:00
parent e4c3f69821
commit a877ac3e7d
7 changed files with 138 additions and 59 deletions

View File

@@ -26,18 +26,6 @@ pub fn build(b: *std.Build) void {
.imports = &.{
.{ .name = "zterm", .module = zterm.module("zterm") },
.{ .name = "zlog", .module = zlog.module("zlog") },
.{
.name = "about",
.module = b.createModule(.{
.root_source_file = b.path("doc/about.md"),
}),
},
.{
.name = "blog",
.module = b.createModule(.{
.root_source_file = b.path("doc/blog.md"),
}),
},
},
}),
});

View File

@@ -6,8 +6,8 @@
.minimum_zig_version = "0.16.0-dev.463+f624191f9",
.dependencies = .{
.zterm = .{
.url = "git+https://gitea.yves-biener.de/yves-biener/zterm#7cd1fb139fbf45a3d99f8359dda0d198a283249a",
.hash = "zterm-0.3.0-1xmmEPf4GwA0W_Aj0yFGfg4efODoLB3y72LAprcAJwwe",
.url = "git+https://gitea.yves-biener.de/yves-biener/zterm#b3dc8096d76d6e246d48f9034a65997c0047c3c6",
.hash = "zterm-0.3.0-1xmmENP4GwBw65udohsoaxc9Kp4Yo7kORt4okY5pLslr",
},
.zlog = .{
.url = "git+https://gitea.yves-biener.de/yves-biener/zlog#411a6dc358a3ef463ab704e2f6b887a019a5decf",

51
doc/test.md Normal file
View File

@@ -0,0 +1,51 @@
---
title: Capabilities of tui-website
---
Not everything that can be done with markdown can be representented by this tui-application. It is simple enough to
support basic markdown files. Currently the text has to be manually reflowed to 120 characters, such that there is no
issue when rendering (i.e. correct line breaks, etc.)
The following list contains all the available features that are supported (with some examples below):
- unordered lists (using `-`, `+` and `*`, where the corresponding symbol is used when rendering)
* links
+ blocks
- `inline blocks` (written as `inline blocks`)
* _italic_ (written as `_italic_`)
+ **bold** (written as `**bold**`)
- headlines (corresponding sub heading six levels deep)
Headings currently require a second newline before the list items to render correctly with a empty line in between the
following contents and the last list item.
Another paragraph.
# Examples
> quotes are not supported (yet)
The following is a block. Which renders as follows (containing line numbers and **no code highlighting**):
---
```zig
fn myFunction() void {
if (hasFeature()) {
// Feature-specific code
} else {
// Default code
}
}
inline fn hasFeature() bool {
return (comptime comptimeCheck()) and runtimeCheck();
}
```
## Combinations are possible
1. see this excellent blog post: [Conditionally Disabling Code with comptime in Zig - Mitchell Hashimoto](https://mitchellh.com/writing/zig-comptime-conditional-disable)
### See deeper levels are possible and will be rendered correctly
This is just some placeholder text.

View File

@@ -1,11 +1,12 @@
pub fn Content(App: type) type {
return struct {
page: App.Model.Pages,
allocator: Allocator,
document: *const App.Model.Document,
pub fn init(allocator: Allocator) @This() {
_ = allocator;
return .{
.page = .blog,
.allocator = allocator,
.document = undefined,
};
}
@@ -22,10 +23,7 @@ pub fn Content(App: type) type {
fn minSize(ctx: *anyopaque, size: zterm.Point) zterm.Point {
const this: *const @This() = @ptrCast(@alignCast(ctx));
const text = switch (this.page) {
.about => @embedFile("about"),
.blog => @embedFile("blog"),
};
const text = this.document.content;
var index: usize = 0;
var new_size: zterm.Point = .{ .x = size.x };
@@ -48,17 +46,25 @@ pub fn Content(App: type) type {
fn handle(ctx: *anyopaque, model: *App.Model, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.init => this.document = &model.document,
.about => {
model.page.deinit(this.allocator);
model.page = .about;
model.document = .init(@embedFile("about"));
model.document.deinit(this.allocator);
model.document = .init(try std.fs.cwd().readFileAlloc("./doc/about.md", this.allocator, .unlimited));
this.document = &model.document;
},
.blog => {
model.page = .blog;
model.document = .init(@embedFile("blog"));
.blog => |path| {
model.page.deinit(this.allocator);
model.page = .{ .blog = path };
model.document.deinit(this.allocator);
model.document = .init(try std.fs.cwd().readFileAlloc(if (path) |p| p else "./doc/blog.md", this.allocator, .unlimited));
this.document = &model.document;
},
else => {},
}
this.page = model.page;
}
fn content(ctx: *anyopaque, model: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
@@ -88,10 +94,6 @@ pub fn Content(App: type) type {
pub fn Title(App: type) type {
return struct {
pub fn init() @This() {
return .{};
}
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
@@ -122,15 +124,8 @@ pub fn Title(App: type) type {
pub fn InfoBanner(App: type) type {
return struct {
left_text: []const u8,
right_text: []const u8,
pub fn init() @This() {
return .{
.left_text = "Build with zig",
.right_text = "Yves Biener (@yves-biener)",
};
}
left_text: []const u8 = "Build with zig",
right_text: []const u8 = "Yves Biener (@yves-biener)",
pub fn element(this: *@This()) App.Element {
return .{
@@ -141,7 +136,7 @@ pub fn InfoBanner(App: type) type {
};
}
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
fn content(ctx: *anyopaque, model: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *const @This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
@@ -154,6 +149,22 @@ pub fn InfoBanner(App: type) type {
cells[idx].cp = cp;
}
switch (model.page) {
.about => {},
.blog => |path| if (path) |p| page: {
const start_idx = (size.x -| p.len) / 2;
if (start_idx <= this.left_text.len) break :page;
for (start_idx.., p) |idx, cp| {
// NOTE do not write over the contents of this `Container`'s `Size`
if (idx == cells.len) break;
cells[idx].style.fg = .default;
cells[idx].cp = cp;
}
},
}
var start_idx = size.x -| this.right_text.len;
if (start_idx <= this.left_text.len) start_idx = this.left_text.len + 1;

View File

@@ -7,8 +7,11 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .init(.{
.document = .init(@embedFile("blog")),
.page = .about,
.document = .init(try std.fs.cwd().readFileAlloc("./doc/about.md", allocator, .unlimited)),
});
defer app.model.deinit(allocator);
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
@@ -36,7 +39,7 @@ pub fn main() !void {
}, .{});
// title
{
var title: Title = .init();
var title: Title = .{};
try header.append(try .init(allocator, .{}, title.element()));
}
// about navigation button
@@ -76,7 +79,7 @@ pub fn main() !void {
}
// footer
{
var info_banner: InfoBanner = .init();
var info_banner: InfoBanner = .{};
const footer: App.Container = try .init(allocator, .{
.size = .{
.dim = .{ .y = 1 },
@@ -98,6 +101,8 @@ pub fn main() !void {
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
// test if the event handling is working correctly
if (key.eql(.{ .cp = zterm.input.Space })) app.postEvent(.{ .blog = allocator.dupe(u8, "./doc/test.md") catch unreachable });
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
@@ -134,10 +139,7 @@ const zlog = @import("zlog");
const zterm = @import("zterm");
const App = zterm.App(
Model,
union(enum) {
about,
blog,
},
Model.Pages,
);
const contents = @import("content.zig");

View File

@@ -1,20 +1,41 @@
page: Pages = .blog,
document: Document = undefined,
page: Pages,
document: Document,
pub const Pages = enum {
pub fn deinit(this: *@This(), allocator: Allocator) void {
this.page.deinit(allocator);
this.document.deinit(allocator);
this.page = undefined;
this.document = undefined;
}
pub const Pages = union(enum) {
about,
blog,
blog: ?[]const u8,
pub fn deinit(this: @This(), allocator: Allocator) void {
switch (this) {
.blog => |path| if (path) |p| allocator.free(p),
else => {},
}
}
};
pub const Document = struct {
// preemble:
title: ?[]const u8 = null,
date: ?[]const u8 = null,
content: []const u8,
content: []const u8 = undefined,
ptr: []const u8,
const Preemble = enum {
title,
date,
};
pub fn init(content: []const u8) @This() {
if (std.mem.startsWith(u8, content, "---\n")) {
var document: @This() = .{ .content = undefined };
var document: @This() = .{ .ptr = content };
// we assume the content includes a preemble
var iter = std.mem.splitSequence(u8, content, "---\n");
assert(iter.next().?.len == 0);
@@ -31,18 +52,20 @@ pub const Document = struct {
};
}
}
document.content = iter.next().?;
document.content = iter.rest();
return document;
} else return .{
.content = content,
.ptr = content,
};
}
};
const Preemble = enum {
title,
date,
pub fn deinit(this: *@This(), allocator: Allocator) void {
allocator.free(this.ptr);
this.* = .{ .ptr = undefined };
}
};
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;

View File

@@ -33,7 +33,11 @@ pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type {
switch (event) {
.about, .blog => this.highlight = event == page,
// TODO accept key input too?
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(page),
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(switch (page) {
.about => .about,
.blog => .{ .blog = null },
else => @compileError("The provided navigation button event to trigger is not a `App.Model.Pages` enum variation."),
}),
else => {},
}
}