151 Commits

Author SHA1 Message Date
89bc3eac0c add(example): WIP popup Element example implementation 2025-07-12 20:08:22 +02:00
df78c7d6eb mod: bump zg dependency version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m19s
2025-07-11 22:38:47 +02:00
66b3a77805 feat(container): negative layout padding
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled
Negative paddings use the current size to calculate the padding from
the opposite orientation. For a given dimension (horizontal or vertical)
if the size is `30` a padding of `5` would be equivalent to a padding
of `-25`. This enables to describe the size of the container using
the padding property of the `Layout` and gives users more freedom to
describe different layouts.
2025-07-11 22:33:32 +02:00
b401b5ece8 lint(container): style
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m41s
2025-07-07 23:08:06 +02:00
9f33c902ee mod(container): cleanup and highlight points for improvement
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 1m42s
2025-07-06 00:42:00 +02:00
9f29ac6a77 feat(element/progress): Progress bar implementation as an Element
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 2m42s
2025-07-05 18:29:49 +02:00
f775a6ab2d formatting
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 13s
2025-06-30 22:53:05 +02:00
a39cee7ccb feat(element/button): add builtin Element implementation for buttons 2025-06-30 22:52:27 +02:00
7875db0aea feat(element/input): make accept event agnostic to u8 and u21 slices
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 50s
There are now comptime checks in place an the corresponding triggered
event will be automatically converted to the correct type to support
simple ascii strings (`[]u8`) or utf-8 strings (`[]u21`).
2025-06-30 22:03:59 +02:00
7595e3b5bb feat(element/input): text input element implementation
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 14s
Moved implementation from example/input as a standalone `Element`
implementation, which is directly used by the example instead.

The provided argument is the `App.Event`'s event that should be
triggered on acceptance for the contents of the Input `Element`.
Currently only `[]u21` strings are supported, but in the future also
`[]u8` strings shall be supported and automatically converted when
pushed as an `App.Event` into the app's queue.
2025-06-29 11:19:09 +02:00
2ba0ed85fb fix(app): remove exported Exec Element - it does not exist in this branch 2025-06-29 10:34:53 +02:00
439520d4fe mod(example/input): ellipse rendering with scrolling for text field contents
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 17s
2025-06-28 22:09:36 +02:00
ded1f2c17e mod(example/input): reorder input handling cases; add alt-b/f binding implementations 2025-06-28 22:08:19 +02:00
92ae8c9681 feat(example/input): readline shortcuts; better navigation;
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 58s
This will be the basis for a Textfield `Element` implementation to make
it easier to add user inputs into the container structures.
2025-06-28 13:04:00 +02:00
743cdca174 mod: bump zg dependency version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 26s
As the dependency is still using version 0.14.0, it still causes an
error during testing. Corresponding examples are still working as
expected.
2025-06-24 20:48:32 +02:00
7d8e902ce2 chor: bump zig version and fix corresponding errors 2025-06-24 20:47:47 +02:00
ed0010c8af lint: correct reported typos (including a rename)
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 26s
2025-06-24 20:35:28 +02:00
a8e138deb7 action: bump zig installation version
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 15s
2025-06-24 20:27:07 +02:00
d0453d08b8 fix: render cursor correctly in case the same character remains the cursor position
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 47s
2025-06-11 20:32:35 +02:00
825fb63bc8 ref: rename gpa usages to the aliased DebugAllocator 2025-06-11 20:31:41 +02:00
76f708d9d7 add(continous): example which provides a fixed render schedule
The other examples did rendering based on events, which this renderer
does not. This makes these applications potentially not that efficient,
but allows for consistent frame times that make animations, etc.
possible. This example serves to show that you can use `zterm` for both
types of render scheduling and even change between them without much
efford.
2025-05-30 23:02:56 +02:00
0d2644f476 mod(input): handle remaining key codes remove unused ones 2025-05-30 23:01:48 +02:00
9a818117d7 feat(panic): panic handler to recover termios when crashing 2025-05-30 23:00:45 +02:00
5ba5b2b372 feat(element/alignment): alignment Element implementation
You can now align a `Container` using the Alignment `Element` similar to
how you make a `Container` scrollable. For usage details please see the
example and the corresponding tests.
2025-05-28 14:42:02 +02:00
3cb0d11e71 add: necessary assert statement; rem: unnecessary render resize in testing 2025-05-28 14:40:25 +02:00
4cc749facc feat(app): read input options correctly 2025-05-28 14:39:27 +02:00
c6d8eec287 feat(debug): render debug support 2025-05-26 15:43:47 +02:00
2572b57697 refactor: remove unnecessary comptime keyword 2025-05-26 15:15:30 +02:00
80a36a9947 refactor: zigify imports and usages 2025-05-26 14:23:18 +02:00
4cde0640c8 fix(element/scrollable): adjust anchor for scrollable element during resize 2025-05-22 23:39:05 +02:00
e9a9c2b680 feat(app): signal WINCH for .resize system event
This allows the application to automatically re-draw and resize if the
application receives the signal by the terminal emulator.
2025-05-21 22:49:08 +02:00
ba25e6056c feat(element/scrollable): scrollbar rendering
Configuration to enable scrollbar rendering for scrollable `Element`s.
Currently only the fg `Color` of the scrollbar can be configured while
the background uses the same fg `Color` but adds the emphasis `.dim` to
make it obvious what the is the actual scrollbar. In the future it might
be necessary to provide the user with more options to configure the
representation of the scrollbar.

Tests have been added to test the scrollbar rendering and placement
accordingly.
2025-05-21 18:20:52 +02:00
aa4adf20f9 refactor: zigify imports and correct minor mistakes 2025-05-20 18:23:44 +02:00
50adf32f14 add(style): cursor style to indicate a cursor position
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 25s
2025-04-20 20:54:30 +02:00
a4293ff243 add(event): mouse event has relative position for receiving elements
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Has been cancelled
2025-04-20 20:53:46 +02:00
50450f3bbc fix(container): growth resize for size all size options and starting sizes (i.e. the smallest being a non grow-able one)
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m14s
2025-04-07 20:55:21 +02:00
bce134f052 rem: unnecessary signal handler
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 2m1s
No event-based re-sizing, instead each re-render resizes by default.
2025-04-01 21:53:52 +02:00
962a384ecf add(Scrollable): init function with corresponding usages
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 42s
2025-03-29 21:21:23 +01:00
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
c2a03e95c1 fix(container/layout): padding calculation for anchor corrections 2025-02-14 21:59:10 +01:00
8998afd9d6 mod(): 2025-02-14 21:49:30 +01:00
4cda202873 fix(renderer): integer overflows 2025-02-14 20:57:48 +01:00
bbe6f4741e WIP: use viewport to allow sizes of scroll to extend further than renderable screen 2025-02-12 22:33:03 +01:00
98031dbd1a add(container/layout): sizing option percentage 2025-02-09 13:41:23 +01:00
ef950809a6 add(container/layout): sizing:fixed option 2025-02-09 12:59:55 +01:00
c72d76470a mod(container/border): change default configuration 2025-02-08 13:52:01 +01:00
29ae75adf5 add(layout/padding): layout configuration for padding
With minor code layout fixes
2025-02-08 13:11:27 +01:00
d326deac97 add(container/rectangle): add content creation 2025-02-07 17:43:16 +01:00
11531e9d4a mod: remove min_size argument from App.start 2025-02-06 22:19:27 +01:00
8586a05508 mod: fix rendering resizing; layout placement of child elements for vertical and horizontal directions
Work in progress for separator configuration of border properties
2025-02-06 20:10:22 +01:00
009d2129b6 mod(container/layout): support gap configuration 2025-02-04 19:06:09 +01:00
9c06ced658 mod(style): styling and color revamp now with fewer characters to print to the terminal 2025-02-04 17:51:28 +01:00
2bfacc0e98 WIP: container rendering for borders + container element rendering 2025-02-03 19:55:33 +01:00
0bf79dc236 mod(container): support layout direction handling for child elements 2025-02-01 11:31:05 +01:00
1293cb065d WIP: add Container type with corresponding Properties configuration
The configuration of the `Container` types is very much inspired by
[clay](https://github.com/nicbarker/clay).
2025-02-01 01:05:56 +01:00
bdbe05c996 mod(structure): update project structure
Remove examples, add description for design goals in README.md and
apply re-names and naming changes accordingly for the project structure.
Implement a flat hierachry, as the library shall remain pretty simple.
2025-01-30 23:02:34 +01:00
3decc541a9 mod(renderer): initial version of double buffer intermediate renderer
This branch will implement the necessary changes for the widgets and
their implementations to use the new renderer correctly.
2025-01-30 20:53:01 +01:00
94 changed files with 6086 additions and 4313 deletions

View File

@@ -15,7 +15,7 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: mlugg/setup-zig@v1
uses: mlugg/setup-zig@v2
with:
version: master
- name: Run tests

View File

@@ -14,11 +14,11 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup zig installation
uses: mlugg/setup-zig@v1
uses: mlugg/setup-zig@v2
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:

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 might will 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,4 +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
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).

163
build.zig
View File

@@ -1,110 +1,105 @@
const std = @import("std");
// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard optimization options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const Examples = enum {
all,
demo,
continuous,
// elements:
alignment,
button,
input,
popup,
progress,
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 debug_rendering = b.option(bool, "debug", "Enable debug rendering. Highlight origin's, size's, padding's, gap's, etc. (default: false)") orelse false;
// NOTE do not support debug rendering in release builds
if (debug_rendering == true and optimize != .Debug) @panic("Cannot enable debug rendering in non-debug builds.");
const options = b.addOptions();
options.addOption(bool, "debug", debug_rendering);
const options_module = options.createModule();
// dependencies
const zg = b.dependency("zg", .{
.target = target,
.optimize = optimize,
});
const interface = b.dependency("interface", .{
.target = target,
.optimize = optimize,
});
// library
const lib = b.addModule("zterm", .{
.root_source_file = b.path("src/zterm.zig"),
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib.addImport("interface", interface.module("interface"));
lib.addImport("code_point", zg.module("code_point"));
lib.addImport("build_options", options_module);
// example executables
const stack_example = b.addExecutable(.{
.name = "stack",
.root_source_file = b.path("examples/stack.zig"),
.target = target,
.optimize = optimize,
});
stack_example.root_module.addImport("zterm", lib);
//--- Examples ---
const examples = std.meta.fields(Examples);
inline for (examples) |e| {
if (@as(Examples, @enumFromInt(e.value)) == .all) continue; // skip `.all` entry
const demo = b.addExecutable(.{
.name = e.name,
.root_source_file = b.path(switch (@as(Examples, @enumFromInt(e.value))) {
.demo => "examples/demo.zig",
.continuous => "examples/continuous.zig",
// elements:
.alignment => "examples/elements/alignment.zig",
.button => "examples/elements/button.zig",
.input => "examples/elements/input.zig",
.popup => "examples/elements/popup.zig",
.progress => "examples/elements/progress.zig",
.scrollable => "examples/elements/scrollable.zig",
// layouts:
.vertical => "examples/layouts/vertical.zig",
.horizontal => "examples/layouts/horizontal.zig",
.grid => "examples/layouts/grid.zig",
.mixed => "examples/layouts/mixed.zig",
// styles:
.text => "examples/styles/text.zig",
.palette => "examples/styles/palette.zig",
// error handling
.errors => "examples/errors.zig",
.all => unreachable, // should never happen
}),
.target = target,
.optimize = optimize,
});
// import dependencies
demo.root_module.addImport("zterm", lib);
// mapping of user selected example to compile step
if (@intFromEnum(example) == e.value or example == .all) b.installArtifact(demo);
}
const container_example = b.addExecutable(.{
.name = "container",
.root_source_file = b.path("examples/container.zig"),
.target = target,
.optimize = optimize,
});
container_example.root_module.addImport("zterm", lib);
const padding_example = b.addExecutable(.{
.name = "padding",
.root_source_file = b.path("examples/padding.zig"),
.target = target,
.optimize = optimize,
});
padding_example.root_module.addImport("zterm", lib);
const exec_example = b.addExecutable(.{
.name = "exec",
.root_source_file = b.path("examples/exec.zig"),
.target = target,
.optimize = optimize,
});
exec_example.root_module.addImport("zterm", lib);
const tui_example = b.addExecutable(.{
.name = "tui",
.root_source_file = b.path("examples/tui.zig"),
.target = target,
.optimize = optimize,
});
tui_example.root_module.addImport("zterm", lib);
const tabs_example = b.addExecutable(.{
.name = "tabs",
.root_source_file = b.path("examples/tabs.zig"),
.target = target,
.optimize = optimize,
});
tabs_example.root_module.addImport("zterm", lib);
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
// step when running `zig build`).
b.installArtifact(stack_example);
b.installArtifact(container_example);
b.installArtifact(padding_example);
b.installArtifact(exec_example);
b.installArtifact(tui_example);
b.installArtifact(tabs_example);
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
// zig build test
const lib_unit_tests = b.addTest(.{
.root_source_file = b.path("src/zterm.zig"),
.root_source_file = b.path("src/root.zig"),
.target = target,
.optimize = optimize,
});
lib_unit_tests.root_module.addImport("zg", zg.module("code_point"));
lib_unit_tests.root_module.addImport("code_point", zg.module("code_point"));
lib_unit_tests.root_module.addImport("DisplayWidth", zg.module("DisplayWidth"));
lib_unit_tests.root_module.addImport("build_options", options_module);
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
// Similar to creating the run step earlier, this exposes a `test` step to
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
}
const std = @import("std");

View File

@@ -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.3.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,20 +37,14 @@
// internet connectivity.
.dependencies = .{
.zg = .{
.url = "git+https://codeberg.org/atman/zg#a363f507fc39b96fc48d693665a823a358345326",
.hash = "1220fe42e39fd141c84fd7d5cf69945309bb47253033e68788f99bdfe5585fbc711a",
},
.interface = .{
.url = "git+https://github.com/yves-biener/zig-interface#ef47e045df19e09250fff45c0702d014fb3d3c37",
.hash = "1220a442e8d9b813572bab7a55eef504c83b628f0b17fd283e776dbc1d1a3d98e842",
.url = "git+https://codeberg.org/atman/zg#9427a9e53aaa29ee071f4dcb35b809a699d75aa9",
.hash = "zg-0.14.1-oGqU3IQ_tALZIiBN026_NTaPJqU-Upm8P_C7QED2Rzm8",
},
},
.paths = .{
"LICENSE",
"build.zig",
"build.zig.zon",
"src",
// For example...
//"LICENSE",
//"README.md",
},
}

