Compare commits
140 Commits
8c130a40d7
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
6781c43fcc
|
|||
| eb36c7c410 | |||
| 4441f1b933 | |||
| 39fb8af174 | |||
|
bfbe75f8d3
|
|||
|
89517b2546
|
|||
|
a71d808250
|
|||
|
97a240c54d
|
|||
| c29c60bd89 | |||
| 4a3bec3edc | |||
|
836d7669e5
|
|||
| 1621715ad8 | |||
| 4874252e8c | |||
| e1e8907848 | |||
| 1cb7fca701 | |||
|
19d1602d3b
|
|||
|
88c7eea356
|
|||
|
b1a0d60ae3
|
|||
|
c49c2a5c6d
|
|||
|
8b5d3757fc
|
|||
|
06ab32bdde
|
|||
|
8e3b43fa61
|
|||
|
6b2797cd8c
|
|||
|
e972a2ea0f
|
|||
|
67bfd90a2c
|
|||
|
40fa080af6
|
|||
|
d92f562a57
|
|||
|
f7025a0fc2
|
|||
|
41229c13d3
|
|||
|
855594a8c8
|
|||
|
9488d0b64d
|
|||
|
424740d350
|
|||
|
28c733352e
|
|||
|
3b5507ec1e
|
|||
|
38d31fae72
|
|||
|
79a0d17a66
|
|||
|
accbb4c97d
|
|||
|
ad32e46bc9
|
|||
|
8ebab702ac
|
|||
|
e53bb7880b
|
|||
|
a83e86f8d9
|
|||
|
4567963ff2
|
|||
|
d4cc520826
|
|||
|
b3dc8096d7
|
|||
|
7cd1fb139f
|
|||
|
f70ea05244
|
|||
|
c645c2efee
|
|||
|
89aeac1e96
|
|||
|
feae9fa1a4
|
|||
|
8f90f57f44
|
|||
|
fee3c796d9
|
|||
|
547f553404
|
|||
|
1f6bbcc45e
|
|||
|
832fc45c3e
|
|||
|
aa17e13b99
|
|||
|
cba07b119c
|
|||
|
f256a79da0
|
|||
|
c50b10f32d
|
|||
|
4f6cabf898
|
|||
|
853f4d9769
|
|||
|
edbca39c38
|
|||
|
0de50e7016
|
|||
|
088e1a9246
|
|||
|
df78c7d6eb
|
|||
|
66b3a77805
|
|||
|
b401b5ece8
|
|||
|
9f33c902ee
|
|||
|
9f29ac6a77
|
|||
|
f775a6ab2d
|
|||
|
a39cee7ccb
|
|||
|
7875db0aea
|
|||
|
7595e3b5bb
|
|||
|
2ba0ed85fb
|
|||
|
439520d4fe
|
|||
|
ded1f2c17e
|
|||
|
92ae8c9681
|
|||
|
743cdca174
|
|||
|
7d8e902ce2
|
|||
|
ed0010c8af
|
|||
|
a8e138deb7
|
|||
|
d0453d08b8
|
|||
|
825fb63bc8
|
|||
|
76f708d9d7
|
|||
|
0d2644f476
|
|||
|
9a818117d7
|
|||
|
5ba5b2b372
|
|||
|
3cb0d11e71
|
|||
|
4cc749facc
|
|||
|
c6d8eec287
|
|||
|
2572b57697
|
|||
|
80a36a9947
|
|||
|
4cde0640c8
|
|||
|
e9a9c2b680
|
|||
|
ba25e6056c
|
|||
|
aa4adf20f9
|
|||
|
50adf32f14
|
|||
|
a4293ff243
|
|||
| 50450f3bbc | |||
| bce134f052 | |||
| 962a384ecf | |||
| 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 |
@@ -15,9 +15,15 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup zig installation
|
- name: Setup zig installation
|
||||||
uses: mlugg/setup-zig@v1
|
uses: https://codeberg.org/mlugg/setup-zig@v2
|
||||||
with:
|
with:
|
||||||
version: master
|
version: latest
|
||||||
|
- name: Lint check
|
||||||
|
run: zig fmt --check .
|
||||||
|
- name: Spell checking
|
||||||
|
uses: crate-ci/typos@v1.39.0
|
||||||
|
with:
|
||||||
|
config: ./.typos-config
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: zig build --release=fast
|
run: zig build --release=fast
|
||||||
- name: Release build artifacts
|
- name: Release build artifacts
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Setup zig installation
|
- name: Setup zig installation
|
||||||
uses: mlugg/setup-zig@v1
|
uses: https://codeberg.org/mlugg/setup-zig@v2
|
||||||
with:
|
with:
|
||||||
version: master
|
version: latest
|
||||||
- name: Lint check
|
- name: Lint check
|
||||||
run: zig fmt --check --exclude src/test .
|
run: zig fmt --check --exclude src/test .
|
||||||
- name: Spell checking
|
- name: Spell checking
|
||||||
uses: crate-ci/typos@v1.25.0
|
uses: crate-ci/typos@v1.39.0
|
||||||
with:
|
with:
|
||||||
config: ./.typos-config
|
config: ./.typos-config
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
|
|||||||
@@ -2,18 +2,15 @@
|
|||||||
|
|
||||||
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
|
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
|
||||||
|
|
||||||
> [!CAUTION]
|
|
||||||
> Only builds using the zig master version are tested to work.
|
|
||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
zig build --release=safe -Dexample=input run
|
zig build --release=safe -Dexample=demo run
|
||||||
```
|
```
|
||||||
|
|
||||||
> [!TIP]
|
> [!tip]
|
||||||
> Every example application can be quit using `ctrl+c`.
|
> 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.
|
See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details.
|
||||||
@@ -33,8 +30,6 @@ const zterm: *Dependency = b.dependency("zterm", .{
|
|||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
});
|
});
|
||||||
// ...
|
|
||||||
exe.root_module.addImport("zterm", zterm.module("zterm"));
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|||||||
191
build.zig
191
build.zig
@@ -1,15 +1,19 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub fn build(b: *std.Build) void {
|
pub fn build(b: *std.Build) void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = b.standardOptimizeOption(.{});
|
||||||
|
|
||||||
const Examples = enum {
|
const Examples = enum {
|
||||||
|
all,
|
||||||
demo,
|
demo,
|
||||||
|
continuous,
|
||||||
// elements:
|
// elements:
|
||||||
|
alignment,
|
||||||
button,
|
button,
|
||||||
input,
|
input,
|
||||||
|
progress,
|
||||||
|
radio_button,
|
||||||
scrollable,
|
scrollable,
|
||||||
|
selection,
|
||||||
// layouts:
|
// layouts:
|
||||||
vertical,
|
vertical,
|
||||||
horizontal,
|
horizontal,
|
||||||
@@ -20,165 +24,88 @@ pub fn build(b: *std.Build) void {
|
|||||||
palette,
|
palette,
|
||||||
// error handling
|
// error handling
|
||||||
errors,
|
errors,
|
||||||
|
// non alternate screen applications
|
||||||
|
direct,
|
||||||
};
|
};
|
||||||
|
|
||||||
const example = b.option(Examples, "example", "Example to build and/or run. (default: demo)") orelse .demo;
|
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
|
||||||
|
const debug_rendering = b.option(bool, "debug", "Enable debug rendering. Highlight origin's, size's, padding's, gap's, etc. (default: false)") orelse false;
|
||||||
|
// NOTE do not support debug rendering in release builds
|
||||||
|
if (debug_rendering == true and optimize != .Debug) @panic("Cannot enable debug rendering in non-debug builds.");
|
||||||
|
|
||||||
const options = b.addOptions();
|
const options = b.addOptions();
|
||||||
options.addOption(Examples, "example", example);
|
options.addOption(bool, "debug", debug_rendering);
|
||||||
|
const options_module = options.createModule();
|
||||||
// dependencies
|
|
||||||
const zg = b.dependency("zg", .{
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
|
|
||||||
// library
|
// library
|
||||||
const lib = b.addModule("zterm", .{
|
const lib = b.addModule("zterm", .{
|
||||||
.root_source_file = b.path("src/zterm.zig"),
|
.root_source_file = b.path("src/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "build_options", .module = options_module },
|
||||||
|
},
|
||||||
});
|
});
|
||||||
lib.addImport("code_point", zg.module("code_point"));
|
|
||||||
|
|
||||||
//--- Examples ---
|
//--- Examples ---
|
||||||
|
const examples = std.meta.fields(Examples);
|
||||||
// demo:
|
inline for (examples) |e| {
|
||||||
|
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
|
||||||
const demo = b.addExecutable(.{
|
const demo = b.addExecutable(.{
|
||||||
.name = "demo",
|
.name = e.name,
|
||||||
.root_source_file = b.path("examples/demo.zig"),
|
.root_module = b.createModule(.{
|
||||||
.target = target,
|
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
|
||||||
.optimize = optimize,
|
.demo => "examples/demo.zig",
|
||||||
});
|
.continuous => "examples/continuous.zig",
|
||||||
demo.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
// elements:
|
// elements:
|
||||||
const button = b.addExecutable(.{
|
.alignment => "examples/elements/alignment.zig",
|
||||||
.name = "button",
|
.button => "examples/elements/button.zig",
|
||||||
.root_source_file = b.path("examples/elements/button.zig"),
|
.input => "examples/elements/input.zig",
|
||||||
.target = target,
|
.progress => "examples/elements/progress.zig",
|
||||||
.optimize = optimize,
|
.radio_button => "examples/elements/radio-button.zig",
|
||||||
});
|
.scrollable => "examples/elements/scrollable.zig",
|
||||||
button.root_module.addImport("zterm", lib);
|
.selection => "examples/elements/selection.zig",
|
||||||
|
|
||||||
const input = b.addExecutable(.{
|
|
||||||
.name = "input",
|
|
||||||
.root_source_file = b.path("examples/elements/input.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
input.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
const scrollable = b.addExecutable(.{
|
|
||||||
.name = "scrollable",
|
|
||||||
.root_source_file = b.path("examples/elements/scrollable.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
scrollable.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
// layouts:
|
// layouts:
|
||||||
const vertical = b.addExecutable(.{
|
.vertical => "examples/layouts/vertical.zig",
|
||||||
.name = "vertical",
|
.horizontal => "examples/layouts/horizontal.zig",
|
||||||
.root_source_file = b.path("examples/layouts/vertical.zig"),
|
.grid => "examples/layouts/grid.zig",
|
||||||
.target = target,
|
.mixed => "examples/layouts/mixed.zig",
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
vertical.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
const horizontal = b.addExecutable(.{
|
|
||||||
.name = "horizontal",
|
|
||||||
.root_source_file = b.path("examples/layouts/horizontal.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
horizontal.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
const grid = b.addExecutable(.{
|
|
||||||
.name = "grid",
|
|
||||||
.root_source_file = b.path("examples/layouts/grid.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
grid.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
const mixed = b.addExecutable(.{
|
|
||||||
.name = "mixed",
|
|
||||||
.root_source_file = b.path("examples/layouts/mixed.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
mixed.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
// styles:
|
// styles:
|
||||||
const palette = b.addExecutable(.{
|
.text => "examples/styles/text.zig",
|
||||||
.name = "palette",
|
.palette => "examples/styles/palette.zig",
|
||||||
.root_source_file = b.path("examples/styles/palette.zig"),
|
// error handling
|
||||||
|
.errors => "examples/errors.zig",
|
||||||
|
// non-alternate screen
|
||||||
|
.direct => "examples/direct.zig",
|
||||||
|
.all => unreachable, // should never happen
|
||||||
|
}),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "zterm", .module = lib },
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
palette.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
const text = b.addExecutable(.{
|
|
||||||
.name = "text",
|
|
||||||
.root_source_file = b.path("examples/styles/text.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
text.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
// error handling:
|
|
||||||
const errors = b.addExecutable(.{
|
|
||||||
.name = "errors",
|
|
||||||
.root_source_file = b.path("examples/errors.zig"),
|
|
||||||
.target = target,
|
|
||||||
.optimize = optimize,
|
|
||||||
});
|
|
||||||
errors.root_module.addImport("zterm", lib);
|
|
||||||
|
|
||||||
// mapping of user selected example to compile step
|
// mapping of user selected example to compile step
|
||||||
const exe = switch (example) {
|
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
|
||||||
.demo => demo,
|
}
|
||||||
// elements:
|
|
||||||
.button => button,
|
|
||||||
.input => input,
|
|
||||||
.scrollable => scrollable,
|
|
||||||
// layouts:
|
|
||||||
.vertical => vertical,
|
|
||||||
.horizontal => horizontal,
|
|
||||||
.grid => grid,
|
|
||||||
.mixed => mixed,
|
|
||||||
// styles:
|
|
||||||
.text => text,
|
|
||||||
.palette => palette,
|
|
||||||
// error handling:
|
|
||||||
.errors => errors,
|
|
||||||
};
|
|
||||||
b.installArtifact(exe);
|
|
||||||
|
|
||||||
// zig build run
|
|
||||||
const run_cmd = b.addRunArtifact(exe);
|
|
||||||
run_cmd.step.dependOn(b.getInstallStep());
|
|
||||||
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
|
|
||||||
if (b.args) |args| run_cmd.addArgs(args);
|
|
||||||
|
|
||||||
// This creates a build step. It will be visible in the `zig build --help` menu,
|
|
||||||
// and can be selected like this: `zig build run`
|
|
||||||
// This will evaluate the `run` step rather than the default, which is "install".
|
|
||||||
const run_step = b.step("run", "Run the app");
|
|
||||||
run_step.dependOn(&run_cmd.step);
|
|
||||||
|
|
||||||
// zig build test
|
// zig build test
|
||||||
const lib_unit_tests = b.addTest(.{
|
const lib_unit_tests = b.addTest(.{
|
||||||
.root_source_file = b.path("src/zterm.zig"),
|
.root_module = b.createModule(.{
|
||||||
|
.root_source_file = b.path("src/root.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
.imports = &.{
|
||||||
|
.{ .name = "build_options", .module = options_module },
|
||||||
|
},
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
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);
|
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
|
||||||
|
|
||||||
const test_step = b.step("test", "Run unit tests");
|
const test_step = b.step("test", "Run unit tests");
|
||||||
test_step.dependOn(&run_lib_unit_tests.step);
|
test_step.dependOn(&run_lib_unit_tests.step);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|||||||
@@ -6,34 +6,40 @@
|
|||||||
//
|
//
|
||||||
// It is redundant to include "zig" in this name because it is already
|
// It is redundant to include "zig" in this name because it is already
|
||||||
// within the Zig package namespace.
|
// 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/).
|
// This is a [Semantic Version](https://semver.org/).
|
||||||
// In a future version of Zig it will be used for package deduplication.
|
// In a future version of Zig it will be used for package deduplication.
|
||||||
.version = "0.0.0",
|
.version = "0.3.0",
|
||||||
|
|
||||||
// This field is optional.
|
// Tracks the earliest Zig version that the package considers to be a
|
||||||
// This is currently advisory only; Zig does not yet do anything
|
// supported use case.
|
||||||
// with this value.
|
.minimum_zig_version = "0.15.0-dev.56+d0911786c",
|
||||||
//.minimum_zig_version = "0.11.0",
|
|
||||||
|
|
||||||
// This field is optional.
|
// This field is optional.
|
||||||
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
// Each dependency must either provide a `url` and `hash`, or a `path`.
|
||||||
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
|
||||||
// Once all dependencies are fetched, `zig build` no longer requires
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
// internet connectivity.
|
// internet connectivity.
|
||||||
.dependencies = .{
|
.dependencies = .{},
|
||||||
.zg = .{
|
|
||||||
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
|
|
||||||
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
.paths = .{
|
.paths = .{
|
||||||
|
"LICENSE",
|
||||||
"build.zig",
|
"build.zig",
|
||||||
"build.zig.zon",
|
"build.zig.zon",
|
||||||
"src",
|
"src",
|
||||||
// For example...
|
|
||||||
//"LICENSE",
|
|
||||||
//"README.md",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
247
examples/continuous.zig
Normal file
247
examples/continuous.zig
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
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, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const row = 2;
|
||||||
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.bg = .black;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Spinner element implementation that runs a simple animation that requires
|
||||||
|
/// the continuous draw loop.
|
||||||
|
const Spinner = struct {
|
||||||
|
counter: u8 = 0,
|
||||||
|
index: u8 = 0,
|
||||||
|
|
||||||
|
const map: [6]u21 = .{ '', '', '', '', '', '' };
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
cells[size.x + 1].cp = map[this.index];
|
||||||
|
|
||||||
|
this.counter += 1;
|
||||||
|
if (this.counter >= 20) {
|
||||||
|
this.index += 1;
|
||||||
|
this.index %= 6;
|
||||||
|
this.counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const InputField = struct {
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
input: std.ArrayList(u21),
|
||||||
|
queue: *App.Queue,
|
||||||
|
|
||||||
|
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
||||||
|
return .{
|
||||||
|
.allocator = allocator,
|
||||||
|
.input = std.ArrayList(u21).initCapacity(allocator, 8) catch unreachable,
|
||||||
|
.queue = queue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(this: *@This()) void {
|
||||||
|
this.input.deinit(this.allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.handle = handle,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
switch (event) {
|
||||||
|
.key => |key| {
|
||||||
|
if (key.isUnicode()) try this.input.append(this.allocator, key.cp);
|
||||||
|
|
||||||
|
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
||||||
|
this.queue.push(.{ .accept = try this.input.toOwnedSlice(this.allocator) });
|
||||||
|
|
||||||
|
if (key.eql(.{ .cp = zterm.input.Backspace }))
|
||||||
|
_ = this.input.pop();
|
||||||
|
|
||||||
|
if (key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
||||||
|
_ = this.input.pop();
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
if (this.input.items.len == 0) return;
|
||||||
|
|
||||||
|
const row = 1;
|
||||||
|
const col = 1;
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (this.input.items, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .black;
|
||||||
|
cells[anchor + idx].style.cursor = false;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
|
||||||
|
if (idx == this.input.items.len - 1) cells[anchor + idx + 1].style.cursor = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(allocator, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var input_field: InputField = .init(allocator, &app.queue);
|
||||||
|
defer input_field.deinit();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
var spinner: Spinner = .{};
|
||||||
|
|
||||||
|
var container = try App.Container.init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
.padding = .all(5),
|
||||||
|
},
|
||||||
|
}, quit_text.element());
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
try container.append(try App.Container.init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal,
|
||||||
|
.dim = .{ .y = 10 },
|
||||||
|
},
|
||||||
|
}, input_field.element()));
|
||||||
|
|
||||||
|
const nested_container: App.Container = try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
}, spinner.element());
|
||||||
|
try container.append(nested_container);
|
||||||
|
|
||||||
|
try app.start(.full);
|
||||||
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
|
var framerate: u64 = 60;
|
||||||
|
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
|
||||||
|
var next_frame_ms: u64 = 0;
|
||||||
|
|
||||||
|
// draw loop
|
||||||
|
draw: while (true) {
|
||||||
|
const now_ms: u64 = @intCast(time.milliTimestamp());
|
||||||
|
if (now_ms >= next_frame_ms) {
|
||||||
|
next_frame_ms = now_ms + tick_ms;
|
||||||
|
} else {
|
||||||
|
std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
|
||||||
|
next_frame_ms += tick_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = blk: {
|
||||||
|
app.queue.lock();
|
||||||
|
defer app.queue.unlock();
|
||||||
|
break :blk app.queue.len();
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle events
|
||||||
|
for (0..len) |_| {
|
||||||
|
const event = app.queue.drain() orelse break;
|
||||||
|
log.debug("handling event: {s}", .{@tagName(event)});
|
||||||
|
// pre event handling
|
||||||
|
switch (event) {
|
||||||
|
.key => |key| {
|
||||||
|
log.debug("key {any}", .{key});
|
||||||
|
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||||
|
},
|
||||||
|
.accept => |input| {
|
||||||
|
defer allocator.free(input);
|
||||||
|
var string = try allocator.alloc(u8, input.len);
|
||||||
|
defer allocator.free(string);
|
||||||
|
for (0.., input) |i, char| string[i] = @intCast(char);
|
||||||
|
log.debug("Accepted input '{s}'", .{string});
|
||||||
|
},
|
||||||
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
|
.focus => |b| {
|
||||||
|
// NOTE reduce framerate in case the window is not focused and restore again when focused
|
||||||
|
framerate = if (b) 60 else 15;
|
||||||
|
tick_ms = @divFloor(time.ms_per_s, framerate);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break :draw,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const time = std.time;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {
|
||||||
|
accept: []u21,
|
||||||
|
});
|
||||||
@@ -1,32 +1,24 @@
|
|||||||
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 QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
|
const text = "Press ctrl+c to quit. Press ctrl+n to launch `vim`.";
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const y = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const x = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (y * size.x) + x;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -35,67 +27,113 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
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.DebugAllocator(.{}) = .init;
|
||||||
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();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var quit_text: QuitText = .{};
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
// TODO: what should the demo application do?
|
// 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?
|
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
|
||||||
// - on the left some buttons?
|
// - on the left some buttons?
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.color = .blue,
|
||||||
|
.sides = .all,
|
||||||
|
},
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 1,
|
.gap = 1,
|
||||||
.padding = .vertical(2),
|
.padding = .vertical(2),
|
||||||
|
.direction = .vertical,
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 90 },
|
||||||
},
|
},
|
||||||
.min_size = .{ .cols = 50 },
|
|
||||||
}, .{});
|
}, .{});
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try box.append(try App.Container.init(allocator, .{
|
try box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer box.deinit();
|
|
||||||
|
|
||||||
var scrollable: App.Scrollable = .{
|
var scrollable: App.Scrollable = .init(box, .disabled);
|
||||||
.container = box,
|
|
||||||
};
|
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{
|
|
||||||
.separator = .{ .enabled = true },
|
|
||||||
},
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 2,
|
.gap = 2,
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
.direction = .horizontal,
|
||||||
},
|
},
|
||||||
}, quit_text.element());
|
}, quit_text.element());
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||||
|
|
||||||
try container.append(try App.Container.init(allocator, .{
|
var nested_container: App.Container = try .init(allocator, .{
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
var inner_container: App.Container = try .init(allocator, .{
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
},
|
||||||
.border = .{
|
.border = .{
|
||||||
.color = .light_blue,
|
.color = .lightblue,
|
||||||
.sides = .all,
|
.sides = .all,
|
||||||
},
|
},
|
||||||
|
}, .{});
|
||||||
|
try inner_container.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{
|
||||||
|
.fill = .blue,
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal,
|
||||||
|
.dim = .{ .y = 5 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
|
try inner_container.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{
|
||||||
|
.fill = .red,
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal,
|
||||||
|
.dim = .{ .y = 5 },
|
||||||
|
},
|
||||||
|
}, .{}));
|
||||||
|
try inner_container.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{
|
||||||
|
.fill = .green,
|
||||||
|
},
|
||||||
|
}, .{}));
|
||||||
|
try nested_container.append(inner_container);
|
||||||
|
try nested_container.append(try .init(allocator, .{
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal,
|
||||||
|
.dim = .{ .y = 1 },
|
||||||
|
},
|
||||||
|
}, .{}));
|
||||||
|
try container.append(nested_container);
|
||||||
try container.append(try App.Container.init(allocator, .{
|
try container.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .x = 30 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -103,17 +141,16 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| {
|
.key => |key| {
|
||||||
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||||
|
|
||||||
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
|
||||||
try app.interrupt();
|
try app.interrupt();
|
||||||
defer app.start() catch @panic("could not start app event loop");
|
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
|
||||||
var child = std.process.Child.init(&.{"hx"}, allocator);
|
defer app.start(.full) catch @panic("could not start app event loop");
|
||||||
|
var child = std.process.Child.init(&.{"vim"}, allocator);
|
||||||
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
_ = child.spawnAndWait() catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
@@ -123,19 +160,37 @@ pub fn main() !void {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// 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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
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(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const input = zterm.input;
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
192
examples/direct.zig
Normal file
192
examples/direct.zig
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
const QuitText = struct {
|
||||||
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.minSize = minSize,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minSize(_: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point {
|
||||||
|
return .{ .x = size.x, .y = 10 }; // this includes the border
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const y = 0;
|
||||||
|
const x = 2;
|
||||||
|
const anchor = (y * size.x) + x;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.emphasis = &.{ .bold, .underline };
|
||||||
|
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 Prompt = struct {
|
||||||
|
len: u16 = 3,
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
// .minSize = minSize,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE size hint is not required as the `.size = .{ .dim = .{..} }` property is set accordingly which denotes the minimal size
|
||||||
|
// fn minSize(ctx: *anyopaque, _: *const App.Model, _: zterm.Point) zterm.Point {
|
||||||
|
// const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
// return .{ .x = this.len, .y = 1 };
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
|
assert(cells.len > 2); // expect at least two cells
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
for (0..this.len) |idx| {
|
||||||
|
cells[idx].style.bg = .blue;
|
||||||
|
cells[idx].style.fg = .black;
|
||||||
|
cells[idx].style.emphasis = &.{.bold};
|
||||||
|
}
|
||||||
|
cells[1].cp = '>';
|
||||||
|
// leave one clear whitespace after the prompt
|
||||||
|
cells[this.len].style.bg = .default;
|
||||||
|
cells[this.len].style.cursor = true; // marks the actual end of the rendering!
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var allocator: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (allocator.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const gpa = allocator.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(gpa, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Direct.init(gpa);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var container: App.Container = try .init(gpa, .{
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
.gap = 1, // show empty line between elements to allow navigation through paragraph jumping
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal_only,
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
var intermediate: App.Container = try .init(gpa, .{
|
||||||
|
.border = .{
|
||||||
|
.sides = .{ .left = true },
|
||||||
|
.color = .grey,
|
||||||
|
},
|
||||||
|
.layout = .{
|
||||||
|
.direction = .horizontal,
|
||||||
|
.padding = .{ .left = 1, .top = 1 },
|
||||||
|
},
|
||||||
|
}, quit_text.element());
|
||||||
|
try intermediate.append(try .init(gpa, .{
|
||||||
|
.rectangle = .{ .fill = .blue },
|
||||||
|
}, .{}));
|
||||||
|
|
||||||
|
try intermediate.append(try .init(gpa, .{
|
||||||
|
.rectangle = .{ .fill = .green },
|
||||||
|
}, .{}));
|
||||||
|
|
||||||
|
var padding_container: App.Container = try .init(gpa, .{
|
||||||
|
.layout = .{
|
||||||
|
.padding = .horizontal(1),
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
try padding_container.append(intermediate);
|
||||||
|
try container.append(padding_container);
|
||||||
|
|
||||||
|
var prompt: Prompt = .{};
|
||||||
|
try container.append(try .init(gpa, .{
|
||||||
|
.rectangle = .{ .fill = .grey },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 1 },
|
||||||
|
},
|
||||||
|
}, prompt.element()));
|
||||||
|
|
||||||
|
try app.start(.direct); // needs to become configurable, as what should be enabled / disabled (i.e. show cursor, hide cursor, use alternate screen, etc.)
|
||||||
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
|
// event loop
|
||||||
|
event: while (true) {
|
||||||
|
// batch events since last iteration
|
||||||
|
const len = blk: {
|
||||||
|
app.queue.poll();
|
||||||
|
app.queue.lock();
|
||||||
|
defer app.queue.unlock();
|
||||||
|
break :blk app.queue.len();
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle events
|
||||||
|
for (0..len) |_| {
|
||||||
|
const event = app.queue.pop();
|
||||||
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
|
switch (event) {
|
||||||
|
// NOTE draw the character with the ctrl indication and a newline to make sure the rendering stays consistent
|
||||||
|
.cancel => try renderer.writeCtrlDWithNewline(),
|
||||||
|
.line => |line| {
|
||||||
|
defer gpa.free(line);
|
||||||
|
log.debug("{s}", .{line});
|
||||||
|
},
|
||||||
|
// 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(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break :event,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there are more events to process continue handling them otherwise I can render the next frame
|
||||||
|
if (app.queue.len() > 0) continue :event;
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const input = zterm.input;
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
99
examples/elements/alignment.zig
Normal file
99
examples/elements/alignment.zig
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
const QuitText = struct {
|
||||||
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const row = 2;
|
||||||
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.bg = .black;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(allocator, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var container: App.Container = try .init(allocator, .{}, .{});
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
const quit_container = try App.Container.init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .blue },
|
||||||
|
.layout = .{
|
||||||
|
.direction = .vertical,
|
||||||
|
.padding = .all(5),
|
||||||
|
},
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .x = 25, .y = 5 },
|
||||||
|
.grow = .fixed,
|
||||||
|
},
|
||||||
|
}, quit_text.element());
|
||||||
|
|
||||||
|
var alignment: App.Alignment = .init(quit_container, .center);
|
||||||
|
try container.append(try .init(allocator, .{}, alignment.element()));
|
||||||
|
|
||||||
|
try app.start(.full);
|
||||||
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
|
// event loop
|
||||||
|
while (true) {
|
||||||
|
const event = app.nextEvent();
|
||||||
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
|
switch (event) {
|
||||||
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
@@ -1,16 +1,34 @@
|
|||||||
const std = @import("std");
|
const QuitText = struct {
|
||||||
const zterm = @import("zterm");
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {
|
pub fn element(this: *@This()) App.Element {
|
||||||
click: [:0]const u8,
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
});
|
}
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
pub const Clickable = struct {
|
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";
|
const text = "Press me";
|
||||||
|
|
||||||
queue: *App.Queue,
|
queue: *App.Queue,
|
||||||
|
color: zterm.Color = .black,
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{
|
return .{
|
||||||
@@ -22,27 +40,35 @@ pub const Clickable = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
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 => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = size.rows / 2 -| (text.len / 2);
|
const row = size.y / 2 -| (text.len / 2);
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
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;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -51,27 +77,32 @@ pub const Clickable = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var clickable: Clickable = .{ .queue = &app.queue };
|
var clickable: Clickable = .{ .queue = &app.queue };
|
||||||
const element = clickable.element();
|
const element = clickable.element();
|
||||||
|
|
||||||
|
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
.layout = .{ .padding = .all(5) },
|
.layout = .{ .padding = .all(5) },
|
||||||
}, .{});
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
defer container.deinit();
|
||||||
|
|
||||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .lightgrey } }, element));
|
||||||
|
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -79,26 +110,45 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
.click => |button| {
|
.click => |b| log.info("Clicked with mouse using Button: {s}", .{b}),
|
||||||
log.info("Clicked with mouse using Button: {s}", .{button});
|
.accept => log.info("Clicked built-in button using the mouse", .{}),
|
||||||
},
|
|
||||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try renderer.render(@TypeOf(container), &container);
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(
|
||||||
|
struct {},
|
||||||
|
union(enum) {
|
||||||
|
click: [:0]const u8,
|
||||||
|
accept,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,26 +1,31 @@
|
|||||||
const std = @import("std");
|
const QuitText = struct {
|
||||||
const zterm = @import("zterm");
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {
|
pub fn element(this: *@This()) App.Element {
|
||||||
accept: []u21,
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
});
|
}
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
pub const InputField = struct {
|
const row = 2;
|
||||||
input: std.ArrayList(u21),
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
queue: *App.Queue,
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
|
for (text, 0..) |cp, idx| {
|
||||||
return .{
|
cells[anchor + idx].style.fg = .white;
|
||||||
.input = .init(allocator),
|
cells[anchor + idx].style.bg = .black;
|
||||||
.queue = queue,
|
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 deinit(this: @This()) void {
|
const MouseDraw = struct {
|
||||||
this.input.deinit();
|
position: ?zterm.Point = null,
|
||||||
}
|
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{
|
return .{
|
||||||
@@ -32,38 +37,22 @@ pub const InputField = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle(ctx: *anyopaque, event: App.Event) !void {
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.key => |key| {
|
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
|
||||||
if (key.isAscii()) try this.input.append(key.cp);
|
else => this.position = null,
|
||||||
|
|
||||||
if (key.eql(.{ .cp = zterm.input.Enter }) or key.eql(.{ .cp = zterm.input.KpEnter }))
|
|
||||||
this.queue.push(.{ .accept = try this.input.toOwnedSlice() });
|
|
||||||
|
|
||||||
if (key.eql(.{ .cp = zterm.input.Backspace }) or key.eql(.{ .cp = zterm.input.Delete }) or key.eql(.{ .cp = zterm.input.KpDelete }))
|
|
||||||
_ = this.input.pop();
|
|
||||||
},
|
|
||||||
else => {},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
|
||||||
|
|
||||||
if (this.input.items.len == 0) return;
|
if (this.position) |pos| {
|
||||||
|
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
|
||||||
const row = 1;
|
cells[idx].cp = 'x';
|
||||||
const col = 1;
|
cells[idx].style.fg = .red;
|
||||||
const anchor = (row * size.cols) + 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`
|
|
||||||
if (anchor + idx == cells.len - 1) break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -71,29 +60,59 @@ pub const InputField = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var input_field: InputField = .init(allocator, &app.queue);
|
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black, .blue));
|
||||||
defer input_field.deinit();
|
var mouse_draw: MouseDraw = .{};
|
||||||
|
var second_mouse_draw: MouseDraw = .{};
|
||||||
const element = input_field.element();
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
.layout = .{ .padding = .all(5) },
|
.layout = .{
|
||||||
}, .{});
|
.direction = .vertical,
|
||||||
|
.padding = .all(5),
|
||||||
|
},
|
||||||
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
defer container.deinit();
|
||||||
|
|
||||||
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
|
try container.append(try App.Container.init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
.size = .{
|
||||||
|
.grow = .horizontal,
|
||||||
|
.dim = .{ .y = 1 },
|
||||||
|
},
|
||||||
|
}, input_field.element()));
|
||||||
|
|
||||||
try app.start();
|
var nested_container: App.Container = try .init(allocator, .{
|
||||||
|
.border = .{
|
||||||
|
.sides = .all,
|
||||||
|
.color = .black,
|
||||||
|
},
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
.layout = .{
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
.color = .black,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, .{});
|
||||||
|
try nested_container.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
}, mouse_draw.element()));
|
||||||
|
try nested_container.append(try .init(allocator, .{
|
||||||
|
.rectangle = .{ .fill = .lightgrey },
|
||||||
|
}, second_mouse_draw.element()));
|
||||||
|
try container.append(nested_container);
|
||||||
|
|
||||||
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -101,27 +120,48 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
.accept => |input| {
|
.accept => |input| {
|
||||||
defer allocator.free(input);
|
defer allocator.free(input);
|
||||||
log.info("Accepted input {any}", .{input});
|
log.debug("Accepted input '{s}'", .{input});
|
||||||
},
|
},
|
||||||
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
try renderer.render(@TypeOf(container), &container);
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const Color = zterm.Color;
|
||||||
|
|
||||||
|
const App = zterm.App(
|
||||||
|
struct {},
|
||||||
|
union(enum) {
|
||||||
|
accept: []u8,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
174
examples/elements/progress.zig
Normal file
174
examples/elements/progress.zig
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
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, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const row = 2;
|
||||||
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.bg = .black;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(allocator, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var progress_percent: u8 = 0;
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
|
var container = try App.Container.init(allocator, .{
|
||||||
|
.layout = .{ .padding = .all(5), .direction = .vertical },
|
||||||
|
}, quit_text.element());
|
||||||
|
defer container.deinit();
|
||||||
|
|
||||||
|
{
|
||||||
|
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||||
|
.percent = .{
|
||||||
|
.enabled = true,
|
||||||
|
.alignment = .left,
|
||||||
|
},
|
||||||
|
.fg = .blue,
|
||||||
|
.bg = .grey,
|
||||||
|
});
|
||||||
|
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||||
|
.percent = .{
|
||||||
|
.enabled = true,
|
||||||
|
.alignment = .middle, // default
|
||||||
|
},
|
||||||
|
.fg = .red,
|
||||||
|
.bg = .grey,
|
||||||
|
});
|
||||||
|
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||||
|
.percent = .{
|
||||||
|
.enabled = true,
|
||||||
|
.alignment = .right,
|
||||||
|
},
|
||||||
|
.fg = .green,
|
||||||
|
.bg = .grey,
|
||||||
|
});
|
||||||
|
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
var progress: App.Progress(.progress) = .init(&app.queue, .{
|
||||||
|
.percent = .{ .enabled = false },
|
||||||
|
.fg = .default,
|
||||||
|
.bg = .grey,
|
||||||
|
});
|
||||||
|
try container.append(try App.Container.init(allocator, .{}, progress.element()));
|
||||||
|
}
|
||||||
|
try app.start(.full);
|
||||||
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
|
var framerate: u64 = 60;
|
||||||
|
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
|
||||||
|
var next_frame_ms: u64 = 0;
|
||||||
|
|
||||||
|
var increase_progress: u64 = 10;
|
||||||
|
|
||||||
|
// Continuous drawing
|
||||||
|
// draw loop
|
||||||
|
draw: while (true) {
|
||||||
|
const now_ms: u64 = @intCast(time.milliTimestamp());
|
||||||
|
if (now_ms >= next_frame_ms) {
|
||||||
|
next_frame_ms = now_ms + tick_ms;
|
||||||
|
} else {
|
||||||
|
std.Thread.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
|
||||||
|
next_frame_ms += tick_ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE time based progress increasion
|
||||||
|
increase_progress -= 1;
|
||||||
|
if (increase_progress == 0) {
|
||||||
|
increase_progress = 10;
|
||||||
|
progress_percent += 1;
|
||||||
|
if (progress_percent > 100) progress_percent = 0;
|
||||||
|
app.postEvent(.{ .progress = progress_percent });
|
||||||
|
}
|
||||||
|
|
||||||
|
const len = blk: {
|
||||||
|
app.queue.lock();
|
||||||
|
defer app.queue.unlock();
|
||||||
|
break :blk app.queue.len();
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle events
|
||||||
|
for (0..len) |_| {
|
||||||
|
const event = app.queue.drain() orelse break;
|
||||||
|
log.debug("handling event: {s}", .{@tagName(event)});
|
||||||
|
// pre event handling
|
||||||
|
switch (event) {
|
||||||
|
.key => |key| {
|
||||||
|
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
|
||||||
|
},
|
||||||
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
|
.focus => |b| {
|
||||||
|
// NOTE reduce framerate in case the window is not focused and restore again when focused
|
||||||
|
framerate = if (b) 60 else 15;
|
||||||
|
tick_ms = @divFloor(time.ms_per_s, framerate);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break :draw,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const time = std.time;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(
|
||||||
|
struct {},
|
||||||
|
union(enum) {
|
||||||
|
progress: u8,
|
||||||
|
},
|
||||||
|
);
|
||||||
98
examples/elements/radio-button.zig
Normal file
98
examples/elements/radio-button.zig
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const row = 2;
|
||||||
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.bg = .black;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(allocator, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var radiobutton: App.RadioButton = .init(false, .{
|
||||||
|
.label = "Test Radio Button",
|
||||||
|
.style = .squared,
|
||||||
|
});
|
||||||
|
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
|
||||||
|
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 .init(allocator, .{}, radiobutton.element()));
|
||||||
|
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.element()));
|
||||||
|
|
||||||
|
try app.start(.full);
|
||||||
|
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(),
|
||||||
|
.accept => log.info("Clicked built-in button using the mouse", .{}),
|
||||||
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {
|
||||||
|
accept,
|
||||||
|
});
|
||||||
@@ -1,12 +1,30 @@
|
|||||||
const std = @import("std");
|
const QuitText = struct {
|
||||||
const zterm = @import("zterm");
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
const input = zterm.input;
|
pub fn element(this: *@This()) App.Element {
|
||||||
const App = zterm.App(union(enum) {});
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
|
}
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
pub const HelloWorldText = packed struct {
|
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";
|
const text = "Hello World";
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
@@ -16,18 +34,21 @@ pub const HelloWorldText = packed struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
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.y / 2;
|
||||||
const row = size.rows / 2;
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const anchor = (row * size.x) + col;
|
||||||
const anchor = (row * size.cols) + col;
|
|
||||||
|
|
||||||
for (0.., text) |idx, char| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .black;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].cp = char;
|
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,8 +56,7 @@ pub const HelloWorldText = packed struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
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.DebugAllocator(.{}) = .init;
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
|
||||||
defer {
|
defer {
|
||||||
const deinit_status = gpa.deinit();
|
const deinit_status = gpa.deinit();
|
||||||
if (deinit_status == .leak) {
|
if (deinit_status == .leak) {
|
||||||
@@ -45,41 +65,61 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
var element_wrapper: HelloWorldText = .{};
|
var element_wrapper: HelloWorldText = .{};
|
||||||
const element = element_wrapper.element();
|
const element = element_wrapper.element();
|
||||||
|
|
||||||
|
var quit_text: QuitText = .{};
|
||||||
|
|
||||||
var top_box = try App.Container.init(allocator, .{
|
var top_box = try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 1,
|
.gap = 2,
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
},
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
.min_size = .{ .rows = 50 },
|
|
||||||
}, .{});
|
}, .{});
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 30 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 5 },
|
||||||
|
},
|
||||||
}, element));
|
}, element));
|
||||||
try top_box.append(try App.Container.init(allocator, .{
|
try top_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .light_green },
|
.rectangle = .{ .fill = .lightgreen },
|
||||||
|
.size = .{
|
||||||
|
.dim = .{ .y = 2 },
|
||||||
|
},
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer top_box.deinit();
|
|
||||||
|
|
||||||
var bottom_box = try App.Container.init(allocator, .{
|
var bottom_box = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
.border = .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.sides = .all,
|
||||||
|
.color = .blue,
|
||||||
|
},
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{
|
||||||
|
.enabled = true,
|
||||||
|
.color = .red,
|
||||||
|
},
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
.padding = .vertical(1),
|
.padding = .vertical(1),
|
||||||
},
|
},
|
||||||
.min_size = .{ .rows = 30 },
|
.size = .{
|
||||||
|
.dim = .{ .y = 30 },
|
||||||
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
try bottom_box.append(try App.Container.init(allocator, .{
|
try bottom_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
@@ -90,31 +130,28 @@ pub fn main() !void {
|
|||||||
try bottom_box.append(try App.Container.init(allocator, .{
|
try bottom_box.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .grey },
|
.rectangle = .{ .fill = .grey },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
defer bottom_box.deinit();
|
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{
|
.layout = .{
|
||||||
|
.gap = 2,
|
||||||
.separator = .{
|
.separator = .{
|
||||||
.enabled = true,
|
.enabled = true,
|
||||||
.line = .double,
|
.line = .double,
|
||||||
},
|
},
|
||||||
},
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.layout = .{
|
|
||||||
.gap = 2,
|
|
||||||
.padding = .all(5),
|
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, quit_text.element());
|
||||||
defer container.deinit();
|
defer container.deinit();
|
||||||
|
|
||||||
// place empty container containing the element of the scrollable Container.
|
// place empty container containing the element of the scrollable Container.
|
||||||
var scrollable_top: App.Scrollable = .init(top_box);
|
var scrollable_top: App.Scrollable = .init(top_box, .enabled(.default, false));
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
|
||||||
|
|
||||||
var scrollable_bottom: App.Scrollable = .init(bottom_box);
|
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white, true));
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -122,22 +159,38 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const input = zterm.input;
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
97
examples/elements/selection.zig
Normal file
97
examples/elements/selection.zig
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
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, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
|
const row = 2;
|
||||||
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
|
for (text, 0..) |cp, idx| {
|
||||||
|
cells[anchor + idx].style.fg = .white;
|
||||||
|
cells[anchor + idx].style.bg = .black;
|
||||||
|
cells[anchor + idx].cp = cp;
|
||||||
|
|
||||||
|
// NOTE do not write over the contents of this `Container`'s `Size`
|
||||||
|
if (anchor + idx == cells.len - 1) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
|
var app: App = .init(allocator, .{}, .{});
|
||||||
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
|
defer renderer.deinit();
|
||||||
|
|
||||||
|
var selection: App.Selection(enum {
|
||||||
|
one,
|
||||||
|
two,
|
||||||
|
three,
|
||||||
|
}) = .init(.{
|
||||||
|
.label = "Selection",
|
||||||
|
});
|
||||||
|
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 .init(allocator, .{}, selection.element()));
|
||||||
|
|
||||||
|
try app.start(.full);
|
||||||
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
|
// event loop
|
||||||
|
while (true) {
|
||||||
|
const event = app.nextEvent();
|
||||||
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
|
switch (event) {
|
||||||
|
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
||||||
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
|
.err = .{
|
||||||
|
.err = err,
|
||||||
|
.msg = "Container Event handling failed",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
|
try renderer.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const Error = zterm.Error;
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -12,20 +5,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,20 +31,20 @@ const InfoText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,31 +57,30 @@ const ErrorNotification = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(ctx: *anyopaque, event: App.Event) !void {
|
fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.key => |key| if (!key.isAscii()) return error.UnsupportedKey,
|
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
|
||||||
.resize => |_| {},
|
|
||||||
.err => |err| this.msg = err.msg,
|
.err => |err| this.msg = err.msg,
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
const this: *@This() = @ptrCast(@alignCast(ctx));
|
const this: *@This() = @ptrCast(@alignCast(ctx));
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
if (this.msg) |msg| {
|
if (this.msg) |msg| {
|
||||||
const row = size.rows -| 2;
|
const row = size.y -| 2;
|
||||||
const col = size.cols -| 2 -| msg.len;
|
const col = size.x -| 2 -| msg.len;
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (msg, 0..) |cp, idx| {
|
for (msg, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,12 +92,12 @@ const ErrorNotification = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -124,29 +116,44 @@ pub fn main() !void {
|
|||||||
try container.append(try App.Container.init(allocator, .{}, info_text.element()));
|
try container.append(try App.Container.init(allocator, .{}, info_text.element()));
|
||||||
try container.append(try App.Container.init(allocator, .{}, error_notification.element()));
|
try container.append(try App.Container.init(allocator, .{}, error_notification.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -15,20 +8,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -50,16 +43,16 @@ pub fn main() !void {
|
|||||||
const element = quit_text.element();
|
const element = quit_text.element();
|
||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
.direction = .horizontal,
|
||||||
},
|
},
|
||||||
}, element);
|
}, element);
|
||||||
for (0..3) |_| {
|
for (0..3) |_| {
|
||||||
var column = try App.Container.init(allocator, .{
|
var column = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.direction = .vertical,
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -76,7 +69,7 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -84,24 +77,39 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
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(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -15,20 +8,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -68,7 +61,7 @@ pub fn main() !void {
|
|||||||
}, .{}));
|
}, .{}));
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -76,24 +69,39 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
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(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -15,20 +8,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -53,13 +46,12 @@ pub fn main() !void {
|
|||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 2,
|
.gap = 2,
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
.direction = .horizontal,
|
|
||||||
},
|
},
|
||||||
}, element);
|
}, element);
|
||||||
for (0..3) |i| {
|
for (0..3) |i| {
|
||||||
var column = try App.Container.init(allocator, .{
|
var column = try App.Container.init(allocator, .{
|
||||||
.border = .{ .separator = .{ .enabled = true } },
|
|
||||||
.layout = .{
|
.layout = .{
|
||||||
|
.separator = .{ .enabled = true },
|
||||||
.direction = if (i > 0) .vertical else .horizontal,
|
.direction = if (i > 0) .vertical else .horizontal,
|
||||||
},
|
},
|
||||||
}, .{});
|
}, .{});
|
||||||
@@ -71,7 +63,12 @@ pub fn main() !void {
|
|||||||
.rectangle = .{ .fill = .yellow },
|
.rectangle = .{ .fill = .yellow },
|
||||||
}, .{}));
|
}, .{}));
|
||||||
} else {
|
} 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, .{
|
try column.append(try App.Container.init(allocator, .{
|
||||||
.rectangle = .{ .fill = .blue },
|
.rectangle = .{ .fill = .blue },
|
||||||
@@ -80,7 +77,7 @@ pub fn main() !void {
|
|||||||
}
|
}
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -88,24 +85,39 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
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(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -15,20 +8,20 @@ const QuitText = struct {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,12 +30,12 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -67,7 +60,7 @@ pub fn main() !void {
|
|||||||
}, .{}));
|
}, .{}));
|
||||||
defer container.deinit(); // also de-initializes the children
|
defer container.deinit(); // also de-initializes the children
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
// event loop
|
// event loop
|
||||||
@@ -75,24 +68,39 @@ pub fn main() !void {
|
|||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
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(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -12,20 +5,20 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,12 +27,12 @@ const QuitText = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -56,40 +49,53 @@ pub fn main() !void {
|
|||||||
|
|
||||||
var box = try App.Container.init(allocator, .{
|
var box = try App.Container.init(allocator, .{
|
||||||
.layout = .{ .direction = .horizontal },
|
.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();
|
|
||||||
|
|
||||||
inline for (std.meta.fields(zterm.Color)) |field| {
|
inline for (std.meta.fields(zterm.Color)) |field| {
|
||||||
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
if (field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||||
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
|
||||||
}
|
}
|
||||||
var scrollable: App.Scrollable = .init(box);
|
var scrollable: App.Scrollable = .init(box, .disabled);
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
const event = app.nextEvent();
|
const event = app.nextEvent();
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
log.debug("received event: {s}", .{@tagName(event)});
|
||||||
|
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
@@ -1,10 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const zterm = @import("zterm");
|
|
||||||
|
|
||||||
const App = zterm.App(union(enum) {});
|
|
||||||
|
|
||||||
const log = std.log.scoped(.default);
|
|
||||||
|
|
||||||
const QuitText = struct {
|
const QuitText = struct {
|
||||||
const text = "Press ctrl+c to quit.";
|
const text = "Press ctrl+c to quit.";
|
||||||
|
|
||||||
@@ -12,66 +5,161 @@ const QuitText = struct {
|
|||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
const row = 2;
|
const row = 2;
|
||||||
const col = size.cols / 2 -| (text.len / 2);
|
const col = size.x / 2 -| (text.len / 2);
|
||||||
const anchor = (row * size.cols) + col;
|
const anchor = (row * size.x) + col;
|
||||||
|
|
||||||
for (text, 0..) |cp, idx| {
|
for (text, 0..) |cp, idx| {
|
||||||
cells[anchor + idx].style.fg = .white;
|
cells[anchor + idx].style.fg = .white;
|
||||||
cells[anchor + idx].style.bg = .black;
|
cells[anchor + idx].style.bg = .black;
|
||||||
cells[anchor + idx].cp = cp;
|
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;
|
if (anchor + idx == cells.len - 1) break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const TableText = struct {
|
||||||
|
pub fn element(this: *@This()) App.Element {
|
||||||
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
_ = ctx;
|
||||||
|
_ = size;
|
||||||
|
var idx: usize = 0;
|
||||||
|
{
|
||||||
|
const text = "Normal ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Bold ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Dim ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Italic ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Underl ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Blink ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Invert ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Hidden ";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const text = "Strikethrough";
|
||||||
|
for (text) |cp| {
|
||||||
|
cells[idx].cp = cp;
|
||||||
|
idx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const TextStyles = struct {
|
const TextStyles = struct {
|
||||||
const text = "Example";
|
const text = "Example";
|
||||||
|
|
||||||
pub fn element(this: *@This()) App.Element {
|
pub fn element(this: *@This()) App.Element {
|
||||||
return .{ .ptr = this, .vtable = &.{ .content = content } };
|
return .{
|
||||||
|
.ptr = this,
|
||||||
|
.vtable = &.{
|
||||||
|
.minSize = minSize,
|
||||||
|
.content = content,
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void {
|
fn minSize(ctx: *anyopaque, _: *const App.Model, size: zterm.Point) zterm.Point {
|
||||||
@setEvalBranchQuota(50000);
|
|
||||||
_ = ctx;
|
_ = ctx;
|
||||||
std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows));
|
_ = size;
|
||||||
|
return .{
|
||||||
|
.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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void {
|
||||||
|
@setEvalBranchQuota(10000);
|
||||||
|
_ = ctx;
|
||||||
|
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
|
||||||
|
|
||||||
var row: usize = 0;
|
var row: usize = 0;
|
||||||
var col: usize = 0;
|
var col: usize = 0;
|
||||||
|
|
||||||
// Color
|
// Color
|
||||||
inline for (std.meta.fields(zterm.Color)) |bg_field| {
|
inline for (std.meta.fields(zterm.Color)) |bg_field| {
|
||||||
if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
if (bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||||
|
|
||||||
inline for (std.meta.fields(zterm.Color)) |fg_field| {
|
inline for (std.meta.fields(zterm.Color)) |fg_field| {
|
||||||
if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
if (fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
|
||||||
if (comptime fg_field.value == bg_field.value) continue;
|
if (fg_field.value == bg_field.value) continue;
|
||||||
|
|
||||||
// witouth any emphasis
|
// witouth any emphasis
|
||||||
for (text) |cp| {
|
for (text) |cp| {
|
||||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||||
cells[(row * size.cols) + col].cp = cp;
|
cells[(row * size.x) + col].cp = cp;
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// emphasis (no combinations)
|
// emphasis (no combinations)
|
||||||
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
|
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
|
||||||
if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
|
if (emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
|
||||||
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
|
||||||
|
|
||||||
for (text) |cp| {
|
for (text) |cp| {
|
||||||
cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value);
|
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
|
||||||
cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value);
|
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
|
||||||
cells[(row * size.cols) + col].style.emphasis = &.{emphasis};
|
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
|
||||||
cells[(row * size.cols) + col].cp = cp;
|
cells[(row * size.x) + col].cp = cp;
|
||||||
col += 1;
|
col += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,12 +173,12 @@ const TextStyles = struct {
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
errdefer |err| log.err("Application Error: {any}", .{err});
|
errdefer |err| log.err("Application Error: {any}", .{err});
|
||||||
|
|
||||||
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
|
var gpa: std.heap.DebugAllocator(.{}) = .init;
|
||||||
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
|
||||||
|
|
||||||
const allocator = gpa.allocator();
|
const allocator = gpa.allocator();
|
||||||
|
|
||||||
var app: App = .init;
|
var app: App = .init(allocator, .{}, .{});
|
||||||
var renderer = zterm.Renderer.Buffered.init(allocator);
|
var renderer = zterm.Renderer.Buffered.init(allocator);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
@@ -101,47 +189,76 @@ pub fn main() !void {
|
|||||||
|
|
||||||
var container = try App.Container.init(allocator, .{
|
var container = try App.Container.init(allocator, .{
|
||||||
.layout = .{
|
.layout = .{
|
||||||
.gap = 2,
|
// .gap = 2,
|
||||||
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
|
||||||
|
.direction = .vertical,
|
||||||
},
|
},
|
||||||
}, element);
|
}, element);
|
||||||
defer container.deinit();
|
defer container.deinit();
|
||||||
|
|
||||||
var box = try App.Container.init(allocator, .{
|
var table_head: TableText = .{};
|
||||||
.layout = .{ .direction = .vertical },
|
try container.append(try .init(allocator, .{
|
||||||
.min_size = .{
|
.size = .{
|
||||||
.rows = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
|
.dim = .{ .y = 1 },
|
||||||
.cols = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
|
.grow = .horizontal,
|
||||||
}, // ensure enough rows and/or columns to render all text styles -> scrollable otherwise
|
},
|
||||||
}, text_styles.element());
|
}, table_head.element()));
|
||||||
defer box.deinit();
|
|
||||||
|
|
||||||
var scrollable: App.Scrollable = .init(box);
|
var scrollable: App.Scrollable = .init(try .init(allocator, .{
|
||||||
|
.layout = .{ .direction = .vertical },
|
||||||
|
}, text_styles.element()), .enabled(.white, true));
|
||||||
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
|
||||||
|
|
||||||
try app.start();
|
try app.start(.full);
|
||||||
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
|
||||||
|
|
||||||
while (true) {
|
// event loop
|
||||||
const event = app.nextEvent();
|
loop: while (true) {
|
||||||
log.debug("received event: {s}", .{@tagName(event)});
|
// batch events since last iteration
|
||||||
|
const len = blk: {
|
||||||
|
app.queue.poll();
|
||||||
|
app.queue.lock();
|
||||||
|
defer app.queue.unlock();
|
||||||
|
break :blk app.queue.len();
|
||||||
|
};
|
||||||
|
|
||||||
|
// handle events
|
||||||
|
for (0..len) |_| {
|
||||||
|
const event = app.queue.pop();
|
||||||
|
log.debug("handling event: {s}", .{@tagName(event)});
|
||||||
|
// pre event handling
|
||||||
switch (event) {
|
switch (event) {
|
||||||
.init => continue,
|
|
||||||
.quit => break,
|
|
||||||
.resize => |size| try renderer.resize(size),
|
|
||||||
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
|
.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 }),
|
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
|
||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
container.handle(event) catch |err| app.postEvent(.{
|
container.handle(&app.model, event) catch |err| app.postEvent(.{
|
||||||
.err = .{
|
.err = .{
|
||||||
.err = err,
|
.err = err,
|
||||||
.msg = "Container Event handling failed",
|
.msg = "Container Event handling failed",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
try renderer.render(@TypeOf(container), &container);
|
|
||||||
|
// post event handling
|
||||||
|
switch (event) {
|
||||||
|
.quit => break :loop,
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container.resize(&app.model, try renderer.resize());
|
||||||
|
container.reposition(&app.model, .{});
|
||||||
|
try renderer.render(@TypeOf(container), &container, App.Model, &app.model);
|
||||||
try renderer.flush();
|
try renderer.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const panic = App.panic_handler;
|
||||||
|
const log = std.log.scoped(.default);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const zterm = @import("zterm");
|
||||||
|
|
||||||
|
const App = zterm.App(struct {}, union(enum) {});
|
||||||
|
|||||||
473
src/app.zig
473
src/app.zig
@@ -1,19 +1,4 @@
|
|||||||
//! Application type for TUI-applications
|
//! Application type for TUI-applications
|
||||||
const std = @import("std");
|
|
||||||
const code_point = @import("code_point");
|
|
||||||
const event = @import("event.zig");
|
|
||||||
const input = @import("input.zig");
|
|
||||||
const terminal = @import("terminal.zig");
|
|
||||||
const queue = @import("queue.zig");
|
|
||||||
|
|
||||||
const mergeTaggedUnions = event.mergeTaggedUnions;
|
|
||||||
const isTaggedUnion = event.isTaggedUnion;
|
|
||||||
|
|
||||||
const Mouse = input.Mouse;
|
|
||||||
const Key = input.Key;
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
const log = std.log.scoped(.app);
|
|
||||||
|
|
||||||
/// Create the App Type with the associated user events _E_ which describes
|
/// Create the App Type with the associated user events _E_ which describes
|
||||||
/// an tagged union for all the user events that can be send through the
|
/// an tagged union for all the user events that can be send through the
|
||||||
@@ -27,91 +12,118 @@ const log = std.log.scoped(.app);
|
|||||||
/// ```zig
|
/// ```zig
|
||||||
/// const zterm = @import("zterm");
|
/// const zterm = @import("zterm");
|
||||||
/// const App = zterm.App(
|
/// const App = zterm.App(
|
||||||
/// union(enum) {},
|
/// struct {}, // empty model
|
||||||
|
/// union(enum) {}, // no additional user event's
|
||||||
/// );
|
/// );
|
||||||
/// // later on create an `App` instance and start the event loop
|
/// // later on create an `App` instance and start the event loop
|
||||||
/// var app: App = .init;
|
/// var app: App = .init(io, .{}); // provide instance of the `std.Io` and `App` model that shall be used
|
||||||
/// try app.start();
|
/// try app.start(.full);
|
||||||
/// defer app.stop() catch unreachable;
|
/// defer app.stop() catch unreachable; // does not clean-up the resources used in the model
|
||||||
/// ```
|
/// ```
|
||||||
pub fn App(comptime E: type) type {
|
pub fn App(comptime M: type, comptime E: type) type {
|
||||||
if (!isTaggedUnion(E)) {
|
if (!isStruct(M)) @compileError("Provided model `M` for `App(comptime M: type, comptime E: type)` is not of type `struct`");
|
||||||
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
|
if (!isTaggedUnion(E)) @compileError("Provided user event `E` for `App(comptime M: type, comptime E: type)` is not of type `union(enum)`.");
|
||||||
}
|
|
||||||
return struct {
|
return struct {
|
||||||
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
|
gpa: Allocator,
|
||||||
pub const Container = @import("container.zig").Container(Event);
|
io: std.Io,
|
||||||
const element = @import("element.zig");
|
model: Model,
|
||||||
pub const Element = element.Element(Event);
|
|
||||||
pub const Scrollable = element.Scrollable(Event);
|
|
||||||
pub const Queue = queue.Queue(Event, 256);
|
|
||||||
|
|
||||||
queue: Queue,
|
queue: Queue,
|
||||||
thread: ?std.Thread,
|
thread: ?Thread = null,
|
||||||
quit_event: std.Thread.ResetEvent,
|
quit_event: Thread.ResetEvent,
|
||||||
termios: ?std.posix.termios = null,
|
termios: ?posix.termios = null,
|
||||||
attached_handler: bool = false,
|
handler_registered: bool = false,
|
||||||
prev_size: Size,
|
config: TerminalConfiguration,
|
||||||
|
|
||||||
pub const SignalHandler = struct {
|
pub const TerminalConfiguration = struct {
|
||||||
context: *anyopaque,
|
altScreen: bool,
|
||||||
callback: *const fn (context: *anyopaque) void,
|
saveScreen: bool,
|
||||||
|
rawMode: bool,
|
||||||
|
hideCursor: bool,
|
||||||
|
|
||||||
|
pub const full: @This() = .{
|
||||||
|
.altScreen = true,
|
||||||
|
.saveScreen = true,
|
||||||
|
.rawMode = true,
|
||||||
|
.hideCursor = true,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const init: @This() = .{
|
pub const direct: @This() = .{
|
||||||
|
.altScreen = false,
|
||||||
|
.saveScreen = false,
|
||||||
|
.rawMode = false,
|
||||||
|
.hideCursor = false,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// global variable for the registered handler for WINCH
|
||||||
|
var handler_ctx: *anyopaque = undefined;
|
||||||
|
/// registered WINCH handler to report resize events
|
||||||
|
fn handleWinch(_: i32) callconv(.c) void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
|
||||||
|
// NOTE this does not have to be done if in-band resize events are supported
|
||||||
|
// -> the signal might not work correctly when hosting the application over ssh!
|
||||||
|
this.postEvent(.resize);
|
||||||
|
}
|
||||||
|
/// registered CONT handler to force a complete redraw
|
||||||
|
fn handleCont(_: i32) callconv(.c) void {
|
||||||
|
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
|
||||||
|
// NOTE this does not have to be done if in-band resize events are supported
|
||||||
|
// -> the signal might not work correctly when hosting the application over ssh!
|
||||||
|
this.postEvent(.resize);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator, io: std.Io, model: Model) @This() {
|
||||||
|
return .{
|
||||||
|
.gpa = gpa,
|
||||||
|
.io = io,
|
||||||
|
.model = model,
|
||||||
.queue = .{},
|
.queue = .{},
|
||||||
.thread = null,
|
|
||||||
.quit_event = .{},
|
.quit_event = .{},
|
||||||
.termios = null,
|
.config = undefined,
|
||||||
.attached_handler = false,
|
|
||||||
.prev_size = .{},
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
pub fn start(this: *@This()) !void {
|
pub fn start(this: *@This(), config: TerminalConfiguration) !void {
|
||||||
|
this.config = config;
|
||||||
if (this.thread) |_| return;
|
if (this.thread) |_| return;
|
||||||
|
|
||||||
if (!this.attached_handler) {
|
|
||||||
var winch_act = std.posix.Sigaction{
|
|
||||||
.handler = .{ .handler = @This().handleWinch },
|
|
||||||
.mask = std.posix.empty_sigset,
|
|
||||||
.flags = 0,
|
|
||||||
};
|
|
||||||
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
|
|
||||||
|
|
||||||
try registerWinch(.{
|
|
||||||
.context = this,
|
|
||||||
.callback = @This().winsizeCallback,
|
|
||||||
});
|
|
||||||
this.attached_handler = true;
|
|
||||||
|
|
||||||
// post init event (as the very first element to be in the queue - event loop)
|
// post init event (as the very first element to be in the queue - event loop)
|
||||||
this.postEvent(.init);
|
this.postEvent(.init);
|
||||||
|
|
||||||
|
if (!this.handler_registered) {
|
||||||
|
handler_ctx = this;
|
||||||
|
posix.sigaction(posix.SIG.WINCH, &.{
|
||||||
|
.handler = .{ .handler = handleWinch },
|
||||||
|
.mask = posix.sigemptyset(),
|
||||||
|
.flags = 0,
|
||||||
|
}, null);
|
||||||
|
posix.sigaction(posix.SIG.CONT, &.{
|
||||||
|
.handler = .{ .handler = handleCont },
|
||||||
|
.mask = posix.sigemptyset(),
|
||||||
|
.flags = 0,
|
||||||
|
}, null);
|
||||||
|
this.handler_registered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.quit_event.reset();
|
this.quit_event.reset();
|
||||||
this.thread = try std.Thread.spawn(.{}, @This().run, .{this});
|
this.thread = try Thread.spawn(.{}, @This().run, .{this});
|
||||||
|
|
||||||
var termios: std.posix.termios = undefined;
|
var termios: posix.termios = undefined;
|
||||||
try terminal.enableRawMode(&termios);
|
if (this.config.rawMode) try terminal.enableRawMode(&termios);
|
||||||
if (this.termios) |_| {} else this.termios = termios;
|
if (this.termios) |_| {} else this.termios = termios;
|
||||||
|
|
||||||
try terminal.saveScreen();
|
if (this.config.altScreen) try terminal.enterAltScreen();
|
||||||
try terminal.enterAltScreen();
|
if (this.config.saveScreen) try terminal.saveScreen();
|
||||||
try terminal.hideCursor();
|
if (this.config.hideCursor) try terminal.hideCursor();
|
||||||
try terminal.enableMouseSupport();
|
if (this.config.altScreen and this.config.rawMode) 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 {
|
pub fn interrupt(this: *@This()) !void {
|
||||||
this.quit_event.set();
|
this.quit_event.set();
|
||||||
try terminal.disableMouseSupport();
|
if (this.config.altScreen and this.config.rawMode) try terminal.disableMouseSupport();
|
||||||
try terminal.exitAltScreen();
|
if (this.config.saveScreen) try terminal.restoreScreen();
|
||||||
try terminal.restoreScreen();
|
if (this.config.altScreen) try terminal.exitAltScreen();
|
||||||
if (this.thread) |thread| {
|
if (this.thread) |*thread| {
|
||||||
thread.join();
|
thread.join();
|
||||||
this.thread = null;
|
this.thread = null;
|
||||||
}
|
}
|
||||||
@@ -119,18 +131,16 @@ pub fn App(comptime E: type) type {
|
|||||||
|
|
||||||
pub fn stop(this: *@This()) !void {
|
pub fn stop(this: *@This()) !void {
|
||||||
try this.interrupt();
|
try this.interrupt();
|
||||||
if (this.termios) |*termios| {
|
if (this.config.hideCursor) try terminal.showCursor();
|
||||||
try terminal.disableMouseSupport();
|
if (this.config.saveScreen) try terminal.resetCursor();
|
||||||
try terminal.showCursor();
|
if (this.termios) |termios| {
|
||||||
try terminal.exitAltScreen();
|
if (this.config.rawMode) try terminal.disableRawMode(&termios);
|
||||||
try terminal.disableRawMode(termios);
|
|
||||||
try terminal.restoreScreen();
|
|
||||||
}
|
|
||||||
this.termios = null;
|
this.termios = null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Quit the application loop.
|
/// Quit the application loop.
|
||||||
/// This will stop the internal input thread and post a **.quit** `Event`.
|
/// This will cancel the internal input thread and post a **.quit** `Event`.
|
||||||
pub fn quit(this: *@This()) void {
|
pub fn quit(this: *@This()) void {
|
||||||
this.quit_event.set();
|
this.quit_event.set();
|
||||||
this.postEvent(.quit);
|
this.postEvent(.quit);
|
||||||
@@ -146,39 +156,54 @@ pub fn App(comptime E: type) type {
|
|||||||
this.queue.push(e);
|
this.queue.push(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var winch_handler: ?SignalHandler = null;
|
|
||||||
|
|
||||||
fn registerWinch(handler: SignalHandler) !void {
|
|
||||||
if (winch_handler) |_| {
|
|
||||||
@panic("Cannot register another WINCH handler.");
|
|
||||||
}
|
|
||||||
winch_handler = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handleWinch(_: c_int) callconv(.C) void {
|
|
||||||
if (winch_handler) |handler| {
|
|
||||||
handler.callback(handler.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run(this: *@This()) !void {
|
fn run(this: *@This()) !void {
|
||||||
// thread to read user inputs
|
// thread to read user inputs
|
||||||
var buf: [256]u8 = undefined;
|
var buf: [512]u8 = undefined;
|
||||||
|
// NOTE set the `NONBLOCK` option for the stdin file, such that reading is not blocking!
|
||||||
|
if (this.config.rawMode) {
|
||||||
|
// TODO is there a better way to do this through the `std.Io` interface?
|
||||||
|
var fl_flags = posix.fcntl(posix.STDIN_FILENO, posix.F.GETFL, 0) catch |err| switch (err) {
|
||||||
|
error.FileBusy => unreachable,
|
||||||
|
error.Locked => unreachable,
|
||||||
|
error.PermissionDenied => unreachable,
|
||||||
|
error.DeadLock => unreachable,
|
||||||
|
error.LockedRegionLimitExceeded => unreachable,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
fl_flags |= 1 << @bitOffsetOf(posix.system.O, "NONBLOCK");
|
||||||
|
_ = posix.fcntl(posix.STDIN_FILENO, posix.F.SETFL, fl_flags) catch |err| switch (err) {
|
||||||
|
error.FileBusy => unreachable,
|
||||||
|
error.Locked => unreachable,
|
||||||
|
error.PermissionDenied => unreachable,
|
||||||
|
error.DeadLock => unreachable,
|
||||||
|
error.LockedRegionLimitExceeded => unreachable,
|
||||||
|
else => |e| return e,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var remaining_bytes: usize = 0;
|
||||||
|
var lines: std.ArrayList(u8) = .empty;
|
||||||
|
defer lines.deinit(this.gpa);
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
// 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 {
|
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
|
// non-blocking read
|
||||||
const read_bytes = try terminal.read(buf[0..]);
|
const read_bytes = terminal.read(buf[remaining_bytes..]) catch |err| switch (err) {
|
||||||
// TODO: `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
|
error.WouldBlock => {
|
||||||
|
// wait a bit
|
||||||
|
std.Thread.sleep(20);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
else => return err,
|
||||||
|
} + remaining_bytes;
|
||||||
|
remaining_bytes = 0;
|
||||||
|
|
||||||
|
if (read_bytes == 0) {
|
||||||
|
// received <EOF>
|
||||||
|
this.postEvent(.cancel);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// escape key presses
|
// escape key presses
|
||||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||||
switch (buf[1]) {
|
switch (buf[1]) {
|
||||||
@@ -205,9 +230,9 @@ pub fn App(comptime E: type) type {
|
|||||||
if (read_bytes < 3) continue;
|
if (read_bytes < 3) continue;
|
||||||
|
|
||||||
// We start iterating at index 2 to get past the '['
|
// We start iterating at index 2 to get past the '['
|
||||||
const sequence = for (buf[2..], 2..) |b, i| {
|
const sequence: []u8 = blk: for (buf[2..], 2..) |b, i| {
|
||||||
switch (b) {
|
switch (b) {
|
||||||
0x40...0xFF => break buf[0 .. i + 1],
|
0x40...0xFF => break :blk buf[0 .. i + 1],
|
||||||
else => continue,
|
else => continue,
|
||||||
}
|
}
|
||||||
} else continue;
|
} else continue;
|
||||||
@@ -218,6 +243,9 @@ pub fn App(comptime E: type) type {
|
|||||||
// Legacy keys
|
// Legacy keys
|
||||||
// CSI {ABCDEFHPQS}
|
// CSI {ABCDEFHPQS}
|
||||||
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
|
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
|
||||||
|
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||||
|
_ = field_iter.next(); // skip first field
|
||||||
|
|
||||||
const key: Key = .{
|
const key: Key = .{
|
||||||
.cp = switch (final) {
|
.cp = switch (final) {
|
||||||
'A' => input.Up,
|
'A' => input.Up,
|
||||||
@@ -231,22 +259,36 @@ pub fn App(comptime E: type) type {
|
|||||||
'Q' => input.F2,
|
'Q' => input.F2,
|
||||||
'R' => input.F3,
|
'R' => input.F3,
|
||||||
'S' => input.F4,
|
'S' => input.F4,
|
||||||
else => unreachable, // switch case prevents in this case form ever happening
|
else => unreachable,
|
||||||
|
},
|
||||||
|
.mod = blk: {
|
||||||
|
// modifier_mask:event_type
|
||||||
|
var mod: Key.Modifier = .{};
|
||||||
|
const field_buf = field_iter.next() orelse break :blk mod;
|
||||||
|
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
|
||||||
|
const modifier_buf = param_iter.next() orelse unreachable;
|
||||||
|
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
|
||||||
|
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
|
||||||
|
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
|
||||||
|
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
|
||||||
|
break :blk mod;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.postEvent(.{ .key = key });
|
this.postEvent(.{ .key = key });
|
||||||
},
|
},
|
||||||
|
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
|
||||||
'~' => {
|
'~' => {
|
||||||
// Legacy keys
|
// Legacy keys
|
||||||
// CSI number ~
|
// CSI number ~
|
||||||
// CSI number ; modifier ~
|
// CSI number ; modifier ~
|
||||||
// CSI number ; modifier:event_type ; text_as_codepoint ~
|
// CSI number ; modifier:event_type ; text_as_codepoint ~
|
||||||
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||||
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
|
||||||
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
|
||||||
|
|
||||||
const key: Key = .{
|
const key: Key = .{
|
||||||
.cp = switch (number) {
|
.cp = blk: {
|
||||||
|
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
||||||
|
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
||||||
|
break :blk switch (number) {
|
||||||
2 => input.Insert,
|
2 => input.Insert,
|
||||||
3 => input.Delete,
|
3 => input.Delete,
|
||||||
5 => input.PageUp,
|
5 => input.PageUp,
|
||||||
@@ -265,26 +307,47 @@ pub fn App(comptime E: type) type {
|
|||||||
21 => input.F10,
|
21 => input.F10,
|
||||||
23 => input.F11,
|
23 => input.F11,
|
||||||
24 => input.F12,
|
24 => input.F12,
|
||||||
|
25 => input.F13,
|
||||||
|
26 => input.F14,
|
||||||
|
28 => input.F15,
|
||||||
|
29 => input.F16,
|
||||||
|
31 => input.F17,
|
||||||
|
32 => input.F18,
|
||||||
|
33 => input.F19,
|
||||||
|
34 => input.F20,
|
||||||
// 200 => return .{ .event = .paste_start, .n = sequence.len },
|
// 200 => return .{ .event = .paste_start, .n = sequence.len },
|
||||||
// 201 => return .{ .event = .paste_end, .n = sequence.len },
|
// 201 => return .{ .event = .paste_end, .n = sequence.len },
|
||||||
57427 => input.KpBegin,
|
57399...57454 => |code| code,
|
||||||
else => unreachable,
|
else => unreachable,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
.mod = blk: {
|
||||||
|
// modifier_mask:event_type
|
||||||
|
var mod: Key.Modifier = .{};
|
||||||
|
const field_buf = field_iter.next() orelse break :blk mod;
|
||||||
|
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
|
||||||
|
const modifier_buf = param_iter.next() orelse unreachable;
|
||||||
|
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
|
||||||
|
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
|
||||||
|
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
|
||||||
|
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
|
||||||
|
break :blk mod;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.postEvent(.{ .key = key });
|
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 }),
|
'I' => this.postEvent(.{ .focus = true }),
|
||||||
'O' => this.postEvent(.{ .focus = false }),
|
'O' => this.postEvent(.{ .focus = false }),
|
||||||
'M', 'm' => {
|
'M', 'm' => {
|
||||||
std.debug.assert(sequence.len >= 4);
|
assert(sequence.len >= 4);
|
||||||
if (sequence[2] != '<') break;
|
if (sequence[2] != '<') break;
|
||||||
|
|
||||||
const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
|
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
|
||||||
const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
|
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
|
||||||
const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
|
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
|
||||||
const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
|
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
|
||||||
const py = std.fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
|
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
|
||||||
|
|
||||||
const mouse_bits = packed struct {
|
const mouse_bits = packed struct {
|
||||||
const motion: u8 = 0b00100000;
|
const motion: u8 = 0b00100000;
|
||||||
@@ -300,22 +363,19 @@ pub fn App(comptime E: type) type {
|
|||||||
// const alt = button_mask & mouse_bits.alt > 0;
|
// const alt = button_mask & mouse_bits.alt > 0;
|
||||||
// const ctrl = button_mask & mouse_bits.ctrl > 0;
|
// const ctrl = button_mask & mouse_bits.ctrl > 0;
|
||||||
|
|
||||||
const mouse: Mouse = .{
|
this.postEvent(.{
|
||||||
|
.mouse = .{
|
||||||
.button = button,
|
.button = button,
|
||||||
.col = px -| 1,
|
.x = px -| 1,
|
||||||
.row = py -| 1,
|
.y = py -| 1,
|
||||||
.kind = blk: {
|
.kind = blk: {
|
||||||
if (motion and button != Mouse.Button.none) {
|
if (motion and button != Mouse.Button.none) break :blk .drag;
|
||||||
break :blk .drag;
|
if (motion and button == Mouse.Button.none) break :blk .motion;
|
||||||
}
|
|
||||||
if (motion and button == Mouse.Button.none) {
|
|
||||||
break :blk .motion;
|
|
||||||
}
|
|
||||||
if (sequence[sequence.len - 1] == 'm') break :blk .release;
|
if (sequence[sequence.len - 1] == 'm') break :blk .release;
|
||||||
break :blk .press;
|
break :blk .press;
|
||||||
},
|
},
|
||||||
};
|
},
|
||||||
this.postEvent(.{ .mouse = mouse });
|
});
|
||||||
},
|
},
|
||||||
'c' => {
|
'c' => {
|
||||||
// Primary DA (CSI ? Pm c)
|
// Primary DA (CSI ? Pm c)
|
||||||
@@ -324,29 +384,26 @@ pub fn App(comptime E: type) type {
|
|||||||
// Device Status Report
|
// Device Status Report
|
||||||
// CSI Ps n
|
// CSI Ps n
|
||||||
// CSI ? Ps n
|
// CSI ? Ps n
|
||||||
std.debug.assert(sequence.len >= 3);
|
assert(sequence.len >= 3);
|
||||||
},
|
},
|
||||||
't' => {
|
't' => {
|
||||||
// XTWINOPS
|
// XTWINOPS
|
||||||
// Split first into fields delimited by ';'
|
// Split first into fields delimited by ';'
|
||||||
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||||
const ps = iter.first();
|
const ps = iter.first();
|
||||||
if (std.mem.eql(u8, "48", ps)) {
|
if (mem.eql(u8, "48", ps)) {
|
||||||
// in band window resize
|
// in band window resize
|
||||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||||
const height_char = iter.next() orelse break;
|
|
||||||
const width_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?
|
_ = width_char;
|
||||||
// because there might be too many resize events (which force a re-draw of the entire screen)
|
_ = height_char;
|
||||||
const size: Size = .{
|
this.postEvent(.resize);
|
||||||
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
// this.postEvent(.{ .size = .{
|
||||||
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||||
};
|
// .y = fmt.parseUnsigned(u16, height_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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'u' => {
|
'u' => {
|
||||||
@@ -361,35 +418,133 @@ pub fn App(comptime E: type) type {
|
|||||||
else => {},
|
else => {},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// TODO: parse corresponding codes
|
0x50 => {
|
||||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
// DCS
|
||||||
else => {},
|
},
|
||||||
|
0x58 => {
|
||||||
|
// SOS
|
||||||
|
},
|
||||||
|
0x5D => {
|
||||||
|
// OSC
|
||||||
|
},
|
||||||
|
// TODO parse corresponding codes
|
||||||
|
0x5F => {
|
||||||
|
// APC
|
||||||
|
// parse for kitty graphics capabilities
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
// alt + <char> keypress
|
||||||
|
this.postEvent(.{
|
||||||
|
.key = .{
|
||||||
|
.cp = buf[1],
|
||||||
|
.mod = .{ .alt = true },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if (this.config.rawMode) {
|
||||||
const b = buf[0];
|
const b = buf[0];
|
||||||
const key: Key = switch (b) {
|
const key: Key = switch (b) {
|
||||||
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
|
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
|
||||||
0x08 => .{ .cp = input.Backspace },
|
0x08 => .{ .cp = input.Backspace },
|
||||||
0x09 => .{ .cp = input.Tab },
|
0x09 => .{ .cp = input.Tab },
|
||||||
0x0a, 0x0d => .{ .cp = input.Enter },
|
0x0a => .{ .cp = 'j', .mod = .{ .ctrl = true } },
|
||||||
|
0x0d => .{ .cp = input.Enter },
|
||||||
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
|
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
|
||||||
0x1b => escape: {
|
0x1b => escape: {
|
||||||
std.debug.assert(read_bytes == 1);
|
assert(read_bytes == 1);
|
||||||
break :escape .{ .cp = input.Escape };
|
break :escape .{ .cp = input.Escape };
|
||||||
},
|
},
|
||||||
0x7f => .{ .cp = input.Backspace },
|
0x7f => .{ .cp = input.Backspace },
|
||||||
else => {
|
else => {
|
||||||
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
|
var len = read_bytes;
|
||||||
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
|
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
|
||||||
continue;
|
remaining_bytes = read_bytes - len;
|
||||||
|
var iter: std.unicode.Utf8Iterator = .{ .bytes = buf[0..len], .i = 0 };
|
||||||
|
while (iter.nextCodepoint()) |cp| this.postEvent(.{ .key = .{ .cp = cp } });
|
||||||
|
if (remaining_bytes > 0) {
|
||||||
|
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
|
||||||
|
}
|
||||||
|
continue; // this switch block does not return a `Key` we continue with loop
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
this.postEvent(.{ .key = key });
|
this.postEvent(.{ .key = key });
|
||||||
|
} else {
|
||||||
|
var len = read_bytes;
|
||||||
|
while (!std.unicode.utf8ValidateSlice(buf[0..len])) len -= 1;
|
||||||
|
remaining_bytes = read_bytes - len;
|
||||||
|
try lines.appendSlice(this.gpa, buf[0..len]);
|
||||||
|
if (remaining_bytes > 0) {
|
||||||
|
@memmove(buf[0..remaining_bytes], buf[len .. len + remaining_bytes]);
|
||||||
|
} else {
|
||||||
|
if (read_bytes != 512 and buf[len - 1] == '\n') this.postEvent(.{ .line = try lines.toOwnedSlice(this.gpa) });
|
||||||
|
// NOTE line did not end with `\n` but with EOF, meaning the user canceled
|
||||||
|
if (read_bytes != 512 and buf[len - 1] != '\n') {
|
||||||
|
lines.clearRetainingCapacity();
|
||||||
|
this.postEvent(.cancel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
|
||||||
|
terminal.disableMouseSupport() catch {};
|
||||||
|
terminal.showCursor() catch {};
|
||||||
|
terminal.resetCursor() catch {};
|
||||||
|
terminal.restoreScreen() catch {};
|
||||||
|
terminal.disableRawMode(&.{
|
||||||
|
.iflag = .{},
|
||||||
|
.lflag = .{},
|
||||||
|
.cflag = .{},
|
||||||
|
.oflag = .{},
|
||||||
|
.cc = undefined,
|
||||||
|
.line = 0,
|
||||||
|
.ispeed = undefined,
|
||||||
|
.ospeed = undefined,
|
||||||
|
}) catch {};
|
||||||
|
terminal.exitAltScreen() catch {};
|
||||||
|
std.debug.defaultPanic(msg, ret_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = @import("element.zig");
|
||||||
|
pub const Model = M;
|
||||||
|
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
|
||||||
|
pub const Container = @import("container.zig").Container(Model, Event);
|
||||||
|
pub const Element = element.Element(Model, Event);
|
||||||
|
pub const Alignment = element.Alignment(Model, Event);
|
||||||
|
pub const Button = element.Button(Model, Event, Queue);
|
||||||
|
pub const TextField = element.TextField(Model, Event);
|
||||||
|
pub const Input = element.Input(Model, Event, Queue);
|
||||||
|
pub const Progress = element.Progress(Model, Event, Queue);
|
||||||
|
pub const RadioButton = element.RadioButton(Model, Event);
|
||||||
|
pub const Scrollable = element.Scrollable(Model, Event);
|
||||||
|
pub const Selection = element.Selection(Model, Event);
|
||||||
|
pub const Queue = queue.Queue(Event, 512);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = std.log.scoped(.app);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const fmt = std.fmt;
|
||||||
|
const posix = std.posix;
|
||||||
|
const Thread = std.Thread;
|
||||||
|
const Allocator = mem.Allocator;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const event = @import("event.zig");
|
||||||
|
const input = @import("input.zig");
|
||||||
|
const terminal = @import("terminal.zig");
|
||||||
|
const queue = @import("queue.zig");
|
||||||
|
const mergeTaggedUnions = event.mergeTaggedUnions;
|
||||||
|
const isTaggedUnion = event.isTaggedUnion;
|
||||||
|
const isStruct = event.isStruct;
|
||||||
|
const Mouse = input.Mouse;
|
||||||
|
const Key = input.Key;
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
|||||||
38
src/cell.zig
38
src/cell.zig
@@ -1,11 +1,7 @@
|
|||||||
const std = @import("std");
|
//! Cell type containing content and formatting for each character in the terminal screen.
|
||||||
const Style = @import("style.zig");
|
|
||||||
|
|
||||||
pub const Cell = @This();
|
|
||||||
|
|
||||||
style: Style = .{ .emphasis = &.{} },
|
|
||||||
// TODO: embrace `zg` dependency more due to utf-8 encoding
|
|
||||||
cp: u21 = ' ',
|
cp: u21 = ' ',
|
||||||
|
style: Style = .{ .emphasis = &.{} },
|
||||||
|
|
||||||
pub fn eql(this: Cell, other: Cell) bool {
|
pub fn eql(this: Cell, other: Cell) bool {
|
||||||
return this.cp == other.cp and this.style.eql(other.style);
|
return this.cp == other.cp and this.style.eql(other.style);
|
||||||
@@ -16,29 +12,31 @@ pub fn reset(this: *Cell) void {
|
|||||||
this.cp = ' ';
|
this.cp = ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(this: Cell, writer: anytype) !void {
|
pub fn value(this: Cell, writer: *std.Io.Writer) !void {
|
||||||
try this.style.value(writer, this.cp);
|
try this.style.value(writer, this.cp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const Style = @import("style.zig");
|
||||||
|
const Cell = @This();
|
||||||
|
|
||||||
test "ascii styled text" {
|
test "ascii styled text" {
|
||||||
const cells: [4]Cell = .{
|
const cells: [4]Cell = .{
|
||||||
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||||
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
|
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
|
||||||
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
|
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
|
||||||
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
.{ .cp = 's', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||||
defer string.deinit();
|
defer writer.deinit();
|
||||||
|
|
||||||
const writer = string.writer();
|
|
||||||
for (cells) |cell| {
|
for (cells) |cell| {
|
||||||
try cell.value(writer);
|
try cell.value(&writer.writer);
|
||||||
}
|
}
|
||||||
try std.testing.expectEqualSlices(
|
try std.testing.expectEqualSlices(
|
||||||
u8,
|
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",
|
"\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,
|
writer.writer.buffer[0..writer.writer.end],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,19 +45,17 @@ test "utf-8 styled text" {
|
|||||||
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
|
||||||
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
|
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
|
||||||
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
|
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
|
||||||
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
|
.{ .cp = '┘', .style = .{ .fg = .lightgreen, .bg = .black, .emphasis = &.{.underline} } },
|
||||||
};
|
};
|
||||||
|
|
||||||
var string = std.ArrayList(u8).init(std.testing.allocator);
|
var writer = std.Io.Writer.Allocating.init(std.testing.allocator);
|
||||||
defer string.deinit();
|
defer writer.deinit();
|
||||||
|
|
||||||
const writer = string.writer();
|
|
||||||
for (cells) |cell| {
|
for (cells) |cell| {
|
||||||
try cell.value(writer);
|
try cell.value(&writer.writer);
|
||||||
}
|
}
|
||||||
try std.testing.expectEqualSlices(
|
try std.testing.expectEqualSlices(
|
||||||
u8,
|
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",
|
"\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,
|
writer.writer.buffer[0..writer.writer.end],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
const std = @import("std");
|
|
||||||
|
|
||||||
pub const Color = enum(u8) {
|
pub const Color = enum(u8) {
|
||||||
default = 0,
|
default = 0,
|
||||||
black = 16,
|
black = 16,
|
||||||
light_red = 1,
|
lightred = 1,
|
||||||
light_green,
|
lightgreen,
|
||||||
light_yellow,
|
lightyellow,
|
||||||
light_blue,
|
lightblue,
|
||||||
light_magenta,
|
lightmagenta,
|
||||||
light_cyan,
|
lightcyan,
|
||||||
light_grey,
|
lightgrey,
|
||||||
grey,
|
grey,
|
||||||
red,
|
red,
|
||||||
green,
|
green,
|
||||||
@@ -18,21 +16,23 @@ pub const Color = enum(u8) {
|
|||||||
magenta,
|
magenta,
|
||||||
cyan,
|
cyan,
|
||||||
white,
|
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 {
|
pub inline fn write(this: Color, writer: *std.Io.Writer, comptime coloring: enum { fg, bg, ul }) !void {
|
||||||
if (this == .default) {
|
if (this == .default) {
|
||||||
switch (coloring) {
|
switch (coloring) {
|
||||||
.fg => try std.fmt.format(writer, "39", .{}),
|
.fg => try writer.printAscii("39", .{}),
|
||||||
.bg => try std.fmt.format(writer, "49", .{}),
|
.bg => try writer.printAscii("49", .{}),
|
||||||
.ul => try std.fmt.format(writer, "59", .{}),
|
.ul => try writer.printAscii("59", .{}),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch (coloring) {
|
switch (coloring) {
|
||||||
.fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
|
.fg => try writer.print("38;5;{d}", .{@intFromEnum(this)}),
|
||||||
.bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
|
.bg => try writer.print("48;5;{d}", .{@intFromEnum(this)}),
|
||||||
.ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
|
.ul => try writer.print("58;5;{d}", .{@intFromEnum(this)}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
|||||||
1188
src/container.zig
1188
src/container.zig
File diff suppressed because it is too large
Load Diff
@@ -50,6 +50,7 @@ pub const home = "\x1b[H";
|
|||||||
pub const cup = "\x1b[{d};{d}H";
|
pub const cup = "\x1b[{d};{d}H";
|
||||||
pub const hide_cursor = "\x1b[?25l";
|
pub const hide_cursor = "\x1b[?25l";
|
||||||
pub const show_cursor = "\x1b[?25h";
|
pub const show_cursor = "\x1b[?25h";
|
||||||
|
pub const reset_cursor_shape = "\x1b[0 q";
|
||||||
pub const cursor_shape = "\x1b[{d} q";
|
pub const cursor_shape = "\x1b[{d} q";
|
||||||
pub const ri = "\x1bM";
|
pub const ri = "\x1bM";
|
||||||
pub const ind = "\n";
|
pub const ind = "\n";
|
||||||
@@ -92,7 +93,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";
|
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
|
||||||
|
|
||||||
// Underlines
|
// 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_single = "\x1b[4m";
|
||||||
pub const ul_double = "\x1b[4:2m";
|
pub const ul_double = "\x1b[4:2m";
|
||||||
pub const ul_curly = "\x1b[4:3m";
|
pub const ul_curly = "\x1b[4:3m";
|
||||||
@@ -139,5 +140,6 @@ pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
|
|||||||
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
|
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
|
||||||
pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg
|
pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg
|
||||||
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
|
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
|
||||||
|
pub const osc12_set = "\x1b]12;{s}\x1b\\"; // set the cursor color through the name of the 8 base colors!
|
||||||
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
|
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
|
||||||
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default
|
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default
|
||||||
|
|||||||
1955
src/element.zig
1955
src/element.zig
File diff suppressed because it is too large
Load Diff
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,
|
||||||
|
};
|
||||||
@@ -1,41 +1,55 @@
|
|||||||
//! Events which are defined by the library. They might be extended by user
|
//! Events which are defined by the library. They might be extended by user
|
||||||
//! events. See `App` for more details about user defined events.
|
//! events. See `App` for more details about user defined events.
|
||||||
const std = @import("std");
|
|
||||||
const input = @import("input.zig");
|
|
||||||
const terminal = @import("terminal.zig");
|
|
||||||
|
|
||||||
const Key = input.Key;
|
|
||||||
const Mouse = input.Mouse;
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
/// System events available to every `zterm.App`
|
/// System events available to every `zterm.App`
|
||||||
pub const SystemEvent = union(enum) {
|
pub const SystemEvent = union(enum) {
|
||||||
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
|
/// 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,
|
init,
|
||||||
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
|
||||||
quit,
|
quit,
|
||||||
|
/// Cancel event to signify that the user provided an EOF
|
||||||
|
///
|
||||||
|
/// Usually this event is only triggered by the system in *non raw mode*
|
||||||
|
/// renderings otherwise the corresponding `.key` event would be fired instead.
|
||||||
|
cancel,
|
||||||
|
/// Resize event to signify that the application should re-draw to resize
|
||||||
|
///
|
||||||
|
/// Usually no `Container` nor `Element` should act on that event, as it
|
||||||
|
/// only serves for event based loops to force a re-draw with a new `Event`.
|
||||||
|
resize,
|
||||||
|
/// Ring the terminal bell to notify the user. This `Event` is handled by
|
||||||
|
/// every `Container` and will not be passed through the container tree.
|
||||||
|
bell,
|
||||||
/// Error event to notify other containers about a recoverable error
|
/// Error event to notify other containers about a recoverable error
|
||||||
err: struct {
|
err: struct {
|
||||||
|
/// actual error
|
||||||
err: anyerror,
|
err: anyerror,
|
||||||
/// associated error message
|
/// associated error message
|
||||||
msg: []const u8,
|
msg: []const u8,
|
||||||
},
|
},
|
||||||
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
|
/// Input line event received in *non raw mode* (instead of individual `key` events)
|
||||||
resize: Size,
|
///
|
||||||
|
/// This event contains the entire line until the ending newline character
|
||||||
|
/// (which is included in the payload of this event).
|
||||||
|
line: []const u8,
|
||||||
/// Input key event received from the user
|
/// Input key event received from the user
|
||||||
key: Key,
|
key: Key,
|
||||||
/// Mouse input event
|
/// Mouse input event
|
||||||
mouse: Mouse,
|
mouse: Mouse,
|
||||||
/// Focus event for mouse interaction
|
/// Focus event indicating that the application has gained the focus of the user
|
||||||
/// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for
|
|
||||||
focus: bool,
|
focus: bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Merge the two provided `union(enum)` `A` and `B` to a tagged union containing all fields of both tagged unions in `comptime`.
|
||||||
|
/// Declarations are not supported for `comptime` created types, see https://github.com/ziglang/zig/issues/6709 for details.
|
||||||
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||||
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
|
// TODO maybe it makes sense to have a nested tagged union type (i.e. system: union(enum) and event: union(enum))
|
||||||
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
|
// - allows re-definition of system / built-in events
|
||||||
}
|
// - clearly shows which events are system / built-in ones and which are user defined events
|
||||||
|
// - the memory footprint for the nesting is not really harmful
|
||||||
|
if (!isTaggedUnion(A) or !isTaggedUnion(B)) @compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
|
||||||
|
|
||||||
const a_fields = @typeInfo(A).@"union".fields;
|
const a_fields = @typeInfo(A).@"union".fields;
|
||||||
const a_fields_tag = @typeInfo(A).@"union".tag_type.?;
|
const a_fields_tag = @typeInfo(A).@"union".tag_type.?;
|
||||||
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
|
const a_enum_fields = @typeInfo(a_fields_tag).@"enum".fields;
|
||||||
@@ -60,11 +74,27 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const log2_i = @bitSizeOf(@TypeOf(i)) - @clz(i);
|
// NOTE declarations are not supported for `comptime` types: https://github.com/ziglang/zig/issues/6709
|
||||||
|
// -> will lead to a compilation error when constructing the tagged union
|
||||||
|
// at the end of this function in case at least one of the provided tagged
|
||||||
|
// unions to merge contains declarations (which in this case can only be the
|
||||||
|
// user provided one)
|
||||||
|
const a_enum_decls = @typeInfo(A).@"union".decls;
|
||||||
|
const b_enum_decls = @typeInfo(B).@"union".decls;
|
||||||
|
var decls: [a_enum_decls.len + b_enum_decls.len]std.builtin.Type.Declaration = undefined;
|
||||||
|
var j: usize = 0;
|
||||||
|
for (a_enum_decls) |decl| {
|
||||||
|
decls[j] = decl;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
for (b_enum_decls) |decl| {
|
||||||
|
decls[j] = decl;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
|
||||||
const EventType = @Type(.{ .int = .{
|
const EventType = @Type(.{ .int = .{
|
||||||
.signedness = .unsigned,
|
.signedness = .unsigned,
|
||||||
.bits = log2_i,
|
.bits = @bitSizeOf(@TypeOf(i)) - @clz(i),
|
||||||
} });
|
} });
|
||||||
|
|
||||||
const Event = @Type(.{ .@"enum" = .{
|
const Event = @Type(.{ .@"enum" = .{
|
||||||
@@ -82,15 +112,28 @@ pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
|||||||
} });
|
} });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine at `comptime` whether the provided type `E` is an `union(enum)`.
|
/// Determine whether the provided type `T` is a tagged union: `union(enum)`.
|
||||||
pub fn isTaggedUnion(comptime E: type) bool {
|
pub fn isTaggedUnion(comptime T: type) bool {
|
||||||
switch (@typeInfo(E)) {
|
switch (@typeInfo(T)) {
|
||||||
.@"union" => |u| {
|
.@"union" => |u| if (u.tag_type) |_| {} else {
|
||||||
if (u.tag_type) |_| {} else {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
},
|
},
|
||||||
else => return false,
|
else => return false,
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determine whether the provided type `T` is a `struct`.
|
||||||
|
pub fn isStruct(comptime T: type) bool {
|
||||||
|
return switch (@typeInfo(T)) {
|
||||||
|
.@"struct" => true,
|
||||||
|
else => false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const input = @import("input.zig");
|
||||||
|
const terminal = @import("terminal.zig");
|
||||||
|
const Key = input.Key;
|
||||||
|
const Mouse = input.Mouse;
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
pub const Mouse = packed struct {
|
pub const Mouse = packed struct {
|
||||||
col: u16,
|
x: u16,
|
||||||
row: u16,
|
y: u16,
|
||||||
button: Button,
|
button: Button,
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
|
|
||||||
@@ -32,12 +29,12 @@ pub const Mouse = packed struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn eql(this: @This(), other: @This()) bool {
|
pub fn eql(this: @This(), other: @This()) bool {
|
||||||
return std.meta.eql(this, other);
|
return meta.eql(this, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn in(this: @This(), size: Size) bool {
|
pub fn in(this: @This(), origin: Point, size: Point) bool {
|
||||||
return this.col >= size.anchor.col and this.col <= size.cols + size.anchor.col and
|
return this.x >= origin.x and this.x < size.x + origin.x and
|
||||||
this.row >= size.anchor.row and this.row <= size.rows + size.anchor.row;
|
this.y >= origin.y and this.y < size.y + origin.y;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -65,7 +62,32 @@ pub const Key = packed struct {
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn eql(this: @This(), other: @This()) bool {
|
pub fn eql(this: @This(), other: @This()) bool {
|
||||||
return std.meta.eql(this, other);
|
return meta.eql(this, other);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine if the `Key` is an unicode character that can be printed to
|
||||||
|
/// the screen. This means that the code point of the `Key` is an ascii
|
||||||
|
/// character between 32 - 255 (with the exception of 127 = Delete) and no
|
||||||
|
/// modifiers (alt and/or ctrl) are used.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Get user input's from the .key event from the application event loop:
|
||||||
|
///
|
||||||
|
/// ```zig
|
||||||
|
/// switch (event) {
|
||||||
|
/// .key => |key| if (key.isUnicode()) try this.input.append(key.cp),
|
||||||
|
/// else => {},
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn isUnicode(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) or // extended ascii codes
|
||||||
|
((this.cp >= 0x0080 and this.cp <= 0x07FF) or
|
||||||
|
(this.cp >= 0x0800 and this.cp <= 0xFFFF) or
|
||||||
|
(this.cp >= 0x100000 and this.cp <= 0x10FFFF)) and // allowed unicode character ranges (2 - 4 byte characters)
|
||||||
|
(this.cp < 57348 or this.cp > 57454); // no other predefined meanings (i.e. arrow keys, etc.)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine if the `Key` is an ascii character that can be printed to
|
/// Determine if the `Key` is an ascii character that can be printed to
|
||||||
@@ -90,24 +112,25 @@ pub const Key = packed struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
test "isAscii with ascii character" {
|
test "isAscii with ascii character" {
|
||||||
try std.testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
|
try testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
|
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
|
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
|
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "isAscii with non-ascii character" {
|
test "isAscii with non-ascii character" {
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Escape }));
|
try testing.expectEqual(false, isAscii(.{ .cp = Escape }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter }));
|
try testing.expectEqual(false, isAscii(.{ .cp = Enter }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
|
try testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "isAscii with excluded input.Delete" {
|
test "isAscii with excluded input.Delete" {
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete }));
|
try testing.expectEqual(false, isAscii(.{ .cp = Delete }));
|
||||||
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
|
try testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: std.ascii has the escape codes too!
|
||||||
// codepoints for keys
|
// codepoints for keys
|
||||||
pub const Tab: u21 = 0x09;
|
pub const Tab: u21 = 0x09;
|
||||||
pub const Enter: u21 = 0x0D;
|
pub const Enter: u21 = 0x0D;
|
||||||
@@ -152,21 +175,6 @@ pub const F17: u21 = 57380;
|
|||||||
pub const F18: u21 = 57381;
|
pub const F18: u21 = 57381;
|
||||||
pub const F19: u21 = 57382;
|
pub const F19: u21 = 57382;
|
||||||
pub const F20: u21 = 57383;
|
pub const F20: u21 = 57383;
|
||||||
pub const F21: u21 = 57384;
|
|
||||||
pub const F22: u21 = 57385;
|
|
||||||
pub const F23: u21 = 57386;
|
|
||||||
pub const F24: u21 = 57387;
|
|
||||||
pub const F25: u21 = 57388;
|
|
||||||
pub const F26: u21 = 57389;
|
|
||||||
pub const F27: u21 = 57390;
|
|
||||||
pub const F28: u21 = 57391;
|
|
||||||
pub const F29: u21 = 57392;
|
|
||||||
pub const F30: u21 = 57393;
|
|
||||||
pub const F31: u21 = 57394;
|
|
||||||
pub const F32: u21 = 57395;
|
|
||||||
pub const F33: u21 = 57396;
|
|
||||||
pub const F34: u21 = 57397;
|
|
||||||
pub const F35: u21 = 57398;
|
|
||||||
pub const Kp0: u21 = 57399;
|
pub const Kp0: u21 = 57399;
|
||||||
pub const Kp1: u21 = 57400;
|
pub const Kp1: u21 = 57400;
|
||||||
pub const Kp2: u21 = 57401;
|
pub const Kp2: u21 = 57401;
|
||||||
@@ -223,3 +231,8 @@ pub const RightHyper: u21 = 57451;
|
|||||||
pub const RightMeta: u21 = 57452;
|
pub const RightMeta: u21 = 57452;
|
||||||
pub const IsoLevel3Shift: u21 = 57453;
|
pub const IsoLevel3Shift: u21 = 57453;
|
||||||
pub const IsoLevel5Shift: u21 = 57454;
|
pub const IsoLevel5Shift: u21 = 57454;
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const meta = std.meta;
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
const testing = std.testing;
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
|
// taken from https://github.com/rockorager/libvaxis/blob/main/src/queue.zig (MIT-License)
|
||||||
// with slight modifications
|
// with slight modifications
|
||||||
const std = @import("std");
|
|
||||||
const assert = std.debug.assert;
|
|
||||||
|
|
||||||
/// Thread safe. Fixed size. Blocking push and pop.
|
/// Queue implementation. Thread safe. Fixed size. _Blocking_ `push` and `pop`. _Polling_ through `tryPop` and `tryPush`.
|
||||||
pub fn Queue(comptime T: type, comptime size: usize) type {
|
pub fn Queue(comptime T: type, comptime size: usize) type {
|
||||||
return struct {
|
return struct {
|
||||||
buf: [size]T = undefined,
|
buf: [size]T = undefined,
|
||||||
@@ -92,6 +90,23 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
|||||||
assert(!this.isEmptyLH());
|
assert(!this.isEmptyLH());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn lock(this: *QueueType) void {
|
||||||
|
this.mutex.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unlock(this: *QueueType) void {
|
||||||
|
this.mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Used to efficiently drain the queue
|
||||||
|
pub fn drain(this: *QueueType) ?T {
|
||||||
|
if (this.isEmptyLH()) return null;
|
||||||
|
|
||||||
|
const result = this.buf[this.mask(this.read_index)];
|
||||||
|
this.read_index = this.mask2(this.read_index + 1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
fn isEmptyLH(this: QueueType) bool {
|
fn isEmptyLH(this: QueueType) bool {
|
||||||
return this.write_index == this.read_index;
|
return this.write_index == this.read_index;
|
||||||
}
|
}
|
||||||
@@ -116,7 +131,7 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the length
|
/// Returns the length
|
||||||
fn len(this: QueueType) usize {
|
pub fn len(this: QueueType) usize {
|
||||||
const wrap_offset = 2 * this.buf.len *
|
const wrap_offset = 2 * this.buf.len *
|
||||||
@intFromBool(this.write_index < this.read_index);
|
@intFromBool(this.write_index < this.read_index);
|
||||||
const adjusted_write_index = this.write_index + wrap_offset;
|
const adjusted_write_index = this.write_index + wrap_offset;
|
||||||
@@ -135,8 +150,12 @@ pub fn Queue(comptime T: type, comptime size: usize) type {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Thread = std.Thread;
|
||||||
const cfg = Thread.SpawnConfig{ .allocator = testing.allocator };
|
const cfg = Thread.SpawnConfig{ .allocator = testing.allocator };
|
||||||
|
|
||||||
test "Queue: simple push / pop" {
|
test "Queue: simple push / pop" {
|
||||||
var queue: Queue(u8, 16) = .{};
|
var queue: Queue(u8, 16) = .{};
|
||||||
queue.push(1);
|
queue.push(1);
|
||||||
@@ -146,7 +165,6 @@ test "Queue: simple push / pop" {
|
|||||||
try testing.expectEqual(2, queue.pop());
|
try testing.expectEqual(2, queue.pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
const Thread = std.Thread;
|
|
||||||
fn testPushPop(q: *Queue(u8, 2)) !void {
|
fn testPushPop(q: *Queue(u8, 2)) !void {
|
||||||
q.push(3);
|
q.push(3);
|
||||||
try testing.expectEqual(2, q.pop());
|
try testing.expectEqual(2, q.pop());
|
||||||
@@ -197,9 +215,9 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
|||||||
// still full and the push in the other thread is still blocked
|
// still full and the push in the other thread is still blocked
|
||||||
// waiting for space.
|
// waiting for space.
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s);
|
std.Thread.sleep(std.time.ns_per_s);
|
||||||
// Finally, let that other thread go.
|
// Finally, let that other thread go.
|
||||||
try std.testing.expectEqual(1, q.pop());
|
try testing.expectEqual(1, q.pop());
|
||||||
|
|
||||||
// This won't continue until the other thread has had a chance to
|
// This won't continue until the other thread has had a chance to
|
||||||
// put at least one item in the queue.
|
// put at least one item in the queue.
|
||||||
@@ -207,7 +225,7 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
|||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
// But we want to ensure that there's a second push waiting, so
|
// But we want to ensure that there's a second push waiting, so
|
||||||
// here's another sleep.
|
// here's another sleep.
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
|
||||||
// Another spurious wake...
|
// Another spurious wake...
|
||||||
q.not_full.signal();
|
q.not_full.signal();
|
||||||
@@ -215,10 +233,10 @@ fn sleepyPop(q: *Queue(u8, 2)) !void {
|
|||||||
// And another chance for the other thread to see that it's
|
// And another chance for the other thread to see that it's
|
||||||
// spurious and go back to sleep.
|
// spurious and go back to sleep.
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
|
||||||
// Pop that thing and we're done.
|
// Pop that thing and we're done.
|
||||||
try std.testing.expectEqual(2, q.pop());
|
try testing.expectEqual(2, q.pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Fill, block, fill, block" {
|
test "Fill, block, fill, block" {
|
||||||
@@ -238,28 +256,28 @@ test "Fill, block, fill, block" {
|
|||||||
|
|
||||||
// Just to make sure the sleeps are yielding to this thread, make
|
// Just to make sure the sleeps are yielding to this thread, make
|
||||||
// sure it took at least 900ms to do the push.
|
// sure it took at least 900ms to do the push.
|
||||||
try std.testing.expect(then - now > 900);
|
try testing.expect(then - now > 900);
|
||||||
|
|
||||||
// This should block again, waiting for the other thread.
|
// This should block again, waiting for the other thread.
|
||||||
queue.push(4);
|
queue.push(4);
|
||||||
|
|
||||||
// And once that push has gone through, the other thread's done.
|
// And once that push has gone through, the other thread's done.
|
||||||
thread.join();
|
thread.join();
|
||||||
try std.testing.expectEqual(3, queue.pop());
|
try testing.expectEqual(3, queue.pop());
|
||||||
try std.testing.expectEqual(4, queue.pop());
|
try testing.expectEqual(4, queue.pop());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sleepyPush(q: *Queue(u8, 1)) !void {
|
fn sleepyPush(q: *Queue(u8, 1)) !void {
|
||||||
// Try to ensure the other thread has already started trying to pop.
|
// Try to ensure the other thread has already started trying to pop.
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
|
||||||
// Spurious wake
|
// Spurious wake
|
||||||
q.not_full.signal();
|
q.not_full.signal();
|
||||||
q.not_empty.signal();
|
q.not_empty.signal();
|
||||||
|
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
|
||||||
// Stick something in the queue so it can be popped.
|
// Stick something in the queue so it can be popped.
|
||||||
q.push(1);
|
q.push(1);
|
||||||
@@ -268,7 +286,7 @@ fn sleepyPush(q: *Queue(u8, 1)) !void {
|
|||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
// Give the other thread time to block again.
|
// Give the other thread time to block again.
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
|
|
||||||
// Spurious wake
|
// Spurious wake
|
||||||
q.not_full.signal();
|
q.not_full.signal();
|
||||||
@@ -284,8 +302,8 @@ test "Drain, block, drain, block" {
|
|||||||
|
|
||||||
var queue: Queue(u8, 1) = .{};
|
var queue: Queue(u8, 1) = .{};
|
||||||
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
|
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
|
||||||
try std.testing.expectEqual(1, queue.pop());
|
try testing.expectEqual(1, queue.pop());
|
||||||
try std.testing.expectEqual(2, queue.pop());
|
try testing.expectEqual(2, queue.pop());
|
||||||
thread.join();
|
thread.join();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +317,7 @@ test "2 readers" {
|
|||||||
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
|
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
|
||||||
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
|
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
|
||||||
try Thread.yield();
|
try Thread.yield();
|
||||||
std.time.sleep(std.time.ns_per_s / 2);
|
std.Thread.sleep(std.time.ns_per_s / 2);
|
||||||
queue.push(1);
|
queue.push(1);
|
||||||
queue.push(1);
|
queue.push(1);
|
||||||
t1.join();
|
t1.join();
|
||||||
|
|||||||
170
src/render.zig
170
src/render.zig
@@ -1,19 +1,14 @@
|
|||||||
const std = @import("std");
|
//! Renderer for `zterm`.
|
||||||
const terminal = @import("terminal.zig");
|
|
||||||
|
|
||||||
const Cell = @import("cell.zig");
|
|
||||||
const Position = @import("size.zig").Position;
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
/// Double-buffered intermediate rendering pipeline
|
/// Double-buffered intermediate rendering pipeline
|
||||||
pub const Buffered = struct {
|
pub const Buffered = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: Allocator,
|
||||||
created: bool,
|
created: bool,
|
||||||
size: Size,
|
size: Point,
|
||||||
screen: []Cell,
|
screen: []Cell,
|
||||||
virtual_screen: []Cell,
|
virtual_screen: []Cell,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator) @This() {
|
pub fn init(allocator: Allocator) @This() {
|
||||||
return .{
|
return .{
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
.created = false,
|
.created = false,
|
||||||
@@ -30,9 +25,12 @@ pub const Buffered = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(this: *@This(), size: Size) !void {
|
pub fn resize(this: *@This()) !Point {
|
||||||
|
const size = terminal.getTerminalSize();
|
||||||
|
if (meta.eql(this.size, size)) return this.size;
|
||||||
|
|
||||||
this.size = size;
|
this.size = size;
|
||||||
const n = @as(usize, size.cols) * @as(usize, size.rows);
|
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||||
|
|
||||||
if (!this.created) {
|
if (!this.created) {
|
||||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
|
||||||
@@ -50,6 +48,7 @@ pub const Buffered = struct {
|
|||||||
@memset(this.virtual_screen, .{});
|
@memset(this.virtual_screen, .{});
|
||||||
}
|
}
|
||||||
try this.clear();
|
try this.clear();
|
||||||
|
return size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
|
||||||
@@ -59,52 +58,165 @@ pub const Buffered = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
|
||||||
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
|
pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void {
|
||||||
const size: Size = container.size;
|
const size: Point = container.size;
|
||||||
const cells: []const Cell = try container.contents();
|
const origin: Point = container.origin;
|
||||||
|
const cells: []const Cell = try container.content(model);
|
||||||
|
|
||||||
if (cells.len == 0) return;
|
if (cells.len == 0) return;
|
||||||
|
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
var vs = this.virtual_screen;
|
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| {
|
blk: for (0..size.y) |row| {
|
||||||
for (0..size.cols) |col| {
|
for (0..size.x) |col| {
|
||||||
const cell = cells[idx];
|
vs[anchor + (row * this.size.x) + col] = cells[idx];
|
||||||
idx += 1;
|
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;
|
if (cells.len == idx) break :blk;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// free immediately
|
// free immediately
|
||||||
container.allocator.free(cells);
|
container.allocator.free(cells);
|
||||||
|
|
||||||
for (container.elements.items) |*element| try this.render(T, element);
|
for (container.elements.items) |*element| try this.render(Container, element, Model, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
|
/// 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 {
|
pub fn flush(this: *@This()) !void {
|
||||||
// TODO: measure timings of rendered frames?
|
try terminal.hideCursor();
|
||||||
const writer = terminal.writer();
|
// TODO measure timings of rendered frames?
|
||||||
|
var cursor_position: ?Point = null;
|
||||||
|
var writer = terminal.writer();
|
||||||
const s = this.screen;
|
const s = this.screen;
|
||||||
const vs = this.virtual_screen;
|
const vs = this.virtual_screen;
|
||||||
for (0..this.size.rows) |row| {
|
|
||||||
for (0..this.size.cols) |col| {
|
for (0..this.size.y) |row| {
|
||||||
const idx = (row * this.size.cols) + col;
|
for (0..this.size.x) |col| {
|
||||||
|
const idx = (row * this.size.x) + col;
|
||||||
const cs = s[idx];
|
const cs = s[idx];
|
||||||
const cvs = vs[idx];
|
const cvs = vs[idx];
|
||||||
|
|
||||||
|
// update the latest found cursor position
|
||||||
|
if (cvs.style.cursor) {
|
||||||
|
assert(cursor_position == null);
|
||||||
|
cursor_position = .{
|
||||||
|
.x = @truncate(col),
|
||||||
|
.y = @truncate(row),
|
||||||
|
};
|
||||||
|
try cvs.style.set_cursor_style(&writer);
|
||||||
|
}
|
||||||
|
|
||||||
if (cs.eql(cvs)) continue;
|
if (cs.eql(cvs)) continue;
|
||||||
|
|
||||||
// render differences found in virtual screen
|
// render differences found in virtual screen
|
||||||
try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) });
|
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
|
||||||
try cvs.value(writer);
|
try cvs.value(&writer);
|
||||||
// update screen to be the virtual screen for the next frame
|
// update screen to be the virtual screen for the next frame
|
||||||
s[idx] = vs[idx];
|
s[idx] = vs[idx];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (cursor_position) |point| {
|
||||||
|
try terminal.showCursor();
|
||||||
|
try terminal.setCursorPosition(point);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const Direct = struct {
|
||||||
|
gpa: Allocator,
|
||||||
|
size: Point,
|
||||||
|
resized: bool,
|
||||||
|
screen: []Cell,
|
||||||
|
|
||||||
|
pub fn init(gpa: Allocator) @This() {
|
||||||
|
return .{
|
||||||
|
.gpa = gpa,
|
||||||
|
.size = .{},
|
||||||
|
.resized = true,
|
||||||
|
.screen = undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(this: *@This()) void {
|
||||||
|
this.gpa.free(this.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resize(this: *@This()) !Point {
|
||||||
|
this.size = .{};
|
||||||
|
if (!this.resized) {
|
||||||
|
this.gpa.free(this.screen);
|
||||||
|
this.screen = undefined;
|
||||||
|
}
|
||||||
|
this.resized = true;
|
||||||
|
return terminal.getTerminalSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(this: *@This()) !void {
|
||||||
|
_ = this;
|
||||||
|
try terminal.clearScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeCtrlDWithNewline(this: *@This()) !void {
|
||||||
|
_ = this;
|
||||||
|
_ = try terminal.write("^D\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn writeNewline(this: *@This()) !void {
|
||||||
|
_ = this;
|
||||||
|
_ = try terminal.write("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render provided cells at size (anchor and dimension) into the *screen*.
|
||||||
|
pub fn render(this: *@This(), comptime Container: type, container: *Container, comptime Model: type, model: *const Model) !void {
|
||||||
|
const size: Point = container.size;
|
||||||
|
const origin: Point = container.origin;
|
||||||
|
|
||||||
|
if (this.resized) {
|
||||||
|
this.size = size;
|
||||||
|
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
|
||||||
|
this.screen = try this.gpa.alloc(Cell, n);
|
||||||
|
@memset(this.screen, .{});
|
||||||
|
this.resized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cells: []const Cell = try container.content(model);
|
||||||
|
|
||||||
|
var idx: usize = 0;
|
||||||
|
var vs = this.screen;
|
||||||
|
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||||
|
|
||||||
|
blk: for (0..size.y) |row| {
|
||||||
|
for (0..size.x) |col| {
|
||||||
|
vs[anchor + (row * this.size.x) + col] = cells[idx];
|
||||||
|
idx += 1;
|
||||||
|
|
||||||
|
if (cells.len == idx) break :blk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// free immediately
|
||||||
|
container.allocator.free(cells);
|
||||||
|
for (container.elements.items) |*element| try this.render(Container, element, Model, model);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn flush(this: *@This()) !void {
|
||||||
|
var writer = terminal.writer();
|
||||||
|
for (0..this.size.y) |row| {
|
||||||
|
for (0..this.size.x) |col| {
|
||||||
|
const idx = (row * this.size.x) + col;
|
||||||
|
const cvs = this.screen[idx];
|
||||||
|
try cvs.value(&writer);
|
||||||
|
if (cvs.style.cursor) return; // that's where the cursor should be left!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const meta = std.meta;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const terminal = @import("terminal.zig");
|
||||||
|
const Cell = @import("cell.zig");
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
// private imports
|
// private imports
|
||||||
const container = @import("container.zig");
|
const container = @import("container.zig");
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const size = @import("size.zig");
|
const size = @import("point.zig");
|
||||||
|
|
||||||
// public exports
|
// public exports
|
||||||
pub const input = @import("input.zig");
|
pub const input = @import("input.zig");
|
||||||
pub const testing = @import("testing.zig");
|
pub const testing = @import("testing.zig");
|
||||||
|
|
||||||
pub const App = @import("app.zig").App;
|
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 also exports further types once initialized with the user events at compile time:
|
||||||
// `App.Container`
|
// `App.Container`
|
||||||
// `App.Element`
|
// `App.Element`
|
||||||
@@ -16,24 +17,23 @@ pub const Renderer = @import("render.zig");
|
|||||||
// Container Configurations
|
// Container Configurations
|
||||||
pub const Border = container.Border;
|
pub const Border = container.Border;
|
||||||
pub const Rectangle = container.Rectangle;
|
pub const Rectangle = container.Rectangle;
|
||||||
pub const Scroll = container.Scroll;
|
|
||||||
pub const Layout = container.Layout;
|
pub const Layout = container.Layout;
|
||||||
|
|
||||||
pub const Cell = @import("cell.zig");
|
pub const Cell = @import("cell.zig");
|
||||||
pub const Color = color.Color;
|
pub const Color = color.Color;
|
||||||
pub const Key = input.Key;
|
pub const Key = input.Key;
|
||||||
pub const Mouse = input.Mouse;
|
pub const Mouse = input.Mouse;
|
||||||
pub const Position = size.Position;
|
pub const Point = @import("point.zig").Point;
|
||||||
pub const Size = size.Size;
|
|
||||||
pub const Style = @import("style.zig");
|
pub const Style = @import("style.zig");
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = @import("terminal.zig");
|
_ = @import("terminal.zig");
|
||||||
_ = @import("container.zig");
|
_ = @import("container.zig");
|
||||||
_ = @import("queue.zig");
|
_ = @import("queue.zig");
|
||||||
|
_ = @import("error.zig");
|
||||||
|
_ = @import("point.zig");
|
||||||
|
|
||||||
_ = color;
|
_ = color;
|
||||||
_ = size;
|
|
||||||
|
|
||||||
_ = Cell;
|
_ = Cell;
|
||||||
_ = Key;
|
_ = Key;
|
||||||
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,
|
|
||||||
};
|
|
||||||
@@ -7,11 +7,15 @@
|
|||||||
|
|
||||||
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
|
||||||
// with slight modifications
|
// with slight modifications
|
||||||
const std = @import("std");
|
|
||||||
|
|
||||||
const Color = @import("color.zig").Color;
|
fg: Color = .default,
|
||||||
|
bg: Color = .default,
|
||||||
pub const Style = @This();
|
ul: Color = .default,
|
||||||
|
cursor: bool = false,
|
||||||
|
cursor_color: Color = .default,
|
||||||
|
cursor_shape: CursorShape = .default,
|
||||||
|
ul_style: Underline = .off,
|
||||||
|
emphasis: []const Emphasis,
|
||||||
|
|
||||||
pub const Underline = enum {
|
pub const Underline = enum {
|
||||||
off,
|
off,
|
||||||
@@ -34,39 +38,61 @@ pub const Emphasis = enum(u8) {
|
|||||||
strikethrough,
|
strikethrough,
|
||||||
};
|
};
|
||||||
|
|
||||||
fg: Color = .default,
|
pub const CursorShape = enum(u4) {
|
||||||
bg: Color = .default,
|
default = 0,
|
||||||
ul: Color = .default,
|
block_blinking = 1,
|
||||||
ul_style: Underline = .off,
|
block_steady,
|
||||||
emphasis: []const Emphasis,
|
underline_blinking,
|
||||||
|
underline_steady,
|
||||||
|
bar_blinking,
|
||||||
|
bar_steady,
|
||||||
|
};
|
||||||
|
|
||||||
pub fn eql(this: Style, other: Style) bool {
|
pub fn eql(this: Style, other: Style) bool {
|
||||||
return std.meta.eql(this, other);
|
// TODO should there be a compare for every field?
|
||||||
|
return meta.eql(this, other);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(this: Style, writer: anytype, cp: u21) !void {
|
pub fn set_cursor_style(this: Style, writer: *std.Io.Writer) !void {
|
||||||
|
if (!this.cursor) return;
|
||||||
|
switch (this.cursor_color) {
|
||||||
|
.default => try writer.print(ctlseqs.osc12_reset, .{}),
|
||||||
|
else => try writer.print(ctlseqs.osc12_set, .{@tagName(this.cursor_color)}),
|
||||||
|
}
|
||||||
|
try writer.print(ctlseqs.cursor_shape, .{@intFromEnum(this.cursor_shape)});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(this: Style, writer: *std.Io.Writer, cp: u21) !void {
|
||||||
var buffer: [4]u8 = undefined;
|
var buffer: [4]u8 = undefined;
|
||||||
const bytes = try std.unicode.utf8Encode(cp, &buffer);
|
const bytes = try unicode.utf8Encode(cp, &buffer);
|
||||||
std.debug.assert(bytes > 0);
|
assert(bytes > 0);
|
||||||
// build ansi sequence for 256 colors ...
|
// build ansi sequence for 256 colors ...
|
||||||
// foreground
|
// foreground
|
||||||
try std.fmt.format(writer, "\x1b[", .{});
|
try writer.printAscii("\x1b[", .{});
|
||||||
try this.fg.write(writer, .fg);
|
try this.fg.write(writer, .fg);
|
||||||
// background
|
// background
|
||||||
try std.fmt.format(writer, ";", .{});
|
try writer.printAsciiChar(';', .{});
|
||||||
try this.bg.write(writer, .bg);
|
try this.bg.write(writer, .bg);
|
||||||
// underline
|
// 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 writer.printAsciiChar(';', .{});
|
||||||
try this.ul.write(writer, .ul);
|
try this.ul.write(writer, .ul);
|
||||||
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
// append styles (aka attributes like bold, italic, strikethrough, etc.)
|
||||||
for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
|
for (this.emphasis) |attribute| try writer.print(";{d}", .{@intFromEnum(attribute)});
|
||||||
try std.fmt.format(writer, "m", .{});
|
try writer.printAsciiChar('m', .{});
|
||||||
// content
|
// content
|
||||||
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
|
try writer.printAscii(buffer[0..bytes], .{});
|
||||||
try std.fmt.format(writer, "\x1b[0m", .{});
|
try writer.printAscii("\x1b[0m", .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement helper functions for terminal capabilities:
|
// TODO implement helper functions for terminal capabilities:
|
||||||
// - links / url display (osc 8)
|
// - links / url display (osc 8)
|
||||||
// - show / hide cursor?
|
// - show / hide cursor?
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const unicode = std.unicode;
|
||||||
|
const meta = std.meta;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const Color = @import("color.zig").Color;
|
||||||
|
const ctlseqs = @import("ctlseqs.zig");
|
||||||
|
const Style = @This();
|
||||||
|
|||||||
132
src/terminal.zig
132
src/terminal.zig
@@ -1,15 +1,3 @@
|
|||||||
const std = @import("std");
|
|
||||||
const code_point = @import("code_point");
|
|
||||||
const ctlseqs = @import("ctlseqs.zig");
|
|
||||||
const input = @import("input.zig");
|
|
||||||
|
|
||||||
const Key = input.Key;
|
|
||||||
const Position = @import("size.zig").Position;
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
const Cell = @import("cell.zig");
|
|
||||||
|
|
||||||
const log = std.log.scoped(.terminal);
|
|
||||||
|
|
||||||
// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html
|
// Ref: https://vt100.net/docs/vt510-rm/DECRPM.html
|
||||||
pub const ReportMode = enum {
|
pub const ReportMode = enum {
|
||||||
not_recognized,
|
not_recognized,
|
||||||
@@ -21,89 +9,104 @@ pub const ReportMode = enum {
|
|||||||
|
|
||||||
/// Gets number of rows and columns in the terminal
|
/// Gets number of rows and columns in the terminal
|
||||||
pub fn getTerminalSize() Size {
|
pub fn getTerminalSize() Size {
|
||||||
var ws: std.posix.winsize = undefined;
|
var ws: posix.winsize = undefined;
|
||||||
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
_ = posix.system.ioctl(posix.STDIN_FILENO, posix.T.IOCGWINSZ, @intFromPtr(&ws));
|
||||||
return .{ .cols = ws.col, .rows = ws.row };
|
return .{ .x = ws.col, .y = ws.row };
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn saveScreen() !void {
|
pub fn saveScreen() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.save_screen);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.save_screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn restoreScreen() !void {
|
pub fn restoreScreen() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.restore_screen);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.restore_screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enterAltScreen() !void {
|
pub fn enterAltScreen() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.smcup);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.smcup);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exitAltScreen() !void {
|
pub fn exitAltScreen() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.rmcup);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.rmcup);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clearScreen() !void {
|
pub fn clearScreen() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.clear_screen);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.clear_screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hideCursor() !void {
|
pub fn hideCursor() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.hide_cursor);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.hide_cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn showCursor() !void {
|
pub fn showCursor() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.show_cursor);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.show_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resetCursor() !void {
|
||||||
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.reset_cursor_shape);
|
||||||
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.osc12_reset);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setCursorPositionHome() !void {
|
pub fn setCursorPositionHome() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.home);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.home);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn enableMouseSupport() !void {
|
pub fn enableMouseSupport() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_set);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_set);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disableMouseSupport() !void {
|
pub fn disableMouseSupport() !void {
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_reset);
|
_ = try posix.write(posix.STDIN_FILENO, ctlseqs.mouse_reset);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ringBell() !void {
|
||||||
|
_ = try posix.write(posix.STDIN_FILENO, &.{7});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read(buf: []u8) !usize {
|
pub fn read(buf: []u8) !usize {
|
||||||
return try std.posix.read(std.posix.STDIN_FILENO, buf);
|
return try posix.read(posix.STDIN_FILENO, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(buf: []const u8) !usize {
|
pub fn write(buf: []const u8) !usize {
|
||||||
return try std.posix.write(std.posix.STDIN_FILENO, buf);
|
return try posix.write(posix.STDIN_FILENO, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn contextWrite(context: @This(), data: []const u8) anyerror!usize {
|
fn drainFn(w: *std.Io.Writer, data: []const []const u8, splat: usize) error{WriteFailed}!usize {
|
||||||
_ = context;
|
_ = w;
|
||||||
return try std.posix.write(std.posix.STDOUT_FILENO, data);
|
if (data.len == 0 or splat == 0) return 0;
|
||||||
|
var len: usize = 0;
|
||||||
|
|
||||||
|
for (data) |bytes| len += posix.write(posix.STDOUT_FILENO, bytes) catch return error.WriteFailed;
|
||||||
|
|
||||||
|
return len;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Writer = std.io.Writer(
|
// TODO I now need to add that much, for just the one function above?
|
||||||
@This(),
|
pub fn writer() std.Io.Writer {
|
||||||
anyerror,
|
return .{
|
||||||
contextWrite,
|
.vtable = &.{
|
||||||
);
|
.drain = drainFn,
|
||||||
|
.flush = std.Io.Writer.noopFlush,
|
||||||
pub fn writer() Writer {
|
},
|
||||||
return .{ .context = .{} };
|
.buffer = &.{},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setCursorPosition(pos: Position) !void {
|
pub fn setCursorPosition(pos: Point) !void {
|
||||||
var buf: [64]u8 = undefined;
|
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 + 1, pos.x + 1 });
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
|
_ = try posix.write(posix.STDIN_FILENO, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getCursorPosition() !Size.Position {
|
pub fn getCursorPosition() !Size {
|
||||||
// Needs Raw mode (no wait for \n) to work properly cause
|
// Needs Raw mode (no wait for \n) to work properly cause
|
||||||
// control sequence will not be written without it.
|
// control sequence will not be written without it.
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
|
_ = try posix.write(posix.STDIN_FILENO, "\x1b[6n");
|
||||||
|
|
||||||
var buf: [64]u8 = undefined;
|
var buf: [64]u8 = undefined;
|
||||||
|
|
||||||
// format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R"
|
// format: \x1b, "[", R1,..., Rn, ";", C1, ..., Cn, "R"
|
||||||
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
|
const len = try posix.read(posix.STDIN_FILENO, &buf);
|
||||||
|
|
||||||
if (!isCursorPosition(buf[0..len])) {
|
if (!isCursorPosition(buf[0..len])) {
|
||||||
return error.InvalidValueReturned;
|
return error.InvalidValueReturned;
|
||||||
@@ -137,8 +140,8 @@ pub fn getCursorPosition() !Size.Position {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
||||||
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
|
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +169,8 @@ pub fn isCursorPosition(buf: []u8) bool {
|
|||||||
///
|
///
|
||||||
/// `bak`: pointer to store termios struct backup before
|
/// `bak`: pointer to store termios struct backup before
|
||||||
/// altering, this is used to disable raw mode.
|
/// altering, this is used to disable raw mode.
|
||||||
pub fn enableRawMode(bak: *std.posix.termios) !void {
|
pub fn enableRawMode(bak: *posix.termios) !void {
|
||||||
var termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO);
|
var termios = try posix.tcgetattr(posix.STDIN_FILENO);
|
||||||
bak.* = termios;
|
bak.* = termios;
|
||||||
|
|
||||||
// termios flags used by termios(3)
|
// termios flags used by termios(3)
|
||||||
@@ -192,20 +195,20 @@ pub fn enableRawMode(bak: *std.posix.termios) !void {
|
|||||||
termios.cflag.CSIZE = .CS8;
|
termios.cflag.CSIZE = .CS8;
|
||||||
termios.cflag.PARENB = false;
|
termios.cflag.PARENB = false;
|
||||||
|
|
||||||
termios.cc[@intFromEnum(std.posix.V.MIN)] = 1;
|
termios.cc[@intFromEnum(posix.V.MIN)] = 1;
|
||||||
termios.cc[@intFromEnum(std.posix.V.TIME)] = 0;
|
termios.cc[@intFromEnum(posix.V.TIME)] = 0;
|
||||||
|
|
||||||
try std.posix.tcsetattr(
|
try posix.tcsetattr(
|
||||||
std.posix.STDIN_FILENO,
|
posix.STDIN_FILENO,
|
||||||
.FLUSH,
|
.FLUSH,
|
||||||
termios,
|
termios,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reverts `enableRawMode` to restore initial functionality.
|
/// Reverts `enableRawMode` to restore initial functionality.
|
||||||
pub fn disableRawMode(bak: *std.posix.termios) !void {
|
pub fn disableRawMode(bak: *const posix.termios) !void {
|
||||||
try std.posix.tcsetattr(
|
try posix.tcsetattr(
|
||||||
std.posix.STDIN_FILENO,
|
posix.STDIN_FILENO,
|
||||||
.FLUSH,
|
.FLUSH,
|
||||||
bak.*,
|
bak.*,
|
||||||
);
|
);
|
||||||
@@ -215,13 +218,13 @@ pub fn disableRawMode(bak: *std.posix.termios) !void {
|
|||||||
pub fn canSynchornizeOutput() !bool {
|
pub fn canSynchornizeOutput() !bool {
|
||||||
// Needs Raw mode (no wait for \n) to work properly cause
|
// Needs Raw mode (no wait for \n) to work properly cause
|
||||||
// control sequence will not be written without it.
|
// control sequence will not be written without it.
|
||||||
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?2026$p");
|
_ = try posix.write(posix.STDIN_FILENO, "\x1b[?2026$p");
|
||||||
|
|
||||||
var buf: [64]u8 = undefined;
|
var buf: [64]u8 = undefined;
|
||||||
|
|
||||||
// format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y"
|
// format: \x1b, "[", "?", "2", "0", "2", "6", ";", n, "$", "y"
|
||||||
const len = try std.posix.read(std.posix.STDIN_FILENO, &buf);
|
const len = try posix.read(posix.STDIN_FILENO, &buf);
|
||||||
if (!std.mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
|
if (!mem.eql(u8, buf[0..len], "\x1b[?2026;") or len < 9) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,3 +241,16 @@ fn getReportMode(ps: u8) ReportMode {
|
|||||||
else => ReportMode.not_recognized,
|
else => ReportMode.not_recognized,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const log = std.log.scoped(.terminal);
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const posix = std.posix;
|
||||||
|
const assert = std.debug.assert;
|
||||||
|
const ctlseqs = @import("ctlseqs.zig");
|
||||||
|
const input = @import("input.zig");
|
||||||
|
const Key = input.Key;
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
const Size = @import("point.zig").Point;
|
||||||
|
const Cell = @import("cell.zig");
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
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/alignment.bottom.zon
Normal file
1
src/test/element/alignment.bottom.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.center.zon
Normal file
1
src/test/element/alignment.center.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.left.zon
Normal file
1
src/test/element/alignment.left.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.right.zon
Normal file
1
src/test/element/alignment.right.zon
Normal file
File diff suppressed because one or more lines are too long
1
src/test/element/alignment.top.zon
Normal file
1
src/test/element/alignment.top.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
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/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
File diff suppressed because one or more lines are too long
1
src/test/element/scrollable.vertical.scrollbar.top.zon
Normal file
1
src/test/element/scrollable.vertical.scrollbar.top.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
166
src/testing.zig
166
src/testing.zig
@@ -1,25 +1,13 @@
|
|||||||
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
|
//! 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 Position = @import("size.zig").Position;
|
|
||||||
const Size = @import("size.zig").Size;
|
|
||||||
|
|
||||||
// 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.
|
/// Single-buffer test rendering pipeline for testing purposes.
|
||||||
pub const Renderer = struct {
|
pub const Renderer = struct {
|
||||||
allocator: std.mem.Allocator,
|
allocator: Allocator,
|
||||||
size: Size,
|
size: Point,
|
||||||
screen: []Cell,
|
screen: []Cell,
|
||||||
|
|
||||||
pub fn init(allocator: std.mem.Allocator, size: Size) @This() {
|
pub fn init(allocator: Allocator, size: Point) @This() {
|
||||||
const screen = allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("testing.zig: Out of memory.");
|
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
|
||||||
@memset(screen, .{});
|
@memset(screen, .{});
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
@@ -33,9 +21,9 @@ pub const Renderer = struct {
|
|||||||
this.allocator.free(this.screen);
|
this.allocator.free(this.screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn resize(this: *@This(), size: Size) !void {
|
pub fn resize(this: *@This(), size: Point) !void {
|
||||||
this.size = size;
|
this.size = size;
|
||||||
const n = @as(usize, size.cols) * @as(usize, size.rows);
|
const n = @as(usize, size.x) * @as(usize, size.y);
|
||||||
|
|
||||||
this.allocator.free(this.screen);
|
this.allocator.free(this.screen);
|
||||||
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
|
||||||
@@ -46,22 +34,23 @@ pub const Renderer = struct {
|
|||||||
@memset(this.screen, .{});
|
@memset(this.screen, .{});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
|
pub fn render(this: *@This(), comptime T: type, container: *const T, comptime Model: type, model: *const Model) !void {
|
||||||
const size: Size = container.size;
|
const size: Point = container.size;
|
||||||
const cells: []const Cell = try container.contents();
|
const origin: Point = container.origin;
|
||||||
|
const cells: []const Cell = try container.content(model);
|
||||||
|
|
||||||
if (cells.len == 0) return;
|
if (cells.len == 0) return;
|
||||||
|
|
||||||
var idx: usize = 0;
|
var idx: usize = 0;
|
||||||
const anchor = (@as(usize, size.anchor.row) * @as(usize, this.size.cols)) + @as(usize, size.anchor.col);
|
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
|
||||||
|
|
||||||
blk: for (0..size.rows) |row| {
|
blk: for (0..size.y) |row| {
|
||||||
for (0..size.cols) |col| {
|
for (0..size.x) |col| {
|
||||||
const cell = cells[idx];
|
const cell = cells[idx];
|
||||||
idx += 1;
|
idx += 1;
|
||||||
|
|
||||||
this.screen[anchor + (row * this.size.cols) + col].style = cell.style;
|
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
|
||||||
this.screen[anchor + (row * this.size.cols) + col].cp = cell.cp;
|
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
|
||||||
|
|
||||||
if (cells.len == idx) break :blk;
|
if (cells.len == idx) break :blk;
|
||||||
}
|
}
|
||||||
@@ -69,7 +58,7 @@ pub const Renderer = struct {
|
|||||||
// free immediately
|
// free immediately
|
||||||
container.allocator.free(cells);
|
container.allocator.free(cells);
|
||||||
|
|
||||||
for (container.elements.items) |*element| try this.render(T, element);
|
for (container.elements.items) |*element| try this.render(T, element, Model, model);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(this: @This(), writer: anytype) !void {
|
pub fn save(this: @This(), writer: anytype) !void {
|
||||||
@@ -85,6 +74,8 @@ pub const Renderer = struct {
|
|||||||
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
|
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
|
||||||
///
|
///
|
||||||
/// ```zig
|
/// ```zig
|
||||||
|
/// const Model = struct {};
|
||||||
|
/// var model: Model = .{};
|
||||||
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
|
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
|
||||||
/// defer file.close();
|
/// defer file.close();
|
||||||
///
|
///
|
||||||
@@ -92,8 +83,8 @@ pub const Renderer = struct {
|
|||||||
/// var renderer: testing.Renderer = .init(allocator, size);
|
/// var renderer: testing.Renderer = .init(allocator, size);
|
||||||
/// defer renderer.deinit();
|
/// defer renderer.deinit();
|
||||||
///
|
///
|
||||||
/// try container.handle(.{ .resize = size });
|
/// try container.handle(&model, .{ .size = size });
|
||||||
/// try renderer.render(Container(event.SystemEvent), &container);
|
/// try renderer.render(@TypeOf(container), &container, Model, &.{});
|
||||||
/// try renderer.save(file.writer());
|
/// try renderer.save(file.writer());
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
@@ -102,7 +93,8 @@ pub const Renderer = struct {
|
|||||||
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
|
||||||
///
|
///
|
||||||
/// ```zig
|
/// ```zig
|
||||||
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
|
/// const Model = struct {};
|
||||||
|
/// var container: Container(Model, event.SystemEvent) = try .init(std.testing.allocator, .{
|
||||||
/// .border = .{
|
/// .border = .{
|
||||||
/// .color = .green,
|
/// .color = .green,
|
||||||
/// .sides = .all,
|
/// .sides = .all,
|
||||||
@@ -113,70 +105,100 @@ pub const Renderer = struct {
|
|||||||
/// try testing.expectContainerScreen(.{
|
/// try testing.expectContainerScreen(.{
|
||||||
/// .rows = 20,
|
/// .rows = 20,
|
||||||
/// .cols = 30,
|
/// .cols = 30,
|
||||||
/// }, &container, @import("test/container/border.all.zon"));
|
/// }, @TypeOf(container), &container, Model, @import("test/container/border.all.zon"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn expectContainerScreen(size: Size, container: *Container(event.SystemEvent), expected: []const Cell) !void {
|
pub fn expectContainerScreen(size: Point, comptime T: type, container: *T, comptime Model: type, expected: []const Cell) !void {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = testing.allocator;
|
||||||
var renderer: Renderer = .init(allocator, size);
|
var renderer: Renderer = .init(allocator, size);
|
||||||
defer renderer.deinit();
|
defer renderer.deinit();
|
||||||
|
|
||||||
try container.handle(.{ .resize = size });
|
const model: Model = .{};
|
||||||
try renderer.render(Container(event.SystemEvent), container);
|
|
||||||
|
|
||||||
try expectEqualCells(renderer.size, expected, renderer.screen);
|
container.resize(&model, size);
|
||||||
|
container.reposition(&model, .{});
|
||||||
|
try renderer.render(T, container, Model, &model);
|
||||||
|
|
||||||
|
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Taken from: https://codeberg.org/atman/zg/src/branch/master/src/DisplayWidth.zig
|
||||||
|
/// Owned by https://codeberg.org/atman licensed under MIT all credits for this function go to him
|
||||||
|
fn center(allocator: Allocator, str: []const u8, total_width: usize, pad: []const u8) ![]u8 {
|
||||||
|
if (str.len > total_width) return error.StrTooLong;
|
||||||
|
if (str.len == total_width) return try allocator.dupe(u8, str);
|
||||||
|
|
||||||
|
if (pad.len > total_width or str.len + pad.len > total_width) return error.PadTooLong;
|
||||||
|
|
||||||
|
const margin_width = @divFloor((total_width - str.len), 2);
|
||||||
|
if (pad.len > margin_width) return error.PadTooLong;
|
||||||
|
|
||||||
|
const extra_pad: usize = if (total_width % 2 != str.len % 2) 1 else 0;
|
||||||
|
const pads = @divFloor(margin_width, pad.len) * 2 + extra_pad;
|
||||||
|
|
||||||
|
var result = try allocator.alloc(u8, pads * pad.len + str.len);
|
||||||
|
var bytes_index: usize = 0;
|
||||||
|
var pads_index: usize = 0;
|
||||||
|
|
||||||
|
while (pads_index < pads / 2) : (pads_index += 1) {
|
||||||
|
@memcpy(result[bytes_index..][0..pad.len], pad);
|
||||||
|
bytes_index += pad.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
@memcpy(result[bytes_index..][0..str.len], str);
|
||||||
|
bytes_index += str.len;
|
||||||
|
|
||||||
|
pads_index = 0;
|
||||||
|
while (pads_index < pads / 2 + extra_pad) : (pads_index += 1) {
|
||||||
|
@memcpy(result[bytes_index..][0..pad.len], pad);
|
||||||
|
bytes_index += pad.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function is intended to be used only in tests. Test if the two
|
/// This function is intended to be used only in tests. Test if the two
|
||||||
/// provided cell arrays are identical. Usually the `Cell` slices are
|
/// provided cell arrays are identical. Usually the `Cell` slices are
|
||||||
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
/// the contents of a given screen from the `zterm.testing.Renderer`. See
|
||||||
/// `zterm.testing.expectContainerScreen` for an example usage.
|
/// `zterm.testing.expectContainerScreen` for an example usage.
|
||||||
pub fn expectEqualCells(size: Size, expected: []const Cell, actual: []const Cell) !void {
|
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
|
||||||
const allocator = std.testing.allocator;
|
const allocator = testing.allocator;
|
||||||
|
|
||||||
try std.testing.expectEqual(expected.len, actual.len);
|
try testing.expectEqual(expected.len, actual.len);
|
||||||
try std.testing.expectEqual(expected.len, @as(usize, size.rows) * @as(usize, size.cols));
|
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
|
||||||
|
|
||||||
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.cols -| size.anchor.col);
|
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
||||||
defer expected_cps.deinit();
|
defer expected_cps.deinit(allocator);
|
||||||
|
|
||||||
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.cols -| size.anchor.col);
|
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
|
||||||
defer actual_cps.deinit();
|
defer actual_cps.deinit(allocator);
|
||||||
|
|
||||||
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.rows);
|
var allocating_writer = std.Io.Writer.Allocating.init(allocator);
|
||||||
defer output.deinit();
|
defer allocating_writer.deinit();
|
||||||
|
|
||||||
var buffer = std.io.bufferedWriter(output.writer());
|
var writer = &allocating_writer.writer;
|
||||||
defer buffer.flush() catch {};
|
|
||||||
|
|
||||||
const writer = buffer.writer();
|
|
||||||
var differ = false;
|
var differ = false;
|
||||||
|
|
||||||
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
|
const expected_centered = try center(allocator, "Expected Screen", size.x, " ");
|
||||||
defer dwd.deinit();
|
|
||||||
const dw: DisplayWidth = .{ .data = &dwd };
|
|
||||||
|
|
||||||
const expected_centered = try dw.center(allocator, "Expected Screen", size.cols, " ");
|
|
||||||
defer allocator.free(expected_centered);
|
defer allocator.free(expected_centered);
|
||||||
|
|
||||||
const actual_centered = try dw.center(allocator, "Actual Screen", size.cols, " ");
|
const actual_centered = try center(allocator, "Actual Screen", size.x, " ");
|
||||||
defer allocator.free(actual_centered);
|
defer allocator.free(actual_centered);
|
||||||
|
|
||||||
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
|
||||||
|
|
||||||
const anchor = (size.anchor.row * size.cols) + size.anchor.col;
|
for (origin.y..size.y) |row| {
|
||||||
for (size.anchor.row..size.rows) |row| {
|
|
||||||
defer {
|
defer {
|
||||||
expected_cps.clearRetainingCapacity();
|
expected_cps.clearRetainingCapacity();
|
||||||
actual_cps.clearRetainingCapacity();
|
actual_cps.clearRetainingCapacity();
|
||||||
}
|
}
|
||||||
for (size.anchor.col..size.cols) |col| {
|
for (origin.x..size.x) |col| {
|
||||||
const expected_cell = expected[anchor + (row * size.cols) + col];
|
const expected_cell = expected[(row * size.x) + col];
|
||||||
const actual_cell = actual[anchor + (row * size.cols) + col];
|
const actual_cell = actual[(row * size.x) + col];
|
||||||
|
|
||||||
if (!expected_cell.eql(actual_cell)) differ = true;
|
if (!expected_cell.eql(actual_cell)) differ = true;
|
||||||
|
|
||||||
try expected_cps.append(expected_cell);
|
try expected_cps.append(allocator, expected_cell);
|
||||||
try actual_cps.append(actual_cell);
|
try actual_cps.append(allocator, actual_cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
// write screens both formatted to buffer
|
// write screens both formatted to buffer
|
||||||
@@ -189,12 +211,24 @@ pub fn expectEqualCells(size: Size, expected: []const Cell, actual: []const Cell
|
|||||||
if (!differ) return;
|
if (!differ) return;
|
||||||
|
|
||||||
// test failed
|
// test failed
|
||||||
try buffer.flush();
|
var buf: [1024]u8 = undefined;
|
||||||
|
|
||||||
std.debug.lockStdErr();
|
std.debug.lockStdErr();
|
||||||
defer std.debug.unlockStdErr();
|
defer std.debug.unlockStdErr();
|
||||||
|
|
||||||
const std_writer = std.io.getStdErr().writer();
|
var buffer = std.fs.File.stderr().writer(&buf);
|
||||||
try std_writer.writeAll(output.items);
|
var error_writer = &buffer.interface;
|
||||||
|
try error_writer.writeAll(writer.buffer[0..writer.end]);
|
||||||
|
try error_writer.flush();
|
||||||
|
|
||||||
return error.TestExpectEqualCells;
|
return error.TestExpectEqualCells;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std = @import("std");
|
||||||
|
const debug = std.debug;
|
||||||
|
const testing = std.testing;
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
const event = @import("event.zig");
|
||||||
|
const Container = @import("container.zig").Container;
|
||||||
|
const Cell = @import("cell.zig");
|
||||||
|
const Point = @import("point.zig").Point;
|
||||||
|
|||||||
Reference in New Issue
Block a user