refactor: zigify imports and correct minor mistakes

This commit is contained in:
2025-05-20 18:23:44 +02:00
parent 50adf32f14
commit aa4adf20f9
26 changed files with 311 additions and 330 deletions

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(.{});
@@ -36,7 +34,7 @@ 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,
});
@@ -184,7 +182,7 @@ pub fn build(b: *std.Build) void {
// 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,
});
@@ -196,3 +194,5 @@ pub fn build(b: *std.Build) void {
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.

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.";
@@ -158,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(.{
@@ -188,10 +181,16 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
const zterm = @import("zterm");
const input = zterm.input;
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.";
@@ -139,10 +130,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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -219,10 +210,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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -190,10 +182,16 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -150,10 +143,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -106,10 +99,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -98,10 +91,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -114,10 +107,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -97,10 +90,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -93,10 +86,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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.";
@@ -148,10 +141,15 @@ 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();
}
}
const log = std.log.scoped(.default);
const std = @import("std");
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,18 +24,10 @@ 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 Exec = element.Exec(Event, Queue);
pub const Queue = queue.Queue(Event, 256);
queue: Queue,
thread: ?std.Thread = null,
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
termios: ?posix.termios = null,
pub const SignalHandler = struct {
context: *anyopaque,
@@ -69,9 +46,9 @@ pub fn App(comptime E: type) type {
this.postEvent(.init);
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;
@@ -192,9 +169,9 @@ pub fn App(comptime E: type) type {
// 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], ';');
var field_iter = 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;
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
const key: Key = .{
.cp = switch (number) {
@@ -231,11 +208,11 @@ pub fn App(comptime E: type) type {
std.debug.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;
@@ -280,9 +257,9 @@ pub fn App(comptime E: type) type {
'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;
@@ -291,8 +268,8 @@ pub fn App(comptime E: type) type {
_ = width_char;
_ = height_char;
// 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,
// } });
}
},
@@ -338,5 +315,31 @@ pub fn App(comptime E: type) type {
break;
}
}
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 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 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,
@@ -25,16 +23,19 @@ pub const Color = enum(u8) {
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,22 +1,5 @@
const std = @import("std");
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;
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
@@ -40,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) {
@@ -156,7 +138,7 @@ 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| {
@@ -300,11 +282,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
@@ -343,13 +320,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;
@@ -574,7 +551,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,
@@ -592,7 +569,7 @@ pub fn Container(comptime Event: type) type {
};
pub fn init(
allocator: std.mem.Allocator,
allocator: Allocator,
properties: Properties,
element: Element,
) !@This() {
@@ -872,7 +849,7 @@ pub fn Container(comptime Event: type) type {
switch (event) {
.mouse => |mouse| if (mouse.in(this.origin, this.size)) {
// the element receives the mouse event with relative position
std.debug.assert(mouse.x >= this.origin.x and mouse.y >= this.origin.y);
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;
@@ -907,10 +884,21 @@ pub fn Container(comptime Event: type) type {
};
}
const log = std.log.scoped(.container);
const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
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 {
@@ -188,8 +181,14 @@ pub fn Scrollable(Event: type) type {
};
}
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;
// 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");

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) {
@@ -17,6 +10,7 @@ pub const SystemEvent = union(enum) {
quit,
/// Error event to notify other containers about a recoverable error
err: struct {
/// actual error
err: anyerror,
/// associated error message
msg: []const u8,
@@ -92,3 +86,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,7 +62,7 @@ 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!
@@ -92,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;
@@ -225,3 +223,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,
@@ -135,8 +133,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 +148,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 +200,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 +219,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 +239,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 +285,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.
@@ -118,3 +115,10 @@ pub const Buffered = struct {
}
}
};
const std = @import("std");
const meta = std.meta;
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,42 +36,43 @@ pub const Emphasis = enum(u8) {
strikethrough,
};
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
cursor: bool = false,
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(
@@ -92,18 +80,18 @@ 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 + 1, pos.x + 1 });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
_ = 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");

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,7 +105,7 @@ 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();
@@ -133,10 +122,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();
@@ -199,3 +188,12 @@ pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actu
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}
const std = @import("std");
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;