intermediate #1

Merged
yves-biener merged 31 commits from intermediate into main 2025-02-16 16:02:59 +01:00
9 changed files with 184 additions and 225 deletions
Showing only changes of commit 3decc541a9 - Show all commits

View File

@@ -3,7 +3,7 @@ const zterm = @import("zterm");
const App = zterm.App( const App = zterm.App(
union(enum) {}, union(enum) {},
zterm.Renderer.Direct, zterm.Renderer.Buffered,
true, true,
); );
const Key = zterm.Key; const Key = zterm.Key;
@@ -26,7 +26,8 @@ pub fn main() !void {
const allocator = gpa.allocator(); const allocator = gpa.allocator();
var app: App = .{}; var app: App = .{};
var renderer: App.Renderer = .{}; var renderer = App.Renderer.init(allocator);
defer renderer.deinit();
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents // TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use? // -> size hint how much should it use?
@@ -75,17 +76,19 @@ pub fn main() !void {
}, },
.{ .{
.layout = Layout.createFrom(Layout.VStack.init(allocator, .{ .layout = Layout.createFrom(Layout.VStack.init(allocator, .{
Widget.createFrom(blk: { // Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
defer file.close(); // defer file.close();
break :blk Widget.RawText.init(allocator, file); // break :blk Widget.RawText.init(allocator, file);
}), // }),
Widget.createFrom(Widget.Spacer.init(allocator)), Widget.createFrom(Widget.Spacer.init(allocator)),
Widget.createFrom(blk: { Widget.createFrom(Widget.Spacer.init(allocator)),
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{}); Widget.createFrom(Widget.Spacer.init(allocator)),
defer file.close(); // Widget.createFrom(blk: {
break :blk Widget.RawText.init(allocator, file); // const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
}), // defer file.close();
// break :blk Widget.RawText.init(allocator, file);
// }),
})), })),
}, },
), ),

View File

@@ -7,7 +7,7 @@ const App = zterm.App(
tui, // view instance to the corresponding view for 'tui' tui, // view instance to the corresponding view for 'tui'
}, },
}, },
zterm.Renderer.Direct, zterm.Renderer.Buffered,
true, true,
); );
const Cell = zterm.Cell; const Cell = zterm.Cell;
@@ -24,49 +24,26 @@ const Tui = struct {
pub fn init(allocator: std.mem.Allocator) *Tui { pub fn init(allocator: std.mem.Allocator) *Tui {
var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig"); var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig");
tui.allocator = allocator; tui.allocator = allocator;
// FIXME: the layout creates an 'incorrect alignment'? tui.layout = Layout.createFrom(Layout.VStack.init(allocator, .{
tui.layout = Layout.createFrom(Layout.VContainer.init(allocator, .{ Layout.createFrom(Layout.HStack.init(allocator, .{
.{ Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
Layout.createFrom(Layout.Framing.init(allocator, .{ .{ .rune = 'Y', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
.title = .{
.str = "Welcome to my terminal website",
.style = .{
.ul = .{ .index = 6 },
.ul_style = .single,
},
},
}, .{
.layout = Layout.createFrom(Layout.HContainer.init(allocator, .{
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "Yves Biener", .style = .{ .bold = true } },
})),
25,
},
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "File name", .style = .{ .bold = true } },
})),
50,
},
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "Contacts", .style = .{ .bold = true } },
})),
25,
},
})),
})), })),
10, Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
}, .{ .rune = 'F', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
.{
Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{
.widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{
.{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } },
})),
})), })),
90, Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
}, .{ .rune = 'C', .style = .{ .fg = .{ .index = 6 }, .bold = true } },
})),
})),
// .{
// Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{
// .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{
// .{ .rune = 'D', .style = .{ .ul = .default, .ul_style = .single } },
// })),
// })),
// 90,
// },
})); }));
return tui; return tui;
} }
@@ -76,6 +53,14 @@ const Tui = struct {
this.allocator.destroy(this); this.allocator.destroy(this);
} }
pub fn enable(this: *Tui) void {
_ = this;
}
pub fn disable(this: *Tui) void {
_ = this;
}
pub fn handle(this: *Tui, event: App.Event) !*Events { pub fn handle(this: *Tui, event: App.Event) !*Events {
return try this.layout.handle(event); return try this.layout.handle(event);
} }
@@ -99,7 +84,9 @@ pub fn main() !void {
const allocator = arena.allocator(); const allocator = arena.allocator();
var app: App = .{}; var app: App = .{};
var renderer: App.Renderer = .{}; var renderer = App.Renderer.init(allocator);
defer renderer.deinit();
var view: View = undefined; var view: View = undefined;
var tui_view = View.createFrom(Tui.init(allocator)); var tui_view = View.createFrom(Tui.init(allocator));
@@ -147,5 +134,6 @@ pub fn main() !void {
app.postEvent(e); app.postEvent(e);
} }
try view.render(&renderer); try view.render(&renderer);
try renderer.flush();
} }
} }

