diff --git a/build.zig b/build.zig index c5be9c5..2534bdd 100644 --- a/build.zig +++ b/build.zig @@ -25,6 +25,10 @@ pub fn build(b: *std.Build) void { .target = target, .timestamp = true, }); + const zmd_dep = b.dependency("zmd", .{ + .optimize = optimize, + .target = target, + }); const exe = b.addExecutable(.{ .name = "tui-website", @@ -34,6 +38,7 @@ pub fn build(b: *std.Build) void { }); 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")); // This declares intent for the executable to be installed into the // standard location when the user invokes the "install" step (the default diff --git a/build.zig.zon b/build.zig.zon index 1aea534..192c11c 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -31,6 +31,10 @@ .url = "git+https://github.com/rockorager/libvaxis#e3062d62b60bf8a351f267a77e1a94e2614394aa", .hash = "1220479d09031168a100748e103ef5a8ffec5d4b0ff74a7e45cf2a9b09f82342b91f", }, + .zmd = .{ + .url = "git+https://github.com/jetzig-framework/zmd#90e52429cacd4fdc90fd615596fe584ae40ec8e9", + .hash = "12202a4edefedd52478223a44cdc9a3b41d4bc5cf3670497a48377f45ff16a5e3363", + }, }, .paths = .{ "build.zig", diff --git a/src/widget/Header.zig b/src/widget/Header.zig index 44c3f7e..c2c2989 100644 --- a/src/widget/Header.zig +++ b/src/widget/Header.zig @@ -33,18 +33,6 @@ pub fn deinit(this: *@This()) void { fn fillView(this: *@This()) void { this.view.?.clear(); - 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); - } - } - const msg = "Yves Biener"; for (msg, 0..) |_, i| { const cell: vaxis.Cell = .{ @@ -58,8 +46,22 @@ fn fillView(this: *@This()) void { .bold = true, }, }; - this.view.?.writeCell(this.view.?.screen.width - msg.len - 3 + i, 0, cell); + 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); + } + } + + // TODO: github icon, discord icon, mail icon with corresponding links to contact me } /// Update loop for a given widget to react to the provided `Event`. It may diff --git a/src/widget/PopupMenu.zig b/src/widget/PopupMenu.zig index 53f7f7e..90d00c7 100644 --- a/src/widget/PopupMenu.zig +++ b/src/widget/PopupMenu.zig @@ -2,7 +2,9 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const Zmd = @import("zmd").Zmd; +const node2buffer = @import("node2buffer.zig"); const widget = @import("../widget.zig"); const Event = widget.Event; @@ -51,23 +53,21 @@ pub fn draw(this: *@This(), win: vaxis.Window) void { const msg = \\# Goto \\ - \\*a* about - \\*h* home + \\**a** about + \\**h** home ; + var zmd = Zmd.init(this.allocator); + defer zmd.deinit(); + + var cells = std.ArrayList(vaxis.Cell).init(this.allocator); + defer cells.deinit(); + + zmd.parse(msg) catch @panic("failed to parse markdown file"); + node2buffer.toBuffer(zmd.nodes.items[0], this.allocator, msg, &cells, .{}) catch @panic("failed to transform to cell array"); + var col: usize = 0; var row: usize = 0; - 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, - }, - }; + for (cells.items) |cell| { view.writeCell(col, row, cell); if (std.mem.eql(u8, cell.char.grapheme, "\n")) { col = 0; diff --git a/src/widget/ViewPort.zig b/src/widget/ViewPort.zig index b98aba4..d1e11ae 100644 --- a/src/widget/ViewPort.zig +++ b/src/widget/ViewPort.zig @@ -2,27 +2,34 @@ const std = @import("std"); const vaxis = @import("vaxis"); +const Zmd = @import("zmd").Zmd; +const node2buffer = @import("node2buffer.zig"); const widget = @import("../widget.zig"); const Event = widget.Event; allocator: std.mem.Allocator = undefined, unicode: *const vaxis.Unicode = undefined, -buffer: vaxis.widgets.TextView.Buffer = 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, - .buffer = .{}, + .contents = null, + .buffer = std.ArrayList(vaxis.Cell).init(allocator), .view = .{ .vertical_scrollbar = null }, }; } pub fn deinit(this: *@This()) void { - this.buffer.deinit(this.allocator); + if (this.contents) |*content| { + this.allocator.free(content.*); + } + this.buffer.deinit(); this.* = undefined; } @@ -60,10 +67,17 @@ pub fn update(this: *@This(), event: Event) void { 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"); - this.buffer.clear(this.allocator); - const writer = this.buffer.writer(this.allocator, &this.unicode.grapheme_data, &this.unicode.width_data); - file.reader().streamUntilDelimiter(writer, 0, null) catch {}; + var zmd = Zmd.init(this.allocator); + defer zmd.deinit(); + + this.buffer.clearRetainingCapacity(); + zmd.parse(this.contents.?) catch @panic("failed to parse markdown contents"); + node2buffer.toBuffer(zmd.nodes.items[0], this.allocator, this.contents.?, &this.buffer, .{}) catch @panic("failed to transform to cell array"); }, else => {}, } @@ -73,30 +87,22 @@ pub fn update(this: *@This(), event: Event) void { /// 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.view.draw(win, .{ .cols = this.buffer.cols, .rows = this.buffer.rows }); + // FIXME: the current node2buffer implementation cannot determine how many rows and columns there are from the read file contents + // this.view.draw(win, .{ .cols = this.buffer.cols, .rows = this.buffer.rows }); // needed for scroll bar scaling and display const Pos = struct { x: usize = 0, y: usize = 0 }; var pos: Pos = .{}; const bounds = this.view.bounds(win); - for (this.buffer.grapheme.items(.len), this.buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| { + for (this.buffer.items) |cell| { if (bounds.above(pos.y)) break; + // if (!bounds.colInside(pos.x)) continue; // FIX: how can I prevent writes out of bounce of the viewport? - const cluster = this.buffer.content.items[g_offset..][0..g_len]; - if (std.mem.eql(u8, cluster, "\n")) { - if (index == this.buffer.grapheme.len - 1) break; - - pos.y +|= 1; + this.view.writeCell(win, pos.x, pos.y, cell); + if (std.mem.eql(u8, cell.char.grapheme, "\n")) { pos.x = 0; - continue; - } else if (bounds.below(pos.y)) { - continue; + pos.y += 1; + } else { + pos.x += 1; } - - const width = win.gwidth(cluster); - defer pos.x +|= width; - - if (!bounds.colInside(pos.x)) continue; - - this.view.writeCell(win, pos.x, pos.y, .{ .char = .{ .grapheme = cluster, .width = width }, .style = .{} }); } } diff --git a/src/widget/node2buffer.zig b/src/widget/node2buffer.zig new file mode 100644 index 0000000..19ee4e7 --- /dev/null +++ b/src/widget/node2buffer.zig @@ -0,0 +1,117 @@ +///! Transform a given `zmd.Note` into a buffer which can be used by any `vaxis.widgets.View` +const std = @import("std"); +const vaxis = @import("vaxis"); +const zmd = @import("zmd"); + +pub fn toBuffer(node: *zmd.Node, allocator: std.mem.Allocator, input: []const u8, array: *std.ArrayList(vaxis.Cell), s: vaxis.Cell.Style) !void { + const content = switch (node.token.element.type) { + .text => input[node.token.start..node.token.end], + .code, .block => node.content, + else => "", + }; + var style = s; + + // determine general styling changes + switch (node.token.element.type) { + .bold => { + style.bold = true; + }, + .bold_close => { + style.bold = false; + }, + .italic => { + style.italic = true; + }, + .italic_close => { + style.italic = false; + }, + .block => { + // TODO: what should I do with blocks? + }, + .block_close => { + // TODO: what should I do with blocks? + }, + .code => { + style.dim = true; + }, + .code_close => { + style.dim = false; + }, + else => {}, + } + + switch (node.token.element.type) { + .root, .none, .eof => {}, + .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, + }); + } + }, + else => { + for (content, 0..) |_, i| { + try array.append(.{ + .char = .{ .grapheme = content[i .. i + 1] }, + .style = style, + }); + } + }, + } + + for (node.children.items) |child_node| { + try toBuffer(child_node, allocator, input, array, style); + } +}