Skip to content

Commit b409bf8

Browse files
committed
feat: console: Add paging output
A bit weird when attempting to display images, but works for plain text
1 parent 5e49ccc commit b409bf8

6 files changed

Lines changed: 147 additions & 13 deletions

File tree

src/app/present.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const std = @import("std");
22
const zd = @import("zigdown");
3-
const RawTTY = @import("RawTTY.zig");
3+
const RawTTY = zd.RawTTY;
44

55
const Allocator = std.mem.Allocator;
66
const ArrayList = std.array_list.Managed;
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ pub fn read(self: Self) u8 {
7474
}
7575
}
7676

77-
fn moveCursor(self: Self, row: usize, col: usize) !void {
77+
/// Move the cursor to the given (zero-indexed) row and column
78+
pub fn moveCursor(self: Self, row: usize, col: usize) !void {
7879
_ = try self.writer.print("\x1B[{};{}H", .{ row + 1, col + 1 });
7980
}

src/lib/cli.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub const ConsoleRenderCmdOpts = struct {
9898
verbose: bool = false,
9999
timeit: bool = false,
100100
nofetch: bool = false,
101+
pager: bool = false,
101102
positional: struct {
102103
file: ?[]const u8,
103104
pub const descriptions = .{
@@ -111,6 +112,7 @@ pub const ConsoleRenderCmdOpts = struct {
111112
.timeit = "Time the parsing & rendering and display the results",
112113
.verbose = "Enable verbose output from the parser",
113114
.nofetch = "Don't fetch images from the internet (just display the image link)",
115+
.pager = "Page the output in the terminal (e.g. like 'less')",
114116
};
115117
pub const switches = .{
116118
.stdin = 'i',
@@ -119,6 +121,7 @@ pub const ConsoleRenderCmdOpts = struct {
119121
.verbose = 'v',
120122
.timeit = 't',
121123
.nofetch = 'n',
124+
.pager = 'p',
122125
};
123126
};
124127

src/lib/render.zig

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ const std = @import("std");
22
const blocks = @import("ast/blocks.zig");
33
const gfx = @import("image.zig");
44
const cli = @import("cli.zig");
5+
const cons = @import("console.zig");
6+
const RawTTY = @import("RawTTY.zig");
57

68
const Allocator = std.mem.Allocator;
79

@@ -57,8 +59,13 @@ pub fn render(opts: RenderOptions) !void {
5759
columns = @min(tsize.cols, columns);
5860
}
5961

62+
var render_buf: std.io.Writer.Allocating = .init(opts.alloc);
63+
defer render_buf.deinit();
64+
65+
const render_writer: *std.io.Writer = if (cfg.pager) &render_buf.writer else opts.out_stream;
66+
6067
var c_renderer = ConsoleRenderer.init(
61-
opts.out_stream,
68+
render_writer,
6269
arena.allocator(),
6370
.{
6471
.root_dir = opts.document_dir,
@@ -71,6 +78,12 @@ pub fn render(opts: RenderOptions) !void {
7178
);
7279
defer c_renderer.deinit();
7380
try c_renderer.renderBlock(opts.document);
81+
82+
if (cfg.pager) {
83+
// If we're paging the output, the render above was to a temporary buffer.
84+
// Take that output and page it to the console
85+
try pageOutput(opts.alloc, opts.out_stream, render_buf.written());
86+
}
7487
},
7588
.range => {
7689
// Get the terminal size; limit our width to that
@@ -112,6 +125,84 @@ pub fn render(opts: RenderOptions) !void {
112125
opts.out_stream.flush() catch @panic("Can't flush output stream");
113126
}
114127

128+
/// Page the output to the terminal given by 'writer'.
129+
///
130+
/// alloc: The allocator to use for all file reading, parsing, and rendering.
131+
/// writer: The writer for the tty to page the output to.
132+
/// output: The rendered output to page.
133+
pub fn pageOutput(alloc: Allocator, writer: *std.io.Writer, output: []const u8) !void {
134+
const raw_tty = try RawTTY.init(writer);
135+
defer raw_tty.deinit();
136+
137+
var lines: std.ArrayList([]const u8) = try splitLines(alloc, output);
138+
defer lines.deinit(alloc);
139+
140+
const n_rows = lines.items.len;
141+
const tsize = gfx.getTerminalSize() catch gfx.TermSize{};
142+
143+
// Begin the presentation, using stdin to go forward/backward
144+
var quit: bool = false;
145+
var row: usize = 0;
146+
while (!quit) {
147+
_ = try writer.write(cons.clear_screen);
148+
try raw_tty.moveCursor(0, 0);
149+
for (lines.items[row..@min(row + tsize.rows - 1, n_rows)]) |line| {
150+
try writer.writeAll(line);
151+
try writer.writeAll("\n");
152+
}
153+
// TODO: Consider putting in lower-right corner like slide # in present mode
154+
// try writer.print("[row {d} / {d}]\n", .{ row, n_rows });
155+
try writer.flush();
156+
157+
switch (raw_tty.read()) {
158+
'n', 'j', 'l' => { // Next Slide
159+
if (row < n_rows)
160+
row += 1;
161+
},
162+
'p', 'h', 'k' => { // Previous Slide
163+
if (row > 0) {
164+
row -= 1;
165+
}
166+
},
167+
'q' => { // Quit
168+
quit = true;
169+
},
170+
27 => { // Escape (0x1b)
171+
if (raw_tty.read() == 91) { // 0x5b (??)
172+
switch (raw_tty.read()) {
173+
66, 67 => { // Down, Right -- Next Slide
174+
if (row < n_rows) {
175+
row += 1;
176+
}
177+
},
178+
65, 68 => { // Up, Left -- Previous Slide
179+
if (row > 0) {
180+
row -= 1;
181+
}
182+
},
183+
else => {},
184+
}
185+
}
186+
},
187+
else => {},
188+
}
189+
}
190+
}
191+
192+
/// Split a document into individual lines.
193+
///
194+
/// The lines of text are not duplicated; they point within the given text slice.
195+
fn splitLines(alloc: Allocator, text: []const u8) !std.ArrayList([]const u8) {
196+
var lines: std.ArrayList([]const u8) = .empty;
197+
198+
var line_iter = std.mem.splitScalar(u8, text, '\n');
199+
while (line_iter.next()) |line| {
200+
try lines.append(alloc, line);
201+
}
202+
203+
return lines;
204+
}
205+
115206
//////////////////////////////////////////////////////////
116207
// Tests
117208
//////////////////////////////////////////////////////////

src/lib/render/render_console.zig

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -627,6 +627,10 @@ pub const ConsoleRenderer = struct {
627627
const ncol = table.content.Table.ncol;
628628
const col_w = @divFloor(self.opts.width - (2 * self.opts.indent) - (ncol + 1), ncol);
629629

630+
// The total width of all text to be rendered (removing inter-column spacing)
631+
const render_width = self.opts.width - (2 * self.opts.indent) - (ncol + 1);
632+
const render_width_f: f32 = @floatFromInt(render_width);
633+
630634
// Create a new renderer to render into a buffer for each cell
631635
// Use an arena to simplify memory management here
632636
var arena = std.heap.ArenaAllocator.init(self.alloc);
@@ -639,11 +643,33 @@ pub const ConsoleRenderer = struct {
639643
};
640644
var cells = ArrayList(Cell).init(alloc);
641645

642-
for (table.children.items) |item| {
646+
const relative_widths = table.content.Table.relative_width.items;
647+
const total_width: usize = blk: {
648+
var sum: usize = 0;
649+
for (relative_widths) |w| {
650+
sum += w;
651+
}
652+
break :blk sum;
653+
};
654+
const total_width_f: f32 = @floatFromInt(total_width);
655+
656+
// Compute & store the (integer) width of each column
657+
var col_widths = try ArrayList(u32).initCapacity(alloc, ncol);
658+
for (relative_widths) |w| {
659+
const rel_width: f32 = @floatFromInt(w);
660+
const width_frac: f32 = rel_width / total_width_f;
661+
col_widths.appendAssumeCapacity(@intFromFloat(@round(width_frac * render_width_f)));
662+
}
663+
664+
for (table.children.items, 0..) |item, i| {
665+
// Compute the width of this cell based on which column it is in
666+
const col_idx: usize = @mod(i, ncol);
667+
const width: usize = col_widths.items[col_idx];
668+
643669
// Render the table cell into a new buffer
644670
var alloc_writer = std.Io.Writer.Allocating.init(alloc);
645671
const sub_opts = Config{
646-
.width = col_w - 1,
672+
.width = width - 1, // col_w - 1,
647673
.indent = 1,
648674
.max_image_rows = self.opts.max_image_rows,
649675
.max_image_cols = col_w - 2 * self.opts.indent,
@@ -664,7 +690,7 @@ pub const ConsoleRenderer = struct {
664690
const nrow: usize = @divFloor(cells.items.len, ncol);
665691
std.debug.assert(cells.items.len == ncol * nrow);
666692

667-
self.writeTableBorderTop(ncol, col_w);
693+
self.writeTableBorderTop(ncol, col_widths.items);
668694

669695
for (0..nrow) |i| {
670696
// Get the max number of rows of text for any cell in the table row
@@ -683,14 +709,20 @@ pub const ConsoleRenderer = struct {
683709
// Loop over the # of rows of text in this single row of the table
684710
for (0..max_rows) |_| {
685711
self.writeLeaders();
712+
var left_col_idx: usize = self.opts.indent + 2;
686713
for (0..ncol) |j| {
687714
const cell_idx: usize = i * ncol + j;
688715
const cell: *Cell = &cells.items[cell_idx];
689716

717+
const rel_width: f32 = @floatFromInt(relative_widths[j]);
718+
const width_f: f32 = (rel_width / total_width_f) * render_width_f;
719+
const width: usize = @intFromFloat(@round(width_f));
720+
690721
// For each cell in the row...
691722
self.write(self.opts.box_style.vb);
692723
self.write(" ");
693724

725+
// --- TODO --- left/center/right alignment should be handled here
694726
if (cell.idx < cell.text.len) {
695727
// Write the next line of text from that cell,
696728
// then increment the write head index of that cell.
@@ -706,11 +738,14 @@ pub const ConsoleRenderer = struct {
706738
cell.idx += text.len + 1;
707739

708740
// Move the cursor to the start of the next cell
709-
const new_col: usize = self.opts.indent + (j + 2) + (j + 1) * col_w;
741+
// const new_col: usize = self.opts.indent + (j + 2) + (j + 1) * col_w;
742+
left_col_idx += width + 1;
743+
const new_col = left_col_idx;
710744
self.printno(cons.set_col, .{new_col});
711745
self.column = new_col;
712746
} else {
713-
self.writeNTimes(" ", col_w - 1);
747+
// self.writeNTimes(" ", col_w - 1);
748+
self.writeNTimes(" ", width - 1);
714749
}
715750
}
716751
self.write(self.opts.box_style.vb);
@@ -721,16 +756,17 @@ pub const ConsoleRenderer = struct {
721756
self.writeLeaders();
722757

723758
if (i == nrow - 1) {
724-
self.writeTableBorderBottom(ncol, col_w);
759+
self.writeTableBorderBottom(ncol, col_widths.items);
725760
} else {
726-
self.writeTableBorderMiddle(ncol, col_w);
761+
self.writeTableBorderMiddle(ncol, col_widths.items);
727762
}
728763
}
729764
}
730765

731-
fn writeTableBorderTop(self: *Self, ncol: usize, col_w: usize) void {
766+
fn writeTableBorderTop(self: *Self, ncol: usize, col_widths: []const u32) void {
732767
self.write(self.opts.box_style.tl);
733768
for (0..ncol) |i| {
769+
const col_w: usize = col_widths[i];
734770
for (0..col_w) |_| {
735771
self.write(self.opts.box_style.hb);
736772
}
@@ -744,9 +780,10 @@ pub const ConsoleRenderer = struct {
744780
self.writeLeaders();
745781
}
746782

747-
fn writeTableBorderMiddle(self: *Self, ncol: usize, col_w: usize) void {
783+
fn writeTableBorderMiddle(self: *Self, ncol: usize, col_widths: []const u32) void {
748784
self.write(self.opts.box_style.lj);
749785
for (0..ncol) |i| {
786+
const col_w: usize = col_widths[i];
750787
for (0..col_w) |_| {
751788
self.write(self.opts.box_style.hb);
752789
}
@@ -760,9 +797,10 @@ pub const ConsoleRenderer = struct {
760797
self.writeLeaders();
761798
}
762799

763-
fn writeTableBorderBottom(self: *Self, ncol: usize, col_w: usize) void {
800+
fn writeTableBorderBottom(self: *Self, ncol: usize, col_widths: []const u32) void {
764801
self.write(self.opts.box_style.bl);
765802
for (0..ncol) |i| {
803+
const col_w: usize = col_widths[i];
766804
for (0..col_w) |_| {
767805
self.write(self.opts.box_style.hb);
768806
}

src/lib/zigdown.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub const blocks = @import("ast/blocks.zig");
2222
pub const gfx = @import("image.zig");
2323
pub const ts_queries = @import("ts_queries.zig");
2424
pub const wasm = @import("wasm.zig");
25+
pub const RawTTY = @import("RawTTY.zig");
2526

2627
// Global public Zigdown types
2728
// pub const Markdown = markdown.Markdown;

0 commit comments

Comments
 (0)