intermediate #1

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

View File

@@ -18,14 +18,14 @@ pub fn build(b: *std.Build) void {
lib.addImport("code_point", zg.module("code_point"));
// TODO: examples (not yet available)
// const stack_example = b.addExecutable(.{
// .name = "stack",
// .root_source_file = b.path("examples/stack.zig"),
// .target = target,
// .optimize = optimize,
// });
// stack_example.root_module.addImport("zterm", lib);
// b.installArtifact(stack_example);
const container = b.addExecutable(.{
.name = "container",
.root_source_file = b.path("examples/container.zig"),
.target = target,
.optimize = optimize,
});
container.root_module.addImport("zterm", lib);
b.installArtifact(container);
// testing
const lib_unit_tests = b.addTest(.{

59
examples/container.zig Normal file
View File

@@ -0,0 +1,59 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const Key = zterm.Key;
const log = std.log.scoped(.example);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory lead", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var container = try App.Container.init(allocator, .{});
defer container.deinit();
// NOTE: should the min-size here be required?
try app.start(null);
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.init => {
if (container.handle(event)) |e| app.postEvent(e);
continue;
},
.quit => break,
.resize => |size| try renderer.resize(size),
.key => |key| {
if (key.matches(.{ .cp = 'q' }))
app.quit();
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg });
},
else => {},
}
// TODO: should instead use tryPost because it may block the main loop from actually removing events from the queue, deadlocking itself
if (container.handle(event)) |e| app.postEvent(e);
renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -7,6 +7,7 @@ const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Key = @import("key.zig");
const Size = @import("size.zig");
const Queue = @import("queue.zig").Queue;
const log = std.log.scoped(.app);
@@ -15,14 +16,6 @@ const log = std.log.scoped(.app);
/// an tagged union for all the user events that can be send through the
/// applications event loop.
///
/// _R_ is the type function for the `Renderer` to use. The parameter boolean
/// will be set to the _fullscreen_ value at compile time. The corresponding
/// `Renderer` type is accessible through the generated type of this function.
///
/// _fullscreen_ will be used to configure the `App` and the `Renderer` to
/// respect the corresponding configuration whether to render a fullscreen tui
/// or an inline tui.
///
/// # Example
///
/// Create an `App` which renders using the `PlainRenderer` in fullscreen with
@@ -32,41 +25,35 @@ const log = std.log.scoped(.app);
/// const zterm = @import("zterm");
/// const App = zterm.App(
/// union(enum) {},
/// zterm.Renderer.Direct,
/// true,
/// );
/// // later on use
/// // later on create an `App` instance and start the event loop
/// var app: App = .{};
/// var renderer: App.Renderer = .{};
/// try app.start(null);
/// defer app.stop() catch unreachable;
/// ```
pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) type {
pub fn App(comptime E: type) type {
if (!isTaggedUnion(E)) {
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
}
return struct {
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Renderer = R(fullscreen);
pub const Layout = @import("layout.zig").Layout(Event, Renderer);
pub const Widget = @import("widget.zig").Widget(Event, Renderer);
pub const View = @import("view.zig").View(Event, Renderer);
pub const Container = @import("container.zig").Container(Event);
queue: Queue(Event, 256) = .{},
thread: ?std.Thread = null,
quit_event: std.Thread.ResetEvent = .{},
termios: ?std.posix.termios = null,
attached_handler: bool = false,
min_size: ?terminal.Size = null,
prev_size: terminal.Size = .{ .cols = 0, .rows = 0 },
min_size: ?Size = null,
prev_size: Size = .{ .cols = 0, .rows = 0 },
pub const SignalHandler = struct {
context: *anyopaque,
callback: *const fn (context: *anyopaque) void,
};
pub fn start(this: *@This(), min_size: ?terminal.Size) !void {
if (fullscreen) { // a minimal size only really makes sense if the application is rendered fullscreen
this.min_size = min_size;
}
pub fn start(this: *@This(), min_size: ?Size) !void {
this.min_size = min_size;
if (this.thread) |_| return;
if (!this.attached_handler) {
@@ -92,19 +79,17 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
if (this.termios) |_| {} else {
this.termios = termios;
}
if (fullscreen) {
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
}
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
// post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init);
}
pub fn interrupt(this: *@This()) !void {
this.quit_event.set();
if (fullscreen) {
try terminal.exitAltScreen();
try terminal.restoreScreen();
}
try terminal.exitAltScreen();
try terminal.restoreScreen();
if (this.thread) |thread| {
thread.join();
this.thread = null;
@@ -115,11 +100,9 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
try this.interrupt();
if (this.termios) |*termios| {
try terminal.disableRawMode(termios);
if (fullscreen) {
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.restoreScreen();
}
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.restoreScreen();
}
this.termios = null;
}
@@ -322,7 +305,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
// TODO: only post the event if the size has changed?
// because there might be too many resize events (which force a re-draw of the entire screen)
const size: terminal.Size = .{
const size: Size = .{
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
};

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const Style = @import("Style.zig");
const Style = @import("style.zig");
pub const Cell = @This();

View File

@@ -0,0 +1,174 @@
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Size = @import("size.zig");
const log = std.log.scoped(.container);
/// Border configuration struct
pub const Border = struct {
/// Color to use for the border
color: Color = .default,
/// Configure the corner type to be used for the border
corners: enum(u1) {
squared,
rounded,
} = .squared,
/// Configure the sides where the borders shall be rendered
sides: packed struct {
top: bool = true,
bottom: bool = true,
left: bool = true,
right: bool = true,
} = .{},
/// Configure separator borders between child element to added to the layout
separator: struct {
enabled: bool = false,
color: Color = .default,
line: enum {
line,
dotted,
// TODO: add more variations which could be used for the separator
} = .line,
} = .{},
};
/// Rectangle configuration struct
pub const Rectangle = struct {
/// `Color` to use to fill the `Rectangle` with
/// NOTE: used as background color when rendering! such that it renders the
/// children accordingly without removing the coloring of the `Rectangle`
fill: Color = .default,
/// Configure the corners of the `Rectangle`
corners: enum(u1) {
squared,
rounded,
} = .squared,
};
/// Scroll configuration struct
pub const Scroll = packed struct {
/// Enable horizontal scrolling for this element
horizontal: bool = false,
/// Enable vertical scrolling for this element
vertical: bool = false,
};
/// Layout configuration struct
pub const Layout = struct {
/// control the direction in which child elements are laid out
direction: enum(u1) { horizontal, vertical } = .horizontal,
/// Padding outside of the child elements
padding: packed struct {
top: u16 = 0,
bottom: u16 = 0,
left: u16 = 0,
right: u16 = 0,
/// Create a padding with equivalent padding in all four directions.
pub fn all(padding: u16) @This() {
return .{ .top = padding, .bottom = padding, .left = padding, .right = padding };
}
/// Create a padding with equivalent padding in the left and right directions; others directions remain the default value.
pub fn horizontal(padding: u16) @This() {
return .{ .left = padding, .right = padding };
}
/// Create a padding with equivalent padding in the top and bottom directions; others directions remain the default value.
pub fn vertical(padding: u16) @This() {
return .{ .top = padding, .bottom = padding };
}
} = .{},
/// Padding used in between child elements as gaps when laid out
gap: u16 = 0,
// TODO: is there a way to make x / y type copied by the compiler at comptime instead? such that this only has to be defined once?
/// Alignment of where the child elements are positioned relative to the parent container when laid out
alignment: packed struct {
x: enum(u2) { center, left, right } = .center,
y: enum(u2) { center, left, right } = .center,
} = .{},
// TODO: is there a way to make width / height type copied by the compiler at comptime instead? such that this only has to be defined once?
// NOTE: `sizing` cannot be *packed* because of the tagged unions? is this necessary -> I would need to measure the size differences
/// Sizing to be used for the width and height of this element to use
sizing: struct {
width: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit,
height: union(enum) { fit, grow, fixed: u16, percent: u16 } = .fit,
} = .{},
};
pub fn Container(comptime Event: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Container(comptime Event: type)`");
}
return struct {
size: Size,
properties: Properties,
elements: std.ArrayList(@This()),
/// Properties for each `Container` to configure their layout,
/// border, styling, etc. For details see the corresponding individual
/// documentation of the members of this struct accordingly.
pub const Properties = struct {
border: Border = .{},
rectangle: Rectangle = .{},
scroll: Scroll = .{},
layout: Layout = .{},
};
pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() {
return .{
.size = .{ .cols = 0, .rows = 0 },
.properties = properties,
.elements = std.ArrayList(@This()).init(allocator),
};
}
pub fn deinit(this: *@This()) void {
for (this.elements.items) |*element| {
element.deinit();
}
this.elements.deinit();
}
pub fn append(this: *@This(), element: @This()) !void {
try this.elements.append(element);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
.init => log.debug(".init event", .{}),
.resize => |size| {
this.size = size;
for (this.elements.items) |*element| {
const element_size: Size = size;
// TODO; adjust size according to the layout of the `Container`
if (element.handle(.{ .resize = element_size })) |e| {
_ = e;
}
}
return null;
},
else => {},
}
for (this.elements.items) |*element| {
if (element.handle(event)) |e| {
// TODO: if only the top level container returns a single
// event (i.e. as a reaction to a certain other event) what
// should happen to potential other events?
_ = e;
}
}
return null;
}
pub fn contents(this: *const @This()) []const Cell {
// TODO: use the size and the corresponding contents to determine what should be show in form of a `Cell` array
_ = this;
return &[0]Cell{};
}
};
}

View File

@@ -6,9 +6,10 @@ const terminal = @import("terminal.zig");
const Size = @import("size.zig");
const Key = @import("key.zig");
// System events available to every application.
/// System events available to every `zterm.App`
pub const SystemEvent = union(enum) {
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
/// TODO: not sure if this is necessary or if there is an actual usecase for this - for now it will remain
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,

View File

@@ -10,109 +10,111 @@
const std = @import("std");
const terminal = @import("terminal.zig");
const Cell = terminal.Cell;
const Position = terminal.Position;
const Size = terminal.Size;
const Cell = @import("cell.zig");
const Size = @import("size.zig");
const Position = Size.Position;
/// Double-buffered intermediate rendering pipeline
pub fn Buffered(comptime fullscreen: bool) type {
pub const Buffered = struct {
const log = std.log.scoped(.renderer_buffered);
// _ = log;
_ = fullscreen;
return struct {
allocator: std.mem.Allocator,
created: bool,
size: Size,
screen: []Cell,
virtual_screen: []Cell,
allocator: std.mem.Allocator,
created: bool,
size: Size,
screen: []Cell,
virtual_screen: []Cell,
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.allocator = allocator,
.created = false,
.size = undefined,
.screen = undefined,
.virtual_screen = undefined,
};
pub fn init(allocator: std.mem.Allocator) @This() {
return .{
.allocator = allocator,
.created = false,
.size = undefined,
.screen = undefined,
.virtual_screen = undefined,
};
}
pub fn deinit(this: *@This()) void {
if (this.created) {
this.allocator.free(this.screen);
this.allocator.free(this.virtual_screen);
}
}
pub fn deinit(this: *@This()) void {
if (this.created) {
this.allocator.free(this.screen);
this.allocator.free(this.virtual_screen);
pub fn resize(this: *@This(), size: Size) !void {
this.size = size;
if (!this.created) {
this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory.");
@memset(this.screen, Cell{});
this.virtual_screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, Cell{});
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();
}
}
}
pub fn resize(this: *@This(), size: Size) void {
this.size = size;
if (!this.created) {
this.screen = this.allocator.alloc(Cell, size.cols * size.rows) catch @panic("render.zig: Out of memory.");
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.");
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) void {
const cells: []const Cell = container.contents();
const size: Size = container.size;
log.debug("renderer:render: cells: {any} @ {any}", .{ cells, size });
if (cells.len == 0) return;
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;
}
}
}
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();
}
/// 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 s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.rows) |row| {
for (0..this.size.cols) |col| {
const idx = (row * this.size.cols) + col;
const cs = s[idx];
const cvs = vs[idx];
if (cs.eql(cvs))
continue;
// render differences found in virtual screen
// TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..))
try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];
}
}
/// 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 s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.rows) |row| {
for (0..this.size.cols) |col| {
const idx = (row * this.size.cols) + col;
const cs = s[idx];
const cvs = vs[idx];
if (cs.eql(cvs))
continue;
// render differences found in virtual screen
// TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..))
try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];
}
}
}
};
}
}
};

View File

@@ -1,8 +1,8 @@
pub const Size = @This();
pub const Position = struct {
col: u16,
row: u16,
col: u16 = 0,
row: u16 = 0,
};
anchor: Position = .{},

View File

@@ -1,5 +1,5 @@
const std = @import("std");
const code_point = @import("code_point");
pub const code_point = @import("code_point");
const Key = @import("key.zig");
const Size = @import("size.zig");