95 Commits

Author SHA1 Message Date
0b7d032b11 tag: 0.2.0
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m15s
2025-03-27 21:43:02 +01:00
7e20dd73d9 mod: add missing inline function attribute
Correct example to use the actual `zterm.Error` type accordingly.
2025-03-27 21:41:18 +01:00
182dec6065 release: 0.1.0
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 21s
2025-03-13 19:50:49 +01:00
54af974c2b mod(container): make reposition public and split from resize
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m3s
2025-03-13 19:48:31 +01:00
dddc09b4ce mod(container/element): remove origin: Point argument from Element.content function
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m12s
Renamed `Container.contents` to `Container.content` to be in line with
the corresponding `Element` function name. This has also been done for
the properties structs used by the `Container`.
2025-03-11 07:52:35 +01:00
adda53c5a9 add(test): container size with fixed and growable container children
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-03-10 22:07:34 +01:00
5c1d61eefd rem(element): unnecessary debug loging
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m20s
2025-03-10 21:44:18 +01:00
54c7e19939 fix(container): growth options to dynamically size Containers
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 29s
2025-03-06 22:31:00 +01:00
5457e91b37 fix(container/grow_size): respect vertical / horizontal provided dimensions 2025-03-06 22:07:39 +01:00
79016f39b2 fix(container/grow_size): children should never grow larger then parents
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 29s
2025-03-06 17:56:13 +01:00
2b9ab1e0fb fix(example/styles): provide necessary size for text display through the element
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 31s
This could also be done through the `resize` function interface of the
`Element` or the corresponding `Container` from the outside (as done in
this example - as the size is know at compile-time).
2025-03-05 23:22:06 +01:00
315cd8d23e fix(layout): remove upper bound of while loop
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 29s
The upper loop was incorrect and therefore removed to create correct
layouts. I should be able to calculate the bound correctly, but for
now).
2025-03-05 23:14:54 +01:00
e3551fa624 add(sizing): grow configuration
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m14s
Currently the 'grid' and 'mixed' examples are not working yet.
2025-03-05 22:53:28 +01:00
9ec335cad8 fix(lint): correct spelling error
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 49s
2025-03-04 23:01:28 +01:00
466e00c16c fix(element/scrollable): support deriving Container size of scrollable
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 13s
2025-03-04 21:54:07 +01:00
fc72cf4abb ref(container): split size and position calculations
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 34s
2025-03-04 19:53:28 +01:00
65d7546efd fix(testing): apply refactor to test implementation
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 27s
2025-03-04 14:54:51 +01:00
ec22e68e8c ref(event): remove .resize and replace with recursize method calls
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 40s
This also means that currently the dynamic resizing through the app's
detached thread is not working, as it cannot send size updates. The
examples have been overhauled to still implement intermediate mode
applications accordingly.
2025-03-04 14:52:19 +01:00
43cdc46853 fix(input/mouse): correct boundary check for mouse position inside of given origin and size
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled
2025-03-04 14:51:26 +01:00
591b990087 ref(event): split Size into two Points (one for the size and one for the anchor / origin)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 39s
2025-03-04 00:04:56 +01:00
91ac6241f4 doc: correct TODO, NOTE and FIX comment statements
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 48s
2025-03-03 21:49:11 +01:00
edefc80759 mod(container/border): render horizontal borders across entire Container
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 27s
2025-03-02 23:03:00 +01:00
4145ff497b fix(elements/scrollable): nested container rendering
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 2m45s
2025-03-02 22:27:26 +01:00
bec0cf2987 fix(container): check border sides configurations before trying to create borders
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 28s
2025-03-01 20:58:34 +01:00
e2fe884925 chors: update build file configurations
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 28s
2025-03-01 17:57:28 +01:00
caee008d50 test: streamline examples with quit texts
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 24s
Improve some examples to provide visual feedback, i.e. for the button
exmample, with fixes to make them compilable with the `Scrollable`
element changes.
2025-03-01 17:12:28 +01:00
af443c6bbf test(elements/scrollable): remove redundance from test code
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 28s
2025-03-01 16:24:10 +01:00
ae9cd08b15 add(error): introduce zterm.Error containing all zterm errors with their corresponding description
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 28s
2025-03-01 15:19:42 +01:00
91794a0197 add(build): .fingerprint property for most recent zig version
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 49s
2025-03-01 15:15:06 +01:00
8a7ce78aaf feat(container): introduce fixed_size property for fixed sizing of Containers
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 42s
Moved min_size property from `Container` to the `Scrollable` element,
where it is only used anyway.
2025-03-01 11:56:14 +01:00
35ebe31008 doc: correct example used in README
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 18s
2025-02-28 15:58:46 +01:00
c28fcd26c1 fix(container/layout): integer overflows by casting to usize
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m5s
2025-02-28 15:55:10 +01:00
3b6848f845 fix(container): rendering scrollable elements with separators
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 23s
Added corresponding test cases to test the corresponding rendering of
scrollable elements.
2025-02-27 17:02:16 +01:00
53b69f034c mod(size): rename merge function to add; new max function
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m6s
2025-02-27 14:17:19 +01:00
54ce697e91 fix(lint): typo
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 47s
2025-02-26 21:54:29 +01:00
8f16435f30 test(container): rectangle color filling
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m4s
2025-02-26 21:04:28 +01:00
ca14bc6106 fix(container): positioning; move separator options to layout struct
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 22s
Added corresponding test cases for padding, borders and corresponding
seperators.
2025-02-26 18:21:55 +01:00
a293ef46da test(container): add missing zon test configurations
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 46s
2025-02-25 20:41:35 +01:00
c66401d941 testing(container): border separator test cases
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 40s
Currently the test case with both a border and separators for two
children is failing to render the separators.
2025-02-25 20:39:18 +01:00
ad4186e1f8 test(container): move test template for zon file creation to wiki
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-02-25 19:04:21 +01:00
8c130a40d7 test(container): correct color
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m15s
2025-02-25 18:46:06 +01:00
9a3bc3dbf7 fix(lint): exclude zon files located in src/test 2025-02-25 18:45:13 +01:00
9d5a661b4e test(container): render Cell slices test against .zon input
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m0s
2025-02-25 18:41:04 +01:00
4234c9ad0c rem(element): template Element moved into wiki documentation
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 45s
2025-02-25 16:55:11 +01:00
a588e2ef21 add(test): input testing of Key.isAscii
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m42s
2025-02-25 16:52:17 +01:00
8519d204f3 fix(lint): correct spelling error
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m1s
2025-02-24 17:20:25 +01:00
33262c9638 add(testing): new namespace containing testing capabilities for zterm
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 39s
The namespace shall also be used for testing the rendering of
`Container`s and `Element`s (including the `Scrollable` element).

The testing renderer currently is a striped down version of the double
buffered render without the secondary buffer and the flushing to stdout.
The internal `Cell` slice (the *screen*) is used for equality checks.

