20 Commits

Author SHA1 Message Date
d0453d08b8 fix: render cursor correctly in case the same character remains the cursor position
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 47s
2025-06-11 20:32:35 +02:00
825fb63bc8 ref: rename gpa usages to the aliased DebugAllocator 2025-06-11 20:31:41 +02:00
76f708d9d7 add(continous): example which provides a fixed render schedule
The other examples did rendering based on events, which this renderer
does not. This makes these applications potentially not that efficient,
but allows for consistent frame times that make animations, etc.
possible. This example serves to show that you can use `zterm` for both
types of render scheduling and even change between them without much
efford.
2025-05-30 23:02:56 +02:00
0d2644f476 mod(input): handle remaining key codes remove unused ones 2025-05-30 23:01:48 +02:00
9a818117d7 feat(panic): panic handler to recover termios when crashing 2025-05-30 23:00:45 +02:00
5ba5b2b372 feat(element/alignment): alignment Element implementation
You can now align a `Container` using the Alignment `Element` similar to
how you make a `Container` scrollable. For usage details please see the
example and the corresponding tests.
2025-05-28 14:42:02 +02:00
3cb0d11e71 add: necessary assert statement; rem: unnecessary render resize in testing 2025-05-28 14:40:25 +02:00
4cc749facc feat(app): read input options correctly 2025-05-28 14:39:27 +02:00
c6d8eec287 feat(debug): render debug support 2025-05-26 15:43:47 +02:00
2572b57697 refactor: remove unnecessary comptime keyword 2025-05-26 15:15:30 +02:00
80a36a9947 refactor: zigify imports and usages 2025-05-26 14:23:18 +02:00
4cde0640c8 fix(element/scrollable): adjust anchor for scrollable element during resize 2025-05-22 23:39:05 +02:00
e9a9c2b680 feat(app): signal WINCH for .resize system event
This allows the application to automatically re-draw and resize if the
application receives the signal by the terminal emulator.
2025-05-21 22:49:08 +02:00
ba25e6056c feat(element/scrollable): scrollbar rendering
Configuration to enable scrollbar rendering for scrollable `Element`s.
Currently only the fg `Color` of the scrollbar can be configured while
the background uses the same fg `Color` but adds the emphasis `.dim` to
make it obvious what the is the actual scrollbar. In the future it might
be necessary to provide the user with more options to configure the
representation of the scrollbar.

Tests have been added to test the scrollbar rendering and placement
accordingly.
2025-05-21 18:20:52 +02:00
aa4adf20f9 refactor: zigify imports and correct minor mistakes 2025-05-20 18:23:44 +02:00
50adf32f14 add(style): cursor style to indicate a cursor position
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 25s
2025-04-20 20:54:30 +02:00
a4293ff243 add(event): mouse event has relative position for receiving elements
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled
2025-04-20 20:53:46 +02:00
50450f3bbc fix(container): growth resize for size all size options and starting sizes (i.e. the smallest being a non grow-able one)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m14s
2025-04-07 20:55:21 +02:00
bce134f052 rem: unnecessary signal handler
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 2m1s
No event-based re-sizing, instead each re-render resizes by default.
2025-04-01 21:53:52 +02:00
962a384ecf add(Scrollable): init function with corresponding usages
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 42s
2025-03-29 21:21:23 +01:00
37 changed files with 1605 additions and 636 deletions

177
build.zig
View File