View File

@@ -1,96 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {},
zterm.Renderer.Direct,
true,
);
const Key = zterm.Key;
const Layout = App.Layout;
const Widget = App.Widget;
const log = std.log.scoped(.container);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer {
const deinit_status = gpa.deinit();
// fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
var layout = Layout.createFrom(Layout.HContainer.init(allocator, .{
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
15,
},
.{
Layout.createFrom(Layout.VContainer.init(allocator, .{
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
25,
},
.{
Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./src/app.zig", .{});
defer file.close();
break :blk Widget.RawText.init(allocator, file);
}),
50,
},
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
25,
},
})),
70,
},
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
15,
},
}));
defer layout.deinit();
try app.start(null);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
else => {},
}
const events = try layout.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try layout.render(&renderer);
}
}

245
examples/continuous.zig Normal file
View File

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

198
examples/demo.zig Normal file
View File

@@ -0,0 +1,198 @@
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;
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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 = .init(box, .disabled);
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()));
var nested_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
.separator = .{
.enabled = true,
},
},
}, .{});
var inner_container: App.Container = try .init(allocator, .{
.layout = .{
.direction = .vertical,
},
.border = .{
.color = .light_blue,
.sides = .all,
},
}, .{});
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .blue,
},
.size = .{
.grow = .horizontal,
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .red,
},
.size = .{
.grow = .horizontal,
.dim = .{ .y = 5 },
},
}, .{}));
try inner_container.append(try .init(allocator, .{
.rectangle = .{
.fill = .green,
},
}, .{}));
try nested_container.append(inner_container);
try nested_container.append(try .init(allocator, .{
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
},
}, .{}));
try container.append(nested_container);
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .blue },
.size = .{
.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();
renderer.size = .{}; // reset size, such that next resize will cause a full re-draw!
defer app.start() catch @panic("could not start app event loop");
var child = std.process.Child.init(&.{"hx"}, allocator);
_ = child.spawnAndWait() catch |err| app.postEvent(.{
.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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});

View File

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

View File

@@ -0,0 +1,151 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
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));
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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var clickable: Clickable = .{ .queue = &app.queue };
const element = clickable.element();
var button: App.Button(.accept) = .init(&app.queue, .init(.default, "Button"));
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.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 container.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = .black } }, button.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 => |b| log.info("Clicked with mouse using Button: {s}", .{b}),
.accept => log.info("Clicked built-in button using the mouse", .{}),
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
click: [:0]const u8,
accept,
});

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

@@ -0,0 +1,166 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var input_field: App.Input(.accept) = .init(allocator, &app.queue, .init(.black));
defer input_field.deinit();
var mouse_draw: MouseDraw = .{};
var second_mouse_draw: MouseDraw = .{};
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.rectangle = .{ .fill = .grey },
.layout = .{
.direction = .vertical,
.padding = .all(5),
},
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{
.rectangle = .{ .fill = .light_grey },
.size = .{
.grow = .horizontal,
.dim = .{ .y = 1 },
},
}, input_field.element()));
var nested_container: App.Container = try .init(allocator, .{
.border = .{
.sides = .all,
.color = .black,
},
.rectangle = .{ .fill = .light_grey },
.layout = .{
.separator = .{
.enabled = true,
.color = .black,
},
},
}, .{});
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
}, mouse_draw.element()));
try nested_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .light_grey },
}, second_mouse_draw.element()));
try container.append(nested_container);
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
// 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.debug("Accepted input '{s}'", .{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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const Color = zterm.Color;
const App = zterm.App(union(enum) {
accept: []u8,
});

247
examples/elements/popup.zig Normal file
View File

@@ -0,0 +1,247 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
const MouseDraw = struct {
position: ?zterm.Point = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.handle = handle,
.content = content,
},
};
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
.mouse => |mouse| this.position = .{ .x = mouse.x, .y = mouse.y },
else => this.position = null,
}
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.position) |pos| {
const idx = @as(usize, size.x) * @as(usize, pos.y) + @as(usize, pos.x);
cells[idx].cp = 'x';
cells[idx].style.fg = .red;
}
}
};
const Popup = struct {
container: ?*App.Container = null,
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{
.resize = resize,
.reposition = reposition,
.handle = handle,
.content = content,
},
};
}
fn resize(ctx: *anyopaque, size: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.resize(size);
}
fn reposition(ctx: *anyopaque, _: zterm.Point) void {
const this: *@This() = @ptrCast(@alignCast(ctx));
if (this.container) |container| container.reposition(.{});
}
fn handle(ctx: *anyopaque, event: App.Event) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
switch (event) {
// TODO should the `Element` handle the pop_down element triggering (i.e. by defining a usual key for this?)
.pop_up => |optional| if (optional) |container| {
this.container = @ptrCast(@alignCast(container));
} else {
this.container = null;
},
else => if (this.container) |container| try container.handle(event),
}
}
fn render_container(container: App.Container, cells: []zterm.Cell, container_size: zterm.Point) !void {
const size = container.size;
const origin = container.origin;
const contents = try container.content();
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;
}
}
// free immediately
container.allocator.free(contents);
for (container.elements.items) |child| try render_container(child, cells, size);
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
const this: *@This() = @ptrCast(@alignCast(ctx));
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
if (this.container) |container| {
assert(cells.len == @as(usize, container.size.x) * @as(usize, container.size.y));
const popup_cells = try container.content();
for (container.elements.items) |child| try render_container(child, popup_cells, size);
assert(cells.len == popup_cells.len);
@memcpy(cells, popup_cells);
container.allocator.free(popup_cells);
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var popup: Popup = .{};
var quit_text: QuitText = .{};
// TODO
// - rendering of first Container contents will be overwritten by contents of the appended Container's (see the missing blue rectangle)
// - when rendering "nothing" it causes the below Container to be overwritten to "nothing" too!
// - not sure how this should be done?
// - provide the pop-up with a area where to draw? (i.e. somewhere to draw the provided `Container`)
// - This however will not suffice as the contents will be overwritten!
var container = try App.Container.init(allocator, .{
.rectangle = .{
.fill = .blue,
},
}, .{});
defer container.deinit();
var popup_root_container = try App.Container.init(allocator, .{
.layout = .{
.padding = .{
.top = -17,
.left = -40,
.right = 5,
.bottom = 5,
},
},
}, .{});
try popup_root_container.append(try App.Container.init(allocator, .{}, popup.element()));
try container.append(try .init(allocator, .{}, quit_text.element()));
try container.append(popup_root_container); // FIXME it should not be appended (as it would become part of the layout)
var mouse: MouseDraw = .{};
var popup_container: App.Container = try .init(allocator, .{
.rectangle = .{ .fill = .green },
.layout = .{
.padding = .{
.top = -4,
.bottom = 1,
.left = 3,
.right = 3,
},
},
}, .{});
// showcase that inner `Container`s handle `Element`s accordingly
try popup_container.append(try .init(allocator, .{
.rectangle = .{ .fill = .grey },
}, mouse.element()));
defer popup_container.deinit();
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 = zterm.input.Space })) app.postEvent(.{ .pop_up = &popup_container });
if (key.eql(.{ .cp = zterm.input.Escape })) app.postEvent(.{ .pop_up = null });
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break,
else => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
pop_up: ?*anyopaque,
});

View File

@@ -0,0 +1,137 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var progress_percent: u8 = 0;
var progress: App.Progress(.progress) = .init(&app.queue, .{
.percent = .{ .enabled = true },
.fg = .green,
.bg = .grey,
});
var quit_text: QuitText = .{};
var container = try App.Container.init(allocator, .{
.layout = .{ .padding = .all(5) },
}, quit_text.element());
defer container.deinit();
try container.append(try App.Container.init(allocator, .{}, progress.element()));
try app.start();
defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err});
var framerate: u64 = 60;
var tick_ms: u64 = @divFloor(time.ms_per_s, framerate);
var next_frame_ms: u64 = 0;
var increase_progress: u64 = 10;
// Continuous drawing
// draw loop
draw: while (true) {
const now_ms: u64 = @intCast(time.milliTimestamp());
if (now_ms >= next_frame_ms) {
next_frame_ms = now_ms + tick_ms;
} else {
time.sleep((next_frame_ms - now_ms) * time.ns_per_ms);
next_frame_ms += tick_ms;
}
// NOTE time based progress increasion
increase_progress -= 1;
if (increase_progress == 0) {
increase_progress = 10;
progress_percent += 1;
if (progress_percent > 100) progress_percent = 0;
app.postEvent(.{ .progress = progress_percent });
}
const len = blk: {
app.queue.lock();
defer app.queue.unlock();
break :blk app.queue.len();
};
// handle events
for (0..len) |_| {
const event = app.queue.drain() orelse break;
log.debug("handling event: {s}", .{@tagName(event)});
// pre event handling
switch (event) {
.key => |key| {
if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit();
},
.err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }),
.focus => |b| {
// NOTE reduce framerate in case the window is not focused and restore again when focused
framerate = if (b) 60 else 15;
tick_ms = @divFloor(time.ms_per_s, framerate);
},
else => {},
}
container.handle(event) catch |err| app.postEvent(.{
.err = .{
.err = err,
.msg = "Container Event handling failed",
},
});
// post event handling
switch (event) {
.quit => break :draw,
else => {},
}
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const time = std.time;
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {
progress: u8,
});

View File

@@ -0,0 +1,199 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
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;
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.DebugAllocator(.{}) = .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 = .init(top_box, .enabled(.grey));
try container.append(try App.Container.init(allocator, .{}, scrollable_top.element()));
var scrollable_bottom: App.Scrollable = .init(bottom_box, .enabled(.white));
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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const input = zterm.input;
const App = zterm.App(union(enum) {});

159
examples/errors.zig Normal file
View File

@@ -0,0 +1,159 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
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;
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));
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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,105 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {},
zterm.Renderer.Direct,
true,
);
const Key = zterm.Key;
const Cell = zterm.Cell;
const Layout = App.Layout;
const Widget = App.Widget;
const log = std.log.scoped(.exec);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer {
const deinit_status = gpa.deinit();
// fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
var layout = Layout.createFrom(Layout.VContainer.init(allocator, .{
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
45,
},
.{
Layout.createFrom(Layout.Framing.init(allocator, .{}, .{
.widget = Widget.createFrom(Widget.Text.init(allocator, .center, &[_]Cell{
.{ .content = "Press " },
.{ .content = "Ctrl+n", .style = .{ .fg = .{ .index = 6 } } },
.{ .content = " to launch $EDITOR" },
})),
})),
10,
},
.{
Widget.createFrom(Widget.Spacer.init(allocator)),
45,
},
}));
defer layout.deinit();
const min_size: zterm.Size = .{
.cols = 25,
.rows = 20,
};
try app.start(min_size);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
if (Key.matches(key, .{ .cp = 'n', .mod = .{ .ctrl = true } })) {
try app.interrupt();
defer app.start(min_size) catch @panic("could not start app event loop");
// TODO: parse environment variables to extract the value of $EDITOR and use it here instead
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}", .{ err.err, err.msg });
},
else => {},
}
const events = try layout.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try layout.render(&renderer);
}
}

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

@@ -0,0 +1,115 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -0,0 +1,107 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

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