View File

@@ -126,8 +126,8 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
return &this.events; return &this.events;
} }
const round_frame = .{ "", "", "", "", "", "" }; const round_frame: [6][]const u8 = .{ "", "", "", "", "", "" };
const square_frame = .{ "", "", "", "", "", "" }; const square_frame: [6][]const u8 = .{ "", "", "", "", "", "" };
fn renderFrame(this: *@This(), renderer: *Renderer) !void { fn renderFrame(this: *@This(), renderer: *Renderer) !void {
// FIXME: use renderer instead! // FIXME: use renderer instead!
@@ -140,14 +140,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
// render top: +---+ // render top: +---+
try terminal.setCursorPosition(this.size.anchor); try terminal.setCursorPosition(this.size.anchor);
const writer = terminal.writer(); const writer = terminal.writer();
try this.config.style.value(writer, frame[0]); // try this.config.style.value(writer, frame[0]);
if (this.config.title.str.len > 0) { for (0..this.config.title.str.len) |i| {
try this.config.title.style.value(writer, this.config.title.str); try this.config.title.style.value(writer, this.config.title.str[i]);
} }
for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| { for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| {
try this.config.style.value(writer, frame[1]); // try this.config.style.value(writer, frame[1]);
} }
try this.config.style.value(writer, frame[2]); // try this.config.style.value(writer, frame[2]);
// render left: | // render left: |
for (1..this.size.rows -| 1) |r| { for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r); const row: u16 = @truncate(r);
@@ -155,7 +155,7 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
.col = this.size.anchor.col, .col = this.size.anchor.col,
.row = this.size.anchor.row + row, .row = this.size.anchor.row + row,
}); });
try this.config.style.value(writer, frame[3]); // try this.config.style.value(writer, frame[3]);
} }
// render right: | // render right: |
for (1..this.size.rows -| 1) |r| { for (1..this.size.rows -| 1) |r| {
@@ -164,18 +164,18 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
.col = this.size.anchor.col + this.size.cols -| 1, .col = this.size.anchor.col + this.size.cols -| 1,
.row = this.size.anchor.row + row, .row = this.size.anchor.row + row,
}); });
try this.config.style.value(writer, frame[3]); // try this.config.style.value(writer, frame[3]);
} }
// render bottom: +---+ // render bottom: +---+
try terminal.setCursorPosition(.{ try terminal.setCursorPosition(.{
.col = this.size.anchor.col, .col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows - 1, .row = this.size.anchor.row + this.size.rows - 1,
}); });
try this.config.style.value(writer, frame[4]); // try this.config.style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| { for (0..this.size.cols -| 2) |_| {
try this.config.style.value(writer, frame[1]); // try this.config.style.value(writer, frame[1]);
} }
try this.config.style.value(writer, frame[5]); // try this.config.style.value(writer, frame[5]);
} }
pub fn render(this: *@This(), renderer: *Renderer) !void { pub fn render(this: *@This(), renderer: *Renderer) !void {

View File

@@ -10,97 +10,107 @@
const std = @import("std"); const std = @import("std");
const terminal = @import("terminal.zig"); const terminal = @import("terminal.zig");
const Cells = []const terminal.Cell; const Cell = terminal.Cell;
const Position = terminal.Position; const Position = terminal.Position;
const Size = terminal.Size; const Size = terminal.Size;
pub fn Direct(comptime fullscreen: bool) type { /// Double-buffered intermediate rendering pipeline
const log = std.log.scoped(.renderer_direct); pub fn Buffered(comptime fullscreen: bool) type {
_ = log; const log = std.log.scoped(.renderer_buffered);
// _ = log;
_ = fullscreen; _ = fullscreen;
return struct { return struct {
size: Size = undefined, allocator: std.mem.Allocator,
created: bool,
size: Size,
screen: []Cell,
virtual_screen: []Cell,
pub fn resize(this: *@This(), size: Size) void { pub fn init(allocator: std.mem.Allocator) @This() {
this.size = size; return .{
.allocator = allocator,
.created = false,
.size = undefined,
.screen = undefined,
.virtual_screen = undefined,
};
} }
pub fn clear(this: *@This(), size: Size) !void { pub fn deinit(this: *@This()) void {
_ = this; if (this.created) {
// NOTE: clear on the entire screen may introduce too much overhead and could instead clear the entire screen instead. this.allocator.free(this.screen);
// - it could also then try to optimize for further *clear* calls, that result in pretty much a nop? -> how to identify those clear calls? this.allocator.free(this.virtual_screen);
// TODO: this should instead by dynamic and correct of size (terminal could be too large currently)
std.debug.assert(1028 > size.cols);
var buf: [1028]u8 = undefined;
@memset(buf[0..], ' ');
for (0..size.rows) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = size.anchor.col,
.row = size.anchor.row + row,
});
_ = try terminal.write(buf[0..size.cols]);
} }
} }
pub fn render(this: *@This(), size: Size, cells: Cells) !void { pub fn resize(this: *@This(), size: Size) void {
_ = this; this.size = size;
try terminal.setCursorPosition(size.anchor); if (!this.created) {
var row: u16 = 0; this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory.");
var remaining_cols = size.cols; this.virtual_screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory.");
this.created = true;
return;
}
if (this.allocator.resize(this.screen, size.cols * size.rows)) {
@panic("render.zig: Could not resize `screen` buffer");
}
if (this.allocator.resize(this.virtual_screen, size.cols * size.rows)) {
@panic("render.zig: Could not resize `virtual screen` buffer.");
}
}
pub fn clear(this: *@This(), size: Size) !void {
log.debug("renderer::clear", .{});
var vs = this.virtual_screen;
const anchor = (size.anchor.row * this.size.rows) + size.anchor.col;
for (0..size.rows) |row| {
for (0..size.cols) |col| {
vs[anchor + (row * this.size.cols) + col].reset();
}
}
}
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), size: Size, cells: []const Cell) !void {
log.debug("renderer:render: cells: {any}", .{cells});
std.debug.assert(cells.len > 0);
var idx: usize = 0;
var vs = this.virtual_screen;
const anchor = (size.anchor.row * this.size.rows) + size.anchor.col;
for (0..size.rows) |row| {
for (0..size.cols) |col| {
const cell = cells[idx];
idx += 1;
vs[anchor + (row * this.size.cols) + col].style = cell.style;
vs[anchor + (row * this.size.cols) + col].rune = cell.rune;
if (cells.len == idx) return;
}
}
}
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
pub fn flush(this: *@This()) !void {
log.debug("renderer::flush", .{});
const writer = terminal.writer(); const writer = terminal.writer();
for (cells) |cell| { const s = this.screen;
var idx: usize = 0; const vs = this.virtual_screen;
print_cell: while (true) { for (0..this.size.rows) |row| {
const cell_len = cell.len(idx); for (0..this.size.cols) |col| {
if (cell_len > remaining_cols) { const idx = (row * this.size.cols) + col;
const result = try cell.writeUpToNewline(writer, idx, idx + remaining_cols); const cs = s[idx];
row += 1; const cvs = vs[idx];
if (row >= size.rows) { if (cs.eql(cvs))
return; // we are done continue;
} // render differences found in virtual screen
try terminal.setCursorPosition(.{ // TODO: improve the writing speed (many unecessary writes (i.e. the style for every character..))
.col = size.anchor.col, try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) });
.row = size.anchor.row + row, try cvs.value(writer);
}); // update screen to be the virtual screen for the next frame
remaining_cols = size.cols; s[idx] = vs[idx];
idx = result.idx;
if (result.newline) {
idx += 1; // skip over newline
} else {
// there is still content to the newline (which will not be printed)
for (idx..cell.content.len) |i| {
if (cell.content[i] == '\n') {
idx = i + 1;
continue :print_cell;
}
}
break; // go to next cell (as we went to the end of the cell and do not print on the next line)
}
} else {
// print rest of cell
const result = try cell.writeUpToNewline(writer, idx, idx + cell_len);
if (result.newline) {
row += 1;
if (row >= size.rows) {
return; // we are done
}
try terminal.setCursorPosition(.{
.col = size.anchor.col,
.row = size.anchor.row + row,
});
remaining_cols = size.cols;
idx = result.idx + 1; // skip over newline
} else {
remaining_cols -= @truncate(cell_len -| idx);
idx = 0;
break; // go to next cell
}
}
// written all cell contents
if (idx >= cell.content.len) {
break; // go to next cell
}
} }
} }
} }