The testing namespace shall provide a way to describe the expected
`Cell` slices that should be validated against.
2025-02-24 17:14:57 +01:00
5c5c59cbfc fix(style): render only necessary bytes and change default fg color to .default
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 45s
2025-02-24 17:11:22 +01:00
c022d1d9e2 fix(lint): correct indentation and naming convention for constants
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m9s
2025-02-24 17:09:22 +01:00
12497e92f8 fix(lint): correct casing for constants
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 21s
2025-02-24 17:01:55 +01:00
d10f738c75 add(test): cell conversion to ansi enhanced strings
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 45s
2025-02-24 16:54:05 +01:00
140f27216a doc: move contents from README to wiki
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 6m49s
2025-02-22 11:00:38 +01:00
04ba88c68b doc: update README
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 42s
2025-02-21 23:17:01 +01:00
6ccab74c94 add(examples/demo): application to showcase a more complex application
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 53s
Further improvements for example applications; Demo example is now
default build target (when not providing example configuration).
2025-02-21 22:57:14 +01:00
c634e1affc doc: update roadmap 2025-02-21 22:29:19 +01:00
dab486a2c1 add(examples/errors): error notifaction handling
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m33s
2025-02-21 22:25:42 +01:00
9b0dd3c52f doc: correct link to wiki
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 43s
2025-02-21 19:17:28 +01:00
f45e722578 fix(linter): spelling error in README
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 57s
2025-02-21 19:15:19 +01:00
7b005ea4b1 add(examples/styles): text and color styling possiblities
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 39s
This also contains some minor refactoring to improve the readability
and understandability of the library (i.e. renaming of Style.Attributes
to Style.Emphasis).
2025-02-21 19:13:11 +01:00
c0c0590bb9 add(examples/styles): color palette to showcase all available colors to render (except for .default)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 54s
2025-02-21 16:43:03 +01:00
16724f6a52 add(example/elements): distinct different scrollable containers
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-02-21 15:58:42 +01:00
44e92735cf ref(examples): avoid unnecessary casts
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 43s
2025-02-21 15:15:15 +01:00
8cc047c1fa fix(lint): spelling error in README
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 57s
2025-02-21 14:57:30 +01:00
8fbc958ca1 add(examples/elements): mouse clickable button
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 39s
2025-02-21 14:50:29 +01:00
b980703350 fix(container): border separator handling
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 39s
Due to the assigning back the increased value for the used gap in case
the separator was active, every .resize `Event` would add more to the
gap, leading to ever smaller sub-container's and different sizes after
resizes. This has been fixed to have consistent layouting done by the
`Container` and `Border`.
2025-02-21 12:25:47 +01:00
eb89f7f98b fix(renderer): reset color after each cell write
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 38s
2025-02-21 12:19:38 +01:00
cc847b7035 add(examples/layout): mixed content with different layout options 2025-02-21 12:19:16 +01:00
9dc1a4b95a add(examples/layout): vertical, horizontal and grid
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 39s
2025-02-21 11:31:18 +01:00
69c1600eb9 mod(build): adjust name of enum values to be easier to type in command line
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 54s
2025-02-20 23:54:53 +01:00
c4639bf4bb add(example): input with simple text field
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m41s
2025-02-20 23:48:57 +01:00
07e932741c doc: update roadmap and further goals
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 43s
2025-02-20 18:56:56 +01:00
e4ff240839 doc: update roadmap and contents for future documentation goals
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 44s
2025-02-20 11:49:30 +01:00
96375e3b72 mod(build): build configuration for examples
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 54s
2025-02-20 11:16:42 +01:00
9322785ca0 mod: update zig version
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 43s
2025-02-19 22:55:03 +01:00
cc831a5cdf fix(element/scrollable): render horizontal directed contents correctly
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-02-19 22:23:32 +01:00
86b3e7d4ed feat(scrollable): make Container scrollable through Element Scrollable
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m35s
2025-02-19 20:32:26 +01:00
f55d71a7cb mod(mouse): fix input.Mouse.in method
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
Pass through mouse events where they actually match the corresponding
`Container`. Adopt mouse event accordingly in `Scrollable` `Element`
trait when passing through the mouse event to the scrollable
`Container`.
2025-02-18 19:09:03 +01:00
f66a870223 ref(input): move mouse.zig and key.zig into public input.zig namespace
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m41s
2025-02-18 18:24:09 +01:00
e2f9408850 add(input): mouse support
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-02-17 23:36:27 +01:00
c2080ab40f ref(ctlseqs): use control sequence file; rename key names
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 46s
2025-02-17 23:08:47 +01:00
a9f48bfb6a ref(key): make Key struct packed and rename constants
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 2m5s
2025-02-17 21:06:15 +01:00
7891af6c6f add(element/scrollable): implement content provider
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m49s
However this is only working for the same size as the parent container
(i.e. same `Size`). The `.resize` event for the `Container` of the
scrollable element needs to be the necessary and/or required size for
the contents (regardless of the screen viewport).
2025-02-17 19:58:25 +01:00
7b690d387b fix(lint): correct spelling errors
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (pull_request) Successful in 1m9s
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 42s
2025-02-16 16:01:04 +01:00
5c929479b2 add(main): main executable with zig build run support
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (pull_request) Failing after 5m10s
The main executable contains a simple `Element` implementation example
showing how interaction through and outside of the event loop can be
implemented to impact the rendered contents.
2025-02-16 15:35:50 +01:00
d8a9e72b67 add(border): seperator line options with corresponding code points 2025-02-16 02:04:53 +01:00
d951906b2b rem: Scroll from Propierties of Container
Updated the corresponding documentation and ideas for how to realize
scrollable contents.
2025-02-15 18:50:36 +01:00
01eb14f1bd doc: update Roadmap section of README 2025-02-15 16:03:06 +01:00
1041b0a955 mod: update zg dependency
`zg` now supports zig 0.14 dev which this library already uses.
2025-02-15 16:00:55 +01:00
4781e9ce39 add(element): interface for injecting user behavior to containers
Some additional refactoring and documentation updates have also been
applied.
2025-02-15 15:56:30 +01:00
5c148e1aa5 fix(cotainer/border): seperator placement without border's and gap 2025-02-15 11:25:20 +01:00
a6aa6e5150 fix(container/border): correct location and rendering of separators between child elements 2025-02-15 11:10:37 +01:00
26d31a38de ref(container): use only one size for each container 2025-02-15 10:49:48 +01:00
01d121ef87 mod: update README and remove alignment options 2025-02-14 22:27:24 +01:00
abaea968a6 rem(container): sizing options
This enables the `Layout` struct to be packed (as well as the
`Properties` struct) which should further reduce the memory footprint.
2025-02-14 22:19:20 +01:00
73a7f740c9 ref(container): move size: Size member from Scroll to Container 2025-02-14 22:03:21 +01:00
55 changed files with 3688 additions and 952 deletions

View File

@@ -18,7 +18,7 @@ jobs:
with:
version: master
- name: Lint check
run: zig fmt --check .
run: zig fmt --check --exclude src/test .
- name: Spell checking
uses: crate-ci/typos@v1.25.0
with:

131
README.md
View File