@@ -0,0 +1,123 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -0,0 +1,106 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{
.ptr = this,
.vtable = &.{ .content = content },
};
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,103 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {},
zterm.Renderer.Direct,
true,
);
const Key = zterm.Key;
const Layout = App.Layout;
const Widget = App.Widget;
const log = std.log.scoped(.padding);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer {
const deinit_status = gpa.deinit();
// fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
var layout = Layout.createFrom(Layout.Padding.init(allocator, .{
.padding = 15,
}, .{
.layout = Layout.createFrom(Layout.Framing.init(
allocator,
.{
.style = .{
.fg = .{
.index = 6,
},
},
.frame = .round,
.title = .{
.str = "Content in Margin",
.style = .{
.ul_style = .single,
.ul = .{ .index = 6 },
.bold = true,
},
},
},
.{
.layout = Layout.createFrom(Layout.Margin.init(
allocator,
.{
.margin = 10,
},
.{
.widget = Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./examples/padding.zig", .{});
defer file.close();
break :blk Widget.RawText.init(allocator, file);
}),
},
)),
},
)),
}));
defer layout.deinit();
try app.start(null);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
else => {},
}
const events = try layout.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try layout.render(&renderer);
}
}

View File

@@ -1,130 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {},
zterm.Renderer.Direct,
true,
);
const Key = zterm.Key;
const Layout = App.Layout;
const Widget = App.Widget;
const log = std.log.scoped(.stack);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer {
const deinit_status = gpa.deinit();
// fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
var layout = Layout.createFrom(Layout.Framing.init(allocator, .{
.style = .{
.fg = .{
.index = 6,
},
},
.frame = .round,
.title = .{
.str = "HStack",
.style = .{
.ul_style = .single,
.ul = .{ .index = 6 },
.bold = true,
},
},
}, .{
.layout = Layout.createFrom(Layout.HStack.init(allocator, .{
Widget.createFrom(Widget.Spacer.init(allocator)),
Layout.createFrom(Layout.Framing.init(
allocator,
.{
.style = .{
.fg = .{
.index = 6,
},
},
.frame = .round,
.title = .{
.str = "VStack",
.style = .{
.ul_style = .single,
.ul = .{ .index = 6 },
.bold = true,
},
},
},
.{
.layout = Layout.createFrom(
Layout.Margin.init(
allocator,
.{
.margin = 10,
},
.{
.layout = Layout.createFrom(Layout.VStack.init(allocator, .{
Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
defer file.close();
break :blk Widget.RawText.init(allocator, file);
}),
Widget.createFrom(Widget.Spacer.init(allocator)),
Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./examples/stack.zig", .{});
defer file.close();
break :blk Widget.RawText.init(allocator, file);
}),
})),
},
),
),
},
)),
Widget.createFrom(Widget.Spacer.init(allocator)),
})),
}));
defer layout.deinit();
try app.start(null);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
else => {},
}
const events = try layout.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try layout.render(&renderer);
}
}

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

@@ -0,0 +1,102 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 (field.value == 0) continue; // zterm.Color.default == 0 -> skip
try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{}));
}
var scrollable: App.Scrollable = .init(box, .disabled);
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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

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

@@ -0,0 +1,157 @@
const QuitText = struct {
const text = "Press ctrl+c to quit.";
pub fn element(this: *@This()) App.Element {
return .{ .ptr = this, .vtable = &.{ .content = content } };
}
fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Point) !void {
_ = ctx;
assert(cells.len == @as(usize, size.x) * @as(usize, size.y));
const row = 2;
const col = size.x / 2 -| (text.len / 2);
const anchor = (row * size.x) + col;
for (text, 0..) |cp, idx| {
cells[anchor + idx].style.fg = .white;
cells[anchor + idx].style.bg = .black;
cells[anchor + idx].cp = cp;
// NOTE do not write over the contents of this `Container`'s `Size`
if (anchor + idx == cells.len - 1) break;
}
}
};
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(10000);
_ = ctx;
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 (bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
inline for (std.meta.fields(zterm.Color)) |fg_field| {
if (fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip
if (fg_field.value == bg_field.value) continue;
// witouth any emphasis
for (text) |cp| {
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 (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.DebugAllocator(.{}) = .init;
defer if (gpa.deinit() == .leak) log.err("memory leak", .{});
const allocator = gpa.allocator();
var app: App = .init;
var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit();
var 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 = .init(box, .disabled);
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 => {},
}
container.resize(try renderer.resize());
container.reposition(.{});
try renderer.render(@TypeOf(container), &container);
try renderer.flush();
}
}
pub const panic = App.panic_handler;
const log = std.log.scoped(.default);
const std = @import("std");
const assert = std.debug.assert;
const zterm = @import("zterm");
const App = zterm.App(union(enum) {});

View File

@@ -1,106 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {},
zterm.Renderer.Direct,
true,
);
const Key = zterm.Key;
const Cell = zterm.Cell;
const Layout = App.Layout;
const Widget = App.Widget;
const log = std.log.scoped(.tabs);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var gpa: std.heap.GeneralPurposeAllocator(.{}) = .{};
defer {
const deinit_status = gpa.deinit();
// fail test; can't try in defer as defer is executed after we return
if (deinit_status == .leak) {
log.err("memory leak", .{});
}
}
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
// TODO: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
var layout = Layout.createFrom(Layout.Tab.init(allocator, .{}, .{
.{
Layout.createFrom(Layout.Margin.init(allocator, .{ .margin = 10 }, .{
.widget = Widget.createFrom(blk: {
const file = try std.fs.cwd().openFile("./examples/tabs.zig", .{});
defer file.close();
break :blk Widget.RawText.init(allocator, file);
}),
})),
"Tab 2",
Cell.Style.Color{ .index = 6 },
},
.{
Layout.createFrom(Layout.Framing.init(
allocator,
.{
.frame = .round,
.title = .{
.str = "Content in Margin",
.style = .{
.ul_style = .single,
.ul = .{ .index = 4 },
.bold = true,
},
},
},
.{
.layout = Layout.createFrom(Layout.Margin.init(allocator, .{ .margin = 10 }, .{
.widget = Widget.createFrom(Widget.List.init(allocator, .ordered, .{
&[_]Cell{.{ .content = "First entry" }},
&[_]Cell{.{ .content = "Second entry" }},
&[_]Cell{.{ .content = "Third entry" }},
})),
})),
},
)),
"Tab 1",
Cell.Style.Color{ .index = 4 },
},
}));
defer layout.deinit();
try app.start(null);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
log.debug("received event: {s}", .{@tagName(event)});
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
else => {},
}
const events = try layout.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try layout.render(&renderer);
}
}

View File

@@ -1,151 +0,0 @@
const std = @import("std");
const zterm = @import("zterm");
const App = zterm.App(
union(enum) {
view: union(enum) {
tui, // view instance to the corresponding view for 'tui'
},
},
zterm.Renderer.Direct,
true,
);
const Cell = zterm.Cell;
const Key = zterm.Key;
const Layout = App.Layout;
const Widget = App.Widget;
const View = App.View;
const Tui = struct {
const Events = std.ArrayList(App.Event);
allocator: std.mem.Allocator,
layout: Layout,
pub fn init(allocator: std.mem.Allocator) *Tui {
var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig");
tui.allocator = allocator;
// FIXME: the layout creates an 'incorrect alignment'?
tui.layout = Layout.createFrom(Layout.VContainer.init(allocator, .{
.{
Layout.createFrom(Layout.Framing.init(allocator, .{
.title = .{
.str = "Welcome to my terminal website",
.style = .{
.ul = .{ .index = 6 },
.ul_style = .single,
},
},
}, .{
.layout = Layout.createFrom(Layout.HContainer.init(allocator, .{
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "Yves Biener", .style = .{ .bold = true } },
})),
25,
},
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "File name", .style = .{ .bold = true } },
})),
50,
},
.{
Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{
.{ .content = "Contacts", .style = .{ .bold = true } },
})),
25,
},
})),
})),
10,
},
.{
Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{
.widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{
.{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } },
})),
})),
90,
},
}));
return tui;
}
pub fn deinit(this: *Tui) void {
this.layout.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *Tui, event: App.Event) !*Events {
return try this.layout.handle(event);
}
pub fn render(this: *Tui, renderer: *App.Renderer) !void {
try this.layout.render(renderer);
}
};
// TODO: create additional example with a bit more complex functionality for
// dynamic layouts, switching views, etc.
const log = std.log.scoped(.tui);
pub fn main() !void {
errdefer |err| log.err("Application Error: {any}", .{err});
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
var view: View = undefined;
var tui_view = View.createFrom(Tui.init(allocator));
defer tui_view.deinit();
view = tui_view;
try app.start(null);
defer app.stop() catch unreachable;
// App.Event loop
while (true) {
const event = app.nextEvent();
switch (event) {
.quit => break,
.resize => |size| {
renderer.resize(size);
},
.key => |key| {
// ctrl+c to quit
if (Key.matches(key, .{ .cp = 'c', .mod = .{ .ctrl = true } })) {
app.quit();
}
},
.err => |err| {
log.err("Received {any} with message: {s}", .{ err.err, err.msg });
},
.view => |v| {
switch (v) {
.tui => {
view = tui_view;
// NOTE: report potentially new screen size
const events = try view.handle(.{ .resize = renderer.size });
for (events.items) |e| {
app.postEvent(e);
}
},
}
},
else => {},
}
const events = try view.handle(event);
for (events.items) |e| {
app.postEvent(e);
}
try view.render(&renderer);
}
}

View File