@@ -1,5 +1,3 @@
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
@@ -7,7 +5,9 @@ pub fn build(b: *std.Build) void {
const Examples = enum {
all,
demo,
continous,
// elements:
alignment,
button,
input,
scrollable,
@@ -24,9 +24,13 @@ pub fn build(b: *std.Build) void {
};
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
const debug_rendering = b.option(bool, "debug", "Enable debug rendering. Highlight origin's, size's, padding's, gap's, etc. (default: false)") orelse false;
// NOTE do not support debug rendering in release builds
if (debug_rendering == true and optimize != .Debug) @panic("Cannot enable debug rendering in non-debug builds.");
const options = b.addOptions();
options.addOption(Examples, "example", example);
options.addOption(bool, "debug", debug_rendering);
const options_module = options.createModule();
// dependencies
const zg = b.dependency("zg", .{
@@ -36,163 +40,62 @@ pub fn build(b: *std.Build) void {
// library
const lib = b.addModule("zterm", .{
.root_source_file = b.path("src/zterm.zig"),
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib.addImport("code_point", zg.module("code_point"));
lib.addImport("build_options", options_module);
//--- Examples ---
// demo:
const examples = std.meta.fields(Examples);
inline for (examples) |e| {
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
const demo = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("examples/demo.zig"),
.name = e.name,
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig",
.continous => "examples/continous.zig",
// elements:
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.scrollable => "examples/elements/scrollable.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig",
.grid => "examples/layouts/grid.zig",
.mixed => "examples/layouts/mixed.zig",
// styles:
.text => "examples/styles/text.zig",
.palette => "examples/styles/palette.zig",
// error handling
.errors => "examples/errors.zig",
.all => unreachable, // should never happen
}),
.target = target,
.optimize = optimize,
});
// import dependencies
demo.root_module.addImport("zterm", lib);
// elements:
const button = b.addExecutable(.{
.name = "button",
.root_source_file = b.path("examples/elements/button.zig"),
.target = target,
.optimize = optimize,
});
button.root_module.addImport("zterm", lib);
const input = b.addExecutable(.{
.name = "input",
.root_source_file = b.path("examples/elements/input.zig"),
.target = target,
.optimize = optimize,
});
input.root_module.addImport("zterm", lib);
const scrollable = b.addExecutable(.{
.name = "scrollable",
.root_source_file = b.path("examples/elements/scrollable.zig"),
.target = target,
.optimize = optimize,
});
scrollable.root_module.addImport("zterm", lib);
// layouts:
const vertical = b.addExecutable(.{
.name = "vertical",
.root_source_file = b.path("examples/layouts/vertical.zig"),
.target = target,
.optimize = optimize,
});
vertical.root_module.addImport("zterm", lib);
const horizontal = b.addExecutable(.{
.name = "horizontal",
.root_source_file = b.path("examples/layouts/horizontal.zig"),
.target = target,
.optimize = optimize,
});
horizontal.root_module.addImport("zterm", lib);
const grid = b.addExecutable(.{
.name = "grid",
.root_source_file = b.path("examples/layouts/grid.zig"),
.target = target,
.optimize = optimize,
});
grid.root_module.addImport("zterm", lib);
const mixed = b.addExecutable(.{
.name = "mixed",
.root_source_file = b.path("examples/layouts/mixed.zig"),
.target = target,
.optimize = optimize,
});
mixed.root_module.addImport("zterm", lib);
// styles:
const palette = b.addExecutable(.{
.name = "palette",
.root_source_file = b.path("examples/styles/palette.zig"),
.target = target,
.optimize = optimize,
});
palette.root_module.addImport("zterm", lib);
const text = b.addExecutable(.{
.name = "text",
.root_source_file = b.path("examples/styles/text.zig"),
.target = target,
.optimize = optimize,
});
text.root_module.addImport("zterm", lib);
// error handling:
const errors = b.addExecutable(.{
.name = "errors",
.root_source_file = b.path("examples/errors.zig"),
.target = target,
.optimize = optimize,
});
errors.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step
const exe = switch (example) {
.demo => demo,
// elements:
.button => button,
.input => input,
.scrollable => scrollable,
// layouts:
.vertical => vertical,
.horizontal => horizontal,
.grid => grid,
.mixed => mixed,
// styles:
.text => text,
.palette => palette,
// error handling:
.errors => errors,
else => blk: {
b.installArtifact(button);
b.installArtifact(input);
b.installArtifact(scrollable);
b.installArtifact(vertical);
b.installArtifact(horizontal);
b.installArtifact(grid);
b.installArtifact(mixed);
b.installArtifact(text);
b.installArtifact(palette);
b.installArtifact(errors);
break :blk demo;
},
};
b.installArtifact(exe);
// zig build run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| run_cmd.addArgs(args);
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build run`
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
}
// zig build test
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/zterm.zig"),
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
lib_unit_tests.root_module.addImport("build_options", options_module);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
}
const std = @import("std");

View File

@@ -24,7 +24,7 @@
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.2.0",
.version = "0.3.0",
// Tracks the earliest Zig version that the package considers to be a
// supported use case.

249
examples/continous.zig Normal file
View File

@@ -0,0 +1,249 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
/// Spinner element implementation that runs a simple animation that requires
/// the continous draw loop.
const Spinner = struct {
counter: u8 = 0,
index: u8 = 0,
const map: [6]u21 = .{ '𜺎', '𜺍', '𜺇', '𜹯', '𜹿', '𜺋' };
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.content = content,
},
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
cells[size.x + 1].cp = map[this.index];
this.counter += 1;
if (this.counter >= 20) {
this.index += 1;
this.index %= 6;
this.counter = 0;
}
}
};
const InputField = struct {
input: std.ArrayList(u21),
queue: *App.Queue,
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{
.input = .init(allocator),
.queue = queue,
};
}
pub fn deinit(this: @This()) void {
this.input.deinit();
}
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isAscii()) try this.input.append(key.cp);
// TODO support arrow keys for navigation?
// TODO support readline keybindings (i.e. ctrl-k, ctrl-u, ctrl-b, ctrl-f, etc. and the equivalent alt bindings)
// create an own `Element` implementation from this
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
if (key.eql(.{ .cp = zterm.input.Backspace }))
_ = this.input.pop();
if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
_ = this.input.pop();
},
else => {},
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.input.items.len == 0) return;
const row = 1;
const col = 1;
const anchor = (row * size.x) + col;
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].style.cursor = false;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var input_field: InputField = .init(allocator, &app.queue);
defer input_field.deinit();
var quit_text: QuitText = .{};
var spinner: Spinner = .{};
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{
.direction = .vertical,
.padding = .all(5),
},
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 10 },
},
}, input_field.element()));
const nested_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
}, spinner.element());
try container.append(nested_container);
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var framerate: u64 = 60;
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
var next_frame_ms: u64 = 0;
// draw loop
draw: while (true) {
const now_ms: u64 = @intCast(time.milliTimestamp());
if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms;
} else {
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms;
}
const len = blk: {
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.drain() orelse break;
log.debug("handling event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
log.debug("key {any}", .{key});
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
},
.accept => |input| {
defer allocator.free(input);
var string = try allocator.alloc(u8, input.len);
defer allocator.free(string);
for (0.., input) |i, char| string[i] = @intCast(char);
log.debug("Accepted input '{s}'", .{string});
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
.focus => |b| {
// NOTE reduce framerate in case the window is not focused and restore again when focused
framerate = if (b) 60 else 15;
tick_ms = @divFloor(time.ms_per_s, framerate);
},
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break :draw,
else => {},
}
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const time = std.time;
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
accept: []u21,
});

View File

@@ -1,11 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
@@ -15,7 +7,7 @@ const QuitText = struct {
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const y = 2;
const x = size.x / 2 -| (text.len / 2);
@@ -36,7 +28,7 @@ pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -75,7 +67,7 @@ pub fn main() !void {
}, .{}));
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box, .disabled);
var container = try App.Container.init(allocator, .{
.layout = .{
@@ -87,15 +79,54 @@ pub fn main() !void {
}, quit_text.element());
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try container.append(try App.Container.init(allocator, .{
var nested_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
.enabled = true,
},
},
}, .{});
var inner_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
},
.border = .{
.color = .light_blue,
.sides = .all,
},
}, .{});
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .blue,
},
.size = .{
.dim = .{ .x = 100 },
.grow = .horizontal,
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .red,
},
.size = .{
.grow = .horizontal,
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .green,
},
}, .{}));
try nested_container.append(inner_container);
try nested_container.append(try .init(allocator, .{
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
},
}, .{}));
try container.append(nested_container);
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
@@ -119,6 +150,7 @@ pub fn main() !void {
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
@@ -149,10 +181,18 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});

View File

@@ -0,0 +1,99 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var container: App.Container = try .init(allocator, .{}, .{});
defer container.deinit();
var quit_text: QuitText = .{};
const quit_container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.layout = .{
.direction = .vertical,
.padding = .all(5),
},
.size = .{
.dim = .{ .x = 25, .y = 5 },
.grow = .fixed,
},
}, quit_text.element());
var alignment: App.Alignment = .init(quit_container, .center);
try container.append(try .init(allocator, .{}, alignment.element()));
try app.start();
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)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,12 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
click: [:0]const u8,
});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -16,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -66,7 +57,7 @@ const Clickable = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = size.y / 2 -| (text.len / 2);
const col = size.x / 2 -| (text.len / 2);
@@ -86,7 +77,7 @@ const Clickable = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -139,10 +130,19 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
click: [:0]const u8,
});

View File

@@ -1,12 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
accept: []u21,
});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -16,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -76,7 +67,7 @@ const InputField = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.input.items.len == 0) return;
@@ -86,10 +77,46 @@ const InputField = struct {
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].style.cursor = false;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
@@ -97,7 +124,7 @@ const InputField = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -109,17 +136,47 @@ pub fn main() !void {
var input_field: InputField = .init(allocator, &app.queue);
defer input_field.deinit();
var mouse_draw: MouseDraw = .{};
var second_mouse_draw: MouseDraw = .{};
var quit_text: QuitText = .{};
const element = input_field.element();
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{ .padding = .all(5) },
.layout = .{
.direction = .vertical,
.padding = .all(5),
},
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 10 },
},
}, input_field.element()));
var nested_container: App.Container = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .black,
},
.rectangle = .{ .fill = .light_grey },
.layout = .{
.separator = .{
.enabled = true,
.color = .black,
},
},
}, .{});
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
}, mouse_draw.element()));
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
}, second_mouse_draw.element()));
try container.append(nested_container);
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
@@ -134,7 +191,10 @@ pub fn main() !void {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => |input| {
defer allocator.free(input);
log.info("Accepted input {any}", .{input});
var string = try allocator.alloc(u8, input.len);
defer allocator.free(string);
for (0.., input) |i, char| string[i] = @intCast(char);
log.debug("Accepted input '{s}'", .{string});
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
@@ -153,10 +213,19 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
accept: []u21,
});

View File

@@ -1,11 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -15,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -44,7 +36,7 @@ const HelloWorldText = packed struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = size.y / 2;
const col = size.x / 2 -| (text.len / 2);
@@ -65,7 +57,7 @@ pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
@@ -156,10 +148,10 @@ pub fn main() !void {
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .{ .container = top_box };
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.grey));
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white));
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start();
@@ -190,10 +182,18 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -14,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -40,7 +33,7 @@ const InfoText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -75,7 +68,7 @@ const ErrorNotification = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.msg) |msg| {
const row = size.y -| 2;
@@ -99,7 +92,7 @@ const ErrorNotification = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -150,10 +143,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -17,7 +10,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -37,7 +30,7 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -106,10 +99,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -17,7 +10,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -37,7 +30,7 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -98,10 +91,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -17,7 +10,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -37,7 +30,7 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -114,10 +107,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -17,7 +10,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -37,7 +30,7 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -97,10 +90,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -14,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -34,7 +27,7 @@ const QuitText = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -60,10 +53,10 @@ pub fn main() !void {
defer box.deinit();
inline for (std.meta.fields(zterm.Color)) |field| {
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
}
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box, .disabled);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
@@ -93,10 +86,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,10 +1,3 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
@@ -14,7 +7,7 @@ const QuitText = struct {
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
@@ -39,20 +32,20 @@ const TextStyles = struct {
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
@setEvalBranchQuota(50000);
@setEvalBranchQuota(10000);
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
var row: usize = 0;
var col: usize = 0;
// Color
inline for (std.meta.fields(zterm.Color)) |bg_field| {
if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
inline for (std.meta.fields(zterm.Color)) |fg_field| {
if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (comptime fg_field.value == bg_field.value) continue;
if (fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (fg_field.value == bg_field.value) continue;
// witouth any emphasis
for (text) |cp| {
@@ -64,7 +57,7 @@ const TextStyles = struct {
// emphasis (no combinations)
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
if (emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
for (text) |cp| {
@@ -85,7 +78,7 @@ const TextStyles = struct {
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
@@ -118,7 +111,7 @@ pub fn main() !void {
}, text_styles.element());
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
var scrollable: App.Scrollable = .init(box, .disabled);
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
@@ -148,10 +141,17 @@ pub fn main() !void {
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,19 +1,4 @@
//! Application type for TUI-applications
const std = @import("std");
const code_point = @import("code_point");
const event = @import("event.zig");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Mouse = input.Mouse;
const Key = input.Key;
const Point = @import("point.zig").Point;
const log = std.log.scoped(.app);
/// Create the App Type with the associated user events _E_ which describes
/// an tagged union for all the user events that can be send through the
@@ -39,57 +24,48 @@ pub fn App(comptime E: type) type {
@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 Container = @import("container.zig").Container(Event);
const element = @import("element.zig");
pub const Element = element.Element(Event);
pub const Scrollable = element.Scrollable(Event);
pub const Queue = queue.Queue(Event, 256);
queue: Queue,
thread: ?std.Thread,
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
attached_handler: bool = false,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
termios: ?posix.termios = null,
winch_registered: bool = false,
pub const SignalHandler = struct {
context: *anyopaque,
callback: *const fn (context: *anyopaque) void,
};
// global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined;
/// registered WINCH handler to report resize events
fn handleWinch(_: c_int) callconv(.C) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize);
}
pub const init: @This() = .{
.queue = .{},
.thread = null,
.quit_event = .{},
.termios = null,
.attached_handler = false,
};
pub fn start(this: *@This()) !void {
if (this.thread) |_| return;
if (!this.attached_handler) {
var winch_act = std.posix.Sigaction{
.handler = .{ .handler = @This().handleWinch },
.mask = std.posix.empty_sigset,
.flags = 0,
};
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
try registerWinch(.{
.context = this,
.callback = @This().winsizeCallback,
});
this.attached_handler = true;
// post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init);
if (!this.winch_registered) {
handler_ctx = this;
var act = posix.Sigaction{
.handler = .{ .handler = handleWinch },
.mask = posix.empty_sigset,
.flags = 0,
};
posix.sigaction(posix.SIG.WINCH, &act, null);
this.winch_registered = true;
}
this.quit_event.reset();
this.thread = try std.Thread.spawn(.{}, @This().run, .{this});
this.thread = try Thread.spawn(.{}, @This().run, .{this});
var termios: std.posix.termios = undefined;
var termios: posix.termios = undefined;
try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else this.termios = termios;
@@ -139,27 +115,6 @@ pub fn App(comptime E: type) type {
this.queue.push(e);
}
fn winsizeCallback(ptr: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ptr));
_ = this;
// this.postEvent(.{ .size = terminal.getTerminalSize() });
}
var winch_handler: ?SignalHandler = null;
fn registerWinch(handler: SignalHandler) !void {
if (winch_handler) |_| {
@panic("Cannot register another WINCH handler.");
}
winch_handler = handler;
}
fn handleWinch(_: c_int) callconv(.C) void {
if (winch_handler) |handler| {
handler.callback(handler.context);
}
}
fn run(this: *@This()) !void {
// thread to read user inputs
var buf: [256]u8 = undefined;
@@ -208,6 +163,9 @@ pub fn App(comptime E: type) type {
// Legacy keys
// CSI {ABCDEFHPQS}
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
_ = field_iter.next(); // skip first field
const key: Key = .{
.cp = switch (final) {
'A' => input.Up,
@@ -221,22 +179,36 @@ pub fn App(comptime E: type) type {
'Q' => input.F2,
'R' => input.F3,
'S' => input.F4,
else => unreachable, // switch case prevents in this case form ever happening
else => unreachable,
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
},
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => {
// Legacy keys
// CSI number ~
// CSI number ; modifier ~
// CSI number ; modifier:event_type ; text_as_codepoint ~
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const key: Key = .{
.cp = switch (number) {
.cp = blk: {
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
break :blk switch (number) {
2 => input.Insert,
3 => input.Delete,
5 => input.PageUp,
@@ -255,10 +227,31 @@ pub fn App(comptime E: type) type {
21 => input.F10,
23 => input.F11,
24 => input.F12,
25 => input.F13,
26 => input.F14,
28 => input.F15,
29 => input.F16,
31 => input.F17,
32 => input.F18,
33 => input.F19,
34 => input.F20,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57427 => input.KpBegin,
57399...57454 => |code| code,
else => unreachable,
};
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
@@ -267,14 +260,14 @@ pub fn App(comptime E: type) type {
'I' => this.postEvent(.{ .focus = true }),
'O' => this.postEvent(.{ .focus = false }),
'M', 'm' => {
std.debug.assert(sequence.len >= 4);
assert(sequence.len >= 4);
if (sequence[2] != '<') break;
const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = std.fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const mouse_bits = packed struct {
const motion: u8 = 0b00100000;
@@ -314,14 +307,14 @@ pub fn App(comptime E: type) type {
// Device Status Report
// CSI Ps n
// CSI ? Ps n
std.debug.assert(sequence.len >= 3);
assert(sequence.len >= 3);
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (std.mem.eql(u8, "48", ps)) {
if (mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const width_char = iter.next() orelse break;
@@ -329,9 +322,10 @@ pub fn App(comptime E: type) type {
_ = width_char;
_ = height_char;
this.postEvent(.resize);
// this.postEvent(.{ .size = .{
// .x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
// } });
}
},
@@ -347,9 +341,29 @@ pub fn App(comptime E: type) type {
else => {},
}
},
0x50 => {
// DCS
},
0x58 => {
// SOS
},
0x5D => {
// OSC
},
// TODO parse corresponding codes
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
else => {},
0x5F => {
// APC
// parse for kitty graphics capabilities
},
else => {
// alt + <char> keypress
this.postEvent(.{
.key = .{
.cp = buf[1],
.mod = .{ .alt = true },
},
});
},
}
} else {
const b = buf[0];
@@ -357,10 +371,11 @@ pub fn App(comptime E: type) type {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a, 0x0d => .{ .cp = input.Enter },
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
std.debug.assert(read_bytes == 1);
assert(read_bytes == 1);
break :escape .{ .cp = input.Escape };
},
0x7f => .{ .cp = input.Backspace },
@@ -377,5 +392,52 @@ pub fn App(comptime E: type) type {
break;
}
}
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {};
terminal.exitAltScreen() catch {};
terminal.showCursor() catch {};
var termios: posix.termios = .{
.iflag = .{},
.lflag = .{},
.cflag = .{},
.oflag = .{},
.cc = undefined,
.line = 0,
.ispeed = undefined,
.ospeed = undefined,
};
terminal.disableRawMode(&termios) catch {};
terminal.restoreScreen() catch {};
std.debug.defaultPanic(msg, ret_addr);
}
const element = @import("element.zig");
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Event);
pub const Element = element.Element(Event);
pub const Alignment = element.Alignment(Event);
pub const Scrollable = element.Scrollable(Event);
pub const Exec = element.Exec(Event, Queue);
pub const Queue = queue.Queue(Event, 256);
};
}
const log = std.log.scoped(.app);
const std = @import("std");
const mem = std.mem;
const fmt = std.fmt;
const posix = std.posix;
const Thread = std.Thread;
const assert = std.debug.assert;
const code_point = @import("code_point");
const event = @import("event.zig");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Mouse = input.Mouse;
const Key = input.Key;
const Point = @import("point.zig").Point;

View File

@@ -1,11 +1,8 @@
const std = @import("std");
const Style = @import("style.zig");
//! Cell type containing content and formatting for each character in the terminal screen.
pub const Cell = @This();
style: Style = .{ .emphasis = &.{} },
// TODO embrace `zg` dependency more due to utf-8 encoding
cp: u21 = ' ',
style: Style = .{ .emphasis = &.{} },
pub fn eql(this: Cell, other: Cell) bool {
return this.cp == other.cp and this.style.eql(other.style);
@@ -20,6 +17,10 @@ pub fn value(this: Cell, writer: anytype) !void {
try this.style.value(writer, this.cp);
}
const std = @import("std");
const Style = @import("style.zig");
const Cell = @This();
test "ascii styled text" {
const cells: [4]Cell = .{
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },

View File

@@ -1,5 +1,3 @@
const std = @import("std");
pub const Color = enum(u8) {
default = 0,
black = 16,
@@ -20,19 +18,24 @@ pub const Color = enum(u8) {
white,
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
// TODO might be useful to use the std.ascii stuff!
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) {
switch (coloring) {
.fg => try std.fmt.format(writer, "39", .{}),
.bg => try std.fmt.format(writer, "49", .{}),
.ul => try std.fmt.format(writer, "59", .{}),
.fg => try format(writer, "39", .{}),
.bg => try format(writer, "49", .{}),
.ul => try format(writer, "59", .{}),
}
} else {
switch (coloring) {
.fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
.bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
.ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
.fg => try format(writer, "38;5;{d}", .{@intFromEnum(this)}),
.bg => try format(writer, "48;5;{d}", .{@intFromEnum(this)}),
.ul => try format(writer, "58;5;{d}", .{@intFromEnum(this)}),
}
}
}
};
const std = @import("std");
const format = std.fmt.format;

View File

@@ -1,21 +1,5 @@
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Point = @import("point.zig").Point;
const Style = @import("style.zig");
const Error = @import("error.zig").Error;
const log = std.log.scoped(.container);
/// Border configuration struct
pub const Border = packed struct {
// corners:
const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
/// Color to use for the border
color: Color = .default,
/// Configure the corner type to be used for the border
@@ -39,13 +23,12 @@ pub const Border = packed struct {
} = .{},
pub fn content(this: @This(), cells: []Cell, size: Point) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const frame = switch (this.corners) {
.rounded => Border.rounded_border,
.squared => Border.squared_border,
const frame: [6]u21 = switch (this.corners) {
.rounded => .{ '╭', '─', '╮', '│', '╰', '╯' },
.squared => .{ '┌', '─', '┐', '│', '└', '┘' },
};
std.debug.assert(frame.len == 6);
// render top and bottom border
if (this.sides.top or this.sides.bottom) {
@@ -155,13 +138,20 @@ pub const Rectangle = packed struct {
// NOTE caller owns `Cells` slice and ensures that `cells.len == size.x * size.y`
pub fn content(this: @This(), cells: []Cell, size: Point) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
for (0..size.y) |row| {
for (0..size.x) |col| {
cells[(row * size.x) + col].style.bg = this.fill;
}
}
// DEBUG render corresponding beginning of the rectangle for this `Container` *red*
if (comptime build_options.debug) {
cells[0].style.fg = .red;
cells[0].style.bg = .black;
cells[0].cp = 'r'; // 'r' for *rectangle*
}
}
test "fill color overwrite parent fill" {
@@ -299,11 +289,6 @@ pub const Rectangle = packed struct {
/// Layout configuration struct
pub const Layout = packed struct {
// separator.line:
const line: [2]u21 = .{ '│', '─' };
const dotted: [2]u21 = .{ '┆', '┄' };
const double: [2]u21 = .{ '║', '═' };
/// control the direction in which child elements are laid out
direction: enum(u1) { horizontal, vertical } = .horizontal,
/// Padding outside of the child elements
@@ -342,13 +327,13 @@ pub const Layout = packed struct {
} = .{},
pub fn content(this: @This(), comptime C: type, cells: []Cell, origin: Point, size: Point, children: []const C) void {
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.separator.enabled and children.len > 1) {
const line_cps: [2]u21 = switch (this.separator.line) {
.line => line,
.dotted => dotted,
.double => double,
.line => .{ '│', '─' },
.dotted => .{ '┆', '┄' },
.double => .{ '║', '═' },
};
const gap: u16 = (this.gap + 1) / 2;
@@ -371,8 +356,11 @@ pub const Layout = packed struct {
}
// DEBUG render corresponding beginning of the separator for this `Container` *red*
// cells[anchor].style.fg = .red;
// cells[anchor].style.bg = .red;
if (comptime build_options.debug) {
cells[anchor].style.fg = .red;
cells[anchor].style.bg = .black;
cells[anchor].cp = 's'; // 's' for *separator*
}
}
}
}
@@ -573,7 +561,7 @@ pub fn Container(comptime Event: type) type {
const Element = @import("element.zig").Element(Event);
return struct {
allocator: std.mem.Allocator,
allocator: Allocator,
origin: Point,
size: Point,
properties: Properties,
@@ -591,7 +579,7 @@ pub fn Container(comptime Event: type) type {
};
pub fn init(
allocator: std.mem.Allocator,
allocator: Allocator,
properties: Properties,
element: Element,
) !@This() {
@@ -697,13 +685,11 @@ pub fn Container(comptime Event: type) type {
.y = @max(size.y, this.properties.size.dim.y),
},
};
log.debug("fit_size returning: {any}", .{this.size});
return this.size;
}
/// growable implicitly requires the root `Container` to have a set a size property to the size of the available terminal screen
fn grow_resize(this: *@This(), max_size: Point) void {
log.debug("grow_size: {any}", .{this.size});
const layout = this.properties.layout;
var remainder = switch (layout.direction) {
.horizontal => max_size.x -| (layout.padding.left + layout.padding.right),
@@ -787,6 +773,10 @@ pub fn Container(comptime Event: type) type {
for (this.elements.items) |child| {
if (child.properties.size.grow == .fixed) continue;
switch (layout.direction) {
.horizontal => if (child.properties.size.grow == .vertical) continue,
.vertical => if (child.properties.size.grow == .horizontal) continue,
}
const size = switch (layout.direction) {
.horizontal => child.size.x,
@@ -817,9 +807,11 @@ pub fn Container(comptime Event: type) type {
switch (layout.direction) {
.horizontal => if (child.properties.size.grow != .vertical) {
child.size.x += size_to_correct;
remainder -|= size_to_correct;
},
.vertical => if (child.properties.size.grow != .horizontal) {
child.size.y += size_to_correct;
remainder -|= size_to_correct;
},
}
if (overflow > 0) {
@@ -836,7 +828,6 @@ pub fn Container(comptime Event: type) type {
},
}
}
remainder -|= size_to_correct;
}
}
}
@@ -867,7 +858,12 @@ pub fn Container(comptime Event: type) type {
pub fn handle(this: *@This(), event: Event) !void {
switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
try this.element.handle(event);
// the element receives the mouse event with relative position
assert(mouse.x >= this.origin.x and mouse.y >= this.origin.y);
var relative_mouse: input.Mouse = mouse;
relative_mouse.x -= this.origin.x;
relative_mouse.y -= this.origin.y;
try this.element.handle(.{ .mouse = relative_mouse });
for (this.elements.items) |*element| try element.handle(event);
},
else => {
@@ -889,19 +885,43 @@ pub fn Container(comptime Event: type) type {
try this.element.content(cells, this.size);
// DEBUG render corresponding top left corner of this `Container` *red*
// cells[0].style.fg = .red;
// cells[0].style.bg = .red;
// DEBUG render corresponding corners (except top left) of this `Container` *red*
if (comptime build_options.debug) {
// top right
cells[this.size.x -| 1].style.fg = .red;
cells[this.size.x -| 1].style.bg = .black;
cells[this.size.x -| 1].cp = 'c'; // 'c' for *container*
// bottom left
cells[this.size.x * (this.size.y -| 1)].style.fg = .red;
cells[this.size.x * (this.size.y -| 1)].style.bg = .black;
cells[this.size.x * (this.size.y -| 1)].cp = 'c'; // 'c' for *container*
// bottom right
cells[this.size.x * this.size.y -| 1].style.fg = .red;
cells[this.size.x * this.size.y -| 1].style.bg = .black;
cells[this.size.x * this.size.y -| 1].cp = 'c'; // 'c' for *container*
}
return cells;
}
};
}
const log = std.log.scoped(.container);
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const build_options = @import("build_options");
const input = @import("input.zig");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Point = @import("point.zig").Point;
const Style = @import("style.zig");
const Error = @import("error.zig").Error;
test {
_ = Border;
_ = Layout;
_ = Rectangle;
@import("std").testing.refAllDeclsRecursive(@This());
}
test "Container Fixed and Grow Size Vertical" {

View File

@@ -1,11 +1,4 @@
//! Interface for Element's which describe the contents of a `Container`.
const std = @import("std");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;
pub fn Element(Event: type) type {
return struct {
@@ -59,25 +52,57 @@ pub fn Element(Event: type) type {
/// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens.
pub inline fn content(this: @This(), cells: []Cell, size: Point) !void {
if (this.vtable.content) |content_fn|
if (this.vtable.content) |content_fn| {
try content_fn(this.ptr, cells, size);
// DEBUG render corresponding top left corner of this `Element` *red*
// - only rendered if the corresponding associated element renders contents into the `Container`
if (comptime build_options.debug) {
cells[0].style.fg = .red;
cells[0].style.bg = .black;
cells[0].cp = 'e'; // 'e' for *element*
}
}
}
};
}
pub fn Scrollable(Event: type) type {
pub fn Alignment(Event: type) type {
return struct {
/// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen.
/// `Size` of the actual contents that should be aligned.
size: Point = .{},
/// `Size` of the `Container` content that is scrollable and mapped to
/// the *size* of the `Scrollable` `Element`.
container_size: Point = .{},
/// Anchor of the viewport of the scrollable `Container`.
anchor: Point = .{},
/// The actual `Container`, that is scrollable.
/// Alignment Configuration to use for aligning the associated contents of this `Element`.
configuration: Configuration,
/// `Container` to render in alignment. It needs to use the sizing options accordingly to be effective.
container: Container(Event),
/// Configuration for Alignment
pub const Configuration = packed struct {
h: Align = .start,
v: Align = .start,
/// Alignment Options for configuration for vertical and horizontal orientations
pub const Align = enum(u2) { start, center, end };
/// Configuration for vertical alignment
pub fn vertical(a: Align) @This() {
return .{ .v = a };
}
/// Configuration for horizontal alignment
pub fn horizontal(a: Align) @This() {
return .{ .h = a };
}
pub const center: @This() = .{ .v = .center, .h = .center };
};
pub fn init(container: Container(Event), configuration: Configuration) @This() {
return .{
.container = container,
.configuration = configuration,
};
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
@@ -93,9 +118,125 @@ pub fn Scrollable(Event: type) type {
fn resize(ctx: *anyopaque, size: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.size = size;
this.container.resize(size);
}
fn reposition(ctx: *anyopaque, anchor: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
var origin = anchor;
origin.x = switch (this.configuration.h) {
.start => origin.x,
.center => origin.x + (this.size.x / 2) -| (this.container.size.x / 2),
.end => this.size.x -| this.container.size.x,
};
origin.y = switch (this.configuration.v) {
.start => origin.y,
.center => origin.y + (this.size.y / 2) -| (this.container.size.y / 2),
.end => this.size.y -| this.container.size.y,
};
this.container.reposition(origin);
}
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
try this.container.handle(event);
}
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const origin = this.container.origin;
const csize = this.container.size;
const container_cells = try this.container.content();
defer this.container.allocator.free(container_cells);
outer: for (0..csize.y) |row| {
inner: for (0..csize.x) |col| {
// do not read/write out of bounce
if (this.size.x < row) break :outer;
if (this.size.y < col) break :inner;
cells[((row + origin.y) * size.x) + col + origin.x] = container_cells[(row * csize.x) + col];
}
}
}
};
}
pub fn Scrollable(Event: type) type {
return struct {
/// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen.
size: Point = .{},
/// `Size` of the `Container` content that is scrollable and mapped to
/// the *size* of the `Scrollable` `Element`.
container_size: Point = .{},
/// Anchor of the viewport of the scrollable `Container`.
anchor: Point = .{},
/// The actual `Container`, that is scrollable.
container: Container(Event),
/// Whether the scrollable contents should show a scroll bar or not.
/// The scroll bar will only be shown if required and enabled. With the
/// corresponding provided color if to be shown.
configuration: Configuration,
pub const Configuration = packed struct {
scrollbar: bool,
color: Color = .default,
x_axis: bool = false,
y_axis: bool = false,
pub const disabled: @This() = .{ .scrollbar = false };
pub fn enabled(color: Color) @This() {
return .{ .scrollbar = true, .color = color };
}
};
pub fn init(container: Container(Event), configuration: Configuration) @This() {
return .{
.container = container,
.configuration = configuration,
};
}
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
const last_max_anchor_x = this.container_size.x -| this.size.x;
const last_max_anchor_y = this.container_size.y -| this.size.y;
this.size = size;
this.container.resize(size);
if (this.configuration.scrollbar) {
if (this.container.properties.size.dim.x > this.size.x or this.container.size.x > this.size.x) this.configuration.y_axis = true;
if (this.container.properties.size.dim.y > this.size.y or this.container.size.y > this.size.y) this.configuration.x_axis = true;
if (this.configuration.x_axis or this.configuration.y_axis) this.container.resize(.{
.x = this.size.x - if (this.configuration.x_axis) @as(u16, 1) else @as(u16, 0),
.y = this.size.y - if (this.configuration.y_axis) @as(u16, 1) else @as(u16, 0),
});
}
const new_max_anchor_x = this.container.size.x -| size.x;
const new_max_anchor_y = this.container.size.y -| size.y;
// correct anchor if necessary
if (new_max_anchor_x < last_max_anchor_x and this.anchor.x > new_max_anchor_x) this.anchor.x = new_max_anchor_x;
if (new_max_anchor_y < last_max_anchor_y and this.anchor.y > new_max_anchor_y) this.anchor.y = new_max_anchor_y;
// TODO scrollbar space - depending on configuration and only if necessary?
this.container.resize(this.size);
this.container_size = this.container.size;
}
@@ -113,8 +254,8 @@ pub fn Scrollable(Event: type) type {
this.anchor.y -|= 1;
},
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
const max_origin_y = this.container_size.y -| this.size.y;
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
const max_anchor_y = this.container_size.y -| this.size.y;
this.anchor.y = @min(this.anchor.y + 1, max_anchor_y);
},
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
this.anchor.x -|= 1;
@@ -159,33 +300,82 @@ pub fn Scrollable(Event: type) type {
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
const offset_x: usize = if (this.configuration.x_axis) 1 else 0;
const offset_y: usize = if (this.configuration.y_axis) 1 else 0;
const container_size = this.container.size;
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.x) * @as(usize, container_size.y));
{
const container_cells_const = try this.container.content();
defer this.container.allocator.free(container_cells_const);
std.debug.assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
@memcpy(container_cells, container_cells_const);
}
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
// TODO render scrollbar according to configuration!
for (0..size.y) |row| {
for (0..size.x) |col| {
for (0..size.y - offset_y) |row| {
for (0..size.x - offset_x) |col| {
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
}
}
if (this.configuration.scrollbar) {
if (this.configuration.x_axis) {
const ratio: f32 = @as(f32, @floatFromInt(this.size.y)) / @as(f32, @floatFromInt(this.container.size.y));
const scrollbar_size: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.y)) * ratio);
const pos_ratio: f32 = @as(f32, @floatFromInt(this.anchor.y)) / @as(f32, @floatFromInt(this.container.size.y));
const scrollbar_starting_pos: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.y)) * pos_ratio);
for (0..size.y) |row| {
cells[(row * size.x) + size.x - 1] = .{
.style = .{
.fg = this.configuration.color,
.emphasis = if (row >= scrollbar_starting_pos and row <= scrollbar_starting_pos + scrollbar_size)
&.{} // scrollbar itself
else
&.{.dim}, // background (around scrollbar)
},
.cp = '🮋', // 7/8 block right
};
}
}
if (this.configuration.y_axis) {
const ratio: f32 = @as(f32, @floatFromInt(this.size.x)) / @as(f32, @floatFromInt(this.container.size.x));
const scrollbar_size: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.x)) * ratio);
const pos_ratio: f32 = @as(f32, @floatFromInt(this.anchor.x)) / @as(f32, @floatFromInt(this.container.size.x));
const scrollbar_starting_pos: usize = @intFromFloat(@as(f32, @floatFromInt(this.size.x)) * pos_ratio);
for (0..size.x) |col| {
cells[((size.y - 1) * size.x) + col] = .{
.style = .{
.fg = this.configuration.color,
.emphasis = if (col >= scrollbar_starting_pos and col <= scrollbar_starting_pos + scrollbar_size)
&.{} // scrollbar itself
else
&.{.dim}, // background (around scrollbar)
},
.cp = '🮄', // 5/8 block top (to make it look more equivalent to the vertical scrollbar)
};
}
}
}
this.container.allocator.free(container_cells);
}
};
}
const std = @import("std");
const assert = std.debug.assert;
const build_options = @import("build_options");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const Color = @import("color.zig").Color;
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;
// TODO nested scrollable `Container`s?'
// TODO reaction only for when the event is actually pushed to the corresponding `Container` rendered container
test "scrollable vertical" {
const event = @import("event.zig");
@@ -222,7 +412,7 @@ test "scrollable vertical" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var scrollable: Scrollable(event.SystemEvent) = .init(box, .disabled);
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -265,6 +455,84 @@ test "scrollable vertical" {
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
}
test "scrollable vertical with scrollbar" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .red,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .vertical,
.padding = .all(1),
},
.size = .{
.dim = .{ .y = size.y + 15 },
},
}, .{});
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .init(box, .enabled(.white));
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.color = .green,
.sides = .vertical,
},
}, scrollable.element());
defer container.deinit();
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
// further scrolling down will not change anything
try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.scrollbar.bottom.zon"), renderer.screen);
}
test "scrollable horizontal" {
const event = @import("event.zig");
const testing = @import("testing.zig");
@@ -300,7 +568,7 @@ test "scrollable horizontal" {
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var scrollable: Scrollable(event.SystemEvent) = .init(box, .disabled);
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
@@ -342,3 +610,213 @@ test "scrollable horizontal" {
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
}
test "scrollable horizontal with scrollbar" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .red,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .horizontal,
.padding = .all(1),
},
.size = .{
.dim = .{ .x = size.x + 15 },
},
}, .{});
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .init(box, .enabled(.white));
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.color = .green,
.sides = .horizontal,
},
}, scrollable.element());
defer container.deinit();
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
// further scrolling right will not change anything
try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.scrollbar.right.zon"), renderer.screen);
}
test "alignment center" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{});
defer container.deinit();
const aligned_container: Container(event.SystemEvent) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 5 },
.grow = .fixed,
},
}, .{});
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .center);
try container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{
.x = 30,
.y = 20,
}, &container, @import("test/element/alignment.center.zon"));
}
test "alignment left" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{});
defer container.deinit();
const aligned_container: Container(event.SystemEvent) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 5 },
.grow = .fixed,
},
}, .{});
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{
.h = .start,
.v = .center,
});
try container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{
.x = 30,
.y = 20,
}, &container, @import("test/element/alignment.left.zon"));
}
test "alignment right" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{});
defer container.deinit();
const aligned_container: Container(event.SystemEvent) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 5 },
.grow = .fixed,
},
}, .{});
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{
.h = .end,
.v = .center,
});
try container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{
.x = 30,
.y = 20,
}, &container, @import("test/element/alignment.right.zon"));
}
test "alignment top" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{});
defer container.deinit();
const aligned_container: Container(event.SystemEvent) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 5 },
.grow = .fixed,
},
}, .{});
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{
.h = .center,
.v = .start,
});
try container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{
.x = 30,
.y = 20,
}, &container, @import("test/element/alignment.top.zon"));
}
test "alignment bottom" {
const allocator = std.testing.allocator;
const event = @import("event.zig");
const testing = @import("testing.zig");
var container: Container(event.SystemEvent) = try .init(allocator, .{}, .{});
defer container.deinit();
const aligned_container: Container(event.SystemEvent) = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.size = .{
.dim = .{ .x = 12, .y = 5 },
.grow = .fixed,
},
}, .{});
var alignment: Alignment(event.SystemEvent) = .init(aligned_container, .{
.h = .center,
.v = .end,
});
try container.append(try .init(allocator, .{}, alignment.element()));
try testing.expectContainerScreen(.{
.x = 30,
.y = 20,
}, &container, @import("test/element/alignment.bottom.zon"));
}

View File

@@ -1,12 +1,5 @@
//! Events which are defined by the library. They might be extended by user
//! events. See `App` for more details about user defined events.
const std = @import("std");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const Key = input.Key;
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;
/// System events available to every `zterm.App`
pub const SystemEvent = union(enum) {
@@ -15,8 +8,11 @@ pub const SystemEvent = union(enum) {
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,
/// Resize event to signify that the application should re-draw to resize
resize,
/// Error event to notify other containers about a recoverable error
err: struct {
/// actual error
err: anyerror,
/// associated error message
msg: []const u8,
@@ -92,3 +88,10 @@ pub fn isTaggedUnion(comptime E: type) bool {
}
return true;
}
const std = @import("std");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const Key = input.Key;
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;

View File

@@ -1,7 +1,4 @@
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
const std = @import("std");
const Point = @import("point.zig").Point;
pub const Mouse = packed struct {
x: u16,
@@ -32,7 +29,7 @@ pub const Mouse = packed struct {
};
pub fn eql(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
return meta.eql(this, other);
}
pub fn in(this: @This(), origin: Point, size: Point) bool {
@@ -65,9 +62,11 @@ pub const Key = packed struct {
/// }
/// ```
pub fn eql(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
return meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
/// Determine if the `Key` is an ascii character that can be printed to
/// the screen. This means that the code point of the `Key` is an ascii
/// character between 32 - 255 (with the exception of 127 = Delete) and no
@@ -90,24 +89,25 @@ pub const Key = packed struct {
}
test "isAscii with ascii character" {
try std.testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
try testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
}
test "isAscii with non-ascii character" {
try std.testing.expectEqual(false, isAscii(.{ .cp = Escape }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
try testing.expectEqual(false, isAscii(.{ .cp = Escape }));
try testing.expectEqual(false, isAscii(.{ .cp = Enter }));
try testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
}
test "isAscii with excluded input.Delete" {
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
try testing.expectEqual(false, isAscii(.{ .cp = Delete }));
try testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
}
};
// TODO: std.ascii has the escape codes too!
// codepoints for keys
pub const Tab: u21 = 0x09;
pub const Enter: u21 = 0x0D;
@@ -152,21 +152,6 @@ pub const F17: u21 = 57380;
pub const F18: u21 = 57381;
pub const F19: u21 = 57382;
pub const F20: u21 = 57383;
pub const F21: u21 = 57384;
pub const F22: u21 = 57385;
pub const F23: u21 = 57386;
pub const F24: u21 = 57387;
pub const F25: u21 = 57388;
pub const F26: u21 = 57389;
pub const F27: u21 = 57390;
pub const F28: u21 = 57391;
pub const F29: u21 = 57392;
pub const F30: u21 = 57393;
pub const F31: u21 = 57394;
pub const F32: u21 = 57395;
pub const F33: u21 = 57396;
pub const F34: u21 = 57397;
pub const F35: u21 = 57398;
pub const Kp0: u21 = 57399;
pub const Kp1: u21 = 57400;
pub const Kp2: u21 = 57401;
@@ -223,3 +208,8 @@ pub const RightHyper: u21 = 57451;
pub const RightMeta: u21 = 57452;
pub const IsoLevel3Shift: u21 = 57453;
pub const IsoLevel5Shift: u21 = 57454;
const std = @import("std");
const meta = std.meta;
const Point = @import("point.zig").Point;
const testing = std.testing;

View File

@@ -1,9 +1,7 @@
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
// with slight modifications
const std = @import("std");
const assert = std.debug.assert;
/// Thread safe. Fixed size. Blocking push and pop.
/// Queue implementation. Thread safe. Fixed size. Blocking push and pop. Polling through tryPop and tryPush.
pub fn Queue(comptime T: type, comptime size: usize) type {
return struct {
buf: [size]T = undefined,
@@ -92,6 +90,23 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
assert(!this.isEmptyLH());
}
pub fn lock(this: *QueueType) void {
this.mutex.lock();
}
pub fn unlock(this: *QueueType) void {
this.mutex.unlock();
}
/// Used to efficiently drain the queue
pub fn drain(this: *QueueType) ?T {
if (this.isEmptyLH()) return null;
const result = this.buf[this.mask(this.read_index)];
this.read_index = this.mask2(this.read_index + 1);
return result;
}
fn isEmptyLH(this: QueueType) bool {
return this.write_index == this.read_index;
}
@@ -116,7 +131,7 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
}
/// Returns the length
fn len(this: QueueType) usize {
pub fn len(this: QueueType) usize {
const wrap_offset = 2 * this.buf.len *
@intFromBool(this.write_index < this.read_index);
const adjusted_write_index = this.write_index + wrap_offset;
@@ -135,8 +150,12 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
};
}
const std = @import("std");
const testing = std.testing;
const assert = std.debug.assert;
const Thread = std.Thread;
const cfg = Thread.SpawnConfig{ .allocator = testing.allocator };
test "Queue: simple push / pop" {
var queue: Queue(u8, 16) = .{};
queue.push(1);
@@ -146,7 +165,6 @@ test "Queue: simple push / pop" {
try testing.expectEqual(2, queue.pop());
}
const Thread = std.Thread;
fn testPushPop(q: *Queue(u8, 2)) !void {
q.push(3);
try testing.expectEqual(2, q.pop());
@@ -199,7 +217,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
try Thread.yield();
std.time.sleep(std.time.ns_per_s);
// Finally, let that other thread go.
try std.testing.expectEqual(1, q.pop());
try testing.expectEqual(1, q.pop());
// This won't continue until the other thread has had a chance to
// put at least one item in the queue.
@@ -218,7 +236,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
std.time.sleep(std.time.ns_per_s / 2);
// Pop that thing and we're done.
try std.testing.expectEqual(2, q.pop());
try testing.expectEqual(2, q.pop());
}
test "Fill, block, fill, block" {
@@ -238,15 +256,15 @@ test "Fill, block, fill, block" {
// Just to make sure the sleeps are yielding to this thread, make
// sure it took at least 900ms to do the push.
try std.testing.expect(then - now > 900);
try testing.expect(then - now > 900);
// This should block again, waiting for the other thread.
queue.push(4);
// And once that push has gone through, the other thread's done.
thread.join();
try std.testing.expectEqual(3, queue.pop());
try std.testing.expectEqual(4, queue.pop());
try testing.expectEqual(3, queue.pop());
try testing.expectEqual(4, queue.pop());
}
fn sleepyPush(q: *Queue(u8, 1)) !void {
@@ -284,8 +302,8 @@ test "Drain, block, drain, block" {
var queue: Queue(u8, 1) = .{};
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
try std.testing.expectEqual(1, queue.pop());
try std.testing.expectEqual(2, queue.pop());
try testing.expectEqual(1, queue.pop());
try testing.expectEqual(2, queue.pop());
thread.join();
}

View File

@@ -1,18 +1,14 @@
const std = @import("std");
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Point = @import("point.zig").Point;
//! Renderer for `zterm`.
/// Double-buffered intermediate rendering pipeline
pub const Buffered = struct {
allocator: std.mem.Allocator,
allocator: Allocator,
created: bool,
size: Point,
screen: []Cell,
virtual_screen: []Cell,
pub fn init(allocator: std.mem.Allocator) @This() {
pub fn init(allocator: Allocator) @This() {
return .{
.allocator = allocator,
.created = false,
@@ -29,9 +25,9 @@ pub const Buffered = struct {
}
}
pub fn resize(this: *@This()) !void {
pub fn resize(this: *@This()) !Point {
const size = terminal.getTerminalSize();
if (std.meta.eql(this.size, size)) return;
if (meta.eql(this.size, size)) return this.size;
this.size = size;
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
@@ -52,6 +48,7 @@ pub const Buffered = struct {
@memset(this.virtual_screen, .{});
}
try this.clear();
return size;
}
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
@@ -88,7 +85,9 @@ pub const Buffered = struct {
/// 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 {
try terminal.hideCursor();
// TODO measure timings of rendered frames?
var cursor_position: ?Point = null;
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
@@ -97,14 +96,36 @@ pub const Buffered = struct {
const idx = (row * this.size.x) + col;
const cs = s[idx];
const cvs = vs[idx];
// update the latest found cursor position
if (cvs.style.cursor) {
assert(cursor_position == null);
cursor_position = .{
.x = @truncate(col),
.y = @truncate(row),
};
}
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];
}
}
if (cursor_position) |point| {
try terminal.showCursor();
try terminal.setCursorPosition(point);
}
}
};
const std = @import("std");
const meta = std.meta;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Point = @import("point.zig").Point;

View File

@@ -17,7 +17,6 @@ pub const Renderer = @import("render.zig");
// Container Configurations
pub const Border = container.Border;
pub const Rectangle = container.Rectangle;
pub const Scroll = container.Scroll;
pub const Layout = container.Layout;
pub const Cell = @import("cell.zig");

View File

@@ -7,11 +7,13 @@
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
// with slight modifications
const std = @import("std");
const Color = @import("color.zig").Color;
pub const Style = @This();
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
cursor: bool = false,
ul_style: Underline = .off,
emphasis: []const Emphasis,
pub const Underline = enum {
off,
@@ -34,39 +36,43 @@ pub const Emphasis = enum(u8) {
strikethrough,
};
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
ul_style: Underline = .off,
emphasis: []const Emphasis,
pub fn eql(this: Style, other: Style) bool {
return std.meta.eql(this, other);
return meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined;
const bytes = try std.unicode.utf8Encode(cp, &buffer);
std.debug.assert(bytes > 0);
const bytes = try unicode.utf8Encode(cp, &buffer);
assert(bytes > 0);
// build ansi sequence for 256 colors ...
// foreground
try std.fmt.format(writer, "\x1b[", .{});
try format(writer, "\x1b[", .{});
try this.fg.write(writer, .fg);
// background
try std.fmt.format(writer, ";", .{});
try format(writer, ";", .{});
try this.bg.write(writer, .bg);
// underline
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try std.fmt.format(writer, ";", .{});
try format(writer, ";", .{});
try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
try std.fmt.format(writer, "m", .{});
for (this.emphasis) |attribute| try format(writer, ";{d}", .{@intFromEnum(attribute)});
try format(writer, "m", .{});
// content
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
try std.fmt.format(writer, "\x1b[0m", .{});
try format(writer, "{s}", .{buffer[0..bytes]});
try format(writer, "\x1b[0m", .{});
}
// TODO implement helper functions for terminal capabilities:
// - links / url display (osc 8)
// - show / hide cursor?
const std = @import("std");
const unicode = std.unicode;
const meta = std.meta;
const assert = std.debug.assert;
const format = std.fmt.format;
const Color = @import("color.zig").Color;
const Style = @This();

View File

@@ -1,15 +1,3 @@
const std = @import("std");
const code_point = @import("code_point");
const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig");
const Key = input.Key;
const Point = @import("point.zig").Point;
const Size = @import("point.zig").Point;
const Cell = @import("cell.zig");
const log = std.log.scoped(.terminal);
// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html
pub const ReportMode = enum {
not_recognized,
@@ -21,62 +9,62 @@ pub const ReportMode = enum {
/// Gets number of rows and columns in the terminal
pub fn getTerminalSize() Size {
var ws: std.posix.winsize = undefined;
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
var ws: posix.winsize = undefined;
_ = posix.system.ioctl(posix.STDIN_FILENO, posix.T.IOCGWINSZ, @intFromPtr(&ws));
return .{ .x = ws.col, .y = ws.row };
}
pub fn saveScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.save_screen);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen);
}
pub fn restoreScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.restore_screen);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen);
}
pub fn enterAltScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.smcup);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup);
}
pub fn exitAltScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.rmcup);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup);
}
pub fn clearScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.clear_screen);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen);
}
pub fn hideCursor() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.hide_cursor);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor);
}
pub fn showCursor() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.show_cursor);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
}
pub fn setCursorPositionHome() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.home);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
}
pub fn enableMouseSupport() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_set);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set);
}
pub fn disableMouseSupport() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_reset);
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
}
pub fn read(buf: []u8) !usize {
return try std.posix.read(std.posix.STDIN_FILENO, buf);
return try posix.read(posix.STDIN_FILENO, buf);
}
pub fn write(buf: []const u8) !usize {
return try std.posix.write(std.posix.STDIN_FILENO, buf);
return try posix.write(posix.STDIN_FILENO, buf);
}
fn contextWrite(context: @This(), data: []const u8) anyerror!usize {
_ = context;
return try std.posix.write(std.posix.STDOUT_FILENO, data);
return try posix.write(posix.STDOUT_FILENO, data);
}
const Writer = std.io.Writer(
@@ -91,19 +79,19 @@ pub fn writer() Writer {
pub fn setCursorPosition(pos: Point) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y + 1, pos.x + 1 });
_ = try posix.write(posix.STDIN_FILENO, value);
}
pub fn getCursorPosition() !Size.Position {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");
var buf: [64]u8 = undefined;
// format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R"
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
const len = try posix.read(posix.STDIN_FILENO, &buf);
if (!isCursorPosition(buf[0..len])) {
return error.InvalidValueReturned;
@@ -166,8 +154,8 @@ pub fn isCursorPosition(buf: []u8) bool {
///
/// `bak`: pointer to store termios struct backup before
/// altering, this is used to disable raw mode.
pub fn enableRawMode(bak: *std.posix.termios) !void {
var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO);
pub fn enableRawMode(bak: *posix.termios) !void {
var termios = try posix.tcgetattr(posix.STDIN_FILENO);
bak.* = termios;
// termios flags used by termios(3)
@@ -192,20 +180,20 @@ pub fn enableRawMode(bak: *std.posix.termios) !void {
termios.cflag.CSIZE = .CS8;
termios.cflag.PARENB = false;
termios.cc[@intFromEnum(std.posix.V.MIN)] = 1;
termios.cc[@intFromEnum(std.posix.V.TIME)] = 0;
termios.cc[@intFromEnum(posix.V.MIN)] = 1;
termios.cc[@intFromEnum(posix.V.TIME)] = 0;
try std.posix.tcsetattr(
std.posix.STDIN_FILENO,
try posix.tcsetattr(
posix.STDIN_FILENO,
.FLUSH,
termios,
);
}
/// Reverts `enableRawMode` to restore initial functionality.
pub fn disableRawMode(bak: *std.posix.termios) !void {
try std.posix.tcsetattr(
std.posix.STDIN_FILENO,
pub fn disableRawMode(bak: *posix.termios) !void {
try posix.tcsetattr(
posix.STDIN_FILENO,
.FLUSH,
bak.*,
);
@@ -215,13 +203,13 @@ pub fn disableRawMode(bak: *std.posix.termios) !void {
pub fn canSynchornizeOutput() !bool {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?2026$p");
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p");
var buf: [64]u8 = undefined;
// format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y"
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
const len = try posix.read(posix.STDIN_FILENO, &buf);
if (!mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
return false;
}
@@ -238,3 +226,16 @@ fn getReportMode(ps: u8) ReportMode {
else => ReportMode.not_recognized,
};
}
const log = std.log.scoped(.terminal);
const std = @import("std");
const mem = std.mem;
const posix = std.posix;
const code_point = @import("code_point");
const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig");
const Key = input.Key;
const Point = @import("point.zig").Point;
const Size = @import("point.zig").Point;
const Cell = @import("cell.zig");

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,12 @@
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
const std = @import("std");
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;
// TODO how would I describe the expected screens?
// - including styling?
// - compare generated strings instead? -> how would this be generated for the user?
/// Single-buffer test rendering pipeline for testing purposes.
pub const Renderer = struct {
allocator: std.mem.Allocator,
allocator: Allocator,
size: Point,
screen: []Cell,
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
pub fn init(allocator: Allocator, size: Point) @This() {
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
@memset(screen, .{});
@@ -116,11 +105,10 @@ pub const Renderer = struct {
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = std.testing.allocator;
const allocator = testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
try renderer.resize(size);
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), container);
@@ -133,10 +121,10 @@ pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEven
/// the contents of a given screen from the `zterm.testing.Renderer`. See
/// `zterm.testing.expectContainerScreen` for an example usage.
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
const allocator = std.testing.allocator;
const allocator = testing.allocator;
try std.testing.expectEqual(expected.len, actual.len);
try std.testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
try testing.expectEqual(expected.len, actual.len);
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit();
@@ -192,10 +180,20 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
// test failed
try buffer.flush();
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
debug.lockStdErr();
defer debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}
const std = @import("std");
const debug = std.debug;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;