@@ -1,9 +1,22 @@
# zterm Terminal User Interface Library
# zterm TUI Library
`zterm` is a terminal user interface library to implement terminal (fullscreen or inline) applications.
`zterm` is a terminal user interface library (*tui*) to implement terminal (fullscreen or inline) applications.
> [!NOTE]
> Only builds using the master version are tested to work.
> [!CAUTION]
> Only builds using the zig master version are tested to work.
## Demo
Clone this repository and run `zig build --help` to see the available examples. Run a given example as follows:
```sh
zig build --release=safe -Dexample=demo run
```
> [!TIP]
> Every example application can be quit using `ctrl+c`.
See the [wiki](https://gitea.yves-biener.de/yves-biener/zterm/wiki) for a showcase of the examples and the further details.
## Usage
@@ -13,7 +26,7 @@ To add or update `zterm` as a dependency in your project run the following comma
zig fetch --save git+https://gitea.yves-biener.de/yves-biener/zterm
```
Add the dependency to your module as follows in your _build.zig_:
Add the dependency to your module as follows in your *build.zig*:
```zig
const zterm: *Dependency = b.dependency("zterm", .{
@@ -24,106 +37,10 @@ const zterm: *Dependency = b.dependency("zterm", .{
exe.root_module.addImport("zterm", zterm.module("zterm"));
```
For an example you can take a look at [build.zig](build.zig) for an example.
### Documentation
---
## Design Goals
This project draws heavy inspiration from
[clay](https://github.com/nicbarker/clay) in the way the layout is declared by
the user. As terminal applications usually are rendered in intermediate mode,
the rendering is also part of the event loop. Such that every time an event
happens a render call will usually be done as well. However this is not strickly
necessary and can be separated to have a fixed rendering of every 16ms (i.e. for
60 fps), etc.
There is only one generic container which contains properties and elements (or
children) which can also be containers, such that each layout in the end is
a tree.
The library is designed to be very basic and not to provide any more complex
elements such as input fields, drop-down menu's, buttons, etc. Some of them are
either easy to implement yourself, specific for you needs or a too complex to
be provided by the library effectively. For these use-cases there may be other
libraries that build on top of this one to provide the complex elements as some
sort of pre-built elements for you to use in your application (or you create
them yourself).
There are only very few system events, that are used by the built-in containers
and properties accordingly. For you own widgets (i.e. a collection of elements)
you can extend the events to include your own events to communicate between
elements, effect the control flow and the corresponding generated layouts and
much more.
As this is a terminal based layout library it also provides a rendering pipeline
alongside the event loop implementation. Usually the event loop is waiting
blocking and will only cause a re-draw (**intermediate mode**) after each event.
Even though the each frame is regenerated from scratch each render loop, the
corresponding application is still pretty performant as the renderer uses a
double buffered intermediate mode implementation to only apply the changes from
each frame to the next to the terminal.
This library is also designed to work accordingly in ssh hosted environments,
such that an application created using this library can be accessed directly
via ssh. This provides security through the ssh protocol and can defer the
synchronization process, as users may access the same running instance. Which is
the primary use-case for myself to create this library in the first place.
---
## Roadmap
- [ ] Container rendering
- [ ] Layout
- [x] direction
- [x] vertical
- [x] horizontal
- [x] padding
- [x] gap
- [ ] alignment
- [ ] center
- [ ] left
- [ ] right
- [ ] sizing
- [ ] width
- [ ] height
- [ ] options
- [ ] fit
- [ ] grow
- [x] fixed
- [x] percent
- [ ] Border
- [x] sides
- [x] corners
- [ ] separators
- [x] Rectangle
- [ ] Scroll
- [ ] vertical
- [ ] horizontal
- [ ] scroll bar(s)
Decorations should respect the layout and the viewport accordingly. This means
that scrollbars are always visible (except there is no need to have a scrollbar)
irrelevant depending on the size of the content. The rectangle apply to all
cells of the content (and may be overwritten by child elements contents).
The border of an element should be around independent of the scrolling of the
contents, just like padding.
- *fit*: adjust virtual space of container by the size of its children (i.e. a
container needs to be able to get the necessary size of its children)
- *grow*: use as much space as available (what exactly would be the difference
between this option and *fit*?)
- *fixed*: use exactly as much cells (in the specified direction)
- *center*: elements should have their anchor be placed accordingly to their
size and the viewport size.
- *left*: the anchor remains at zero (relative to the location of the
container on the screen) -> similar to the current implementation!
- *right*: the anchor is fixed to the right side (i.e. size of the contents -
size of the viewport)
### Input
How is the user input handled in the containers? Should there be active
containers? Some input may happen for a specific container (i.e. when using
mouse input). How would I handle scrolling for outer and inner elements of
a container?
A wiki should be created containing a bright overview of the structure and usage
of the library. For details it should refer to the examples. The documentation
should be minimal in terms of updateability in case the library changes. Maybe
some documentation could be derived from the code documentation (there is a tool
for this if I recall correctly).

169
build.zig
View File

@@ -4,6 +4,31 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const Examples = enum {
all,
demo,
// elements:
button,
input,
scrollable,
// layouts:
vertical,
horizontal,
grid,
mixed,
// styles:
text,
palette,
// error handling
errors,
};
const example = b.option(Examples, "example", "Example to build and/or run. (default: all)") orelse .all;
const options = b.addOptions();
options.addOption(Examples, "example", example);
// dependencies
const zg = b.dependency("zg", .{
.target = target,
.optimize = optimize,
@@ -17,22 +42,154 @@ pub fn build(b: *std.Build) void {
});
lib.addImport("code_point", zg.module("code_point"));
const container = b.addExecutable(.{
.name = "container",
.root_source_file = b.path("examples/container.zig"),
//--- Examples ---
// demo:
const demo = b.addExecutable(.{
.name = "demo",
.root_source_file = b.path("examples/demo.zig"),
.target = target,
.optimize = optimize,
});
container.root_module.addImport("zterm", lib);
b.installArtifact(container);
demo.root_module.addImport("zterm", lib);
// testing
// elements:
const button = b.addExecutable(.{
.name = "button",
.root_source_file = b.path("examples/elements/button.zig"),
.target = target,
.optimize = optimize,
});
button.root_module.addImport("zterm", lib);
const input = b.addExecutable(.{
.name = "input",
.root_source_file = b.path("examples/elements/input.zig"),
.target = target,
.optimize = optimize,
});
input.root_module.addImport("zterm", lib);
const scrollable = b.addExecutable(.{
.name = "scrollable",
.root_source_file = b.path("examples/elements/scrollable.zig"),
.target = target,
.optimize = optimize,
});
scrollable.root_module.addImport("zterm", lib);
// layouts:
const vertical = b.addExecutable(.{
.name = "vertical",
.root_source_file = b.path("examples/layouts/vertical.zig"),
.target = target,
.optimize = optimize,
});
vertical.root_module.addImport("zterm", lib);
const horizontal = b.addExecutable(.{
.name = "horizontal",
.root_source_file = b.path("examples/layouts/horizontal.zig"),
.target = target,
.optimize = optimize,
});
horizontal.root_module.addImport("zterm", lib);
const grid = b.addExecutable(.{
.name = "grid",
.root_source_file = b.path("examples/layouts/grid.zig"),
.target = target,
.optimize = optimize,
});
grid.root_module.addImport("zterm", lib);
const mixed = b.addExecutable(.{
.name = "mixed",
.root_source_file = b.path("examples/layouts/mixed.zig"),
.target = target,
.optimize = optimize,
});
mixed.root_module.addImport("zterm", lib);
// styles:
const palette = b.addExecutable(.{
.name = "palette",
.root_source_file = b.path("examples/styles/palette.zig"),
.target = target,
.optimize = optimize,
});
palette.root_module.addImport("zterm", lib);
const text = b.addExecutable(.{
.name = "text",
.root_source_file = b.path("examples/styles/text.zig"),
.target = target,
.optimize = optimize,
});
text.root_module.addImport("zterm", lib);
// error handling:
const errors = b.addExecutable(.{
.name = "errors",
.root_source_file = b.path("examples/errors.zig"),
.target = target,
.optimize = optimize,
});
errors.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step
const exe = switch (example) {
.demo => demo,
// elements:
.button => button,
.input => input,
.scrollable => scrollable,
// layouts:
.vertical => vertical,
.horizontal => horizontal,
.grid => grid,
.mixed => mixed,
// styles:
.text => text,
.palette => palette,
// error handling:
.errors => errors,
else => blk: {
b.installArtifact(button);
b.installArtifact(input);
b.installArtifact(scrollable);
b.installArtifact(vertical);
b.installArtifact(horizontal);
b.installArtifact(grid);
b.installArtifact(mixed);
b.installArtifact(text);
b.installArtifact(palette);
b.installArtifact(errors);
break :blk demo;
},
};
b.installArtifact(exe);
// zig build run
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
// Allow additional arguments, like this: `zig build run -- arg1 arg2 etc`
if (b.args) |args| run_cmd.addArgs(args);
// This creates a build step. It will be visible in the `zig build --help` menu,
// and can be selected like this: `zig build run`
// This will evaluate the `run` step rather than the default, which is "install".
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
// zig build test
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/zterm.zig"),
.target = target,
.optimize = optimize,
});
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);

View File

@@ -6,16 +6,29 @@
//
// It is redundant to include "zig" in this name because it is already
// within the Zig package namespace.
.name = "zterm",
.name = .zterm,
// Together with name, this represents a globally unique package
// identifier. This field is generated by the Zig toolchain when the
// package is first created, and then *never changes*. This allows
// unambiguous detection of one package being an updated version of
// another.
//
// When forking a Zig project, this id should be regenerated (delete the
// field and run `zig build`) if the upstream project is still maintained.
// Otherwise, the fork is *hostile*, attempting to take control over the
// original project's identity. Thus it is recommended to leave the comment
// on the following line intact, so that it shows up in code reviews that
// modify the field.
.fingerprint = 0xf10b37e210a619d7, // Changing this has security and trust implications.
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
.version = "0.2.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// Tracks the earliest Zig version that the package considers to be a
// supported use case.
.minimum_zig_version = "0.15.0-dev.56+d0911786c",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
@@ -24,16 +37,14 @@
// internet connectivity.
.dependencies = .{
.zg = .{
.url = "git+https://codeberg.org/atman/zg#a363f507fc39b96fc48d693665a823a358345326",
.hash = "1220fe42e39fd141c84fd7d5cf69945309bb47253033e68788f99bdfe5585fbc711a",
.url = "git+https://codeberg.org/atman/zg#4a002763419a34d61dcbb1f415821b83b9bf8ddc",
.hash = "1220f3e29bc40856bfc06e0ee133f814b0011c76de987d8a6a458c2f34d82708899a",
},
},
.paths = .{
"LICENSE",
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -1,109 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const Key = zterm.Key;
const log = std.log.scoped(.example);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO: maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory lead", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var container = try App.Container.init(allocator, .{
.border = .{
.color = .blue,
.corners = .rounded,
.sides = .all(),
.separator = .{ .enabled = false },
},
.layout = .{
.padding = .all(5),
.direction = .vertical,
},
});
var box = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.layout = .{
.gap = 1,
.direction = .horizontal,
.padding = .vertical(1),
.sizing = .{
// .width = .{ .fixed = 700 },
// .height = .{ .fixed = 180 },
},
},
});
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}));
try container.append(box);
try container.append(try App.Container.init(allocator, .{
.border = .{ .color = .light_blue, .corners = .squared },
}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}));
defer container.deinit();
// NOTE: should the min-size here be required?
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.init => {
try container.handle(event);
continue; // do not render
},
.quit => break,
.resize => |size| try renderer.resize(size),
.key => |key| {
if (key.matches(.{ .cp = 'q' })) app.quit();
if (key.matches(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
},
});
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ @errorName(err.err), err.msg });
},
else => {},
}
try container.handle(event);
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

158
examples/demo.zig Normal file
View File

@@ -0,0 +1,158 @@
const std = @import("std");
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit. Press ctrl+n to launch helix.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const y = 2;
const x = size.x / 2 -| (text.len / 2);
const anchor = (y * size.x) + x;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
// TODO what should the demo application do?
// - some sort of chat? -> write messages and have them displayed in a scrollable array at the right hand side?
// - on the left some buttons?
var box = try App.Container.init(allocator, .{
.border = .{
.color = .blue,
.sides = .all,
},
.layout = .{
.gap = 1,
.padding = .vertical(2),
.direction = .vertical,
},
.size = .{
.dim = .{ .y = 90 },
},
}, .{});
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
try box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
}, .{}));
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.separator = .{ .enabled = true },
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .horizontal,
},
}, quit_text.element());
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try container.append(try App.Container.init(allocator, .{
.border = .{
.color = .light_blue,
.sides = .all,
},
.size = .{
.dim = .{ .x = 100 },
},
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
.dim = .{ .x = 30 },
},
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
if (key.eql(.{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Spawning $EDITOR failed",
},
});
continue;
}
},
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -0,0 +1,148 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
click: [:0]const u8,
});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const Clickable = struct {
const text = "Press me";
queue: *App.Queue,
color: zterm.Color = .black,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) {
var value = @intFromEnum(this.color);
value += 1;
value %= 17;
if (value == 0) value = 1;
this.color = @enumFromInt(value);
this.queue.push(.{ .click = @tagName(mouse.button) });
},
else => {},
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = size.y / 2 -| (text.len / 2);
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = this.color;
cells[anchor + idx].style.emphasis = &.{.bold};
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var clickable: Clickable = .{ .queue = &app.queue };
const element = clickable.element();
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.click => |button| {
log.info("Clicked with mouse using Button: {s}", .{button});
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

162
examples/elements/input.zig Normal file
View File

@@ -0,0 +1,162 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
accept: []u21,
});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const InputField = struct {
input: std.ArrayList(u21),
queue: *App.Queue,
pub fn init(allocator: std.mem.Allocator, queue: *App.Queue) @This() {
return .{
.input = .init(allocator),
.queue = queue,
};
}
pub fn deinit(this: @This()) void {
this.input.deinit();
}
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| {
if (key.isAscii()) try this.input.append(key.cp);
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.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.input.items.len == 0) return;
const row = 1;
const col = 1;
const anchor = (row * size.x) + col;
for (this.input.items, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var input_field: InputField = .init(allocator, &app.queue);
defer input_field.deinit();
var quit_text: QuitText = .{};
const element = input_field.element();
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .light_grey } }, element));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.accept => |input| {
defer allocator.free(input);
log.info("Accepted input {any}", .{input});
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -0,0 +1,199 @@
const std = @import("std");
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const HelloWorldText = packed struct {
const text = "Hello World";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = size.y / 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});
// TODO maybe create own allocator as some sort of arena allocator to have consistent memory usage
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer {
const deinit_status = gpa.deinit();
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var element_wrapper: HelloWorldText = .{};
const element = element_wrapper.element();
var quit_text: QuitText = .{};
var top_box = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.layout = .{
.gap = 2,
.separator = .{
.enabled = true,
},
.direction = .vertical,
.padding = .vertical(1),
},
}, .{});
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 30 },
},
}, .{}));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 5 },
},
}, element));
try top_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_green },
.size = .{
.dim = .{ .y = 2 },
},
}, .{}));
defer top_box.deinit();
var bottom_box = try App.Container.init(allocator, .{
.border = .{
.sides = .all,
.color = .blue,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .vertical,
.padding = .vertical(1),
},
.size = .{
.dim = .{ .y = 30 },
},
}, .{});
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, element));
try bottom_box.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer bottom_box.deinit();
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.separator = .{
.enabled = true,
.line = .double,
},
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .vertical,
},
}, quit_text.element());
defer container.deinit();
// place empty container containing the element of the scrollable Container.
var scrollable_top: App.Scrollable = .{ .container = top_box };
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .{ .container = bottom_box };
try container.append(try App.Container.init(allocator, .{}, scrollable_bottom.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

159
examples/errors.zig Normal file
View File

@@ -0,0 +1,159 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const InfoText = struct {
const text = "Press any key; Non-Ascii inputs (i.e. `Enter` or `Backspace`) to trigger an exception";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const ErrorNotification = struct {
msg: ?[]const u8 = null,
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .handle = handle, .content = content } };
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.key => |key| if (!key.isAscii()) return zterm.Error.TooSmall,
.err => |err| this.msg = err.msg,
else => {},
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.msg) |msg| {
const row = size.y -| 2;
const col = size.x -| 2 -| msg.len;
const anchor = (row * size.x) + col;
for (msg, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
this.msg = null;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
var info_text: InfoText = .{};
var error_notification: ErrorNotification = .{};
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
},
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{}, info_text.element()));
try container.append(try App.Container.init(allocator, .{}, error_notification.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

115
examples/layouts/grid.zig Normal file
View File

@@ -0,0 +1,115 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.layout = .{
.separator = .{ .enabled = true },
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .horizontal,
},
}, element);
for (0..3) |_| {
var column = try App.Container.init(allocator, .{
.layout = .{
.separator = .{ .enabled = true },
.direction = .vertical,
},
}, .{});
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(column);
}
defer container.deinit(); // also de-initializes the children
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -0,0 +1,107 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.border = .{},
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .horizontal,
},
}, element);
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

123
examples/layouts/mixed.zig Normal file
View File

@@ -0,0 +1,123 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
},
}, element);
for (0..3) |i| {
var column = try App.Container.init(allocator, .{
.layout = .{
.separator = .{ .enabled = true },
.direction = if (i > 0) .vertical else .horizontal,
},
}, .{});
if (i != 1) {
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .green },
}, .{}));
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .yellow },
}, .{}));
} else {
try column.append(try App.Container.init(allocator, .{
.size = .{
.dim = .{ .y = 4 },
.grow = .horizontal,
},
}, .{}));
}
try column.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(column);
}
defer container.deinit(); // also de-initializes the children
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -0,0 +1,106 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
.direction = .vertical,
},
}, element);
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
}, .{}));
defer container.deinit(); // also de-initializes the children
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
// NOTE errors could be displayed in another container in case one was received, etc. to provide the user with feedback
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
// NOTE returned errors should be propagated back to the application
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

102
examples/styles/palette.zig Normal file
View File

@@ -0,0 +1,102 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
},
}, element);
defer container.deinit();
var box = try App.Container.init(allocator, .{
.layout = .{ .direction = .horizontal },
}, .{});
defer box.deinit();
inline for (std.meta.fields(zterm.Color)) |field| {
if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
}
var scrollable: App.Scrollable = .{ .container = box };
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

157
examples/styles/text.zig Normal file
View File

@@ -0,0 +1,157 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});
const log = std.log.scoped(.default);
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const TextStyles = struct {
const text = "Example";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
@setEvalBranchQuota(50000);
_ = ctx;
std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
var row: usize = 0;
var col: usize = 0;
// Color
inline for (std.meta.fields(zterm.Color)) |bg_field| {
if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
inline for (std.meta.fields(zterm.Color)) |fg_field| {
if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (comptime fg_field.value == bg_field.value) continue;
// witouth any emphasis
for (text) |cp| {
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.x) + col].cp = cp;
col += 1;
}
// emphasis (no combinations)
inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| {
if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip
const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value);
for (text) |cp| {
cells[(row * size.x) + col].style.bg = @enumFromInt(bg_field.value);
cells[(row * size.x) + col].style.fg = @enumFromInt(fg_field.value);
cells[(row * size.x) + col].style.emphasis = &.{emphasis};
cells[(row * size.x) + col].cp = cp;
col += 1;
}
}
row += 1;
col = 0;
}
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var quit_text: QuitText = .{};
const element = quit_text.element();
var text_styles: TextStyles = .{};
var container = try App.Container.init(allocator, .{
.layout = .{
.gap = 2,
.padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 },
},
}, element);
defer container.deinit();
var box = try App.Container.init(allocator, .{
.layout = .{ .direction = .vertical },
.size = .{
.dim = .{
.x = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len,
.y = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2),
},
},
}, text_styles.element());
defer box.deinit();
var scrollable: App.Scrollable = .{ .container = box };
try container.append(try App.Container.init(allocator, .{}, scrollable.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
try renderer.resize();
container.resize(renderer.size);
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}

View File

@@ -1,14 +1,17 @@
//! Application type for TUI-applications
const std = @import("std");
const terminal = @import("terminal.zig");
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 Key = @import("key.zig");
const Size = @import("size.zig").Size;
const Queue = @import("queue.zig").Queue;
const Mouse = input.Mouse;
const Key = input.Key;
const Point = @import("point.zig").Point;
const log = std.log.scoped(.app);
@@ -27,8 +30,8 @@ const log = std.log.scoped(.app);
/// union(enum) {},
/// );
/// // later on create an `App` instance and start the event loop
/// var app: App = .{};
/// try app.start(null);
/// var app: App = .init;
/// try app.start();
/// defer app.stop() catch unreachable;
/// ```
pub fn App(comptime E: type) type {
@@ -38,19 +41,30 @@ pub fn App(comptime E: type) type {
return struct {
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Event);
const element = @import("element.zig");
pub const Element = element.Element(Event);
pub const Scrollable = element.Scrollable(Event);
pub const Queue = queue.Queue(Event, 256);
queue: Queue(Event, 256) = .{},
thread: ?std.Thread = null,
quit_event: std.Thread.ResetEvent = .{},
queue: Queue,
thread: ?std.Thread,
quit_event: std.Thread.ResetEvent,
termios: ?std.posix.termios = null,
attached_handler: bool = false,
prev_size: Size = .{ .cols = 0, .rows = 0 },
pub const SignalHandler = struct {
context: *anyopaque,
callback: *const fn (context: *anyopaque) void,
};
pub const init: @This() = .{
.queue = .{},
.thread = null,
.quit_event = .{},
.termios = null,
.attached_handler = false,
};
pub fn start(this: *@This()) !void {
if (this.thread) |_| return;
@@ -82,15 +96,12 @@ pub fn App(comptime E: type) type {
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
// send initial size afterwards
const size = terminal.getTerminalSize();
this.postEvent(.{ .resize = size });
this.prev_size = size;
try terminal.enableMouseSupport();
}
pub fn interrupt(this: *@This()) !void {
this.quit_event.set();
try terminal.disableMouseSupport();
try terminal.exitAltScreen();
try terminal.restoreScreen();
if (this.thread) |thread| {
@@ -102,9 +113,10 @@ pub fn App(comptime E: type) type {
pub fn stop(this: *@This()) !void {
try this.interrupt();
if (this.termios) |*termios| {
try terminal.disableRawMode(termios);
try terminal.disableMouseSupport();
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.disableRawMode(termios);
try terminal.restoreScreen();
}
this.termios = null;
@@ -129,11 +141,8 @@ pub fn App(comptime E: type) type {
fn winsizeCallback(ptr: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ptr));
const size = terminal.getTerminalSize();
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
this.postEvent(.{ .resize = size });
this.prev_size = size;
}
_ = this;
// this.postEvent(.{ .size = terminal.getTerminalSize() });
}
var winch_handler: ?SignalHandler = null;
@@ -152,39 +161,35 @@ pub fn App(comptime E: type) type {
}
fn run(this: *@This()) !void {
// send initial terminal size
// changes are handled by the winch signal handler
// see `App.start` and `App.registerWinch` for details
{}
// thread to read user inputs
var buf: [256]u8 = undefined;
while (true) {
// FIX: I still think that there is a race condition (I'm just waiting 'long' enough)
// FIX I still think that there is a race condition (I'm just waiting 'long' enough)
this.quit_event.timedWait(20 * std.time.ns_per_ms) catch {
// FIX in case the queue is full -> the next user input should panic and quit the application? because something seems to clock up the event queue
const read_bytes = try terminal.read(buf[0..]);
// TODO `break` should not terminate the reading of the user inputs, but instead only the received faulty input!
// escape key presses
if (buf[0] == 0x1b and read_bytes > 1) {
switch (buf[1]) {
0x4F => { // ss3
if (read_bytes < 3) continue;
const key: ?Key = switch (buf[2]) {
0x1B => null,
'A' => .{ .cp = Key.up },
'B' => .{ .cp = Key.down },
'C' => .{ .cp = Key.right },
'D' => .{ .cp = Key.left },
'E' => .{ .cp = Key.kp_begin },
'F' => .{ .cp = Key.end },
'H' => .{ .cp = Key.home },
'P' => .{ .cp = Key.f1 },
'Q' => .{ .cp = Key.f2 },
'R' => .{ .cp = Key.f3 },
'S' => .{ .cp = Key.f4 },
else => null,
const key: Key = switch (buf[2]) {
'A' => .{ .cp = input.Up },
'B' => .{ .cp = input.Down },
'C' => .{ .cp = input.Right },
'D' => .{ .cp = input.Left },
'E' => .{ .cp = input.KpBegin },
'F' => .{ .cp = input.End },
'H' => .{ .cp = input.Home },
'P' => .{ .cp = input.F1 },
'Q' => .{ .cp = input.F2 },
'R' => .{ .cp = input.F3 },
'S' => .{ .cp = input.F4 },
else => continue,
};
if (key) |k| this.postEvent(.{ .key = k });
this.postEvent(.{ .key = key });
},
0x5B => { // csi
if (read_bytes < 3) continue;
@@ -205,17 +210,17 @@ pub fn App(comptime E: type) type {
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
const key: Key = .{
.cp = switch (final) {
'A' => Key.up,
'B' => Key.down,
'C' => Key.right,
'D' => Key.left,
'E' => Key.kp_begin,
'F' => Key.end,
'H' => Key.home,
'P' => Key.f1,
'Q' => Key.f2,
'R' => Key.f3,
'S' => Key.f4,
'A' => input.Up,
'B' => input.Down,
'C' => input.Right,
'D' => input.Left,
'E' => input.KpBegin,
'F' => input.End,
'H' => input.Home,
'P' => input.F1,
'Q' => input.F2,
'R' => input.F3,
'S' => input.F4,
else => unreachable, // switch case prevents in this case form ever happening
},
};
@@ -232,35 +237,76 @@ pub fn App(comptime E: type) type {
const key: Key = .{
.cp = switch (number) {
2 => Key.insert,
3 => Key.delete,
5 => Key.page_up,
6 => Key.page_down,
7 => Key.home,
8 => Key.end,
11 => Key.f1,
12 => Key.f2,
13 => Key.f3,
14 => Key.f4,
15 => Key.f5,
17 => Key.f6,
18 => Key.f7,
19 => Key.f8,
20 => Key.f9,
21 => Key.f10,
23 => Key.f11,
24 => Key.f12,
2 => input.Insert,
3 => input.Delete,
5 => input.PageUp,
6 => input.PageDown,
7 => input.Home,
8 => input.End,
11 => input.F1,
12 => input.F2,
13 => input.F3,
14 => input.F4,
15 => input.F5,
17 => input.F6,
18 => input.F7,
19 => input.F8,
20 => input.F9,
21 => input.F10,
23 => input.F11,
24 => input.F12,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57427 => Key.kp_begin,
57427 => input.KpBegin,
else => unreachable,
},
};
this.postEvent(.{ .key = key });
},
// TODO focus usage? should this even be in the default event system?
'I' => this.postEvent(.{ .focus = true }),
'O' => this.postEvent(.{ .focus = false }),
// 'M', 'm' => return parseMouse(sequence), // TODO: parse mouse inputs
'M', 'm' => {
std.debug.assert(sequence.len >= 4);
if (sequence[2] != '<') break;
const delim1 = std.mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = std.fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = std.mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = std.fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = std.fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const mouse_bits = packed struct {
const motion: u8 = 0b00100000;
const buttons: u8 = 0b11000011;
const shift: u8 = 0b00000100;
const alt: u8 = 0b00001000;
const ctrl: u8 = 0b00010000;
};
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
const motion = button_mask & mouse_bits.motion > 0;
// const shift = button_mask & mouse_bits.shift > 0;
// const alt = button_mask & mouse_bits.alt > 0;
// const ctrl = button_mask & mouse_bits.ctrl > 0;
const mouse: Mouse = .{
.button = button,
.x = px -| 1,
.y = py -| 1,
.kind = blk: {
if (motion and button != Mouse.Button.none) {
break :blk .drag;
}
if (motion and button == Mouse.Button.none) {
break :blk .motion;
}
if (sequence[sequence.len - 1] == 'm') break :blk .release;
break :blk .press;
},
};
this.postEvent(.{ .mouse = mouse });
},
'c' => {
// Primary DA (CSI ? Pm c)
},
@@ -278,19 +324,15 @@ pub fn App(comptime E: type) type {
if (std.mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const height_char = iter.next() orelse break;
const width_char = iter.next() orelse break;
const height_char = iter.next() orelse break;
// TODO: only post the event if the size has changed?
// because there might be too many resize events (which force a re-draw of the entire screen)
const size: Size = .{
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
};
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
this.postEvent(.{ .resize = size });
this.prev_size = size;
}
_ = width_char;
_ = height_char;
// this.postEvent(.{ .size = .{
// .x = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
// } });
}
},
'u' => {
@@ -305,7 +347,7 @@ pub fn App(comptime E: type) type {
else => {},
}
},
// TODO: parse corresponding codes
// TODO parse corresponding codes
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
else => {},
}
@@ -313,17 +355,17 @@ pub fn App(comptime E: type) type {
const b = buf[0];
const key: Key = switch (b) {
0x00 => .{ .cp = '@', .mod = .{ .ctrl = true } },
0x08 => .{ .cp = Key.backspace },
0x09 => .{ .cp = Key.tab },
0x0a, 0x0d => .{ .cp = Key.enter },
0x08 => .{ .cp = input.Backspace },
0x09 => .{ .cp = input.Tab },
0x0a, 0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
std.debug.assert(read_bytes == 1);
break :escape .{ .cp = Key.escape };
break :escape .{ .cp = input.Escape };
},
0x7f => .{ .cp = Key.backspace },
0x7f => .{ .cp = input.Backspace },
else => {
var iter = terminal.code_point.Iterator{ .bytes = buf[0..read_bytes] };
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
continue;
},

View File

@@ -3,8 +3,8 @@ const Style = @import("style.zig");
pub const Cell = @This();
style: Style = .{ .attributes = &.{} },
// TODO: embrace `zg` dependency more due to utf-8 encoding
style: Style = .{ .emphasis = &.{} },
// TODO embrace `zg` dependency more due to utf-8 encoding
cp: u21 = ' ',
pub fn eql(this: Cell, other: Cell) bool {
@@ -12,10 +12,54 @@ pub fn eql(this: Cell, other: Cell) bool {
}
pub fn reset(this: *Cell) void {
this.style = .{ .attributes = &.{} };
this.style = .{ .emphasis = &.{} };
this.cp = ' ';
}
pub fn value(this: Cell, writer: anytype) !void {
try this.style.value(writer, this.cp);
}
test "ascii styled text" {
const cells: [4]Cell = .{
.{ .cp = 'Y', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = 'v', .style = .{ .emphasis = &.{ .bold, .underline } } },
.{ .cp = 'e', .style = .{ .emphasis = &.{.italic} } },
.{ .cp = 's', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(writer);
}
try std.testing.expectEqualSlices(
u8,
"\x1b[38;5;10;48;5;8;59mY\x1b[0m\x1b[39;49;59;1;4mv\x1b[0m\x1b[39;49;59;3me\x1b[0m\x1b[38;5;2;48;5;16;59;4ms\x1b[0m",
string.items,
);
}
test "utf-8 styled text" {
const cells: [4]Cell = .{
.{ .cp = '╭', .style = .{ .fg = .green, .bg = .grey, .emphasis = &.{} } },
.{ .cp = '─', .style = .{ .emphasis = &.{} } },
.{ .cp = '┄', .style = .{ .emphasis = &.{} } },
.{ .cp = '┘', .style = .{ .fg = .light_green, .bg = .black, .emphasis = &.{.underline} } },
};
var string = std.ArrayList(u8).init(std.testing.allocator);
defer string.deinit();
const writer = string.writer();
for (cells) |cell| {
try cell.value(writer);
}
try std.testing.expectEqualSlices(
u8,
"\x1b[38;5;10;48;5;8;59m╭\x1b[0m\x1b[39;49;59m─\x1b[0m\x1b[39;49;59m┄\x1b[0m\x1b[38;5;2;48;5;16;59;4m┘\x1b[0m",
string.items,
);
}

View File

@@ -18,7 +18,7 @@ pub const Color = enum(u8) {
magenta,
cyan,
white,
// TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
// TODO add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
if (this == .default) {

File diff suppressed because it is too large Load Diff

View File

@@ -58,8 +58,11 @@ pub const cub = "\x1b[{d}D";
// Erase
pub const erase_below_cursor = "\x1b[J";
pub const clear_screen = "\x1b[2J";
// alt screen
pub const save_screen = "\x1b[?47h";
pub const restore_screen = "\x1b[?47l";
pub const smcup = "\x1b[?1049h";
pub const rmcup = "\x1b[?1049l";
@@ -89,7 +92,7 @@ pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
// Underlines
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
pub const ul_off = "\x1b[24m"; // NOTE this could be \x1b[4:0m but is not as widely supported
pub const ul_single = "\x1b[4m";
pub const ul_double = "\x1b[4:2m";
pub const ul_curly = "\x1b[4:3m";

View File

@@ -0,0 +1,344 @@
//! Interface for Element's which describe the contents of a `Container`.
const std = @import("std");
const input = @import("input.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;
pub fn Element(Event: type) type {
return struct {
ptr: *anyopaque = undefined,
vtable: *const VTable = &.{},
pub const VTable = struct {
resize: ?*const fn (ctx: *anyopaque, size: Point) void = null,
reposition: ?*const fn (ctx: *anyopaque, origin: Point) void = null,
handle: ?*const fn (ctx: *anyopaque, event: Event) anyerror!void = null,
content: ?*const fn (ctx: *anyopaque, cells: []Cell, size: Point) anyerror!void = null,
};
/// Resize the corresponding `Element` with the given *size*.
pub inline fn resize(this: @This(), size: Point) void {
if (this.vtable.resize) |resize_fn|
resize_fn(this.ptr, size);
}
/// Reposition the corresponding `Element` with the given *origin*.
pub inline fn reposition(this: @This(), origin: Point) void {
if (this.vtable.reposition) |reposition_fn|
reposition_fn(this.ptr, origin);
}
/// Handle the received event. The event is one of the user provided
/// events or a system event, with the exception of the `.size`
/// `Event` as every `Container` already handles that event.
///
/// In case of user errors this function should return an error. This
/// error may then be used by the application to display information
/// about the user error.
pub inline fn handle(this: @This(), event: Event) !void {
if (this.vtable.handle) |handle_fn|
try handle_fn(this.ptr, event);
}
/// Write content into the `cells` of the `Container`. The associated
/// `cells` slice has the size of (`size.x * size.y`). The
/// renderer will know where to place the contents on the screen.
///
/// # Note
///
/// - Caller owns `cells` slice and ensures that the size usually by assertion:
/// ```zig
/// std.debug.assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
/// ```
///
/// - This function should only fail with an error if the error is
/// non-recoverable (i.e. an allocation error, system error, etc.).
/// Otherwise user specific errors should be caught using the `handle`
/// function before the rendering of the `Container` happens.
pub inline fn content(this: @This(), cells: []Cell, size: Point) !void {
if (this.vtable.content) |content_fn|
try content_fn(this.ptr, cells, size);
}
};
}
pub fn Scrollable(Event: type) type {
return struct {
/// `Size` of the actual contents where the anchor and the size is
/// representing the size and location on screen.
size: Point = .{},
/// `Size` of the `Container` content that is scrollable and mapped to
/// the *size* of the `Scrollable` `Element`.
container_size: Point = .{},
/// Anchor of the viewport of the scrollable `Container`.
anchor: Point = .{},
/// The actual `Container`, that is scrollable.
container: Container(Event),
pub fn element(this: *@This()) Element(Event) {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.size = size;
// TODO scrollbar space - depending on configuration and only if necessary?
this.container.resize(this.size);
this.container_size = this.container.size;
}
fn reposition(ctx: *anyopaque, _: Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
this.container.reposition(.{});
}
fn handle(ctx: *anyopaque, event: Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO other means to scroll except with the mouse? (i.e. Ctrl-u/d, k/j, etc.?)
.mouse => |mouse| switch (mouse.button) {
Mouse.Button.wheel_up => if (this.container_size.y > this.size.y) {
this.anchor.y -|= 1;
},
Mouse.Button.wheel_down => if (this.container_size.y > this.size.y) {
const max_origin_y = this.container_size.y -| this.size.y;
this.anchor.y = @min(this.anchor.y + 1, max_origin_y);
},
Mouse.Button.wheel_left => if (this.container_size.x > this.size.x) {
this.anchor.x -|= 1;
},
Mouse.Button.wheel_right => if (this.container_size.x > this.size.x) {
const max_anchor_x = this.container_size.x -| this.size.x;
this.anchor.x = @min(this.anchor.x + 1, max_anchor_x);
},
else => try this.container.handle(.{
.mouse = .{
.x = mouse.x + this.anchor.x,
.y = mouse.y + this.anchor.y,
.button = mouse.button,
.kind = mouse.kind,
},
}),
},
else => try this.container.handle(event),
}
}
fn render_container(container: Container(Event), cells: []Cell, container_size: Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
defer container.allocator.free(contents);
const anchor = (@as(usize, origin.y) * @as(usize, container_size.x)) + @as(usize, origin.x);
var idx: usize = 0;
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
cells[anchor + (row * container_size.x) + col] = contents[idx];
idx += 1;
if (contents.len == idx) break :blk;
}
}
for (container.elements.items) |child| try render_container(child, cells, size);
}
fn content(ctx: *anyopaque, cells: []Cell, size: Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
std.debug.assert(cells.len == @as(usize, this.size.x) * @as(usize, this.size.y));
const container_size = this.container.size;
const container_cells = try this.container.allocator.alloc(Cell, @as(usize, container_size.x) * @as(usize, container_size.y));
{
const container_cells_const = try this.container.content();
defer this.container.allocator.free(container_cells_const);
std.debug.assert(container_cells_const.len == @as(usize, container_size.x) * @as(usize, container_size.y));
@memcpy(container_cells, container_cells_const);
}
for (this.container.elements.items) |child| try render_container(child, container_cells, container_size);
const anchor = (@as(usize, this.anchor.y) * @as(usize, container_size.x)) + @as(usize, this.anchor.x);
// TODO render scrollbar according to configuration!
for (0..size.y) |row| {
for (0..size.x) |col| {
cells[(row * size.x) + col] = container_cells[anchor + (row * container_size.x) + col];
}
}
this.container.allocator.free(container_cells);
}
};
}
// TODO nested scrollable `Container`s?'
// TODO reaction only for when the event is actually pushed to the corresponding `Container` rendered container
test "scrollable vertical" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .red,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .vertical,
.padding = .all(1),
},
.size = .{
.dim = .{ .y = size.y + 15 },
},
}, .{});
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.color = .green,
.sides = .vertical,
},
}, scrollable.element());
defer container.deinit();
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.top.zon"), renderer.screen);
// scroll down 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
// further scrolling down will not change anything
try container.handle(.{
.mouse = .{
.button = .wheel_down,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.vertical.bottom.zon"), renderer.screen);
}
test "scrollable horizontal" {
const event = @import("event.zig");
const testing = @import("testing.zig");
const allocator = std.testing.allocator;
const size: Point = .{
.x = 30,
.y = 20,
};
var box: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .red,
},
.layout = .{
.separator = .{
.enabled = true,
.color = .red,
},
.direction = .horizontal,
.padding = .all(1),
},
.size = .{
.dim = .{ .x = size.x + 15 },
},
}, .{});
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
try box.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, .{}));
defer box.deinit();
var scrollable: Scrollable(event.SystemEvent) = .{ .container = box };
var container: Container(event.SystemEvent) = try .init(allocator, .{
.border = .{
.color = .green,
.sides = .horizontal,
},
}, scrollable.element());
defer container.deinit();
var renderer: testing.Renderer = .init(allocator, size);
defer renderer.deinit();
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.left.zon"), renderer.screen);
// scroll right 15 times (exactly to the end)
for (0..15) |_| try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
// further scrolling right will not change anything
try container.handle(.{
.mouse = .{
.button = .wheel_right,
.kind = .press,
.x = 5,
.y = 5,
},
});
try renderer.render(Container(event.SystemEvent), &container);
try testing.expectEqualCells(.{}, renderer.size, @import("test/element/scrollable.horizontal.right.zon"), renderer.screen);
}

4
src/error.zig Normal file
View 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,
};

View File

@@ -1,15 +1,17 @@
//! Events which are defined by the library. They might be extended by user
//! events. See `App` for more details about user defined events.
const std = @import("std");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const Size = @import("size.zig").Size;
const Key = @import("key.zig");
const Key = input.Key;
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;
/// System events available to every `zterm.App`
pub const SystemEvent = union(enum) {
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
/// TODO: not sure if this is necessary or if there is an actual usecase for this - for now it will remain
/// TODO not sure if this is necessary or if there is an actual usecase for this - for now it will remain
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,
@@ -19,12 +21,12 @@ pub const SystemEvent = union(enum) {
/// associated error message
msg: []const u8,
},
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
resize: Size,
/// Input key event received from the user
key: Key,
/// Mouse input event
mouse: Mouse,
/// Focus event for mouse interaction
/// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for
/// TODO this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool,
};

225
src/input.zig Normal file
View File

@@ -0,0 +1,225 @@
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
const std = @import("std");
const Point = @import("point.zig").Point;
pub const Mouse = packed struct {
x: u16,
y: u16,
button: Button,
kind: Kind,
pub const Button = enum(u8) {
left,
middle,
right,
none,
wheel_up = 64,
wheel_down = 65,
wheel_right = 66,
wheel_left = 67,
button_8 = 128,
button_9 = 129,
button_10 = 130,
button_11 = 131,
};
pub const Kind = enum(u2) {
press,
release,
motion,
drag,
};
pub fn eql(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
}
pub fn in(this: @This(), origin: Point, size: Point) bool {
return this.x >= origin.x and this.x < size.x + origin.x and
this.y >= origin.y and this.y < size.y + origin.y;
}
};
pub const Key = packed struct {
cp: u21,
mod: Modifier = .{},
pub const Modifier = packed struct {
shift: bool = false,
alt: bool = false,
ctrl: bool = false,
};
/// Compare _this_ `Key` with an _other_ `Key`.
///
/// # Example
///
/// Configure `ctrl+c` to quit the application (done in main event loop of the application):
///
/// ```zig
/// switch (event) {
/// .quit => break,
/// .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit.set(),
/// else => {},
/// }
/// ```
pub fn eql(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
}
/// Determine if the `Key` is an ascii character that can be printed to
/// the screen. This means that the code point of the `Key` is an ascii
/// character between 32 - 255 (with the exception of 127 = Delete) and no
/// 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.isAscii()) try this.input.append(key.cp),
/// else => {},
/// }
/// ```
pub fn isAscii(this: @This()) bool {
return this.mod.alt == false and this.mod.ctrl == false and // no modifier keys
(this.cp >= 32 and this.cp <= 126 or // ascii printable characters (except for input.Delete)
this.cp >= 128 and this.cp <= 255); // extended ascii codes
}
test "isAscii with ascii character" {
try std.testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
try std.testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
}
test "isAscii with non-ascii character" {
try std.testing.expectEqual(false, isAscii(.{ .cp = Escape }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
}
test "isAscii with excluded input.Delete" {
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete }));
try std.testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
}
};
// codepoints for keys
pub const Tab: u21 = 0x09;
pub const Enter: u21 = 0x0D;
pub const Escape: u21 = 0x1B;
pub const Space: u21 = 0x20;
pub const Backspace: u21 = 0x7F;
// kitty key encodings (re-used here)
pub const Insert: u21 = 57348;
pub const Delete: u21 = 57349;
pub const Left: u21 = 57350;
pub const Right: u21 = 57351;
pub const Up: u21 = 57352;
pub const Down: u21 = 57353;
pub const PageUp: u21 = 57354;
pub const PageDown: u21 = 57355;
pub const Home: u21 = 57356;
pub const End: u21 = 57357;
pub const CapsLock: u21 = 57358;
pub const ScrollLock: u21 = 57359;
pub const NumLock: u21 = 57360;
pub const PrintScreen: u21 = 57361;
pub const Pause: u21 = 57362;
pub const Menu: u21 = 57363;
pub const F1: u21 = 57364;
pub const F2: u21 = 57365;
pub const F3: u21 = 57366;
pub const F4: u21 = 57367;
pub const F5: u21 = 57368;
pub const F6: u21 = 57369;
pub const F7: u21 = 57370;
pub const F8: u21 = 57371;
pub const F9: u21 = 57372;
pub const F10: u21 = 57373;
pub const F11: u21 = 57374;
pub const F12: u21 = 57375;
pub const F13: u21 = 57376;
pub const F14: u21 = 57377;
pub const F15: u21 = 57378;
pub const F16: u21 = 57379;
pub const F17: u21 = 57380;
pub const F18: u21 = 57381;
pub const F19: u21 = 57382;
pub const F20: u21 = 57383;
pub const F21: u21 = 57384;
pub const F22: u21 = 57385;
pub const F23: u21 = 57386;
pub const F24: u21 = 57387;
pub const F25: u21 = 57388;
pub const F26: u21 = 57389;
pub const F27: u21 = 57390;
pub const F28: u21 = 57391;
pub const F29: u21 = 57392;
pub const F30: u21 = 57393;
pub const F31: u21 = 57394;
pub const F32: u21 = 57395;
pub const F33: u21 = 57396;
pub const F34: u21 = 57397;
pub const F35: u21 = 57398;
pub const Kp0: u21 = 57399;
pub const Kp1: u21 = 57400;
pub const Kp2: u21 = 57401;
pub const Kp3: u21 = 57402;
pub const Kp4: u21 = 57403;
pub const Kp5: u21 = 57404;
pub const Kp6: u21 = 57405;
pub const Kp7: u21 = 57406;
pub const Kp8: u21 = 57407;
pub const Kp9: u21 = 57408;
pub const KpDecimal: u21 = 57409;
pub const KpDivide: u21 = 57410;
pub const KpMultiply: u21 = 57411;
pub const KpSubtract: u21 = 57412;
pub const KpAdd: u21 = 57413;
pub const KpEnter: u21 = 57414;
pub const KpEqual: u21 = 57415;
pub const KpSeparator: u21 = 57416;
pub const KpLeft: u21 = 57417;
pub const KpRight: u21 = 57418;
pub const KpUp: u21 = 57419;
pub const KpDown: u21 = 57420;
pub const KpPageUp: u21 = 57421;
pub const KpPageDown: u21 = 57422;
pub const KpHome: u21 = 57423;
pub const KpEnd: u21 = 57424;
pub const KpInsert: u21 = 57425;
pub const KpDelete: u21 = 57426;
pub const KpBegin: u21 = 57427;
pub const MediaPlay: u21 = 57428;
pub const MediaPause: u21 = 57429;
pub const MediaPlayPause: u21 = 57430;
pub const MediaReverse: u21 = 57431;
pub const MediaStop: u21 = 57432;
pub const MediaFastForward: u21 = 57433;
pub const MediaRewind: u21 = 57434;
pub const MediaTrackNext: u21 = 57435;
pub const MediaTrackPrevious: u21 = 57436;
pub const MediaRecord: u21 = 57437;
pub const LowerVolume: u21 = 57438;
pub const RaiseVolume: u21 = 57439;
pub const MuteVolume: u21 = 57440;
pub const LeftShift: u21 = 57441;
pub const LeftControl: u21 = 57442;
pub const LeftAlt: u21 = 57443;
pub const LeftSuper: u21 = 57444;
pub const LeftHyper: u21 = 57445;
pub const LeftMeta: u21 = 57446;
pub const RightShift: u21 = 57447;
pub const RightControl: u21 = 57448;
pub const RightAlt: u21 = 57449;
pub const RightSuper: u21 = 57450;
pub const RightHyper: u21 = 57451;
pub const RightMeta: u21 = 57452;
pub const IsoLevel3Shift: u21 = 57453;
pub const IsoLevel5Shift: u21 = 57454;

View File

@@ -1,151 +0,0 @@
//! Keybindings and Modifiers for user input detection and selection.
const std = @import("std");
pub const Key = @This();
pub const Modifier = struct {
shift: bool = false,
alt: bool = false,
ctrl: bool = false,
};
cp: u21,
mod: Modifier = .{},
/// Compare _this_ `Key` with an _other_ `Key`.
///
/// # Example
///
/// Configure `ctrl+c` to quit the application (done in main event loop of the application):
///
/// ```zig
/// switch (event) {
/// .quit => break,
/// .key => |key| {
/// // ctrl+c to quit
/// if (terminal.Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
/// app.quit.set();
/// }
/// },
/// else => {},
/// }
/// ```
pub fn matches(this: @This(), other: @This()) bool {
return std.meta.eql(this, other);
}
// codepoints for keys
pub const tab: u21 = 0x09;
pub const enter: u21 = 0x0D;
pub const escape: u21 = 0x1B;
pub const space: u21 = 0x20;
pub const backspace: u21 = 0x7F;
// kitty key encodings (re-used here)
pub const insert: u21 = 57348;
pub const delete: u21 = 57349;
pub const left: u21 = 57350;
pub const right: u21 = 57351;
pub const up: u21 = 57352;
pub const down: u21 = 57353;
pub const page_up: u21 = 57354;
pub const page_down: u21 = 57355;
pub const home: u21 = 57356;
pub const end: u21 = 57357;
pub const caps_lock: u21 = 57358;
pub const scroll_lock: u21 = 57359;
pub const num_lock: u21 = 57360;
pub const print_screen: u21 = 57361;
pub const pause: u21 = 57362;
pub const menu: u21 = 57363;
pub const f1: u21 = 57364;
pub const f2: u21 = 57365;
pub const f3: u21 = 57366;
pub const f4: u21 = 57367;
pub const f5: u21 = 57368;
pub const f6: u21 = 57369;
pub const f7: u21 = 57370;
pub const f8: u21 = 57371;
pub const f9: u21 = 57372;
pub const f10: u21 = 57373;
pub const f11: u21 = 57374;
pub const f12: u21 = 57375;
pub const f13: u21 = 57376;
pub const f14: u21 = 57377;
pub const f15: u21 = 57378;
pub const @"f16": u21 = 57379;
pub const f17: u21 = 57380;
pub const f18: u21 = 57381;
pub const f19: u21 = 57382;
pub const f20: u21 = 57383;
pub const f21: u21 = 57384;
pub const f22: u21 = 57385;
pub const f23: u21 = 57386;
pub const f24: u21 = 57387;
pub const f25: u21 = 57388;
pub const f26: u21 = 57389;
pub const f27: u21 = 57390;
pub const f28: u21 = 57391;
pub const f29: u21 = 57392;
pub const f30: u21 = 57393;
pub const f31: u21 = 57394;
pub const @"f32": u21 = 57395;
pub const f33: u21 = 57396;
pub const f34: u21 = 57397;
pub const f35: u21 = 57398;
pub const kp_0: u21 = 57399;
pub const kp_1: u21 = 57400;
pub const kp_2: u21 = 57401;
pub const kp_3: u21 = 57402;
pub const kp_4: u21 = 57403;
pub const kp_5: u21 = 57404;
pub const kp_6: u21 = 57405;
pub const kp_7: u21 = 57406;
pub const kp_8: u21 = 57407;
pub const kp_9: u21 = 57408;
pub const kp_decimal: u21 = 57409;
pub const kp_divide: u21 = 57410;
pub const kp_multiply: u21 = 57411;
pub const kp_subtract: u21 = 57412;
pub const kp_add: u21 = 57413;
pub const kp_enter: u21 = 57414;
pub const kp_equal: u21 = 57415;
pub const kp_separator: u21 = 57416;
pub const kp_left: u21 = 57417;
pub const kp_right: u21 = 57418;
pub const kp_up: u21 = 57419;
pub const kp_down: u21 = 57420;
pub const kp_page_up: u21 = 57421;
pub const kp_page_down: u21 = 57422;
pub const kp_home: u21 = 57423;
pub const kp_end: u21 = 57424;
pub const kp_insert: u21 = 57425;
pub const kp_delete: u21 = 57426;
pub const kp_begin: u21 = 57427;
pub const media_play: u21 = 57428;
pub const media_pause: u21 = 57429;
pub const media_play_pause: u21 = 57430;
pub const media_reverse: u21 = 57431;
pub const media_stop: u21 = 57432;
pub const media_fast_forward: u21 = 57433;
pub const media_rewind: u21 = 57434;
pub const media_track_next: u21 = 57435;
pub const media_track_previous: u21 = 57436;
pub const media_record: u21 = 57437;
pub const lower_volume: u21 = 57438;
pub const raise_volume: u21 = 57439;
pub const mute_volume: u21 = 57440;
pub const left_shift: u21 = 57441;
pub const left_control: u21 = 57442;
pub const left_alt: u21 = 57443;
pub const left_super: u21 = 57444;
pub const left_hyper: u21 = 57445;
pub const left_meta: u21 = 57446;
pub const right_shift: u21 = 57447;
pub const right_control: u21 = 57448;
pub const right_alt: u21 = 57449;
pub const right_super: u21 = 57450;
pub const right_hyper: u21 = 57451;
pub const right_meta: u21 = 57452;
pub const iso_level_3_shift: u21 = 57453;
pub const iso_level_5_shift: u21 = 57454;

60
src/point.zig Normal file
View 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;
}

View File

View File

@@ -2,16 +2,13 @@ const std = @import("std");
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Point = @import("point.zig").Point;
/// Double-buffered intermediate rendering pipeline
pub const Buffered = struct {
const log = std.log.scoped(.renderer_buffered);
// _ = log;
allocator: std.mem.Allocator,
created: bool,
size: Size,
size: Point,
screen: []Cell,
virtual_screen: []Cell,
@@ -32,22 +29,26 @@ pub const Buffered = struct {
}
}
pub fn resize(this: *@This(), size: Size) !void {
log.debug("renderer::resize", .{});
defer this.size = size;
pub fn resize(this: *@This()) !void {
const size = terminal.getTerminalSize();
if (std.meta.eql(this.size, size)) return;
this.size = size;
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
if (!this.created) {
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
@memset(this.screen, .{});
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, .{});
this.created = true;
} else {
this.allocator.free(this.screen);
this.screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
@memset(this.screen, .{});
this.screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
this.allocator.free(this.virtual_screen);
this.virtual_screen = this.allocator.alloc(Cell, @as(usize, size.cols) * @as(usize, size.rows)) catch @panic("render.zig: Out of memory.");
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, .{});
}
try this.clear();
@@ -55,57 +56,51 @@ pub const Buffered = struct {
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
pub fn clear(this: *@This()) !void {
log.debug("renderer::clear", .{});
try terminal.clearScreen();
@memset(this.screen, .{});
}
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const viewport: Size = container.viewport;
const cells: []const Cell = try container.contents();
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
var idx: usize = 0;
var vs = this.virtual_screen;
const anchor: usize = (@as(usize, viewport.anchor.row) * @as(usize, this.size.cols)) + @as(usize, viewport.anchor.col);
const anchor: usize = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..viewport.rows) |row| {
for (0..viewport.cols) |col| {
const cell = cells[idx];
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
vs[anchor + (row * this.size.x) + col] = cells[idx];
idx += 1;
vs[anchor + (row * this.size.cols) + col].style = cell.style;
vs[anchor + (row * this.size.cols) + col].cp = cell.cp;
if (cells.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| {
try this.render(T, element);
}
for (container.elements.items) |*element| try this.render(T, element);
}
/// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop).
pub fn flush(this: *@This()) !void {
// TODO: measure timings of rendered frames?
log.debug("renderer::flush", .{});
// TODO measure timings of rendered frames?
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
for (0..this.size.rows) |row| {
for (0..this.size.cols) |col| {
const idx = (row * this.size.cols) + col;
for (0..this.size.y) |row| {
for (0..this.size.x) |col| {
const idx = (row * this.size.x) + col;
const cs = s[idx];
const cvs = vs[idx];
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) });
try terminal.setCursorPosition(.{ .y = @truncate(row + 1), .x = @truncate(col + 1) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];

View File

@@ -1,10 +0,0 @@
pub const Size = packed struct {
anchor: Position = .{},
cols: u16 = 0,
rows: u16 = 0,
};
pub const Position = packed struct {
col: u16 = 0,
row: u16 = 0,
};

View File

@@ -22,7 +22,7 @@ pub const Underline = enum {
dashed,
};
pub const Attribute = enum(u8) {
pub const Emphasis = enum(u8) {
reset = 0,
bold = 1,
dim,
@@ -34,11 +34,11 @@ pub const Attribute = enum(u8) {
strikethrough,
};
fg: Color = .white,
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
ul_style: Underline = .off,
attributes: []const Attribute,
emphasis: []const Emphasis,
pub fn eql(this: Style, other: Style) bool {
return std.meta.eql(this, other);
@@ -56,18 +56,17 @@ pub fn value(this: Style, writer: anytype, cp: u21) !void {
try std.fmt.format(writer, ";", .{});
try this.bg.write(writer, .bg);
// underline
// FIX: assert that if the underline property is set that the ul style and the attribute for underlining is available
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try std.fmt.format(writer, ";", .{});
try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.attributes) |attribute| {
try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
}
for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)});
try std.fmt.format(writer, "m", .{});
// content
try std.fmt.format(writer, "{s}", .{buffer});
try std.fmt.format(writer, "{s}", .{buffer[0..bytes]});
try std.fmt.format(writer, "\x1b[0m", .{});
}
// TODO: implement helper functions for terminal capabilities:
// TODO implement helper functions for terminal capabilities:
// - links / url display (osc 8)
// - show / hide cursor?

View File

@@ -1,9 +1,11 @@
const std = @import("std");
pub const code_point = @import("code_point");
const code_point = @import("code_point");
const ctlseqs = @import("ctlseqs.zig");
const input = @import("input.zig");
const Key = @import("key.zig");
const Position = @import("size.zig").Position;
const Size = @import("size.zig").Size;
const Key = input.Key;
const Point = @import("point.zig").Point;
const Size = @import("point.zig").Point;
const Cell = @import("cell.zig");
const log = std.log.scoped(.terminal);
@@ -21,39 +23,47 @@ pub const ReportMode = enum {
pub fn getTerminalSize() Size {
var ws: std.posix.winsize = undefined;
_ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws));
return .{ .cols = ws.col, .rows = ws.row };
return .{ .x = ws.col, .y = ws.row };
}
pub fn saveScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?47h");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.save_screen);
}
pub fn restoreScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?47l");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.restore_screen);
}
pub fn enterAltScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049h");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.smcup);
}
pub fn exitAltScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?1049l");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.rmcup);
}
pub fn clearScreen() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[2J");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.clear_screen);
}
pub fn hideCursor() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?25l");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.hide_cursor);
}
pub fn showCursor() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[?25h");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.show_cursor);
}
pub fn setCursorPositionHome() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[H");
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.home);
}
pub fn enableMouseSupport() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_set);
}
pub fn disableMouseSupport() !void {
_ = try std.posix.write(std.posix.STDIN_FILENO, ctlseqs.mouse_reset);
}
pub fn read(buf: []u8) !usize {
@@ -79,9 +89,9 @@ pub fn writer() Writer {
return .{ .context = .{} };
}
pub fn setCursorPosition(pos: Position) !void {
pub fn setCursorPosition(pos: Point) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.y, pos.x });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
}
@@ -127,8 +137,8 @@ pub fn getCursorPosition() !Size.Position {
}
return .{
.row = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
.col = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
.x = try std.fmt.parseInt(u16, col[0..cidx], 10) - 1,
.y = try std.fmt.parseInt(u16, row[0..ridx], 10) - 1,
};
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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

201
src/testing.zig Normal file
View File

@@ -0,0 +1,201 @@
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
const std = @import("std");
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;
// TODO how would I describe the expected screens?
// - including styling?
// - compare generated strings instead? -> how would this be generated for the user?
/// Single-buffer test rendering pipeline for testing purposes.
pub const Renderer = struct {
allocator: std.mem.Allocator,
size: Point,
screen: []Cell,
pub fn init(allocator: std.mem.Allocator, size: Point) @This() {
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
@memset(screen, .{});
return .{
.allocator = allocator,
.size = size,
.screen = screen,
};
}
pub fn deinit(this: *@This()) void {
this.allocator.free(this.screen);
}
pub fn resize(this: *@This(), size: Point) !void {
this.size = size;
const n = @as(usize, size.x) * @as(usize, size.y);
this.allocator.free(this.screen);
this.screen = this.allocator.alloc(Cell, n) catch @panic("testing.zig: Out of memory.");
@memset(this.screen, .{});
}
pub fn clear(this: *@This()) !void {
@memset(this.screen, .{});
}
pub fn render(this: *@This(), comptime T: type, container: *const T) !void {
const size: Point = container.size;
const origin: Point = container.origin;
const cells: []const Cell = try container.content();
if (cells.len == 0) return;
var idx: usize = 0;
const anchor = (@as(usize, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
const cell = cells[idx];
idx += 1;
this.screen[anchor + (row * this.size.x) + col].style = cell.style;
this.screen[anchor + (row * this.size.x) + col].cp = cell.cp;
if (cells.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(cells);
for (container.elements.items) |*element| try this.render(T, element);
}
pub fn save(this: @This(), writer: anytype) !void {
try std.zon.stringify.serialize(this.screen, .{ .whitespace = false }, writer);
}
};
/// This function is intended to be used only in tests. Test if a `Container`'s
/// rendered contents are equal to the expected `Cell` slice.
///
/// # Test data creation
///
/// Create a .zon file containing the expected `Cell` slice using the `zterm.testing.Renderer.save` method:
///
/// ```zig
/// const file = try std.fs.cwd().createFile("test/container/border/all.zon", .{ .truncate = true });
/// defer file.close();
///
/// const allocator = std.testing.allocator;
/// var renderer: testing.Renderer = .init(allocator, size);
/// defer renderer.deinit();
///
/// try container.handle(.{ .size = size });
/// try renderer.render(Container(event.SystemEvent), &container);
/// try renderer.save(file.writer());
/// ```
///
/// # Testing against created data
///
/// Then later load that .zon file at compile time and run your test against this `Cell` slice.
///
/// ```zig
/// var container: Container(event.SystemEvent) = try .init(std.testing.allocator, .{
/// .border = .{
/// .color = .green,
/// .sides = .all,
/// },
/// }, .{});
/// defer container.deinit();
///
/// try testing.expectContainerScreen(.{
/// .rows = 20,
/// .cols = 30,
/// }, &container, @import("test/container/border.all.zon"));
/// ```
pub fn expectContainerScreen(size: Point, container: *Container(event.SystemEvent), expected: []const Cell) !void {
const allocator = std.testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
try renderer.resize(size);
container.resize(size);
container.reposition(.{});
try renderer.render(Container(event.SystemEvent), container);
try expectEqualCells(.{}, renderer.size, expected, renderer.screen);
}
/// This function is intended to be used only in tests. Test if the two
/// provided cell arrays are identical. Usually the `Cell` slices are
/// the contents of a given screen from the `zterm.testing.Renderer`. See
/// `zterm.testing.expectContainerScreen` for an example usage.
pub fn expectEqualCells(origin: Point, size: Point, expected: []const Cell, actual: []const Cell) !void {
const allocator = std.testing.allocator;
try std.testing.expectEqual(expected.len, actual.len);
try std.testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit();
var actual_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer actual_cps.deinit();
var output = try std.ArrayList(u8).initCapacity(allocator, expected_cps.capacity * actual_cps.capacity + 5 * size.y);
defer output.deinit();
var buffer = std.io.bufferedWriter(output.writer());
defer buffer.flush() catch {};
const writer = buffer.writer();
var differ = false;
const dwd = try DisplayWidth.DisplayWidthData.init(allocator);
defer dwd.deinit();
const dw: DisplayWidth = .{ .data = &dwd };
const expected_centered = try dw.center(allocator, "Expected Screen", size.x, " ");
defer allocator.free(expected_centered);
const actual_centered = try dw.center(allocator, "Actual Screen", size.x, " ");
defer allocator.free(actual_centered);
try writer.print("Screens are not equivalent.\n{s} ┆ {s}\n", .{ expected_centered, actual_centered });
for (origin.y..size.y) |row| {
defer {
expected_cps.clearRetainingCapacity();
actual_cps.clearRetainingCapacity();
}
for (origin.x..size.x) |col| {
const expected_cell = expected[(row * size.x) + col];
const actual_cell = actual[(row * size.x) + col];
if (!expected_cell.eql(actual_cell)) differ = true;
try expected_cps.append(expected_cell);
try actual_cps.append(actual_cell);
}
// write screens both formatted to buffer
for (expected_cps.items) |cell| try cell.value(writer);
_ = try writer.write("");
for (actual_cps.items) |cell| try cell.value(writer);
_ = try writer.write("\n");
}
if (!differ) return;
// test failed
try buffer.flush();
std.debug.lockStdErr();
defer std.debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}

View File

@@ -1,10 +1,17 @@
// private imports
const container = @import("container.zig");
const color = @import("color.zig");
const size = @import("size.zig");
const size = @import("point.zig");
// public exports
pub const input = @import("input.zig");
pub const testing = @import("testing.zig");
pub const App = @import("app.zig").App;
pub const Error = @import("error.zig").Error;
// App also exports further types once initialized with the user events at compile time:
// `App.Container`
// `App.Element`
pub const Renderer = @import("render.zig");
// Container Configurations
@@ -15,16 +22,19 @@ pub const Layout = container.Layout;
pub const Cell = @import("cell.zig");
pub const Color = color.Color;
pub const Key = @import("key.zig");
pub const Size = size.Size;
pub const Key = input.Key;
pub const Mouse = input.Mouse;
pub const Point = @import("point.zig").Point;
pub const Style = @import("style.zig");
test {
_ = @import("terminal.zig");
_ = @import("container.zig");
_ = @import("queue.zig");
_ = @import("error.zig");
_ = @import("point.zig");
_ = color;
_ = size;
_ = Cell;
_ = Key;