@@ -1,28 +1,9 @@
//! Application type for TUI-applications
const std = @import("std");
const terminal = @import("terminal.zig");
const event = @import("event.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Key = terminal.Key;
const Queue = @import("queue.zig").Queue;
const log = std.log.scoped(.app);
/// Create the App Type with the associated user events _E_ which describes
/// an tagged union for all the user events that can be send through the
/// applications event loop.
///
/// _R_ is the type function for the `Renderer` to use. The parameter boolean
/// will be set to the _fullscreen_ value at compile time. The corresponding
/// `Renderer` type is accessible through the generated type of this function.
///
/// _fullscreen_ will be used to configure the `App` and the `Renderer` to
/// respect the corresponding configuration whether to render a fullscreen tui
/// or an inline tui.
///
/// # Example
///
/// Create an `App` which renders using the `PlainRenderer` in fullscreen with
@@ -32,79 +13,73 @@ const log = std.log.scoped(.app);
/// const zterm = @import("zterm");
/// const App = zterm.App(
/// union(enum) {},
/// zterm.Renderer.Direct,
/// true,
/// );
/// // later on use
/// var app: App = .{};
/// var renderer: App.Renderer = .{};
/// // later on create an `App` instance and start the event loop
/// var app: App = .init;
/// try app.start();
/// defer app.stop() catch unreachable;
/// ```
pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fullscreen: bool) type {
pub fn App(comptime E: type) type {
if (!isTaggedUnion(E)) {
@compileError("Provided user event `E` for `App(comptime E: type)` is not of type `union(enum)`.");
}
return struct {
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Renderer = R(fullscreen);
pub const Layout = @import("layout.zig").Layout(Event, Renderer);
pub const Widget = @import("widget.zig").Widget(Event, Renderer);
pub const View = @import("view.zig").View(Event, Renderer);
queue: Queue,
thread: ?Thread = null,
quit_event: Thread.ResetEvent,
termios: ?posix.termios = null,
winch_registered: bool = false,
queue: Queue(Event, 256) = .{},
thread: ?std.Thread = null,
quit_event: std.Thread.ResetEvent = .{},
termios: ?std.posix.termios = null,
attached_handler: bool = false,
min_size: ?terminal.Size = null,
prev_size: terminal.Size = .{ .cols = 0, .rows = 0 },
// global variable for the registered handler for WINCH
var handler_ctx: *anyopaque = undefined;
/// registered WINCH handler to report resize events
fn handleWinch(_: c_int) callconv(.C) void {
const this: *@This() = @ptrCast(@alignCast(handler_ctx));
// NOTE this does not have to be done if in-band resize events are supported
// -> the signal might not work correctly when hosting the application over ssh!
this.postEvent(.resize);
}
pub const SignalHandler = struct {
context: *anyopaque,
callback: *const fn (context: *anyopaque) void,
pub const init: @This() = .{
.queue = .{},
.quit_event = .{},
};
pub fn start(this: *@This(), min_size: ?terminal.Size) !void {
if (fullscreen) { // a minimal size only really makes sense if the application is rendered fullscreen
this.min_size = min_size;
}
pub fn start(this: *@This()) !void {
if (this.thread) |_| return;
if (!this.attached_handler) {
var winch_act = std.posix.Sigaction{
.handler = .{ .handler = @This().handleWinch },
.mask = std.posix.empty_sigset,
// post init event (as the very first element to be in the queue - event loop)
this.postEvent(.init);
if (!this.winch_registered) {
handler_ctx = this;
var act = posix.Sigaction{
.handler = .{ .handler = handleWinch },
.mask = posix.sigemptyset(),
.flags = 0,
};
std.posix.sigaction(std.posix.SIG.WINCH, &winch_act, null);
try registerWinch(.{
.context = this,
.callback = @This().winsizeCallback,
});
this.attached_handler = true;
posix.sigaction(posix.SIG.WINCH, &act, null);
this.winch_registered = true;
}
this.quit_event.reset();
this.thread = try std.Thread.spawn(.{}, @This().run, .{this});
this.thread = try Thread.spawn(.{}, @This().run, .{this});
var termios: std.posix.termios = undefined;
var termios: posix.termios = undefined;
try terminal.enableRawMode(&termios);
if (this.termios) |_| {} else {
this.termios = termios;
}
if (fullscreen) {
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
}
if (this.termios) |_| {} else this.termios = termios;
try terminal.saveScreen();
try terminal.enterAltScreen();
try terminal.hideCursor();
try terminal.enableMouseSupport();
}
pub fn interrupt(this: *@This()) !void {
this.quit_event.set();
if (fullscreen) {
try terminal.exitAltScreen();
try terminal.restoreScreen();
}
try terminal.disableMouseSupport();
try terminal.exitAltScreen();
try terminal.restoreScreen();
if (this.thread) |thread| {
thread.join();
this.thread = null;
@@ -114,12 +89,11 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
pub fn stop(this: *@This()) !void {
try this.interrupt();
if (this.termios) |*termios| {
try terminal.disableMouseSupport();
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.disableRawMode(termios);
if (fullscreen) {
try terminal.showCursor();
try terminal.exitAltScreen();
try terminal.restoreScreen();
}
try terminal.restoreScreen();
}
this.termios = null;
}
@@ -141,93 +115,40 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
this.queue.push(e);
}
fn winsizeCallback(ptr: *anyopaque) void {
const this: *@This() = @ptrCast(@alignCast(ptr));
const size = terminal.getTerminalSize();
// check for minimal size (if any was provided)
if (this.min_size) |min_size| {
if (size.cols < min_size.cols or size.rows < min_size.rows) {
this.postEvent(.{
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
});
return;
}
}
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
this.postEvent(.{ .resize = size });
this.prev_size = size;
}
}
var winch_handler: ?SignalHandler = null;
fn registerWinch(handler: SignalHandler) !void {
if (winch_handler) |_| {
@panic("Cannot register another WINCH handler.");
}
winch_handler = handler;
}
fn handleWinch(_: c_int) callconv(.C) void {
if (winch_handler) |handler| {
handler.callback(handler.context);
}
}
fn run(this: *@This()) !void {
// send initial terminal size
// changes are handled by the winch signal handler
// see `App.start` and `App.registerWinch` for details
{
// TODO: what should happen if the initial window size is too small?
// -> currently the first render call will then crash the application (which happens anyway)
const size = terminal.getTerminalSize();
if (this.min_size) |min_size| {
if (size.cols < min_size.cols or size.rows < min_size.rows) {
this.postEvent(.{
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
});
}
}
this.postEvent(.{ .resize = size });
}
// 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,
if (read_bytes < 3) continue;
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;
}
if (read_bytes < 3) continue;
// We start iterating at index 2 to get past the '['
const sequence = for (buf[2..], 2..) |b, i| {
switch (b) {
@@ -242,64 +163,139 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
// Legacy keys
// CSI {ABCDEFHPQS}
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
_ = field_iter.next(); // skip first field
const key: Key = .{
.cp = switch (final) {
'A' => 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,
else => unreachable, // switch case prevents in this case form ever happening
'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,
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
},
'Z' => this.postEvent(.{ .key = .{ .cp = input.Tab, .mod = .{ .shift = true } } }),
'~' => {
// Legacy keys
// CSI number ~
// CSI number ; modifier ~
// CSI number ; modifier:event_type ; text_as_codepoint ~
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
var field_iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const key: Key = .{
.cp = switch (number) {
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,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57427 => Key.kp_begin,
else => unreachable,
.cp = blk: {
const number_buf = field_iter.next() orelse unreachable; // always will have one field
const number = fmt.parseUnsigned(u16, number_buf, 10) catch break;
break :blk switch (number) {
2 => input.Insert,
3 => input.Delete,
5 => input.PageUp,
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,
25 => input.F13,
26 => input.F14,
28 => input.F15,
29 => input.F16,
31 => input.F17,
32 => input.F18,
33 => input.F19,
34 => input.F20,
// 200 => return .{ .event = .paste_start, .n = sequence.len },
// 201 => return .{ .event = .paste_end, .n = sequence.len },
57399...57454 => |code| code,
else => unreachable,
};
},
.mod = blk: {
// modifier_mask:event_type
var mod: Key.Modifier = .{};
const field_buf = field_iter.next() orelse break :blk mod;
var param_iter = std.mem.splitScalar(u8, field_buf, ':');
const modifier_buf = param_iter.next() orelse unreachable;
const modifier_mask = fmt.parseUnsigned(u8, modifier_buf, 10) catch break :blk mod;
if ((modifier_mask -| 1) & 1 != 0) mod.shift = true;
if ((modifier_mask -| 1) & 2 != 0) mod.alt = true;
if ((modifier_mask -| 1) & 4 != 0) mod.ctrl = true;
break :blk mod;
},
};
this.postEvent(.{ .key = key });
},
// 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' => {
assert(sequence.len >= 4);
if (sequence[2] != '<') break;
const delim1 = mem.indexOfScalarPos(u8, sequence, 3, ';') orelse break;
const button_mask = fmt.parseUnsigned(u16, sequence[3..delim1], 10) catch break;
const delim2 = mem.indexOfScalarPos(u8, sequence, delim1 + 1, ';') orelse break;
const px = fmt.parseUnsigned(u16, sequence[delim1 + 1 .. delim2], 10) catch break;
const py = fmt.parseUnsigned(u16, sequence[delim2 + 1 .. sequence.len - 1], 10) catch break;
const mouse_bits = packed struct {
const motion: u8 = 0b00100000;
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)
},
@@ -307,38 +303,26 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
// Device Status Report
// CSI Ps n
// CSI ? Ps n
std.debug.assert(sequence.len >= 3);
assert(sequence.len >= 3);
},
't' => {
// XTWINOPS
// Split first into fields delimited by ';'
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
var iter = mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
const ps = iter.first();
if (std.mem.eql(u8, "48", ps)) {
if (mem.eql(u8, "48", ps)) {
// in band window resize
// CSI 48 ; height ; width ; height_pix ; width_pix t
const 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: terminal.Size = .{
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
};
// check for minimal size (if any was provided)
if (this.min_size) |min_size| {
if (size.cols < min_size.cols or size.rows < min_size.rows) {
this.postEvent(.{
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
});
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(.resize);
// this.postEvent(.{ .size = .{
// .x = fmt.parseUnsigned(u16, width_char, 10) catch break,
// .y = fmt.parseUnsigned(u16, height_char, 10) catch break,
// } });
}
},
'u' => {
@@ -353,28 +337,47 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
else => {},
}
},
// TODO: parse corresponding codes
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
else => {},
0x50 => {
// DCS
},
0x58 => {
// SOS
},
0x5D => {
// OSC
},
// TODO parse corresponding codes
0x5F => {
// APC
// parse for kitty graphics capabilities
},
else => {
// alt + <char> keypress
this.postEvent(.{
.key = .{
.cp = buf[1],
.mod = .{ .alt = true },
},
});
},
}
} else {
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 => .{ .cp = 'j', .mod = .{ .ctrl = true } },
0x0d => .{ .cp = input.Enter },
0x01...0x07, 0x0b...0x0c, 0x0e...0x1a => .{ .cp = b + 0x60, .mod = .{ .ctrl = true } },
0x1b => escape: {
std.debug.assert(read_bytes == 1);
break :escape .{ .cp = Key.escape };
assert(read_bytes == 1);
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] };
while (iter.next()) |cp| {
this.postEvent(.{ .key = .{ .cp = cp.code } });
}
var iter = code_point.Iterator{ .bytes = buf[0..read_bytes] };
while (iter.next()) |cp| this.postEvent(.{ .key = .{ .cp = cp.code } });
continue;
},
};
@@ -385,5 +388,54 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
break;
}
}
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
terminal.disableMouseSupport() catch {};
terminal.exitAltScreen() catch {};
terminal.showCursor() catch {};
var termios: posix.termios = .{
.iflag = .{},
.lflag = .{},
.cflag = .{},
.oflag = .{},
.cc = undefined,
.line = 0,
.ispeed = undefined,
.ospeed = undefined,
};
terminal.disableRawMode(&termios) catch {};
terminal.restoreScreen() catch {};
std.debug.defaultPanic(msg, ret_addr);
}
const element = @import("element.zig");
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Container = @import("container.zig").Container(Event);
pub const Element = element.Element(Event);
pub const Alignment = element.Alignment(Event);
pub const Button = element.Button(Event, Queue);
pub const Input = element.Input(Event, Queue);
pub const Progress = element.Progress(Event, Queue);
pub const Scrollable = element.Scrollable(Event);
pub const Queue = queue.Queue(Event, 256);
};
}
const log = std.log.scoped(.app);
const std = @import("std");
const mem = std.mem;
const fmt = std.fmt;
const posix = std.posix;
const Thread = std.Thread;
const assert = std.debug.assert;
const code_point = @import("code_point");
const event = @import("event.zig");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const queue = @import("queue.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Mouse = input.Mouse;
const Key = input.Key;
const Point = @import("point.zig").Point;

66
src/cell.zig Normal file
View File

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

41
src/color.zig Normal file
View File

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

1005
src/container.zig Normal file

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";

1398
src/element.zig Normal file

File diff suppressed because it is too large Load Diff

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,28 +1,32 @@
//! 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 terminal = @import("terminal.zig");
const Size = terminal.Size;
const Key = terminal.Key;
pub const Error = struct {
err: anyerror,
msg: []const u8,
};
// System events available to every application.
// TODO: should this also already include the .view enum option?
/// 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
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,
err: Error,
resize: Size,
/// Resize event to signify that the application should re-draw to resize
resize,
/// Error event to notify other containers about a recoverable error
err: struct {
/// actual error
err: anyerror,
/// associated error message
msg: []const u8,
},
/// 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
focus: bool,
};
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
// TODO: should this expect one of the unions to contain the .view value option with its corresponding associated type?
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
}
@@ -84,3 +88,10 @@ pub fn isTaggedUnion(comptime E: type) bool {
}
return true;
}
const std = @import("std");
const input = @import("input.zig");
const terminal = @import("terminal.zig");
const Key = input.Key;
const Mouse = input.Mouse;
const Point = @import("point.zig").Point;

215
src/input.zig Normal file
View File

@@ -0,0 +1,215 @@
//! Input module for `zterm`. Contains structs to represent key events and mouse events.
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 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 meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
/// Determine if the `Key` is an ascii character that can be printed to
/// the screen. This means that the code point of the `Key` is an ascii
/// character between 32 - 255 (with the exception of 127 = Delete) and no
/// 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 testing.expectEqual(true, isAscii(.{ .cp = 'c' }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .ctrl = true } }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true } }));
try testing.expectEqual(false, isAscii(.{ .cp = 'c', .mod = .{ .alt = true, .ctrl = true } }));
}
test "isAscii with non-ascii character" {
try testing.expectEqual(false, isAscii(.{ .cp = Escape }));
try testing.expectEqual(false, isAscii(.{ .cp = Enter }));
try testing.expectEqual(false, isAscii(.{ .cp = Enter, .mod = .{ .alt = true } }));
}
test "isAscii with excluded input.Delete" {
try testing.expectEqual(false, isAscii(.{ .cp = Delete }));
try testing.expectEqual(false, isAscii(.{ .cp = Delete, .mod = .{ .alt = false, .ctrl = false } }));
}
};
// TODO: std.ascii has the escape codes too!
// codepoints for keys
pub const Tab: u21 = 0x09;
pub const Enter: u21 = 0x0D;
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 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;
const std = @import("std");
const meta = std.meta;
const Point = @import("point.zig").Point;
const testing = std.testing;

View File

