Compare commits
55 Commits
dab486a2c1
...
0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b7d032b11 | |||
| 7e20dd73d9 | |||
| 182dec6065 | |||
| 54af974c2b | |||
| dddc09b4ce | |||
| adda53c5a9 | |||
| 5c1d61eefd | |||
| 54c7e19939 | |||
| 5457e91b37 | |||
| 79016f39b2 | |||
| 2b9ab1e0fb | |||
| 315cd8d23e | |||
| e3551fa624 | |||
| 9ec335cad8 | |||
| 466e00c16c | |||
| fc72cf4abb | |||
| 65d7546efd | |||
| ec22e68e8c | |||
| 43cdc46853 | |||
| 591b990087 | |||
| 91ac6241f4 | |||
| edefc80759 | |||
| 4145ff497b | |||
| bec0cf2987 | |||
| e2fe884925 | |||
| caee008d50 | |||
| af443c6bbf | |||
| ae9cd08b15 | |||
| 91794a0197 | |||
| 8a7ce78aaf | |||
| 35ebe31008 | |||
| c28fcd26c1 | |||
| 3b6848f845 | |||
| 53b69f034c | |||
| 54ce697e91 | |||
| 8f16435f30 | |||
| ca14bc6106 | |||
| a293ef46da | |||
| c66401d941 | |||
| ad4186e1f8 | |||
| 8c130a40d7 | |||
| 9a3bc3dbf7 | |||
| 9d5a661b4e | |||
| 4234c9ad0c | |||
| a588e2ef21 | |||
| 8519d204f3 | |||
| 33262c9638 | |||
| 5c5c59cbfc | |||
| c022d1d9e2 | |||
| 12497e92f8 | |||
| d10f738c75 | |||
| 140f27216a | |||
| 04ba88c68b | |||
| 6ccab74c94 | |||
| c634e1affc |
@@ -18,7 +18,7 @@ jobs:
|
||||
with:
|
||||
version: master
|
||||
- name: Lint check
|
||||
run: zig fmt --check .
|
||||
run: zig fmt --check --exclude src/test .
|
||||
- name: Spell checking
|
||||
uses: crate-ci/typos@v1.25.0
|
||||
with:
|
||||
|
||||
216
README.md
216
README.md
@@ -1,19 +1,19 @@
|
||||
# zterm Terminal User Interface Library
|
||||
# zterm TUI Library
|
||||
|
||||
`zterm` is a terminal user interface library to implement terminal (fullscreen or inline) applications.
|
||||
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
|
||||
|
||||
> [!NOTE]
|
||||
> Only builds using the master version are tested to work.
|
||||
> [!CAUTION]
|
||||
> Only builds using the zig master version are tested to work.
|
||||
|
||||
## Demo
|
||||
|
||||
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
||||
|
||||
```sh
|
||||
zig build --release=safe -Dexample=input run
|
||||
zig build --release=safe -Dexample=demo run
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> [!TIP]
|
||||
> Every example application can be quit using `ctrl+c`.
|
||||
|
||||
See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details.
|
||||
@@ -26,7 +26,7 @@ To add or update `zterm` as a dependency in your project run the following comma
|
||||
zig fetch --save git+https://gitea.yves-biener.de/yves-biener/zterm
|
||||
```
|
||||
|
||||
Add the dependency to your module as follows in your _build.zig_:
|
||||
Add the dependency to your module as follows in your *build.zig*:
|
||||
|
||||
```zig
|
||||
const zterm: *Dependency = b.dependency("zterm", .{
|
||||
@@ -37,181 +37,6 @@ const zterm: *Dependency = b.dependency("zterm", .{
|
||||
exe.root_module.addImport("zterm", zterm.module("zterm"));
|
||||
```
|
||||
|
||||
---
|
||||
## Design Goals
|
||||
|
||||
This project draws heavy inspiration from
|
||||
[clay](https://github.com/nicbarker/clay) in the way the layout is declared by
|
||||
the user. As terminal applications usually are rendered in intermediate mode,
|
||||
the rendering is also part of the event loop. Such that every time an event
|
||||
happens a render call will usually be done as well. However this is not strickly
|
||||
necessary and can be separated to have a fixed rendering of every 16ms (i.e. for
|
||||
60 fps), etc.
|
||||
|
||||
There is only one generic container which contains properties and elements (or
|
||||
children) which can also be containers, such that each layout in the end is
|
||||
a tree.
|
||||
|
||||
The library is designed to be very basic and not to provide any more complex
|
||||
elements such as input fields, drop-down menu's, buttons, etc. Some of them are
|
||||
either easy to implement yourself, specific for you needs or a too complex to
|
||||
be provided by the library effectively. For these use-cases there may be other
|
||||
libraries that build on top of this one to provide the complex elements as some
|
||||
sort of pre-built elements for you to use in your application (or you create
|
||||
them yourself).
|
||||
|
||||
There are only very few system events, that are used by the built-in containers
|
||||
and properties accordingly. For you own widgets (i.e. a collection of elements)
|
||||
you can extend the events to include your own events to communicate between
|
||||
elements, effect the control flow and the corresponding generated layouts and
|
||||
much more.
|
||||
|
||||
As this is a terminal based layout library it also provides a rendering pipeline
|
||||
alongside the event loop implementation. Usually the event loop is waiting
|
||||
blocking and will only cause a re-draw (**intermediate mode**) after each event.
|
||||
Even though the each frame is regenerated from scratch each render loop, the
|
||||
corresponding application is still pretty performant as the renderer uses a
|
||||
double buffered intermediate mode implementation to only apply the changes from
|
||||
each frame to the next to the terminal.
|
||||
|
||||
This library is also designed to work accordingly in ssh hosted environments,
|
||||
such that an application created using this library can be accessed directly
|
||||
via ssh. This provides security through the ssh protocol and can defer the
|
||||
synchronization process, as users may access the same running instance. Which is
|
||||
the primary use-case for myself to create this library in the first place.
|
||||
|
||||
---
|
||||
## Roadmap
|
||||
|
||||
- [ ] Container rendering
|
||||
- [x] Layout
|
||||
- [x] direction
|
||||
- [x] vertical
|
||||
- [x] horizontal
|
||||
- [x] padding
|
||||
- [x] gap
|
||||
- [x] Border
|
||||
- [x] sides
|
||||
- [x] corners
|
||||
- [x] separators
|
||||
- [x] Rectangle
|
||||
- [x] min size
|
||||
- [ ] User control
|
||||
- [x] event loop handling
|
||||
- [x] mouse support
|
||||
- [x] user content
|
||||
- [ ] Default `Element` implementations
|
||||
- [ ] Scrollable
|
||||
- [x] user input handling
|
||||
- [x] vertical
|
||||
- [x] horizontal
|
||||
- [x] mouse input
|
||||
- [ ] scroll bar(s) rendering
|
||||
- [ ] vertical
|
||||
- [ ] horizontal
|
||||
- [ ] Content alignment (i.e. standard calculations done with the provided `Size`)
|
||||
- [x] User input
|
||||
- [x] single line
|
||||
- [x] multi line
|
||||
- [x] min size (provide size to use which would be a minimal size - as if the actual size is smaller then the `Container` will scroll and otherwise the contents expand to the available space instead?)
|
||||
- [ ] image support through kitty protocol (**later**)
|
||||
- [ ] Inline rendering (**later**)
|
||||
- [ ] Examples
|
||||
- [x] Layouts
|
||||
- [x] vertical
|
||||
- [x] horizontal
|
||||
- [x] grid
|
||||
- [x] mixed (some sort of form)
|
||||
- [ ] Elements
|
||||
- [x] Button
|
||||
- [x] Text Input field
|
||||
- [ ] Popup-menu
|
||||
- [x] Scrollable Content (i.e. show long text of an except of something and other smaller `Container`)
|
||||
- [x] min size
|
||||
- [x] mouse scrolling aware of mouse position (i.e. through multiple different scrollable `Container`)
|
||||
- [ ] Styles
|
||||
- [ ] Text styles
|
||||
- [ ] Colors
|
||||
- [x] foreground
|
||||
- [x] background
|
||||
- [ ] underline
|
||||
- [ ] Emphasis
|
||||
- [x] none
|
||||
- [x] bold
|
||||
- [x] dim
|
||||
- [x] italic
|
||||
- [x] underline
|
||||
- [x] blink
|
||||
- [x] invert
|
||||
- [x] hidden
|
||||
- [x] strikethrough
|
||||
- [ ] combinations
|
||||
- [x] Color palette
|
||||
- [ ] Error Handling
|
||||
- [ ] log and show error's without crashing the application
|
||||
- [ ] show application error that will cause a crash
|
||||
- [ ] Demo
|
||||
- [ ] use another tui application to launch and come back to (showcase the interrupt behavior)
|
||||
- [ ] Launch sub-applications (not inside of a `Container` but during the application workflow, like an editor)
|
||||
- [ ] Testability
|
||||
- [ ] snapshot ability to safe current screen (from `Renderer`) to test against
|
||||
- [ ] try to integrate them into the library itself such that they also serve as examples on how to test
|
||||
|
||||
Decorations should respect the layout and the viewport accordingly. This means
|
||||
that scrollbars are always visible (except there is no need to have a scrollbar)
|
||||
irrelevant depending on the size of the content. The rectangle apply to all
|
||||
cells of the content (and may be overwritten by child elements contents).
|
||||
The border of an element should be around independent of the scrolling of the
|
||||
contents, just like padding.
|
||||
|
||||
For most of the `Element`s a standalone implementation would not make a lot
|
||||
of sense due to the complexity of the user application states. Therefore the
|
||||
library should instead provide small examples to show how you can implement
|
||||
such user fields yourself and connect them using your own event system loops to
|
||||
communicate with other `Container`s and/or `Element`s.
|
||||
|
||||
### Scrollable contents
|
||||
|
||||
Contents that is scrollable should be done *virtually* through the contents of
|
||||
the `Container`. This means each container contents implements scrolling for
|
||||
itself if required.
|
||||
|
||||
This still has one issue: Layout of child elements that are already too large
|
||||
(i.e. or become too small). The library could provide automatic rendering of a
|
||||
scrollbar given the right parameters however. The scrolling input action would
|
||||
then also be implemented by the user.
|
||||
|
||||
Open questions are regarding the sizing options (i.e. how is the size of a
|
||||
`Container` actually controlled?, how should it be controlled?, etc.). There
|
||||
should be support for the child elements to provide some kind of 'list'
|
||||
functionality built-in.
|
||||
|
||||
**REMINDER**: (mostly for myself) The library should be and remain simple. This
|
||||
means that some code for using the library may be duplicated, but this is not
|
||||
the main goal. Others may provide more re-usable code snippets that build on top
|
||||
of this library instead.
|
||||
|
||||
### User specific event handling and content rendering
|
||||
|
||||
For interactions controlled by the user each container can use an `Element`
|
||||
interface which contains functions which are called by the `Container`
|
||||
during event handling (i.e. `fn handle(..)`) and during rendering (i.e. `fn
|
||||
content(..)`) to provide user specific content and user interaction. The
|
||||
`Element` may be stateful, but may also be stateless and then be re-used in
|
||||
multiple different `Container`s.
|
||||
|
||||
Composing multiple `Element`s currently requires the implementation of a wrapper
|
||||
which contains the `Element`s that need to be handled (should work pretty well
|
||||
for stateless `Element`s). Such *stateless* `Element`s may be provided by this
|
||||
library.
|
||||
|
||||
### Input
|
||||
|
||||
How is the user input handled in the containers? Should there be active
|
||||
containers? Some input may happen for a specific container (i.e. when using
|
||||
mouse input). How would I handle scrolling for outer and inner elements of
|
||||
a container?
|
||||
|
||||
### Documentation
|
||||
|
||||
A wiki should be created containing a bright overview of the structure and usage
|
||||
@@ -219,30 +44,3 @@ of the library. For details it should refer to the examples. The documentation
|
||||
should be minimal in terms of updateability in case the library changes. Maybe
|
||||
some documentation could be derived from the code documentation (there is a tool
|
||||
for this if I recall correctly).
|
||||
|
||||
Afterwards this README file contents can be updated to provide an actual
|
||||
overview of the library in a short form containing links to the detailed
|
||||
information required for usage.
|
||||
|
||||
The documentation may contain tips about how to implement corresponding event
|
||||
loops or how to design own `Element`s. And also on how to test accordingly and
|
||||
use the library itself for examples on how to design the test cases.
|
||||
|
||||
### Archive
|
||||
|
||||
The alignment and sizing options only make sense if both are available. For
|
||||
this the current implementation has the viewport size and the content size too
|
||||
linked. Therefore they have both been removed (at least for now):
|
||||
|
||||
- *fit*: adjust virtual space of container by the size of its children (i.e. a
|
||||
container needs to be able to get the necessary size of its children)
|
||||
- *grow*: use as much space as available (what exactly would be the difference
|
||||
between this option and *fit*?)
|
||||
- *fixed*: use exactly as much cells (in the specified direction)
|
||||
|
||||
- *center*: elements should have their anchor be placed accordingly to their
|
||||
size and the viewport size.
|
||||
- *left*: the anchor remains at zero (relative to the location of the
|
||||
container on the screen) -> similar to the current implementation!
|
||||
- *right*: the anchor is fixed to the right side (i.e. size of the contents -
|
||||
size of the viewport)
|
||||
|
||||
28
build.zig
28
build.zig
@@ -5,6 +5,8 @@ pub fn build(b: *std.Build) void {
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const Examples = enum {
|
||||
all,
|
||||
demo,
|
||||
// elements:
|
||||
button,
|
||||
input,
|
||||
@@ -21,7 +23,7 @@ pub fn build(b: *std.Build) void {
|
||||
errors,
|
||||
};
|
||||
|
||||
const example = b.option(Examples, "example", "Example to build and/or run. (default: vertical)") orelse .vertical;
|
||||
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(Examples, "example", example);
|
||||
@@ -42,6 +44,15 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
//--- Examples ---
|
||||
|
||||
// demo:
|
||||
const demo = b.addExecutable(.{
|
||||
.name = "demo",
|
||||
.root_source_file = b.path("examples/demo.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
demo.root_module.addImport("zterm", lib);
|
||||
|
||||
// elements:
|
||||
const button = b.addExecutable(.{
|
||||
.name = "button",
|
||||
@@ -128,6 +139,7 @@ pub fn build(b: *std.Build) void {
|
||||
|
||||
// mapping of user selected example to compile step
|
||||
const exe = switch (example) {
|
||||
.demo => demo,
|
||||
// elements:
|
||||
.button => button,
|
||||
.input => input,
|
||||
@@ -142,6 +154,19 @@ pub fn build(b: *std.Build) void {
|
||||
.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);
|
||||
|
||||
@@ -164,6 +189,7 @@ pub fn build(b: *std.Build) void {
|
||||
.optimize = optimize,
|
||||
});
|
||||
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
|
||||
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
|
||||
|
||||
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||
|
||||
|
||||
@@ -6,16 +6,29 @@
|
||||
//
|
||||
// It is redundant to include "zig" in this name because it is already
|
||||
// within the Zig package namespace.
|
||||
.name = "zterm",
|
||||
.name = .zterm,
|
||||
|
||||
// Together with name, this represents a globally unique package
|
||||
// identifier. This field is generated by the Zig toolchain when the
|
||||
// package is first created, and then *never changes*. This allows
|
||||
// unambiguous detection of one package being an updated version of
|
||||
// another.
|
||||
//
|
||||
// When forking a Zig project, this id should be regenerated (delete the
|
||||
// field and run `zig build`) if the upstream project is still maintained.
|
||||
// Otherwise, the fork is *hostile*, attempting to take control over the
|
||||
// original project's identity. Thus it is recommended to leave the comment
|
||||
// on the following line intact, so that it shows up in code reviews that
|
||||
// modify the field.
|
||||
.fingerprint = 0xf10b37e210a619d7, // Changing this has security and trust implications.
|
||||
|
||||
// This is a [Semantic Version](https://semver.org/).
|
||||
// In a future version of Zig it will be used for package deduplication.
|
||||
.version = "0.0.0",
|
||||
.version = "0.2.0",
|
||||
|
||||
// This field is optional.
|
||||
// This is currently advisory only; Zig does not yet do anything
|
||||
// with this value.
|
||||
//.minimum_zig_version = "0.11.0",
|
||||
// Tracks the earliest Zig version that the package considers to be a
|
||||
// supported use case.
|
||||
.minimum_zig_version = "0.15.0-dev.56+d0911786c",
|
||||
|
||||
// This field is optional.
|
||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||
@@ -29,11 +42,9 @@
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"LICENSE",
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
// For example...
|
||||
//"LICENSE",
|
||||
//"README.md",
|
||||
},
|
||||
}
|
||||
|
||||
158
examples/demo.zig
Normal file
158
examples/demo.zig
Normal file
@@ -0,0 +1,158 @@
|
||||
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.";
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
const y = 2;
|
||||
const x = size.x / 2 -| (text.len / 2);
|
||||
const anchor = (y * size.x) + x;
|
||||
|
||||
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});
|
||||
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .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 quit_text: QuitText = .{};
|
||||
|
||||
// TODO what should the demo application do?
|
||||
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
|
||||
// - on the left some buttons?
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .blue,
|
||||
.sides = .all,
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 1,
|
||||
.padding = .vertical(2),
|
||||
.direction = .vertical,
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .y = 90 },
|
||||
},
|
||||
}, .{});
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
try box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
}, .{}));
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{ .enabled = true },
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, quit_text.element());
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.color = .light_blue,
|
||||
.sides = .all,
|
||||
},
|
||||
.size = .{
|
||||
.dim = .{ .x = 100 },
|
||||
},
|
||||
}, .{}));
|
||||
try container.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.size = .{
|
||||
.dim = .{ .x = 30 },
|
||||
},
|
||||
}, .{}));
|
||||
defer container.deinit(); // also de-initializes the children
|
||||
|
||||
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();
|
||||
|
||||
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||
try app.interrupt();
|
||||
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(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Spawning $EDITOR failed",
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
},
|
||||
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,37 @@ const App = zterm.App(union(enum) {
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
pub const Clickable = struct {
|
||||
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;
|
||||
std.debug.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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Clickable = struct {
|
||||
const text = "Press me";
|
||||
|
||||
queue: *App.Queue,
|
||||
color: zterm.Color = .black,
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
return .{
|
||||
@@ -25,24 +52,32 @@ pub const Clickable = struct {
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.mouse => |mouse| this.queue.push(.{ .click = @tagName(mouse.button) }),
|
||||
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
|
||||
var value = @intFromEnum(this.color);
|
||||
value += 1;
|
||||
value %= 17;
|
||||
if (value == 0) value = 1;
|
||||
this.color = @enumFromInt(value);
|
||||
this.queue.push(.{ .click = @tagName(mouse.button) });
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
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));
|
||||
|
||||
const row = size.rows / 2 -| (text.len / 2);
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
const row = size.y / 2 -| (text.len / 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 = .black;
|
||||
cells[anchor + idx].style.fg = this.color;
|
||||
cells[anchor + idx].style.emphasis = &.{.bold};
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -52,9 +87,7 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
@@ -65,10 +98,12 @@ pub fn main() !void {
|
||||
var clickable: Clickable = .{ .queue = &app.queue };
|
||||
const element = clickable.element();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{ .padding = .all(5) },
|
||||
}, .{});
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
||||
@@ -81,10 +116,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.click => |button| {
|
||||
log.info("Clicked with mouse using Button: {s}", .{button});
|
||||
@@ -100,6 +133,15 @@ pub fn main() !void {
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,33 @@ const App = zterm.App(union(enum) {
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
pub const InputField = struct {
|
||||
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;
|
||||
std.debug.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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const InputField = struct {
|
||||
input: std.ArrayList(u21),
|
||||
queue: *App.Queue,
|
||||
|
||||
@@ -48,21 +74,21 @@ pub const InputField = struct {
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
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.cols) * @as(usize, size.rows));
|
||||
std.debug.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.cols) + col;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (this.input.items, 0..) |cp, idx| {
|
||||
cells[anchor + idx].style.fg = .black;
|
||||
cells[anchor + idx].cp = cp;
|
||||
|
||||
// NOTE: do not write over the contents of this `Container`'s `Size`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -72,9 +98,7 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
@@ -85,12 +109,14 @@ pub fn main() !void {
|
||||
var input_field: InputField = .init(allocator, &app.queue);
|
||||
defer input_field.deinit();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
const element = input_field.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
.layout = .{ .padding = .all(5) },
|
||||
}, .{});
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
||||
@@ -103,10 +129,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
.accept => |input| {
|
||||
defer allocator.free(input);
|
||||
@@ -123,6 +147,15 @@ pub fn main() !void {
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -6,7 +6,33 @@ const App = zterm.App(union(enum) {});
|
||||
|
||||
const log = std.log.scoped(.default);
|
||||
|
||||
pub const HelloWorldText = packed struct {
|
||||
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;
|
||||
std.debug.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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const HelloWorldText = packed struct {
|
||||
const text = "Hello World";
|
||||
|
||||
pub fn element(this: *@This()) App.Element {
|
||||
@@ -16,18 +42,21 @@ pub const HelloWorldText = packed struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
// NOTE: error should only be returned here in case an in-recoverable exception has occurred
|
||||
const row = size.rows / 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
const row = size.y / 2;
|
||||
const col = size.x / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (0.., text) |idx, char| {
|
||||
cells[anchor + idx].style.fg = .black;
|
||||
cells[anchor + idx].cp = char;
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -35,7 +64,7 @@ pub const HelloWorldText = packed struct {
|
||||
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
|
||||
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer {
|
||||
const deinit_status = gpa.deinit();
|
||||
@@ -52,34 +81,55 @@ pub fn main() !void {
|
||||
var element_wrapper: HelloWorldText = .{};
|
||||
const element = element_wrapper.element();
|
||||
|
||||
var quit_text: QuitText = .{};
|
||||
|
||||
var top_box = try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.layout = .{
|
||||
.gap = 1,
|
||||
.gap = 2,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
},
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
.min_size = .{ .rows = 50 },
|
||||
}, .{});
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 30 },
|
||||
},
|
||||
}, .{}));
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 5 },
|
||||
},
|
||||
}, element));
|
||||
try top_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .light_green },
|
||||
.size = .{
|
||||
.dim = .{ .y = 2 },
|
||||
},
|
||||
}, .{}));
|
||||
defer top_box.deinit();
|
||||
|
||||
var bottom_box = try App.Container.init(allocator, .{
|
||||
.border = .{ .separator = .{ .enabled = true } },
|
||||
.rectangle = .{ .fill = .blue },
|
||||
.border = .{
|
||||
.sides = .all,
|
||||
.color = .blue,
|
||||
},
|
||||
.layout = .{
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.color = .red,
|
||||
},
|
||||
.direction = .vertical,
|
||||
.padding = .vertical(1),
|
||||
},
|
||||
.min_size = .{ .rows = 30 },
|
||||
.size = .{
|
||||
.dim = .{ .y = 30 },
|
||||
},
|
||||
}, .{});
|
||||
try bottom_box.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .grey },
|
||||
@@ -93,25 +143,23 @@ pub fn main() !void {
|
||||
defer bottom_box.deinit();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.border = .{
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.separator = .{
|
||||
.enabled = true,
|
||||
.line = .double,
|
||||
},
|
||||
},
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .all(5),
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
}, quit_text.element());
|
||||
defer container.deinit();
|
||||
|
||||
// place empty container containing the element of the scrollable Container.
|
||||
var scrollable_top: App.Scrollable = .init(top_box);
|
||||
var scrollable_top: App.Scrollable = .{ .container = top_box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
||||
|
||||
var scrollable_bottom: App.Scrollable = .init(bottom_box);
|
||||
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -122,10 +170,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.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 => {},
|
||||
@@ -137,6 +183,16 @@ pub fn main() !void {
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,20 +38,20 @@ const InfoText = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -64,31 +64,30 @@ const ErrorNotification = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
|
||||
}
|
||||
|
||||
pub fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
||||
.resize => |_| {},
|
||||
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
|
||||
.err => |err| this.msg = err.msg,
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
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.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
if (this.msg) |msg| {
|
||||
const row = size.rows -| 2;
|
||||
const col = size.cols -| 2 -| msg.len;
|
||||
const anchor = (row * size.cols) + col;
|
||||
const row = size.y -| 2;
|
||||
const col = size.x -| 2 -| msg.len;
|
||||
const anchor = (row * size.x) + col;
|
||||
|
||||
for (msg, 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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
|
||||
@@ -101,9 +100,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -132,10 +130,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.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 => {},
|
||||
@@ -147,6 +143,16 @@ pub fn main() !void {
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -51,16 +50,16 @@ pub fn main() !void {
|
||||
const element = quit_text.element();
|
||||
|
||||
var container = try App.Container.init(allocator, .{
|
||||
.border = .{ .separator = .{ .enabled = true } },
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, element);
|
||||
for (0..3) |_| {
|
||||
var column = try App.Container.init(allocator, .{
|
||||
.border = .{ .separator = .{ .enabled = true } },
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.direction = .vertical,
|
||||
},
|
||||
}, .{});
|
||||
@@ -85,23 +84,31 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -77,23 +76,31 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -54,13 +53,12 @@ pub fn main() !void {
|
||||
.layout = .{
|
||||
.gap = 2,
|
||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||
.direction = .horizontal,
|
||||
},
|
||||
}, element);
|
||||
for (0..3) |i| {
|
||||
var column = try App.Container.init(allocator, .{
|
||||
.border = .{ .separator = .{ .enabled = true } },
|
||||
.layout = .{
|
||||
.separator = .{ .enabled = true },
|
||||
.direction = if (i > 0) .vertical else .horizontal,
|
||||
},
|
||||
}, .{});
|
||||
@@ -72,7 +70,12 @@ pub fn main() !void {
|
||||
.rectangle = .{ .fill = .yellow },
|
||||
}, .{}));
|
||||
} else {
|
||||
try column.append(try App.Container.init(allocator, .{}, .{}));
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.size = .{
|
||||
.dim = .{ .y = 4 },
|
||||
.grow = .horizontal,
|
||||
},
|
||||
}, .{}));
|
||||
}
|
||||
try column.append(try App.Container.init(allocator, .{
|
||||
.rectangle = .{ .fill = .blue },
|
||||
@@ -89,23 +92,31 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -15,20 +15,20 @@ const QuitText = struct {
|
||||
};
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,9 +38,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -76,23 +75,31 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||
// NOTE: errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
|
||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||
else => {},
|
||||
}
|
||||
|
||||
// NOTE: returned errors should be propagated back to the application
|
||||
// NOTE returned errors should be propagated back to the application
|
||||
container.handle(event) catch |err| app.postEvent(.{
|
||||
.err = .{
|
||||
.err = err,
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -35,9 +35,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -57,7 +56,6 @@ pub fn main() !void {
|
||||
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.layout = .{ .direction = .horizontal },
|
||||
.min_size = .{ .cols = 3 * std.meta.fields(zterm.Color).len }, // ensure enough columns to render all colors -> scrollable otherwise
|
||||
}, .{});
|
||||
defer box.deinit();
|
||||
|
||||
@@ -65,7 +63,7 @@ pub fn main() !void {
|
||||
if (comptime 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 = .init(box);
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -75,10 +73,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.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 => {},
|
||||
@@ -90,6 +86,16 @@ pub fn main() !void {
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
@@ -12,20 +12,20 @@ const QuitText = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
const row = 2;
|
||||
const col = size.cols / 2 -| (text.len / 2);
|
||||
const anchor = (row * size.cols) + col;
|
||||
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`
|
||||
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||
if (anchor + idx == cells.len - 1) break;
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,10 @@ const TextStyles = struct {
|
||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||
}
|
||||
|
||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||
@setEvalBranchQuota(50000);
|
||||
_ = ctx;
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
|
||||
var row: usize = 0;
|
||||
var col: usize = 0;
|
||||
@@ -56,9 +56,9 @@ const TextStyles = struct {
|
||||
|
||||
// witouth any emphasis
|
||||
for (text) |cp| {
|
||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.cols) + col].cp = cp;
|
||||
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.x) + col].cp = cp;
|
||||
col += 1;
|
||||
}
|
||||
|
||||
@@ -68,10 +68,10 @@ const TextStyles = struct {
|
||||
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
||||
|
||||
for (text) |cp| {
|
||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.cols) + col].style.emphasis = &.{emphasis};
|
||||
cells[(row * size.cols) + col].cp = cp;
|
||||
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
|
||||
cells[(row * size.x) + col].cp = cp;
|
||||
col += 1;
|
||||
}
|
||||
}
|
||||
@@ -86,9 +86,8 @@ pub fn main() !void {
|
||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||
|
||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
||||
defer if (gpa.deinit() == .leak) {
|
||||
log.err("memory leak", .{});
|
||||
};
|
||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app: App = .init;
|
||||
@@ -110,14 +109,16 @@ pub fn main() !void {
|
||||
|
||||
var box = try App.Container.init(allocator, .{
|
||||
.layout = .{ .direction = .vertical },
|
||||
.min_size = .{
|
||||
.rows = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
||||
.cols = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
||||
}, // ensure enough rows and/or columns to render all text styles -> scrollable otherwise
|
||||
.size = .{
|
||||
.dim = .{
|
||||
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
||||
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
||||
},
|
||||
},
|
||||
}, text_styles.element());
|
||||
defer box.deinit();
|
||||
|
||||
var scrollable: App.Scrollable = .init(box);
|
||||
var scrollable: App.Scrollable = .{ .container = box };
|
||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||
|
||||
try app.start();
|
||||
@@ -127,10 +128,8 @@ pub fn main() !void {
|
||||
const event = app.nextEvent();
|
||||
log.debug("received event: {s}", .{@tagName(event)});
|
||||
|
||||
// pre event handling
|
||||
switch (event) {
|
||||
.init => continue,
|
||||
.quit => break,
|
||||
.resize => |size| try renderer.resize(size),
|
||||
.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 => {},
|
||||
@@ -142,6 +141,16 @@ pub fn main() !void {
|
||||
.msg = "Container Event handling failed",
|
||||
},
|
||||
});
|
||||
|
||||
// post event handling
|
||||
switch (event) {
|
||||
.quit => break,
|
||||
else => {},
|
||||
}
|
||||
|
||||
try renderer.resize();
|
||||
container.resize(renderer.size);
|
||||
container.reposition(.{});
|
||||
try renderer.render(@TypeOf(container), &container);
|
||||
try renderer.flush();
|
||||
}
|
||||
|
||||
48
src/app.zig
48
src/app.zig
@@ -11,7 +11,7 @@ const isTaggedUnion = event.isTaggedUnion;
|
||||
|
||||
const Mouse = input.Mouse;
|
||||
const Key = input.Key;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
const log = std.log.scoped(.app);
|
||||
|
||||
@@ -51,7 +51,6 @@ pub fn App(comptime E: type) type {
|
||||
quit_event: std.Thread.ResetEvent,
|
||||
termios: ?std.posix.termios = null,
|
||||
attached_handler: bool = false,
|
||||
prev_size: Size,
|
||||
|
||||
pub const SignalHandler = struct {
|
||||
context: *anyopaque,
|
||||
@@ -64,7 +63,6 @@ pub fn App(comptime E: type) type {
|
||||
.quit_event = .{},
|
||||
.termios = null,
|
||||
.attached_handler = false,
|
||||
.prev_size = .{},
|
||||
};
|
||||
|
||||
pub fn start(this: *@This()) !void {
|
||||
@@ -99,11 +97,6 @@ pub fn App(comptime E: type) type {
|
||||
try terminal.enterAltScreen();
|
||||
try terminal.hideCursor();
|
||||
try terminal.enableMouseSupport();
|
||||
|
||||
// send initial size afterwards
|
||||
const size = terminal.getTerminalSize();
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
|
||||
pub fn interrupt(this: *@This()) !void {
|
||||
@@ -148,11 +141,8 @@ pub fn App(comptime E: type) type {
|
||||
|
||||
fn winsizeCallback(ptr: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||
const size = terminal.getTerminalSize();
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
_ = this;
|
||||
// this.postEvent(.{ .size = terminal.getTerminalSize() });
|
||||
}
|
||||
|
||||
var winch_handler: ?SignalHandler = null;
|
||||
@@ -174,11 +164,11 @@ pub fn App(comptime E: type) type {
|
||||
// thread to read user inputs
|
||||
var buf: [256]u8 = undefined;
|
||||
while (true) {
|
||||
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
|
||||
// FIX I still think that there is a race condition (I'm just waiting 'long' enough)
|
||||
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
|
||||
// FIX: in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
|
||||
// FIX in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
|
||||
const read_bytes = try terminal.read(buf[0..]);
|
||||
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
||||
// TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
||||
// escape key presses
|
||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||
switch (buf[1]) {
|
||||
@@ -273,7 +263,7 @@ pub fn App(comptime E: type) type {
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
// TODO: focus usage? should this even be in the default event system?
|
||||
// TODO focus usage? should this even be in the default event system?
|
||||
'I' => this.postEvent(.{ .focus = true }),
|
||||
'O' => this.postEvent(.{ .focus = false }),
|
||||
'M', 'm' => {
|
||||
@@ -302,8 +292,8 @@ pub fn App(comptime E: type) type {
|
||||
|
||||
const mouse: Mouse = .{
|
||||
.button = button,
|
||||
.col = px -| 1,
|
||||
.row = py -| 1,
|
||||
.x = px -| 1,
|
||||
.y = py -| 1,
|
||||
.kind = blk: {
|
||||
if (motion and button != Mouse.Button.none) {
|
||||
break :blk .drag;
|
||||
@@ -334,19 +324,15 @@ pub fn App(comptime E: type) type {
|
||||
if (std.mem.eql(u8, "48", ps)) {
|
||||
// in band window resize
|
||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||
const height_char = iter.next() orelse break;
|
||||
const width_char = iter.next() orelse break;
|
||||
const height_char = iter.next() orelse break;
|
||||
|
||||
// TODO: only post the event if the size has changed?
|
||||
// because there might be too many resize events (which force a re-draw of the entire screen)
|
||||
const size: Size = .{
|
||||
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
};
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
_ = 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,
|
||||
// } });
|
||||
}
|
||||
},
|
||||
'u' => {
|
||||
@@ -361,7 +347,7 @@ pub fn App(comptime E: type) type {
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
// TODO: parse corresponding codes
|
||||
// TODO parse corresponding codes
|
||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||
else => {},
|
||||
}
|
||||
|
||||
46
src/cell.zig
46
src/cell.zig
@@ -4,7 +4,7 @@ const Style = @import("style.zig");
|
||||
pub const Cell = @This();
|
||||
|
||||
style: Style = .{ .emphasis = &.{} },
|
||||
// TODO: embrace `zg` dependency more due to utf-8 encoding
|
||||
// TODO embrace `zg` dependency more due to utf-8 encoding
|
||||
cp: u21 = ' ',
|
||||
|
||||
pub fn eql(this: Cell, other: Cell) bool {
|
||||
@@ -19,3 +19,47 @@ pub fn reset(this: *Cell) void {
|
||||
pub fn value(this: Cell, writer: anytype) !void {
|
||||
try this.style.value(writer, this.cp);
|
||||
}
|
||||
|
||||
test "ascii styled text" {
|
||||
const cells: [4]Cell = .{
|
||||
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
|
||||
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
|
||||
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
}
|
||||
try std.testing.expectEqualSlices(
|
||||
u8,
|
||||
"\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m",
|
||||
string.items,
|
||||
);
|
||||
}
|
||||
|
||||
test "utf-8 styled text" {
|
||||
const cells: [4]Cell = .{
|
||||
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
|
||||
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
|
||||
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
||||
};
|
||||
|
||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
||||
defer string.deinit();
|
||||
|
||||
const writer = string.writer();
|
||||
for (cells) |cell| {
|
||||
try cell.value(writer);
|
||||
}
|
||||
try std.testing.expectEqualSlices(
|
||||
u8,
|
||||
"\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m",
|
||||
string.items,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ pub const Color = enum(u8) {
|
||||
magenta,
|
||||
cyan,
|
||||
white,
|
||||
// TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
||||
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
|
||||
|
||||
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
|
||||
if (this == .default) {
|
||||
|
||||
1052
src/container.zig
1052
src/container.zig
File diff suppressed because it is too large
Load Diff
@@ -92,7 +92,7 @@ pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
|
||||
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
|
||||
|
||||
// Underlines
|
||||
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
|
||||
pub const ul_off = "\x1b[24m"; // NOTE this could be \x1b[4:0m but is not as widely supported
|
||||
pub const ul_single = "\x1b[4m";
|
||||
pub const ul_double = "\x1b[4:2m";
|
||||
pub const ul_curly = "\x1b[4:3m";
|
||||
|
||||
362
src/element.zig
362
src/element.zig
@@ -1,13 +1,11 @@
|
||||
//! Interface for Element's which describe the contents of a `Container`.
|
||||
const std = @import("std");
|
||||
const s = @import("size.zig");
|
||||
const input = @import("input.zig");
|
||||
|
||||
const Container = @import("container.zig").Container;
|
||||
const Cell = @import("cell.zig");
|
||||
const Mouse = input.Mouse;
|
||||
const Position = s.Position;
|
||||
const Size = s.Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub fn Element(Event: type) type {
|
||||
return struct {
|
||||
@@ -15,12 +13,26 @@ pub fn Element(Event: type) type {
|
||||
vtable: *const VTable = &.{},
|
||||
|
||||
pub const VTable = struct {
|
||||
resize: ?*const fn (ctx: *anyopaque, size: Point) void = null,
|
||||
reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null,
|
||||
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null,
|
||||
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Size) anyerror!void = null,
|
||||
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Point) anyerror!void = null,
|
||||
};
|
||||
|
||||
/// Resize the corresponding `Element` with the given *size*.
|
||||
pub inline fn resize(this: @This(), size: Point) void {
|
||||
if (this.vtable.resize) |resize_fn|
|
||||
resize_fn(this.ptr, size);
|
||||
}
|
||||
|
||||
/// Reposition the corresponding `Element` with the given *origin*.
|
||||
pub inline fn reposition(this: @This(), origin: Point) void {
|
||||
if (this.vtable.reposition) |reposition_fn|
|
||||
reposition_fn(this.ptr, origin);
|
||||
}
|
||||
|
||||
/// Handle the received event. The event is one of the user provided
|
||||
/// events or a system event, with the exception of the `.resize`
|
||||
/// events or a system event, with the exception of the `.size`
|
||||
/// `Event` as every `Container` already handles that event.
|
||||
///
|
||||
/// In case of user errors this function should return an error. This
|
||||
@@ -32,114 +44,89 @@ pub fn Element(Event: type) type {
|
||||
}
|
||||
|
||||
/// Write content into the `cells` of the `Container`. The associated
|
||||
/// `cells` slice has the size of (`size.cols * size.rows`). The
|
||||
/// `cells` slice has the size of (`size.x * size.y`). The
|
||||
/// renderer will know where to place the contents on the screen.
|
||||
///
|
||||
/// This function should only fail with an error if the error is
|
||||
/// non-recoverable (i.e. an allocation error, system error, etc.).
|
||||
/// 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: Size) !void {
|
||||
/// # Note
|
||||
///
|
||||
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
|
||||
/// ```zig
|
||||
/// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||
/// ```
|
||||
///
|
||||
/// - This function should only fail with an error if the error is
|
||||
/// non-recoverable (i.e. an allocation error, system error, etc.).
|
||||
/// 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|
|
||||
try content_fn(this.ptr, cells, size);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// This is an empty template implementation for an Element type which `zterm` may provide.
|
||||
///
|
||||
/// TODO: Should elements need to be composible with each other, such that they may build complexer outputs?
|
||||
/// - the goal would rather be to have re-usable parts of handlers and/or content functions which serve similar functionalities.
|
||||
/// - composible through empty `Container`'s? -> make sense for the `Scrollable` `Element`
|
||||
pub fn Template(Event: type) type {
|
||||
return packed struct {
|
||||
pub fn element(this: *@This()) Element(Event) {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
_ = this;
|
||||
switch (event) {
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
||||
_ = this;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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: Size = .{},
|
||||
size: Point = .{},
|
||||
/// `Size` of the `Container` content that is scrollable and mapped to
|
||||
/// the *size* of the `Scrollable` `Element`.
|
||||
container_size: Size = .{},
|
||||
container_size: Point = .{},
|
||||
/// Anchor of the viewport of the scrollable `Container`.
|
||||
anchor: Position = .{},
|
||||
/// The actual container, that is scrollable.
|
||||
anchor: Point = .{},
|
||||
/// The actual `Container`, that is scrollable.
|
||||
container: Container(Event),
|
||||
|
||||
pub fn element(this: *@This()) Element(Event) {
|
||||
return .{
|
||||
.ptr = this,
|
||||
.vtable = &.{
|
||||
.resize = resize,
|
||||
.reposition = reposition,
|
||||
.handle = handle,
|
||||
.content = content,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn init(container: Container(Event)) @This() {
|
||||
return .{ .container = container };
|
||||
fn resize(ctx: *anyopaque, size: Point) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
this.size = size;
|
||||
|
||||
// TODO scrollbar space - depending on configuration and only if necessary?
|
||||
this.container.resize(this.size);
|
||||
this.container_size = this.container.size;
|
||||
}
|
||||
|
||||
fn reposition(ctx: *anyopaque, _: Point) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
this.container.reposition(.{});
|
||||
}
|
||||
|
||||
fn handle(ctx: *anyopaque, event: Event) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
switch (event) {
|
||||
.resize => |size| {
|
||||
this.size = size;
|
||||
// TODO: scrollbar space - depending on configuration and only if necessary?
|
||||
const min_size = this.container.minSize();
|
||||
this.container_size = .{
|
||||
.anchor = size.anchor,
|
||||
.cols = @max(min_size.cols, size.cols),
|
||||
.rows = @max(min_size.rows, size.rows),
|
||||
};
|
||||
try this.container.handle(.{ .resize = this.container_size });
|
||||
},
|
||||
// TODO: other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
|
||||
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
|
||||
.mouse => |mouse| switch (mouse.button) {
|
||||
Mouse.Button.wheel_up => if (this.container_size.rows > this.size.rows) {
|
||||
this.anchor.row -|= 1;
|
||||
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
|
||||
this.anchor.y -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_down => if (this.container_size.rows > this.size.rows) {
|
||||
const max_anchor_row = this.container_size.rows -| this.size.rows;
|
||||
this.anchor.row = @min(this.anchor.row + 1, max_anchor_row);
|
||||
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);
|
||||
},
|
||||
Mouse.Button.wheel_left => if (this.container_size.cols > this.size.cols) {
|
||||
this.anchor.col -|= 1;
|
||||
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
|
||||
this.anchor.x -|= 1;
|
||||
},
|
||||
Mouse.Button.wheel_right => if (this.container_size.cols > this.size.cols) {
|
||||
const max_anchor_col = this.container_size.cols -| this.size.cols;
|
||||
this.anchor.col = @min(this.anchor.col + 1, max_anchor_col);
|
||||
Mouse.Button.wheel_right => if (this.container_size.x > this.size.x) {
|
||||
const max_anchor_x = this.container_size.x -| this.size.x;
|
||||
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
|
||||
},
|
||||
else => try this.container.handle(.{
|
||||
.mouse = .{
|
||||
.col = mouse.col + this.anchor.col,
|
||||
.row = mouse.row + this.anchor.row,
|
||||
.x = mouse.x + this.anchor.x,
|
||||
.y = mouse.y + this.anchor.y,
|
||||
.button = mouse.button,
|
||||
.kind = mouse.kind,
|
||||
},
|
||||
@@ -149,48 +136,209 @@ pub fn Scrollable(Event: type) type {
|
||||
}
|
||||
}
|
||||
|
||||
fn content(ctx: *anyopaque, cells: []Cell, size: Size) !void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||
std.debug.assert(cells.len == @as(usize, this.size.cols) * @as(usize, this.size.rows));
|
||||
fn render_container(container: Container(Event), cells: []Cell, container_size: Point) !void {
|
||||
const size = container.size;
|
||||
const origin = container.origin;
|
||||
const contents = try container.content();
|
||||
defer container.allocator.free(contents);
|
||||
|
||||
const container_size = this.container.size;
|
||||
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
||||
{
|
||||
const container_cells_const = try this.container.contents();
|
||||
defer this.container.allocator.free(container_cells_const);
|
||||
std.debug.assert(container_cells_const.len == @as(usize, container_size.cols) * @as(usize, container_size.rows));
|
||||
@memcpy(container_cells, container_cells_const);
|
||||
}
|
||||
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
|
||||
|
||||
for (this.container.elements.items) |*e| {
|
||||
const e_size = e.size;
|
||||
const element_cells = try e.contents();
|
||||
defer e.allocator.free(element_cells);
|
||||
var idx: usize = 0;
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
cells[anchor + (row * container_size.x) + col] = contents[idx];
|
||||
idx += 1;
|
||||
|
||||
const anchor = (@as(usize, e_size.anchor.row -| container_size.anchor.row) * @as(usize, container_size.cols)) +
|
||||
@as(usize, e_size.anchor.col -| container_size.anchor.col);
|
||||
|
||||
var idx: usize = 0;
|
||||
blk: for (0..e_size.rows) |row| {
|
||||
for (0..e_size.cols) |col| {
|
||||
const cell = element_cells[idx];
|
||||
idx += 1;
|
||||
|
||||
container_cells[anchor + (row * container_size.cols) + col] = cell;
|
||||
|
||||
if (element_cells.len == idx) break :blk;
|
||||
}
|
||||
if (contents.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
|
||||
const anchor = (@as(usize, this.anchor.row) * @as(usize, container_size.cols)) + @as(usize, this.anchor.col);
|
||||
// TODO: render scrollbar according to configuration!
|
||||
for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
cells[(row * size.cols) + col] = container_cells[anchor + (row * container_size.cols) + col];
|
||||
for (container.elements.items) |child| try render_container(child, cells, size);
|
||||
}
|
||||
|
||||
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));
|
||||
|
||||
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));
|
||||
@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| {
|
||||
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
|
||||
}
|
||||
}
|
||||
this.container.allocator.free(container_cells);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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");
|
||||
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) = .{ .container = box };
|
||||
|
||||
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.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.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.bottom.zon"), renderer.screen);
|
||||
}
|
||||
|
||||
test "scrollable horizontal" {
|
||||
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) = .{ .container = box };
|
||||
|
||||
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.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.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.right.zon"), renderer.screen);
|
||||
}
|
||||
|
||||
4
src/error.zig
Normal file
4
src/error.zig
Normal file
@@ -0,0 +1,4 @@
|
||||
pub const Error = error{
|
||||
/// Thrown when a `Container` is too small to be rendered in the current screen part.
|
||||
TooSmall,
|
||||
};
|
||||
@@ -6,12 +6,12 @@ const terminal = @import("terminal.zig");
|
||||
|
||||
const Key = input.Key;
|
||||
const Mouse = input.Mouse;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
/// System events available to every `zterm.App`
|
||||
pub const SystemEvent = union(enum) {
|
||||
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
||||
/// TODO: not sure if this is necessary or if there is an actual usecase for this - for now it will remain
|
||||
/// TODO not sure if this is necessary or if there is an actual usecase for this - for now it will remain
|
||||
init,
|
||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||
quit,
|
||||
@@ -21,14 +21,12 @@ pub const SystemEvent = union(enum) {
|
||||
/// associated error message
|
||||
msg: []const u8,
|
||||
},
|
||||
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
|
||||
resize: Size,
|
||||
/// Input key event received from the user
|
||||
key: Key,
|
||||
/// Mouse input event
|
||||
mouse: Mouse,
|
||||
/// Focus event for mouse interaction
|
||||
/// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for
|
||||
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
|
||||
focus: bool,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
||||
const std = @import("std");
|
||||
|
||||
const Size = @import("size.zig").Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
pub const Mouse = packed struct {
|
||||
col: u16,
|
||||
row: u16,
|
||||
x: u16,
|
||||
y: u16,
|
||||
button: Button,
|
||||
kind: Kind,
|
||||
|
||||
@@ -35,9 +35,9 @@ pub const Mouse = packed struct {
|
||||
return std.meta.eql(this, other);
|
||||
}
|
||||
|
||||
pub fn in(this: @This(), size: Size) bool {
|
||||
return this.col >= size.anchor.col and this.col <= size.cols + size.anchor.col and
|
||||
this.row >= size.anchor.row and this.row <= size.rows + size.anchor.row;
|
||||
pub fn in(this: @This(), origin: Point, size: Point) bool {
|
||||
return this.x >= origin.x and this.x < size.x + origin.x and
|
||||
this.y >= origin.y and this.y < size.y + origin.y;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,7 +86,25 @@ pub const Key = packed struct {
|
||||
pub fn isAscii(this: @This()) bool {
|
||||
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
|
||||
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
|
||||
this.cp >= 128 and this.cp <= 255); // extended ascii codes
|
||||
this.cp >= 128 and this.cp <= 255); // extended ascii codes
|
||||
}
|
||||
|
||||
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 } }));
|
||||
}
|
||||
|
||||
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 } }));
|
||||
}
|
||||
|
||||
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 } }));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -171,8 +189,8 @@ pub const KpLeft: u21 = 57417;
|
||||
pub const KpRight: u21 = 57418;
|
||||
pub const KpUp: u21 = 57419;
|
||||
pub const KpDown: u21 = 57420;
|
||||
pub const KpPage_up: u21 = 57421;
|
||||
pub const KpPage_down: u21 = 57422;
|
||||
pub const KpPageUp: u21 = 57421;
|
||||
pub const KpPageDown: u21 = 57422;
|
||||
pub const KpHome: u21 = 57423;
|
||||
pub const KpEnd: u21 = 57424;
|
||||
pub const KpInsert: u21 = 57425;
|
||||
|
||||
60
src/point.zig
Normal file
60
src/point.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
pub const Point = packed struct {
|
||||
x: u16 = 0,
|
||||
y: u16 = 0,
|
||||
|
||||
pub fn add(a: @This(), b: @This()) @This() {
|
||||
return .{
|
||||
.x = a.x + b.x,
|
||||
.y = a.y + b.y,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn max(a: @This(), b: @This()) @This() {
|
||||
return .{
|
||||
.x = @max(a.x, b.x),
|
||||
.y = @max(a.y, b.y),
|
||||
};
|
||||
}
|
||||
|
||||
test "adding" {
|
||||
const testing = @import("std").testing;
|
||||
|
||||
const a: @This() = .{
|
||||
.x = 10,
|
||||
.y = 20,
|
||||
};
|
||||
|
||||
const b: @This() = .{
|
||||
.x = 20,
|
||||
.y = 10,
|
||||
};
|
||||
|
||||
try testing.expectEqual(@This(){
|
||||
.x = 30,
|
||||
.y = 30,
|
||||
}, a.add(b));
|
||||
}
|
||||
|
||||
test "maximum" {
|
||||
const testing = @import("std").testing;
|
||||
|
||||
const a: @This() = .{
|
||||
.x = 10,
|
||||
.y = 20,
|
||||
};
|
||||
|
||||
const b: @This() = .{
|
||||
.x = 20,
|
||||
.y = 10,
|
||||
};
|
||||
|
||||
try testing.expectEqual(@This(){
|
||||
.x = 20,
|
||||
.y = 20,
|
||||
}, a.max(b));
|
||||
}
|
||||
};
|
||||
|
||||
test {
|
||||
_ = Point;
|
||||
}
|
||||
@@ -2,16 +2,13 @@ const std = @import("std");
|
||||
const terminal = @import("terminal.zig");
|
||||
|
||||
const Cell = @import("cell.zig");
|
||||
const Position = @import("size.zig").Position;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
|
||||
/// Double-buffered intermediate rendering pipeline
|
||||
pub const Buffered = struct {
|
||||
const log = std.log.scoped(.renderer_buffered);
|
||||
// _ = log;
|
||||
allocator: std.mem.Allocator,
|
||||
created: bool,
|
||||
size: Size,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
virtual_screen: []Cell,
|
||||
|
||||
@@ -32,22 +29,26 @@ pub const Buffered = struct {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(this: *@This(), size: Size) !void {
|
||||
log.debug("renderer::resize", .{});
|
||||
defer this.size = size;
|
||||
pub fn resize(this: *@This()) !void {
|
||||
const size = terminal.getTerminalSize();
|
||||
if (std.meta.eql(this.size, size)) return;
|
||||
|
||||
this.size = size;
|
||||
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||
|
||||
if (!this.created) {
|
||||
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
|
||||
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
|
||||
this.created = true;
|
||||
} else {
|
||||
this.allocator.free(this.screen);
|
||||
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
|
||||
this.allocator.free(this.virtual_screen);
|
||||
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
|
||||
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||
@memset(this.virtual_screen, .{});
|
||||
}
|
||||
try this.clear();
|
||||
@@ -55,57 +56,51 @@ pub const Buffered = struct {
|
||||
|
||||
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
||||
pub fn clear(this: *@This()) !void {
|
||||
log.debug("renderer::clear", .{});
|
||||
try terminal.clearScreen();
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
||||
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
||||
const size: Size = container.size;
|
||||
const cells: []const Cell = try container.contents();
|
||||
const size: Point = container.size;
|
||||
const origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
var idx: usize = 0;
|
||||
var vs = this.virtual_screen;
|
||||
const anchor: usize = (@as(usize, size.anchor.row) * @as(usize, this.size.cols)) + @as(usize, size.anchor.col);
|
||||
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||
|
||||
blk: for (0..size.rows) |row| {
|
||||
for (0..size.cols) |col| {
|
||||
const cell = cells[idx];
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
vs[anchor + (row * this.size.x) + col] = cells[idx];
|
||||
idx += 1;
|
||||
|
||||
vs[anchor + (row * this.size.cols) + col].style = cell.style;
|
||||
vs[anchor + (row * this.size.cols) + col].cp = cell.cp;
|
||||
|
||||
if (cells.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
// free immediately
|
||||
container.allocator.free(cells);
|
||||
|
||||
for (container.elements.items) |*element| {
|
||||
try this.render(T, element);
|
||||
}
|
||||
for (container.elements.items) |*element| try this.render(T, element);
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
// TODO: measure timings of rendered frames?
|
||||
log.debug("renderer::flush", .{});
|
||||
// TODO measure timings of rendered frames?
|
||||
const writer = terminal.writer();
|
||||
const s = this.screen;
|
||||
const vs = this.virtual_screen;
|
||||
for (0..this.size.rows) |row| {
|
||||
for (0..this.size.cols) |col| {
|
||||
const idx = (row * this.size.cols) + col;
|
||||
for (0..this.size.y) |row| {
|
||||
for (0..this.size.x) |col| {
|
||||
const idx = (row * this.size.x) + col;
|
||||
const cs = s[idx];
|
||||
const cvs = vs[idx];
|
||||
if (cs.eql(cvs)) continue;
|
||||
|
||||
// render differences found in virtual screen
|
||||
try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) });
|
||||
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
|
||||
try cvs.value(writer);
|
||||
// update screen to be the virtual screen for the next frame
|
||||
s[idx] = vs[idx];
|
||||
|
||||
17
src/size.zig
17
src/size.zig
@@ -1,17 +0,0 @@
|
||||
pub const Size = packed struct {
|
||||
anchor: Position = .{},
|
||||
cols: u16 = 0,
|
||||
rows: u16 = 0,
|
||||
|
||||
pub fn merge(this: @This(), other: @This()) Size {
|
||||
return .{
|
||||
.cols = this.cols + other.cols,
|
||||
.rows = this.rows + other.rows,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
pub const Position = packed struct {
|
||||
col: u16 = 0,
|
||||
row: u16 = 0,
|
||||
};
|
||||
@@ -34,7 +34,7 @@ pub const Emphasis = enum(u8) {
|
||||
strikethrough,
|
||||
};
|
||||
|
||||
fg: Color = .white,
|
||||
fg: Color = .default,
|
||||
bg: Color = .default,
|
||||
ul: Color = .default,
|
||||
ul_style: Underline = .off,
|
||||
@@ -56,17 +56,17 @@ pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
||||
try std.fmt.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
|
||||
// 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 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", .{});
|
||||
// content
|
||||
try std.fmt.format(writer, "{s}", .{buffer});
|
||||
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
|
||||
try std.fmt.format(writer, "\x1b[0m", .{});
|
||||
}
|
||||
|
||||
// TODO: implement helper functions for terminal capabilities:
|
||||
// TODO implement helper functions for terminal capabilities:
|
||||
// - links / url display (osc 8)
|
||||
// - show / hide cursor?
|
||||
|
||||
@@ -4,8 +4,8 @@ const ctlseqs = @import("ctlseqs.zig");
|
||||
const input = @import("input.zig");
|
||||
|
||||
const Key = input.Key;
|
||||
const Position = @import("size.zig").Position;
|
||||
const Size = @import("size.zig").Size;
|
||||
const Point = @import("point.zig").Point;
|
||||
const Size = @import("point.zig").Point;
|
||||
const Cell = @import("cell.zig");
|
||||
|
||||
const log = std.log.scoped(.terminal);
|
||||
@@ -23,7 +23,7 @@ pub const ReportMode = enum {
|
||||
pub fn getTerminalSize() Size {
|
||||
var ws: std.posix.winsize = undefined;
|
||||
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
||||
return .{ .cols = ws.col, .rows = ws.row };
|
||||
return .{ .x = ws.col, .y = ws.row };
|
||||
}
|
||||
|
||||
pub fn saveScreen() !void {
|
||||
@@ -89,9 +89,9 @@ pub fn writer() Writer {
|
||||
return .{ .context = .{} };
|
||||
}
|
||||
|
||||
pub fn setCursorPosition(pos: Position) !void {
|
||||
pub fn setCursorPosition(pos: Point) !void {
|
||||
var buf: [64]u8 = undefined;
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
|
||||
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
|
||||
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ pub fn getCursorPosition() !Size.Position {
|
||||
}
|
||||
|
||||
return .{
|
||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
1
src/test/container/border.all.zon
Normal file
1
src/test/container/border.all.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/border.horizontal.zon
Normal file
1
src/test/container/border.horizontal.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/border.vertical.zon
Normal file
1
src/test/container/border.vertical.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/fixed_grow_horizontal.zon
Normal file
1
src/test/container/fixed_grow_horizontal.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/fixed_grow_vertical.zon
Normal file
1
src/test/container/fixed_grow_vertical.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_gap.zon
Normal file
1
src/test/container/rectangle_with_gap.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_padding.zon
Normal file
1
src/test/container/rectangle_with_padding.zon
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_parent_padding.zon
Normal file
1
src/test/container/rectangle_with_parent_padding.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/rectangle_with_separator.zon
Normal file
1
src/test/container/rectangle_with_separator.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps.zon
Normal file
1
src/test/container/separator_2x_no_gaps.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps_with_border.zon
Normal file
1
src/test/container/separator_2x_no_gaps_with_border.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_2x_no_gaps_with_padding.zon
Normal file
1
src/test/container/separator_2x_no_gaps_with_padding.zon
Normal file
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
1
src/test/container/separator_no_gaps.zon
Normal file
1
src/test/container/separator_no_gaps.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/container/separator_no_gaps_with_padding.zon
Normal file
1
src/test/container/separator_no_gaps_with_padding.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.horizontal.left.zon
Normal file
1
src/test/element/scrollable.horizontal.left.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.horizontal.right.zon
Normal file
1
src/test/element/scrollable.horizontal.right.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.bottom.zon
Normal file
1
src/test/element/scrollable.vertical.bottom.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.top.zon
Normal file
1
src/test/element/scrollable.vertical.top.zon
Normal file
File diff suppressed because one or more lines are too long
201
src/testing.zig
Normal file
201
src/testing.zig
Normal file
@@ -0,0 +1,201 @@
|
||||
//! 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,
|
||||
size: Point,
|
||||
screen: []Cell,
|
||||
|
||||
pub fn init(allocator: std.mem.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, .{});
|
||||
|
||||
return .{
|
||||
.allocator = allocator,
|
||||
.size = size,
|
||||
.screen = screen,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(this: *@This()) void {
|
||||
this.allocator.free(this.screen);
|
||||
}
|
||||
|
||||
pub fn resize(this: *@This(), size: Point) !void {
|
||||
this.size = size;
|
||||
const n = @as(usize, size.x) * @as(usize, size.y);
|
||||
|
||||
this.allocator.free(this.screen);
|
||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
pub fn clear(this: *@This()) !void {
|
||||
@memset(this.screen, .{});
|
||||
}
|
||||
|
||||
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
|
||||
const size: Point = container.size;
|
||||
const origin: Point = container.origin;
|
||||
const cells: []const Cell = try container.content();
|
||||
|
||||
if (cells.len == 0) return;
|
||||
|
||||
var idx: usize = 0;
|
||||
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||
|
||||
blk: for (0..size.y) |row| {
|
||||
for (0..size.x) |col| {
|
||||
const cell = cells[idx];
|
||||
idx += 1;
|
||||
|
||||
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
|
||||
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
|
||||
|
||||
if (cells.len == idx) break :blk;
|
||||
}
|
||||
}
|
||||
// free immediately
|
||||
container.allocator.free(cells);
|
||||
|
||||
for (container.elements.items) |*element| try this.render(T, element);
|
||||
}
|
||||
|
||||
pub fn save(this: @This(), writer: anytype) !void {
|
||||
try std.zon.stringify.serialize(this.screen, .{ .whitespace = false }, writer);
|
||||
}
|
||||
};
|
||||
|
||||
/// This function is intended to be used only in tests. Test if a `Container`'s
|
||||
/// rendered contents are equal to the expected `Cell` slice.
|
||||
///
|
||||
/// # Test data creation
|
||||
///
|
||||
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
|
||||
///
|
||||
/// ```zig
|
||||
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
|
||||
/// defer file.close();
|
||||
///
|
||||
/// const allocator = std.testing.allocator;
|
||||
/// var renderer: testing.Renderer = .init(allocator, size);
|
||||
/// defer renderer.deinit();
|
||||
///
|
||||
/// try container.handle(.{ .size = size });
|
||||
/// try renderer.render(Container(event.SystemEvent), &container);
|
||||
/// try renderer.save(file.writer());
|
||||
/// ```
|
||||
///
|
||||
/// # Testing against created data
|
||||
///
|
||||
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
||||
///
|
||||
/// ```zig
|
||||
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||
/// .border = .{
|
||||
/// .color = .green,
|
||||
/// .sides = .all,
|
||||
/// },
|
||||
/// }, .{});
|
||||
/// defer container.deinit();
|
||||
///
|
||||
/// try testing.expectContainerScreen(.{
|
||||
/// .rows = 20,
|
||||
/// .cols = 30,
|
||||
/// }, &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;
|
||||
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);
|
||||
|
||||
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
|
||||
}
|
||||
|
||||
/// This function is intended to be used only in tests. Test if the two
|
||||
/// provided cell arrays are identical. Usually the `Cell` slices are
|
||||
/// 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;
|
||||
|
||||
try std.testing.expectEqual(expected.len, actual.len);
|
||||
try std.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();
|
||||
|
||||
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
||||
defer actual_cps.deinit();
|
||||
|
||||
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
|
||||
defer output.deinit();
|
||||
|
||||
var buffer = std.io.bufferedWriter(output.writer());
|
||||
defer buffer.flush() catch {};
|
||||
|
||||
const writer = buffer.writer();
|
||||
var differ = false;
|
||||
|
||||
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
|
||||
defer dwd.deinit();
|
||||
const dw: DisplayWidth = .{ .data = &dwd };
|
||||
|
||||
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
|
||||
defer allocator.free(expected_centered);
|
||||
|
||||
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
|
||||
defer allocator.free(actual_centered);
|
||||
|
||||
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
||||
|
||||
for (origin.y..size.y) |row| {
|
||||
defer {
|
||||
expected_cps.clearRetainingCapacity();
|
||||
actual_cps.clearRetainingCapacity();
|
||||
}
|
||||
for (origin.x..size.x) |col| {
|
||||
const expected_cell = expected[(row * size.x) + col];
|
||||
const actual_cell = actual[(row * size.x) + col];
|
||||
|
||||
if (!expected_cell.eql(actual_cell)) differ = true;
|
||||
|
||||
try expected_cps.append(expected_cell);
|
||||
try actual_cps.append(actual_cell);
|
||||
}
|
||||
|
||||
// write screens both formatted to buffer
|
||||
for (expected_cps.items) |cell| try cell.value(writer);
|
||||
_ = try writer.write(" ┆ ");
|
||||
for (actual_cps.items) |cell| try cell.value(writer);
|
||||
_ = try writer.write("\n");
|
||||
}
|
||||
|
||||
if (!differ) return;
|
||||
|
||||
// test failed
|
||||
try buffer.flush();
|
||||
|
||||
std.debug.lockStdErr();
|
||||
defer std.debug.unlockStdErr();
|
||||
|
||||
const std_writer = std.io.getStdErr().writer();
|
||||
try std_writer.writeAll(output.items);
|
||||
return error.TestExpectEqualCells;
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
// private imports
|
||||
const container = @import("container.zig");
|
||||
const color = @import("color.zig");
|
||||
const size = @import("size.zig");
|
||||
const size = @import("point.zig");
|
||||
|
||||
// public exports
|
||||
pub const input = @import("input.zig");
|
||||
pub const testing = @import("testing.zig");
|
||||
|
||||
pub const App = @import("app.zig").App;
|
||||
pub const Error = @import("error.zig").Error;
|
||||
// App also exports further types once initialized with the user events at compile time:
|
||||
// `App.Container`
|
||||
// `App.Element`
|
||||
@@ -22,16 +24,17 @@ pub const Cell = @import("cell.zig");
|
||||
pub const Color = color.Color;
|
||||
pub const Key = input.Key;
|
||||
pub const Mouse = input.Mouse;
|
||||
pub const Position = size.Position;
|
||||
pub const Size = size.Size;
|
||||
pub const Point = @import("point.zig").Point;
|
||||
pub const Style = @import("style.zig");
|
||||
|
||||
test {
|
||||
_ = @import("terminal.zig");
|
||||
_ = @import("container.zig");
|
||||
_ = @import("queue.zig");
|
||||
_ = @import("error.zig");
|
||||
_ = @import("point.zig");
|
||||
|
||||
_ = color;
|
||||
_ = size;
|
||||
|
||||
_ = Cell;
|
||||
_ = Key;
|
||||
|
||||
Reference in New Issue
Block a user