View File

@@ -20,7 +20,7 @@ pub const ReportMode = enum {
pub fn getTerminalSize() Size { pub fn getTerminalSize() Size {
var ws: std.posix.winsize = undefined; var ws: std.posix.winsize = undefined;
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); _ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
return .{ .cols = ws.col, .rows = ws.row }; return .{ .cols = ws.col - 1, .rows = ws.row - 1 };
} }
pub fn saveScreen() !void { pub fn saveScreen() !void {
@@ -126,8 +126,8 @@ pub fn getCursorPosition() !Position {
} }
return .{ return .{
.row = try std.fmt.parseInt(u16, row[0..ridx], 10), .row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
.col = try std.fmt.parseInt(u16, col[0..cidx], 10), .col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
}; };
} }

View File

@@ -2,60 +2,24 @@ const std = @import("std");
pub const Style = @import("Style.zig"); pub const Style = @import("Style.zig");
style: Style = .{}, style: Style = .{},
content: []const u8 = undefined, rune: u8 = ' ',
pub const Result = struct { pub const Character = struct {
idx: usize, grapheme: []const u8,
newline: bool, width: u8,
}; };
pub fn len(this: @This(), start: usize) usize { pub fn eql(this: @This(), other: @This()) bool {
std.debug.assert(this.content.len > start); return this.rune == other.rune and this.style.eql(other.style);
return this.content[start..].len;
} }
pub fn write(this: @This(), writer: anytype, start: usize, end: usize) !void { pub fn reset(this: *@This()) void {
std.debug.assert(this.content.len > start); this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off };
std.debug.assert(this.content.len >= end); this.rune = ' ';
std.debug.assert(start < end);
try this.style.value(writer, this.content[start..end]);
} }
pub fn writeUpToNewline(this: @This(), writer: anytype, start: usize, end: usize) !Result { pub fn value(this: @This(), writer: anytype) !void {
std.debug.assert(this.content.len > start); try this.style.value(writer, this.rune);
std.debug.assert(this.content.len >= end);
std.debug.assert(start < end);
for (start..end) |i| {
if (this.content[i] == '\n') {
if (start < i) {
// this is just an empty line with a newline
try this.value(writer, start, i);
}
return .{
.idx = i,
.newline = true,
};
}
}
try this.write(writer, start, end);
return .{
.idx = end,
.newline = false,
};
}
pub fn value(this: @This(), writer: anytype, start: usize, end: usize) !void {
std.debug.assert(start < this.content.len);
std.debug.assert(this.content.len >= end);
std.debug.assert(start < end);
try this.style.value(writer, this.content[start..end]);
}
// not really supported
pub fn format(this: @This(), writer: anytype, comptime fmt: []const u8, args: anytype) !void {
try this.style.format(writer, fmt, args); // NOTE: args should contain this.content[start..end] or this.content
} }
test { test {

View File

@@ -1,5 +1,5 @@
const Position = @import("Position.zig"); const Position = @import("Position.zig");
anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default anchor: Position = .{ .col = 0, .row = 0 }, // top left corner by default
cols: u16, cols: u16,
rows: u16, rows: u16,

View File

@@ -113,6 +113,20 @@ reverse: bool = false,
invisible: bool = false, invisible: bool = false,
strikethrough: bool = false, strikethrough: bool = false,
pub fn eql(this: @This(), other: @This()) bool {
return this.fg.eql(other.fg) and
this.bg.eql(other.bg) and
this.ul.eql(other.ul) and
other.ul_style == this.ul_style and
other.bold == this.bold and
other.dim == this.dim and
other.italic == this.italic and
other.blink == this.blink and
other.reverse == this.reverse and
other.invisible == this.invisible and
other.strikethrough == this.strikethrough;
}
/// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value /// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value
/// if the _other_ value differs from the default value. /// if the _other_ value differs from the default value.
pub fn merge(this: *@This(), other: @This()) void { pub fn merge(this: *@This(), other: @This()) void {
@@ -284,15 +298,9 @@ fn end(this: @This(), writer: anytype) !void {
} }
} }
pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void { pub fn value(this: @This(), writer: anytype, content: u8) !void {
try this.start(writer); try this.start(writer);
try std.fmt.format(writer, content, args); _ = try writer.write(&[_]u8{content});
try this.end(writer);
}
pub fn value(this: @This(), writer: anytype, content: []const u8) !void {
try this.start(writer);
_ = try writer.write(content);
try this.end(writer); try this.end(writer);
} }

View File

@@ -62,10 +62,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
switch (this.alignment) { switch (this.alignment) {
.default => break :blk this.size, .default => break :blk this.size,
.center => { .center => {
var length_usize: usize = 0; const length_usize = this.contents.len;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize); const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols); const cols = @min(length, this.size.cols);
const rows = cols / length; const rows = cols / length;
@@ -79,10 +76,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
}; };
}, },
.top => { .top => {
var length_usize: usize = 0; const length_usize = this.contents.len;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize); const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols); const cols = @min(length, this.size.cols);
const rows = cols / length; const rows = cols / length;
@@ -96,10 +90,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
}; };
}, },
.bottom => { .bottom => {
var length_usize: usize = 0; const length_usize = this.contents.len;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize); const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols); const cols = @min(length, this.size.cols);
const rows = cols / length; const rows = cols / length;
@@ -113,10 +104,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
}; };
}, },
.left => { .left => {
var length_usize: usize = 0; const length_usize = this.contents.len;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize); const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols); const cols = @min(length, this.size.cols);
const rows = cols / length; const rows = cols / length;
@@ -130,10 +118,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
}; };
}, },
.right => { .right => {
var length_usize: usize = 0; const length_usize = this.contents.len;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize); const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols); const cols = @min(length, this.size.cols);
const rows = cols / length; const rows = cols / length;
@@ -148,6 +133,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
}, },
} }
}; };
log.debug("Text.contents: {any}", .{this.contents});
try renderer.render(size, this.contents); try renderer.render(size, this.contents);
this.require_render = false; this.require_render = false;
} }