@@ -1,106 +0,0 @@
//! Dynamic dispatch for layout implementations. Each `Layout` has to implement
//! the `Layout.Interface`.
//!
//! Create a `Layout` using `createFrom(object: anytype)` and use them through
//! the defined `Layout.Interface`. The layout will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! Each `Layout` is responsible for clearing the allocated memory of the used
//! `Element`s (union of `Layout` or `Widget`) when deallocated. This means
//! that `deinit()` will also deallocate every used `Element` too.
//!
//! When `Layout.render` is called the provided `Renderer` type is expected
//! which handles how contents are rendered for a given layout.
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
pub fn Layout(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
const Events = std.ArrayList(Event);
const Type = struct {
const LayoutType = @This();
const Element = union(enum) {
layout: LayoutType,
widget: @import("widget.zig").Widget(Event, Renderer),
};
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) anyerror!*Events,
.render = fn (anytype, *Renderer) anyerror!void,
.deinit = fn (anytype) void,
}, .{});
const VTable = struct {
handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events,
render: *const fn (this: *LayoutType, renderer: *Renderer) anyerror!void,
deinit: *const fn (this: *LayoutType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Layout`.
pub fn handle(this: *LayoutType, event: Event) !*Events {
return try this.vtable.handle(this, event);
}
// Render this `Layout` completely. This will render contained sub-elements too.
pub fn render(this: *LayoutType, renderer: *Renderer) !void {
return try this.vtable.render(this, renderer);
}
pub fn deinit(this: *LayoutType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) LayoutType {
return LayoutType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
// Handle the provided `Event` for this `Layout`.
fn handle(this: *LayoutType, event: Event) !*Events {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return try layout.handle(event);
}
}.handle,
.render = struct {
// Render the contents of this `Layout`.
fn render(this: *LayoutType, renderer: *Renderer) !void {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try layout.render(renderer);
}
}.render,
.deinit = struct {
fn deinit(this: *LayoutType) void {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
layout.deinit();
}
}.deinit,
},
};
}
// import and export of `Layout` implementations
pub const HContainer = @import("layout/HContainer.zig").Layout(Event, Element, Renderer);
pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer);
pub const VContainer = @import("layout/VContainer.zig").Layout(Event, Element, Renderer);
pub const VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer);
pub const Padding = @import("layout/Padding.zig").Layout(Event, Element, Renderer);
pub const Margin = @import("layout/Margin.zig").Layout(Event, Element, Renderer);
pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer);
pub const Tab = @import("layout/Tab.zig").Layout(Event, Element, Renderer);
};
// test layout implementation satisfies the interface
comptime Type.Interface.satisfiedBy(Type);
comptime Type.Interface.satisfiedBy(Type.HContainer);
comptime Type.Interface.satisfiedBy(Type.HStack);
comptime Type.Interface.satisfiedBy(Type.VContainer);
comptime Type.Interface.satisfiedBy(Type.VStack);
comptime Type.Interface.satisfiedBy(Type.Padding);
comptime Type.Interface.satisfiedBy(Type.Margin);
comptime Type.Interface.satisfiedBy(Type.Framing);
comptime Type.Interface.satisfiedBy(Type.Tab);
return Type;
}

View File

@@ -1,198 +0,0 @@
//! Framing layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Style = terminal.Cell.Style;
const log = std.log.scoped(.layout_framing);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
style: Style = .{ .fg = .default },
frame: Frame = .round,
title: Title = .{},
const Title = struct {
str: []const u8 = &.{},
style: Style = .{ .fg = .default },
};
const Frame = enum {
round,
square,
};
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
var this = allocator.create(@This()) catch @panic("Framing.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the containing elements
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + 1,
.row = size.anchor.row + 1,
},
.cols = size.cols -| 2,
.rows = size.rows -| 2,
},
};
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
const round_frame = .{ "", "", "", "", "", "" };
const square_frame = .{ "", "", "", "", "", "" };
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
// FIXME: use renderer instead!
_ = renderer;
const frame = switch (this.config.frame) {
.round => round_frame,
.square => square_frame,
};
std.debug.assert(frame.len == 6);
// render top: +---+
try terminal.setCursorPosition(this.size.anchor);
const writer = terminal.writer();
try this.config.style.value(writer, frame[0]);
if (this.config.title.str.len > 0) {
try this.config.title.style.value(writer, this.config.title.str);
}
for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| {
try this.config.style.value(writer, frame[1]);
}
try this.config.style.value(writer, frame[2]);
// render left: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + row,
});
try this.config.style.value(writer, frame[3]);
}
// render right: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col + this.size.cols -| 1,
.row = this.size.anchor.row + row,
});
try this.config.style.value(writer, frame[3]);
}
// render bottom: +---+
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows - 1,
});
try this.config.style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| {
try this.config.style.value(writer, frame[1]);
}
try this.config.style.value(writer, frame[5]);
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
try this.renderFrame(renderer);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,187 +0,0 @@
//! Horizontal Container layout for nested `Layout`s and/or `Widget`s.
//! The contained elements are sized according to the provided configuration.
//! For an evenly spaced horizontal stacking see the `HStack` layout.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_hcontainer);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Container = struct {
element: Element,
container_size: u8, // 0 - 100 %
};
const Containers = std.ArrayList(Container);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
containers: Containers,
events: Events,
pub fn init(allocator: std.mem.Allocator, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
comptime var total_size = 0;
const fields_info = args_type_info.@"struct".fields;
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("HContainer.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
const child_type_info = @typeInfo(ChildType);
if (child_type_info != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 2) {
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const element_size = @field(child, child_fields[1].name);
const ElementSizeType = @TypeOf(element_size);
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
}
total_size += element_size;
if (total_size > 100) {
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
}
if (ElementType == WidgetType) {
containers.append(.{
.element = .{ .widget = element },
.container_size = element_size,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
containers.append(.{
.element = .{ .layout = element },
.container_size = element_size,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("HContainer.zig: Failed to create.");
this.allocator = allocator;
this.containers = containers;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.containers.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the container size
var offset: u16 = 0;
for (this.containers.items) |*container| {
const cols = @divTrunc(size.cols * container.container_size, 100);
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
},
};
offset += cols;
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,162 +0,0 @@
//! Horizontal Stacking layout for nested `Layout`s and/or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_hstack);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Elements = std.ArrayList(Element);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
elements: Elements,
events: Events,
pub fn init(allocator: std.mem.Allocator, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
const fields_info = args_type_info.@"struct".fields;
var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("HStack.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
if (ChildType == WidgetType) {
elements.append(.{ .widget = child }) catch {};
continue;
}
if (ChildType == LayoutType) {
elements.append(.{ .layout = child }) catch {};
continue;
}
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("HStack.zig: Failed to create.");
this.allocator = allocator;
this.elements = elements;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.elements.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
const len: u16 = @truncate(this.elements.items.len);
const element_cols = @divTrunc(size.cols, len);
var overflow = size.cols % len;
var offset: u16 = 0;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var cols = element_cols;
if (overflow > 0) {
overflow -|= 1;
cols += 1;
}
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
},
};
offset += cols;
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,164 +0,0 @@
//! Margin layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_margin);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
margin: ?u8 = null,
left: u8 = 0,
right: u8 = 0,
top: u8 = 0,
bottom: u8 = 0,
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
if (config.margin) |margin| {
std.debug.assert(margin <= 50);
} else {
std.debug.assert(config.left + config.right < 100);
std.debug.assert(config.top + config.bottom < 100);
}
var this = allocator.create(@This()) catch @panic("Margin.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
var sub_event: Event = undefined;
if (this.config.margin) |margin| {
// used overall margin
const h_margin: u16 = @divTrunc(margin * size.cols, 100);
const v_margin: u16 = @divFloor(margin * size.rows, 100);
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + h_margin,
.row = size.anchor.row + v_margin,
},
.cols = size.cols -| (h_margin * 2),
.rows = size.rows -| (v_margin * 2),
},
};
} else {
// use all for directions individually
const left_margin: u16 = @divFloor(this.config.left * size.cols, 100);
const right_margin: u16 = @divFloor(this.config.right * size.cols, 100);
const top_margin: u16 = @divFloor(this.config.top * size.rows, 100);
const bottom_margin: u16 = @divFloor(this.config.bottom * size.rows, 100);
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + left_margin,
.row = size.anchor.row + top_margin,
},
.cols = size.cols -| left_margin -| right_margin,
.rows = size.rows -| top_margin -| bottom_margin,
},
};
}
// adjust size according to the containing elements
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,152 +0,0 @@
//! Padding layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_padding);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
padding: ?u16 = null,
left: u16 = 0,
right: u16 = 0,
top: u16 = 0,
bottom: u16 = 0,
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
var this = allocator.create(@This()) catch @panic("Padding.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
var sub_event: Event = undefined;
if (this.config.padding) |padding| {
// used overall padding
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + padding,
.row = size.anchor.row + padding,
},
.cols = size.cols -| (padding * 2),
.rows = size.rows -| (padding * 2),
},
};
} else {
// use all for directions individually
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + this.config.left,
.row = size.anchor.row + this.config.top,
},
.cols = size.cols -| this.config.left -| this.config.right,
.rows = size.rows -| this.config.top -| this.config.bottom,
},
};
}
// adjust size according to the containing elements
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,314 +0,0 @@
//! Tab layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Cell = terminal.Cell;
const Style = Cell.Style;
const Color = Style.Color;
const log = std.log.scoped(.layout_tab);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Tab = struct {
element: Element,
title: []const u8,
color: Color,
};
const Tabs = std.ArrayList(Tab);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
tabs: Tabs,
active_tab: usize,
events: Events,
config: Config,
const Config = struct {
frame: Frame = .round,
const Frame = enum {
round,
square,
};
};
pub fn init(allocator: std.mem.Allocator, config: Config, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
const fields_info = args_type_info.@"struct".fields;
var tabs = Tabs.initCapacity(allocator, fields_info.len) catch @panic("Tab.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
const child_type_info = @typeInfo(ChildType);
if (child_type_info != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 3) {
@compileError("expected nested tuple or struct to have exactly 3 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const tab_title = @field(child, child_fields[1].name);
const TabTitleType = @TypeOf(tab_title);
const tab_title_type_info = @typeInfo(TabTitleType);
const tab_color = @field(child, child_fields[2].name);
const TabColorType = @TypeOf(tab_color);
if (tab_title_type_info != .array and tab_title_type_info != .pointer) {
// TODO: check for inner type of the title to be u8
@compileError("expected an u8 array second argument of nested tuple or struct child, but found " ++ @tagName(tab_title_type_info));
}
if (TabColorType != Color) {
@compileError("expected an Color typed third argument of nested tuple or struct child, but found " ++ @typeName(TabColorType));
}
if (ElementType == WidgetType) {
tabs.append(.{
.element = .{ .widget = element },
.title = tab_title,
.color = tab_color,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
tabs.append(.{
.element = .{ .layout = element },
.title = tab_title,
.color = tab_color,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("Tab.zig: Failed to create.");
this.allocator = allocator;
this.active_tab = 0;
this.require_render = true;
this.config = config;
this.tabs = tabs;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.tabs.items) |*tab| {
switch (tab.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.tabs.deinit();
this.allocator.destroy(this);
}
fn resize_active_tab(this: *@This()) !void {
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = this.size.anchor.col + 1,
.row = this.size.anchor.row + 1,
},
.cols = this.size.cols -| 2,
.rows = this.size.rows -| 2,
},
};
// resize active tab to re-render the widget in the following render loop
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
if (this.tabs.items.len == 0) {
return &this.events;
}
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
try this.resize_active_tab();
},
.key => |key| {
// tab -> cycle forward
// back-tab -> cycle backward
if (key.matches(.{ .cp = Key.tab })) {
this.active_tab += 1;
this.active_tab %= this.tabs.items.len;
this.require_render = true;
try this.resize_active_tab();
} else if (key.matches(.{ .cp = Key.tab, .mod = .{ .shift = true } })) { // backtab / shift + tab
if (this.active_tab > 0) {
this.active_tab -|= 1;
} else {
this.active_tab = this.tabs.items.len - 1;
}
this.require_render = true;
try this.resize_active_tab();
} else {
// TODO: absorb tab key or send key down too?
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
// NOTE: should this only send the event to the 'active_tab'
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
const round_frame = .{ "", "", "", "", "", "" };
const square_frame = .{ "", "", "", "", "", "" };
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
// FIXME: use renderer instead!
_ = renderer;
const frame = switch (this.config.frame) {
.round => round_frame,
.square => square_frame,
};
std.debug.assert(frame.len == 6);
// render top: +---+
try terminal.setCursorPosition(this.size.anchor);
const writer = terminal.writer();
var style: Style = .{ .fg = this.tabs.items[this.active_tab].color };
try style.value(writer, frame[0]);
var tab_title_len: usize = 0;
for (this.tabs.items, 0..) |tab, idx| {
var tab_style: Cell.Style = .{
.fg = tab.color,
.bg = .default,
};
if (idx == this.active_tab) {
tab_style.fg = .default;
tab_style.bg = tab.color;
}
const cell: Cell = .{
.content = tab.title,
.style = tab_style,
};
try cell.value(writer, 0, tab.title.len);
tab_title_len += tab.title.len;
}
for (0..this.size.cols -| 2 -| tab_title_len) |_| {
try style.value(writer, frame[1]);
}
try style.value(writer, frame[2]);
// render left: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + row,
});
try style.value(writer, frame[3]);
}
// render right: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col + this.size.cols -| 1,
.row = this.size.anchor.row + row,
});
try style.value(writer, frame[3]);
}
// render bottom: +---+
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows - 1,
});
try style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| {
try style.value(writer, frame[1]);
}
try style.value(writer, frame[5]);
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
try this.renderFrame(renderer);
this.require_render = false;
}
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,187 +0,0 @@
//! Vertical Container layout for nested `Layout`s and/or `Widget`s.
//! The contained elements are sized according to the provided configuration.
//! For an evenly spaced vertical stacking see the `VStack` layout.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_vcontainer);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Container = struct {
element: Element,
container_size: u8, // 0 - 100 %
};
const Containers = std.ArrayList(Container);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
containers: Containers,
events: Events,
pub fn init(allocator: std.mem.Allocator, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
comptime var total_size = 0;
const fields_info = args_type_info.@"struct".fields;
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("VContainer.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
const child_type_info = @typeInfo(ChildType);
if (child_type_info != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 2) {
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const element_size = @field(child, child_fields[1].name);
const ElementSizeType = @TypeOf(element_size);
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
}
total_size += element_size;
if (total_size > 100) {
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
}
if (ElementType == WidgetType) {
containers.append(.{
.element = .{ .widget = element },
.container_size = element_size,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
containers.append(.{
.element = .{ .layout = element },
.container_size = element_size,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("VContainer.zig: Failed to create.");
this.allocator = allocator;
this.containers = containers;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.containers.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the container size
var offset: u16 = 0;
for (this.containers.items) |*container| {
const rows = @divTrunc(size.rows * container.container_size, 100);
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col,
.row = size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
},
};
offset += rows;
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,161 +0,0 @@
//! Vertical Stacking layout for nested `Layout`s and/or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_vstack);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Elements = std.ArrayList(Element);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
elements: Elements,
events: Events,
pub fn init(allocator: std.mem.Allocator, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
const fields_info = args_type_info.@"struct".fields;
var elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("VStack.zig out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
if (ChildType == WidgetType) {
elements.append(.{ .widget = child }) catch {};
continue;
}
if (ChildType == LayoutType) {
elements.append(.{ .layout = child }) catch {};
continue;
}
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("VStack.zig: Failed to create.");
this.allocator = allocator;
this.elements = elements;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.elements.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
const len: u16 = @truncate(this.elements.items.len);
const element_rows = @divTrunc(size.rows, len);
var overflow = size.rows % len;
var offset: u16 = 0;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var rows = element_rows;
if (overflow > 0) {
overflow -|= 1;
rows += 1;
}
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col,
.row = size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
},
};
offset += rows;
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

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

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

View File

@@ -1,108 +1,131 @@
//! Renderer which holds the screen to compare with the previous screen for efficient rendering.
//! Each renderer should at least implement these functions:
//! - resize(this: *@This(), size: Size) void {}
//! - clear(this: *@This(), size: Size) !void {}
//! - render(this: *@This(), size: Size, contents: []u8) !void {}
//!
//! Each `Renderer` should be able to be used interchangeable without having to
//! change any code of any `Layout` or `Widget`. The only change should be the
//! passed type to `zterm.App` _R_ parameter.
const std = @import("std");
const terminal = @import("terminal.zig");
//! Renderer for `zterm`.
const Cells = []const terminal.Cell;
const Position = terminal.Position;
const Size = terminal.Size;
/// Double-buffered intermediate rendering pipeline
pub const Buffered = struct {
allocator: Allocator,
created: bool,
size: Point,
screen: []Cell,
virtual_screen: []Cell,
pub fn Direct(comptime fullscreen: bool) type {
const log = std.log.scoped(.renderer_direct);
_ = log;
_ = fullscreen;
return struct {
size: Size = undefined,
pub fn init(allocator: Allocator) @This() {
return .{
.allocator = allocator,
.created = false,
.size = undefined,
.screen = undefined,
.virtual_screen = undefined,
};
}
pub fn resize(this: *@This(), size: Size) void {
this.size = size;
pub fn deinit(this: *@This()) void {
if (this.created) {
this.allocator.free(this.screen);
this.allocator.free(this.virtual_screen);
}
}
pub fn clear(this: *@This(), size: Size) !void {
_ = this;
// NOTE: clear on the entire screen may introduce too much overhead and could instead clear the entire screen instead.
// - it could also then try to optimize for further *clear* calls, that result in pretty much a nop? -> how to identify those clear calls?
// TODO: this should instead by dynamic and correct of size (terminal could be too large currently)
std.debug.assert(1028 > size.cols);
var buf: [1028]u8 = undefined;
@memset(buf[0..], ' ');
for (0..size.rows) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = size.anchor.col,
.row = size.anchor.row + row,
});
_ = try terminal.write(buf[0..size.cols]);
pub fn resize(this: *@This()) !Point {
const size = terminal.getTerminalSize();
if (meta.eql(this.size, size)) return this.size;
this.size = size;
const n = @as(usize, this.size.x) * @as(usize, this.size.y);
if (!this.created) {
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, n) catch @panic("render.zig: Out of memory.");
this.allocator.free(this.virtual_screen);
this.virtual_screen = this.allocator.alloc(Cell, n) catch @panic("render.zig: Out of memory.");
@memset(this.virtual_screen, .{});
}
try this.clear();
return size;
}
/// Clear the entire screen and reset the screen buffer, to force a re-draw with the next `flush` call.
pub fn clear(this: *@This()) !void {
try terminal.clearScreen();
@memset(this.screen, .{});
}
/// Render provided cells at size (anchor and dimension) into the *virtual screen*.
pub fn render(this: *@This(), comptime T: type, container: *T) !void {
const size: 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, origin.y) * @as(usize, this.size.x)) + @as(usize, origin.x);
blk: for (0..size.y) |row| {
for (0..size.x) |col| {
vs[anchor + (row * this.size.x) + col] = cells[idx];
idx += 1;
if (cells.len == idx) break :blk;
}
}
// free immediately
container.allocator.free(cells);
pub fn render(this: *@This(), size: Size, cells: Cells) !void {
_ = this;
try terminal.setCursorPosition(size.anchor);
var row: u16 = 0;
var remaining_cols = size.cols;
const writer = terminal.writer();
for (cells) |cell| {
var idx: usize = 0;
print_cell: while (true) {
const cell_len = cell.len(idx);
if (cell_len > remaining_cols) {
const result = try cell.writeUpToNewline(writer, idx, idx + remaining_cols);
row += 1;
if (row >= size.rows) {
return; // we are done
}
try terminal.setCursorPosition(.{
.col = size.anchor.col,
.row = size.anchor.row + row,
});
remaining_cols = size.cols;
idx = result.idx;
if (result.newline) {
idx += 1; // skip over newline
} else {
// there is still content to the newline (which will not be printed)
for (idx..cell.content.len) |i| {
if (cell.content[i] == '\n') {
idx = i + 1;
continue :print_cell;
}
}
break; // go to next cell (as we went to the end of the cell and do not print on the next line)
}
} else {
// print rest of cell
const result = try cell.writeUpToNewline(writer, idx, idx + cell_len);
if (result.newline) {
row += 1;
if (row >= size.rows) {
return; // we are done
}
try terminal.setCursorPosition(.{
.col = size.anchor.col,
.row = size.anchor.row + row,
});
remaining_cols = size.cols;
idx = result.idx + 1; // skip over newline
} else {
remaining_cols -= @truncate(cell_len -| idx);
idx = 0;
break; // go to next cell
}
}
// written all cell contents
if (idx >= cell.content.len) {
break; // go to next cell
}
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 {
try terminal.hideCursor();
// TODO measure timings of rendered frames?
var cursor_position: ?Point = null;
const writer = terminal.writer();
const s = this.screen;
const vs = this.virtual_screen;
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];
// update the latest found cursor position
if (cvs.style.cursor) {
assert(cursor_position == null);
cursor_position = .{
.x = @truncate(col),
.y = @truncate(row),
};
}
if (cs.eql(cvs)) continue;
// render differences found in virtual screen
try terminal.setCursorPosition(.{ .y = @truncate(row), .x = @truncate(col) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame
s[idx] = vs[idx];
}
}
};
}
if (cursor_position) |point| {
try terminal.showCursor();
try terminal.setCursorPosition(point);
}
}
};
const std = @import("std");
const meta = std.meta;
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const terminal = @import("terminal.zig");
const Cell = @import("cell.zig");
const Point = @import("point.zig").Point;

41
src/root.zig Normal file
View File

@@ -0,0 +1,41 @@
// private imports
const container = @import("container.zig");
const color = @import("color.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
pub const Border = container.Border;
pub const Rectangle = container.Rectangle;
pub const Layout = container.Layout;
pub const Cell = @import("cell.zig");
pub const Color = color.Color;
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;
_ = Cell;
_ = Key;
_ = Style;
}

78
src/style.zig Normal file
View File

@@ -0,0 +1,78 @@
//! Helper function collection to provide ascii encodings for styling outputs.
//! Stylings are implemented such that they can be nested in anyway to support
//! multiple styles (i.e. bold and italic).
//!
//! Stylings however also include highlighting for specific terminal capabilities.
//! For example url highlighting.
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
// with slight modifications
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
cursor: bool = false,
ul_style: Underline = .off,
emphasis: []const Emphasis,
pub const Underline = enum {
off,
single,
double,
curly,
dotted,
dashed,
};
pub const Emphasis = enum(u8) {
reset = 0,
bold = 1,
dim,
italic,
underline,
blink,
invert = 7,
hidden,
strikethrough,
};
pub fn eql(this: Style, other: Style) bool {
return meta.eql(this, other);
}
// TODO might be useful to use the std.ascii stuff!
pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined;
const bytes = try unicode.utf8Encode(cp, &buffer);
assert(bytes > 0);
// build ansi sequence for 256 colors ...
// foreground
try format(writer, "\x1b[", .{});
try this.fg.write(writer, .fg);
// background
try format(writer, ";", .{});
try this.bg.write(writer, .bg);
// underline
// FIX assert that if the underline property is set that the ul style and the attribute for underlining is available
try format(writer, ";", .{});
try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.emphasis) |attribute| try format(writer, ";{d}", .{@intFromEnum(attribute)});
try format(writer, "m", .{});
// content
try format(writer, "{s}", .{buffer[0..bytes]});
try format(writer, "\x1b[0m", .{});
}
// TODO implement helper functions for terminal capabilities:
// - links / url display (osc 8)
// - show / hide cursor?
const std = @import("std");
const unicode = std.unicode;
const meta = std.meta;
const assert = std.debug.assert;
const format = std.fmt.format;
const Color = @import("color.zig").Color;
const Style = @This();

View File

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

View File

@@ -1,63 +0,0 @@
const std = @import("std");
pub const Style = @import("Style.zig");
style: Style = .{},
content: []const u8 = undefined,
pub const Result = struct {
idx: usize,
newline: bool,
};
pub fn len(this: @This(), start: usize) usize {
std.debug.assert(this.content.len > start);
return this.content[start..].len;
}
pub fn write(this: @This(), writer: anytype, start: usize, end: usize) !void {
std.debug.assert(this.content.len > start);
std.debug.assert(this.content.len >= end);
std.debug.assert(start < end);
try this.style.value(writer, this.content[start..end]);
}
pub fn writeUpToNewline(this: @This(), writer: anytype, start: usize, end: usize) !Result {
std.debug.assert(this.content.len > start);
std.debug.assert(this.content.len >= end);
std.debug.assert(start < end);
for (start..end) |i| {
if (this.content[i] == '\n') {
if (start < i) {
// this is just an empty line with a newline
try this.value(writer, start, i);
}
return .{
.idx = i,
.newline = true,
};
}
}
try this.write(writer, start, end);
return .{
.idx = end,
.newline = false,
};
}
pub fn value(this: @This(), writer: anytype, start: usize, end: usize) !void {
std.debug.assert(start < this.content.len);
std.debug.assert(this.content.len >= end);
std.debug.assert(start < end);
try this.style.value(writer, this.content[start..end]);
}
// not really supported
pub fn format(this: @This(), writer: anytype, comptime fmt: []const u8, args: anytype) !void {
try this.style.format(writer, fmt, args); // NOTE: args should contain this.content[start..end] or this.content
}
test {
_ = Style;
}

View File

@@ -1,149 +0,0 @@
//! Keybindings and Modifiers for user input detection and selection.
const std = @import("std");
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;

View File

@@ -1,2 +0,0 @@
col: u16,
row: u16,

View File

@@ -1,5 +0,0 @@
const Position = @import("Position.zig");
anchor: Position = .{ .col = 1, .row = 1 }, // top left corner by default
cols: u16,
rows: u16,

View File

@@ -1,305 +0,0 @@
//! Helper function collection to provide ascii encodings for styling outputs.
//! Stylings are implemented such that they can be nested in anyway to support
//! multiple styles (i.e. bold and italic).
//!
//! Stylings however also include highlighting for specific terminal capabilities.
//! For example url highlighting.
// taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License)
// with slight modifications
const std = @import("std");
const ctlseqs = @import("ctlseqs.zig");
pub const Underline = enum {
off,
single,
double,
curly,
dotted,
dashed,
};
pub const Color = union(enum) {
default,
index: u8,
rgb: [3]u8,
pub fn eql(a: @This(), b: @This()) bool {
switch (a) {
.default => return b == .default,
.index => |a_idx| {
switch (b) {
.index => |b_idx| return a_idx == b_idx,
else => return false,
}
},
.rgb => |a_rgb| {
switch (b) {
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
a_rgb[1] == b_rgb[1] and
a_rgb[2] == b_rgb[2],
else => return false,
}
},
}
}
pub fn rgbFromUint(val: u24) Color {
const r_bits = val & 0b11111111_00000000_00000000;
const g_bits = val & 0b00000000_11111111_00000000;
const b_bits = val & 0b00000000_00000000_11111111;
const rgb = [_]u8{
@truncate(r_bits >> 16),
@truncate(g_bits >> 8),
@truncate(b_bits),
};
return .{ .rgb = rgb };
}
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
/// be the same as the low two bits.
pub fn rgbFromSpec(spec: []const u8) !Color {
var iter = std.mem.splitScalar(u8, spec, ':');
const prefix = iter.next() orelse return error.InvalidColorSpec;
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
const spec_str = iter.next() orelse return error.InvalidColorSpec;
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (r_raw.len != 4) return error.InvalidColorSpec;
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (g_raw.len != 4) return error.InvalidColorSpec;
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (b_raw.len != 4) return error.InvalidColorSpec;
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
return .{
.rgb = [_]u8{ r, g, b },
};
}
test "rgbFromSpec" {
const spec = "rgb:aaaa/bbbb/cccc";
const actual = try rgbFromSpec(spec);
switch (actual) {
.rgb => |rgb| {
try std.testing.expectEqual(0xAA, rgb[0]);
try std.testing.expectEqual(0xBB, rgb[1]);
try std.testing.expectEqual(0xCC, rgb[2]);
},
else => try std.testing.expect(false),
}
}
};
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
ul_style: Underline = .off,
bold: bool = false,
dim: bool = false,
italic: bool = false,
blink: bool = false,
reverse: bool = false,
invisible: bool = false,
strikethrough: bool = false,
/// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value
/// if the _other_ value differs from the default value.
pub fn merge(this: *@This(), other: @This()) void {
if (other.fg != .default) this.fg = other.fg;
if (other.bg != .default) this.bg = other.bg;
if (other.ul != .default) this.ul = other.ul;
if (other.ul_style != .off) this.ul_style = other.ul_style;
if (other.bold != false) this.bold = other.bold;
if (other.dim != false) this.dim = other.dim;
if (other.italic != false) this.italic = other.italic;
if (other.blink != false) this.blink = other.blink;
if (other.reverse != false) this.reverse = other.reverse;
if (other.invisible != false) this.invisible = other.invisible;
if (other.strikethrough != false) this.strikethrough = other.strikethrough;
}
fn start(this: @This(), writer: anytype) !void {
// foreground
switch (this.fg) {
.default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
.index => |idx| {
switch (idx) {
0...7 => {
try std.fmt.format(writer, ctlseqs.fg_base, .{idx});
},
8...15 => {
try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8});
},
else => {
try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx});
},
}
},
.rgb => |rgb| {
try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
// background
switch (this.bg) {
.default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
.index => |idx| {
switch (idx) {
0...7 => {
try std.fmt.format(writer, ctlseqs.bg_base, .{idx});
},
8...15 => {
try std.fmt.format(writer, ctlseqs.bg_bright, .{idx});
},
else => {
try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx});
},
}
},
.rgb => |rgb| {
try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
// underline color
switch (this.ul) {
.default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
.index => |idx| {
try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx});
},
.rgb => |rgb| {
try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] });
},
}
// underline style
switch (this.ul_style) {
.off => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
.single => try std.fmt.format(writer, ctlseqs.ul_single, .{}),
.double => try std.fmt.format(writer, ctlseqs.ul_double, .{}),
.curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}),
.dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}),
.dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}),
}
// bold
switch (this.bold) {
true => try std.fmt.format(writer, ctlseqs.bold_set, .{}),
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
}
// dim
switch (this.dim) {
true => try std.fmt.format(writer, ctlseqs.dim_set, .{}),
false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
}
// italic
switch (this.italic) {
true => try std.fmt.format(writer, ctlseqs.italic_set, .{}),
false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
}
// blink
switch (this.blink) {
true => try std.fmt.format(writer, ctlseqs.blink_set, .{}),
false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
}
// reverse
switch (this.reverse) {
true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}),
false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
}
// invisible
switch (this.invisible) {
true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}),
false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
}
// strikethrough
switch (this.strikethrough) {
true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}),
false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
}
}
fn end(this: @This(), writer: anytype) !void {
// foreground
switch (this.fg) {
.default => {},
else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
}
// background
switch (this.bg) {
.default => {},
else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}),
}
// underline color
switch (this.ul) {
.default => {},
else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}),
}
// underline style
switch (this.ul_style) {
.off => {},
else => try std.fmt.format(writer, ctlseqs.ul_off, .{}),
}
// bold
switch (this.bold) {
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
false => {},
}
// dim
switch (this.dim) {
true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}),
false => {},
}
// italic
switch (this.italic) {
true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}),
false => {},
}
// blink
switch (this.blink) {
true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}),
false => {},
}
// reverse
switch (this.reverse) {
true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}),
false => {},
}
// invisible
switch (this.invisible) {
true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}),
false => {},
}
// strikethrough
switch (this.strikethrough) {
true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}),
false => {},
}
}
pub fn format(this: @This(), writer: anytype, comptime content: []const u8, args: anytype) !void {
try this.start(writer);
try std.fmt.format(writer, content, args);
try this.end(writer);
}
pub fn value(this: @This(), writer: anytype, content: []const u8) !void {
try this.start(writer);
_ = try writer.write(content);
try this.end(writer);
}
// TODO: implement helper functions for terminal capabilities:
// - links / url display (osc 8)
// - show / hide cursor?
test {
_ = Color;
}

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

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

199
src/testing.zig Normal file
View File

@@ -0,0 +1,199 @@
//! Testing namespace for `zterm` to provide testing capabilities for `Containers`, `Event` handling, `App`s and `Element` implementations.
/// Single-buffer test rendering pipeline for testing purposes.
pub const Renderer = struct {
allocator: Allocator,
size: Point,
screen: []Cell,
pub fn init(allocator: Allocator, size: Point) @This() {
const screen = allocator.alloc(Cell, @as(usize, size.x) * @as(usize, size.y)) catch @panic("testing.zig: Out of memory.");
@memset(screen, .{});
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 = testing.allocator;
var renderer: Renderer = .init(allocator, size);
defer renderer.deinit();
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 = testing.allocator;
try testing.expectEqual(expected.len, actual.len);
try testing.expectEqual(expected.len, @as(usize, size.y) * @as(usize, size.x));
var expected_cps = try std.ArrayList(Cell).initCapacity(allocator, size.x);
defer expected_cps.deinit();
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();
debug.lockStdErr();
defer debug.unlockStdErr();
const std_writer = std.io.getStdErr().writer();
try std_writer.writeAll(output.items);
return error.TestExpectEqualCells;
}
const std = @import("std");
const debug = std.debug;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const event = @import("event.zig");
const Container = @import("container.zig").Container;
const Cell = @import("cell.zig");
const DisplayWidth = @import("DisplayWidth");
const Point = @import("point.zig").Point;

View File

@@ -1,116 +0,0 @@
//! Dynamic dispatch for view implementations. Each `View` has to implement the `View.Interface`
//!
//! Create a `View` using `createFrom(object: anytype)` and use them through
//! the defined `View.Interface`. The view will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! A `View` holds the necessary `Layout`'s for different screen sizes as well
//! as the corresponding used `Widget`'s alongside holding the corresponding memory
//! for the data shown through the `View`.
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const log = std.log.scoped(.view);
pub fn View(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `View(comptime Event: type)` is not of type `union(enum)`.");
}
const Events = std.ArrayList(Event);
return struct {
const ViewType = @This();
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) anyerror!*Events,
.render = fn (anytype, *Renderer) anyerror!void,
.enable = fn (anytype) void,
.disable = fn (anytype) void,
.deinit = fn (anytype) void,
}, .{});
// TODO: this VTable creation and abstraction could maybe even be done through a comptime implementation -> another library?
const VTable = struct {
handle: *const fn (this: *ViewType, event: Event) anyerror!*Events,
render: *const fn (this: *ViewType, renderer: *Renderer) anyerror!void,
enable: *const fn (this: *ViewType) void,
disable: *const fn (this: *ViewType) void,
deinit: *const fn (this: *ViewType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
/// Handle the provided `Event` for this `View`.
pub fn handle(this: *ViewType, event: Event) !*Events {
switch (event) {
.resize => |size| {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
},
else => {},
}
return this.vtable.handle(this, event);
}
/// Render the content of this `View` given the `Size` of the available terminal (.resize System`Event`).
pub fn render(this: *ViewType, renderer: *Renderer) !void {
try this.vtable.render(this, renderer);
}
/// Function to call when this `View` will be handled and rendered as the 'active' `View`.
pub fn enable(this: *ViewType) void {
this.vtable.enable(this);
}
/// Function to call when this `View` will no longer be 'active'.
pub fn disable(this: *ViewType) void {
this.vtable.disable(this);
}
pub fn deinit(this: *ViewType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) ViewType {
return ViewType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
fn handle(this: *ViewType, event: Event) !*Events {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return view.handle(event);
}
}.handle,
.render = struct {
fn render(this: *ViewType, renderer: *Renderer) !void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try view.render(renderer);
}
}.render,
.enable = struct {
fn enable(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.enable();
}
}.enable,
.disable = struct {
fn disable(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.disable();
}
}.disable,
.deinit = struct {
fn deinit(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.deinit();
}
}.deinit,
},
};
}
};
}

View File

@@ -1,107 +0,0 @@
//! Dynamic dispatch for widget implementations. Each `Widget` has to implement
//! the `Widget.Interface`.
//!
//! Create a `Widget` using `createFrom(object: anytype)` and use them through
//! the defined `Widget.Interface`. The widget will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! Each `Widget` may cache its content and should if the contents will not
//! change for a long time.
//!
//! When `Widget.render` is called the provided `Renderer` type is expected
//! which handles how contents are rendered for a given widget.
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const log = @import("std").log.scoped(.widget);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
const Type = struct {
const WidgetType = @This();
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) ?Event,
.render = fn (anytype, *Renderer) anyerror!void,
.deinit = fn (anytype) void,
}, .{});
const VTable = struct {
handle: *const fn (this: *WidgetType, event: Event) ?Event,
render: *const fn (this: *WidgetType, renderer: *Renderer) anyerror!void,
deinit: *const fn (this: *WidgetType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Widget`.
pub fn handle(this: *WidgetType, event: Event) ?Event {
switch (event) {
.resize => |size| {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
},
else => {},
}
return this.vtable.handle(this, event);
}
// Render the content of this `Widget` given the `Size` of the widget (.resize System`Event`).
pub fn render(this: *WidgetType, renderer: *Renderer) !void {
try this.vtable.render(this, renderer);
}
pub fn deinit(this: *WidgetType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) WidgetType {
return WidgetType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
// Handle the provided `Event` for this `Widget`.
fn handle(this: *WidgetType, event: Event) ?Event {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return widget.handle(event);
}
}.handle,
.render = struct {
// Return the entire content of this `Widget`.
fn render(this: *WidgetType, renderer: *Renderer) !void {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try widget.render(renderer);
}
}.render,
.deinit = struct {
fn deinit(this: *WidgetType) void {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
widget.deinit();
}
}.deinit,
},
};
}
// TODO: implement a minimal size requirement for Widgets to render correctly?
// import and export of `Widget` implementations
pub const Input = @import("widget/Input.zig").Widget(Event, Renderer);
pub const Text = @import("widget/Text.zig").Widget(Event, Renderer);
pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer);
pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer);
pub const List = @import("widget/List.zig").Widget(Event, Renderer);
};
// test widget implementation satisfies the interface
comptime Type.Interface.satisfiedBy(Type);
comptime Type.Interface.satisfiedBy(Type.Input);
comptime Type.Interface.satisfiedBy(Type.Text);
comptime Type.Interface.satisfiedBy(Type.RawText);
comptime Type.Interface.satisfiedBy(Type.Spacer);
comptime Type.Interface.satisfiedBy(Type.List);
return Type;
}

View File

@@ -1,250 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Cell = terminal.Cell;
const Key = terminal.Key;
const Size = terminal.Size;
const log = std.log.scoped(.widget_input);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
return struct {
active: bool,
allocator: std.mem.Allocator,
label: ?[]const u8,
placeholder: ?[]const u8,
size: Size,
require_render: bool,
value: std.ArrayList(u8),
/// value content length
value_len: usize,
/// current cursor position
cursor_idx: usize,
pub fn init(allocator: std.mem.Allocator, label: ?[]const u8, placeholder: ?[]const u8) *@This() {
var value = std.ArrayList(u8).init(allocator);
value.resize(32) catch @panic("Input.zig: out of memory");
var this = allocator.create(@This()) catch @panic("Input.zig: Failed to create.");
this.allocator = allocator;
this.active = false;
this.require_render = true;
this.label = null;
this.placeholder = null;
this.value_len = 0;
this.cursor_idx = 0;
this.value = value;
this.label = label;
this.placeholder = placeholder;
return this;
}
pub fn deinit(this: *@This()) void {
this.value.deinit();
this.allocator.destroy(this);
}
pub fn getValue(this: *const @This()) []const u8 {
return this.value.items[0..this.value_len];
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
var required_cols: u16 = 4; // '...c'
if (this.label) |label| {
required_cols += @as(u16, @truncate(label.len)); // <label>
required_cols += 2; // ': '
}
if (this.size.cols < required_cols) {
return .{ .err = .{
.err = error.InsufficientSize,
.msg = "Received Size is too small to render App.Widget.Input correctly",
} };
}
},
.key => |key| {
if (!this.active) {
return null;
}
if (key.matches(.{ .cp = Key.tab }) or key.matches(.{ .cp = Key.enter })) {
// ignored keys
} else if (key.mod.alt or key.mod.ctrl or key.matches(.{ .cp = Key.escape })) {
// TODO: what about ctrl-v, ctrl-w, alt-b, alt-f?
// ignored keys
} else if (key.matches(.{ .cp = Key.backspace })) {
// remove one character
_ = this.value.orderedRemove(this.cursor_idx);
this.cursor_idx -|= 1;
this.value_len -|= 1;
this.require_render = true;
} else if (key.matches(.{ .cp = Key.left }) or key.matches(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
// left
this.cursor_idx -|= 1;
this.require_render = true;
} else if (key.matches(.{ .cp = Key.right }) or key.matches(.{ .cp = 'f', .mod = .{ .ctrl = true } })) {
// right
if (this.cursor_idx < this.value_len) {
this.cursor_idx += 1;
this.require_render = true;
}
} else {
if (this.value.items.len <= this.value_len) {
// double capacity in case we need more space
this.value.resize(this.value.capacity * 2) catch |err| {
return .{
.err = .{
.err = err,
.msg = "Could not resize input value buffer",
},
};
};
}
this.value.insert(this.cursor_idx, @as(u8, @truncate(key.cp))) catch @panic("Input.zig: out of memory");
this.cursor_idx += 1;
this.value_len += 1;
this.require_render = true;
}
// TODO: handle key input that should be used for the input field value
// - move cursor using arrow keys
// - allow word-wise navigation?
// - add / remove characters
// - allow removal of words?
// - do not support pasting, as that can be done by the terminal emulator (not sure if this would even work correctly over ssh)
},
else => {},
}
return null;
}
// Overview of the rendered contents:
//
// With both label and placeholder:
// <label>: <placeholder>
// Without any label:
// <placeholder>
// Without any placeholder, but a label:
// <label>: ____________
// With neither label nor placeholder:
// ____________
// When value is not an empty string, the corresponding placeholder
// (if any) will be replaced with the current value. The current
// cursor position is show when this input field is `active`.
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
var size = this.size;
this.require_render = false;
if (this.label) |label| {
const label_style: Cell.Style = .{
.fg = .default,
.italic = true,
};
try renderer.render(size, &[_]Cell{
.{
.content = label,
.style = label_style,
},
});
size.anchor.col += @as(u16, @truncate(label.len));
size.cols -= @as(u16, @truncate(label.len));
try renderer.render(size, &[_]Cell{
.{
.content = ":",
},
});
size.anchor.col += 2;
size.cols -= 2;
}
if (this.value_len > 0) {
var start: usize = 0;
// TODO: moving the cursor position will change position of the '..' placement (i.e. at the beginning, at the end or both)
// truncate representation according to the available space
if (this.value_len >= size.cols - 1) {
start = this.value_len -| (size.cols - 3);
try renderer.render(size, &[_]Cell{
.{
.content = "..",
.style = .{ .dim = true },
},
});
size.anchor.col += 2;
size.cols -|= 2;
}
// print current value representation (and cursor position if active)
if (this.cursor_idx == 0 and this.value_len > 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[0..1],
.style = .{ .reverse = true },
},
.{
.content = this.value.items[1..this.value_len],
},
});
} else if (this.cursor_idx == 0 and this.value_len == 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[0..1],
.style = .{ .reverse = true },
},
});
} else if (this.cursor_idx == this.value_len) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[start..this.cursor_idx],
},
.{
.content = " ",
.style = .{ .reverse = true },
},
});
} else {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[start..this.cursor_idx],
},
.{
.content = this.value.items[this.cursor_idx .. this.cursor_idx + 1],
.style = .{ .reverse = true, .blink = true },
},
});
size.anchor.col += @as(u16, @truncate(this.cursor_idx)) + 1;
size.cols -= @as(u16, @truncate(this.cursor_idx)) + 1;
if (this.value_len > this.cursor_idx + 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[this.cursor_idx + 1 .. this.value_len],
},
});
}
}
} else {
if (this.placeholder) |placeholder| {
var placeholder_style: Cell.Style = .{
.fg = .default,
.dim = true,
};
if (this.active) {
placeholder_style.blink = true;
}
try renderer.render(size, &[_]Cell{
.{
.content = placeholder,
.style = placeholder_style,
},
});
}
}
}
};
}

View File

@@ -1,147 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Cell = terminal.Cell;
const Size = terminal.Size;
const log = std.log.scoped(.widget_list);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
const ListItems = std.ArrayList([]const Cell);
return struct {
allocator: std.mem.Allocator,
idx: usize,
config: ListType,
contents: ListItems,
size: terminal.Size,
require_render: bool,
const ListType = enum {
unordered,
ordered,
};
pub fn init(allocator: std.mem.Allocator, config: ListType, children: anytype) *@This() {
const ArgsType = @TypeOf(children);
const args_type_info = @typeInfo(ArgsType);
if (args_type_info != .@"struct") {
@compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType));
}
const fields_info = args_type_info.@"struct".fields;
var contents = ListItems.initCapacity(allocator, fields_info.len) catch @panic("List.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
const child_type_info = @typeInfo(ChildType);
if (child_type_info != .array and child_type_info != .pointer) {
@compileError("child: " ++ field.name ++ " is not an array of const Cell but " ++ @typeName(ChildType));
}
contents.append(child) catch {};
}
var this = allocator.create(@This()) catch @panic("List.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.idx = 0;
this.config = config;
this.contents = contents;
return this;
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
// store the received size
.resize => |size| {
this.size = size;
this.require_render = true;
},
.key => |key| {
var require_render = true;
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = terminal.Key.home })) {
// top
if (this.idx != 0) {
this.idx = 0;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = terminal.Key.end })) {
// bottom
if (this.idx < this.contents.items.len -| 1) {
this.idx = this.contents.items.len -| 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = terminal.Key.down })) {
// down
if (this.idx < this.contents.items.len -| 1) {
this.idx += 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = terminal.Key.up })) {
// up
if (this.idx > 0) {
this.idx -= 1;
} else {
require_render = false;
}
} else {
require_render = false;
}
this.require_render = require_render;
},
else => {},
}
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
var row: u16 = 0;
for (this.contents.items[this.idx..], this.idx + 1..) |content, num| {
var size: Size = .{
.anchor = .{
.col = this.size.anchor.col,
.row = this.size.anchor.row + row,
},
.rows = this.size.rows -| row,
.cols = this.size.cols,
};
switch (this.config) {
.unordered => {
try renderer.render(size, &[_]Cell{
.{ .content = "" },
});
size.anchor.col += 2;
size.cols -|= 2;
},
.ordered => {
var buf: [32]u8 = undefined;
const val = try std.fmt.bufPrint(&buf, "{d}.", .{num});
try renderer.render(size, &[_]Cell{
.{ .content = val },
});
const cols: u16 = @truncate(val.len + 1);
size.anchor.col += cols;
size.cols -|= cols;
},
}
try renderer.render(size, content);
row += 1; // NOTE: as there are no line breaks currently there will always exactly one line be written
}
this.require_render = false;
}
};
}

View File

@@ -1,127 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Style = terminal.Style;
const log = std.log.scoped(.widget_rawtext);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
const Contents = std.ArrayList(u8);
return struct {
allocator: std.mem.Allocator,
contents: Contents,
line_index: std.ArrayList(usize),
line: usize,
size: terminal.Size,
require_render: bool,
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) *@This() {
var contents = Contents.init(allocator);
var line_index = std.ArrayList(usize).init(allocator);
file.reader().readAllArrayList(&contents, std.math.maxInt(usize)) catch {};
line_index.append(0) catch {};
for (contents.items, 0..) |item, i| {
if (item == '\n') {
line_index.append(i + 1) catch {};
}
}
var this = allocator.create(@This()) catch @panic("RawText.zig: Failed to create.");
this.allocator = allocator;
this.line = 0;
this.require_render = true;
this.contents = contents;
this.line_index = line_index;
return this;
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.line_index.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
var require_render = true;
switch (event) {
// store the received size
.resize => |size| {
this.size = size;
if (this.line > this.line_index.items.len -| 1 -| size.rows) {
this.line = this.line_index.items.len -| 1 -| size.rows;
}
},
.key => |key| {
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = Key.home })) {
// top
if (this.line != 0) {
this.line = 0;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = Key.end })) {
// bottom
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
this.line = this.line_index.items.len -| 1 -| this.size.rows;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = Key.down })) {
// down
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
this.line += 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = Key.up })) {
// up
if (this.line > 0) {
this.line -= 1;
} else {
require_render = false;
}
} else {
require_render = false;
}
},
else => {
require_render = false;
},
}
this.require_render = require_render;
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
if (this.size.rows >= this.line_index.items.len) {
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items, .style = .{ .dim = true, .fg = .{ .index = 8 } } },
});
} else {
// more rows than we can display
const i = this.line_index.items[this.line];
const e = this.size.rows + this.line;
if (e > this.line_index.items.len) {
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items[i..], .style = .{ .dim = true, .fg = .{ .index = 7 } } },
});
return;
}
const x = this.line_index.items[e];
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items[i..x], .style = .{ .dim = true, .fg = .{ .index = 9 } } },
});
}
this.require_render = false;
}
};
}

View File

@@ -1,47 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const log = std.log.scoped(.widget_spacer);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
size_changed: bool,
pub fn init(allocator: std.mem.Allocator) *@This() {
var this = allocator.create(@This()) catch @panic("Space.zig: Failed to create.");
this.allocator = allocator;
this.size_changed = true;
return this;
}
pub fn deinit(this: *@This()) void {
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
.resize => |size| {
this.size = size;
this.size_changed = true;
},
else => {},
}
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.size_changed) {
try renderer.clear(this.size);
this.size_changed = false;
}
}
};
}

View File

@@ -1,155 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Cell = terminal.Cell;
const log = std.log.scoped(.widget_text);
pub fn Widget(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
}
return struct {
allocator: std.mem.Allocator,
alignment: Alignment,
contents: []const Cell,
size: terminal.Size,
require_render: bool,
const Alignment = enum {
default,
center,
top,
bottom,
left,
right,
};
pub fn init(allocator: std.mem.Allocator, alignment: Alignment, contents: []const Cell) *@This() {
var this = allocator.create(@This()) catch @panic("Text.zig: Failed to create");
this.allocator = allocator;
this.require_render = true;
this.alignment = alignment;
this.contents = contents;
return this;
}
pub fn deinit(this: *@This()) void {
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
// store the received size
.resize => |size| {
this.size = size;
this.require_render = true;
},
else => {},
}
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
// update size for aligned contents, default will not change size
const size: terminal.Size = blk: {
switch (this.alignment) {
.default => break :blk this.size,
.center => {
var length_usize: usize = 0;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2),
},
.rows = rows,
.cols = cols,
};
},
.top => {
var length_usize: usize = 0;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row,
},
.rows = rows,
.cols = cols,
};
},
.bottom => {
var length_usize: usize = 0;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row + this.size.rows - rows,
},
.rows = rows,
.cols = cols,
};
},
.left => {
var length_usize: usize = 0;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col,
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
},
.rows = rows,
.cols = cols,
};
},
.right => {
var length_usize: usize = 0;
for (this.contents) |content| {
length_usize += content.content.len;
}
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + this.size.cols - cols,
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
},
.rows = rows,
.cols = cols,
};
},
}
};
try renderer.render(size, this.contents);
this.require_render = false;
}
};
}

View File

@@ -1,16 +0,0 @@
// private imports
// public import / exports
pub const terminal = @import("terminal.zig");
pub const App = @import("app.zig").App;
pub const Renderer = @import("render.zig");
pub const Key = terminal.Key;
pub const Position = terminal.Position;
pub const Size = terminal.Size;
pub const Cell = terminal.Cell;
test {
_ = @import("terminal.zig");
_ = @import("queue.zig");
}