+4
-4
.github/workflows/docs.yml
+4
-4
.github/workflows/docs.yml
···
25
uses: actions/checkout@v3
26
- name: Setup Pages
27
uses: actions/configure-pages@v2
28
-
- uses: mlugg/setup-zig@v1
29
with:
30
-
version: 0.13.0
31
- run: zig build docs
32
- name: Upload artifact
33
-
uses: actions/upload-pages-artifact@v1
34
with:
35
path: "zig-out/docs"
36
- name: Deploy to GitHub Pages
37
id: deployment
38
-
uses: actions/deploy-pages@v1
···
25
uses: actions/checkout@v3
26
- name: Setup Pages
27
uses: actions/configure-pages@v2
28
+
- uses: mlugg/setup-zig@v2
29
with:
30
+
version: 0.15.1
31
- run: zig build docs
32
- name: Upload artifact
33
+
uses: actions/upload-pages-artifact@v3
34
with:
35
path: "zig-out/docs"
36
- name: Deploy to GitHub Pages
37
id: deployment
38
+
uses: actions/deploy-pages@v4
+29
.github/workflows/mirror.yml
+29
.github/workflows/mirror.yml
···
···
1
+
name: Mirror to tangled
2
+
3
+
on:
4
+
push:
5
+
branches:
6
+
- main
7
+
8
+
jobs:
9
+
mirror:
10
+
runs-on: ubuntu-latest
11
+
12
+
steps:
13
+
- name: Checkout source repo
14
+
uses: actions/checkout@v3
15
+
with:
16
+
fetch-depth: 0
17
+
18
+
- name: Set up SSH key
19
+
run: |
20
+
mkdir -p ~/.ssh
21
+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
22
+
chmod 600 ~/.ssh/id_ed25519
23
+
ssh-keyscan -H tangled.sh >> ~/.ssh/known_hosts
24
+
shell: bash
25
+
26
+
- name: Mirror
27
+
run: |
28
+
git remote add tangled git@tangled.sh:rockorager.dev/libvaxis
29
+
git push --mirror tangled
+4
-4
.github/workflows/test.yml
+4
-4
.github/workflows/test.yml
···
13
runs-on: ${{matrix.os}}
14
steps:
15
- uses: actions/checkout@v3
16
-
- uses: mlugg/setup-zig@v1
17
with:
18
-
version: 0.13.0
19
- run: zig build test
20
check-fmt:
21
runs-on: ubuntu-latest
22
steps:
23
- uses: actions/checkout@v3
24
-
- uses: mlugg/setup-zig@v1
25
with:
26
-
version: 0.13.0
27
- run: zig fmt --check .
···
13
runs-on: ${{matrix.os}}
14
steps:
15
- uses: actions/checkout@v3
16
+
- uses: mlugg/setup-zig@v2
17
with:
18
+
version: 0.15.1
19
- run: zig build test
20
check-fmt:
21
runs-on: ubuntu-latest
22
steps:
23
- uses: actions/checkout@v3
24
+
- uses: mlugg/setup-zig@v2
25
with:
26
+
version: 0.15.1
27
- run: zig fmt --check .
+2
.gitignore
+2
.gitignore
+248
-32
README.md
+248
-32
README.md
···
9
Libvaxis _does not use terminfo_. Support for vt features is detected through
10
terminal queries.
11
12
-
Contributions are welcome.
13
-
14
-
Vaxis uses zig `0.13.0`.
15
16
## Features
17
18
libvaxis supports all major platforms: macOS, Windows, Linux/BSD/and other
19
Unix-likes.
20
21
-
| Feature | libvaxis |
22
-
| ------------------------------ | :------: |
23
-
| RGB | โ
|
24
-
| Hyperlinks | โ
|
25
-
| Bracketed Paste | โ
|
26
-
| Kitty Keyboard | โ
|
27
-
| Styled Underlines | โ
|
28
-
| Mouse Shapes (OSC 22) | โ
|
29
-
| System Clipboard (OSC 52) | โ
|
30
-
| System Notifications (OSC 9) | โ
|
31
-
| System Notifications (OSC 777) | โ
|
32
-
| Synchronized Output (DEC 2026) | โ
|
33
-
| Unicode Core (DEC 2027) | โ
|
34
-
| Color Mode Updates (DEC 2031) | โ
|
35
-
| [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) | โ
|
36
-
| Images (kitty) | โ
|
37
38
## Usage
39
40
[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
41
42
-
[Starter repo](https://github.com/rockorager/libvaxis-starter)
43
44
Vaxis requires three basic primitives to operate:
45
···
52
use the event loop of their choice. The event loop is responsible for reading
53
the TTY, passing the read bytes to the vaxis parser, and handling events.
54
55
-
A core feature of Vaxis is it's ability to detect features via terminal queries
56
instead of relying on a terminfo database. This requires that the event loop
57
also handle these query responses and update the Vaxis.caps struct accordingly.
58
See the `Loop` implementation to see how this is done if writing your own event
59
loop.
60
61
-
## Example
62
-
63
```zig
64
const std = @import("std");
65
const vaxis = @import("vaxis");
···
89
const alloc = gpa.allocator();
90
91
// Initialize a tty
92
-
var tty = try vaxis.Tty.init();
93
defer tty.deinit();
94
95
// Initialize Vaxis
96
var vx = try vaxis.init(alloc, .{});
97
// deinit takes an optional allocator. If your program is exiting, you can
98
// choose to pass a null allocator to save some exit time.
99
-
defer vx.deinit(alloc, tty.anyWriter());
100
101
102
// The event loop requires an intrusive init. We create an instance with
···
116
defer loop.stop();
117
118
// Optionally enter the alternate screen
119
-
try vx.enterAltScreen(tty.anyWriter());
120
121
// We'll adjust the color index every keypress for the border
122
var color_idx: u8 = 0;
123
124
// init our text input widget. The text input widget needs an allocator to
125
// store the contents of the input
126
-
var text_input = TextInput.init(alloc, &vx.unicode);
127
defer text_input.deinit();
128
129
// Sends queries to terminal to detect certain features. This should always
130
// be called after entering the alt screen, if you are using the alt screen
131
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
132
133
while (true) {
134
// nextEvent blocks until an event is in the queue
···
164
// more than one byte will incur an allocation on the first render
165
// after it is drawn. Thereafter, it will not allocate unless the
166
// screen is resized
167
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
168
else => {},
169
}
170
···
187
const child = win.child(.{
188
.x_off = win.width / 2 - 20,
189
.y_off = win.height / 2 - 3,
190
-
.width = .{ .limit = 40 },
191
-
.height = .{ .limit = 3 },
192
.border = .{
193
.where = .all,
194
.style = style,
···
200
201
// Render the screen. Using a buffered writer will offer much better
202
// performance, but is not required
203
-
try vx.render(tty.anyWriter());
204
}
205
}
206
```
···
9
Libvaxis _does not use terminfo_. Support for vt features is detected through
10
terminal queries.
11
12
+
Vaxis uses zig `0.15.1`.
13
14
## Features
15
16
libvaxis supports all major platforms: macOS, Windows, Linux/BSD/and other
17
Unix-likes.
18
19
+
- RGB
20
+
- [Hyperlinks](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda) (OSC 8)
21
+
- Bracketed Paste
22
+
- [Kitty Keyboard Protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/)
23
+
- [Fancy underlines](https://sw.kovidgoyal.net/kitty/underlines/) (undercurl, etc)
24
+
- Mouse Shapes (OSC 22)
25
+
- System Clipboard (OSC 52)
26
+
- System Notifications (OSC 9)
27
+
- System Notifications (OSC 777)
28
+
- Synchronized Output (Mode 2026)
29
+
- [Unicode Core](https://github.com/contour-terminal/terminal-unicode-core) (Mode 2027)
30
+
- Color Mode Updates (Mode 2031)
31
+
- [In-Band Resize Reports](https://gist.github.com/rockorager/e695fb2924d36b2bcf1fff4a3704bd83) (Mode 2048)
32
+
- Images ([kitty graphics protocol](https://sw.kovidgoyal.net/kitty/graphics-protocol/))
33
+
- [Explicit Width](https://github.com/kovidgoyal/kitty/blob/master/docs/text-sizing-protocol.rst) (width modifiers only)
34
35
## Usage
36
37
[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
38
39
+
The library provides both a low level API suitable for making applications of
40
+
any sort as well as a higher level framework. The low level API is suitable for
41
+
making applications of any type, providing your own event loop, and gives you
42
+
full control over each cell on the screen.
43
+
44
+
The high level API, called `vxfw` (Vaxis framework), provides a Flutter-like
45
+
style of API. The framework provides an application runtime which handles the
46
+
event loop, focus management, mouse handling, and more. Several widgets are
47
+
provided, and custom widgets are easy to build. This API is most likely what you
48
+
want to use for typical TUI applications.
49
+
50
+
### Add libvaxis to your project
51
+
52
+
```console
53
+
zig fetch --save git+https://github.com/rockorager/libvaxis.git
54
+
```
55
+
Add this to your build.zig
56
+
57
+
```zig
58
+
const vaxis = b.dependency("vaxis", .{
59
+
.target = target,
60
+
.optimize = optimize,
61
+
});
62
+
63
+
exe.root_module.addImport("vaxis", vaxis.module("vaxis"));
64
+
```
65
+
66
+
or for ZLS support
67
+
68
+
```zig
69
+
// create module
70
+
const exe_mod = b.createModule(.{
71
+
.root_source_file = b.path("src/main.zig"),
72
+
.target = target,
73
+
.optimize = optimize,
74
+
});
75
+
76
+
// add vaxis dependency to module
77
+
const vaxis = b.dependency("vaxis", .{
78
+
.target = target,
79
+
.optimize = optimize,
80
+
});
81
+
exe_mod.addImport("vaxis", vaxis.module("vaxis"));
82
+
83
+
//create executable
84
+
const exe = b.addExecutable(.{
85
+
.name = "project_foo",
86
+
.root_module = exe_mod,
87
+
});
88
+
// install exe below
89
+
```
90
+
91
+
### vxfw (Vaxis framework)
92
+
93
+
Let's build a simple button counter application. This example can be run using
94
+
the command `zig build example -Dexample=counter`. The below application has
95
+
full mouse support: the button *and mouse shape* will change style on hover, on
96
+
click, and has enough logic to cancel a press if the release does not occur over
97
+
the button. Try it! Click the button, move the mouse off the button and release.
98
+
All of this logic is baked into the base `Button` widget.
99
+
100
+
```zig
101
+
const std = @import("std");
102
+
const vaxis = @import("vaxis");
103
+
const vxfw = vaxis.vxfw;
104
+
105
+
/// Our main application state
106
+
const Model = struct {
107
+
/// State of the counter
108
+
count: u32 = 0,
109
+
/// The button. This widget is stateful and must live between frames
110
+
button: vxfw.Button,
111
+
112
+
/// Helper function to return a vxfw.Widget struct
113
+
pub fn widget(self: *Model) vxfw.Widget {
114
+
return .{
115
+
.userdata = self,
116
+
.eventHandler = Model.typeErasedEventHandler,
117
+
.drawFn = Model.typeErasedDrawFn,
118
+
};
119
+
}
120
+
121
+
/// This function will be called from the vxfw runtime.
122
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
123
+
const self: *Model = @ptrCast(@alignCast(ptr));
124
+
switch (event) {
125
+
// The root widget is always sent an init event as the first event. Users of the
126
+
// library can also send this event to other widgets they create if they need to do
127
+
// some initialization.
128
+
.init => return ctx.requestFocus(self.button.widget()),
129
+
.key_press => |key| {
130
+
if (key.matches('c', .{ .ctrl = true })) {
131
+
ctx.quit = true;
132
+
return;
133
+
}
134
+
},
135
+
// We can request a specific widget gets focus. In this case, we always want to focus
136
+
// our button. Having focus means that key events will be sent up the widget tree to
137
+
// the focused widget, and then bubble back down the tree to the root. Users can tell
138
+
// the runtime the event was handled and the capture or bubble phase will stop
139
+
.focus_in => return ctx.requestFocus(self.button.widget()),
140
+
else => {},
141
+
}
142
+
}
143
+
144
+
/// This function is called from the vxfw runtime. It will be called on a regular interval, and
145
+
/// only when any event handler has marked the redraw flag in EventContext as true. By
146
+
/// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
147
+
/// which don't change state (ie mouse motion, unhandled key events, etc)
148
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
149
+
const self: *Model = @ptrCast(@alignCast(ptr));
150
+
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
151
+
// constraint. The minimum constraint will always be set, even if it is set to 0x0. The
152
+
// maximum constraint can have null width and/or height - meaning there is no constraint in
153
+
// that direction and the widget should take up as much space as it needs. By calling size()
154
+
// on the max, we assert that it has some constrained size. This is *always* the case for
155
+
// the root widget - the maximum size will always be the size of the terminal screen.
156
+
const max_size = ctx.max.size();
157
+
158
+
// The DrawContext also contains an arena allocator that can be used for each frame. The
159
+
// lifetime of this allocation is until the next time we draw a frame. This is useful for
160
+
// temporary allocations such as the one below: we have an integer we want to print as text.
161
+
// We can safely allocate this with the ctx arena since we only need it for this frame.
162
+
const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
163
+
const text: vxfw.Text = .{ .text = count_text };
164
+
165
+
// Each widget returns a Surface from its draw function. A Surface contains the rectangular
166
+
// area of the widget, as well as some information about the surface or widget: can we focus
167
+
// it? does it handle the mouse?
168
+
//
169
+
// It DOES NOT contain the location it should be within its parent. Only the parent can set
170
+
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
171
+
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
172
+
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
173
+
// child and place it within itself
174
+
const text_child: vxfw.SubSurface = .{
175
+
.origin = .{ .row = 0, .col = 0 },
176
+
.surface = try text.draw(ctx),
177
+
};
178
+
179
+
const button_child: vxfw.SubSurface = .{
180
+
.origin = .{ .row = 2, .col = 0 },
181
+
.surface = try self.button.draw(ctx.withConstraints(
182
+
ctx.min,
183
+
// Here we explicitly set a new maximum size constraint for the Button. A Button will
184
+
// expand to fill its area and must have some hard limit in the maximum constraint
185
+
.{ .width = 16, .height = 3 },
186
+
)),
187
+
};
188
+
189
+
// We also can use our arena to allocate the slice for our SubSurfaces. This slice only
190
+
// needs to live until the next frame, making this safe.
191
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
192
+
children[0] = text_child;
193
+
children[1] = button_child;
194
+
195
+
return .{
196
+
// A Surface must have a size. Our root widget is the size of the screen
197
+
.size = max_size,
198
+
.widget = self.widget(),
199
+
// We didn't actually need to draw anything for the root. In this case, we can set
200
+
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
201
+
// assert that its length is equal to the size.width * size.height.
202
+
.buffer = &.{},
203
+
.children = children,
204
+
};
205
+
}
206
+
207
+
/// The onClick callback for our button. This is also called if we press enter while the button
208
+
/// has focus
209
+
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
210
+
const ptr = maybe_ptr orelse return;
211
+
const self: *Model = @ptrCast(@alignCast(ptr));
212
+
self.count +|= 1;
213
+
return ctx.consumeAndRedraw();
214
+
}
215
+
};
216
+
217
+
pub fn main() !void {
218
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
219
+
defer _ = gpa.deinit();
220
+
221
+
const allocator = gpa.allocator();
222
+
223
+
var app = try vxfw.App.init(allocator);
224
+
defer app.deinit();
225
+
226
+
// We heap allocate our model because we will require a stable pointer to it in our Button
227
+
// widget
228
+
const model = try allocator.create(Model);
229
+
defer allocator.destroy(model);
230
+
231
+
// Set the initial state of our button
232
+
model.* = .{
233
+
.count = 0,
234
+
.button = .{
235
+
.label = "Click me!",
236
+
.onClick = Model.onClick,
237
+
.userdata = model,
238
+
},
239
+
};
240
+
241
+
try app.run(model.widget(), .{});
242
+
}
243
+
```
244
+
245
+
### Low level API
246
247
Vaxis requires three basic primitives to operate:
248
···
255
use the event loop of their choice. The event loop is responsible for reading
256
the TTY, passing the read bytes to the vaxis parser, and handling events.
257
258
+
A core feature of Vaxis is its ability to detect features via terminal queries
259
instead of relying on a terminfo database. This requires that the event loop
260
also handle these query responses and update the Vaxis.caps struct accordingly.
261
See the `Loop` implementation to see how this is done if writing your own event
262
loop.
263
264
```zig
265
const std = @import("std");
266
const vaxis = @import("vaxis");
···
290
const alloc = gpa.allocator();
291
292
// Initialize a tty
293
+
var buffer: [1024]u8 = undefined;
294
+
var tty = try vaxis.Tty.init(&buffer);
295
defer tty.deinit();
296
297
// Initialize Vaxis
298
var vx = try vaxis.init(alloc, .{});
299
// deinit takes an optional allocator. If your program is exiting, you can
300
// choose to pass a null allocator to save some exit time.
301
+
defer vx.deinit(alloc, tty.writer());
302
303
304
// The event loop requires an intrusive init. We create an instance with
···
318
defer loop.stop();
319
320
// Optionally enter the alternate screen
321
+
try vx.enterAltScreen(tty.writer());
322
323
// We'll adjust the color index every keypress for the border
324
var color_idx: u8 = 0;
325
326
// init our text input widget. The text input widget needs an allocator to
327
// store the contents of the input
328
+
var text_input = TextInput.init(alloc);
329
defer text_input.deinit();
330
331
// Sends queries to terminal to detect certain features. This should always
332
// be called after entering the alt screen, if you are using the alt screen
333
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
334
335
while (true) {
336
// nextEvent blocks until an event is in the queue
···
366
// more than one byte will incur an allocation on the first render
367
// after it is drawn. Thereafter, it will not allocate unless the
368
// screen is resized
369
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
370
else => {},
371
}
372
···
389
const child = win.child(.{
390
.x_off = win.width / 2 - 20,
391
.y_off = win.height / 2 - 3,
392
+
.width = 40 ,
393
+
.height = 3 ,
394
.border = .{
395
.where = .all,
396
.style = style,
···
402
403
// Render the screen. Using a buffered writer will offer much better
404
// performance, but is not required
405
+
try vx.render(tty.writer());
406
}
407
}
408
```
409
+
410
+
## Contributing
411
+
412
+
Contributions are welcome. Please submit a PR on Github,
413
+
[tangled](https://tangled.sh/@rockorager.dev/libvaxis), or a patch on the
414
+
[mailing list](mailto:~rockorager/libvaxis@lists.sr.ht)
415
+
416
+
## Community
417
+
418
+
We use [Github Discussions](https://github.com/rockorager/libvaxis/discussions)
419
+
as the primary location for community support, showcasing what you are working
420
+
on, and discussing library features and usage.
421
+
422
+
We also have an IRC channel on libera.chat: join us in #vaxis.
+502
USAGE.md
+502
USAGE.md
···
···
1
+
# Usage
2
+
3
+
## Custom Event Loops
4
+
5
+
Vaxis provides an abstract enough API to allow the usage of a custom event loop.
6
+
An event loop implementation is responsible for three primary tasks:
7
+
8
+
1. Read raw bytes from the TTY
9
+
2. Pass bytes to the Vaxis input event parser
10
+
3. Handle the returned events
11
+
12
+
Everything after this can be left up to user code, or brought into an event loop
13
+
to be a more abstract application layer. One important part of handling the
14
+
events is to update the Vaxis struct with discovered terminal capabilities. This
15
+
lets Vaxis know what features it can use. For example, the Kitty Keyboard
16
+
protocol, in-band-resize reports, and Unicode width measurements are just a few
17
+
examples.
18
+
19
+
### `libxev`
20
+
21
+
Below is an example [`libxev`](https://github.com/mitchellh/libxev) event loop.
22
+
Note that this code is not necessarily up-to-date with the latest `libxev`
23
+
release and is shown here merely as a proof of concept.
24
+
25
+
```zig
26
+
const std = @import("std");
27
+
const xev = @import("xev");
28
+
29
+
const Tty = @import("main.zig").Tty;
30
+
const Winsize = @import("main.zig").Winsize;
31
+
const Vaxis = @import("Vaxis.zig");
32
+
const Parser = @import("Parser.zig");
33
+
const Key = @import("Key.zig");
34
+
const Mouse = @import("Mouse.zig");
35
+
const Color = @import("Cell.zig").Color;
36
+
37
+
const log = std.log.scoped(.vaxis_xev);
38
+
39
+
pub const Event = union(enum) {
40
+
key_press: Key,
41
+
key_release: Key,
42
+
mouse: Mouse,
43
+
focus_in,
44
+
focus_out,
45
+
paste_start, // bracketed paste start
46
+
paste_end, // bracketed paste end
47
+
paste: []const u8, // osc 52 paste, caller must free
48
+
color_report: Color.Report, // osc 4, 10, 11, 12 response
49
+
color_scheme: Color.Scheme,
50
+
winsize: Winsize,
51
+
};
52
+
53
+
pub fn TtyWatcher(comptime Userdata: type) type {
54
+
return struct {
55
+
const Self = @This();
56
+
57
+
file: xev.File,
58
+
tty: *Tty,
59
+
60
+
read_buf: [4096]u8,
61
+
read_buf_start: usize,
62
+
read_cmp: xev.Completion,
63
+
64
+
winsize_wakeup: xev.Async,
65
+
winsize_cmp: xev.Completion,
66
+
67
+
callback: *const fn (
68
+
ud: ?*Userdata,
69
+
loop: *xev.Loop,
70
+
watcher: *Self,
71
+
event: Event,
72
+
) xev.CallbackAction,
73
+
74
+
ud: ?*Userdata,
75
+
vx: *Vaxis,
76
+
parser: Parser,
77
+
78
+
pub fn init(
79
+
self: *Self,
80
+
tty: *Tty,
81
+
vaxis: *Vaxis,
82
+
loop: *xev.Loop,
83
+
userdata: ?*Userdata,
84
+
callback: *const fn (
85
+
ud: ?*Userdata,
86
+
loop: *xev.Loop,
87
+
watcher: *Self,
88
+
event: Event,
89
+
) xev.CallbackAction,
90
+
) !void {
91
+
self.* = .{
92
+
.tty = tty,
93
+
.file = xev.File.initFd(tty.fd),
94
+
.read_buf = undefined,
95
+
.read_buf_start = 0,
96
+
.read_cmp = .{},
97
+
98
+
.winsize_wakeup = try xev.Async.init(),
99
+
.winsize_cmp = .{},
100
+
101
+
.callback = callback,
102
+
.ud = userdata,
103
+
.vx = vaxis,
104
+
.parser = .{ .grapheme_data = &vaxis.unicode.width_data.g_data },
105
+
};
106
+
107
+
self.file.read(
108
+
loop,
109
+
&self.read_cmp,
110
+
.{ .slice = &self.read_buf },
111
+
Self,
112
+
self,
113
+
Self.ttyReadCallback,
114
+
);
115
+
self.winsize_wakeup.wait(
116
+
loop,
117
+
&self.winsize_cmp,
118
+
Self,
119
+
self,
120
+
winsizeCallback,
121
+
);
122
+
const handler: Tty.SignalHandler = .{
123
+
.context = self,
124
+
.callback = Self.signalCallback,
125
+
};
126
+
try Tty.notifyWinsize(handler);
127
+
}
128
+
129
+
fn signalCallback(ptr: *anyopaque) void {
130
+
const self: *Self = @ptrCast(@alignCast(ptr));
131
+
self.winsize_wakeup.notify() catch |err| {
132
+
log.warn("couldn't wake up winsize callback: {}", .{err});
133
+
};
134
+
}
135
+
136
+
fn ttyReadCallback(
137
+
ud: ?*Self,
138
+
loop: *xev.Loop,
139
+
c: *xev.Completion,
140
+
_: xev.File,
141
+
buf: xev.ReadBuffer,
142
+
r: xev.ReadError!usize,
143
+
) xev.CallbackAction {
144
+
const n = r catch |err| {
145
+
log.err("read error: {}", .{err});
146
+
return .disarm;
147
+
};
148
+
const self = ud orelse unreachable;
149
+
150
+
// reset read start state
151
+
self.read_buf_start = 0;
152
+
153
+
var seq_start: usize = 0;
154
+
parse_loop: while (seq_start < n) {
155
+
const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
156
+
log.err("couldn't parse input: {}", .{err});
157
+
return .disarm;
158
+
};
159
+
if (result.n == 0) {
160
+
// copy the read to the beginning. We don't use memcpy because
161
+
// this could be overlapping, and it's also rare
162
+
const initial_start = seq_start;
163
+
while (seq_start < n) : (seq_start += 1) {
164
+
self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
165
+
}
166
+
self.read_buf_start = seq_start - initial_start + 1;
167
+
return .rearm;
168
+
}
169
+
seq_start += n;
170
+
const event_inner = result.event orelse {
171
+
log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
172
+
continue :parse_loop;
173
+
};
174
+
175
+
// Capture events we want to bubble up
176
+
const event: ?Event = switch (event_inner) {
177
+
.key_press => |key| .{ .key_press = key },
178
+
.key_release => |key| .{ .key_release = key },
179
+
.mouse => |mouse| .{ .mouse = mouse },
180
+
.focus_in => .focus_in,
181
+
.focus_out => .focus_out,
182
+
.paste_start => .paste_start,
183
+
.paste_end => .paste_end,
184
+
.paste => |paste| .{ .paste = paste },
185
+
.color_report => |report| .{ .color_report = report },
186
+
.color_scheme => |scheme| .{ .color_scheme = scheme },
187
+
.winsize => |ws| .{ .winsize = ws },
188
+
189
+
// capability events which we handle below
190
+
.cap_kitty_keyboard,
191
+
.cap_kitty_graphics,
192
+
.cap_rgb,
193
+
.cap_unicode,
194
+
.cap_sgr_pixels,
195
+
.cap_color_scheme_updates,
196
+
.cap_da1,
197
+
=> null, // handled below
198
+
};
199
+
200
+
if (event) |ev| {
201
+
const action = self.callback(self.ud, loop, self, ev);
202
+
switch (action) {
203
+
.disarm => return .disarm,
204
+
else => continue :parse_loop,
205
+
}
206
+
}
207
+
208
+
switch (event_inner) {
209
+
.key_press,
210
+
.key_release,
211
+
.mouse,
212
+
.focus_in,
213
+
.focus_out,
214
+
.paste_start,
215
+
.paste_end,
216
+
.paste,
217
+
.color_report,
218
+
.color_scheme,
219
+
.winsize,
220
+
=> unreachable, // handled above
221
+
222
+
.cap_kitty_keyboard => {
223
+
log.info("kitty keyboard capability detected", .{});
224
+
self.vx.caps.kitty_keyboard = true;
225
+
},
226
+
.cap_kitty_graphics => {
227
+
if (!self.vx.caps.kitty_graphics) {
228
+
log.info("kitty graphics capability detected", .{});
229
+
self.vx.caps.kitty_graphics = true;
230
+
}
231
+
},
232
+
.cap_rgb => {
233
+
log.info("rgb capability detected", .{});
234
+
self.vx.caps.rgb = true;
235
+
},
236
+
.cap_unicode => {
237
+
log.info("unicode capability detected", .{});
238
+
self.vx.caps.unicode = .unicode;
239
+
self.vx.screen.width_method = .unicode;
240
+
},
241
+
.cap_sgr_pixels => {
242
+
log.info("pixel mouse capability detected", .{});
243
+
self.vx.caps.sgr_pixels = true;
244
+
},
245
+
.cap_color_scheme_updates => {
246
+
log.info("color_scheme_updates capability detected", .{});
247
+
self.vx.caps.color_scheme_updates = true;
248
+
},
249
+
.cap_da1 => {
250
+
self.vx.enableDetectedFeatures(self.tty.writer()) catch |err| {
251
+
log.err("couldn't enable features: {}", .{err});
252
+
};
253
+
},
254
+
}
255
+
}
256
+
257
+
self.file.read(
258
+
loop,
259
+
c,
260
+
.{ .slice = &self.read_buf },
261
+
Self,
262
+
self,
263
+
Self.ttyReadCallback,
264
+
);
265
+
return .disarm;
266
+
}
267
+
268
+
fn winsizeCallback(
269
+
ud: ?*Self,
270
+
l: *xev.Loop,
271
+
c: *xev.Completion,
272
+
r: xev.Async.WaitError!void,
273
+
) xev.CallbackAction {
274
+
_ = r catch |err| {
275
+
log.err("async error: {}", .{err});
276
+
return .disarm;
277
+
};
278
+
const self = ud orelse unreachable; // no userdata
279
+
const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
280
+
log.err("couldn't get winsize: {}", .{err});
281
+
return .disarm;
282
+
};
283
+
const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
284
+
if (ret == .disarm) return .disarm;
285
+
286
+
self.winsize_wakeup.wait(
287
+
l,
288
+
c,
289
+
Self,
290
+
self,
291
+
winsizeCallback,
292
+
);
293
+
return .disarm;
294
+
}
295
+
};
296
+
}
297
+
```
298
+
299
+
### zig-aio
300
+
301
+
Below is an example [`zig-aio`](https://github.com/Cloudef/zig-aio) event loop.
302
+
Note that this code is not necessarily up-to-date with the latest `zig-aio`
303
+
release and is shown here merely as a proof of concept.
304
+
305
+
```zig
306
+
const builtin = @import("builtin");
307
+
const std = @import("std");
308
+
const vaxis = @import("vaxis");
309
+
const handleEventGeneric = vaxis.loop.handleEventGeneric;
310
+
const log = std.log.scoped(.vaxis_aio);
311
+
312
+
const Yield = enum { no_state, took_event };
313
+
314
+
/// zig-aio based event loop
315
+
/// <https://github.com/Cloudef/zig-aio>
316
+
pub fn LoopWithModules(T: type, aio: type, coro: type) type {
317
+
return struct {
318
+
const Event = T;
319
+
320
+
winsize_task: ?coro.Task.Generic2(winsizeTask) = null,
321
+
reader_task: ?coro.Task.Generic2(ttyReaderTask) = null,
322
+
queue: std.BoundedArray(T, 512) = .{},
323
+
source: aio.EventSource,
324
+
fatal: bool = false,
325
+
326
+
pub fn init() !@This() {
327
+
return .{ .source = try aio.EventSource.init() };
328
+
}
329
+
330
+
pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
331
+
vx.deviceStatusReport(tty.writer()) catch {};
332
+
if (self.winsize_task) |task| task.cancel();
333
+
if (self.reader_task) |task| task.cancel();
334
+
self.source.deinit();
335
+
self.* = undefined;
336
+
}
337
+
338
+
fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void {
339
+
const Context = struct {
340
+
loop: *@TypeOf(self.*),
341
+
tty: *vaxis.Tty,
342
+
winsize: ?vaxis.Winsize = null,
343
+
fn cb(ptr: *anyopaque) void {
344
+
std.debug.assert(coro.current() == null);
345
+
const ctx: *@This() = @ptrCast(@alignCast(ptr));
346
+
ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return;
347
+
ctx.loop.source.notify();
348
+
}
349
+
};
350
+
351
+
// keep on stack
352
+
var ctx: Context = .{ .loop = self, .tty = tty };
353
+
if (builtin.target.os.tag != .windows) {
354
+
if (@hasField(Event, "winsize")) {
355
+
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
356
+
try vaxis.Tty.notifyWinsize(handler);
357
+
}
358
+
}
359
+
360
+
while (true) {
361
+
try coro.io.single(aio.WaitEventSource{ .source = &self.source });
362
+
if (ctx.winsize) |winsize| {
363
+
if (!@hasField(Event, "winsize")) unreachable;
364
+
ctx.loop.postEvent(.{ .winsize = winsize }) catch {};
365
+
ctx.winsize = null;
366
+
}
367
+
}
368
+
}
369
+
370
+
fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void {
371
+
self.winsizeInner(tty) catch |err| {
372
+
if (err != error.Canceled) log.err("winsize: {}", .{err});
373
+
self.fatal = true;
374
+
};
375
+
}
376
+
377
+
fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event {
378
+
var state: vaxis.Tty.EventState = .{};
379
+
while (true) {
380
+
var bytes_read: usize = 0;
381
+
var input_record: vaxis.Tty.INPUT_RECORD = undefined;
382
+
try coro.io.single(aio.ReadTty{
383
+
.tty = .{ .handle = tty.stdin },
384
+
.buffer = std.mem.asBytes(&input_record),
385
+
.out_read = &bytes_read,
386
+
});
387
+
388
+
if (try tty.eventFromRecord(&input_record, &state)) |ev| {
389
+
return ev;
390
+
}
391
+
}
392
+
}
393
+
394
+
fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void {
395
+
var cache: vaxis.GraphemeCache = .{};
396
+
while (true) {
397
+
const event = try windowsReadEvent(tty);
398
+
try handleEventGeneric(self, vx, &cache, Event, event, null);
399
+
}
400
+
}
401
+
402
+
fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
403
+
// initialize a grapheme cache
404
+
var cache: vaxis.GraphemeCache = .{};
405
+
406
+
// get our initial winsize
407
+
const winsize = try vaxis.Tty.getWinsize(tty.fd);
408
+
if (@hasField(Event, "winsize")) {
409
+
try self.postEvent(.{ .winsize = winsize });
410
+
}
411
+
412
+
var parser: vaxis.Parser = .{
413
+
.grapheme_data = &vx.unicode.width_data.g_data,
414
+
};
415
+
416
+
const file: std.fs.File = .{ .handle = tty.fd };
417
+
while (true) {
418
+
var buf: [4096]u8 = undefined;
419
+
var n: usize = undefined;
420
+
var read_start: usize = 0;
421
+
try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n });
422
+
var seq_start: usize = 0;
423
+
while (seq_start < n) {
424
+
const result = try parser.parse(buf[seq_start..n], paste_allocator);
425
+
if (result.n == 0) {
426
+
// copy the read to the beginning. We don't use memcpy because
427
+
// this could be overlapping, and it's also rare
428
+
const initial_start = seq_start;
429
+
while (seq_start < n) : (seq_start += 1) {
430
+
buf[seq_start - initial_start] = buf[seq_start];
431
+
}
432
+
read_start = seq_start - initial_start + 1;
433
+
continue;
434
+
}
435
+
read_start = 0;
436
+
seq_start += result.n;
437
+
438
+
const event = result.event orelse continue;
439
+
try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
440
+
}
441
+
}
442
+
}
443
+
444
+
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
445
+
return switch (builtin.target.os.tag) {
446
+
.windows => self.ttyReaderWindows(vx, tty),
447
+
else => self.ttyReaderPosix(vx, tty, paste_allocator),
448
+
} catch |err| {
449
+
if (err != error.Canceled) log.err("ttyReader: {}", .{err});
450
+
self.fatal = true;
451
+
};
452
+
}
453
+
454
+
/// Spawns tasks to handle winsize signal and tty
455
+
pub fn spawn(
456
+
self: *@This(),
457
+
scheduler: *coro.Scheduler,
458
+
vx: *vaxis.Vaxis,
459
+
tty: *vaxis.Tty,
460
+
paste_allocator: ?std.mem.Allocator,
461
+
spawn_options: coro.Scheduler.SpawnOptions,
462
+
) coro.Scheduler.SpawnError!void {
463
+
if (self.reader_task) |_| unreachable; // programming error
464
+
// This is required even if app doesn't care about winsize
465
+
// It is because it consumes the EventSource, so it can wakeup the scheduler
466
+
// Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update
467
+
self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options);
468
+
self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options);
469
+
}
470
+
471
+
pub const PopEventError = error{TtyCommunicationSevered};
472
+
473
+
/// Call this in a while loop in the main event handler until it returns null
474
+
pub fn popEvent(self: *@This()) PopEventError!?T {
475
+
if (self.fatal) return error.TtyCommunicationSevered;
476
+
defer self.winsize_task.?.wakeupIf(Yield.took_event);
477
+
defer self.reader_task.?.wakeupIf(Yield.took_event);
478
+
return self.queue.popOrNull();
479
+
}
480
+
481
+
pub const PostEventError = error{Overflow};
482
+
483
+
pub fn postEvent(self: *@This(), event: T) !void {
484
+
if (coro.current()) |_| {
485
+
while (true) {
486
+
self.queue.insert(0, event) catch {
487
+
// wait for the app to take event
488
+
try coro.yield(Yield.took_event);
489
+
continue;
490
+
};
491
+
break;
492
+
}
493
+
} else {
494
+
// queue can be full, app could handle this error by spinning the scheduler
495
+
try self.queue.insert(0, event);
496
+
}
497
+
// wakes up the scheduler, so custom events update UI
498
+
self.source.notify();
499
+
}
500
+
};
501
+
}
502
+
```
+62
bench/bench.zig
+62
bench/bench.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
4
+
fn parseIterations(allocator: std.mem.Allocator) !usize {
5
+
var args = try std.process.argsWithAllocator(allocator);
6
+
defer args.deinit();
7
+
_ = args.next();
8
+
if (args.next()) |val| {
9
+
return std.fmt.parseUnsigned(usize, val, 10);
10
+
}
11
+
return 200;
12
+
}
13
+
14
+
fn printResults(writer: anytype, label: []const u8, iterations: usize, elapsed_ns: u64, total_bytes: usize) !void {
15
+
const ns_per_frame = elapsed_ns / @as(u64, @intCast(iterations));
16
+
const bytes_per_frame = total_bytes / iterations;
17
+
try writer.print(
18
+
"{s}: frames={d} total_ns={d} ns/frame={d} bytes={d} bytes/frame={d}\n",
19
+
.{ label, iterations, elapsed_ns, ns_per_frame, total_bytes, bytes_per_frame },
20
+
);
21
+
}
22
+
23
+
pub fn main() !void {
24
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
25
+
defer _ = gpa.deinit();
26
+
const allocator = gpa.allocator();
27
+
28
+
const iterations = try parseIterations(allocator);
29
+
30
+
var vx = try vaxis.init(allocator, .{});
31
+
var init_writer = std.io.Writer.Allocating.init(allocator);
32
+
defer init_writer.deinit();
33
+
defer vx.deinit(allocator, &init_writer.writer);
34
+
35
+
const winsize = vaxis.Winsize{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 };
36
+
try vx.resize(allocator, &init_writer.writer, winsize);
37
+
38
+
const stdout = std.fs.File.stdout().deprecatedWriter();
39
+
40
+
var idle_writer = std.io.Writer.Allocating.init(allocator);
41
+
defer idle_writer.deinit();
42
+
var timer = try std.time.Timer.start();
43
+
var i: usize = 0;
44
+
while (i < iterations) : (i += 1) {
45
+
try vx.render(&idle_writer.writer);
46
+
}
47
+
const idle_ns = timer.read();
48
+
const idle_bytes: usize = idle_writer.writer.end;
49
+
try printResults(stdout, "idle", iterations, idle_ns, idle_bytes);
50
+
51
+
var dirty_writer = std.io.Writer.Allocating.init(allocator);
52
+
defer dirty_writer.deinit();
53
+
timer.reset();
54
+
i = 0;
55
+
while (i < iterations) : (i += 1) {
56
+
vx.queueRefresh();
57
+
try vx.render(&dirty_writer.writer);
58
+
}
59
+
const dirty_ns = timer.read();
60
+
const dirty_bytes: usize = dirty_writer.writer.end;
61
+
try printResults(stdout, "dirty", iterations, dirty_ns, dirty_bytes);
62
+
}
+61
-73
build.zig
+61
-73
build.zig
···
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
4
-
const include_libxev = b.option(bool, "libxev", "Enable support for libxev library (default: true)") orelse true;
5
-
const include_images = b.option(bool, "images", "Enable support for images (default: true)") orelse true;
6
-
const include_text_input = b.option(bool, "text_input", "Enable support for the TextInput widget (default: true)") orelse true;
7
-
const include_aio = b.option(bool, "aio", "Enable support for zig-aio library (default: false)") orelse false;
8
-
9
-
const options = b.addOptions();
10
-
options.addOption(bool, "libxev", include_libxev);
11
-
options.addOption(bool, "images", include_images);
12
-
options.addOption(bool, "text_input", include_text_input);
13
-
options.addOption(bool, "aio", include_aio);
14
-
15
-
const options_mod = options.createModule();
16
-
17
const target = b.standardTargetOptions(.{});
18
const optimize = b.standardOptimizeOption(.{});
19
const root_source_file = b.path("src/main.zig");
20
21
// Dependencies
22
-
const zg_dep = b.dependency("zg", .{
23
.optimize = optimize,
24
.target = target,
25
});
26
-
const zigimg_dep = if (include_images) b.lazyDependency("zigimg", .{
27
-
.optimize = optimize,
28
.target = target,
29
-
}) else null;
30
-
const gap_buffer_dep = if (include_text_input) b.lazyDependency("gap_buffer", .{
31
.optimize = optimize,
32
-
.target = target,
33
-
}) else null;
34
-
const xev_dep = if (include_libxev) b.lazyDependency("libxev", .{
35
-
.optimize = optimize,
36
-
.target = target,
37
-
}) else null;
38
-
const aio_dep = if (include_aio) b.lazyDependency("aio", .{
39
-
.optimize = optimize,
40
-
.target = target,
41
-
}) else null;
42
43
// Module
44
const vaxis_mod = b.addModule("vaxis", .{
···
46
.target = target,
47
.optimize = optimize,
48
});
49
-
vaxis_mod.addImport("code_point", zg_dep.module("code_point"));
50
-
vaxis_mod.addImport("grapheme", zg_dep.module("grapheme"));
51
-
vaxis_mod.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
52
-
if (zigimg_dep) |dep| vaxis_mod.addImport("zigimg", dep.module("zigimg"));
53
-
if (gap_buffer_dep) |dep| vaxis_mod.addImport("gap_buffer", dep.module("gap_buffer"));
54
-
if (xev_dep) |dep| vaxis_mod.addImport("xev", dep.module("xev"));
55
-
if (aio_dep) |dep| vaxis_mod.addImport("aio", dep.module("aio"));
56
-
if (aio_dep) |dep| vaxis_mod.addImport("coro", dep.module("coro"));
57
-
vaxis_mod.addImport("build_options", options_mod);
58
59
// Examples
60
const Example = enum {
61
cli,
62
image,
63
main,
64
-
nvim,
65
table,
66
text_input,
67
vaxis,
68
vt,
69
-
xev,
70
-
aio,
71
};
72
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
73
const example_step = b.step("example", "Run example");
74
const example = b.addExecutable(.{
75
.name = "example",
76
-
// future versions should use b.path, see zig PR #19597
77
-
.root_source_file = b.path(
78
-
b.fmt("examples/{s}.zig", .{@tagName(example_option)}),
79
-
),
80
-
.target = target,
81
-
.optimize = optimize,
82
});
83
-
example.root_module.addImport("vaxis", vaxis_mod);
84
-
if (xev_dep) |dep| example.root_module.addImport("xev", dep.module("xev"));
85
-
if (aio_dep) |dep| example.root_module.addImport("aio", dep.module("aio"));
86
-
if (aio_dep) |dep| example.root_module.addImport("coro", dep.module("coro"));
87
88
const example_run = b.addRunArtifact(example);
89
example_step.dependOn(&example_run.step);
90
91
// Tests
92
const tests_step = b.step("test", "Run tests");
93
94
const tests = b.addTest(.{
95
-
.root_source_file = b.path("src/main.zig"),
96
-
.target = target,
97
-
.optimize = optimize,
98
});
99
-
tests.root_module.addImport("code_point", zg_dep.module("code_point"));
100
-
tests.root_module.addImport("grapheme", zg_dep.module("grapheme"));
101
-
tests.root_module.addImport("DisplayWidth", zg_dep.module("DisplayWidth"));
102
-
if (zigimg_dep) |dep| tests.root_module.addImport("zigimg", dep.module("zigimg"));
103
-
if (gap_buffer_dep) |dep| tests.root_module.addImport("gap_buffer", dep.module("gap_buffer"));
104
-
tests.root_module.addImport("build_options", options_mod);
105
106
const tests_run = b.addRunArtifact(tests);
107
b.installArtifact(tests);
108
tests_step.dependOn(&tests_run.step);
109
110
-
// Lints
111
-
const lints_step = b.step("lint", "Run lints");
112
-
113
-
const lints = b.addFmt(.{
114
-
.paths = &.{ "src", "build.zig" },
115
-
.check = true,
116
-
});
117
-
118
-
lints_step.dependOn(&lints.step);
119
-
b.default_step.dependOn(lints_step);
120
-
121
// Docs
122
const docs_step = b.step("docs", "Build the vaxis library docs");
123
const docs_obj = b.addObject(.{
124
.name = "vaxis",
125
-
.root_source_file = root_source_file,
126
-
.target = target,
127
-
.optimize = optimize,
128
});
129
const docs = docs_obj.getEmittedDocs();
130
docs_step.dependOn(&b.addInstallDirectory(.{
···
1
const std = @import("std");
2
3
pub fn build(b: *std.Build) void {
4
const target = b.standardTargetOptions(.{});
5
const optimize = b.standardOptimizeOption(.{});
6
const root_source_file = b.path("src/main.zig");
7
8
// Dependencies
9
+
const zigimg_dep = b.dependency("zigimg", .{
10
.optimize = optimize,
11
.target = target,
12
});
13
+
const uucode_dep = b.dependency("uucode", .{
14
.target = target,
15
.optimize = optimize,
16
+
.fields = @as([]const []const u8, &.{
17
+
"east_asian_width",
18
+
"grapheme_break",
19
+
"general_category",
20
+
"is_emoji_presentation",
21
+
}),
22
+
});
23
24
// Module
25
const vaxis_mod = b.addModule("vaxis", .{
···
27
.target = target,
28
.optimize = optimize,
29
});
30
+
vaxis_mod.addImport("zigimg", zigimg_dep.module("zigimg"));
31
+
vaxis_mod.addImport("uucode", uucode_dep.module("uucode"));
32
33
// Examples
34
const Example = enum {
35
cli,
36
+
counter,
37
+
fuzzy,
38
image,
39
main,
40
+
scroll,
41
+
split_view,
42
table,
43
text_input,
44
+
text_view,
45
+
list_view,
46
vaxis,
47
+
view,
48
vt,
49
};
50
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
51
const example_step = b.step("example", "Run example");
52
const example = b.addExecutable(.{
53
.name = "example",
54
+
.root_module = b.createModule(.{
55
+
.root_source_file = b.path(
56
+
b.fmt("examples/{s}.zig", .{@tagName(example_option)}),
57
+
),
58
+
.target = target,
59
+
.optimize = optimize,
60
+
.imports = &.{
61
+
.{ .name = "vaxis", .module = vaxis_mod },
62
+
},
63
+
}),
64
});
65
66
const example_run = b.addRunArtifact(example);
67
example_step.dependOn(&example_run.step);
68
69
+
// Benchmarks
70
+
const bench_step = b.step("bench", "Run benchmarks");
71
+
const bench = b.addExecutable(.{
72
+
.name = "bench",
73
+
.root_module = b.createModule(.{
74
+
.root_source_file = b.path("bench/bench.zig"),
75
+
.target = target,
76
+
.optimize = optimize,
77
+
.imports = &.{
78
+
.{ .name = "vaxis", .module = vaxis_mod },
79
+
},
80
+
}),
81
+
});
82
+
const bench_run = b.addRunArtifact(bench);
83
+
if (b.args) |args| {
84
+
bench_run.addArgs(args);
85
+
}
86
+
bench_step.dependOn(&bench_run.step);
87
+
88
// Tests
89
const tests_step = b.step("test", "Run tests");
90
91
const tests = b.addTest(.{
92
+
.root_module = b.createModule(.{
93
+
.root_source_file = b.path("src/main.zig"),
94
+
.target = target,
95
+
.optimize = optimize,
96
+
.imports = &.{
97
+
.{ .name = "zigimg", .module = zigimg_dep.module("zigimg") },
98
+
.{ .name = "uucode", .module = uucode_dep.module("uucode") },
99
+
},
100
+
}),
101
});
102
103
const tests_run = b.addRunArtifact(tests);
104
b.installArtifact(tests);
105
tests_step.dependOn(&tests_run.step);
106
107
// Docs
108
const docs_step = b.step("docs", "Build the vaxis library docs");
109
const docs_obj = b.addObject(.{
110
.name = "vaxis",
111
+
.root_module = b.createModule(.{
112
+
.root_source_file = root_source_file,
113
+
.target = target,
114
+
.optimize = optimize,
115
+
}),
116
});
117
const docs = docs_obj.getEmittedDocs();
118
docs_step.dependOn(&b.addInstallDirectory(.{
+9
-23
build.zig.zon
+9
-23
build.zig.zon
···
1
.{
2
-
.name = "vaxis",
3
-
.version = "0.1.0",
4
-
.minimum_zig_version = "0.13.0",
5
.dependencies = .{
6
.zigimg = .{
7
-
.url = "git+https://github.com/zigimg/zigimg#3a667bdb3d7f0955a5a51c8468eac83210c1439e",
8
-
.hash = "1220dd654ef941fc76fd96f9ec6adadf83f69b9887a0d3f4ee5ac0a1a3e11be35cf5",
9
-
.lazy = true,
10
-
},
11
-
.gap_buffer = .{
12
-
.url = "git+https://github.com/ryleelyman/GapBuffer.zig#9039708e09fc3eb5f698ab5694a436afe503c6a6",
13
-
.hash = "1220f525973ae804ec0284556bfc47db7b6a8dc86464a853956ef859d6e0fb5fa93b",
14
-
},
15
-
.zg = .{
16
-
.url = "git+https://codeberg.org/dude_the_builder/zg?ref=master#689ab6b83d08c02724b99d199d650ff731250998",
17
-
.hash = "12200d1ce5f9733a9437415d85665ad5fbc85a4d27689fd337fecad8014acffe3aa5",
18
-
},
19
-
.libxev = .{
20
-
.url = "git+https://github.com/mitchellh/libxev#f6a672a78436d8efee1aa847a43a900ad773618b",
21
-
.hash = "12207b7a5b538ffb7fb18f954ae17d2f8490b6e3778a9e30564ad82c58ee8da52361",
22
-
.lazy = true,
23
},
24
-
.aio = .{
25
-
.url = "git+https://github.com/Cloudef/zig-aio#b5a407344379508466c5dcbe4c74438a6166e2ca",
26
-
.hash = "1220a55aedabdd10578d0c514719ea39ae1bc6d7ed990f508dc100db7f0ccf391437",
27
-
.lazy = true,
28
},
29
},
30
.paths = .{
···
1
.{
2
+
.name = .vaxis,
3
+
.fingerprint = 0x14fbbb94fc556305,
4
+
.version = "0.5.1",
5
+
.minimum_zig_version = "0.15.1",
6
.dependencies = .{
7
.zigimg = .{
8
+
.url = "git+https://github.com/zigimg/zigimg#eab2522c023b9259db8b13f2f90d609b7437e5f6",
9
+
.hash = "zigimg-0.1.0-8_eo2vUZFgAAtN1c6dAO5DdqL0d4cEWHtn6iR5ucZJti",
10
},
11
+
.uucode = .{
12
+
.url = "git+https://github.com/jacobsandlund/uucode#5f05f8f83a75caea201f12cc8ea32a2d82ea9732",
13
+
.hash = "uucode-0.1.0-ZZjBPj96QADXyt5sqwBJUnhaDYs_qBeeKijZvlRa0eqM",
14
},
15
},
16
.paths = .{
-172
examples/aio.zig
-172
examples/aio.zig
···
1
-
const builtin = @import("builtin");
2
-
const std = @import("std");
3
-
const vaxis = @import("vaxis");
4
-
const aio = @import("aio");
5
-
const coro = @import("coro");
6
-
7
-
pub const panic = vaxis.panic_handler;
8
-
9
-
const Event = union(enum) {
10
-
key_press: vaxis.Key,
11
-
winsize: vaxis.Winsize,
12
-
};
13
-
14
-
const Loop = vaxis.aio.Loop(Event);
15
-
16
-
const Video = enum { no_state, ready, end };
17
-
const Audio = enum { no_state, ready, end };
18
-
19
-
fn downloadTask(allocator: std.mem.Allocator, url: []const u8) ![]const u8 {
20
-
var client: std.http.Client = .{ .allocator = allocator };
21
-
defer client.deinit();
22
-
var body = std.ArrayList(u8).init(allocator);
23
-
_ = try client.fetch(.{
24
-
.location = .{ .url = url },
25
-
.response_storage = .{ .dynamic = &body },
26
-
.max_append_size = 1.6e+7,
27
-
});
28
-
return try body.toOwnedSlice();
29
-
}
30
-
31
-
fn audioTask(allocator: std.mem.Allocator) !void {
32
-
// signals end of audio in case there's a error
33
-
errdefer coro.yield(Audio.end) catch {};
34
-
35
-
// var child = std.process.Child.init(&.{ "aplay", "-Dplug:default", "-q", "-f", "S16_LE", "-r", "8000" }, allocator);
36
-
var child = std.process.Child.init(&.{ "mpv", "--audio-samplerate=16000", "--audio-channels=mono", "--audio-format=s16", "-" }, allocator);
37
-
child.stdin_behavior = .Pipe;
38
-
child.stdout_behavior = .Ignore;
39
-
child.stderr_behavior = .Ignore;
40
-
try child.spawn();
41
-
defer _ = child.kill() catch {};
42
-
43
-
const sound = blk: {
44
-
var tpool = try coro.ThreadPool.init(allocator, .{});
45
-
defer tpool.deinit();
46
-
break :blk try tpool.yieldForCompletition(downloadTask, .{ allocator, "https://keroserene.net/lol/roll.s16" }, .{});
47
-
};
48
-
defer allocator.free(sound);
49
-
50
-
try coro.yield(Audio.ready);
51
-
52
-
var audio_off: usize = 0;
53
-
while (audio_off < sound.len) {
54
-
var written: usize = 0;
55
-
try coro.io.single(aio.Write{ .file = child.stdin.?, .buffer = sound[audio_off..], .out_written = &written });
56
-
audio_off += written;
57
-
}
58
-
59
-
// the audio is already fed to the player and the defer
60
-
// would kill the child, so stay here chilling
61
-
coro.yield(Audio.end) catch {};
62
-
}
63
-
64
-
fn videoTask(writer: std.io.AnyWriter) !void {
65
-
// signals end of video
66
-
defer coro.yield(Video.end) catch {};
67
-
68
-
var socket: std.posix.socket_t = undefined;
69
-
try coro.io.single(aio.Socket{
70
-
.domain = std.posix.AF.INET,
71
-
.flags = std.posix.SOCK.STREAM | std.posix.SOCK.CLOEXEC,
72
-
.protocol = std.posix.IPPROTO.TCP,
73
-
.out_socket = &socket,
74
-
});
75
-
defer std.posix.close(socket);
76
-
77
-
const address = std.net.Address.initIp4(.{ 44, 224, 41, 160 }, 1987);
78
-
try coro.io.single(aio.Connect{
79
-
.socket = socket,
80
-
.addr = &address.any,
81
-
.addrlen = address.getOsSockLen(),
82
-
});
83
-
84
-
try coro.yield(Video.ready);
85
-
86
-
var buf: [1024]u8 = undefined;
87
-
while (true) {
88
-
var read: usize = 0;
89
-
try coro.io.single(aio.Recv{ .socket = socket, .buffer = &buf, .out_read = &read });
90
-
if (read == 0) break;
91
-
_ = try writer.write(buf[0..read]);
92
-
}
93
-
}
94
-
95
-
fn loadingTask(vx: *vaxis.Vaxis, writer: std.io.AnyWriter) !void {
96
-
var color_idx: u8 = 30;
97
-
var dir: enum { up, down } = .up;
98
-
99
-
while (true) {
100
-
try coro.io.single(aio.Timeout{ .ns = 8 * std.time.ns_per_ms });
101
-
102
-
const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ color_idx, color_idx, color_idx } } };
103
-
const segment: vaxis.Segment = .{ .text = vaxis.logo, .style = style };
104
-
105
-
const win = vx.window();
106
-
win.clear();
107
-
108
-
var loc = vaxis.widgets.alignment.center(win, 28, 4);
109
-
_ = try loc.printSegment(segment, .{ .wrap = .grapheme });
110
-
111
-
switch (dir) {
112
-
.up => {
113
-
color_idx += 1;
114
-
if (color_idx == 255) dir = .down;
115
-
},
116
-
.down => {
117
-
color_idx -= 1;
118
-
if (color_idx == 30) dir = .up;
119
-
},
120
-
}
121
-
122
-
try vx.render(writer);
123
-
}
124
-
}
125
-
126
-
pub fn main() !void {
127
-
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
128
-
defer _ = gpa.deinit();
129
-
const allocator = gpa.allocator();
130
-
131
-
var tty = try vaxis.Tty.init();
132
-
defer tty.deinit();
133
-
134
-
var vx = try vaxis.init(allocator, .{});
135
-
defer vx.deinit(allocator, tty.anyWriter());
136
-
137
-
var scheduler = try coro.Scheduler.init(allocator, .{});
138
-
defer scheduler.deinit();
139
-
140
-
var loop = try Loop.init();
141
-
try loop.spawn(&scheduler, &vx, &tty, null, .{});
142
-
defer loop.deinit(&vx, &tty);
143
-
144
-
try vx.enterAltScreen(tty.anyWriter());
145
-
try vx.queryTerminalSend(tty.anyWriter());
146
-
147
-
var buffered_tty_writer = tty.bufferedWriter();
148
-
const loading = try scheduler.spawn(loadingTask, .{ &vx, buffered_tty_writer.writer().any() }, .{});
149
-
const audio = try scheduler.spawn(audioTask, .{allocator}, .{});
150
-
const video = try scheduler.spawn(videoTask, .{buffered_tty_writer.writer().any()}, .{});
151
-
152
-
main: while (try scheduler.tick(.blocking) > 0) {
153
-
while (try loop.popEvent()) |event| switch (event) {
154
-
.key_press => |key| {
155
-
if (key.matches('c', .{ .ctrl = true })) {
156
-
break :main;
157
-
}
158
-
},
159
-
.winsize => |ws| try vx.resize(allocator, buffered_tty_writer.writer().any(), ws),
160
-
};
161
-
162
-
if (audio.state(Video) == .ready and video.state(Audio) == .ready) {
163
-
loading.cancel();
164
-
audio.wakeup();
165
-
video.wakeup();
166
-
} else if (audio.state(Audio) == .end and video.state(Video) == .end) {
167
-
break :main;
168
-
}
169
-
170
-
try buffered_tty_writer.flush();
171
-
}
172
-
}
···
+9
-8
examples/cli.zig
+9
-8
examples/cli.zig
···
14
}
15
const alloc = gpa.allocator();
16
17
-
var tty = try vaxis.Tty.init();
18
defer tty.deinit();
19
20
var vx = try vaxis.init(alloc, .{});
21
-
defer vx.deinit(alloc, tty.anyWriter());
22
23
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
24
try loop.init();
···
26
try loop.start();
27
defer loop.stop();
28
29
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
30
31
-
var text_input = TextInput.init(alloc, &vx.unicode);
32
defer text_input.deinit();
33
34
var selected_option: ?usize = null;
···
62
} else {
63
selected_option.? = selected_option.? -| 1;
64
}
65
-
} else if (key.matches(vaxis.Key.enter, .{})) {
66
if (selected_option) |i| {
67
log.err("enter", .{});
68
try text_input.insertSliceAtCursor(options[i]);
···
74
}
75
},
76
.winsize => |ws| {
77
-
try vx.resize(alloc, tty.anyWriter(), ws);
78
},
79
else => {},
80
}
···
92
.text = opt,
93
.style = if (j == i) .{ .reverse = true } else .{},
94
}};
95
-
_ = try win.print(&seg, .{ .row_offset = j + 1 });
96
}
97
}
98
-
try vx.render(tty.anyWriter());
99
}
100
}
101
···
14
}
15
const alloc = gpa.allocator();
16
17
+
var buffer: [1024]u8 = undefined;
18
+
var tty = try vaxis.Tty.init(&buffer);
19
defer tty.deinit();
20
21
var vx = try vaxis.init(alloc, .{});
22
+
defer vx.deinit(alloc, tty.writer());
23
24
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
25
try loop.init();
···
27
try loop.start();
28
defer loop.stop();
29
30
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
31
32
+
var text_input = TextInput.init(alloc);
33
defer text_input.deinit();
34
35
var selected_option: ?usize = null;
···
63
} else {
64
selected_option.? = selected_option.? -| 1;
65
}
66
+
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
67
if (selected_option) |i| {
68
log.err("enter", .{});
69
try text_input.insertSliceAtCursor(options[i]);
···
75
}
76
},
77
.winsize => |ws| {
78
+
try vx.resize(alloc, tty.writer(), ws);
79
},
80
else => {},
81
}
···
93
.text = opt,
94
.style = if (j == i) .{ .reverse = true } else .{},
95
}};
96
+
_ = win.print(&seg, .{ .row_offset = @intCast(j + 1) });
97
}
98
}
99
+
try vx.render(tty.writer());
100
}
101
}
102
+139
examples/counter.zig
+139
examples/counter.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
/// Our main application state
6
+
const Model = struct {
7
+
/// State of the counter
8
+
count: u32 = 0,
9
+
/// The button. This widget is stateful and must live between frames
10
+
button: vxfw.Button,
11
+
12
+
/// Helper function to return a vxfw.Widget struct
13
+
pub fn widget(self: *Model) vxfw.Widget {
14
+
return .{
15
+
.userdata = self,
16
+
.eventHandler = Model.typeErasedEventHandler,
17
+
.drawFn = Model.typeErasedDrawFn,
18
+
};
19
+
}
20
+
21
+
/// This function will be called from the vxfw runtime.
22
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
23
+
const self: *Model = @ptrCast(@alignCast(ptr));
24
+
switch (event) {
25
+
// The root widget is always sent an init event as the first event. Users of the
26
+
// library can also send this event to other widgets they create if they need to do
27
+
// some initialization.
28
+
.init => return ctx.requestFocus(self.button.widget()),
29
+
.key_press => |key| {
30
+
if (key.matches('c', .{ .ctrl = true })) {
31
+
ctx.quit = true;
32
+
return;
33
+
}
34
+
},
35
+
// We can request a specific widget gets focus. In this case, we always want to focus
36
+
// our button. Having focus means that key events will be sent up the widget tree to
37
+
// the focused widget, and then bubble back down the tree to the root. Users can tell
38
+
// the runtime the event was handled and the capture or bubble phase will stop
39
+
.focus_in => return ctx.requestFocus(self.button.widget()),
40
+
else => {},
41
+
}
42
+
}
43
+
44
+
/// This function is called from the vxfw runtime. It will be called on a regular interval, and
45
+
/// only when any event handler has marked the redraw flag in EventContext as true. By
46
+
/// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
47
+
/// which don't change state (ie mouse motion, unhandled key events, etc)
48
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
49
+
const self: *Model = @ptrCast(@alignCast(ptr));
50
+
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
51
+
// constraint. The minimum constraint will always be set, even if it is set to 0x0. The
52
+
// maximum constraint can have null width and/or height - meaning there is no constraint in
53
+
// that direction and the widget should take up as much space as it needs. By calling size()
54
+
// on the max, we assert that it has some constrained size. This is *always* the case for
55
+
// the root widget - the maximum size will always be the size of the terminal screen.
56
+
const max_size = ctx.max.size();
57
+
58
+
// The DrawContext also contains an arena allocator that can be used for each frame. The
59
+
// lifetime of this allocation is until the next time we draw a frame. This is useful for
60
+
// temporary allocations such as the one below: we have an integer we want to print as text.
61
+
// We can safely allocate this with the ctx arena since we only need it for this frame.
62
+
if (self.count > 0) {
63
+
self.button.label = try std.fmt.allocPrint(ctx.arena, "Clicks: {d}", .{self.count});
64
+
} else {
65
+
self.button.label = "Click me!";
66
+
}
67
+
68
+
// Each widget returns a Surface from it's draw function. A Surface contains the rectangular
69
+
// area of the widget, as well as some information about the surface or widget: can we focus
70
+
// it? does it handle the mouse?
71
+
//
72
+
// It DOES NOT contain the location it should be within it's parent. Only the parent can set
73
+
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
74
+
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
75
+
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
76
+
// child and place it within itself
77
+
const button_child: vxfw.SubSurface = .{
78
+
.origin = .{ .row = 0, .col = 0 },
79
+
.surface = try self.button.draw(ctx.withConstraints(
80
+
ctx.min,
81
+
// Here we explicitly set a new maximum size constraint for the Button. A Button will
82
+
// expand to fill it's area and must have some hard limit in the maximum constraint
83
+
.{ .width = 16, .height = 3 },
84
+
)),
85
+
};
86
+
87
+
// We also can use our arena to allocate the slice for our SubSurfaces. This slice only
88
+
// needs to live until the next frame, making this safe.
89
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
90
+
children[0] = button_child;
91
+
92
+
return .{
93
+
// A Surface must have a size. Our root widget is the size of the screen
94
+
.size = max_size,
95
+
.widget = self.widget(),
96
+
// We didn't actually need to draw anything for the root. In this case, we can set
97
+
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
98
+
// assert that it's length is equal to the size.width * size.height.
99
+
.buffer = &.{},
100
+
.children = children,
101
+
};
102
+
}
103
+
104
+
/// The onClick callback for our button. This is also called if we press enter while the button
105
+
/// has focus
106
+
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
107
+
const ptr = maybe_ptr orelse return;
108
+
const self: *Model = @ptrCast(@alignCast(ptr));
109
+
self.count +|= 1;
110
+
return ctx.consumeAndRedraw();
111
+
}
112
+
};
113
+
114
+
pub fn main() !void {
115
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
116
+
defer _ = gpa.deinit();
117
+
118
+
const allocator = gpa.allocator();
119
+
120
+
var app = try vxfw.App.init(allocator);
121
+
defer app.deinit();
122
+
123
+
// We heap allocate our model because we will require a stable pointer to it in our Button
124
+
// widget
125
+
const model = try allocator.create(Model);
126
+
defer allocator.destroy(model);
127
+
128
+
// Set the initial state of our button
129
+
model.* = .{
130
+
.count = 0,
131
+
.button = .{
132
+
.label = "Click me!",
133
+
.onClick = Model.onClick,
134
+
.userdata = model,
135
+
},
136
+
};
137
+
138
+
try app.run(model.widget(), .{});
139
+
}
+243
examples/fuzzy.zig
+243
examples/fuzzy.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
const Model = struct {
6
+
list: std.ArrayList(vxfw.Text),
7
+
/// Memory owned by .arena
8
+
filtered: std.ArrayList(vxfw.RichText),
9
+
list_view: vxfw.ListView,
10
+
text_field: vxfw.TextField,
11
+
12
+
/// Used for filtered RichText Spans and result
13
+
arena: std.heap.ArenaAllocator,
14
+
filtered: std.ArrayList(vxfw.RichText),
15
+
result: []const u8,
16
+
17
+
pub fn init(gpa: std.mem.Allocator) !*Model {
18
+
const model = try gpa.create(Model);
19
+
errdefer gpa.destroy(model);
20
+
21
+
model.* = .{
22
+
.list = .empty,
23
+
.filtered = .empty,
24
+
.list_view = .{
25
+
.children = .{
26
+
.builder = .{
27
+
.userdata = model,
28
+
.buildFn = Model.widgetBuilder,
29
+
},
30
+
},
31
+
},
32
+
.text_field = .{
33
+
.buf = vxfw.TextField.Buffer.init(gpa),
34
+
.userdata = model,
35
+
.onChange = Model.onChange,
36
+
.onSubmit = Model.onSubmit,
37
+
},
38
+
.result = "",
39
+
.arena = std.heap.ArenaAllocator.init(gpa),
40
+
};
41
+
42
+
return model;
43
+
}
44
+
45
+
pub fn deinit(self: *Model, gpa: std.mem.Allocator) void {
46
+
self.arena.deinit();
47
+
self.text_field.deinit();
48
+
self.list.deinit(gpa);
49
+
gpa.destroy(self);
50
+
}
51
+
52
+
pub fn widget(self: *Model) vxfw.Widget {
53
+
return .{
54
+
.userdata = self,
55
+
.eventHandler = Model.typeErasedEventHandler,
56
+
.drawFn = Model.typeErasedDrawFn,
57
+
};
58
+
}
59
+
60
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
61
+
const self: *Model = @ptrCast(@alignCast(ptr));
62
+
switch (event) {
63
+
.init => {
64
+
// Initialize the filtered list
65
+
const arena = self.arena.allocator();
66
+
for (self.list.items) |line| {
67
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
68
+
const span: vxfw.RichText.TextSpan = .{ .text = line.text };
69
+
try spans.append(arena, span);
70
+
try self.filtered.append(arena, .{ .text = spans.items });
71
+
}
72
+
73
+
return ctx.requestFocus(self.text_field.widget());
74
+
},
75
+
.key_press => |key| {
76
+
if (key.matches('c', .{ .ctrl = true })) {
77
+
ctx.quit = true;
78
+
return;
79
+
}
80
+
return self.list_view.handleEvent(ctx, event);
81
+
},
82
+
.focus_in => {
83
+
return ctx.requestFocus(self.text_field.widget());
84
+
},
85
+
else => {},
86
+
}
87
+
}
88
+
89
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
90
+
const self: *Model = @ptrCast(@alignCast(ptr));
91
+
const max = ctx.max.size();
92
+
93
+
const list_view: vxfw.SubSurface = .{
94
+
.origin = .{ .row = 2, .col = 0 },
95
+
.surface = try self.list_view.draw(ctx.withConstraints(
96
+
ctx.min,
97
+
.{ .width = max.width, .height = max.height - 3 },
98
+
)),
99
+
};
100
+
101
+
const text_field: vxfw.SubSurface = .{
102
+
.origin = .{ .row = 0, .col = 2 },
103
+
.surface = try self.text_field.draw(ctx.withConstraints(
104
+
ctx.min,
105
+
.{ .width = max.width, .height = 1 },
106
+
)),
107
+
};
108
+
109
+
const prompt: vxfw.Text = .{ .text = "๏", .style = .{ .fg = .{ .index = 4 } } };
110
+
111
+
const prompt_surface: vxfw.SubSurface = .{
112
+
.origin = .{ .row = 0, .col = 0 },
113
+
.surface = try prompt.draw(ctx.withConstraints(ctx.min, .{ .width = 2, .height = 1 })),
114
+
};
115
+
116
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 3);
117
+
children[0] = list_view;
118
+
children[1] = text_field;
119
+
children[2] = prompt_surface;
120
+
121
+
return .{
122
+
.size = max,
123
+
.widget = self.widget(),
124
+
.buffer = &.{},
125
+
.children = children,
126
+
};
127
+
}
128
+
129
+
fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
130
+
const self: *const Model = @ptrCast(@alignCast(ptr));
131
+
if (idx >= self.filtered.items.len) return null;
132
+
133
+
return self.filtered.items[idx].widget();
134
+
}
135
+
136
+
fn onChange(maybe_ptr: ?*anyopaque, _: *vxfw.EventContext, str: []const u8) anyerror!void {
137
+
const ptr = maybe_ptr orelse return;
138
+
const self: *Model = @ptrCast(@alignCast(ptr));
139
+
const arena = self.arena.allocator();
140
+
self.filtered.clearAndFree(arena);
141
+
_ = self.arena.reset(.free_all);
142
+
143
+
const hasUpper = for (str) |b| {
144
+
if (std.ascii.isUpper(b)) break true;
145
+
} else false;
146
+
147
+
// Loop each line
148
+
// If our input is only lowercase, we convert the line to lowercase
149
+
// Iterate the input graphemes, looking for them _in order_ in the line
150
+
outer: for (self.list.items) |item| {
151
+
const tgt = if (hasUpper)
152
+
item.text
153
+
else
154
+
try toLower(arena, item.text);
155
+
156
+
var spans = std.ArrayList(vxfw.RichText.TextSpan).empty;
157
+
var i: usize = 0;
158
+
var iter = vaxis.unicode.graphemeIterator(str);
159
+
while (iter.next()) |g| {
160
+
if (std.mem.indexOfPos(u8, tgt, i, g.bytes(str))) |idx| {
161
+
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..idx] };
162
+
const match: vxfw.RichText.TextSpan = .{
163
+
.text = item.text[idx .. idx + g.len],
164
+
.style = .{ .fg = .{ .index = 4 }, .reverse = true },
165
+
};
166
+
try spans.append(arena, up_to_here);
167
+
try spans.append(arena, match);
168
+
i = idx + g.len;
169
+
} else continue :outer;
170
+
}
171
+
const up_to_here: vxfw.RichText.TextSpan = .{ .text = item.text[i..] };
172
+
try spans.append(arena, up_to_here);
173
+
try self.filtered.append(arena, .{ .text = spans.items });
174
+
}
175
+
self.list_view.scroll.top = 0;
176
+
self.list_view.scroll.offset = 0;
177
+
self.list_view.cursor = 0;
178
+
}
179
+
180
+
fn onSubmit(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext, _: []const u8) anyerror!void {
181
+
const ptr = maybe_ptr orelse return;
182
+
const self: *Model = @ptrCast(@alignCast(ptr));
183
+
if (self.list_view.cursor < self.filtered.items.len) {
184
+
const selected = self.filtered.items[self.list_view.cursor];
185
+
const arena = self.arena.allocator();
186
+
var result = std.ArrayList(u8).empty;
187
+
for (selected.text) |span| {
188
+
try result.appendSlice(arena, span.text);
189
+
}
190
+
self.result = result.items;
191
+
}
192
+
ctx.quit = true;
193
+
}
194
+
};
195
+
196
+
fn toLower(arena: std.mem.Allocator, src: []const u8) std.mem.Allocator.Error![]const u8 {
197
+
const lower = try arena.alloc(u8, src.len);
198
+
for (src, 0..) |b, i| {
199
+
lower[i] = std.ascii.toLower(b);
200
+
}
201
+
return lower;
202
+
}
203
+
204
+
pub fn main() !void {
205
+
var debug_allocator = std.heap.GeneralPurposeAllocator(.{}){};
206
+
defer _ = debug_allocator.deinit();
207
+
208
+
const gpa = debug_allocator.allocator();
209
+
210
+
var app = try vxfw.App.init(gpa);
211
+
errdefer app.deinit();
212
+
213
+
const model = try Model.init(gpa);
214
+
defer model.deinit(gpa);
215
+
216
+
// Run the command
217
+
var fd = std.process.Child.init(&.{"fd"}, gpa);
218
+
fd.stdout_behavior = .Pipe;
219
+
fd.stderr_behavior = .Pipe;
220
+
var stdout = std.ArrayList(u8).empty;
221
+
var stderr = std.ArrayList(u8).empty;
222
+
defer stdout.deinit(gpa);
223
+
defer stderr.deinit(gpa);
224
+
try fd.spawn();
225
+
try fd.collectOutput(gpa, &stdout, &stderr, 10_000_000);
226
+
_ = try fd.wait();
227
+
228
+
var iter = std.mem.splitScalar(u8, stdout.items, '\n');
229
+
while (iter.next()) |line| {
230
+
if (line.len == 0) continue;
231
+
try model.list.append(gpa, .{ .text = line });
232
+
}
233
+
234
+
try app.run(model.widget(), .{});
235
+
app.deinit();
236
+
237
+
if (model.result.len > 0) {
238
+
_ = try std.posix.write(std.posix.STDOUT_FILENO, model.result);
239
+
_ = try std.posix.write(std.posix.STDOUT_FILENO, "\n");
240
+
} else {
241
+
std.process.exit(130);
242
+
}
243
+
}
+17
-13
examples/image.zig
+17
-13
examples/image.zig
···
18
}
19
const alloc = gpa.allocator();
20
21
-
var tty = try vaxis.Tty.init();
22
defer tty.deinit();
23
24
var vx = try vaxis.init(alloc, .{});
25
-
defer vx.deinit(alloc, tty.anyWriter());
26
27
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
28
try loop.init();
···
30
try loop.start();
31
defer loop.stop();
32
33
-
try vx.enterAltScreen(tty.anyWriter());
34
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
35
36
-
var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png");
37
const imgs = [_]vaxis.Image{
38
-
try vx.transmitImage(alloc, tty.anyWriter(), &img1, .rgba),
39
// var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png");
40
-
// try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/zig.png" }),
41
-
try vx.loadImage(alloc, tty.anyWriter(), .{ .path = "examples/vaxis.png" }),
42
};
43
-
defer vx.freeImage(tty.anyWriter(), imgs[0].id);
44
-
defer vx.freeImage(tty.anyWriter(), imgs[1].id);
45
46
var n: usize = 0;
47
48
-
var clip_y: usize = 0;
49
50
while (true) {
51
const event = loop.nextEvent();
···
60
else if (key.matches('k', .{}))
61
clip_y -|= 1;
62
},
63
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
64
}
65
66
n = (n + 1) % imgs.len;
···
74
.y = clip_y,
75
} });
76
77
-
try vx.render(tty.anyWriter());
78
}
79
}
···
18
}
19
const alloc = gpa.allocator();
20
21
+
var buffer: [1024]u8 = undefined;
22
+
var tty = try vaxis.Tty.init(&buffer);
23
defer tty.deinit();
24
25
var vx = try vaxis.init(alloc, .{});
26
+
defer vx.deinit(alloc, tty.writer());
27
28
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
29
try loop.init();
···
31
try loop.start();
32
defer loop.stop();
33
34
+
try vx.enterAltScreen(tty.writer());
35
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
36
+
37
+
var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer
38
+
var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png", &read_buffer);
39
+
defer img1.deinit(alloc);
40
41
const imgs = [_]vaxis.Image{
42
+
try vx.transmitImage(alloc, tty.writer(), &img1, .rgba),
43
// var img1 = try vaxis.zigimg.Image.fromFilePath(alloc, "examples/zig.png");
44
+
// try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/zig.png" }),
45
+
try vx.loadImage(alloc, tty.writer(), .{ .path = "examples/vaxis.png" }),
46
};
47
+
defer vx.freeImage(tty.writer(), imgs[0].id);
48
+
defer vx.freeImage(tty.writer(), imgs[1].id);
49
50
var n: usize = 0;
51
52
+
var clip_y: u16 = 0;
53
54
while (true) {
55
const event = loop.nextEvent();
···
64
else if (key.matches('k', .{}))
65
clip_y -|= 1;
66
},
67
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
68
}
69
70
n = (n + 1) % imgs.len;
···
78
.y = clip_y,
79
} });
80
81
+
try vx.render(tty.writer());
82
}
83
}
+99
examples/list_view.zig
+99
examples/list_view.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
const Text = vxfw.Text;
6
+
const ListView = vxfw.ListView;
7
+
const Widget = vxfw.Widget;
8
+
9
+
const Model = struct {
10
+
list_view: ListView,
11
+
12
+
pub fn widget(self: *Model) Widget {
13
+
return .{
14
+
.userdata = self,
15
+
.eventHandler = Model.typeErasedEventHandler,
16
+
.drawFn = Model.typeErasedDrawFn,
17
+
};
18
+
}
19
+
20
+
pub fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
21
+
const self: *Model = @ptrCast(@alignCast(ptr));
22
+
try ctx.requestFocus(self.list_view.widget());
23
+
switch (event) {
24
+
.key_press => |key| {
25
+
if (key.matches('q', .{}) or key.matchExact('c', .{ .ctrl = true })) {
26
+
ctx.quit = true;
27
+
return;
28
+
}
29
+
},
30
+
else => {},
31
+
}
32
+
}
33
+
34
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
35
+
const self: *Model = @ptrCast(@alignCast(ptr));
36
+
const max = ctx.max.size();
37
+
38
+
const list_view: vxfw.SubSurface = .{
39
+
.origin = .{ .row = 1, .col = 1 },
40
+
.surface = try self.list_view.draw(ctx),
41
+
};
42
+
43
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
44
+
children[0] = list_view;
45
+
46
+
return .{
47
+
.size = max,
48
+
.widget = self.widget(),
49
+
.buffer = &.{},
50
+
.children = children,
51
+
};
52
+
}
53
+
};
54
+
55
+
pub fn main() !void {
56
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
57
+
defer _ = gpa.deinit();
58
+
59
+
const allocator = gpa.allocator();
60
+
61
+
var app = try vxfw.App.init(allocator);
62
+
defer app.deinit();
63
+
64
+
const model = try allocator.create(Model);
65
+
defer allocator.destroy(model);
66
+
67
+
const n = 80;
68
+
var texts = try std.ArrayList(Widget).initCapacity(allocator, n);
69
+
70
+
var allocs = try std.ArrayList(*Text).initCapacity(allocator, n);
71
+
defer {
72
+
for (allocs.items) |tw| {
73
+
allocator.free(tw.text);
74
+
allocator.destroy(tw);
75
+
}
76
+
allocs.deinit(allocator);
77
+
texts.deinit(allocator);
78
+
}
79
+
80
+
for (0..n) |i| {
81
+
const t = std.fmt.allocPrint(allocator, "List Item {d}", .{i}) catch "placeholder";
82
+
const tw = try allocator.create(Text);
83
+
tw.* = .{ .text = t };
84
+
_ = try allocs.append(allocator, tw);
85
+
_ = try texts.append(allocator, tw.widget());
86
+
}
87
+
88
+
model.* = .{
89
+
.list_view = .{
90
+
.wheel_scroll = 3,
91
+
.scroll = .{
92
+
.wants_cursor = true,
93
+
},
94
+
.children = .{ .slice = texts.items },
95
+
},
96
+
};
97
+
98
+
try app.run(model.widget(), .{});
99
+
}
+35
-8
examples/main.zig
+35
-8
examples/main.zig
···
14
}
15
const alloc = gpa.allocator();
16
17
-
var tty = try vaxis.Tty.init();
18
defer tty.deinit();
19
20
var vx = try vaxis.init(alloc, .{});
21
-
defer vx.deinit(alloc, tty.anyWriter());
22
23
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
24
try loop.init();
···
27
defer loop.stop();
28
29
// Optionally enter the alternate screen
30
-
try vx.enterAltScreen(tty.anyWriter());
31
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
32
33
// We'll adjust the color index every keypress
34
var color_idx: u8 = 0;
35
const msg = "Hello, world!";
36
37
// The main event loop. Vaxis provides a thread safe, blocking, buffered
38
// queue which can serve as the primary event queue for an application
···
51
if (key.codepoint == 'c' and key.mods.ctrl) {
52
break;
53
}
54
},
55
.winsize => |ws| {
56
-
try vx.resize(alloc, tty.anyWriter(), ws);
57
},
58
else => {},
59
}
···
67
// the old and only updated cells will be drawn
68
win.clear();
69
70
// Create some child window. .expand means the height and width will
71
// fill the remaining space of the parent. Child windows do not store a
72
// reference to their parent: this is true immediate mode. Do not store
73
// windows, always create new windows each render cycle
74
-
const child = win.initChild(win.width / 2 - msg.len / 2, win.height / 2, .expand, .expand);
75
// Loop through the message and print the cells to the screen
76
for (msg, 0..) |_, i| {
77
const cell: Cell = .{
···
83
.style = .{
84
.fg = .{ .index = color_idx },
85
},
86
};
87
-
child.writeCell(i, 0, cell);
88
}
89
// Render the screen
90
-
try vx.render(tty.anyWriter());
91
}
92
}
93
···
14
}
15
const alloc = gpa.allocator();
16
17
+
var buffer: [1024]u8 = undefined;
18
+
var tty = try vaxis.Tty.init(&buffer);
19
defer tty.deinit();
20
21
var vx = try vaxis.init(alloc, .{});
22
+
defer vx.deinit(alloc, tty.writer());
23
24
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
25
try loop.init();
···
28
defer loop.stop();
29
30
// Optionally enter the alternate screen
31
+
try vx.enterAltScreen(tty.writer());
32
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
33
34
// We'll adjust the color index every keypress
35
var color_idx: u8 = 0;
36
const msg = "Hello, world!";
37
+
38
+
var scale: u3 = 1;
39
40
// The main event loop. Vaxis provides a thread safe, blocking, buffered
41
// queue which can serve as the primary event queue for an application
···
54
if (key.codepoint == 'c' and key.mods.ctrl) {
55
break;
56
}
57
+
if (key.matches('j', .{})) {
58
+
if (vx.caps.scaled_text and scale > 1) {
59
+
scale -= 1;
60
+
}
61
+
}
62
+
if (key.matches('k', .{})) {
63
+
if (vx.caps.scaled_text and scale < 7) {
64
+
scale += 1;
65
+
}
66
+
}
67
},
68
.winsize => |ws| {
69
+
try vx.resize(alloc, tty.writer(), ws);
70
},
71
else => {},
72
}
···
80
// the old and only updated cells will be drawn
81
win.clear();
82
83
+
const msg_len: u16 = @intCast(msg.len);
84
// Create some child window. .expand means the height and width will
85
// fill the remaining space of the parent. Child windows do not store a
86
// reference to their parent: this is true immediate mode. Do not store
87
// windows, always create new windows each render cycle
88
+
const child = win.child(
89
+
.{ .x_off = win.width / 2 - msg_len / 2, .y_off = win.height / 2 },
90
+
);
91
// Loop through the message and print the cells to the screen
92
for (msg, 0..) |_, i| {
93
const cell: Cell = .{
···
99
.style = .{
100
.fg = .{ .index = color_idx },
101
},
102
+
.scale = .{
103
+
.scale = scale,
104
+
},
105
};
106
+
const second_cell: Cell = .{
107
+
.char = .{ .grapheme = msg[i .. i + 1] },
108
+
.style = .{
109
+
.fg = .{ .index = color_idx },
110
+
},
111
+
};
112
+
child.writeCell(@intCast(i * scale), 0, cell);
113
+
child.writeCell(@intCast(i), scale - 1, second_cell);
114
+
child.writeCell(@intCast(i), scale, second_cell);
115
}
116
// Render the screen
117
+
try vx.render(tty.writer());
118
}
119
}
120
+214
examples/scroll.zig
+214
examples/scroll.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
const ModelRow = struct {
6
+
text: []const u8,
7
+
idx: usize,
8
+
wrap_lines: bool = true,
9
+
10
+
pub fn widget(self: *ModelRow) vxfw.Widget {
11
+
return .{
12
+
.userdata = self,
13
+
.drawFn = ModelRow.typeErasedDrawFn,
14
+
};
15
+
}
16
+
17
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
18
+
const self: *ModelRow = @ptrCast(@alignCast(ptr));
19
+
20
+
const idx_text = try std.fmt.allocPrint(ctx.arena, "{d: >4}", .{self.idx});
21
+
const idx_widget: vxfw.Text = .{ .text = idx_text };
22
+
23
+
const idx_surf: vxfw.SubSurface = .{
24
+
.origin = .{ .row = 0, .col = 0 },
25
+
.surface = try idx_widget.draw(ctx.withConstraints(
26
+
// We're only interested in constraining the width, and we know the height will
27
+
// always be 1 row.
28
+
.{ .width = 1, .height = 1 },
29
+
.{ .width = 4, .height = 1 },
30
+
)),
31
+
};
32
+
33
+
const text_widget: vxfw.Text = .{ .text = self.text, .softwrap = self.wrap_lines };
34
+
const text_surf: vxfw.SubSurface = .{
35
+
.origin = .{ .row = 0, .col = 6 },
36
+
.surface = try text_widget.draw(ctx.withConstraints(
37
+
ctx.min,
38
+
// We've shifted the origin over 6 columns so we need to take that into account or
39
+
// we'll draw outside the window.
40
+
if (self.wrap_lines)
41
+
.{ .width = ctx.min.width -| 6, .height = ctx.max.height }
42
+
else
43
+
.{ .width = if (ctx.max.width) |w| w - 6 else null, .height = ctx.max.height },
44
+
)),
45
+
};
46
+
47
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
48
+
children[0] = idx_surf;
49
+
children[1] = text_surf;
50
+
51
+
return .{
52
+
.size = .{
53
+
.width = 6 + text_surf.surface.size.width,
54
+
.height = @max(idx_surf.surface.size.height, text_surf.surface.size.height),
55
+
},
56
+
.widget = self.widget(),
57
+
.buffer = &.{},
58
+
.children = children,
59
+
};
60
+
}
61
+
};
62
+
63
+
const Model = struct {
64
+
scroll_bars: vxfw.ScrollBars,
65
+
rows: std.ArrayList(ModelRow),
66
+
67
+
pub fn widget(self: *Model) vxfw.Widget {
68
+
return .{
69
+
.userdata = self,
70
+
.eventHandler = Model.typeErasedEventHandler,
71
+
.drawFn = Model.typeErasedDrawFn,
72
+
};
73
+
}
74
+
75
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
76
+
const self: *Model = @ptrCast(@alignCast(ptr));
77
+
switch (event) {
78
+
.key_press => |key| {
79
+
if (key.matches('c', .{ .ctrl = true })) {
80
+
ctx.quit = true;
81
+
return;
82
+
}
83
+
if (key.matches('w', .{ .ctrl = true })) {
84
+
for (self.rows.items) |*row| {
85
+
row.wrap_lines = !row.wrap_lines;
86
+
}
87
+
self.scroll_bars.estimated_content_height =
88
+
if (self.scroll_bars.estimated_content_height == 800)
89
+
@intCast(self.rows.items.len)
90
+
else
91
+
800;
92
+
93
+
return ctx.consumeAndRedraw();
94
+
}
95
+
if (key.matches('e', .{ .ctrl = true })) {
96
+
if (self.scroll_bars.estimated_content_height == null)
97
+
self.scroll_bars.estimated_content_height = 800
98
+
else
99
+
self.scroll_bars.estimated_content_height = null;
100
+
101
+
return ctx.consumeAndRedraw();
102
+
}
103
+
if (key.matches(vaxis.Key.tab, .{})) {
104
+
self.scroll_bars.scroll_view.draw_cursor = !self.scroll_bars.scroll_view.draw_cursor;
105
+
return ctx.consumeAndRedraw();
106
+
}
107
+
if (key.matches('v', .{ .ctrl = true })) {
108
+
self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar;
109
+
return ctx.consumeAndRedraw();
110
+
}
111
+
if (key.matches('h', .{ .ctrl = true })) {
112
+
self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar;
113
+
return ctx.consumeAndRedraw();
114
+
}
115
+
if (key.matches(vaxis.Key.tab, .{ .shift = true })) {
116
+
self.scroll_bars.draw_vertical_scrollbar = !self.scroll_bars.draw_vertical_scrollbar;
117
+
self.scroll_bars.draw_horizontal_scrollbar = !self.scroll_bars.draw_horizontal_scrollbar;
118
+
return ctx.consumeAndRedraw();
119
+
}
120
+
return self.scroll_bars.scroll_view.handleEvent(ctx, event);
121
+
},
122
+
else => {},
123
+
}
124
+
}
125
+
126
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
127
+
const self: *Model = @ptrCast(@alignCast(ptr));
128
+
const max = ctx.max.size();
129
+
130
+
const scroll_view: vxfw.SubSurface = .{
131
+
.origin = .{ .row = 0, .col = 0 },
132
+
.surface = try self.scroll_bars.draw(ctx),
133
+
};
134
+
135
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
136
+
children[0] = scroll_view;
137
+
138
+
return .{
139
+
.size = max,
140
+
.widget = self.widget(),
141
+
.buffer = &.{},
142
+
.children = children,
143
+
};
144
+
}
145
+
146
+
fn widgetBuilder(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
147
+
const self: *const Model = @ptrCast(@alignCast(ptr));
148
+
if (idx >= self.rows.items.len) return null;
149
+
150
+
return self.rows.items[idx].widget();
151
+
}
152
+
};
153
+
154
+
pub fn main() !void {
155
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
156
+
defer _ = gpa.deinit();
157
+
158
+
const allocator = gpa.allocator();
159
+
160
+
var app = try vxfw.App.init(allocator);
161
+
errdefer app.deinit();
162
+
163
+
var arena = std.heap.ArenaAllocator.init(allocator);
164
+
defer arena.deinit();
165
+
166
+
const model = try allocator.create(Model);
167
+
defer allocator.destroy(model);
168
+
model.* = .{
169
+
.scroll_bars = .{
170
+
.scroll_view = .{
171
+
.children = .{
172
+
.builder = .{
173
+
.userdata = model,
174
+
.buildFn = Model.widgetBuilder,
175
+
},
176
+
},
177
+
},
178
+
// NOTE: This is not the actual content height, but rather an estimate. In reality
179
+
// you would want to do some calculations to keep this up to date and as close to
180
+
// the real value as possible, but this suffices for the sake of the example. Try
181
+
// playing around with the value to see how it affects the scrollbar. Try removing
182
+
// it as well to see what that does.
183
+
.estimated_content_height = 800,
184
+
},
185
+
.rows = std.ArrayList(ModelRow).empty,
186
+
};
187
+
defer model.rows.deinit(allocator);
188
+
189
+
var lipsum = std.ArrayList([]const u8).empty;
190
+
defer lipsum.deinit(allocator);
191
+
192
+
try lipsum.append(allocator, " Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc sit amet nunc porta, commodo tellus eu, blandit lectus. Aliquam dignissim rhoncus mi eu ultrices. Suspendisse lectus massa, bibendum sed lorem sit amet, egestas aliquam ante. Mauris venenatis nibh neque. Nulla a mi eget purus porttitor malesuada. Sed ac porta felis. Morbi ultricies urna nisi, et maximus elit convallis a. Morbi ut felis nec orci euismod congue efficitur egestas ex. Quisque eu feugiat magna. Pellentesque porttitor tortor ut iaculis dictum. Nulla erat neque, sollicitudin vitae enim nec, pharetra blandit tortor. Sed orci ante, condimentum vitae sodales in, sodales ut nulla. Suspendisse quam felis, aliquet ut neque a, lacinia sagittis turpis. Vivamus nec dui purus. Proin tempor nisl et porttitor consequat.");
193
+
try lipsum.append(allocator, " Vivamus elit massa, commodo in laoreet nec, scelerisque ac orci. Donec nec ante sit amet nisi ullamcorper dictum quis non enim. Proin ante libero, consequat sit amet semper a, vulputate non odio. Mauris ut suscipit lacus. Mauris nec dolor id ex mollis tempor at quis ligula. Integer varius commodo ipsum id gravida. Sed ut lobortis est, id egestas nunc. In fringilla ullamcorper porttitor. Donec quis dignissim arcu, vitae sagittis tortor. Sed tempor porttitor arcu, sit amet elementum est ornare id. Morbi rhoncus, ipsum eget tincidunt volutpat, mauris enim vestibulum nibh, mollis iaculis ante enim quis enim. Donec pharetra odio vel ex fringilla, ut laoreet ipsum commodo. Praesent tempus, leo a pellentesque sodales, erat ipsum pretium nulla, id faucibus sem turpis at nibh. Aenean ut dui luctus, vehicula felis vel, aliquam nulla.");
194
+
try lipsum.append(allocator, " Cras interdum mattis elit non varius. In condimentum velit a tellus sollicitudin interdum. Etiam pulvinar semper ex, eget congue ante tristique ut. Phasellus commodo magna magna, at fermentum tortor porttitor ac. Fusce a efficitur diam, a congue ante. Mauris maximus ultrices leo, non viverra ex hendrerit eu. Donec laoreet turpis nulla, eget imperdiet tortor mollis aliquam. Donec a est eget ante consequat rhoncus.");
195
+
try lipsum.append(allocator, " Morbi facilisis libero nec viverra imperdiet. Ut dictum faucibus bibendum. Vestibulum ut nisl eu magna sollicitudin elementum vel eu ante. Phasellus euismod ligula massa, vel rutrum elit hendrerit ut. Vivamus id luctus lectus, at ullamcorper leo. Pellentesque in risus finibus, viverra ligula sed, porta nisl. Aliquam pretium accumsan placerat. Etiam a elit posuere, varius erat sed, aliquet quam. Morbi finibus gravida erat, non imperdiet dolor sollicitudin dictum. Aenean eget ullamcorper lacus, et hendrerit lorem. Quisque sed varius mauris.");
196
+
try lipsum.append(allocator, " Nullam vitae euismod mauris, eu gravida dolor. Nunc vel urna laoreet justo faucibus tempus. Vestibulum tincidunt sagittis metus ac dignissim. Curabitur eleifend dolor consequat malesuada posuere. In hac habitasse platea dictumst. Fusce eget ipsum tincidunt, placerat orci ut, malesuada ante. Vivamus ultrices purus vel orci posuere, sed posuere eros porta. Vestibulum a tellus et tortor scelerisque varius. Pellentesque vel leo sed est semper bibendum. Mauris tellus ante, cursus et nunc vitae, dictum pellentesque ex. In tristique purus felis, non efficitur ante mollis id. Nulla quam nisi, suscipit sit amet mattis vel, placerat sit amet lectus. Vestibulum cursus auctor quam, at convallis felis euismod non. Sed nec magna nisi. Morbi scelerisque accumsan nunc, sed sagittis sem varius sit amet. Maecenas arcu dui, euismod et sem quis, condimentum blandit tellus.");
197
+
try lipsum.append(allocator, " Nullam auctor lobortis libero non viverra. Mauris a imperdiet eros, a luctus est. Integer pellentesque eros et metus rhoncus egestas. Suspendisse eu risus mauris. Mauris posuere nulla in justo pharetra molestie. Maecenas sagittis at nunc et finibus. Vestibulum quis leo ac mauris malesuada vestibulum vitae eu enim. Ut et maximus elit. Pellentesque lorem felis, tristique vitae posuere vitae, auctor tempus magna. Fusce cursus purus sit amet risus pulvinar, non egestas ligula imperdiet.");
198
+
try lipsum.append(allocator, " Proin rhoncus tincidunt congue. Curabitur pretium mauris eu erat iaculis semper. Vestibulum augue tortor, vehicula id maximus at, semper eu leo. Vivamus feugiat at purus eu dapibus. Mauris luctus sollicitudin nibh, in placerat est mattis vitae. Morbi ut risus felis. Etiam lobortis mollis diam, id tempor odio sollicitudin a. Morbi congue, lacus ac accumsan consequat, ipsum eros facilisis est, in congue metus ex nec ligula. Vestibulum dolor ligula, interdum nec iaculis vel, interdum a diam. Curabitur mattis, risus at rhoncus gravida, diam est viverra diam, ut mattis augue nulla sed lacus.");
199
+
try lipsum.append(allocator, " Duis rutrum orci sit amet dui imperdiet porta. In pulvinar imperdiet enim nec tristique. Etiam egestas pulvinar arcu, viverra mollis ipsum. Ut sit amet sapien nibh. Maecenas ut velit egestas, suscipit dolor vel, interdum tellus. Pellentesque faucibus euismod risus, ac vehicula erat sodales a. Aliquam egestas sit amet enim ac posuere. In id venenatis eros, et pharetra neque. Proin facilisis, odio id vehicula elementum, sapien ligula interdum dui, quis vestibulum est quam sit amet nisl. Aliquam in orci et felis aliquet tempus quis id magna. Sed interdum malesuada sem. Proin sagittis est metus, eu vestibulum nunc lacinia in. Vestibulum enim erat, cursus at justo at, porta feugiat quam. Phasellus vestibulum finibus nulla, at egestas augue imperdiet dapibus. Nunc in felis at ante congue interdum ut nec sapien.");
200
+
try lipsum.append(allocator, " Etiam lacinia ornare mauris, ut lacinia elit sollicitudin non. Morbi cursus dictum enim, et vulputate mi sollicitudin vel. Fusce rutrum augue justo. Phasellus et mauris tincidunt erat lacinia bibendum sed eu orci. Sed nunc lectus, dignissim sit amet ultricies sit amet, efficitur eu urna. Fusce feugiat malesuada ipsum nec congue. Praesent ultrices metus eu pulvinar laoreet. Maecenas pellentesque, metus ac lobortis rhoncus, ligula eros consequat urna, eget dictum lectus sem ut orci. Donec lobortis, lacus sed bibendum auctor, odio turpis suscipit odio, vitae feugiat leo metus ac lectus. Curabitur sed sem arcu.");
201
+
try lipsum.append(allocator, " Mauris nisi tortor, auctor venenatis turpis a, finibus condimentum lectus. Donec id velit odio. Curabitur ac varius lorem. Nam cursus quam in velit gravida, in bibendum purus fermentum. Sed non rutrum dui, nec ultrices ligula. Integer lacinia blandit nisl non sollicitudin. Praesent nec malesuada eros, sit amet tincidunt nunc.");
202
+
203
+
// Try playing around with the amount of items in the scroll view to see how the scrollbar
204
+
// reacts.
205
+
for (0..10) |i| {
206
+
for (lipsum.items, 0..) |paragraph, j| {
207
+
const number = i * 10 + j;
208
+
try model.rows.append(allocator, .{ .idx = number, .text = paragraph });
209
+
}
210
+
}
211
+
212
+
try app.run(model.widget(), .{});
213
+
app.deinit();
214
+
}
+73
examples/split_view.zig
+73
examples/split_view.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const vxfw = vaxis.vxfw;
4
+
5
+
const Model = struct {
6
+
split: vxfw.SplitView,
7
+
lhs: vxfw.Text,
8
+
rhs: vxfw.Text,
9
+
children: [1]vxfw.SubSurface = undefined,
10
+
11
+
pub fn widget(self: *Model) vxfw.Widget {
12
+
return .{
13
+
.userdata = self,
14
+
.eventHandler = Model.typeErasedEventHandler,
15
+
.drawFn = Model.typeErasedDrawFn,
16
+
};
17
+
}
18
+
19
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
20
+
const self: *Model = @ptrCast(@alignCast(ptr));
21
+
switch (event) {
22
+
.init => {
23
+
self.split.lhs = self.lhs.widget();
24
+
self.split.rhs = self.rhs.widget();
25
+
},
26
+
.key_press => |key| {
27
+
if (key.matches('c', .{ .ctrl = true })) {
28
+
ctx.quit = true;
29
+
return;
30
+
}
31
+
},
32
+
else => {},
33
+
}
34
+
}
35
+
36
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
37
+
const self: *Model = @ptrCast(@alignCast(ptr));
38
+
const surf = try self.split.widget().draw(ctx);
39
+
self.children[0] = .{
40
+
.surface = surf,
41
+
.origin = .{ .row = 0, .col = 0 },
42
+
};
43
+
return .{
44
+
.size = ctx.max.size(),
45
+
.widget = self.widget(),
46
+
.buffer = &.{},
47
+
.children = &self.children,
48
+
};
49
+
}
50
+
};
51
+
52
+
pub fn main() !void {
53
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
54
+
defer _ = gpa.deinit();
55
+
56
+
const allocator = gpa.allocator();
57
+
58
+
var app = try vxfw.App.init(allocator);
59
+
defer app.deinit();
60
+
61
+
const model = try allocator.create(Model);
62
+
defer allocator.destroy(model);
63
+
model.* = .{
64
+
.lhs = .{ .text = "Left hand side" },
65
+
.rhs = .{ .text = "right hand side" },
66
+
.split = .{ .lhs = undefined, .rhs = undefined, .width = 10 },
67
+
};
68
+
69
+
model.split.lhs = model.lhs.widget();
70
+
model.split.rhs = model.rhs.widget();
71
+
72
+
try app.run(model.widget(), .{});
73
+
}
+142
-52
examples/table.zig
+142
-52
examples/table.zig
···
21
22
// Users set up below the main function
23
const users_buf = try alloc.dupe(User, users[0..]);
24
-
const user_list = std.ArrayList(User).fromOwnedSlice(alloc, users_buf);
25
-
defer user_list.deinit();
26
27
-
var tty = try vaxis.Tty.init();
28
defer tty.deinit();
29
-
30
-
var vx = try vaxis.init(alloc, .{});
31
-
defer vx.deinit(alloc, tty.anyWriter());
32
33
var loop: vaxis.Loop(union(enum) {
34
key_press: vaxis.Key,
35
winsize: vaxis.Winsize,
36
}) = .{ .tty = &tty, .vaxis = &vx };
37
-
38
try loop.start();
39
defer loop.stop();
40
-
try vx.enterAltScreen(tty.anyWriter());
41
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
42
43
const logo =
44
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
···
59
};
60
var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer };
61
62
-
var cmd_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode);
63
defer cmd_input.deinit();
64
65
// Colors
66
-
const selected_bg: vaxis.Cell.Color = .{ .rgb = .{ 64, 128, 255 } };
67
const other_bg: vaxis.Cell.Color = .{ .rgb = .{ 32, 32, 48 } };
68
69
// Table Context
70
-
var demo_tbl: vaxis.widgets.Table.TableContext = .{ .selected_bg = selected_bg };
71
72
// TUI State
73
var active: ActiveSection = .mid;
74
var moving = false;
75
76
while (true) {
77
-
// Create an Arena Allocator for easy allocations on each Event.
78
-
var event_arena = heap.ArenaAllocator.init(alloc);
79
-
defer event_arena.deinit();
80
const event_alloc = event_arena.allocator();
81
const event = loop.nextEvent();
82
···
101
key.matchesAny(&.{ ':', '/', 'g', 'G' }, .{}))
102
{
103
active = .btm;
104
-
for (0..cmd_input.buf.items.len) |_| _ = cmd_input.buf.orderedRemove(0);
105
try cmd_input.update(.{ .key_press = key });
106
-
cmd_input.cursor_idx = 1;
107
break :keyEvt;
108
}
109
···
123
// Change Column
124
if (key.matchesAny(&.{ vaxis.Key.left, 'h' }, .{})) demo_tbl.col -|= 1;
125
if (key.matchesAny(&.{ vaxis.Key.right, 'l' }, .{})) demo_tbl.col +|= 1;
126
},
127
.btm => {
128
if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{}) and moving) active = .mid
129
-
// Run Command and Clear Command Bar
130
-
else if (key.matchExact(vaxis.Key.enter, .{})) {
131
-
const cmd = cmd_input.buf.items;
132
if (mem.eql(u8, ":q", cmd) or
133
mem.eql(u8, ":quit", cmd) or
134
mem.eql(u8, ":exit", cmd)) return;
135
if (mem.eql(u8, "G", cmd)) {
136
-
demo_tbl.row = user_list.items.len - 1;
137
active = .mid;
138
}
139
if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) {
140
-
const goto_row = fmt.parseInt(usize, cmd[2..], 0) catch 0;
141
demo_tbl.row = goto_row;
142
active = .mid;
143
}
144
-
for (0..cmd_input.buf.items.len) |_| _ = cmd_input.buf.orderedRemove(0);
145
-
cmd_input.cursor_idx = 0;
146
} else try cmd_input.update(.{ .key_press = key });
147
},
148
}
149
moving = false;
150
},
151
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
152
-
//else => {},
153
}
154
155
// Sections
···
159
160
// - Top
161
const top_div = 6;
162
-
const top_bar = win.initChild(
163
-
0,
164
-
0,
165
-
.{ .limit = win.width },
166
-
.{ .limit = win.height / top_div },
167
-
);
168
for (title_segs[0..]) |*title_seg|
169
-
title_seg.*.style.bg = if (active == .top) selected_bg else other_bg;
170
top_bar.fill(.{ .style = .{
171
.bg = if (active == .top) selected_bg else other_bg,
172
} });
···
175
44,
176
top_bar.height - (top_bar.height / 3),
177
);
178
-
_ = try logo_bar.print(title_segs[0..], .{ .wrap = .word });
179
180
// - Middle
181
-
const middle_bar = win.initChild(
182
-
0,
183
-
win.height / top_div,
184
-
.{ .limit = win.width },
185
-
.{ .limit = win.height - (top_bar.height + 1) },
186
-
);
187
-
if (user_list.items.len > 0) {
188
demo_tbl.active = active == .mid;
189
try vaxis.widgets.Table.drawTable(
190
-
event_alloc,
191
middle_bar,
192
-
&.{ "First", "Last", "Username", "Email", "Phone#" },
193
-
user_list,
194
&demo_tbl,
195
);
196
}
197
198
// - Bottom
199
-
const bottom_bar = win.initChild(
200
-
0,
201
-
win.height - 1,
202
-
.{ .limit = win.width },
203
-
.{ .limit = 1 },
204
-
);
205
-
if (active == .btm) bottom_bar.fill(.{ .style = .{ .bg = selected_bg } });
206
cmd_input.draw(bottom_bar);
207
208
// Render the screen
209
-
try vx.render(tty.anyWriter());
210
}
211
}
212
···
21
22
// Users set up below the main function
23
const users_buf = try alloc.dupe(User, users[0..]);
24
25
+
var buffer: [1024]u8 = undefined;
26
+
var tty = try vaxis.Tty.init(&buffer);
27
defer tty.deinit();
28
+
const tty_writer = tty.writer();
29
+
var vx = try vaxis.init(alloc, .{
30
+
.kitty_keyboard_flags = .{ .report_events = true },
31
+
});
32
+
defer vx.deinit(alloc, tty.writer());
33
34
var loop: vaxis.Loop(union(enum) {
35
key_press: vaxis.Key,
36
winsize: vaxis.Winsize,
37
+
table_upd,
38
}) = .{ .tty = &tty, .vaxis = &vx };
39
+
try loop.init();
40
try loop.start();
41
defer loop.stop();
42
+
try vx.enterAltScreen(tty.writer());
43
+
try vx.queryTerminal(tty.writer(), 250 * std.time.ns_per_ms);
44
45
const logo =
46
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
···
61
};
62
var title_segs = [_]vaxis.Cell.Segment{ title_logo, title_info, title_disclaimer };
63
64
+
var cmd_input = vaxis.widgets.TextInput.init(alloc);
65
defer cmd_input.deinit();
66
67
// Colors
68
+
const active_bg: vaxis.Cell.Color = .{ .rgb = .{ 64, 128, 255 } };
69
+
const selected_bg: vaxis.Cell.Color = .{ .rgb = .{ 32, 64, 255 } };
70
const other_bg: vaxis.Cell.Color = .{ .rgb = .{ 32, 32, 48 } };
71
72
// Table Context
73
+
var demo_tbl: vaxis.widgets.Table.TableContext = .{
74
+
.active_bg = active_bg,
75
+
.active_fg = .{ .rgb = .{ 0, 0, 0 } },
76
+
.row_bg_1 = .{ .rgb = .{ 8, 8, 8 } },
77
+
.selected_bg = selected_bg,
78
+
.header_names = .{ .custom = &.{ "First", "Last", "Username", "Phone#", "Email" } },
79
+
//.header_align = .left,
80
+
.col_indexes = .{ .by_idx = &.{ 0, 1, 2, 4, 3 } },
81
+
//.col_align = .{ .by_idx = &.{ .left, .left, .center, .center, .left } },
82
+
//.col_align = .{ .all = .center },
83
+
//.header_borders = true,
84
+
//.col_borders = true,
85
+
//.col_width = .{ .static_all = 15 },
86
+
//.col_width = .{ .dynamic_header_len = 3 },
87
+
//.col_width = .{ .static_individual = &.{ 10, 20, 15, 25, 15 } },
88
+
//.col_width = .dynamic_fill,
89
+
//.y_off = 10,
90
+
};
91
+
defer if (demo_tbl.sel_rows) |rows| alloc.free(rows);
92
93
// TUI State
94
var active: ActiveSection = .mid;
95
var moving = false;
96
+
var see_content = false;
97
98
+
// Create an Arena Allocator for easy allocations on each Event.
99
+
var event_arena = heap.ArenaAllocator.init(alloc);
100
+
defer event_arena.deinit();
101
while (true) {
102
+
defer _ = event_arena.reset(.retain_capacity);
103
+
defer tty_writer.flush() catch {};
104
const event_alloc = event_arena.allocator();
105
const event = loop.nextEvent();
106
···
125
key.matchesAny(&.{ ':', '/', 'g', 'G' }, .{}))
126
{
127
active = .btm;
128
+
cmd_input.clearAndFree();
129
try cmd_input.update(.{ .key_press = key });
130
break :keyEvt;
131
}
132
···
146
// Change Column
147
if (key.matchesAny(&.{ vaxis.Key.left, 'h' }, .{})) demo_tbl.col -|= 1;
148
if (key.matchesAny(&.{ vaxis.Key.right, 'l' }, .{})) demo_tbl.col +|= 1;
149
+
// Select/Unselect Row
150
+
if (key.matches(vaxis.Key.space, .{})) {
151
+
const rows = demo_tbl.sel_rows orelse createRows: {
152
+
demo_tbl.sel_rows = try alloc.alloc(u16, 1);
153
+
break :createRows demo_tbl.sel_rows.?;
154
+
};
155
+
var rows_list = std.ArrayList(u16).fromOwnedSlice(rows);
156
+
for (rows_list.items, 0..) |row, idx| {
157
+
if (row != demo_tbl.row) continue;
158
+
_ = rows_list.orderedRemove(idx);
159
+
break;
160
+
} else try rows_list.append(alloc, demo_tbl.row);
161
+
demo_tbl.sel_rows = try rows_list.toOwnedSlice(alloc);
162
+
}
163
+
// See Row Content
164
+
if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) see_content = !see_content;
165
},
166
.btm => {
167
if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{}) and moving) active = .mid
168
+
// Run Command and Clear Command Bar
169
+
else if (key.matchExact(vaxis.Key.enter, .{}) or key.matchExact('j', .{ .ctrl = true })) {
170
+
const cmd = try cmd_input.toOwnedSlice();
171
+
defer alloc.free(cmd);
172
if (mem.eql(u8, ":q", cmd) or
173
mem.eql(u8, ":quit", cmd) or
174
mem.eql(u8, ":exit", cmd)) return;
175
if (mem.eql(u8, "G", cmd)) {
176
+
demo_tbl.row = @intCast(users_buf.len - 1);
177
active = .mid;
178
}
179
if (cmd.len >= 2 and mem.eql(u8, "gg", cmd[0..2])) {
180
+
const goto_row = fmt.parseInt(u16, cmd[2..], 0) catch 0;
181
demo_tbl.row = goto_row;
182
active = .mid;
183
}
184
} else try cmd_input.update(.{ .key_press = key });
185
},
186
}
187
moving = false;
188
},
189
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
190
+
else => {},
191
+
}
192
+
193
+
// Content
194
+
seeRow: {
195
+
if (!see_content) {
196
+
demo_tbl.active_content_fn = null;
197
+
demo_tbl.active_ctx = &{};
198
+
break :seeRow;
199
+
}
200
+
const RowContext = struct {
201
+
row: []const u8,
202
+
bg: vaxis.Color,
203
+
};
204
+
const row_ctx = RowContext{
205
+
.row = try fmt.allocPrint(event_alloc, "Row #: {d}", .{demo_tbl.row}),
206
+
.bg = demo_tbl.active_bg,
207
+
};
208
+
demo_tbl.active_ctx = &row_ctx;
209
+
demo_tbl.active_content_fn = struct {
210
+
fn see(win: *vaxis.Window, ctx_raw: *const anyopaque) !u16 {
211
+
const ctx: *const RowContext = @ptrCast(@alignCast(ctx_raw));
212
+
win.height = 5;
213
+
const see_win = win.child(.{
214
+
.x_off = 0,
215
+
.y_off = 1,
216
+
.width = win.width,
217
+
.height = 4,
218
+
});
219
+
see_win.fill(.{ .style = .{ .bg = ctx.bg } });
220
+
const content_logo =
221
+
\\
222
+
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
223
+
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
224
+
\\โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
225
+
;
226
+
const content_segs: []const vaxis.Cell.Segment = &.{
227
+
.{
228
+
.text = ctx.row,
229
+
.style = .{ .bg = ctx.bg },
230
+
},
231
+
.{
232
+
.text = content_logo,
233
+
.style = .{ .bg = ctx.bg },
234
+
},
235
+
};
236
+
_ = see_win.print(content_segs, .{});
237
+
return see_win.height;
238
+
}
239
+
}.see;
240
+
loop.postEvent(.table_upd);
241
}
242
243
// Sections
···
247
248
// - Top
249
const top_div = 6;
250
+
const top_bar = win.child(.{
251
+
.x_off = 0,
252
+
.y_off = 0,
253
+
.width = win.width,
254
+
.height = win.height / top_div,
255
+
});
256
for (title_segs[0..]) |*title_seg|
257
+
title_seg.style.bg = if (active == .top) selected_bg else other_bg;
258
top_bar.fill(.{ .style = .{
259
.bg = if (active == .top) selected_bg else other_bg,
260
} });
···
263
44,
264
top_bar.height - (top_bar.height / 3),
265
);
266
+
_ = logo_bar.print(title_segs[0..], .{ .wrap = .word });
267
268
// - Middle
269
+
const middle_bar = win.child(.{
270
+
.x_off = 0,
271
+
.y_off = win.height / top_div,
272
+
.width = win.width,
273
+
.height = win.height - (top_bar.height + 1),
274
+
});
275
+
if (users_buf.len > 0) {
276
demo_tbl.active = active == .mid;
277
try vaxis.widgets.Table.drawTable(
278
+
null,
279
+
// event_alloc,
280
middle_bar,
281
+
//users_buf[0..],
282
+
//user_list,
283
+
users_buf,
284
&demo_tbl,
285
);
286
}
287
288
// - Bottom
289
+
const bottom_bar = win.child(.{
290
+
.x_off = 0,
291
+
.y_off = win.height - 1,
292
+
.width = win.width,
293
+
.height = 1,
294
+
});
295
+
if (active == .btm) bottom_bar.fill(.{ .style = .{ .bg = active_bg } });
296
cmd_input.draw(bottom_bar);
297
298
// Render the screen
299
+
try vx.render(tty_writer);
300
}
301
}
302
+14
-14
examples/text_input.zig
+14
-14
examples/text_input.zig
···
30
const alloc = gpa.allocator();
31
32
// Initalize a tty
33
-
var tty = try vaxis.Tty.init();
34
defer tty.deinit();
35
36
// Use a buffered writer for better performance. There are a lot of writes
37
// in the render loop and this can have a significant savings
38
-
var buffered_writer = tty.bufferedWriter();
39
-
const writer = buffered_writer.writer().any();
40
41
// Initialize Vaxis
42
var vx = try vaxis.init(alloc, .{
43
.kitty_keyboard_flags = .{ .report_events = true },
44
});
45
-
defer vx.deinit(alloc, tty.anyWriter());
46
47
var loop: vaxis.Loop(Event) = .{
48
.vaxis = &vx,
···
63
64
// init our text input widget. The text input widget needs an allocator to
65
// store the contents of the input
66
-
var text_input = TextInput.init(alloc, &vx.unicode);
67
defer text_input.deinit();
68
69
try vx.setMouseMode(writer, true);
70
71
-
try buffered_writer.flush();
72
// Sends queries to terminal to detect certain features. This should
73
// _always_ be called, but is left to the application to decide when
74
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
75
76
// The main event loop. Vaxis provides a thread safe, blocking, buffered
77
// queue which can serve as the primary event queue for an application
···
92
} else if (key.matches('l', .{ .ctrl = true })) {
93
vx.queueRefresh();
94
} else if (key.matches('n', .{ .ctrl = true })) {
95
-
try vx.notify(tty.anyWriter(), "vaxis", "hello from vaxis");
96
loop.stop();
97
var child = std.process.Child.init(&.{"nvim"}, alloc);
98
_ = try child.spawnAndWait();
99
try loop.start();
100
-
try vx.enterAltScreen(tty.anyWriter());
101
vx.queueRefresh();
102
-
} else if (key.matches(vaxis.Key.enter, .{})) {
103
text_input.clearAndFree();
104
} else {
105
try text_input.update(.{ .key_press = key });
···
121
// more than one byte will incur an allocation on the first render
122
// after it is drawn. Thereafter, it will not allocate unless the
123
// screen is resized
124
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
125
else => {},
126
}
127
···
141
const child = win.child(.{
142
.x_off = win.width / 2 - 20,
143
.y_off = win.height / 2 - 3,
144
-
.width = .{ .limit = 40 },
145
-
.height = .{ .limit = 3 },
146
.border = .{
147
.where = .all,
148
.style = style,
···
152
153
// Render the screen
154
try vx.render(writer);
155
-
try buffered_writer.flush();
156
}
157
}
···
30
const alloc = gpa.allocator();
31
32
// Initalize a tty
33
+
var buffer: [1024]u8 = undefined;
34
+
var tty = try vaxis.Tty.init(&buffer);
35
defer tty.deinit();
36
37
// Use a buffered writer for better performance. There are a lot of writes
38
// in the render loop and this can have a significant savings
39
+
const writer = tty.writer();
40
41
// Initialize Vaxis
42
var vx = try vaxis.init(alloc, .{
43
.kitty_keyboard_flags = .{ .report_events = true },
44
});
45
+
defer vx.deinit(alloc, tty.writer());
46
47
var loop: vaxis.Loop(Event) = .{
48
.vaxis = &vx,
···
63
64
// init our text input widget. The text input widget needs an allocator to
65
// store the contents of the input
66
+
var text_input = TextInput.init(alloc);
67
defer text_input.deinit();
68
69
try vx.setMouseMode(writer, true);
70
71
+
try writer.flush();
72
// Sends queries to terminal to detect certain features. This should
73
// _always_ be called, but is left to the application to decide when
74
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
75
76
// The main event loop. Vaxis provides a thread safe, blocking, buffered
77
// queue which can serve as the primary event queue for an application
···
92
} else if (key.matches('l', .{ .ctrl = true })) {
93
vx.queueRefresh();
94
} else if (key.matches('n', .{ .ctrl = true })) {
95
+
try vx.notify(tty.writer(), "vaxis", "hello from vaxis");
96
loop.stop();
97
var child = std.process.Child.init(&.{"nvim"}, alloc);
98
_ = try child.spawnAndWait();
99
try loop.start();
100
+
try vx.enterAltScreen(tty.writer());
101
vx.queueRefresh();
102
+
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
103
text_input.clearAndFree();
104
} else {
105
try text_input.update(.{ .key_press = key });
···
121
// more than one byte will incur an allocation on the first render
122
// after it is drawn. Thereafter, it will not allocate unless the
123
// screen is resized
124
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
125
else => {},
126
}
127
···
141
const child = win.child(.{
142
.x_off = win.width / 2 - 20,
143
.y_off = win.height / 2 - 3,
144
+
.width = 40,
145
+
.height = 3,
146
.border = .{
147
.where = .all,
148
.style = style,
···
152
153
// Render the screen
154
try vx.render(writer);
155
+
try writer.flush();
156
}
157
}
+66
examples/text_view.zig
+66
examples/text_view.zig
···
···
1
+
const std = @import("std");
2
+
const log = std.log.scoped(.main);
3
+
const vaxis = @import("vaxis");
4
+
5
+
const TextView = vaxis.widgets.TextView;
6
+
7
+
const Event = union(enum) {
8
+
key_press: vaxis.Key,
9
+
winsize: vaxis.Winsize,
10
+
};
11
+
12
+
pub fn main() !void {
13
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
14
+
15
+
defer {
16
+
const deinit_status = gpa.deinit();
17
+
if (deinit_status == .leak) {
18
+
log.err("memory leak", .{});
19
+
}
20
+
}
21
+
22
+
const alloc = gpa.allocator();
23
+
var buffer: [1024]u8 = undefined;
24
+
var tty = try vaxis.Tty.init(&buffer);
25
+
defer tty.deinit();
26
+
var vx = try vaxis.init(alloc, .{});
27
+
defer vx.deinit(alloc, tty.writer());
28
+
var loop: vaxis.Loop(Event) = .{
29
+
.vaxis = &vx,
30
+
.tty = &tty,
31
+
};
32
+
try loop.init();
33
+
try loop.start();
34
+
defer loop.stop();
35
+
try vx.enterAltScreen(tty.writer());
36
+
try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s);
37
+
var text_view = TextView{};
38
+
var text_view_buffer = TextView.Buffer{};
39
+
defer text_view_buffer.deinit(alloc);
40
+
try text_view_buffer.append(alloc, .{ .bytes = "Press Enter to add a line, Up/Down to scroll, 'c' to close." });
41
+
42
+
var counter: i32 = 0;
43
+
var lineBuf: [128]u8 = undefined;
44
+
45
+
while (true) {
46
+
const event = loop.nextEvent();
47
+
switch (event) {
48
+
.key_press => |key| {
49
+
// Close demo
50
+
if (key.matches('c', .{})) break;
51
+
if (key.matches(vaxis.Key.enter, .{})) {
52
+
counter += 1;
53
+
const new_content = try std.fmt.bufPrint(&lineBuf, "\nLine {d}", .{counter});
54
+
try text_view_buffer.append(alloc, .{ .bytes = new_content });
55
+
}
56
+
text_view.input(key);
57
+
},
58
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
59
+
}
60
+
const win = vx.window();
61
+
win.clear();
62
+
text_view.draw(win, text_view_buffer);
63
+
try vx.render(tty.writer());
64
+
try tty.writer.flush();
65
+
}
66
+
}
+15
-11
examples/vaxis.zig
+15
-11
examples/vaxis.zig
···
20
}
21
const alloc = gpa.allocator();
22
23
-
var tty = try vaxis.Tty.init();
24
defer tty.deinit();
25
26
var vx = try vaxis.init(alloc, .{});
27
-
defer vx.deinit(alloc, tty.anyWriter());
28
29
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
30
try loop.init();
···
32
try loop.start();
33
defer loop.stop();
34
35
-
try vx.enterAltScreen(tty.anyWriter());
36
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
37
38
-
try vx.queryColor(tty.anyWriter(), .fg);
39
-
try vx.queryColor(tty.anyWriter(), .bg);
40
var pct: u8 = 0;
41
var dir: enum {
42
up,
···
52
switch (event) {
53
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
54
.winsize => |ws| {
55
-
try vx.resize(alloc, tty.anyWriter(), ws);
56
break;
57
},
58
}
···
62
while (loop.tryEvent()) |event| {
63
switch (event) {
64
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
65
-
.winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws),
66
}
67
}
68
···
78
.style = style,
79
};
80
const center = vaxis.widgets.alignment.center(win, 28, 4);
81
-
_ = try center.printSegment(segment, .{ .wrap = .grapheme });
82
-
try vx.render(tty.anyWriter());
83
-
std.time.sleep(16 * std.time.ns_per_ms);
84
switch (dir) {
85
.up => {
86
pct += 1;
···
20
}
21
const alloc = gpa.allocator();
22
23
+
var buffer: [1024]u8 = undefined;
24
+
var tty = try vaxis.Tty.init(&buffer);
25
defer tty.deinit();
26
27
var vx = try vaxis.init(alloc, .{});
28
+
defer vx.deinit(alloc, tty.writer());
29
30
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
31
try loop.init();
···
33
try loop.start();
34
defer loop.stop();
35
36
+
try vx.enterAltScreen(tty.writer());
37
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
38
39
+
try vx.queryColor(tty.writer(), .fg);
40
+
try vx.queryColor(tty.writer(), .bg);
41
var pct: u8 = 0;
42
var dir: enum {
43
up,
···
53
switch (event) {
54
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
55
.winsize => |ws| {
56
+
try vx.resize(alloc, tty.writer(), ws);
57
break;
58
},
59
}
···
63
while (loop.tryEvent()) |event| {
64
switch (event) {
65
.key_press => |key| if (key.matches('c', .{ .ctrl = true })) return,
66
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
67
}
68
}
69
···
79
.style = style,
80
};
81
const center = vaxis.widgets.alignment.center(win, 28, 4);
82
+
_ = center.printSegment(segment, .{ .wrap = .grapheme });
83
+
// var bw = tty.bufferedWriter();
84
+
// try vx.render(bw.writer().any());
85
+
// try bw.flush();
86
+
try vx.render(tty.writer());
87
+
std.Thread.sleep(16 * std.time.ns_per_ms);
88
switch (dir) {
89
.up => {
90
pct += 1;
+361
examples/view.zig
+361
examples/view.zig
···
···
1
+
const std = @import("std");
2
+
const log = std.log.scoped(.main);
3
+
const mem = std.mem;
4
+
const process = std.process;
5
+
6
+
const vaxis = @import("vaxis");
7
+
const View = vaxis.widgets.View;
8
+
const Cell = vaxis.Cell;
9
+
const border = vaxis.widgets.border;
10
+
11
+
const Event = union(enum) {
12
+
key_press: vaxis.Key,
13
+
winsize: vaxis.Winsize,
14
+
};
15
+
16
+
pub fn main() !void {
17
+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
18
+
defer {
19
+
const deinit_status = gpa.deinit();
20
+
if (deinit_status == .leak) {
21
+
log.err("memory leak", .{});
22
+
}
23
+
}
24
+
const alloc = gpa.allocator();
25
+
26
+
var world_map: []const u8 = lg_world_map;
27
+
var map_width = lg_map_width;
28
+
var map_height = lg_map_height;
29
+
var use_sm_map = false;
30
+
var use_mini_view = false;
31
+
32
+
var x: u16 = 0;
33
+
var y: u16 = 0;
34
+
var h: u16 = 0;
35
+
var w: u16 = 0;
36
+
defer log.info(
37
+
\\Results:
38
+
\\ - Map Size: {d}x{d}
39
+
\\ - Screen Size: {d}x{d}
40
+
\\ - Position: {d}, {d}
41
+
, .{
42
+
map_width, map_height,
43
+
w, h,
44
+
x, y,
45
+
});
46
+
47
+
var buffer: [1024]u8 = undefined;
48
+
var tty = try vaxis.Tty.init(&buffer);
49
+
defer tty.deinit();
50
+
51
+
const writer = tty.writer();
52
+
53
+
// Initialize Vaxis
54
+
var vx = try vaxis.init(alloc, .{
55
+
.kitty_keyboard_flags = .{ .report_events = true },
56
+
});
57
+
defer vx.deinit(alloc, tty.writer());
58
+
var loop: vaxis.Loop(Event) = .{
59
+
.vaxis = &vx,
60
+
.tty = &tty,
61
+
};
62
+
try loop.init();
63
+
try loop.start();
64
+
defer loop.stop();
65
+
try vx.enterAltScreen(writer);
66
+
try writer.flush();
67
+
try vx.queryTerminal(tty.writer(), 20 * std.time.ns_per_s);
68
+
69
+
// Initialize Views
70
+
// - Large Map
71
+
var lg_map_view = try View.init(alloc, .{ .width = lg_map_width, .height = lg_map_height });
72
+
defer lg_map_view.deinit();
73
+
//w = lg_map_view.screen.width;
74
+
//h = lg_map_view.screen.height;
75
+
var lg_map_buf: [lg_map_width * lg_map_height]u8 = undefined;
76
+
_ = mem.replace(u8, lg_world_map, "\n", "", lg_map_buf[0..]);
77
+
_ = lg_map_view.printSegment(.{ .text = lg_map_buf[0..] }, .{ .wrap = .grapheme });
78
+
// - Small Map
79
+
var sm_map_view = try View.init(alloc, .{ .width = sm_map_width, .height = sm_map_height });
80
+
defer sm_map_view.deinit();
81
+
w = sm_map_view.screen.width;
82
+
h = sm_map_view.screen.height;
83
+
var sm_map_buf: [sm_map_width * sm_map_height]u8 = undefined;
84
+
_ = mem.replace(u8, sm_world_map, "\n", "", sm_map_buf[0..]);
85
+
_ = sm_map_view.printSegment(.{ .text = sm_map_buf[0..] }, .{ .wrap = .grapheme });
86
+
// - Active Map
87
+
var map_view = lg_map_view;
88
+
89
+
// TUI Loop
90
+
while (true) {
91
+
const event = loop.nextEvent();
92
+
switch (event) {
93
+
.key_press => |key| {
94
+
// Close Demo
95
+
if (key.matches('c', .{ .ctrl = true })) break;
96
+
// Scroll
97
+
if (key.matches(vaxis.Key.left, .{})) x -|= 1;
98
+
if (key.matches(vaxis.Key.right, .{})) x +|= 1;
99
+
if (key.matches(vaxis.Key.up, .{})) y -|= 1;
100
+
if (key.matches(vaxis.Key.down, .{})) y +|= 1;
101
+
// Quick Scroll
102
+
if (key.matches(vaxis.Key.left, .{ .ctrl = true })) x -|= 30;
103
+
if (key.matches(vaxis.Key.right, .{ .ctrl = true })) x +|= 30;
104
+
if (key.matches(vaxis.Key.up, .{ .ctrl = true })) y -|= 10;
105
+
if (key.matches(vaxis.Key.down, .{ .ctrl = true })) y +|= 10;
106
+
// Goto Side
107
+
if (key.matches(vaxis.Key.left, .{ .shift = true })) x -|= map_width;
108
+
if (key.matches(vaxis.Key.right, .{ .shift = true })) x +|= map_width;
109
+
if (key.matches(vaxis.Key.up, .{ .shift = true })) y -|= map_height;
110
+
if (key.matches(vaxis.Key.down, .{ .shift = true })) y +|= map_height;
111
+
// Change Zoom (Swap Map Views)
112
+
if (key.matches('z', .{})) {
113
+
if (use_sm_map) {
114
+
world_map = lg_world_map;
115
+
map_width = lg_map_width;
116
+
map_height = lg_map_height;
117
+
map_view = lg_map_view;
118
+
} else {
119
+
world_map = sm_world_map;
120
+
map_width = sm_map_width;
121
+
map_height = sm_map_height;
122
+
map_view = sm_map_view;
123
+
}
124
+
use_sm_map = !use_sm_map;
125
+
w = map_width;
126
+
h = map_height;
127
+
}
128
+
// Mini View (Forced Width & Height Limits)
129
+
if (key.matches('m', .{})) use_mini_view = !use_mini_view;
130
+
},
131
+
.winsize => |ws| try vx.resize(alloc, tty.writer(), ws),
132
+
}
133
+
134
+
const win = vx.window();
135
+
win.clear();
136
+
137
+
const controls_win = win.child(.{
138
+
.height = 1,
139
+
});
140
+
_ = controls_win.print(
141
+
if (win.width >= 112) &.{
142
+
.{ .text = "Controls:", .style = .{ .bold = true, .ul_style = .single } },
143
+
.{ .text = " Exit: ctrl + c | Scroll: dpad | Quick Scroll: ctrl + dpad | Goto Side: shift + dpad | Zoom: z | Mini: m" },
144
+
} else if (win.width >= 25) &.{
145
+
.{ .text = "Controls:", .style = .{ .bold = true, .ul_style = .single } },
146
+
.{ .text = " Win too small!" },
147
+
} else &.{
148
+
.{ .text = "" },
149
+
},
150
+
.{ .wrap = .none },
151
+
);
152
+
153
+
// Views require a Window to render to.
154
+
const map_win = if (use_mini_view)
155
+
win.child(.{
156
+
.y_off = controls_win.height,
157
+
.border = .{ .where = .top },
158
+
.width = 45,
159
+
.height = 15,
160
+
})
161
+
else
162
+
win.child(.{
163
+
.y_off = controls_win.height,
164
+
.border = .{ .where = .top },
165
+
});
166
+
167
+
// Clamp x and y
168
+
x = @min(x, map_width -| map_win.width);
169
+
y = @min(y, map_height -| map_win.height);
170
+
171
+
map_view.draw(map_win, .{
172
+
.x_off = x,
173
+
.y_off = y,
174
+
});
175
+
if (use_mini_view) {
176
+
_ = win.printSegment(
177
+
.{ .text = "This is a mini portion of the View." },
178
+
.{ .row_offset = 16, .col_offset = 5, .wrap = .word },
179
+
);
180
+
}
181
+
182
+
// Render the screen
183
+
try vx.render(writer);
184
+
try writer.flush();
185
+
}
186
+
}
187
+
188
+
const sm_world_map =
189
+
\\ +NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN+
190
+
\\ W................................................................................................E
191
+
\\ W................................................................................................E
192
+
\\ W............................ @:::::::::@ ..................... .............................E
193
+
\\ W..... . ..... ::: ...::::::: ........ ...... %@%...........:*%@ . ..............E
194
+
\\ W..@:::::::::::::::::::: ::: ::::@ @@ ..... %#%%%%%%%%%%%%...........................* .......E
195
+
\\ W ::::@::::::::::::: .. :: ... * ....... . %%% %%%%%%%%%%%%%..................... ..* .........E
196
+
\\ W .....@::::::::::::.=::::: ........... @% @. %% %%%-...................:%% @. ..........E
197
+
\\ W...... :::%@@:@:::::::::@ :-........... @%%%%%%%%%%%%%%%%........................ .. ..........E
198
+
\\ W......@::::@@@.=-:::::=: ............. %@%%@# %%%%%%%..%%%.........@............ @=............E
199
+
\\ W..... :::::::::::::: ................. %%% @ % ........................... . . .............E
200
+
\\ W..... #:::::::::::@ .................. @=====@ .........................@ .= ............E
201
+
\\ W...... :::::@ @ ................... ============== .... ..................... .. .............E
202
+
\\ W....... .::=.. :# ................. ================ ...... ............... .................E
203
+
\\ W......... ::::... @-............... ================- ....@ ...-... . ....@ .. ...............E
204
+
\\ W............. :=.. % ...............==================+ ......@. .... ... .. @ ..............E
205
+
\\ W............... ######@ ............ ========@=@@@======@........ @.... . .. ................E
206
+
\\ W................ ######### ................. ===========@............... .@ ..@ =..............E
207
+
\\ W............... #############= ............. ========== ................. . ..*. . --- .......E
208
+
\\ W................ ###@@@#@#####%.............. ======== .................... @.@.... .*-=- ......E
209
+
\\ W................. ##@##@@#@### .............. ========-. -...................... -- + ........E
210
+
\\ W................... ##########................=======% *= .................... @-------- .......E
211
+
\\ W................... #######* ................-====== .-+....................#-@--@-@#--- ......E
212
+
\\ W................... ####### .................. ====- ........................@----------- ......E
213
+
\\ W................... ###### ....................@-* .......................... = ----:..... .E
214
+
\\ W....................####@ ......................................................... - .... -@ E
215
+
\\ W.................... ###.................................................................%-@ ...E
216
+
\\ W..................... #@........................................................................E
217
+
\\ W...................... ......................................................................E
218
+
\\ +SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS+
219
+
\\
220
+
;
221
+
const sm_map_width = mapWidth: {
222
+
@setEvalBranchQuota(100_000);
223
+
break :mapWidth mem.indexOfScalar(u8, sm_world_map, '\n').?;
224
+
};
225
+
const sm_map_height = mapHeight: {
226
+
@setEvalBranchQuota(100_000);
227
+
break :mapHeight mem.count(u8, sm_world_map, "\n");
228
+
};
229
+
230
+
const lg_world_map =
231
+
\\ +NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN+
232
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
233
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
234
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
235
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
236
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
237
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
238
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
239
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
240
+
\\ W........................................................................................................................................................ ............................................................................................................................................................................................................................................E
241
+
\\ W................................................................................................................................ ................................................................................................................................................................................................................................E
242
+
\\ W........................................................................................................................... @@@@@@@@%-::::---::::::=#@@@@.... ................................................................................................................................................................................................................................E
243
+
\\ W..................................................................................................................... .@@@@:::::::::::::::::::::::::::::::::::::-@@ ...............................................................................................................................................................................................................................E
244
+
\\ W.................................................................................................................... %@@@#:::::::::::::::::::::::::::::::::::::@@- ...................................................................................... .........................................................................................................................E
245
+
\\ W................................................................................................................... @:::::::::::::::::::::::::::::::::::::::::@ .................................................................................... .@....@@: .......................................................................................................................E
246
+
\\ W............................................................................................... .... +:::::::::::::::::::::::::::::::::- ........................................................................... .. #@-.................@ ......................................................................................................E
247
+
\\ W......................................................................................... +@@@@@@@@@@ .... @:::::::::::::::::::::::::::::::@: ........................................................................ @@...........................%%%%*....%%@@= ..................................................................................E
248
+
\\ W........................ ... .................... @:::::::::::::@ ........ @::::::::::::::::::::::::::::+% ............................... ....................... @%%%%@@@%%%............................................-@@ %@@*******@@ .. ............................................................E
249
+
\\ W................... *@@#+++++++++@@%=== ======== ... @@::@ +::::::::::::::::-@ ....... *:::::::::::::::::::::::::::%@ ............................. %@@@@@= .. :@%%%%%%%%%%#.........................................................................+@= ................................................E
250
+
\\ W................ :@%=:::::::::::::::::::::::%%%-.:::::::::::::+%@@ =:::::--:::::@##%%::::::::@ ..... @:::::::::::::::::::::::%@+ ............................ @@%#%%%%%%%%%%#@@* +***@@%%%%%%%%%%%%%##..........................................................................................-%@@@@@+ .........................................E
251
+
\\ W............. .*::::::::::::::::::::::::::::::::::::::::::::::::::::::::::%@@@=:::::::::::::::@@ @::::::::@ .... @:::::::::::::::::::@@. ...................... @%%%%%%%%%%%%%%%#%%%%%%%@@%%%%@@@%%%%%%%%%#%%%%%%%%%%%%=.....................................................................................................@@@%. ....................................E
252
+
\\ W............ =#-:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::=@* @+:::::=:::+@ ... @::::::::::::::::@. @@@@@%%@@@ ................... @@%#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%%%%%%%%%%%%%%............................................................................................................@@@ ...................................E
253
+
\\ W......... *::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::@ *%::::::::::@ @@ ... *::::::::::@@@ ... %%%%%%%%@ ................ @@%%%%%##%* -%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%%......................................................................................................* @...@ ...................................E
254
+
\\ W....... =@@=::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::@ =@@@@ @:::::=@ ..... *:::::::::@ ....... :=@%%@ ............. %@%%%%%%%%%@: @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#=.......................................................................................................@ ...................................E
255
+
\\ W...... @:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::@: @:::-@ @@* ........ @::::::@- .............. ............... @@@%%%%%%%%%%@ @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.....................................................................................................+@@ .......................................E
256
+
\\ W..... :@::::::::::::::::#%*::::::::::::::::::::::::::::::::::::::::::::::::::::@ ...... @:::::::@ .......... ..@:@: .............................. .... *@%%%%%%%%%%%@* +@%#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%....................................................................................@ .@.=@.........@ .........................................E
257
+
\\ W.... @::::::::::=%@@ #%:::::::::::::::::::::::::::::::::::::::::::::- ........ @::::::::@:@=::@ ........... ................................ -@@ .. @%%%@@%%%%%%%=# @%%%%%%%%%%%%%%%%%#%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.........................................................................%@@@@@####@# +....#% ............................................E
258
+
\\ W. -@-:::*@- .. @::::::::::::::::::::::::::::::::::::::::::::@+ ..... @:::::::::::::::.- ................................................... @%%%@% ... @%%%@# #@%%% : : - %= :: : +%%%%%%%%%%%%%%%%%%%%%%.......................................................................-. @.....@ ...............................................E
259
+
\\ W @:::::%@ ............. ::::::::::::::::::::::::::::::::::::::::::::::::%+ +@:::::::::::::::::@ ............................................... %@%%%= ... %@ @%%%@ @%%%%% %. . - : . . +%%%%%%%%%%%%%%%#*%##%-.......................................................................:* ..... @......@: ..............................................E
260
+
\\ W@::::+@ .................. ::::::::::::::::::::::::::::::::::::::::::::::::::.+@ @*::::::::::::::::::::@ ........................................... :@@ @%%@# . #%%- @@@%%%%%% . . - # : . *#%%%%%%%%%%%#...............................................................................-@%% .... @........ ..............................................E
261
+
\\ W ...................... :::::::::::::::::::::::::::::::::::::::::::::::::::-. @::::::::::::::::::::::::* ........................................... @%%%@ @%%%@ @@@%%%%%%%%%%%%%%%%%%% @. . - : *#. *%%%%%%%%%%%%%#.........................................................................................@ .... @.....% ..............................................E
262
+
\\ W.. ......................... ::::::::::@:@:@@@:%@@%%@@@@@=@:::::::::::::::::::::= #=:::::::::::::::::::::::@ ........................................... :@%%%@ #%%%%%@ @@%%%%%%%%%%%%%%%%%%%%%% % % . .* += *%: -%%%%%%%%%#%............................................................................................@ ... +@..# ..............................................E
263
+
\\ W............................... .@.::::::::@#@.@+@@%@*@:%@:@@#@:::::::::::::::::::::::::::::::::::::::::::::::::@ ........................................... @@@ @@%@@@@ @@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%...............................................................................................@- .... @ ..............................................E
264
+
\\ W............................... @::::::::::@@@:@+@%%@%@:%@:@@@@:::::::::::::::::::::::::::::::::::::::%@@ @:::::@ ........................................... @@@@@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%.................................................................................................@ ..... ...............................................E
265
+
\\ W............................... @:::::::::@=@:@*@#%@+@:%@:@@=@::::::::::::::::::::::::::::::::::::::*@ @::::::* ................................................ .@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%##%##%%%%%%%%%%%%%.............................................................................................@@=.-: .........................................................E
266
+
\\ W............................... @:::::::::::::::::::::::::::.::::::::::::::::::::::::::::::::::::::. @@@:@-:+= ................................................ @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%#.......*%##%%%%%%%#%...........................................................................................- @.@ .........................................................E
267
+
\\ W.............................. @::::::::.:@@::@@:@@+@@.@@@:@*:@@*:@@*:::::::::::::::::::::::::::::::::@ ............................................. . @%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%#%%#............%%%%%%%%%%..............................@@.+@-@.@.@@@................................................@ .....................................................E
268
+
\\ W............................ -=:::::::::::@@*:@@*@@+@::@:@:@*@@=@:@@@::::::::::::::::::::::::::::::@@@ ..................................................... @%%%%%%%%%#%%%%%%@-@@%%%#%%%%%%%%%%%%%%%%%%%%##%#.........%#%%%%%%%%%=...........................@@=.@@..@.@.@...............................................@ *@@ ....................................................E
269
+
\\ W........................... @:::::::::::::@=@:@@@%@+@@.@@@:@*@@:::@.@:::::::::::::::::::::@@# :%@ ...................................................... @%%%%%%%%%%%%%@@@@@- *@%#@- .@%%%%%%%%%%%%%%%%%%%%%%%%%%%%%........#%%%%%%%%%%..........................*@@@.:*@.@.@+@..............................................@ =....# ....................................................E
270
+
\\ W.......................... @:::::::::::::+@@@:@=@%@+@::@.@:@*#@=@:@@@:::::::::::::::::::.@ ........................................................ @%%%%%%%%%%%%%%@ #@%%@@ .@%%%%%%%%%%%%%%%#%%%%%%%%%%..........%%%%%%%#%=.........................@@=@.@@@.@.@.@............................................@ . =..++ .....................................................E
271
+
\\ W......................... @::::::::::::::-=:=:=:--=:==:=:=:=::*@::=:=::::::::::::::::@# ................................................................. @%%%%%%%%%%%@@- .... .@@%%@* @%%%%@@@***%%%#......................%%%%%%%%%..................................................................................% ... .....................................................E
272
+
\\ W......................... @.:::::::::::::::::::::::::::::::::::::::::::::::::::::::-% ................................................................... @%%%%%%%%%%%@ ....... @%@ @%%#@ -...............................#%%%%#.........................................................................% #*@@....+ ..... ..@ .....................................................E
273
+
\\ W......................... @::::::::::::::::::::::::::::::::::::::::::::::::::::::@ ....................................................................... @%%%%%%%%%@: @%%%%@ :%%% .@.............................................................................................................@ @....@ ... ...@ .....................................................E
274
+
\\ W.......................... @:::::::::::::::::::::::::::::::::::::::::::::::::::::- .......................................................................... ==-@@@@@ .@@+--*#####@# @%@ @@@ @................................................................................................................= @... +...@ .....................................................E
275
+
\\ W.......................... @::::::::::::::::::::::::::::::::::::::::::::::::::::.@ ........................................................................... %@@@@@@=-============@ . @+.=.....................................................................................................@ #..@ @@%....@ .....................................................E
276
+
\\ W.......................... =@::::::::::::::::::::::::::::::::::::::::::::::::::@ ........................................................................... @=====================@ .. ...... @......................................................................................................@ +- @.....@ .....................................................E
277
+
\\ W........................... *::::::::::::::::::::::::::::::::::::::::::::::@= ............................................................................ @-======================-@+.. ... @.......................................................................................................@ .@...%*+ .......................................................E
278
+
\\ W............................ *:::::::::::::::::::::::::::::::::::::::::::@ ............................................................................. :*-============================-@:. +====-@@@@@@@@* %.........................................................................................................@ .. @..-@ @@ .........................................................E
279
+
\\ W............................ @:::@@::::::::::::::::::::::::::::-*=:::::=. ............................................................................... @==================================--===============-@#..........................................................................................................- .. *@ @.@ .........................................................E
280
+
\\ W............................. @:@ @:::::::::::::::::::.@...... #@::: ............................................................................. ##=====================================================@ *................- #.....................................................................................= ... :.. .........................................................E
281
+
\\ W.............................. @:@ @::::::::::::::::@ :-:: ............................................................................ @@-======================================================% -...............@ @..................................................................................+. ... ............................................................E
282
+
\\ W.............................. @::@ %::::::::::::::+ ........... @.: .......................................................................... @=========================================================-@ %.................@# @.:@@:...........................................................................@ ...... ..............................................................E
283
+
\\ W.............................. @:.- *-:::::::::::.@ ............ .@ ......................................................................... @============================================================-= *+.................@ . ==@@......................................................................:. .........................................................................E
284
+
\\ W............................... =@:+% @::::::::::.@ ........... ........................................................................ @-============================================================-@. @................:%%:..:@ :+-...........................................................% .........................................................................E
285
+
\\ W................................ =.@ -@::::::::.% ..... -@::::*@@ .................................................................... #-===============================================================:@ @:........................+ .. %........................................................@% ...........................................................................E
286
+
\\ W................................. =::::::::.@ ... #@ @: -@:::@@: ............................................................. @=================================================================% @........................@ ...... @ .....................-@@-@.........................** ............................................................................E
287
+
\\ W....................................... @:::::::::.@ #::% *:::=: ........................................................... *-=================================================================- @.......................@ ........ ...................@ @..................@ -@@ ....... ....................................................................E
288
+
\\ W....................................... +@.:::::::::%@@@@@@:::@ ..... @::::@ .......................................................... @-=================================================================@. @....................@ ............ :@...............@ . :-...............- ........ ..................................................................E
289
+
\\ W........................................ -@::::::::::::::::@ ....... .-+@@-: ........................................................... :==================================================================-@ @.................@ ............. @..............@ .... *................+@ ......... @..@ ..................................................................E
290
+
\\ W.......................................... @@**+:.::::::::@ ......... ........................................................... @====================================================================-+ @.............@+ ............... :*..........*@ ....... +.................@ ........ +:..- .................................................................E
291
+
\\ W............................................. *%::::::::::@ ............................................................................. +-=====================================================================*@ #........-#@ .................. #........** ........ @..............- ....... @:..@# ................................................................E
292
+
\\ W.................................................... -@::::::::% ......... .............................................................. +-========================================================================% @...:@@ ...................... @.......@ ............. =.............+ ....... @@..# ...............................................................E
293
+
\\ W........................................................ @+::::@ ...... :+: ....................................................... @-========================================================================-@ =%- ....................... @......* ............... ..............@ ......... *@@ ...............................................................E
294
+
\\ W.......................................................... @:::@ ... @@%####@. ................................................... %+===================================@@=-@@=@@@-@@-@@@-@@@=================. .#@+-=@ ........................ @.....: ................ ..= @.......-= ........... ................................................................E
295
+
\\ W............................................................. -::+@ ###################@ ................................................... #-================================-@%%-@--@=@+@@=@-@-@+@-=========================@ .......................... @...@- ................ ..@ @...@@ ..................................................................................E
296
+
\\ W............................................................. -@@:+@@@#######################@ ................................................... *-================================@=@-@=-@=@+@@=@-=-@:@=========================@ ........................... @.% ................ ...@ @@: ........ ........................................................................E
297
+
\\ W............................................................... @%########################@ ............................................... --==============================*@*@-@--@=@+@@-@-@-@+@========================-* ............................. @+ ............. @...@ ......... : ......................................................................E
298
+
\\ W................................................................... =###########################@@* .............................................. @-================---========================================================@ ............................... *.- ........... %%...@ ......... #.@ ....................................................................E
299
+
\\ W....................................................................... .##############################+@ ............................................. @%=---=-*+--%@ @-====================================================-@ .................................. ........... @.@ @...@ ....... @#....-: ....................................................................E
300
+
\\ W...................................................................... @################################=@ ............................................... @-==============================================-@ .................................... .............. @..@ @..@ ... =@.....@ .. ..............................................................E
301
+
\\ W..................................................................... @##################################@ ................................................................... +============================================--* ......................................................... *...@ @.@ @........@ .............................................................E
302
+
\\ W.................................................................... @####################################@ .................................................................. *===========================================%# ............................................................ @..#@@ @............@ .++#@.@ .... ....................................................E
303
+
\\ W................................................................... @######################################@+ ............................................................. @==========================================-@ ............................................................... +....@ @..........@# *.....% .. ............................................E
304
+
\\ W................................................................... @#############################################%@+ ......................................................... @-========================================-@ .................................................................. @-...@- @.........+: @...*@ .. @---+: .......................................E
305
+
\\ W................................................................... @##################################################@@ ...................................................... =-=====================================-@ .................................................................... @....% @........@ @....# .... #+--+##---=@% .....................................E
306
+
\\ W................................................................... %##############%@@@#@@@#@%@%@@@#@%@@#################@. ..................................................... @-===================================-@ ........................................................................ %@..@ @@@@#@ .....+ ..... @+=-----------+@ ..................................E
307
+
\\ W................................................................... @###############@@###@#@#@%@%#@%#@@@@####################@= .................................................... @-==================================@ .......................................................................... @..@ .:@@@@ ...... @------------#@ .................................E
308
+
\\ W................................................................... +%################@@#@#@#@%@%#@%#@@@@#####################* ..................................................... :-=================================@ ............................................................................. *...:@@@@# ........... #-----------@ ................................E
309
+
\\ W.................................................................... @#############%@%@#@#@#@%@##@%#@%@@#####################@ ...................................................... @=================================@ .............................................................................. @........@# ...................... @------------@ ...............................E
310
+
\\ W..................................................................... #######################################################. ....................................................... @=================================@ .............................................................................. @@%.++@ ............... .. :+:@---@ @--*% ..............................E
311
+
\\ W..................................................................... @############@@#%@%%@%@@@#@@@#@#%@@%#@@##############% ........................................................ .*=================================== .................................................................................. .............. @%%@@ .............................E
312
+
\\ W....................................................................... @###########@@#%@@@@%@@##@#@#@#@%@@#@@@#############* ........................................................ %=================================-# .... ................................................................................................. %=@@@@= . :@ .............................E
313
+
\\ W........................................................................ @#########%@@@%@@@@%@@@%@@@#@#@%###@%@###########@ ........................................................ @-=================================% ... :*@ ........................................................................................ @------+ . -=-@ .... ...............................E
314
+
\\ W......................................................................... @########@@@@%@@@@%@@#%@#@#@#@%@@%@@@########### ......................................................... @===================================% @-=+ ....................................................................................... #@@@@%@--------: @--+ .....................................E
315
+
\\ W.......................................................................... @############################%#################+ ......................................................... ===================================-@ =@@===+ .................................................................................... %=--------------= .=---=+ ....................................E
316
+
\\ W........................................................................... @@#########################################** ......................................................... ==================================-@ @-======@ ................................................................................... %+------------------*@= @-----# ....................................E
317
+
\\ W.............................................................................. @#######################################@ ......................................................... +===============================-@ =======% ................................................................................. .@-----------------------------# ....................................E
318
+
\\ W................................................................................. =#######################################@ ......................................................... -+============================-@ . @=====+ ................................................................................ :@--------------------------------%* ..................................E
319
+
\\ W................................................................................. @#####################################@ ........................................................... --==========================@ .. @====-@ .............................................................................. @--------------------------------------@ .................................E
320
+
\\ W.................................................................................. @#####################################@ ............................................................. %==========================@ ... @-=====@ ............................................................................. @-----------------------------------------% ................................E
321
+
\\ W.................................................................................. @*#################################%@ .............................................................. %=========================@ ... +-====* ............................................................................. @-----+@@-@*@@-@@@*@@@*@@#-@@=-@--@-*@@-----=@ ...............................E
322
+
\\ W.................................................................................. :*############################+@* ................................................................ %========================-@ ... @-==-@ .............................................................................. *-----@+@-@*@@+@---#@-*@*@-@*#-@--@-@+@-------* ..............................E
323
+
\\ W.................................................................................. :*###########################@ ..................................................................... @-=======================* ..... @@-@ .............................................................................. *-----@-@-@*@@--@@-#@-*@%@-@=@-@--@-@-@-------@ ..............................E
324
+
\\ W.................................................................................. :*########################### ........................................................................ =+=====================@ ....... ............................................................................... @-----@#@+@#@@+@-@-#@-*@+@*@%@-@--@-@#@+------@ ..............................E
325
+
\\ W.................................................................................. :############################@ ........................................................................ #===================*. ................................................................................................ @---------------------------------------------= ...............................E
326
+
\\ W.................................................................................. %##########################@ ......................................................................... +================-@ ................................................................................................ =--------------------------------------------# ...............................E
327
+
\\ W.................................................................................. @#########################@ ........................................................................... @================@ .................................................................................................. @-------------------------------------------@ ................................E
328
+
\\ W................................................................................... @#######################%* ............................................................................. @-===========-@: .................................................................................................... *----------------------------------------+@ .................................E
329
+
\\ W................................................................................... @#######################* ............................................................................... *:=========-@* ...................................................................................................... *----% @--------------------@ .................. ..........E
330
+
\\ W................................................................................... @#####################%= ................................................................................ .@-=-#%%%%@ ........................................................................................................ :*... @------------------@ .................. @@ ..........E
331
+
\\ W................................................................................... @####################@ ................................................................................. ........................................................................................................... ........... @--------------@ .................... @--@ .........E
332
+
\\ W................................................................................... %#################### ..................................................................................... ...................................................................................................................................... @-----------=. ..................... @---@: .......E
333
+
\\ W................................................................................... @##################% ................................................................................................................................................................................................................................... .%=---------@ .................... @#-----@ .......E
334
+
\\ W.................................................................................... @##############%@- .................................................................................................................................................................................................................................... @@@ .................. ==----=@ .......E
335
+
\\ W.................................................................................... @#############@ ......................................................................................................................................................................................................................................... ................... @+----@ .........E
336
+
\\ W..................................................................................... @############ .................................................................................................................................................................................................................................................................... %@----*@: .............E
337
+
\\ W...................................................................................... %##########= ................................................................................................................................................................................................................................................................. @-----@ ...............E
338
+
\\ W...................................................................................... @#########@ ................................................................................................................................................................................................................................................................. =@-----#@# ..................E
339
+
\\ W....................................................................................... %#######@ ................................................................................................................................................................................................................................................................. %--+@#% ....................E
340
+
\\ W....................................................................................... @#######@@. ................................................................................................................................................................................................................................................................. ........................E
341
+
\\ W........................................................................................ @########@ .................................................................................................................................................................................................................................................................. .............................E
342
+
\\ W......................................................................................... @#####%. ....................................................................................................................................................................................................................................................................................................E
343
+
\\ W.......................................................................................... @#####+ ....................................................................................................................................................................................................................................................................................................E
344
+
\\ W........................................................................................... @#####@ .................................................................................................................................................................................................................................................................................................E
345
+
\\ W............................................................................................ +@@@##%@# ...............................................................................................................................................................................................................................................................................................E
346
+
\\ W............................................................................................. +@@@@@ ...............................................................................................................................................................................................................................................................................................E
347
+
\\ W................................................................................................. ................................................................................................................................................................................................................................................................................................E
348
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
349
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
350
+
\\ W...........................................................................................................................................................................................................................................................................................................................................................................................................E
351
+
\\ +SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS+
352
+
\\
353
+
;
354
+
const lg_map_width: u16 = mapWidth: {
355
+
@setEvalBranchQuota(100_000);
356
+
break :mapWidth @intCast(mem.indexOfScalar(u8, lg_world_map, '\n').?);
357
+
};
358
+
const lg_map_height: u16 = mapHeight: {
359
+
@setEvalBranchQuota(100_000);
360
+
break :mapHeight @intCast(mem.count(u8, lg_world_map, "\n"));
361
+
};
+14
-16
examples/vt.zig
+14
-16
examples/vt.zig
···
20
}
21
const alloc = gpa.allocator();
22
23
-
var tty = try vaxis.Tty.init();
24
var vx = try vaxis.init(alloc, .{});
25
-
defer vx.deinit(alloc, tty.anyWriter());
26
27
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
28
try loop.init();
···
30
try loop.start();
31
defer loop.stop();
32
33
-
var buffered = tty.bufferedWriter();
34
-
35
-
try vx.enterAltScreen(tty.anyWriter());
36
-
try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s);
37
var env = try std.process.getEnvMap(alloc);
38
defer env.deinit();
39
···
49
};
50
const shell = env.get("SHELL") orelse "bash";
51
const argv = [_][]const u8{shell};
52
var vt = try vaxis.widgets.Terminal.init(
53
alloc,
54
&argv,
55
&env,
56
-
&vx.unicode,
57
vt_opts,
58
);
59
defer vt.deinit();
60
try vt.spawn();
61
62
var redraw: bool = false;
63
while (true) {
64
-
std.time.sleep(8 * std.time.ns_per_ms);
65
// try vt events first
66
while (vt.tryEvent()) |event| {
67
redraw = true;
···
80
if (key.matches('c', .{ .ctrl = true })) return;
81
try vt.update(.{ .key_press = key });
82
},
83
-
.winsize => |ws| {
84
-
try vx.resize(alloc, tty.anyWriter(), ws);
85
-
},
86
}
87
}
88
if (!redraw) continue;
···
94
const child = win.child(.{
95
.x_off = 4,
96
.y_off = 2,
97
-
.width = .{ .limit = win.width - 8 },
98
-
.height = .{ .limit = win.width - 6 },
99
.border = .{
100
.where = .all,
101
},
···
107
.x_pixel = 0,
108
.y_pixel = 0,
109
});
110
-
try vt.draw(child);
111
112
-
try vx.render(buffered.writer().any());
113
-
try buffered.flush();
114
}
115
}
···
20
}
21
const alloc = gpa.allocator();
22
23
+
var buffer: [1024]u8 = undefined;
24
+
var tty = try vaxis.Tty.init(&buffer);
25
+
const writer = tty.writer();
26
var vx = try vaxis.init(alloc, .{});
27
+
defer vx.deinit(alloc, writer);
28
29
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
30
try loop.init();
···
32
try loop.start();
33
defer loop.stop();
34
35
+
try vx.enterAltScreen(writer);
36
+
try vx.queryTerminal(writer, 1 * std.time.ns_per_s);
37
var env = try std.process.getEnvMap(alloc);
38
defer env.deinit();
39
···
49
};
50
const shell = env.get("SHELL") orelse "bash";
51
const argv = [_][]const u8{shell};
52
+
var write_buf: [4096]u8 = undefined;
53
var vt = try vaxis.widgets.Terminal.init(
54
alloc,
55
&argv,
56
&env,
57
vt_opts,
58
+
&write_buf,
59
);
60
defer vt.deinit();
61
try vt.spawn();
62
63
var redraw: bool = false;
64
while (true) {
65
+
std.Thread.sleep(8 * std.time.ns_per_ms);
66
// try vt events first
67
while (vt.tryEvent()) |event| {
68
redraw = true;
···
81
if (key.matches('c', .{ .ctrl = true })) return;
82
try vt.update(.{ .key_press = key });
83
},
84
+
.winsize => |ws| try vx.resize(alloc, writer, ws),
85
}
86
}
87
if (!redraw) continue;
···
93
const child = win.child(.{
94
.x_off = 4,
95
.y_off = 2,
96
+
.width = 120,
97
+
.height = 40,
98
.border = .{
99
.where = .all,
100
},
···
106
.x_pixel = 0,
107
.y_pixel = 0,
108
});
109
+
try vt.draw(alloc, child);
110
111
+
try vx.render(writer);
112
}
113
}
-127
examples/xev.zig
-127
examples/xev.zig
···
1
-
const std = @import("std");
2
-
const vaxis = @import("vaxis");
3
-
const xev = @import("xev");
4
-
const Cell = vaxis.Cell;
5
-
6
-
pub const panic = vaxis.panic_handler;
7
-
8
-
const App = struct {
9
-
const lower_limit: u8 = 30;
10
-
const next_ms: u64 = 8;
11
-
12
-
allocator: std.mem.Allocator,
13
-
vx: *vaxis.Vaxis,
14
-
buffered_writer: std.io.BufferedWriter(4096, std.io.AnyWriter),
15
-
color_idx: u8,
16
-
dir: enum {
17
-
up,
18
-
down,
19
-
},
20
-
21
-
fn draw(self: *App) !void {
22
-
const style: vaxis.Style = .{ .fg = .{ .rgb = [_]u8{ self.color_idx, self.color_idx, self.color_idx } } };
23
-
24
-
const segment: vaxis.Segment = .{
25
-
.text = vaxis.logo,
26
-
.style = style,
27
-
};
28
-
const win = self.vx.window();
29
-
win.clear();
30
-
const center = vaxis.widgets.alignment.center(win, 28, 4);
31
-
_ = try center.printSegment(segment, .{ .wrap = .grapheme });
32
-
switch (self.dir) {
33
-
.up => {
34
-
self.color_idx += 1;
35
-
if (self.color_idx == 255) self.dir = .down;
36
-
},
37
-
.down => {
38
-
self.color_idx -= 1;
39
-
if (self.color_idx == lower_limit) self.dir = .up;
40
-
},
41
-
}
42
-
try self.vx.render(self.buffered_writer.writer().any());
43
-
try self.buffered_writer.flush();
44
-
}
45
-
};
46
-
47
-
pub fn main() !void {
48
-
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
49
-
defer {
50
-
const deinit_status = gpa.deinit();
51
-
//fail test; can't try in defer as defer is executed after we return
52
-
if (deinit_status == .leak) {
53
-
std.log.err("memory leak", .{});
54
-
}
55
-
}
56
-
const alloc = gpa.allocator();
57
-
58
-
var tty = try vaxis.Tty.init();
59
-
defer tty.deinit();
60
-
61
-
var vx = try vaxis.init(alloc, .{});
62
-
defer vx.deinit(alloc, tty.anyWriter());
63
-
64
-
var pool = xev.ThreadPool.init(.{});
65
-
var loop = try xev.Loop.init(.{
66
-
.thread_pool = &pool,
67
-
});
68
-
defer loop.deinit();
69
-
70
-
var app: App = .{
71
-
.allocator = alloc,
72
-
.buffered_writer = tty.bufferedWriter(),
73
-
.color_idx = App.lower_limit,
74
-
.dir = .up,
75
-
.vx = &vx,
76
-
};
77
-
78
-
var vx_loop: vaxis.xev.TtyWatcher(App) = undefined;
79
-
try vx_loop.init(&tty, &vx, &loop, &app, eventCallback);
80
-
81
-
try vx.enterAltScreen(tty.anyWriter());
82
-
// send queries asynchronously
83
-
try vx.queryTerminalSend(tty.anyWriter());
84
-
85
-
const timer = try xev.Timer.init();
86
-
var timer_cmp: xev.Completion = .{};
87
-
timer.run(&loop, &timer_cmp, App.next_ms, App, &app, timerCallback);
88
-
89
-
try loop.run(.until_done);
90
-
}
91
-
92
-
fn eventCallback(
93
-
ud: ?*App,
94
-
loop: *xev.Loop,
95
-
watcher: *vaxis.xev.TtyWatcher(App),
96
-
event: vaxis.xev.Event,
97
-
) xev.CallbackAction {
98
-
const app = ud orelse unreachable;
99
-
switch (event) {
100
-
.key_press => |key| {
101
-
if (key.matches('c', .{ .ctrl = true })) {
102
-
loop.stop();
103
-
return .disarm;
104
-
}
105
-
},
106
-
.winsize => |ws| watcher.vx.resize(app.allocator, watcher.tty.anyWriter(), ws) catch @panic("TODO"),
107
-
else => {},
108
-
}
109
-
return .rearm;
110
-
}
111
-
112
-
fn timerCallback(
113
-
ud: ?*App,
114
-
l: *xev.Loop,
115
-
c: *xev.Completion,
116
-
r: xev.Timer.RunError!void,
117
-
) xev.CallbackAction {
118
-
_ = r catch @panic("timer error");
119
-
120
-
var app = ud orelse return .disarm;
121
-
app.draw() catch @panic("couldn't draw");
122
-
123
-
const timer = try xev.Timer.init();
124
-
timer.run(l, c, App.next_ms, App, ud, timerCallback);
125
-
126
-
return .disarm;
127
-
}
···
+22
-4
src/Cell.zig
+22
-4
src/Cell.zig
···
9
/// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if
10
/// it should rely on the terminal's autowrap feature which can help with primary screen resizes
11
wrapped: bool = false,
12
13
/// Segment is a contiguous run of text that has a constant style
14
pub const Segment = struct {
···
23
/// will measure the same width. This can be ensure by using the gwidth method
24
/// included in libvaxis. If width is 0, libvaxis will measure the glyph at
25
/// render time
26
-
width: usize = 1,
27
};
28
29
pub const CursorShape = enum {
···
40
uri: []const u8 = "",
41
/// ie "id=app-1234"
42
params: []const u8 = "",
43
};
44
45
pub const Style = struct {
···
93
.invisible = b.invisible,
94
.strikethrough = b.strikethrough,
95
};
96
-
const a_cast: u7 = @bitCast(a_sgr);
97
-
const b_cast: u7 = @bitCast(b_sgr);
98
-
return a_cast == b_cast and
99
Color.eql(a.fg, b.fg) and
100
Color.eql(a.bg, b.bg) and
101
Color.eql(a.ul, b.ul) and
···
9
/// Set to true if this cell is the last cell printed in a row before wrap. Vaxis will determine if
10
/// it should rely on the terminal's autowrap feature which can help with primary screen resizes
11
wrapped: bool = false,
12
+
scale: Scale = .{},
13
14
/// Segment is a contiguous run of text that has a constant style
15
pub const Segment = struct {
···
24
/// will measure the same width. This can be ensure by using the gwidth method
25
/// included in libvaxis. If width is 0, libvaxis will measure the glyph at
26
/// render time
27
+
width: u8 = 1,
28
};
29
30
pub const CursorShape = enum {
···
41
uri: []const u8 = "",
42
/// ie "id=app-1234"
43
params: []const u8 = "",
44
+
};
45
+
46
+
pub const Scale = packed struct {
47
+
scale: u3 = 1,
48
+
// The spec allows up to 15, but we limit to 7
49
+
numerator: u4 = 1,
50
+
// The spec allows up to 15, but we limit to 7
51
+
denominator: u4 = 1,
52
+
vertical_alignment: enum(u2) {
53
+
top = 0,
54
+
bottom = 1,
55
+
center = 2,
56
+
} = .top,
57
+
58
+
pub fn eql(self: Scale, other: Scale) bool {
59
+
const a_scale: u13 = @bitCast(self);
60
+
const b_scale: u13 = @bitCast(other);
61
+
return a_scale == b_scale;
62
+
}
63
};
64
65
pub const Style = struct {
···
113
.invisible = b.invisible,
114
.strikethrough = b.strikethrough,
115
};
116
+
return a_sgr == b_sgr and
117
Color.eql(a.fg, b.fg) and
118
Color.eql(a.bg, b.bg) and
119
Color.eql(a.ul, b.ul) and
+23
-17
src/Image.zig
+23
-17
src/Image.zig
···
21
png,
22
};
23
24
pub const Placement = struct {
25
img_id: u32,
26
options: Image.DrawOptions,
27
};
28
29
pub const CellSize = struct {
30
-
rows: usize,
31
-
cols: usize,
32
};
33
34
pub const DrawOptions = struct {
···
36
/// origin of the image. These must be less than the pixel size of a single
37
/// cell
38
pixel_offset: ?struct {
39
-
x: usize,
40
-
y: usize,
41
} = null,
42
/// the vertical stacking order
43
/// < 0: Drawn beneath text
···
45
z_index: ?i32 = null,
46
/// A clip region of the source image to draw.
47
clip_region: ?struct {
48
-
x: ?usize = null,
49
-
y: ?usize = null,
50
-
width: ?usize = null,
51
-
height: ?usize = null,
52
} = null,
53
/// Scaling to apply to the Image
54
scale: enum {
···
65
/// field, and should prefer to use scale. `draw` will fill in this field with
66
/// the correct values if a scale method is applied.
67
size: ?struct {
68
-
rows: ?usize = null,
69
-
cols: ?usize = null,
70
} = null,
71
};
72
···
74
id: u32,
75
76
/// width in pixels
77
-
width: usize,
78
/// height in pixels
79
-
height: usize,
80
81
pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
82
var p_opts = opts;
···
115
.rows = win.height,
116
}
117
118
-
// Does the image require horizontal scaling?
119
else if (!fit_x and fit_y)
120
p_opts.size = .{
121
.cols = win.width,
···
170
const w = win.screen.width;
171
const h = win.screen.height;
172
173
-
const pix_per_col = try std.math.divCeil(usize, x_pix, w);
174
-
const pix_per_row = try std.math.divCeil(usize, y_pix, h);
175
176
-
const cell_width = std.math.divCeil(usize, self.width, pix_per_col) catch 0;
177
-
const cell_height = std.math.divCeil(usize, self.height, pix_per_row) catch 0;
178
return .{
179
.rows = cell_height,
180
.cols = cell_width,
···
21
png,
22
};
23
24
+
pub const TransmitMedium = enum {
25
+
file,
26
+
temp_file,
27
+
shared_mem,
28
+
};
29
+
30
pub const Placement = struct {
31
img_id: u32,
32
options: Image.DrawOptions,
33
};
34
35
pub const CellSize = struct {
36
+
rows: u16,
37
+
cols: u16,
38
};
39
40
pub const DrawOptions = struct {
···
42
/// origin of the image. These must be less than the pixel size of a single
43
/// cell
44
pixel_offset: ?struct {
45
+
x: u16,
46
+
y: u16,
47
} = null,
48
/// the vertical stacking order
49
/// < 0: Drawn beneath text
···
51
z_index: ?i32 = null,
52
/// A clip region of the source image to draw.
53
clip_region: ?struct {
54
+
x: ?u16 = null,
55
+
y: ?u16 = null,
56
+
width: ?u16 = null,
57
+
height: ?u16 = null,
58
} = null,
59
/// Scaling to apply to the Image
60
scale: enum {
···
71
/// field, and should prefer to use scale. `draw` will fill in this field with
72
/// the correct values if a scale method is applied.
73
size: ?struct {
74
+
rows: ?u16 = null,
75
+
cols: ?u16 = null,
76
} = null,
77
};
78
···
80
id: u32,
81
82
/// width in pixels
83
+
width: u16,
84
/// height in pixels
85
+
height: u16,
86
87
pub fn draw(self: Image, win: Window, opts: DrawOptions) !void {
88
var p_opts = opts;
···
121
.rows = win.height,
122
}
123
124
+
// Does the image require horizontal scaling?
125
else if (!fit_x and fit_y)
126
p_opts.size = .{
127
.cols = win.width,
···
176
const w = win.screen.width;
177
const h = win.screen.height;
178
179
+
const pix_per_col = try std.math.divCeil(u16, x_pix, w);
180
+
const pix_per_row = try std.math.divCeil(u16, y_pix, h);
181
182
+
const cell_width = std.math.divCeil(u16, self.width, pix_per_col) catch 0;
183
+
const cell_height = std.math.divCeil(u16, self.height, pix_per_row) catch 0;
184
return .{
185
.rows = cell_height,
186
.cols = cell_width,
+66
-42
src/InternalScreen.zig
+66
-42
src/InternalScreen.zig
···
10
const InternalScreen = @This();
11
12
pub const InternalCell = struct {
13
-
char: std.ArrayList(u8) = undefined,
14
style: Style = .{},
15
-
uri: std.ArrayList(u8) = undefined,
16
-
uri_id: std.ArrayList(u8) = undefined,
17
// if we got skipped because of a wide character
18
skipped: bool = false,
19
default: bool = true,
20
21
pub fn eql(self: InternalCell, cell: Cell) bool {
22
// fastpath when both cells are default
23
if (self.default and cell.default) return true;
24
-
// this is actually faster than std.meta.eql on the individual items.
25
-
// Our strings are always small, usually less than 4 bytes so the simd
26
-
// usage in std.mem.eql has too much overhead vs looping the bytes
27
-
if (!std.mem.eql(u8, self.char.items, cell.char.grapheme)) return false;
28
-
if (!Style.eql(self.style, cell.style)) return false;
29
-
if (!std.mem.eql(u8, self.uri.items, cell.link.uri)) return false;
30
-
if (!std.mem.eql(u8, self.uri_id.items, cell.link.params)) return false;
31
-
return true;
32
}
33
};
34
35
-
width: usize = 0,
36
-
height: usize = 0,
37
38
-
buf: []InternalCell = undefined,
39
40
-
cursor_row: usize = 0,
41
-
cursor_col: usize = 0,
42
cursor_vis: bool = false,
43
cursor_shape: CursorShape = .default,
44
45
mouse_shape: MouseShape = .default,
46
47
/// sets each cell to the default cell
48
-
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !InternalScreen {
49
var screen = InternalScreen{
50
-
.buf = try alloc.alloc(InternalCell, w * h),
51
};
52
for (screen.buf, 0..) |_, i| {
53
screen.buf[i] = .{
54
-
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
55
-
.uri = std.ArrayList(u8).init(alloc),
56
-
.uri_id = std.ArrayList(u8).init(alloc),
57
};
58
-
try screen.buf[i].char.append(' ');
59
}
60
screen.width = w;
61
screen.height = h;
···
63
}
64
65
pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
66
-
for (self.buf, 0..) |_, i| {
67
-
self.buf[i].char.deinit();
68
-
self.buf[i].uri.deinit();
69
-
self.buf[i].uri_id.deinit();
70
-
}
71
-
72
-
alloc.free(self.buf);
73
}
74
75
/// writes a cell to a location. 0 indexed
76
pub fn writeCell(
77
self: *InternalScreen,
78
-
col: usize,
79
-
row: usize,
80
cell: Cell,
81
) void {
82
-
if (self.width < col) {
83
// column out of bounds
84
return;
85
}
86
-
if (self.height < row) {
87
// height out of bounds
88
return;
89
}
90
-
const i = (row * self.width) + col;
91
assert(i < self.buf.len);
92
self.buf[i].char.clearRetainingCapacity();
93
-
self.buf[i].char.appendSlice(cell.char.grapheme) catch {
94
log.warn("couldn't write grapheme", .{});
95
};
96
self.buf[i].uri.clearRetainingCapacity();
97
-
self.buf[i].uri.appendSlice(cell.link.uri) catch {
98
log.warn("couldn't write uri", .{});
99
};
100
self.buf[i].uri_id.clearRetainingCapacity();
101
-
self.buf[i].uri_id.appendSlice(cell.link.params) catch {
102
log.warn("couldn't write uri_id", .{});
103
};
104
self.buf[i].style = cell.style;
105
self.buf[i].default = cell.default;
106
}
107
108
-
pub fn readCell(self: *InternalScreen, col: usize, row: usize) ?Cell {
109
-
if (self.width < col) {
110
// column out of bounds
111
return null;
112
}
113
-
if (self.height < row) {
114
// height out of bounds
115
return null;
116
}
117
const i = (row * self.width) + col;
118
assert(i < self.buf.len);
119
return .{
120
-
.char = .{ .grapheme = self.buf[i].char.items },
121
-
.style = self.buf[i].style,
122
};
123
}
···
10
const InternalScreen = @This();
11
12
pub const InternalCell = struct {
13
+
char: std.ArrayListUnmanaged(u8) = .empty,
14
style: Style = .{},
15
+
uri: std.ArrayListUnmanaged(u8) = .empty,
16
+
uri_id: std.ArrayListUnmanaged(u8) = .empty,
17
// if we got skipped because of a wide character
18
skipped: bool = false,
19
default: bool = true,
20
21
+
// If we should skip rendering *this* round due to being printed over previously (from a scaled
22
+
// cell, for example)
23
+
skip: bool = false,
24
+
25
+
scale: Cell.Scale = .{},
26
+
27
pub fn eql(self: InternalCell, cell: Cell) bool {
28
+
29
// fastpath when both cells are default
30
if (self.default and cell.default) return true;
31
+
32
+
return std.mem.eql(u8, self.char.items, cell.char.grapheme) and
33
+
Style.eql(self.style, cell.style) and
34
+
std.mem.eql(u8, self.uri.items, cell.link.uri) and
35
+
std.mem.eql(u8, self.uri_id.items, cell.link.params);
36
}
37
};
38
39
+
arena: *std.heap.ArenaAllocator,
40
+
width: u16 = 0,
41
+
height: u16 = 0,
42
43
+
buf: []InternalCell,
44
45
+
cursor_row: u16 = 0,
46
+
cursor_col: u16 = 0,
47
cursor_vis: bool = false,
48
cursor_shape: CursorShape = .default,
49
50
mouse_shape: MouseShape = .default,
51
52
/// sets each cell to the default cell
53
+
pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !InternalScreen {
54
+
const arena = try alloc.create(std.heap.ArenaAllocator);
55
+
arena.* = .init(alloc);
56
var screen = InternalScreen{
57
+
.arena = arena,
58
+
.buf = try arena.allocator().alloc(InternalCell, @as(usize, @intCast(w)) * h),
59
};
60
for (screen.buf, 0..) |_, i| {
61
screen.buf[i] = .{
62
+
.char = try std.ArrayListUnmanaged(u8).initCapacity(arena.allocator(), 1),
63
+
.uri = .empty,
64
+
.uri_id = .empty,
65
};
66
+
screen.buf[i].char.appendAssumeCapacity(' ');
67
}
68
screen.width = w;
69
screen.height = h;
···
71
}
72
73
pub fn deinit(self: *InternalScreen, alloc: std.mem.Allocator) void {
74
+
self.arena.deinit();
75
+
alloc.destroy(self.arena);
76
+
self.* = undefined;
77
}
78
79
/// writes a cell to a location. 0 indexed
80
pub fn writeCell(
81
self: *InternalScreen,
82
+
col: u16,
83
+
row: u16,
84
cell: Cell,
85
) void {
86
+
if (self.width <= col) {
87
// column out of bounds
88
return;
89
}
90
+
if (self.height <= row) {
91
// height out of bounds
92
return;
93
}
94
+
const i = (@as(usize, @intCast(row)) * self.width) + col;
95
assert(i < self.buf.len);
96
self.buf[i].char.clearRetainingCapacity();
97
+
self.buf[i].char.appendSlice(self.arena.allocator(), cell.char.grapheme) catch {
98
log.warn("couldn't write grapheme", .{});
99
};
100
self.buf[i].uri.clearRetainingCapacity();
101
+
self.buf[i].uri.appendSlice(self.arena.allocator(), cell.link.uri) catch {
102
log.warn("couldn't write uri", .{});
103
};
104
self.buf[i].uri_id.clearRetainingCapacity();
105
+
self.buf[i].uri_id.appendSlice(self.arena.allocator(), cell.link.params) catch {
106
log.warn("couldn't write uri_id", .{});
107
};
108
self.buf[i].style = cell.style;
109
self.buf[i].default = cell.default;
110
}
111
112
+
pub fn readCell(self: *InternalScreen, col: u16, row: u16) ?Cell {
113
+
if (self.width <= col) {
114
// column out of bounds
115
return null;
116
}
117
+
if (self.height <= row) {
118
// height out of bounds
119
return null;
120
}
121
const i = (row * self.width) + col;
122
assert(i < self.buf.len);
123
+
const cell = self.buf[i];
124
return .{
125
+
.char = .{ .grapheme = cell.char.items },
126
+
.style = cell.style,
127
+
.link = .{
128
+
.uri = cell.uri.items,
129
+
.params = cell.uri_id.items,
130
+
},
131
+
.default = cell.default,
132
};
133
}
134
+
135
+
test "InternalScreen: out-of-bounds read/write are ignored" {
136
+
var screen = try InternalScreen.init(std.testing.allocator, 2, 2);
137
+
defer screen.deinit(std.testing.allocator);
138
+
139
+
const sentinel: Cell = .{ .char = .{ .grapheme = "A", .width = 1 } };
140
+
screen.writeCell(0, 1, sentinel);
141
+
142
+
const oob_cell: Cell = .{ .char = .{ .grapheme = "X", .width = 1 } };
143
+
screen.writeCell(2, 0, oob_cell);
144
+
const read_back = screen.readCell(0, 1) orelse return error.TestUnexpectedResult;
145
+
try std.testing.expect(std.mem.eql(u8, read_back.char.grapheme, "A"));
146
+
try std.testing.expect(screen.readCell(2, 0) == null);
147
+
}
+25
-1
src/Key.zig
+25
-1
src/Key.zig
···
13
meta: bool = false,
14
caps_lock: bool = false,
15
num_lock: bool = false,
16
};
17
18
/// Flags for the Kitty Protocol.
···
105
self_mods.shift = false;
106
self_mods.caps_lock = false;
107
var arg_mods = mods;
108
arg_mods.num_lock = false;
109
arg_mods.shift = false;
110
arg_mods.caps_lock = false;
111
112
var buf: [4]u8 = undefined;
113
-
const n = std.unicode.utf8Encode(cp, buf[0..]) catch return false;
114
return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods);
115
}
116
···
274
.{ "comma", ',' },
275
276
// special keys
277
.{ "insert", insert },
278
.{ "delete", delete },
279
.{ "left", left },
···
388
const key: Key = .{
389
.codepoint = 'a',
390
.mods = .{ .num_lock = true },
391
};
392
try testing.expect(key.matches('a', .{}));
393
}
394
395
test "matches 'shift+a'" {
396
const key: Key = .{
397
.codepoint = 'a',
398
.mods = .{ .shift = true },
399
.text = "A",
400
};
401
try testing.expect(key.matches('a', .{ .shift = true }));
402
try testing.expect(key.matches('A', .{}));
403
try testing.expect(!key.matches('A', .{ .ctrl = true }));
404
}
···
13
meta: bool = false,
14
caps_lock: bool = false,
15
num_lock: bool = false,
16
+
17
+
pub fn eql(self: Modifiers, other: Modifiers) bool {
18
+
const a: u8 = @bitCast(self);
19
+
const b: u8 = @bitCast(other);
20
+
return a == b;
21
+
}
22
};
23
24
/// Flags for the Kitty Protocol.
···
111
self_mods.shift = false;
112
self_mods.caps_lock = false;
113
var arg_mods = mods;
114
+
115
+
// TODO: Use zg case_data for full unicode support. We'll need to allocate the case data
116
+
// somewhere
117
+
const _cp: u21 = if (cp < 128 and (mods.shift or mods.caps_lock))
118
+
// Uppercase our codepoint
119
+
std.ascii.toUpper(@intCast(cp))
120
+
else
121
+
cp;
122
+
123
arg_mods.num_lock = false;
124
arg_mods.shift = false;
125
arg_mods.caps_lock = false;
126
127
var buf: [4]u8 = undefined;
128
+
const n = std.unicode.utf8Encode(_cp, &buf) catch return false;
129
return std.mem.eql(u8, self.text.?, buf[0..n]) and std.meta.eql(self_mods, arg_mods);
130
}
131
···
289
.{ "comma", ',' },
290
291
// special keys
292
+
.{ "tab", tab },
293
+
.{ "enter", enter },
294
+
.{ "escape", escape },
295
+
.{ "space", space },
296
+
.{ "backspace", backspace },
297
.{ "insert", insert },
298
.{ "delete", delete },
299
.{ "left", left },
···
408
const key: Key = .{
409
.codepoint = 'a',
410
.mods = .{ .num_lock = true },
411
+
.text = "a",
412
};
413
try testing.expect(key.matches('a', .{}));
414
+
try testing.expect(!key.matches('a', .{ .shift = true }));
415
}
416
417
test "matches 'shift+a'" {
418
const key: Key = .{
419
.codepoint = 'a',
420
+
.shifted_codepoint = 'A',
421
.mods = .{ .shift = true },
422
.text = "A",
423
};
424
try testing.expect(key.matches('a', .{ .shift = true }));
425
+
try testing.expect(!key.matches('a', .{}));
426
try testing.expect(key.matches('A', .{}));
427
try testing.expect(!key.matches('A', .{ .ctrl = true }));
428
}
+113
-17
src/Loop.zig
+113
-17
src/Loop.zig
···
1
const std = @import("std");
2
const builtin = @import("builtin");
3
4
-
const grapheme = @import("grapheme");
5
-
6
const GraphemeCache = @import("GraphemeCache.zig");
7
const Parser = @import("Parser.zig");
8
const Queue = @import("queue.zig").Queue;
···
31
switch (builtin.os.tag) {
32
.windows => {},
33
else => {
34
-
const handler: Tty.SignalHandler = .{
35
-
.context = self,
36
-
.callback = Self.winsizeCallback,
37
-
};
38
-
try Tty.notifyWinsize(handler);
39
},
40
}
41
}
···
45
if (self.thread) |_| return;
46
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
47
self,
48
-
&self.vaxis.unicode.grapheme_data,
49
self.vaxis.opts.system_clipboard_allocator,
50
});
51
}
···
56
if (self.thread == null) return;
57
self.should_quit = true;
58
// trigger a read
59
-
self.vaxis.deviceStatusReport(self.tty.anyWriter()) catch {};
60
61
if (self.thread) |thread| {
62
thread.join();
···
105
/// read input from the tty. This is run in a separate thread
106
fn ttyRun(
107
self: *Self,
108
-
grapheme_data: *const grapheme.GraphemeData,
109
paste_allocator: ?std.mem.Allocator,
110
) !void {
111
// initialize a grapheme cache
112
var cache: GraphemeCache = .{};
113
114
switch (builtin.os.tag) {
115
.windows => {
116
-
var parser: Parser = .{
117
-
.grapheme_data = grapheme_data,
118
-
};
119
while (!self.should_quit) {
120
const event = try self.tty.nextEvent(&parser, paste_allocator);
121
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
···
128
self.postEvent(.{ .winsize = winsize });
129
}
130
131
-
var parser: Parser = .{
132
-
.grapheme_data = grapheme_data,
133
-
};
134
135
// initialize the read buffer
136
var buf: [1024]u8 = undefined;
···
175
}
176
},
177
.key_press => |key| {
178
if (@hasField(Event, "key_press")) {
179
// HACK: yuck. there has to be a better way
180
var mut_key = key;
···
196
},
197
.cap_da1 => {
198
std.Thread.Futex.wake(&vx.query_futex, 10);
199
},
200
-
.mouse => {}, // Unsupported currently
201
else => {},
202
}
203
},
204
else => {
205
switch (event) {
206
.key_press => |key| {
207
if (@hasField(Event, "key_press")) {
208
// HACK: yuck. there has to be a better way
209
var mut_key = key;
···
226
.mouse => |mouse| {
227
if (@hasField(Event, "mouse")) {
228
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
229
}
230
},
231
.focus_in => {
···
293
log.info("color_scheme_updates capability detected", .{});
294
vx.caps.color_scheme_updates = true;
295
},
296
.cap_da1 => {
297
std.Thread.Futex.wake(&vx.query_futex, 10);
298
},
299
.winsize => |winsize| {
300
vx.state.in_band_resize = true;
301
if (@hasField(Event, "winsize")) {
302
return self.postEvent(.{ .winsize = winsize });
303
}
···
306
},
307
}
308
}
···
1
const std = @import("std");
2
const builtin = @import("builtin");
3
4
const GraphemeCache = @import("GraphemeCache.zig");
5
const Parser = @import("Parser.zig");
6
const Queue = @import("queue.zig").Queue;
···
29
switch (builtin.os.tag) {
30
.windows => {},
31
else => {
32
+
if (!builtin.is_test) {
33
+
const handler: Tty.SignalHandler = .{
34
+
.context = self,
35
+
.callback = Self.winsizeCallback,
36
+
};
37
+
try Tty.notifyWinsize(handler);
38
+
}
39
},
40
}
41
}
···
45
if (self.thread) |_| return;
46
self.thread = try std.Thread.spawn(.{}, Self.ttyRun, .{
47
self,
48
self.vaxis.opts.system_clipboard_allocator,
49
});
50
}
···
55
if (self.thread == null) return;
56
self.should_quit = true;
57
// trigger a read
58
+
self.vaxis.deviceStatusReport(self.tty.writer()) catch {};
59
60
if (self.thread) |thread| {
61
thread.join();
···
104
/// read input from the tty. This is run in a separate thread
105
fn ttyRun(
106
self: *Self,
107
paste_allocator: ?std.mem.Allocator,
108
) !void {
109
+
// Return early if we're in test mode to avoid infinite loops
110
+
if (builtin.is_test) return;
111
+
112
// initialize a grapheme cache
113
var cache: GraphemeCache = .{};
114
115
switch (builtin.os.tag) {
116
.windows => {
117
+
var parser: Parser = .{};
118
while (!self.should_quit) {
119
const event = try self.tty.nextEvent(&parser, paste_allocator);
120
try handleEventGeneric(self, self.vaxis, &cache, Event, event, null);
···
127
self.postEvent(.{ .winsize = winsize });
128
}
129
130
+
var parser: Parser = .{};
131
132
// initialize the read buffer
133
var buf: [1024]u8 = undefined;
···
172
}
173
},
174
.key_press => |key| {
175
+
// Check for a cursor position response for our explicit width query. This will
176
+
// always be an F3 key with shift = true, and we must be looking for queries
177
+
if (key.codepoint == vaxis.Key.f3 and
178
+
key.mods.shift and
179
+
!vx.queries_done.load(.unordered))
180
+
{
181
+
log.info("explicit width capability detected", .{});
182
+
vx.caps.explicit_width = true;
183
+
vx.caps.unicode = .unicode;
184
+
vx.screen.width_method = .unicode;
185
+
return;
186
+
}
187
+
// Check for a cursor position response for our scaled text query. This will
188
+
// always be an F3 key with alt = true, and we must be looking for queries
189
+
if (key.codepoint == vaxis.Key.f3 and
190
+
key.mods.alt and
191
+
!vx.queries_done.load(.unordered))
192
+
{
193
+
log.info("scaled text capability detected", .{});
194
+
vx.caps.scaled_text = true;
195
+
return;
196
+
}
197
if (@hasField(Event, "key_press")) {
198
// HACK: yuck. there has to be a better way
199
var mut_key = key;
···
215
},
216
.cap_da1 => {
217
std.Thread.Futex.wake(&vx.query_futex, 10);
218
+
vx.queries_done.store(true, .unordered);
219
},
220
+
.mouse => |mouse| {
221
+
if (@hasField(Event, "mouse")) {
222
+
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
223
+
}
224
+
},
225
+
.focus_in => {
226
+
if (@hasField(Event, "focus_in")) {
227
+
return self.postEvent(.focus_in);
228
+
}
229
+
},
230
+
.focus_out => {
231
+
if (@hasField(Event, "focus_out")) {
232
+
return self.postEvent(.focus_out);
233
+
}
234
+
}, // Unsupported currently
235
else => {},
236
}
237
},
238
else => {
239
switch (event) {
240
.key_press => |key| {
241
+
// Check for a cursor position response for our explicity width query. This will
242
+
// always be an F3 key with shift = true, and we must be looking for queries
243
+
if (key.codepoint == vaxis.Key.f3 and
244
+
key.mods.shift and
245
+
!vx.queries_done.load(.unordered))
246
+
{
247
+
log.info("explicit width capability detected", .{});
248
+
vx.caps.explicit_width = true;
249
+
vx.caps.unicode = .unicode;
250
+
vx.screen.width_method = .unicode;
251
+
return;
252
+
}
253
+
// Check for a cursor position response for our scaled text query. This will
254
+
// always be an F3 key with alt = true, and we must be looking for queries
255
+
if (key.codepoint == vaxis.Key.f3 and
256
+
key.mods.alt and
257
+
!vx.queries_done.load(.unordered))
258
+
{
259
+
log.info("scaled text capability detected", .{});
260
+
vx.caps.scaled_text = true;
261
+
return;
262
+
}
263
if (@hasField(Event, "key_press")) {
264
// HACK: yuck. there has to be a better way
265
var mut_key = key;
···
282
.mouse => |mouse| {
283
if (@hasField(Event, "mouse")) {
284
return self.postEvent(.{ .mouse = vx.translateMouse(mouse) });
285
+
}
286
+
},
287
+
.mouse_leave => {
288
+
if (@hasField(Event, "mouse_leave")) {
289
+
return self.postEvent(.mouse_leave);
290
}
291
},
292
.focus_in => {
···
354
log.info("color_scheme_updates capability detected", .{});
355
vx.caps.color_scheme_updates = true;
356
},
357
+
.cap_multi_cursor => {
358
+
log.info("multi cursor capability detected", .{});
359
+
vx.caps.multi_cursor = true;
360
+
},
361
.cap_da1 => {
362
std.Thread.Futex.wake(&vx.query_futex, 10);
363
+
vx.queries_done.store(true, .unordered);
364
},
365
.winsize => |winsize| {
366
vx.state.in_band_resize = true;
367
+
switch (builtin.os.tag) {
368
+
.windows => {},
369
+
// Reset the signal handler if we are receiving in_band_resize
370
+
else => Tty.resetSignalHandler(),
371
+
}
372
if (@hasField(Event, "winsize")) {
373
return self.postEvent(.{ .winsize = winsize });
374
}
···
377
},
378
}
379
}
380
+
381
+
test Loop {
382
+
const Event = union(enum) {
383
+
key_press: vaxis.Key,
384
+
winsize: vaxis.Winsize,
385
+
focus_in,
386
+
foo: u8,
387
+
};
388
+
389
+
var tty = try vaxis.Tty.init(&.{});
390
+
defer tty.deinit();
391
+
392
+
var vx = try vaxis.init(std.testing.allocator, .{});
393
+
defer vx.deinit(std.testing.allocator, tty.writer());
394
+
395
+
var loop: vaxis.Loop(Event) = .{ .tty = &tty, .vaxis = &vx };
396
+
try loop.init();
397
+
398
+
try loop.start();
399
+
defer loop.stop();
400
+
401
+
// Optionally enter the alternate screen
402
+
try vx.enterAltScreen(tty.writer());
403
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_ms);
404
+
}
+6
-4
src/Mouse.zig
+6
-4
src/Mouse.zig
···
20
none,
21
wheel_up = 64,
22
wheel_down = 65,
23
+
wheel_right = 66,
24
+
wheel_left = 67,
25
button_8 = 128,
26
button_9 = 129,
27
button_10 = 130,
···
41
drag,
42
};
43
44
+
col: i16,
45
+
row: i16,
46
+
xoffset: u16 = 0,
47
+
yoffset: u16 = 0,
48
button: Button,
49
mods: Modifiers,
50
type: Type,
+277
-103
src/Parser.zig
+277
-103
src/Parser.zig
···
4
const Event = @import("event.zig").Event;
5
const Key = @import("Key.zig");
6
const Mouse = @import("Mouse.zig");
7
-
const code_point = @import("code_point");
8
-
const grapheme = @import("grapheme");
9
const Winsize = @import("main.zig").Winsize;
10
11
const log = std.log.scoped(.vaxis_parser);
···
25
const shift: u8 = 0b00000100;
26
const alt: u8 = 0b00001000;
27
const ctrl: u8 = 0b00010000;
28
};
29
30
// the state of the parser
···
44
// a buffer to temporarily store text in. We need this to encode
45
// text-as-codepoints
46
buf: [128]u8 = undefined,
47
-
48
-
grapheme_data: *const grapheme.GraphemeData,
49
50
/// Parse the first event from the input buffer. If a completion event is not
51
/// present, Result.event will be null and Result.n will be 0
···
77
};
78
},
79
}
80
-
} else return parseGround(input, self.grapheme_data);
81
}
82
83
/// Parse ground state
84
-
inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Result {
85
std.debug.assert(input.len > 0);
86
87
const b = input[0];
···
94
0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
95
0x08 => .{ .codepoint = Key.backspace },
96
0x09 => .{ .codepoint = Key.tab },
97
-
0x0A,
98
-
0x0D,
99
-
=> .{ .codepoint = Key.enter },
100
0x01...0x07,
101
0x0B...0x0C,
102
0x0E...0x1A,
···
109
},
110
0x7F => .{ .codepoint = Key.backspace },
111
else => blk: {
112
-
var iter: code_point.Iterator = .{ .bytes = input };
113
// return null if we don't have a valid codepoint
114
-
const cp = iter.next() orelse return error.InvalidUTF8;
115
116
-
n = cp.len;
117
118
// Check if we have a multi-codepoint grapheme
119
-
var code = cp.code;
120
-
var g_state: grapheme.State = .{};
121
-
var prev_cp = code;
122
-
while (iter.next()) |next_cp| {
123
-
if (grapheme.graphemeBreak(prev_cp, next_cp.code, data, &g_state)) {
124
break;
125
}
126
-
prev_cp = next_cp.code;
127
-
code = Key.multicodepoint;
128
-
n += next_cp.len;
129
}
130
131
break :blk .{ .codepoint = code, .text = input[0..n] };
···
468
469
'I' => return .{ .event = .focus_in, .n = sequence.len },
470
'O' => return .{ .event = .focus_out, .n = sequence.len },
471
-
'M', 'm' => return parseMouse(sequence),
472
'c' => {
473
// Primary DA (CSI ? Pm c)
474
std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
···
522
const width_pix = iter.next() orelse "0";
523
524
const winsize: Winsize = .{
525
-
.rows = std.fmt.parseUnsigned(usize, height_char, 10) catch return null_event,
526
-
.cols = std.fmt.parseUnsigned(usize, width_char, 10) catch return null_event,
527
-
.x_pixel = std.fmt.parseUnsigned(usize, width_pix, 10) catch return null_event,
528
-
.y_pixel = std.fmt.parseUnsigned(usize, height_pix, 10) catch return null_event,
529
};
530
return .{
531
.event = .{ .winsize = winsize },
···
593
key.text = text_buf[0..total];
594
}
595
596
const event: Event = if (is_release)
597
.{ .key_release = key }
598
else
···
624
else => return null_event,
625
}
626
},
627
else => return null_event,
628
}
629
}
···
631
/// Parse a param buffer, returning a default value if the param was empty
632
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
633
if (buf.len == 0) return default;
634
-
return std.fmt.parseUnsigned(T, buf, 10) catch return null;
635
}
636
637
/// Parse a mouse event
638
-
inline fn parseMouse(input: []const u8) Result {
639
-
std.debug.assert(input.len >= 4); // ESC [ < [Mm]
640
const null_event: Result = .{ .event = null, .n = input.len };
641
642
-
if (input[2] != '<') return null_event;
643
644
-
const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
645
-
const button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
646
-
const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
647
-
const px = parseParam(u16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
648
-
const py = parseParam(u16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
649
650
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
651
const motion = button_mask & mouse_bits.motion > 0;
···
669
if (motion and button == Mouse.Button.none) {
670
break :blk .motion;
671
}
672
if (input[input.len - 1] == 'm') break :blk .release;
673
break :blk .press;
674
},
675
};
676
-
return .{ .event = .{ .mouse = mouse }, .n = input.len };
677
}
678
679
test "parse: single xterm keypress" {
680
const alloc = testing.allocator_instance.allocator();
681
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
682
-
defer grapheme_data.deinit();
683
const input = "a";
684
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
685
const result = try parser.parse(input, alloc);
686
const expected_key: Key = .{
687
.codepoint = 'a',
···
695
696
test "parse: single xterm keypress backspace" {
697
const alloc = testing.allocator_instance.allocator();
698
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
699
-
defer grapheme_data.deinit();
700
const input = "\x08";
701
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
702
const result = try parser.parse(input, alloc);
703
const expected_key: Key = .{
704
.codepoint = Key.backspace,
···
711
712
test "parse: single xterm keypress with more buffer" {
713
const alloc = testing.allocator_instance.allocator();
714
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
715
-
defer grapheme_data.deinit();
716
const input = "ab";
717
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
718
const result = try parser.parse(input, alloc);
719
const expected_key: Key = .{
720
.codepoint = 'a',
···
729
730
test "parse: xterm escape keypress" {
731
const alloc = testing.allocator_instance.allocator();
732
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
733
-
defer grapheme_data.deinit();
734
const input = "\x1b";
735
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
736
const result = try parser.parse(input, alloc);
737
const expected_key: Key = .{ .codepoint = Key.escape };
738
const expected_event: Event = .{ .key_press = expected_key };
···
743
744
test "parse: xterm ctrl+a" {
745
const alloc = testing.allocator_instance.allocator();
746
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
747
-
defer grapheme_data.deinit();
748
const input = "\x01";
749
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
750
const result = try parser.parse(input, alloc);
751
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
752
const expected_event: Event = .{ .key_press = expected_key };
···
757
758
test "parse: xterm alt+a" {
759
const alloc = testing.allocator_instance.allocator();
760
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
761
-
defer grapheme_data.deinit();
762
const input = "\x1ba";
763
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
764
const result = try parser.parse(input, alloc);
765
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
766
const expected_event: Event = .{ .key_press = expected_key };
···
771
772
test "parse: xterm key up" {
773
const alloc = testing.allocator_instance.allocator();
774
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
775
-
defer grapheme_data.deinit();
776
{
777
// normal version
778
const input = "\x1b[A";
779
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
780
const result = try parser.parse(input, alloc);
781
const expected_key: Key = .{ .codepoint = Key.up };
782
const expected_event: Event = .{ .key_press = expected_key };
···
788
{
789
// application keys version
790
const input = "\x1bOA";
791
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
792
const result = try parser.parse(input, alloc);
793
const expected_key: Key = .{ .codepoint = Key.up };
794
const expected_event: Event = .{ .key_press = expected_key };
···
800
801
test "parse: xterm shift+up" {
802
const alloc = testing.allocator_instance.allocator();
803
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
804
-
defer grapheme_data.deinit();
805
const input = "\x1b[1;2A";
806
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
807
const result = try parser.parse(input, alloc);
808
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
809
const expected_event: Event = .{ .key_press = expected_key };
···
814
815
test "parse: xterm insert" {
816
const alloc = testing.allocator_instance.allocator();
817
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
818
-
defer grapheme_data.deinit();
819
const input = "\x1b[2~";
820
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
821
const result = try parser.parse(input, alloc);
822
const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
823
const expected_event: Event = .{ .key_press = expected_key };
···
828
829
test "parse: paste_start" {
830
const alloc = testing.allocator_instance.allocator();
831
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
832
-
defer grapheme_data.deinit();
833
const input = "\x1b[200~";
834
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
835
const result = try parser.parse(input, alloc);
836
const expected_event: Event = .paste_start;
837
···
841
842
test "parse: paste_end" {
843
const alloc = testing.allocator_instance.allocator();
844
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
845
-
defer grapheme_data.deinit();
846
const input = "\x1b[201~";
847
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
848
const result = try parser.parse(input, alloc);
849
const expected_event: Event = .paste_end;
850
···
854
855
test "parse: osc52 paste" {
856
const alloc = testing.allocator_instance.allocator();
857
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
858
-
defer grapheme_data.deinit();
859
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
860
const expected_text = "osc52 paste";
861
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
862
const result = try parser.parse(input, alloc);
863
864
try testing.expectEqual(25, result.n);
···
873
874
test "parse: focus_in" {
875
const alloc = testing.allocator_instance.allocator();
876
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
877
-
defer grapheme_data.deinit();
878
const input = "\x1b[I";
879
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
880
const result = try parser.parse(input, alloc);
881
const expected_event: Event = .focus_in;
882
···
886
887
test "parse: focus_out" {
888
const alloc = testing.allocator_instance.allocator();
889
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
890
-
defer grapheme_data.deinit();
891
const input = "\x1b[O";
892
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
893
const result = try parser.parse(input, alloc);
894
const expected_event: Event = .focus_out;
895
···
899
900
test "parse: kitty: shift+a without text reporting" {
901
const alloc = testing.allocator_instance.allocator();
902
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
903
-
defer grapheme_data.deinit();
904
const input = "\x1b[97:65;2u";
905
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
906
const result = try parser.parse(input, alloc);
907
const expected_key: Key = .{
908
.codepoint = 'a',
909
.shifted_codepoint = 'A',
910
.mods = .{ .shift = true },
911
};
912
const expected_event: Event = .{ .key_press = expected_key };
913
914
try testing.expectEqual(10, result.n);
915
-
try testing.expectEqual(expected_event, result.event);
916
}
917
918
test "parse: kitty: alt+shift+a without text reporting" {
919
const alloc = testing.allocator_instance.allocator();
920
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
921
-
defer grapheme_data.deinit();
922
const input = "\x1b[97:65;4u";
923
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
924
const result = try parser.parse(input, alloc);
925
const expected_key: Key = .{
926
.codepoint = 'a',
···
935
936
test "parse: kitty: a without text reporting" {
937
const alloc = testing.allocator_instance.allocator();
938
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
939
-
defer grapheme_data.deinit();
940
const input = "\x1b[97u";
941
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
942
const result = try parser.parse(input, alloc);
943
const expected_key: Key = .{
944
.codepoint = 'a',
···
951
952
test "parse: kitty: release event" {
953
const alloc = testing.allocator_instance.allocator();
954
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
955
-
defer grapheme_data.deinit();
956
const input = "\x1b[97;1:3u";
957
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
958
const result = try parser.parse(input, alloc);
959
const expected_key: Key = .{
960
.codepoint = 'a',
···
967
968
test "parse: single codepoint" {
969
const alloc = testing.allocator_instance.allocator();
970
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
971
-
defer grapheme_data.deinit();
972
const input = "๐";
973
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
974
const result = try parser.parse(input, alloc);
975
const expected_key: Key = .{
976
.codepoint = 0x1F642,
···
984
985
test "parse: single codepoint with more in buffer" {
986
const alloc = testing.allocator_instance.allocator();
987
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
988
-
defer grapheme_data.deinit();
989
const input = "๐a";
990
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
991
const result = try parser.parse(input, alloc);
992
const expected_key: Key = .{
993
.codepoint = 0x1F642,
···
1001
1002
test "parse: multiple codepoint grapheme" {
1003
const alloc = testing.allocator_instance.allocator();
1004
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
1005
-
defer grapheme_data.deinit();
1006
const input = "๐ฉโ๐";
1007
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1008
const result = try parser.parse(input, alloc);
1009
const expected_key: Key = .{
1010
.codepoint = Key.multicodepoint,
···
1018
1019
test "parse: multiple codepoint grapheme with more after" {
1020
const alloc = testing.allocator_instance.allocator();
1021
-
const grapheme_data = try grapheme.GraphemeData.init(alloc);
1022
-
defer grapheme_data.deinit();
1023
const input = "๐ฉโ๐abc";
1024
-
var parser: Parser = .{ .grapheme_data = &grapheme_data };
1025
const result = try parser.parse(input, alloc);
1026
const expected_key: Key = .{
1027
.codepoint = Key.multicodepoint,
···
1034
try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1035
}
1036
1037
test "parse(csi): decrpm" {
1038
var buf: [1]u8 = undefined;
1039
{
···
1128
try testing.expectEqual(expected.n, result.n);
1129
try testing.expectEqual(expected.event, result.event);
1130
}
···
4
const Event = @import("event.zig").Event;
5
const Key = @import("Key.zig");
6
const Mouse = @import("Mouse.zig");
7
+
const uucode = @import("uucode");
8
const Winsize = @import("main.zig").Winsize;
9
10
const log = std.log.scoped(.vaxis_parser);
···
24
const shift: u8 = 0b00000100;
25
const alt: u8 = 0b00001000;
26
const ctrl: u8 = 0b00010000;
27
+
const leave: u16 = 0b100000000;
28
};
29
30
// the state of the parser
···
44
// a buffer to temporarily store text in. We need this to encode
45
// text-as-codepoints
46
buf: [128]u8 = undefined,
47
48
/// Parse the first event from the input buffer. If a completion event is not
49
/// present, Result.event will be null and Result.n will be 0
···
75
};
76
},
77
}
78
+
} else return parseGround(input);
79
}
80
81
/// Parse ground state
82
+
inline fn parseGround(input: []const u8) !Result {
83
std.debug.assert(input.len > 0);
84
85
const b = input[0];
···
92
0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
93
0x08 => .{ .codepoint = Key.backspace },
94
0x09 => .{ .codepoint = Key.tab },
95
+
0x0A => .{ .codepoint = 'j', .mods = .{ .ctrl = true } },
96
+
0x0D => .{ .codepoint = Key.enter },
97
0x01...0x07,
98
0x0B...0x0C,
99
0x0E...0x1A,
···
106
},
107
0x7F => .{ .codepoint = Key.backspace },
108
else => blk: {
109
+
var iter = uucode.utf8.Iterator.init(input);
110
// return null if we don't have a valid codepoint
111
+
const first_cp = iter.next() orelse return error.InvalidUTF8;
112
113
+
n = std.unicode.utf8CodepointSequenceLength(first_cp) catch return error.InvalidUTF8;
114
115
// Check if we have a multi-codepoint grapheme
116
+
var code = first_cp;
117
+
var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(input));
118
+
var grapheme_len: usize = 0;
119
+
var cp_count: usize = 0;
120
+
121
+
while (grapheme_iter.next()) |result| {
122
+
cp_count += 1;
123
+
if (result.is_break) {
124
+
// Found the first grapheme boundary
125
+
grapheme_len = grapheme_iter.i;
126
break;
127
}
128
+
}
129
+
130
+
if (grapheme_len > 0) {
131
+
n = grapheme_len;
132
+
if (cp_count > 1) {
133
+
code = Key.multicodepoint;
134
+
}
135
}
136
137
break :blk .{ .codepoint = code, .text = input[0..n] };
···
474
475
'I' => return .{ .event = .focus_in, .n = sequence.len },
476
'O' => return .{ .event = .focus_out, .n = sequence.len },
477
+
'M', 'm' => return parseMouse(sequence, input),
478
'c' => {
479
// Primary DA (CSI ? Pm c)
480
std.debug.assert(sequence.len >= 4); // ESC [ ? c == 4 bytes
···
528
const width_pix = iter.next() orelse "0";
529
530
const winsize: Winsize = .{
531
+
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch return null_event,
532
+
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch return null_event,
533
+
.x_pixel = std.fmt.parseUnsigned(u16, width_pix, 10) catch return null_event,
534
+
.y_pixel = std.fmt.parseUnsigned(u16, height_pix, 10) catch return null_event,
535
};
536
return .{
537
.event = .{ .winsize = winsize },
···
599
key.text = text_buf[0..total];
600
}
601
602
+
{
603
+
// We check if we have *only* shift, no text, and a printable character. This can
604
+
// happen when we have disambiguate on and a key is pressed and encoded as CSI u,
605
+
// for example shift + space can produce CSI 32 ; 2 u
606
+
const mod_test: Key.Modifiers = .{
607
+
.shift = true,
608
+
.caps_lock = key.mods.caps_lock,
609
+
.num_lock = key.mods.num_lock,
610
+
};
611
+
if (key.text == null and
612
+
key.mods.eql(mod_test) and
613
+
key.codepoint <= std.math.maxInt(u8) and
614
+
std.ascii.isPrint(@intCast(key.codepoint)))
615
+
{
616
+
// Encode the codepoint as upper
617
+
const upper = std.ascii.toUpper(@intCast(key.codepoint));
618
+
const n = std.unicode.utf8Encode(upper, text_buf) catch unreachable;
619
+
key.text = text_buf[0..n];
620
+
key.shifted_codepoint = upper;
621
+
}
622
+
}
623
+
624
const event: Event = if (is_release)
625
.{ .key_release = key }
626
else
···
652
else => return null_event,
653
}
654
},
655
+
'q' => {
656
+
// kitty multi cursor cap (CSI > 1;2;3;29;30;40;100;101 TRAILER) (TRAILER is " q")
657
+
const second_final = sequence[sequence.len - 2];
658
+
if (second_final != ' ') return null_event;
659
+
// check for any digits. we're not too picky about checking the supported cursor types here
660
+
for (sequence[0 .. sequence.len - 2]) |c| switch (c) {
661
+
'0'...'9' => return .{ .event = .cap_multi_cursor, .n = sequence.len },
662
+
else => continue,
663
+
};
664
+
return null_event;
665
+
},
666
else => return null_event,
667
}
668
}
···
670
/// Parse a param buffer, returning a default value if the param was empty
671
inline fn parseParam(comptime T: type, buf: []const u8, default: ?T) ?T {
672
if (buf.len == 0) return default;
673
+
return std.fmt.parseInt(T, buf, 10) catch return null;
674
}
675
676
/// Parse a mouse event
677
+
inline fn parseMouse(input: []const u8, full_input: []const u8) Result {
678
const null_event: Result = .{ .event = null, .n = input.len };
679
680
+
var button_mask: u16 = undefined;
681
+
var px: i16 = undefined;
682
+
var py: i16 = undefined;
683
+
var xterm: bool = undefined;
684
+
if (input.len == 3 and (input[2] == 'M') and full_input.len >= 6) {
685
+
xterm = true;
686
+
button_mask = full_input[3] - 32;
687
+
px = full_input[4] - 32;
688
+
py = full_input[5] - 32;
689
+
} else if (input.len >= 4 and input[2] == '<') {
690
+
xterm = false;
691
+
const delim1 = std.mem.indexOfScalarPos(u8, input, 3, ';') orelse return null_event;
692
+
button_mask = parseParam(u16, input[3..delim1], null) orelse return null_event;
693
+
const delim2 = std.mem.indexOfScalarPos(u8, input, delim1 + 1, ';') orelse return null_event;
694
+
px = parseParam(i16, input[delim1 + 1 .. delim2], 1) orelse return null_event;
695
+
py = parseParam(i16, input[delim2 + 1 .. input.len - 1], 1) orelse return null_event;
696
+
} else {
697
+
return null_event;
698
+
}
699
700
+
if (button_mask & mouse_bits.leave > 0)
701
+
return .{ .event = .mouse_leave, .n = if (xterm) 6 else input.len };
702
703
const button: Mouse.Button = @enumFromInt(button_mask & mouse_bits.buttons);
704
const motion = button_mask & mouse_bits.motion > 0;
···
722
if (motion and button == Mouse.Button.none) {
723
break :blk .motion;
724
}
725
+
if (xterm) {
726
+
if (button == Mouse.Button.none) {
727
+
break :blk .release;
728
+
}
729
+
break :blk .press;
730
+
}
731
if (input[input.len - 1] == 'm') break :blk .release;
732
break :blk .press;
733
},
734
};
735
+
return .{ .event = .{ .mouse = mouse }, .n = if (xterm) 6 else input.len };
736
}
737
738
test "parse: single xterm keypress" {
739
const alloc = testing.allocator_instance.allocator();
740
const input = "a";
741
+
var parser: Parser = .{};
742
const result = try parser.parse(input, alloc);
743
const expected_key: Key = .{
744
.codepoint = 'a',
···
752
753
test "parse: single xterm keypress backspace" {
754
const alloc = testing.allocator_instance.allocator();
755
const input = "\x08";
756
+
var parser: Parser = .{};
757
const result = try parser.parse(input, alloc);
758
const expected_key: Key = .{
759
.codepoint = Key.backspace,
···
766
767
test "parse: single xterm keypress with more buffer" {
768
const alloc = testing.allocator_instance.allocator();
769
const input = "ab";
770
+
var parser: Parser = .{};
771
const result = try parser.parse(input, alloc);
772
const expected_key: Key = .{
773
.codepoint = 'a',
···
782
783
test "parse: xterm escape keypress" {
784
const alloc = testing.allocator_instance.allocator();
785
const input = "\x1b";
786
+
var parser: Parser = .{};
787
const result = try parser.parse(input, alloc);
788
const expected_key: Key = .{ .codepoint = Key.escape };
789
const expected_event: Event = .{ .key_press = expected_key };
···
794
795
test "parse: xterm ctrl+a" {
796
const alloc = testing.allocator_instance.allocator();
797
const input = "\x01";
798
+
var parser: Parser = .{};
799
const result = try parser.parse(input, alloc);
800
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .ctrl = true } };
801
const expected_event: Event = .{ .key_press = expected_key };
···
806
807
test "parse: xterm alt+a" {
808
const alloc = testing.allocator_instance.allocator();
809
const input = "\x1ba";
810
+
var parser: Parser = .{};
811
const result = try parser.parse(input, alloc);
812
const expected_key: Key = .{ .codepoint = 'a', .mods = .{ .alt = true } };
813
const expected_event: Event = .{ .key_press = expected_key };
···
818
819
test "parse: xterm key up" {
820
const alloc = testing.allocator_instance.allocator();
821
{
822
// normal version
823
const input = "\x1b[A";
824
+
var parser: Parser = .{};
825
const result = try parser.parse(input, alloc);
826
const expected_key: Key = .{ .codepoint = Key.up };
827
const expected_event: Event = .{ .key_press = expected_key };
···
833
{
834
// application keys version
835
const input = "\x1bOA";
836
+
var parser: Parser = .{};
837
const result = try parser.parse(input, alloc);
838
const expected_key: Key = .{ .codepoint = Key.up };
839
const expected_event: Event = .{ .key_press = expected_key };
···
845
846
test "parse: xterm shift+up" {
847
const alloc = testing.allocator_instance.allocator();
848
const input = "\x1b[1;2A";
849
+
var parser: Parser = .{};
850
const result = try parser.parse(input, alloc);
851
const expected_key: Key = .{ .codepoint = Key.up, .mods = .{ .shift = true } };
852
const expected_event: Event = .{ .key_press = expected_key };
···
857
858
test "parse: xterm insert" {
859
const alloc = testing.allocator_instance.allocator();
860
const input = "\x1b[2~";
861
+
var parser: Parser = .{};
862
const result = try parser.parse(input, alloc);
863
const expected_key: Key = .{ .codepoint = Key.insert, .mods = .{} };
864
const expected_event: Event = .{ .key_press = expected_key };
···
869
870
test "parse: paste_start" {
871
const alloc = testing.allocator_instance.allocator();
872
const input = "\x1b[200~";
873
+
var parser: Parser = .{};
874
const result = try parser.parse(input, alloc);
875
const expected_event: Event = .paste_start;
876
···
880
881
test "parse: paste_end" {
882
const alloc = testing.allocator_instance.allocator();
883
const input = "\x1b[201~";
884
+
var parser: Parser = .{};
885
const result = try parser.parse(input, alloc);
886
const expected_event: Event = .paste_end;
887
···
891
892
test "parse: osc52 paste" {
893
const alloc = testing.allocator_instance.allocator();
894
const input = "\x1b]52;c;b3NjNTIgcGFzdGU=\x1b\\";
895
const expected_text = "osc52 paste";
896
+
var parser: Parser = .{};
897
const result = try parser.parse(input, alloc);
898
899
try testing.expectEqual(25, result.n);
···
908
909
test "parse: focus_in" {
910
const alloc = testing.allocator_instance.allocator();
911
const input = "\x1b[I";
912
+
var parser: Parser = .{};
913
const result = try parser.parse(input, alloc);
914
const expected_event: Event = .focus_in;
915
···
919
920
test "parse: focus_out" {
921
const alloc = testing.allocator_instance.allocator();
922
const input = "\x1b[O";
923
+
var parser: Parser = .{};
924
const result = try parser.parse(input, alloc);
925
const expected_event: Event = .focus_out;
926
···
930
931
test "parse: kitty: shift+a without text reporting" {
932
const alloc = testing.allocator_instance.allocator();
933
const input = "\x1b[97:65;2u";
934
+
var parser: Parser = .{};
935
const result = try parser.parse(input, alloc);
936
const expected_key: Key = .{
937
.codepoint = 'a',
938
.shifted_codepoint = 'A',
939
.mods = .{ .shift = true },
940
+
.text = "A",
941
};
942
const expected_event: Event = .{ .key_press = expected_key };
943
944
try testing.expectEqual(10, result.n);
945
+
try testing.expectEqualDeep(expected_event, result.event);
946
}
947
948
test "parse: kitty: alt+shift+a without text reporting" {
949
const alloc = testing.allocator_instance.allocator();
950
const input = "\x1b[97:65;4u";
951
+
var parser: Parser = .{};
952
const result = try parser.parse(input, alloc);
953
const expected_key: Key = .{
954
.codepoint = 'a',
···
963
964
test "parse: kitty: a without text reporting" {
965
const alloc = testing.allocator_instance.allocator();
966
const input = "\x1b[97u";
967
+
var parser: Parser = .{};
968
const result = try parser.parse(input, alloc);
969
const expected_key: Key = .{
970
.codepoint = 'a',
···
977
978
test "parse: kitty: release event" {
979
const alloc = testing.allocator_instance.allocator();
980
const input = "\x1b[97;1:3u";
981
+
var parser: Parser = .{};
982
const result = try parser.parse(input, alloc);
983
const expected_key: Key = .{
984
.codepoint = 'a',
···
991
992
test "parse: single codepoint" {
993
const alloc = testing.allocator_instance.allocator();
994
const input = "๐";
995
+
var parser: Parser = .{};
996
const result = try parser.parse(input, alloc);
997
const expected_key: Key = .{
998
.codepoint = 0x1F642,
···
1006
1007
test "parse: single codepoint with more in buffer" {
1008
const alloc = testing.allocator_instance.allocator();
1009
const input = "๐a";
1010
+
var parser: Parser = .{};
1011
const result = try parser.parse(input, alloc);
1012
const expected_key: Key = .{
1013
.codepoint = 0x1F642,
···
1021
1022
test "parse: multiple codepoint grapheme" {
1023
const alloc = testing.allocator_instance.allocator();
1024
const input = "๐ฉโ๐";
1025
+
var parser: Parser = .{};
1026
const result = try parser.parse(input, alloc);
1027
const expected_key: Key = .{
1028
.codepoint = Key.multicodepoint,
···
1036
1037
test "parse: multiple codepoint grapheme with more after" {
1038
const alloc = testing.allocator_instance.allocator();
1039
const input = "๐ฉโ๐abc";
1040
+
var parser: Parser = .{};
1041
const result = try parser.parse(input, alloc);
1042
const expected_key: Key = .{
1043
.codepoint = Key.multicodepoint,
···
1050
try testing.expectEqual(expected_key.codepoint, actual.codepoint);
1051
}
1052
1053
+
test "parse: flag emoji" {
1054
+
const alloc = testing.allocator_instance.allocator();
1055
+
const input = "๐บ๐ธ";
1056
+
var parser: Parser = .{};
1057
+
const result = try parser.parse(input, alloc);
1058
+
const expected_key: Key = .{
1059
+
.codepoint = Key.multicodepoint,
1060
+
.text = input,
1061
+
};
1062
+
const expected_event: Event = .{ .key_press = expected_key };
1063
+
1064
+
try testing.expectEqual(input.len, result.n);
1065
+
try testing.expectEqual(expected_event, result.event);
1066
+
}
1067
+
1068
+
test "parse: combining mark" {
1069
+
const alloc = testing.allocator_instance.allocator();
1070
+
// a with combining acute accent (NFD form)
1071
+
const input = "a\u{0301}";
1072
+
var parser: Parser = .{};
1073
+
const result = try parser.parse(input, alloc);
1074
+
const expected_key: Key = .{
1075
+
.codepoint = Key.multicodepoint,
1076
+
.text = input,
1077
+
};
1078
+
const expected_event: Event = .{ .key_press = expected_key };
1079
+
1080
+
try testing.expectEqual(input.len, result.n);
1081
+
try testing.expectEqual(expected_event, result.event);
1082
+
}
1083
+
1084
+
test "parse: skin tone emoji" {
1085
+
const alloc = testing.allocator_instance.allocator();
1086
+
const input = "๐๐ฟ";
1087
+
var parser: Parser = .{};
1088
+
const result = try parser.parse(input, alloc);
1089
+
const expected_key: Key = .{
1090
+
.codepoint = Key.multicodepoint,
1091
+
.text = input,
1092
+
};
1093
+
const expected_event: Event = .{ .key_press = expected_key };
1094
+
1095
+
try testing.expectEqual(input.len, result.n);
1096
+
try testing.expectEqual(expected_event, result.event);
1097
+
}
1098
+
1099
+
test "parse: text variation selector" {
1100
+
const alloc = testing.allocator_instance.allocator();
1101
+
// Heavy black heart with text variation selector
1102
+
const input = "โค๏ธ";
1103
+
var parser: Parser = .{};
1104
+
const result = try parser.parse(input, alloc);
1105
+
const expected_key: Key = .{
1106
+
.codepoint = Key.multicodepoint,
1107
+
.text = input,
1108
+
};
1109
+
const expected_event: Event = .{ .key_press = expected_key };
1110
+
1111
+
try testing.expectEqual(input.len, result.n);
1112
+
try testing.expectEqual(expected_event, result.event);
1113
+
}
1114
+
1115
+
test "parse: keycap sequence" {
1116
+
const alloc = testing.allocator_instance.allocator();
1117
+
const input = "1๏ธโฃ";
1118
+
var parser: Parser = .{};
1119
+
const result = try parser.parse(input, alloc);
1120
+
const expected_key: Key = .{
1121
+
.codepoint = Key.multicodepoint,
1122
+
.text = input,
1123
+
};
1124
+
const expected_event: Event = .{ .key_press = expected_key };
1125
+
1126
+
try testing.expectEqual(input.len, result.n);
1127
+
try testing.expectEqual(expected_event, result.event);
1128
+
}
1129
+
1130
+
test "parse(csi): kitty multi cursor" {
1131
+
var buf: [1]u8 = undefined;
1132
+
{
1133
+
const input = "\x1b[>1;2;3;29;30;40;100;101 q";
1134
+
const result = parseCsi(input, &buf);
1135
+
const expected: Result = .{
1136
+
.event = .cap_multi_cursor,
1137
+
.n = input.len,
1138
+
};
1139
+
1140
+
try testing.expectEqual(expected.n, result.n);
1141
+
try testing.expectEqual(expected.event, result.event);
1142
+
}
1143
+
{
1144
+
const input = "\x1b[> q";
1145
+
const result = parseCsi(input, &buf);
1146
+
const expected: Result = .{
1147
+
.event = null,
1148
+
.n = input.len,
1149
+
};
1150
+
1151
+
try testing.expectEqual(expected.n, result.n);
1152
+
try testing.expectEqual(expected.event, result.event);
1153
+
}
1154
+
}
1155
+
1156
test "parse(csi): decrpm" {
1157
var buf: [1]u8 = undefined;
1158
{
···
1247
try testing.expectEqual(expected.n, result.n);
1248
try testing.expectEqual(expected.event, result.event);
1249
}
1250
+
1251
+
test "parse(csi): mouse (negative)" {
1252
+
var buf: [1]u8 = undefined;
1253
+
const input = "\x1b[<35;-50;-100m";
1254
+
const result = parseCsi(input, &buf);
1255
+
const expected: Result = .{
1256
+
.event = .{ .mouse = .{
1257
+
.col = -51,
1258
+
.row = -101,
1259
+
.button = .none,
1260
+
.type = .motion,
1261
+
.mods = .{},
1262
+
} },
1263
+
.n = input.len,
1264
+
};
1265
+
1266
+
try testing.expectEqual(expected.n, result.n);
1267
+
try testing.expectEqual(expected.event, result.event);
1268
+
}
1269
+
1270
+
test "parse(csi): xterm mouse" {
1271
+
var buf: [1]u8 = undefined;
1272
+
const input = "\x1b[M\x20\x21\x21";
1273
+
const result = parseCsi(input, &buf);
1274
+
const expected: Result = .{
1275
+
.event = .{ .mouse = .{
1276
+
.col = 0,
1277
+
.row = 0,
1278
+
.button = .left,
1279
+
.type = .press,
1280
+
.mods = .{},
1281
+
} },
1282
+
.n = input.len,
1283
+
};
1284
+
1285
+
try testing.expectEqual(expected.n, result.n);
1286
+
try testing.expectEqual(expected.event, result.event);
1287
+
}
1288
+
1289
+
test "parse: disambiguate shift + space" {
1290
+
const alloc = testing.allocator_instance.allocator();
1291
+
const input = "\x1b[32;2u";
1292
+
var parser: Parser = .{};
1293
+
const result = try parser.parse(input, alloc);
1294
+
const expected_key: Key = .{
1295
+
.codepoint = ' ',
1296
+
.shifted_codepoint = ' ',
1297
+
.mods = .{ .shift = true },
1298
+
.text = " ",
1299
+
};
1300
+
const expected_event: Event = .{ .key_press = expected_key };
1301
+
1302
+
try testing.expectEqual(7, result.n);
1303
+
try testing.expectEqualDeep(expected_event, result.event);
1304
+
}
+26
-31
src/Screen.zig
+26
-31
src/Screen.zig
···
5
const Shape = @import("Mouse.zig").Shape;
6
const Image = @import("Image.zig");
7
const Winsize = @import("main.zig").Winsize;
8
-
const Unicode = @import("Unicode.zig");
9
const Method = @import("gwidth.zig").Method;
10
11
const Screen = @This();
12
13
-
width: usize = 0,
14
-
height: usize = 0,
15
16
-
width_pix: usize = 0,
17
-
height_pix: usize = 0,
18
19
-
buf: []Cell = undefined,
20
21
-
cursor_row: usize = 0,
22
-
cursor_col: usize = 0,
23
cursor_vis: bool = false,
24
25
-
unicode: *const Unicode = undefined,
26
-
27
width_method: Method = .wcwidth,
28
29
mouse_shape: Shape = .default,
30
cursor_shape: Cell.CursorShape = .default,
31
32
-
pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) !Screen {
33
const w = winsize.cols;
34
const h = winsize.rows;
35
const self = Screen{
36
-
.buf = try alloc.alloc(Cell, w * h),
37
.width = w,
38
.height = h,
39
.width_pix = winsize.x_pixel,
40
.height_pix = winsize.y_pixel,
41
-
.unicode = unicode,
42
};
43
const base_cell: Cell = .{};
44
@memset(self.buf, base_cell);
45
return self;
46
}
47
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
48
alloc.free(self.buf);
49
}
50
51
/// writes a cell to a location. 0 indexed
52
-
pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void {
53
-
if (self.width <= col) {
54
-
// column out of bounds
55
return;
56
-
}
57
-
if (self.height <= row) {
58
-
// height out of bounds
59
-
return;
60
-
}
61
-
const i = (row * self.width) + col;
62
assert(i < self.buf.len);
63
self.buf[i] = cell;
64
}
65
66
-
pub fn readCell(self: *Screen, col: usize, row: usize) ?Cell {
67
-
if (self.width <= col) {
68
-
// column out of bounds
69
-
return null;
70
-
}
71
-
if (self.height <= row) {
72
-
// height out of bounds
73
return null;
74
-
}
75
-
const i = (row * self.width) + col;
76
assert(i < self.buf.len);
77
return self.buf[i];
78
}
···
5
const Shape = @import("Mouse.zig").Shape;
6
const Image = @import("Image.zig");
7
const Winsize = @import("main.zig").Winsize;
8
const Method = @import("gwidth.zig").Method;
9
10
const Screen = @This();
11
12
+
width: u16 = 0,
13
+
height: u16 = 0,
14
15
+
width_pix: u16 = 0,
16
+
height_pix: u16 = 0,
17
18
+
buf: []Cell = &.{},
19
20
+
cursor_row: u16 = 0,
21
+
cursor_col: u16 = 0,
22
cursor_vis: bool = false,
23
24
width_method: Method = .wcwidth,
25
26
mouse_shape: Shape = .default,
27
cursor_shape: Cell.CursorShape = .default,
28
29
+
pub fn init(alloc: std.mem.Allocator, winsize: Winsize) std.mem.Allocator.Error!Screen {
30
const w = winsize.cols;
31
const h = winsize.rows;
32
const self = Screen{
33
+
.buf = try alloc.alloc(Cell, @as(usize, @intCast(w)) * h),
34
.width = w,
35
.height = h,
36
.width_pix = winsize.x_pixel,
37
.height_pix = winsize.y_pixel,
38
};
39
const base_cell: Cell = .{};
40
@memset(self.buf, base_cell);
41
return self;
42
}
43
+
44
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
45
alloc.free(self.buf);
46
}
47
48
/// writes a cell to a location. 0 indexed
49
+
pub fn writeCell(self: *Screen, col: u16, row: u16, cell: Cell) void {
50
+
if (col >= self.width or
51
+
row >= self.height)
52
return;
53
+
const i = (@as(usize, @intCast(row)) * self.width) + col;
54
assert(i < self.buf.len);
55
self.buf[i] = cell;
56
}
57
58
+
pub fn readCell(self: *const Screen, col: u16, row: u16) ?Cell {
59
+
if (col >= self.width or
60
+
row >= self.height)
61
return null;
62
+
const i = (@as(usize, @intCast(row)) * self.width) + col;
63
assert(i < self.buf.len);
64
return self.buf[i];
65
}
66
+
67
+
pub fn clear(self: *Screen) void {
68
+
@memset(self.buf, .{});
69
+
}
70
+
71
+
test "refAllDecls" {
72
+
std.testing.refAllDecls(@This());
73
+
}
-28
src/Unicode.zig
-28
src/Unicode.zig
···
1
-
const std = @import("std");
2
-
const grapheme = @import("grapheme");
3
-
const DisplayWidth = @import("DisplayWidth");
4
-
5
-
/// A thin wrapper around zg data
6
-
const Unicode = @This();
7
-
8
-
grapheme_data: grapheme.GraphemeData,
9
-
width_data: DisplayWidth.DisplayWidthData,
10
-
11
-
/// initialize all unicode data vaxis may possibly need
12
-
pub fn init(alloc: std.mem.Allocator) !Unicode {
13
-
return .{
14
-
.grapheme_data = try grapheme.GraphemeData.init(alloc),
15
-
.width_data = try DisplayWidth.DisplayWidthData.init(alloc),
16
-
};
17
-
}
18
-
19
-
/// free all data
20
-
pub fn deinit(self: *const Unicode) void {
21
-
self.grapheme_data.deinit();
22
-
self.width_data.deinit();
23
-
}
24
-
25
-
/// creates a grapheme iterator based on str
26
-
pub fn graphemeIterator(self: *const Unicode, str: []const u8) grapheme.Iterator {
27
-
return grapheme.Iterator.init(str, &self.grapheme_data);
28
-
}
···
+686
-109
src/Vaxis.zig
+686
-109
src/Vaxis.zig
···
3
const atomic = std.atomic;
4
const base64Encoder = std.base64.standard.Encoder;
5
const zigimg = @import("zigimg");
6
7
const Cell = @import("Cell.zig");
8
const Image = @import("Image.zig");
···
10
const Key = @import("Key.zig");
11
const Mouse = @import("Mouse.zig");
12
const Screen = @import("Screen.zig");
13
-
const Unicode = @import("Unicode.zig");
14
const Window = @import("Window.zig");
15
16
-
const AnyWriter = std.io.AnyWriter;
17
const Hyperlink = Cell.Hyperlink;
18
const KittyFlags = Key.KittyFlags;
19
const Shape = Mouse.Shape;
···
23
const ctlseqs = @import("ctlseqs.zig");
24
const gwidth = @import("gwidth.zig");
25
26
const Vaxis = @This();
27
28
const log = std.log.scoped(.vaxis);
···
34
unicode: gwidth.Method = .wcwidth,
35
sgr_pixels: bool = false,
36
color_scheme_updates: bool = false,
37
};
38
39
pub const Options = struct {
···
48
screen: Screen,
49
/// The last screen we drew. We keep this so we can efficiently update on
50
/// the next render
51
-
screen_last: InternalScreen = undefined,
52
53
caps: Capabilities = .{},
54
···
61
/// futex times out
62
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),
63
64
// images
65
next_img_id: u32 = 1,
66
67
-
unicode: Unicode,
68
-
69
-
// statistics
70
-
renders: usize = 0,
71
-
render_dur: u64 = 0,
72
-
render_timer: std.time.Timer,
73
-
74
sgr: enum {
75
standard,
76
legacy,
77
} = .standard,
78
79
state: struct {
80
/// if we are in the alt screen
81
alt_screen: bool = false,
···
86
pixel_mouse: bool = false,
87
color_scheme_updates: bool = false,
88
in_band_resize: bool = false,
89
cursor: struct {
90
-
row: usize = 0,
91
-
col: usize = 0,
92
} = .{},
93
} = .{},
94
···
97
return .{
98
.opts = opts,
99
.screen = .{},
100
-
.screen_last = .{},
101
-
.render_timer = try std.time.Timer.start(),
102
-
.unicode = try Unicode.init(alloc),
103
};
104
}
105
···
107
/// passed, this will free resources associated with Vaxis. This is left as an
108
/// optional so applications can choose to not free resources when the
109
/// application will be exiting anyways
110
-
pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: AnyWriter) void {
111
self.resetState(tty) catch {};
112
113
-
// always show the cursor on exit
114
-
tty.writeAll(ctlseqs.show_cursor) catch {};
115
if (alloc) |a| {
116
self.screen.deinit(a);
117
self.screen_last.deinit(a);
118
}
119
-
if (self.renders > 0) {
120
-
const tpr = @divTrunc(self.render_dur, self.renders);
121
-
log.debug("total renders = {d}\r", .{self.renders});
122
-
log.debug("microseconds per render = {d}\r", .{tpr});
123
-
}
124
-
self.unicode.deinit();
125
}
126
127
/// resets enabled features, sends cursor to home and clears below cursor
128
-
pub fn resetState(self: *Vaxis, tty: AnyWriter) !void {
129
if (self.state.kitty_keyboard) {
130
try tty.writeAll(ctlseqs.csi_u_pop);
131
self.state.kitty_keyboard = false;
···
142
try self.exitAltScreen(tty);
143
} else {
144
try tty.writeByte('\r');
145
-
var i: usize = 0;
146
while (i < self.state.cursor.row) : (i += 1) {
147
try tty.writeAll(ctlseqs.ri);
148
}
···
156
try tty.writeAll(ctlseqs.in_band_resize_reset);
157
self.state.in_band_resize = false;
158
}
159
}
160
161
/// resize allocates a slice of cells equal to the number of cells
···
165
pub fn resize(
166
self: *Vaxis,
167
alloc: std.mem.Allocator,
168
-
tty: AnyWriter,
169
winsize: Winsize,
170
) !void {
171
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
172
self.screen.deinit(alloc);
173
-
self.screen = try Screen.init(alloc, winsize, &self.unicode);
174
self.screen.width_method = self.caps.unicode;
175
// try self.screen.int(alloc, winsize.cols, winsize.rows);
176
// we only init our current screen. This has the effect of redrawing
···
180
if (self.state.alt_screen)
181
try tty.writeAll(ctlseqs.home)
182
else {
183
-
try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row);
184
try tty.writeByte('\r');
185
}
186
self.state.cursor.row = 0;
187
self.state.cursor.col = 0;
188
try tty.writeAll(ctlseqs.sgr_reset ++ ctlseqs.erase_below_cursor);
189
}
190
191
/// returns a Window comprising of the entire terminal screen
···
193
return .{
194
.x_off = 0,
195
.y_off = 0,
196
.width = self.screen.width,
197
.height = self.screen.height,
198
.screen = &self.screen,
···
200
}
201
202
/// enter the alternate screen. The alternate screen will automatically
203
-
/// be exited if calling deinit while in the alt screen
204
-
pub fn enterAltScreen(self: *Vaxis, tty: AnyWriter) !void {
205
try tty.writeAll(ctlseqs.smcup);
206
self.state.alt_screen = true;
207
}
208
209
-
/// exit the alternate screen
210
-
pub fn exitAltScreen(self: *Vaxis, tty: AnyWriter) !void {
211
try tty.writeAll(ctlseqs.rmcup);
212
self.state.alt_screen = false;
213
}
214
···
218
///
219
/// This call will block until Vaxis.query_futex is woken up, or the timeout.
220
/// Event loops can wake up this futex when cap_da1 is received
221
-
pub fn queryTerminal(self: *Vaxis, tty: AnyWriter, timeout_ns: u64) !void {
222
try self.queryTerminalSend(tty);
223
// 1 second timeout
224
std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
225
try self.enableDetectedFeatures(tty);
226
}
227
228
/// write queries to the terminal to determine capabilities. This function
229
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
230
/// you are using Loop.run()
231
-
pub fn queryTerminalSend(_: Vaxis, tty: AnyWriter) !void {
232
233
// TODO: re-enable this
234
// const colorterm = std.posix.getenv("COLORTERM") orelse "";
···
249
ctlseqs.decrqm_unicode ++
250
ctlseqs.decrqm_color_scheme ++
251
ctlseqs.in_band_resize_set ++
252
ctlseqs.xtversion ++
253
ctlseqs.csi_u_query ++
254
ctlseqs.kitty_graphics_query ++
255
ctlseqs.primary_device_attrs);
256
}
257
258
/// Enable features detected by responses to queryTerminal. This function
259
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
260
/// you are using Loop.run()
261
-
pub fn enableDetectedFeatures(self: *Vaxis, tty: AnyWriter) !void {
262
switch (builtin.os.tag) {
263
.windows => {
264
// No feature detection on windows. We just hard enable some knowns for ConPTY
···
273
self.caps.kitty_keyboard = false;
274
self.sgr = .legacy;
275
}
276
if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_|
277
self.sgr = .legacy;
278
if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_|
···
284
if (self.caps.kitty_keyboard) {
285
try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
286
}
287
-
if (self.caps.unicode == .unicode) {
288
try tty.writeAll(ctlseqs.unicode_set);
289
}
290
},
291
}
292
}
293
294
// the next render call will refresh the entire screen
···
297
}
298
299
/// draws the screen to the terminal
300
-
pub fn render(self: *Vaxis, tty: AnyWriter) !void {
301
-
self.renders += 1;
302
-
self.render_timer.reset();
303
-
defer {
304
-
self.render_dur += self.render_timer.read() / std.time.ns_per_us;
305
-
}
306
-
307
defer self.refresh = false;
308
309
-
// Set up sync before we write anything
310
-
// TODO: optimize sync so we only sync _when we have changes_. This
311
-
// requires a smarter buffered writer, we'll probably have to write
312
-
// our own
313
-
try tty.writeAll(ctlseqs.sync_set);
314
-
defer tty.writeAll(ctlseqs.sync_reset) catch {};
315
316
-
// Send the cursor to 0,0
317
-
// TODO: this needs to move after we optimize writes. We only do
318
-
// this if we have an update to make. We also need to hide cursor
319
-
// and then reshow it if needed
320
-
try tty.writeAll(ctlseqs.hide_cursor);
321
-
if (self.state.alt_screen)
322
-
try tty.writeAll(ctlseqs.home)
323
-
else {
324
-
try tty.writeByte('\r');
325
-
try tty.writeBytesNTimes(ctlseqs.ri, self.state.cursor.row);
326
-
}
327
-
try tty.writeAll(ctlseqs.sgr_reset);
328
329
// initialize some variables
330
var reposition: bool = false;
331
-
var row: usize = 0;
332
-
var col: usize = 0;
333
var cursor: Style = .{};
334
var link: Hyperlink = .{};
335
-
var cursor_pos: struct {
336
-
row: usize = 0,
337
-
col: usize = 0,
338
-
} = .{};
339
340
-
// Clear all images
341
-
if (self.caps.kitty_graphics)
342
-
try tty.writeAll(ctlseqs.kitty_graphics_clear);
343
344
var i: usize = 0;
345
while (i < self.screen.buf.len) {
346
const cell = self.screen.buf[i];
347
-
const w = blk: {
348
if (cell.char.width != 0) break :blk cell.char.width;
349
350
const method: gwidth.Method = self.caps.unicode;
351
-
const width = gwidth.gwidth(cell.char.grapheme, method, &self.unicode.width_data) catch 1;
352
break :blk @max(1, width);
353
};
354
defer {
···
372
// If cell is the same as our last frame, we don't need to do
373
// anything
374
const last = self.screen_last.buf[i];
375
-
if (!self.refresh and last.eql(cell) and !last.skipped and cell.image == null) {
376
reposition = true;
377
// Close any osc8 sequence we might be in before
378
// repositioning
···
381
}
382
continue;
383
}
384
self.screen_last.buf[i].skipped = false;
385
defer {
386
cursor = cell.style;
···
389
// Set this cell in the last frame
390
self.screen_last.writeCell(col, row, cell);
391
392
// reposition the cursor, if needed
393
if (reposition) {
394
reposition = false;
395
if (self.state.alt_screen)
396
try tty.print(ctlseqs.cup, .{ row + 1, col + 1 })
397
else {
···
401
try tty.print(ctlseqs.cuf, .{n});
402
} else {
403
const n = row - cursor_pos.row;
404
-
try tty.writeByteNTimes('\n', n);
405
try tty.writeByte('\r');
406
if (col > 0)
407
try tty.print(ctlseqs.cuf, .{col});
···
504
}
505
},
506
.rgb => |rgb| {
507
-
switch (self.sgr) {
508
.standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
509
.legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
510
}
···
596
}
597
try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
598
}
599
-
try tty.writeAll(cell.char.grapheme);
600
cursor_pos.col = col + w;
601
cursor_pos.row = row;
602
}
603
if (self.screen.cursor_vis) {
604
if (self.state.alt_screen) {
605
try tty.print(
···
612
} else {
613
// TODO: position cursor relative to current location
614
try tty.writeByte('\r');
615
-
if (self.screen.cursor_row >= cursor_pos.row)
616
-
try tty.writeByteNTimes('\n', self.screen.cursor_row - cursor_pos.row)
617
-
else
618
-
try tty.writeBytesNTimes(ctlseqs.ri, cursor_pos.row - self.screen.cursor_row);
619
if (self.screen.cursor_col > 0)
620
try tty.print(ctlseqs.cuf, .{self.screen.cursor_col});
621
}
···
626
self.state.cursor.row = cursor_pos.row;
627
self.state.cursor.col = cursor_pos.col;
628
}
629
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
630
try tty.print(
631
ctlseqs.osc22_mouse_shape,
···
640
);
641
self.screen_last.cursor_shape = self.screen.cursor_shape;
642
}
643
}
644
645
-
fn enableKittyKeyboard(self: *Vaxis, tty: AnyWriter, flags: Key.KittyFlags) !void {
646
const flag_int: u5 = @bitCast(flags);
647
try tty.print(ctlseqs.csi_u_push, .{flag_int});
648
self.state.kitty_keyboard = true;
649
}
650
651
/// send a system notification
652
-
pub fn notify(_: *Vaxis, tty: AnyWriter, title: ?[]const u8, body: []const u8) !void {
653
if (title) |t|
654
try tty.print(ctlseqs.osc777_notify, .{ t, body })
655
else
656
try tty.print(ctlseqs.osc9_notify, .{body});
657
}
658
659
/// sets the window title
660
-
pub fn setTitle(_: *Vaxis, tty: AnyWriter, title: []const u8) !void {
661
try tty.print(ctlseqs.osc2_set_title, .{title});
662
}
663
664
// turn bracketed paste on or off. An event will be sent at the
665
// beginning and end of a detected paste. All keystrokes between these
666
// events were pasted
667
-
pub fn setBracketedPaste(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
668
const seq = if (enable)
669
ctlseqs.bp_set
670
else
671
ctlseqs.bp_reset;
672
try tty.writeAll(seq);
673
self.state.bracketed_paste = enable;
674
}
675
···
679
}
680
681
/// Change the mouse reporting mode
682
-
pub fn setMouseMode(self: *Vaxis, tty: AnyWriter, enable: bool) !void {
683
if (enable) {
684
self.state.mouse = true;
685
if (self.caps.sgr_pixels) {
···
693
} else {
694
try tty.writeAll(ctlseqs.mouse_reset);
695
}
696
}
697
698
/// Translate pixel mouse coordinates to cell + offset
···
706
const ypos = mouse.row;
707
const xextra = self.screen.width_pix % self.screen.width;
708
const yextra = self.screen.height_pix % self.screen.height;
709
-
const xcell = (self.screen.width_pix - xextra) / self.screen.width;
710
-
const ycell = (self.screen.height_pix - yextra) / self.screen.height;
711
-
result.col = xpos / xcell;
712
-
result.row = ypos / ycell;
713
-
result.xoffset = xpos % xcell;
714
-
result.yoffset = ypos % ycell;
715
}
716
return result;
717
}
718
719
/// Transmit an image which has been pre-base64 encoded
720
pub fn transmitPreEncodedImage(
721
self: *Vaxis,
722
-
tty: AnyWriter,
723
bytes: []const u8,
724
-
width: usize,
725
-
height: usize,
726
format: Image.TransmitFormat,
727
) !Image {
728
defer self.next_img_id += 1;
729
const id = self.next_img_id;
730
···
764
);
765
}
766
}
767
return .{
768
.id = id,
769
.width = width,
···
774
pub fn transmitImage(
775
self: *Vaxis,
776
alloc: std.mem.Allocator,
777
-
tty: AnyWriter,
778
img: *zigimg.Image,
779
format: Image.TransmitFormat,
780
) !Image {
···
786
const buf = switch (format) {
787
.png => png: {
788
const png_buf = try arena.allocator().alloc(u8, img.imageByteSize());
789
-
const png = try img.writeToMemory(png_buf, .{ .png = .{} });
790
break :png png;
791
},
792
.rgb => rgb: {
793
-
try img.convert(.rgb24);
794
break :rgb img.rawBytes();
795
},
796
.rgba => rgba: {
797
-
try img.convert(.rgba32);
798
break :rgba img.rawBytes();
799
},
800
};
···
802
const b64_buf = try arena.allocator().alloc(u8, base64Encoder.calcSize(buf.len));
803
const encoded = base64Encoder.encode(b64_buf, buf);
804
805
-
return self.transmitPreEncodedImage(tty, encoded, img.width, img.height, format);
806
}
807
808
pub fn loadImage(
809
self: *Vaxis,
810
alloc: std.mem.Allocator,
811
-
tty: AnyWriter,
812
src: Image.Source,
813
) !Image {
814
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
815
816
var img = switch (src) {
817
-
.path => |path| try zigimg.Image.fromFilePath(alloc, path),
818
.mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
819
};
820
-
defer img.deinit();
821
return self.transmitImage(alloc, tty, &img, .png);
822
}
823
824
/// deletes an image from the terminal's memory
825
-
pub fn freeImage(_: Vaxis, tty: AnyWriter, id: u32) void {
826
tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| {
827
log.err("couldn't delete image {d}: {}", .{ id, err });
828
return;
829
};
830
}
831
832
-
pub fn copyToSystemClipboard(_: Vaxis, tty: AnyWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void {
833
const encoder = std.base64.standard.Encoder;
834
const size = encoder.calcSize(text.len);
835
const buf = try encode_allocator.alloc(u8, size);
···
839
ctlseqs.osc52_clipboard_copy,
840
.{b64},
841
);
842
}
843
844
-
pub fn requestSystemClipboard(self: Vaxis, tty: AnyWriter) !void {
845
if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator;
846
try tty.print(
847
ctlseqs.osc52_clipboard_request,
848
.{},
849
);
850
}
851
852
/// Request a color report from the terminal. Note: not all terminals support
853
/// reporting colors. It is always safe to try, but you may not receive a
854
/// response.
855
-
pub fn queryColor(_: Vaxis, tty: AnyWriter, kind: Cell.Color.Kind) !void {
856
switch (kind) {
857
.fg => try tty.writeAll(ctlseqs.osc10_query),
858
.bg => try tty.writeAll(ctlseqs.osc11_query),
859
.cursor => try tty.writeAll(ctlseqs.osc12_query),
860
.index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}),
861
}
862
}
863
864
/// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must
···
866
/// capability. Support can be detected by checking the value of
867
/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when
868
/// subscribing.
869
-
pub fn subscribeToColorSchemeUpdates(self: Vaxis, tty: AnyWriter) !void {
870
try tty.writeAll(ctlseqs.color_scheme_request);
871
try tty.writeAll(ctlseqs.color_scheme_set);
872
self.state.color_scheme_updates = true;
873
}
874
875
-
pub fn deviceStatusReport(_: Vaxis, tty: AnyWriter) !void {
876
try tty.writeAll(ctlseqs.device_status_report);
877
}
···
3
const atomic = std.atomic;
4
const base64Encoder = std.base64.standard.Encoder;
5
const zigimg = @import("zigimg");
6
+
const IoWriter = std.io.Writer;
7
8
const Cell = @import("Cell.zig");
9
const Image = @import("Image.zig");
···
11
const Key = @import("Key.zig");
12
const Mouse = @import("Mouse.zig");
13
const Screen = @import("Screen.zig");
14
+
const unicode = @import("unicode.zig");
15
const Window = @import("Window.zig");
16
17
const Hyperlink = Cell.Hyperlink;
18
const KittyFlags = Key.KittyFlags;
19
const Shape = Mouse.Shape;
···
23
const ctlseqs = @import("ctlseqs.zig");
24
const gwidth = @import("gwidth.zig");
25
26
+
const assert = std.debug.assert;
27
+
28
const Vaxis = @This();
29
30
const log = std.log.scoped(.vaxis);
···
36
unicode: gwidth.Method = .wcwidth,
37
sgr_pixels: bool = false,
38
color_scheme_updates: bool = false,
39
+
explicit_width: bool = false,
40
+
scaled_text: bool = false,
41
+
multi_cursor: bool = false,
42
};
43
44
pub const Options = struct {
···
53
screen: Screen,
54
/// The last screen we drew. We keep this so we can efficiently update on
55
/// the next render
56
+
screen_last: InternalScreen,
57
58
caps: Capabilities = .{},
59
···
66
/// futex times out
67
query_futex: atomic.Value(u32) = atomic.Value(u32).init(0),
68
69
+
/// If Queries were sent, we set this to false. We reset to true when all queries are complete. This
70
+
/// is used because we do explicit cursor position reports in the queries, which interfere with F3
71
+
/// key encoding. This can be used as a flag to determine how we should evaluate this sequence
72
+
queries_done: atomic.Value(bool) = atomic.Value(bool).init(true),
73
+
74
// images
75
next_img_id: u32 = 1,
76
77
sgr: enum {
78
standard,
79
legacy,
80
} = .standard,
81
82
+
/// Enable workarounds for escape sequence handling issues/bugs in terminals
83
+
/// So far this just enables a UL escape sequence workaround for conpty
84
+
enable_workarounds: bool = true,
85
+
86
state: struct {
87
/// if we are in the alt screen
88
alt_screen: bool = false,
···
93
pixel_mouse: bool = false,
94
color_scheme_updates: bool = false,
95
in_band_resize: bool = false,
96
+
changed_default_fg: bool = false,
97
+
changed_default_bg: bool = false,
98
+
changed_cursor_color: bool = false,
99
cursor: struct {
100
+
row: u16 = 0,
101
+
col: u16 = 0,
102
} = .{},
103
} = .{},
104
···
107
return .{
108
.opts = opts,
109
.screen = .{},
110
+
.screen_last = try .init(alloc, 0, 0),
111
};
112
}
113
···
115
/// passed, this will free resources associated with Vaxis. This is left as an
116
/// optional so applications can choose to not free resources when the
117
/// application will be exiting anyways
118
+
pub fn deinit(self: *Vaxis, alloc: ?std.mem.Allocator, tty: *IoWriter) void {
119
self.resetState(tty) catch {};
120
121
if (alloc) |a| {
122
self.screen.deinit(a);
123
self.screen_last.deinit(a);
124
}
125
}
126
127
/// resets enabled features, sends cursor to home and clears below cursor
128
+
pub fn resetState(self: *Vaxis, tty: *IoWriter) !void {
129
+
// always show the cursor on state reset
130
+
tty.writeAll(ctlseqs.show_cursor) catch {};
131
+
tty.writeAll(ctlseqs.sgr_reset) catch {};
132
+
if (self.screen.cursor_shape != .default) {
133
+
// In many terminals, `.default` will set to the configured cursor shape. Others, it will
134
+
// change to a blinking block.
135
+
tty.print(ctlseqs.cursor_shape, .{@intFromEnum(Cell.CursorShape.default)}) catch {};
136
+
}
137
if (self.state.kitty_keyboard) {
138
try tty.writeAll(ctlseqs.csi_u_pop);
139
self.state.kitty_keyboard = false;
···
150
try self.exitAltScreen(tty);
151
} else {
152
try tty.writeByte('\r');
153
+
var i: u16 = 0;
154
while (i < self.state.cursor.row) : (i += 1) {
155
try tty.writeAll(ctlseqs.ri);
156
}
···
164
try tty.writeAll(ctlseqs.in_band_resize_reset);
165
self.state.in_band_resize = false;
166
}
167
+
if (self.state.changed_default_fg) {
168
+
try tty.writeAll(ctlseqs.osc10_reset);
169
+
self.state.changed_default_fg = false;
170
+
}
171
+
if (self.state.changed_default_bg) {
172
+
try tty.writeAll(ctlseqs.osc11_reset);
173
+
self.state.changed_default_bg = false;
174
+
}
175
+
if (self.state.changed_cursor_color) {
176
+
try tty.writeAll(ctlseqs.osc12_reset);
177
+
self.state.changed_cursor_color = false;
178
+
}
179
+
180
+
try tty.flush();
181
}
182
183
/// resize allocates a slice of cells equal to the number of cells
···
187
pub fn resize(
188
self: *Vaxis,
189
alloc: std.mem.Allocator,
190
+
tty: *IoWriter,
191
winsize: Winsize,
192
) !void {
193
log.debug("resizing screen: width={d} height={d}", .{ winsize.cols, winsize.rows });
194
self.screen.deinit(alloc);
195
+
self.screen = try Screen.init(alloc, winsize);
196
self.screen.width_method = self.caps.unicode;
197
// try self.screen.int(alloc, winsize.cols, winsize.rows);
198
// we only init our current screen. This has the effect of redrawing
···
202
if (self.state.alt_screen)
203
try tty.writeAll(ctlseqs.home)
204
else {
205
+
for (0..self.state.cursor.row) |_| {
206
+
try tty.writeAll(ctlseqs.ri);
207
+
}
208
try tty.writeByte('\r');
209
}
210
self.state.cursor.row = 0;
211
self.state.cursor.col = 0;
212
try tty.writeAll(ctlseqs.sgr_reset ++ ctlseqs.erase_below_cursor);
213
+
try tty.flush();
214
}
215
216
/// returns a Window comprising of the entire terminal screen
···
218
return .{
219
.x_off = 0,
220
.y_off = 0,
221
+
.parent_x_off = 0,
222
+
.parent_y_off = 0,
223
.width = self.screen.width,
224
.height = self.screen.height,
225
.screen = &self.screen,
···
227
}
228
229
/// enter the alternate screen. The alternate screen will automatically
230
+
/// be exited if calling deinit while in the alt screen.
231
+
pub fn enterAltScreen(self: *Vaxis, tty: *IoWriter) !void {
232
try tty.writeAll(ctlseqs.smcup);
233
+
try tty.flush();
234
self.state.alt_screen = true;
235
}
236
237
+
/// exit the alternate screen. Does not flush the writer.
238
+
pub fn exitAltScreen(self: *Vaxis, tty: *IoWriter) !void {
239
try tty.writeAll(ctlseqs.rmcup);
240
+
try tty.flush();
241
self.state.alt_screen = false;
242
}
243
···
247
///
248
/// This call will block until Vaxis.query_futex is woken up, or the timeout.
249
/// Event loops can wake up this futex when cap_da1 is received
250
+
pub fn queryTerminal(self: *Vaxis, tty: *IoWriter, timeout_ns: u64) !void {
251
try self.queryTerminalSend(tty);
252
// 1 second timeout
253
std.Thread.Futex.timedWait(&self.query_futex, 0, timeout_ns) catch {};
254
+
self.queries_done.store(true, .unordered);
255
try self.enableDetectedFeatures(tty);
256
}
257
258
/// write queries to the terminal to determine capabilities. This function
259
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
260
/// you are using Loop.run()
261
+
pub fn queryTerminalSend(vx: *Vaxis, tty: *IoWriter) !void {
262
+
vx.queries_done.store(false, .unordered);
263
264
// TODO: re-enable this
265
// const colorterm = std.posix.getenv("COLORTERM") orelse "";
···
280
ctlseqs.decrqm_unicode ++
281
ctlseqs.decrqm_color_scheme ++
282
ctlseqs.in_band_resize_set ++
283
+
284
+
// Explicit width query. We send the cursor home, then do an explicit width command, then
285
+
// query the position. If the parsed value is an F3 with shift, we support explicit width.
286
+
// The returned response will be something like \x1b[1;2R...which when parsed as a Key is a
287
+
// shift + F3 (the row is ignored). We only care if the column has moved from 1->2, which is
288
+
// why we see a Shift modifier
289
+
ctlseqs.home ++
290
+
ctlseqs.explicit_width_query ++
291
+
ctlseqs.cursor_position_request ++
292
+
// Explicit width query. We send the cursor home, then do an scaled text command, then
293
+
// query the position. If the parsed value is an F3 with al, we support scaled text.
294
+
// The returned response will be something like \x1b[1;3R...which when parsed as a Key is a
295
+
// alt + F3 (the row is ignored). We only care if the column has moved from 1->3, which is
296
+
// why we see a Shift modifier
297
+
ctlseqs.home ++
298
+
ctlseqs.scaled_text_query ++
299
+
ctlseqs.multi_cursor_query ++
300
+
ctlseqs.cursor_position_request ++
301
ctlseqs.xtversion ++
302
ctlseqs.csi_u_query ++
303
ctlseqs.kitty_graphics_query ++
304
ctlseqs.primary_device_attrs);
305
+
306
+
try tty.flush();
307
}
308
309
/// Enable features detected by responses to queryTerminal. This function
310
/// is only for use with a custom main loop. Call Vaxis.queryTerminal() if
311
/// you are using Loop.run()
312
+
pub fn enableDetectedFeatures(self: *Vaxis, tty: *IoWriter) !void {
313
switch (builtin.os.tag) {
314
.windows => {
315
// No feature detection on windows. We just hard enable some knowns for ConPTY
···
324
self.caps.kitty_keyboard = false;
325
self.sgr = .legacy;
326
}
327
+
if (std.posix.getenv("TERM_PROGRAM")) |prg| {
328
+
if (std.mem.eql(u8, prg, "vscode"))
329
+
self.sgr = .legacy;
330
+
}
331
if (std.posix.getenv("VAXIS_FORCE_LEGACY_SGR")) |_|
332
self.sgr = .legacy;
333
if (std.posix.getenv("VAXIS_FORCE_WCWIDTH")) |_|
···
339
if (self.caps.kitty_keyboard) {
340
try self.enableKittyKeyboard(tty, self.opts.kitty_keyboard_flags);
341
}
342
+
// Only enable mode 2027 if we don't have explicit width
343
+
if (self.caps.unicode == .unicode and !self.caps.explicit_width) {
344
try tty.writeAll(ctlseqs.unicode_set);
345
}
346
},
347
}
348
+
349
+
try tty.flush();
350
}
351
352
// the next render call will refresh the entire screen
···
355
}
356
357
/// draws the screen to the terminal
358
+
pub fn render(self: *Vaxis, tty: *IoWriter) !void {
359
defer self.refresh = false;
360
+
assert(self.screen.buf.len == @as(usize, @intCast(self.screen.width)) * self.screen.height); // correct size
361
+
assert(self.screen.buf.len == self.screen_last.buf.len); // same size
362
363
+
var started: bool = false;
364
+
var sync_active: bool = false;
365
+
errdefer if (sync_active) tty.writeAll(ctlseqs.sync_reset) catch {};
366
367
+
const cursor_vis_changed = self.screen.cursor_vis != self.screen_last.cursor_vis;
368
+
const cursor_shape_changed = self.screen.cursor_shape != self.screen_last.cursor_shape;
369
+
const mouse_shape_changed = self.screen.mouse_shape != self.screen_last.mouse_shape;
370
+
const cursor_pos_changed = self.screen.cursor_vis and
371
+
(self.screen.cursor_row != self.state.cursor.row or
372
+
self.screen.cursor_col != self.state.cursor.col);
373
+
const needs_render = self.refresh or cursor_vis_changed or cursor_shape_changed or mouse_shape_changed or cursor_pos_changed;
374
375
// initialize some variables
376
var reposition: bool = false;
377
+
var row: u16 = 0;
378
+
var col: u16 = 0;
379
var cursor: Style = .{};
380
var link: Hyperlink = .{};
381
+
const CursorPos = struct {
382
+
row: u16 = 0,
383
+
col: u16 = 0,
384
+
};
385
+
var cursor_pos: CursorPos = .{};
386
387
+
const startRender = struct {
388
+
fn run(
389
+
vx: *Vaxis,
390
+
io: *IoWriter,
391
+
cursor_pos_ptr: *CursorPos,
392
+
reposition_ptr: *bool,
393
+
started_ptr: *bool,
394
+
sync_active_ptr: *bool,
395
+
) !void {
396
+
if (started_ptr.*) return;
397
+
started_ptr.* = true;
398
+
sync_active_ptr.* = true;
399
+
// Set up sync before we write anything
400
+
try io.writeAll(ctlseqs.sync_set);
401
+
// Send the cursor to 0,0
402
+
try io.writeAll(ctlseqs.hide_cursor);
403
+
if (vx.state.alt_screen)
404
+
try io.writeAll(ctlseqs.home)
405
+
else {
406
+
try io.writeByte('\r');
407
+
for (0..vx.state.cursor.row) |_| {
408
+
try io.writeAll(ctlseqs.ri);
409
+
}
410
+
}
411
+
try io.writeAll(ctlseqs.sgr_reset);
412
+
cursor_pos_ptr.* = .{};
413
+
reposition_ptr.* = true;
414
+
// Clear all images
415
+
if (vx.caps.kitty_graphics)
416
+
try io.writeAll(ctlseqs.kitty_graphics_clear);
417
+
}
418
+
};
419
+
420
+
// Reset skip flag on all last_screen cells
421
+
for (self.screen_last.buf) |*last_cell| {
422
+
last_cell.skip = false;
423
+
}
424
+
425
+
if (needs_render) {
426
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
427
+
}
428
429
var i: usize = 0;
430
while (i < self.screen.buf.len) {
431
const cell = self.screen.buf[i];
432
+
const w: u16 = blk: {
433
if (cell.char.width != 0) break :blk cell.char.width;
434
435
const method: gwidth.Method = self.caps.unicode;
436
+
const width: u16 = @intCast(gwidth.gwidth(cell.char.grapheme, method));
437
break :blk @max(1, width);
438
};
439
defer {
···
457
// If cell is the same as our last frame, we don't need to do
458
// anything
459
const last = self.screen_last.buf[i];
460
+
if ((!self.refresh and
461
+
last.eql(cell) and
462
+
!last.skipped and
463
+
cell.image == null) or
464
+
last.skip)
465
+
{
466
reposition = true;
467
// Close any osc8 sequence we might be in before
468
// repositioning
···
471
}
472
continue;
473
}
474
+
if (!started) {
475
+
try startRender.run(self, tty, &cursor_pos, &reposition, &started, &sync_active);
476
+
}
477
self.screen_last.buf[i].skipped = false;
478
defer {
479
cursor = cell.style;
···
482
// Set this cell in the last frame
483
self.screen_last.writeCell(col, row, cell);
484
485
+
// If we support scaled text, we set the flags now
486
+
if (self.caps.scaled_text and cell.scale.scale > 1) {
487
+
// The cell is scaled. Set appropriate skips. We only need to do this if the scale factor is
488
+
// > 1
489
+
assert(cell.char.width > 0);
490
+
const cols = cell.scale.scale * cell.char.width;
491
+
const rows = cell.scale.scale;
492
+
for (0..rows) |skipped_row| {
493
+
for (0..cols) |skipped_col| {
494
+
if (skipped_row == 0 and skipped_col == 0) {
495
+
continue;
496
+
}
497
+
const skipped_i = (@as(usize, @intCast(skipped_row + row)) * self.screen_last.width) + (skipped_col + col);
498
+
self.screen_last.buf[skipped_i].skip = true;
499
+
}
500
+
}
501
+
}
502
+
503
// reposition the cursor, if needed
504
if (reposition) {
505
reposition = false;
506
+
link = .{};
507
if (self.state.alt_screen)
508
try tty.print(ctlseqs.cup, .{ row + 1, col + 1 })
509
else {
···
513
try tty.print(ctlseqs.cuf, .{n});
514
} else {
515
const n = row - cursor_pos.row;
516
+
for (0..n) |_| {
517
+
try tty.writeByte('\n');
518
+
}
519
try tty.writeByte('\r');
520
if (col > 0)
521
try tty.print(ctlseqs.cuf, .{col});
···
618
}
619
},
620
.rgb => |rgb| {
621
+
if (self.enable_workarounds)
622
+
try tty.print(ctlseqs.ul_rgb_conpty, .{ rgb[0], rgb[1], rgb[2] })
623
+
else switch (self.sgr) {
624
.standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
625
.legacy => try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
626
}
···
712
}
713
try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
714
}
715
+
716
+
// scale
717
+
if (self.caps.scaled_text and !cell.scale.eql(.{})) {
718
+
const scale = cell.scale;
719
+
// We have a scaled cell.
720
+
switch (cell.scale.denominator) {
721
+
// Denominator cannot be 0
722
+
0 => unreachable,
723
+
1 => {
724
+
// no fractional scaling, just a straight scale factor
725
+
try tty.print(
726
+
ctlseqs.scaled_text,
727
+
.{ scale.scale, w, cell.char.grapheme },
728
+
);
729
+
},
730
+
else => {
731
+
// fractional scaling
732
+
// no fractional scaling, just a straight scale factor
733
+
try tty.print(
734
+
ctlseqs.scaled_text_with_fractions,
735
+
.{
736
+
scale.scale,
737
+
w,
738
+
scale.numerator,
739
+
scale.denominator,
740
+
@intFromEnum(scale.vertical_alignment),
741
+
cell.char.grapheme,
742
+
},
743
+
);
744
+
},
745
+
}
746
+
cursor_pos.col = col + (w * scale.scale);
747
+
cursor_pos.row = row;
748
+
continue;
749
+
}
750
+
751
+
// If we have explicit width and our width is greater than 1, let's use it
752
+
if (self.caps.explicit_width and w > 1) {
753
+
try tty.print(ctlseqs.explicit_width, .{ w, cell.char.grapheme });
754
+
} else {
755
+
try tty.writeAll(cell.char.grapheme);
756
+
}
757
cursor_pos.col = col + w;
758
cursor_pos.row = row;
759
}
760
+
if (!started) return;
761
if (self.screen.cursor_vis) {
762
if (self.state.alt_screen) {
763
try tty.print(
···
770
} else {
771
// TODO: position cursor relative to current location
772
try tty.writeByte('\r');
773
+
if (self.screen.cursor_row >= cursor_pos.row) {
774
+
for (0..(self.screen.cursor_row - cursor_pos.row)) |_| {
775
+
try tty.writeByte('\n');
776
+
}
777
+
} else {
778
+
for (0..(cursor_pos.row - self.screen.cursor_row)) |_| {
779
+
try tty.writeAll(ctlseqs.ri);
780
+
}
781
+
}
782
if (self.screen.cursor_col > 0)
783
try tty.print(ctlseqs.cuf, .{self.screen.cursor_col});
784
}
···
789
self.state.cursor.row = cursor_pos.row;
790
self.state.cursor.col = cursor_pos.col;
791
}
792
+
self.screen_last.cursor_vis = self.screen.cursor_vis;
793
if (self.screen.mouse_shape != self.screen_last.mouse_shape) {
794
try tty.print(
795
ctlseqs.osc22_mouse_shape,
···
804
);
805
self.screen_last.cursor_shape = self.screen.cursor_shape;
806
}
807
+
808
+
try tty.writeAll(ctlseqs.sync_reset);
809
+
try tty.flush();
810
}
811
812
+
fn enableKittyKeyboard(self: *Vaxis, tty: *IoWriter, flags: Key.KittyFlags) !void {
813
const flag_int: u5 = @bitCast(flags);
814
try tty.print(ctlseqs.csi_u_push, .{flag_int});
815
+
try tty.flush();
816
self.state.kitty_keyboard = true;
817
}
818
819
/// send a system notification
820
+
pub fn notify(_: *Vaxis, tty: *IoWriter, title: ?[]const u8, body: []const u8) !void {
821
if (title) |t|
822
try tty.print(ctlseqs.osc777_notify, .{ t, body })
823
else
824
try tty.print(ctlseqs.osc9_notify, .{body});
825
+
826
+
try tty.flush();
827
}
828
829
/// sets the window title
830
+
pub fn setTitle(_: *Vaxis, tty: *IoWriter, title: []const u8) !void {
831
try tty.print(ctlseqs.osc2_set_title, .{title});
832
+
try tty.flush();
833
}
834
835
// turn bracketed paste on or off. An event will be sent at the
836
// beginning and end of a detected paste. All keystrokes between these
837
// events were pasted
838
+
pub fn setBracketedPaste(self: *Vaxis, tty: *IoWriter, enable: bool) !void {
839
const seq = if (enable)
840
ctlseqs.bp_set
841
else
842
ctlseqs.bp_reset;
843
try tty.writeAll(seq);
844
+
try tty.flush();
845
self.state.bracketed_paste = enable;
846
}
847
···
851
}
852
853
/// Change the mouse reporting mode
854
+
pub fn setMouseMode(self: *Vaxis, tty: *IoWriter, enable: bool) !void {
855
if (enable) {
856
self.state.mouse = true;
857
if (self.caps.sgr_pixels) {
···
865
} else {
866
try tty.writeAll(ctlseqs.mouse_reset);
867
}
868
+
869
+
try tty.flush();
870
}
871
872
/// Translate pixel mouse coordinates to cell + offset
···
880
const ypos = mouse.row;
881
const xextra = self.screen.width_pix % self.screen.width;
882
const yextra = self.screen.height_pix % self.screen.height;
883
+
const xcell: i16 = @intCast((self.screen.width_pix - xextra) / self.screen.width);
884
+
const ycell: i16 = @intCast((self.screen.height_pix - yextra) / self.screen.height);
885
+
if (xcell == 0 or ycell == 0) return mouse;
886
+
result.col = @divFloor(xpos, xcell);
887
+
result.row = @divFloor(ypos, ycell);
888
+
result.xoffset = @intCast(@mod(xpos, xcell));
889
+
result.yoffset = @intCast(@mod(ypos, ycell));
890
}
891
return result;
892
}
893
894
+
/// Transmit an image using the local filesystem. Allocates only for base64 encoding
895
+
pub fn transmitLocalImagePath(
896
+
self: *Vaxis,
897
+
allocator: std.mem.Allocator,
898
+
tty: *IoWriter,
899
+
payload: []const u8,
900
+
width: u16,
901
+
height: u16,
902
+
medium: Image.TransmitMedium,
903
+
format: Image.TransmitFormat,
904
+
) !Image {
905
+
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
906
+
907
+
defer self.next_img_id += 1;
908
+
909
+
const id = self.next_img_id;
910
+
911
+
const size = base64Encoder.calcSize(payload.len);
912
+
if (size >= 4096) return error.PathTooLong;
913
+
914
+
const buf = try allocator.alloc(u8, size);
915
+
const encoded = base64Encoder.encode(buf, payload);
916
+
defer allocator.free(buf);
917
+
918
+
const medium_char: u8 = switch (medium) {
919
+
.file => 'f',
920
+
.temp_file => 't',
921
+
.shared_mem => 's',
922
+
};
923
+
924
+
switch (format) {
925
+
.rgb => {
926
+
try tty.print(
927
+
"\x1b_Gf=24,s={d},v={d},i={d},t={c};{s}\x1b\\",
928
+
.{ width, height, id, medium_char, encoded },
929
+
);
930
+
},
931
+
.rgba => {
932
+
try tty.print(
933
+
"\x1b_Gf=32,s={d},v={d},i={d},t={c};{s}\x1b\\",
934
+
.{ width, height, id, medium_char, encoded },
935
+
);
936
+
},
937
+
.png => {
938
+
try tty.print(
939
+
"\x1b_Gf=100,i={d},t={c};{s}\x1b\\",
940
+
.{ id, medium_char, encoded },
941
+
);
942
+
},
943
+
}
944
+
945
+
try tty.flush();
946
+
return .{
947
+
.id = id,
948
+
.width = width,
949
+
.height = height,
950
+
};
951
+
}
952
+
953
/// Transmit an image which has been pre-base64 encoded
954
pub fn transmitPreEncodedImage(
955
self: *Vaxis,
956
+
tty: *IoWriter,
957
bytes: []const u8,
958
+
width: u16,
959
+
height: u16,
960
format: Image.TransmitFormat,
961
) !Image {
962
+
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
963
+
964
defer self.next_img_id += 1;
965
const id = self.next_img_id;
966
···
1000
);
1001
}
1002
}
1003
+
1004
+
try tty.flush();
1005
return .{
1006
.id = id,
1007
.width = width,
···
1012
pub fn transmitImage(
1013
self: *Vaxis,
1014
alloc: std.mem.Allocator,
1015
+
tty: *IoWriter,
1016
img: *zigimg.Image,
1017
format: Image.TransmitFormat,
1018
) !Image {
···
1024
const buf = switch (format) {
1025
.png => png: {
1026
const png_buf = try arena.allocator().alloc(u8, img.imageByteSize());
1027
+
const png = try img.writeToMemory(arena.allocator(), png_buf, .{ .png = .{} });
1028
break :png png;
1029
},
1030
.rgb => rgb: {
1031
+
try img.convert(arena.allocator(), .rgb24);
1032
break :rgb img.rawBytes();
1033
},
1034
.rgba => rgba: {
1035
+
try img.convert(arena.allocator(), .rgba32);
1036
break :rgba img.rawBytes();
1037
},
1038
};
···
1040
const b64_buf = try arena.allocator().alloc(u8, base64Encoder.calcSize(buf.len));
1041
const encoded = base64Encoder.encode(b64_buf, buf);
1042
1043
+
return self.transmitPreEncodedImage(tty, encoded, @intCast(img.width), @intCast(img.height), format);
1044
}
1045
1046
pub fn loadImage(
1047
self: *Vaxis,
1048
alloc: std.mem.Allocator,
1049
+
tty: *IoWriter,
1050
src: Image.Source,
1051
) !Image {
1052
if (!self.caps.kitty_graphics) return error.NoGraphicsCapability;
1053
1054
+
var read_buffer: [1024 * 1024]u8 = undefined; // 1MB buffer
1055
var img = switch (src) {
1056
+
.path => |path| try zigimg.Image.fromFilePath(alloc, path, &read_buffer),
1057
.mem => |bytes| try zigimg.Image.fromMemory(alloc, bytes),
1058
};
1059
+
defer img.deinit(alloc);
1060
return self.transmitImage(alloc, tty, &img, .png);
1061
}
1062
1063
/// deletes an image from the terminal's memory
1064
+
pub fn freeImage(_: Vaxis, tty: *IoWriter, id: u32) void {
1065
tty.print("\x1b_Ga=d,d=I,i={d};\x1b\\", .{id}) catch |err| {
1066
log.err("couldn't delete image {d}: {}", .{ id, err });
1067
return;
1068
};
1069
+
tty.flush() catch {};
1070
}
1071
1072
+
pub fn copyToSystemClipboard(_: Vaxis, tty: *IoWriter, text: []const u8, encode_allocator: std.mem.Allocator) !void {
1073
const encoder = std.base64.standard.Encoder;
1074
const size = encoder.calcSize(text.len);
1075
const buf = try encode_allocator.alloc(u8, size);
···
1079
ctlseqs.osc52_clipboard_copy,
1080
.{b64},
1081
);
1082
+
1083
+
try tty.flush();
1084
}
1085
1086
+
pub fn requestSystemClipboard(self: Vaxis, tty: *IoWriter) !void {
1087
if (self.opts.system_clipboard_allocator == null) return error.NoClipboardAllocator;
1088
try tty.print(
1089
ctlseqs.osc52_clipboard_request,
1090
.{},
1091
);
1092
+
try tty.flush();
1093
+
}
1094
+
1095
+
/// Set the default terminal foreground color
1096
+
pub fn setTerminalForegroundColor(self: *Vaxis, tty: *IoWriter, rgb: [3]u8) !void {
1097
+
try tty.print(ctlseqs.osc10_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] });
1098
+
try tty.flush();
1099
+
self.state.changed_default_fg = true;
1100
+
}
1101
+
1102
+
/// Set the default terminal background color
1103
+
pub fn setTerminalBackgroundColor(self: *Vaxis, tty: *IoWriter, rgb: [3]u8) !void {
1104
+
try tty.print(ctlseqs.osc11_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] });
1105
+
try tty.flush();
1106
+
self.state.changed_default_bg = true;
1107
+
}
1108
+
1109
+
/// Set the terminal cursor color
1110
+
pub fn setTerminalCursorColor(self: *Vaxis, tty: *IoWriter, rgb: [3]u8) !void {
1111
+
try tty.print(ctlseqs.osc12_set, .{ rgb[0], rgb[0], rgb[1], rgb[1], rgb[2], rgb[2] });
1112
+
try tty.flush();
1113
+
self.state.changed_cursor_color = true;
1114
}
1115
1116
/// Request a color report from the terminal. Note: not all terminals support
1117
/// reporting colors. It is always safe to try, but you may not receive a
1118
/// response.
1119
+
pub fn queryColor(_: Vaxis, tty: *IoWriter, kind: Cell.Color.Kind) !void {
1120
switch (kind) {
1121
.fg => try tty.writeAll(ctlseqs.osc10_query),
1122
.bg => try tty.writeAll(ctlseqs.osc11_query),
1123
.cursor => try tty.writeAll(ctlseqs.osc12_query),
1124
.index => |idx| try tty.print(ctlseqs.osc4_query, .{idx}),
1125
}
1126
+
try tty.flush();
1127
}
1128
1129
/// Subscribe to color theme updates. A `color_scheme: Color.Scheme` tag must
···
1131
/// capability. Support can be detected by checking the value of
1132
/// vaxis.caps.color_scheme_updates. The initial scheme will be reported when
1133
/// subscribing.
1134
+
pub fn subscribeToColorSchemeUpdates(self: *Vaxis, tty: *IoWriter) !void {
1135
try tty.writeAll(ctlseqs.color_scheme_request);
1136
try tty.writeAll(ctlseqs.color_scheme_set);
1137
+
try tty.flush();
1138
self.state.color_scheme_updates = true;
1139
}
1140
1141
+
pub fn deviceStatusReport(_: Vaxis, tty: *IoWriter) !void {
1142
try tty.writeAll(ctlseqs.device_status_report);
1143
+
try tty.flush();
1144
+
}
1145
+
1146
+
/// prettyPrint is used to print the contents of the Screen to the tty. The state is not stored, and
1147
+
/// the cursor will be put on the next line after the last line is printed. This is useful to
1148
+
/// sequentially print data in a styled format to eg. stdout. This function returns an error if you
1149
+
/// are not in the alt screen. The cursor is always hidden, and mouse shapes are not available
1150
+
pub fn prettyPrint(self: *Vaxis, tty: *IoWriter) !void {
1151
+
if (self.state.alt_screen) return error.NotInPrimaryScreen;
1152
+
1153
+
try tty.writeAll(ctlseqs.hide_cursor);
1154
+
try tty.writeAll(ctlseqs.sync_set);
1155
+
defer tty.writeAll(ctlseqs.sync_reset) catch {};
1156
+
try tty.writeAll(ctlseqs.sgr_reset);
1157
+
defer tty.writeAll(ctlseqs.sgr_reset) catch {};
1158
+
1159
+
var reposition: bool = false;
1160
+
var row: u16 = 0;
1161
+
var col: u16 = 0;
1162
+
var cursor: Style = .{};
1163
+
var link: Hyperlink = .{};
1164
+
var cursor_pos: struct {
1165
+
row: u16 = 0,
1166
+
col: u16 = 0,
1167
+
} = .{};
1168
+
1169
+
var i: u16 = 0;
1170
+
while (i < self.screen.buf.len) {
1171
+
const cell = self.screen.buf[i];
1172
+
const w = blk: {
1173
+
if (cell.char.width != 0) break :blk cell.char.width;
1174
+
1175
+
const method: gwidth.Method = self.caps.unicode;
1176
+
const width = gwidth.gwidth(cell.char.grapheme, method);
1177
+
break :blk @max(1, width);
1178
+
};
1179
+
defer {
1180
+
// advance by the width of this char mod 1
1181
+
std.debug.assert(w > 0);
1182
+
var j = i + 1;
1183
+
while (j < i + w) : (j += 1) {
1184
+
if (j >= self.screen_last.buf.len) break;
1185
+
self.screen_last.buf[j].skipped = true;
1186
+
}
1187
+
col += w;
1188
+
i += w;
1189
+
}
1190
+
if (col >= self.screen.width) {
1191
+
row += 1;
1192
+
col = 0;
1193
+
// Rely on terminal wrapping to reposition into next row instead of forcing it
1194
+
if (!cell.wrapped)
1195
+
reposition = true;
1196
+
}
1197
+
if (cell.default) {
1198
+
reposition = true;
1199
+
continue;
1200
+
}
1201
+
defer {
1202
+
cursor = cell.style;
1203
+
link = cell.link;
1204
+
}
1205
+
1206
+
// reposition the cursor, if needed
1207
+
if (reposition) {
1208
+
reposition = false;
1209
+
link = .{};
1210
+
if (cursor_pos.row == row) {
1211
+
const n = col - cursor_pos.col;
1212
+
if (n > 0)
1213
+
try tty.print(ctlseqs.cuf, .{n});
1214
+
} else {
1215
+
const n = row - cursor_pos.row;
1216
+
for (0..n) |_| {
1217
+
try tty.writeByte('\n');
1218
+
}
1219
+
try tty.writeByte('\r');
1220
+
if (col > 0)
1221
+
try tty.print(ctlseqs.cuf, .{col});
1222
+
}
1223
+
}
1224
+
1225
+
if (cell.image) |img| {
1226
+
try tty.print(
1227
+
ctlseqs.kitty_graphics_preamble,
1228
+
.{img.img_id},
1229
+
);
1230
+
if (img.options.pixel_offset) |offset| {
1231
+
try tty.print(
1232
+
",X={d},Y={d}",
1233
+
.{ offset.x, offset.y },
1234
+
);
1235
+
}
1236
+
if (img.options.clip_region) |clip| {
1237
+
if (clip.x) |x|
1238
+
try tty.print(",x={d}", .{x});
1239
+
if (clip.y) |y|
1240
+
try tty.print(",y={d}", .{y});
1241
+
if (clip.width) |width|
1242
+
try tty.print(",w={d}", .{width});
1243
+
if (clip.height) |height|
1244
+
try tty.print(",h={d}", .{height});
1245
+
}
1246
+
if (img.options.size) |size| {
1247
+
if (size.rows) |rows|
1248
+
try tty.print(",r={d}", .{rows});
1249
+
if (size.cols) |cols|
1250
+
try tty.print(",c={d}", .{cols});
1251
+
}
1252
+
if (img.options.z_index) |z| {
1253
+
try tty.print(",z={d}", .{z});
1254
+
}
1255
+
try tty.writeAll(ctlseqs.kitty_graphics_closing);
1256
+
}
1257
+
1258
+
// something is different, so let's loop through everything and
1259
+
// find out what
1260
+
1261
+
// foreground
1262
+
if (!Cell.Color.eql(cursor.fg, cell.style.fg)) {
1263
+
switch (cell.style.fg) {
1264
+
.default => try tty.writeAll(ctlseqs.fg_reset),
1265
+
.index => |idx| {
1266
+
switch (idx) {
1267
+
0...7 => try tty.print(ctlseqs.fg_base, .{idx}),
1268
+
8...15 => try tty.print(ctlseqs.fg_bright, .{idx - 8}),
1269
+
else => {
1270
+
switch (self.sgr) {
1271
+
.standard => try tty.print(ctlseqs.fg_indexed, .{idx}),
1272
+
.legacy => try tty.print(ctlseqs.fg_indexed_legacy, .{idx}),
1273
+
}
1274
+
},
1275
+
}
1276
+
},
1277
+
.rgb => |rgb| {
1278
+
switch (self.sgr) {
1279
+
.standard => try tty.print(ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1280
+
.legacy => try tty.print(ctlseqs.fg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
1281
+
}
1282
+
},
1283
+
}
1284
+
}
1285
+
// background
1286
+
if (!Cell.Color.eql(cursor.bg, cell.style.bg)) {
1287
+
switch (cell.style.bg) {
1288
+
.default => try tty.writeAll(ctlseqs.bg_reset),
1289
+
.index => |idx| {
1290
+
switch (idx) {
1291
+
0...7 => try tty.print(ctlseqs.bg_base, .{idx}),
1292
+
8...15 => try tty.print(ctlseqs.bg_bright, .{idx - 8}),
1293
+
else => {
1294
+
switch (self.sgr) {
1295
+
.standard => try tty.print(ctlseqs.bg_indexed, .{idx}),
1296
+
.legacy => try tty.print(ctlseqs.bg_indexed_legacy, .{idx}),
1297
+
}
1298
+
},
1299
+
}
1300
+
},
1301
+
.rgb => |rgb| {
1302
+
switch (self.sgr) {
1303
+
.standard => try tty.print(ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1304
+
.legacy => try tty.print(ctlseqs.bg_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] }),
1305
+
}
1306
+
},
1307
+
}
1308
+
}
1309
+
// underline color
1310
+
if (!Cell.Color.eql(cursor.ul, cell.style.ul)) {
1311
+
switch (cell.style.ul) {
1312
+
.default => try tty.writeAll(ctlseqs.ul_reset),
1313
+
.index => |idx| {
1314
+
switch (self.sgr) {
1315
+
.standard => try tty.print(ctlseqs.ul_indexed, .{idx}),
1316
+
.legacy => try tty.print(ctlseqs.ul_indexed_legacy, .{idx}),
1317
+
}
1318
+
},
1319
+
.rgb => |rgb| {
1320
+
if (self.enable_workarounds)
1321
+
try tty.print(ctlseqs.ul_rgb_conpty, .{ rgb[0], rgb[1], rgb[2] })
1322
+
else switch (self.sgr) {
1323
+
.standard => try tty.print(ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }),
1324
+
.legacy => {
1325
+
try tty.print(ctlseqs.ul_rgb_legacy, .{ rgb[0], rgb[1], rgb[2] });
1326
+
},
1327
+
}
1328
+
},
1329
+
}
1330
+
}
1331
+
// underline style
1332
+
if (cursor.ul_style != cell.style.ul_style) {
1333
+
const seq = switch (cell.style.ul_style) {
1334
+
.off => ctlseqs.ul_off,
1335
+
.single => ctlseqs.ul_single,
1336
+
.double => ctlseqs.ul_double,
1337
+
.curly => ctlseqs.ul_curly,
1338
+
.dotted => ctlseqs.ul_dotted,
1339
+
.dashed => ctlseqs.ul_dashed,
1340
+
};
1341
+
try tty.writeAll(seq);
1342
+
}
1343
+
// bold
1344
+
if (cursor.bold != cell.style.bold) {
1345
+
const seq = switch (cell.style.bold) {
1346
+
true => ctlseqs.bold_set,
1347
+
false => ctlseqs.bold_dim_reset,
1348
+
};
1349
+
try tty.writeAll(seq);
1350
+
if (cell.style.dim) {
1351
+
try tty.writeAll(ctlseqs.dim_set);
1352
+
}
1353
+
}
1354
+
// dim
1355
+
if (cursor.dim != cell.style.dim) {
1356
+
const seq = switch (cell.style.dim) {
1357
+
true => ctlseqs.dim_set,
1358
+
false => ctlseqs.bold_dim_reset,
1359
+
};
1360
+
try tty.writeAll(seq);
1361
+
if (cell.style.bold) {
1362
+
try tty.writeAll(ctlseqs.bold_set);
1363
+
}
1364
+
}
1365
+
// dim
1366
+
if (cursor.italic != cell.style.italic) {
1367
+
const seq = switch (cell.style.italic) {
1368
+
true => ctlseqs.italic_set,
1369
+
false => ctlseqs.italic_reset,
1370
+
};
1371
+
try tty.writeAll(seq);
1372
+
}
1373
+
// dim
1374
+
if (cursor.blink != cell.style.blink) {
1375
+
const seq = switch (cell.style.blink) {
1376
+
true => ctlseqs.blink_set,
1377
+
false => ctlseqs.blink_reset,
1378
+
};
1379
+
try tty.writeAll(seq);
1380
+
}
1381
+
// reverse
1382
+
if (cursor.reverse != cell.style.reverse) {
1383
+
const seq = switch (cell.style.reverse) {
1384
+
true => ctlseqs.reverse_set,
1385
+
false => ctlseqs.reverse_reset,
1386
+
};
1387
+
try tty.writeAll(seq);
1388
+
}
1389
+
// invisible
1390
+
if (cursor.invisible != cell.style.invisible) {
1391
+
const seq = switch (cell.style.invisible) {
1392
+
true => ctlseqs.invisible_set,
1393
+
false => ctlseqs.invisible_reset,
1394
+
};
1395
+
try tty.writeAll(seq);
1396
+
}
1397
+
// strikethrough
1398
+
if (cursor.strikethrough != cell.style.strikethrough) {
1399
+
const seq = switch (cell.style.strikethrough) {
1400
+
true => ctlseqs.strikethrough_set,
1401
+
false => ctlseqs.strikethrough_reset,
1402
+
};
1403
+
try tty.writeAll(seq);
1404
+
}
1405
+
1406
+
// url
1407
+
if (!std.mem.eql(u8, link.uri, cell.link.uri)) {
1408
+
var ps = cell.link.params;
1409
+
if (cell.link.uri.len == 0) {
1410
+
// Empty out the params no matter what if we don't have
1411
+
// a url
1412
+
ps = "";
1413
+
}
1414
+
try tty.print(ctlseqs.osc8, .{ ps, cell.link.uri });
1415
+
}
1416
+
try tty.writeAll(cell.char.grapheme);
1417
+
cursor_pos.col = col + w;
1418
+
cursor_pos.row = row;
1419
+
}
1420
+
try tty.writeAll("\r\n");
1421
+
try tty.flush();
1422
+
}
1423
+
1424
+
/// Set the terminal's current working directory
1425
+
pub fn setTerminalWorkingDirectory(_: *Vaxis, tty: *IoWriter, path: []const u8) !void {
1426
+
if (path.len == 0 or path[0] != '/')
1427
+
return error.InvalidAbsolutePath;
1428
+
const hostname = switch (builtin.os.tag) {
1429
+
.windows => null,
1430
+
else => std.posix.getenv("HOSTNAME"),
1431
+
} orelse "localhost";
1432
+
1433
+
const uri: std.Uri = .{
1434
+
.scheme = "file",
1435
+
.host = .{ .raw = hostname },
1436
+
.path = .{ .raw = path },
1437
+
};
1438
+
try tty.print(ctlseqs.osc7, .{uri.fmt(.{ .scheme = true, .authority = true, .path = true })});
1439
+
try tty.flush();
1440
+
}
1441
+
1442
+
test "render: no output when no changes" {
1443
+
var vx = try Vaxis.init(std.testing.allocator, .{});
1444
+
var deinit_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1445
+
defer deinit_writer.deinit();
1446
+
defer vx.deinit(std.testing.allocator, &deinit_writer.writer);
1447
+
1448
+
var render_writer = std.io.Writer.Allocating.init(std.testing.allocator);
1449
+
defer render_writer.deinit();
1450
+
try vx.render(&render_writer.writer);
1451
+
const output = try render_writer.toOwnedSlice();
1452
+
defer std.testing.allocator.free(output);
1453
+
try std.testing.expectEqual(@as(usize, 0), output.len);
1454
}
+167
-132
src/Window.zig
+167
-132
src/Window.zig
···
4
const Cell = @import("Cell.zig");
5
const Mouse = @import("Mouse.zig");
6
const Segment = @import("Cell.zig").Segment;
7
-
const Unicode = @import("Unicode.zig");
8
const gw = @import("gwidth.zig");
9
10
const Window = @This();
11
12
-
pub const Size = union(enum) {
13
-
expand,
14
-
limit: usize,
15
-
};
16
-
17
-
/// horizontal offset from the screen
18
-
x_off: usize,
19
-
/// vertical offset from the screen
20
-
y_off: usize,
21
/// width of the window. This can't be larger than the terminal screen
22
-
width: usize,
23
/// height of the window. This can't be larger than the terminal screen
24
-
height: usize,
25
26
screen: *Screen,
27
28
-
/// Deprecated. Use `child` instead
29
-
///
30
/// Creates a new window with offset relative to parent and size clamped to the
31
/// parent's size. Windows do not retain a reference to their parent and are
32
/// unaware of resizes.
33
-
pub fn initChild(
34
self: Window,
35
-
x_off: usize,
36
-
y_off: usize,
37
-
width: Size,
38
-
height: Size,
39
) Window {
40
-
const resolved_width = switch (width) {
41
-
.expand => self.width -| x_off,
42
-
.limit => |w| blk: {
43
-
if (w + x_off > self.width) {
44
-
break :blk self.width -| x_off;
45
-
}
46
-
break :blk w;
47
-
},
48
-
};
49
-
const resolved_height = switch (height) {
50
-
.expand => self.height -| y_off,
51
-
.limit => |h| blk: {
52
-
if (h + y_off > self.height) {
53
-
break :blk self.height -| y_off;
54
-
}
55
-
break :blk h;
56
-
},
57
-
};
58
return Window{
59
.x_off = x_off + self.x_off,
60
.y_off = y_off + self.y_off,
61
-
.width = resolved_width,
62
-
.height = resolved_height,
63
.screen = self.screen,
64
};
65
}
66
67
pub const ChildOptions = struct {
68
-
x_off: usize = 0,
69
-
y_off: usize = 0,
70
/// the width of the resulting child, including any borders
71
-
width: Size = .expand,
72
/// the height of the resulting child, including any borders
73
-
height: Size = .expand,
74
border: BorderOptions = .{},
75
};
76
···
143
.other => |loc| loc,
144
};
145
if (loc.top) {
146
-
var i: usize = 0;
147
while (i < w) : (i += 1) {
148
result.writeCell(i, 0, .{ .char = horizontal, .style = style });
149
}
150
}
151
if (loc.bottom) {
152
-
var i: usize = 0;
153
while (i < w) : (i += 1) {
154
result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
155
}
156
}
157
if (loc.left) {
158
-
var i: usize = 0;
159
while (i < h) : (i += 1) {
160
result.writeCell(0, i, .{ .char = vertical, .style = style });
161
}
162
}
163
if (loc.right) {
164
-
var i: usize = 0;
165
while (i < h) : (i += 1) {
166
result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
167
}
···
170
if (loc.top and loc.left)
171
result.writeCell(0, 0, .{ .char = top_left, .style = style });
172
if (loc.top and loc.right)
173
-
result.writeCell(w - 1, 0, .{ .char = top_right, .style = style });
174
if (loc.bottom and loc.left)
175
result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
176
if (loc.bottom and loc.right)
177
-
result.writeCell(w - 1, h -| 1, .{ .char = bottom_right, .style = style });
178
179
-
const x_off: usize = if (loc.left) 1 else 0;
180
-
const y_off: usize = if (loc.top) 1 else 0;
181
-
const h_delt: usize = if (loc.bottom) 1 else 0;
182
-
const w_delt: usize = if (loc.right) 1 else 0;
183
-
const h_ch: usize = h -| y_off -| h_delt;
184
-
const w_ch: usize = w -| x_off -| w_delt;
185
-
return result.initChild(x_off, y_off, .{ .limit = w_ch }, .{ .limit = h_ch });
186
}
187
188
/// writes a cell to the location in the window
189
-
pub fn writeCell(self: Window, col: usize, row: usize, cell: Cell) void {
190
-
if (self.height == 0 or self.width == 0) return;
191
-
if (self.height <= row or self.width <= col) return;
192
-
self.screen.writeCell(col + self.x_off, row + self.y_off, cell);
193
}
194
195
/// reads a cell at the location in the window
196
-
pub fn readCell(self: Window, col: usize, row: usize) ?Cell {
197
-
if (self.height == 0 or self.width == 0) return null;
198
-
if (self.height <= row or self.width <= col) return null;
199
-
return self.screen.readCell(col + self.x_off, row + self.y_off);
200
}
201
202
/// fills the window with the default cell
···
205
}
206
207
/// returns the width of the grapheme. This depends on the terminal capabilities
208
-
pub fn gwidth(self: Window, str: []const u8) usize {
209
-
return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1;
210
}
211
212
/// fills the window with the provided cell
213
pub fn fill(self: Window, cell: Cell) void {
214
-
if (self.screen.width < self.x_off)
215
return;
216
-
if (self.screen.height < self.y_off)
217
-
return;
218
if (self.x_off == 0 and self.width == self.screen.width) {
219
// we have a full width window, therefore contiguous memory.
220
-
const start = self.y_off * self.width;
221
-
const end = start + (self.height * self.width);
222
@memset(self.screen.buf[start..end], cell);
223
} else {
224
// Non-contiguous. Iterate over rows an memset
225
-
var row: usize = self.y_off;
226
const last_row = @min(self.height + self.y_off, self.screen.height);
227
while (row < last_row) : (row += 1) {
228
-
const start = self.x_off + (row * self.screen.width);
229
-
const end = @min(start + self.width, start + (self.screen.width - self.x_off));
230
@memset(self.screen.buf[start..end], cell);
231
}
232
}
···
238
}
239
240
/// show the cursor at the given coordinates, 0 indexed
241
-
pub fn showCursor(self: Window, col: usize, row: usize) void {
242
-
if (self.height == 0 or self.width == 0) return;
243
-
if (self.height <= row or self.width <= col) return;
244
self.screen.cursor_vis = true;
245
-
self.screen.cursor_row = row + self.y_off;
246
-
self.screen.cursor_col = col + self.x_off;
247
}
248
249
pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
···
253
/// Options to use when printing Segments to a window
254
pub const PrintOptions = struct {
255
/// vertical offset to start printing at
256
-
row_offset: usize = 0,
257
/// horizontal offset to start printing at
258
-
col_offset: usize = 0,
259
260
/// wrap behavior for printing
261
wrap: enum {
···
274
};
275
276
pub const PrintResult = struct {
277
-
col: usize,
278
-
row: usize,
279
overflow: bool,
280
};
281
282
/// prints segments to the window. returns true if the text overflowed with the
283
/// given wrap strategy and size.
284
-
pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) !PrintResult {
285
var row = opts.row_offset;
286
switch (opts.wrap) {
287
.grapheme => {
288
-
var col: usize = opts.col_offset;
289
const overflow: bool = blk: for (segments) |segment| {
290
-
var iter = self.screen.unicode.graphemeIterator(segment.text);
291
while (iter.next()) |grapheme| {
292
if (col >= self.width) {
293
row += 1;
···
305
if (opts.commit) self.writeCell(col, row, .{
306
.char = .{
307
.grapheme = s,
308
-
.width = w,
309
},
310
.style = segment.style,
311
.link = segment.link,
···
325
};
326
},
327
.word => {
328
-
var col: usize = opts.col_offset;
329
var overflow: bool = false;
330
var soft_wrapped: bool = false;
331
outer: for (segments) |segment| {
···
370
col = 0;
371
}
372
373
-
var grapheme_iterator = self.screen.unicode.graphemeIterator(word);
374
while (grapheme_iterator.next()) |grapheme| {
375
soft_wrapped = false;
376
if (row >= self.height) {
···
382
if (opts.commit) self.writeCell(col, row, .{
383
.char = .{
384
.grapheme = s,
385
-
.width = w,
386
},
387
.style = segment.style,
388
.link = segment.link,
···
407
};
408
},
409
.none => {
410
-
var col: usize = opts.col_offset;
411
const overflow: bool = blk: for (segments) |segment| {
412
-
var iter = self.screen.unicode.graphemeIterator(segment.text);
413
while (iter.next()) |grapheme| {
414
if (col >= self.width) break :blk true;
415
const s = grapheme.bytes(segment.text);
···
419
if (opts.commit) self.writeCell(col, row, .{
420
.char = .{
421
.grapheme = s,
422
-
.width = w,
423
},
424
.style = segment.style,
425
.link = segment.link,
···
438
}
439
440
/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
441
-
pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) !PrintResult {
442
return self.print(&.{segment}, opts);
443
}
444
445
/// scrolls the window down one row (IE inserts a blank row at the bottom of the
446
/// screen and shifts all rows up one)
447
-
pub fn scroll(self: Window, n: usize) void {
448
if (n > self.height) return;
449
-
var row = self.y_off;
450
while (row < self.height - n) : (row += 1) {
451
-
const dst_start = (row * self.width) + self.x_off;
452
const dst_end = dst_start + self.width;
453
454
-
const src_start = ((row + n) * self.width) + self.x_off;
455
const src_end = src_start + self.width;
456
@memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
457
}
···
475
var parent = Window{
476
.x_off = 0,
477
.y_off = 0,
478
.width = 20,
479
.height = 20,
480
.screen = undefined,
481
};
482
483
-
const ch = parent.initChild(1, 1, .expand, .expand);
484
try std.testing.expectEqual(19, ch.width);
485
try std.testing.expectEqual(19, ch.height);
486
}
···
489
var parent = Window{
490
.x_off = 0,
491
.y_off = 0,
492
.width = 20,
493
.height = 20,
494
.screen = undefined,
495
};
496
497
-
const ch = parent.initChild(0, 0, .{ .limit = 21 }, .{ .limit = 21 });
498
try std.testing.expectEqual(20, ch.width);
499
try std.testing.expectEqual(20, ch.height);
500
}
···
503
var parent = Window{
504
.x_off = 0,
505
.y_off = 0,
506
.width = 20,
507
.height = 20,
508
.screen = undefined,
509
};
510
511
-
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
512
try std.testing.expectEqual(10, ch.width);
513
try std.testing.expectEqual(10, ch.height);
514
}
···
517
var parent = Window{
518
.x_off = 1,
519
.y_off = 1,
520
.width = 20,
521
.height = 20,
522
.screen = undefined,
523
};
524
525
-
const ch = parent.initChild(10, 10, .{ .limit = 21 }, .{ .limit = 21 });
526
try std.testing.expectEqual(11, ch.x_off);
527
try std.testing.expectEqual(11, ch.y_off);
528
}
529
530
test "print: grapheme" {
531
-
const alloc = std.testing.allocator_instance.allocator();
532
-
const unicode = try Unicode.init(alloc);
533
-
defer unicode.deinit();
534
-
var screen: Screen = .{ .width_method = .unicode, .unicode = &unicode };
535
const win: Window = .{
536
.x_off = 0,
537
.y_off = 0,
538
.width = 4,
539
.height = 2,
540
.screen = &screen,
···
548
var segments = [_]Segment{
549
.{ .text = "a" },
550
};
551
-
const result = try win.print(&segments, opts);
552
try std.testing.expectEqual(1, result.col);
553
try std.testing.expectEqual(0, result.row);
554
try std.testing.expectEqual(false, result.overflow);
···
557
var segments = [_]Segment{
558
.{ .text = "abcd" },
559
};
560
-
const result = try win.print(&segments, opts);
561
try std.testing.expectEqual(0, result.col);
562
try std.testing.expectEqual(1, result.row);
563
try std.testing.expectEqual(false, result.overflow);
···
566
var segments = [_]Segment{
567
.{ .text = "abcde" },
568
};
569
-
const result = try win.print(&segments, opts);
570
try std.testing.expectEqual(1, result.col);
571
try std.testing.expectEqual(1, result.row);
572
try std.testing.expectEqual(false, result.overflow);
···
575
var segments = [_]Segment{
576
.{ .text = "abcdefgh" },
577
};
578
-
const result = try win.print(&segments, opts);
579
try std.testing.expectEqual(0, result.col);
580
try std.testing.expectEqual(2, result.row);
581
try std.testing.expectEqual(false, result.overflow);
···
584
var segments = [_]Segment{
585
.{ .text = "abcdefghi" },
586
};
587
-
const result = try win.print(&segments, opts);
588
try std.testing.expectEqual(0, result.col);
589
try std.testing.expectEqual(2, result.row);
590
try std.testing.expectEqual(true, result.overflow);
···
592
}
593
594
test "print: word" {
595
-
const alloc = std.testing.allocator_instance.allocator();
596
-
const unicode = try Unicode.init(alloc);
597
-
defer unicode.deinit();
598
var screen: Screen = .{
599
.width_method = .unicode,
600
-
.unicode = &unicode,
601
};
602
const win: Window = .{
603
.x_off = 0,
604
.y_off = 0,
605
.width = 4,
606
.height = 2,
607
.screen = &screen,
···
615
var segments = [_]Segment{
616
.{ .text = "a" },
617
};
618
-
const result = try win.print(&segments, opts);
619
try std.testing.expectEqual(1, result.col);
620
try std.testing.expectEqual(0, result.row);
621
try std.testing.expectEqual(false, result.overflow);
···
624
var segments = [_]Segment{
625
.{ .text = " " },
626
};
627
-
const result = try win.print(&segments, opts);
628
try std.testing.expectEqual(1, result.col);
629
try std.testing.expectEqual(0, result.row);
630
try std.testing.expectEqual(false, result.overflow);
···
633
var segments = [_]Segment{
634
.{ .text = " a" },
635
};
636
-
const result = try win.print(&segments, opts);
637
try std.testing.expectEqual(2, result.col);
638
try std.testing.expectEqual(0, result.row);
639
try std.testing.expectEqual(false, result.overflow);
···
642
var segments = [_]Segment{
643
.{ .text = "a b" },
644
};
645
-
const result = try win.print(&segments, opts);
646
try std.testing.expectEqual(3, result.col);
647
try std.testing.expectEqual(0, result.row);
648
try std.testing.expectEqual(false, result.overflow);
···
651
var segments = [_]Segment{
652
.{ .text = "a b c" },
653
};
654
-
const result = try win.print(&segments, opts);
655
try std.testing.expectEqual(1, result.col);
656
try std.testing.expectEqual(1, result.row);
657
try std.testing.expectEqual(false, result.overflow);
···
660
var segments = [_]Segment{
661
.{ .text = "hello" },
662
};
663
-
const result = try win.print(&segments, opts);
664
try std.testing.expectEqual(1, result.col);
665
try std.testing.expectEqual(1, result.row);
666
try std.testing.expectEqual(false, result.overflow);
···
669
var segments = [_]Segment{
670
.{ .text = "hi tim" },
671
};
672
-
const result = try win.print(&segments, opts);
673
try std.testing.expectEqual(3, result.col);
674
try std.testing.expectEqual(1, result.row);
675
try std.testing.expectEqual(false, result.overflow);
···
678
var segments = [_]Segment{
679
.{ .text = "hello tim" },
680
};
681
-
const result = try win.print(&segments, opts);
682
try std.testing.expectEqual(0, result.col);
683
try std.testing.expectEqual(2, result.row);
684
try std.testing.expectEqual(true, result.overflow);
···
687
var segments = [_]Segment{
688
.{ .text = "hello ti" },
689
};
690
-
const result = try win.print(&segments, opts);
691
try std.testing.expectEqual(0, result.col);
692
try std.testing.expectEqual(2, result.row);
693
try std.testing.expectEqual(false, result.overflow);
···
697
.{ .text = "h" },
698
.{ .text = "e" },
699
};
700
-
const result = try win.print(&segments, opts);
701
try std.testing.expectEqual(2, result.col);
702
try std.testing.expectEqual(0, result.row);
703
try std.testing.expectEqual(false, result.overflow);
···
710
.{ .text = "l" },
711
.{ .text = "o" },
712
};
713
-
const result = try win.print(&segments, opts);
714
try std.testing.expectEqual(1, result.col);
715
try std.testing.expectEqual(1, result.row);
716
try std.testing.expectEqual(false, result.overflow);
···
719
var segments = [_]Segment{
720
.{ .text = "he\n" },
721
};
722
-
const result = try win.print(&segments, opts);
723
try std.testing.expectEqual(0, result.col);
724
try std.testing.expectEqual(1, result.row);
725
try std.testing.expectEqual(false, result.overflow);
···
728
var segments = [_]Segment{
729
.{ .text = "he\n\n" },
730
};
731
-
const result = try win.print(&segments, opts);
732
try std.testing.expectEqual(0, result.col);
733
try std.testing.expectEqual(2, result.row);
734
try std.testing.expectEqual(false, result.overflow);
···
737
var segments = [_]Segment{
738
.{ .text = "not now" },
739
};
740
-
const result = try win.print(&segments, opts);
741
try std.testing.expectEqual(3, result.col);
742
try std.testing.expectEqual(1, result.row);
743
try std.testing.expectEqual(false, result.overflow);
···
746
var segments = [_]Segment{
747
.{ .text = "note now" },
748
};
749
-
const result = try win.print(&segments, opts);
750
try std.testing.expectEqual(3, result.col);
751
try std.testing.expectEqual(1, result.row);
752
try std.testing.expectEqual(false, result.overflow);
···
756
.{ .text = "note" },
757
.{ .text = " now" },
758
};
759
-
const result = try win.print(&segments, opts);
760
try std.testing.expectEqual(3, result.col);
761
try std.testing.expectEqual(1, result.row);
762
try std.testing.expectEqual(false, result.overflow);
···
766
.{ .text = "note " },
767
.{ .text = "now" },
768
};
769
-
const result = try win.print(&segments, opts);
770
try std.testing.expectEqual(3, result.col);
771
try std.testing.expectEqual(1, result.row);
772
try std.testing.expectEqual(false, result.overflow);
···
852
}
853
}
854
};
···
4
const Cell = @import("Cell.zig");
5
const Mouse = @import("Mouse.zig");
6
const Segment = @import("Cell.zig").Segment;
7
+
const unicode = @import("unicode.zig");
8
const gw = @import("gwidth.zig");
9
10
const Window = @This();
11
12
+
/// absolute horizontal offset from the screen
13
+
x_off: i17,
14
+
/// absolute vertical offset from the screen
15
+
y_off: i17,
16
+
/// relative horizontal offset, from parent window. This only accumulates if it is negative so that
17
+
/// we can clip the window correctly
18
+
parent_x_off: i17,
19
+
/// relative vertical offset, from parent window. This only accumulates if it is negative so that
20
+
/// we can clip the window correctly
21
+
parent_y_off: i17,
22
/// width of the window. This can't be larger than the terminal screen
23
+
width: u16,
24
/// height of the window. This can't be larger than the terminal screen
25
+
height: u16,
26
27
screen: *Screen,
28
29
/// Creates a new window with offset relative to parent and size clamped to the
30
/// parent's size. Windows do not retain a reference to their parent and are
31
/// unaware of resizes.
32
+
fn initChild(
33
self: Window,
34
+
x_off: i17,
35
+
y_off: i17,
36
+
maybe_width: ?u16,
37
+
maybe_height: ?u16,
38
) Window {
39
+
const max_height = @max(self.height - y_off, 0);
40
+
const max_width = @max(self.width - x_off, 0);
41
+
const width: u16 = maybe_width orelse max_width;
42
+
const height: u16 = maybe_height orelse max_height;
43
+
44
return Window{
45
.x_off = x_off + self.x_off,
46
.y_off = y_off + self.y_off,
47
+
.parent_x_off = @min(self.parent_x_off + x_off, 0),
48
+
.parent_y_off = @min(self.parent_y_off + y_off, 0),
49
+
.width = @min(width, max_width),
50
+
.height = @min(height, max_height),
51
.screen = self.screen,
52
};
53
}
54
55
pub const ChildOptions = struct {
56
+
x_off: i17 = 0,
57
+
y_off: i17 = 0,
58
/// the width of the resulting child, including any borders
59
+
width: ?u16 = null,
60
/// the height of the resulting child, including any borders
61
+
height: ?u16 = null,
62
border: BorderOptions = .{},
63
};
64
···
131
.other => |loc| loc,
132
};
133
if (loc.top) {
134
+
var i: u16 = 0;
135
while (i < w) : (i += 1) {
136
result.writeCell(i, 0, .{ .char = horizontal, .style = style });
137
}
138
}
139
if (loc.bottom) {
140
+
var i: u16 = 0;
141
while (i < w) : (i += 1) {
142
result.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
143
}
144
}
145
if (loc.left) {
146
+
var i: u16 = 0;
147
while (i < h) : (i += 1) {
148
result.writeCell(0, i, .{ .char = vertical, .style = style });
149
}
150
}
151
if (loc.right) {
152
+
var i: u16 = 0;
153
while (i < h) : (i += 1) {
154
result.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
155
}
···
158
if (loc.top and loc.left)
159
result.writeCell(0, 0, .{ .char = top_left, .style = style });
160
if (loc.top and loc.right)
161
+
result.writeCell(w -| 1, 0, .{ .char = top_right, .style = style });
162
if (loc.bottom and loc.left)
163
result.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
164
if (loc.bottom and loc.right)
165
+
result.writeCell(w -| 1, h -| 1, .{ .char = bottom_right, .style = style });
166
167
+
const x_off: u16 = if (loc.left) 1 else 0;
168
+
const y_off: u16 = if (loc.top) 1 else 0;
169
+
const h_delt: u16 = if (loc.bottom) 1 else 0;
170
+
const w_delt: u16 = if (loc.right) 1 else 0;
171
+
const h_ch: u16 = h -| y_off -| h_delt;
172
+
const w_ch: u16 = w -| x_off -| w_delt;
173
+
return result.initChild(x_off, y_off, w_ch, h_ch);
174
}
175
176
/// writes a cell to the location in the window
177
+
pub fn writeCell(self: Window, col: u16, row: u16, cell: Cell) void {
178
+
if (self.height <= row or
179
+
self.width <= col or
180
+
self.x_off + col < 0 or
181
+
self.y_off + row < 0 or
182
+
self.parent_x_off + col < 0 or
183
+
self.parent_y_off + row < 0)
184
+
return;
185
+
186
+
self.screen.writeCell(@intCast(col + self.x_off), @intCast(row + self.y_off), cell);
187
}
188
189
/// reads a cell at the location in the window
190
+
pub fn readCell(self: Window, col: u16, row: u16) ?Cell {
191
+
if (self.height <= row or
192
+
self.width <= col or
193
+
self.x_off + col < 0 or
194
+
self.y_off + row < 0 or
195
+
self.parent_x_off + col < 0 or
196
+
self.parent_y_off + row < 0)
197
+
return null;
198
+
return self.screen.readCell(@intCast(col + self.x_off), @intCast(row + self.y_off));
199
}
200
201
/// fills the window with the default cell
···
204
}
205
206
/// returns the width of the grapheme. This depends on the terminal capabilities
207
+
pub fn gwidth(self: Window, str: []const u8) u16 {
208
+
return gw.gwidth(str, self.screen.width_method);
209
}
210
211
/// fills the window with the provided cell
212
pub fn fill(self: Window, cell: Cell) void {
213
+
if (self.x_off + self.width < 0 or
214
+
self.y_off + self.height < 0 or
215
+
self.screen.width < self.x_off or
216
+
self.screen.height < self.y_off)
217
return;
218
+
const first_row: usize = @intCast(@max(self.y_off, 0));
219
if (self.x_off == 0 and self.width == self.screen.width) {
220
// we have a full width window, therefore contiguous memory.
221
+
const start = @min(first_row * self.width, self.screen.buf.len);
222
+
const end = @min(start + (@as(usize, @intCast(self.height)) * self.width), self.screen.buf.len);
223
@memset(self.screen.buf[start..end], cell);
224
} else {
225
// Non-contiguous. Iterate over rows an memset
226
+
var row: usize = first_row;
227
+
const first_col: usize = @max(self.x_off, 0);
228
const last_row = @min(self.height + self.y_off, self.screen.height);
229
while (row < last_row) : (row += 1) {
230
+
const start = @min(first_col + (row * self.screen.width), self.screen.buf.len);
231
+
var end = @min(start + self.width, start + (self.screen.width - first_col));
232
+
end = @min(end, self.screen.buf.len);
233
@memset(self.screen.buf[start..end], cell);
234
}
235
}
···
241
}
242
243
/// show the cursor at the given coordinates, 0 indexed
244
+
pub fn showCursor(self: Window, col: u16, row: u16) void {
245
+
if (self.x_off + col < 0 or
246
+
self.y_off + row < 0 or
247
+
row >= self.height or
248
+
col >= self.width)
249
+
return;
250
self.screen.cursor_vis = true;
251
+
self.screen.cursor_row = @intCast(row + self.y_off);
252
+
self.screen.cursor_col = @intCast(col + self.x_off);
253
}
254
255
pub fn setCursorShape(self: Window, shape: Cell.CursorShape) void {
···
259
/// Options to use when printing Segments to a window
260
pub const PrintOptions = struct {
261
/// vertical offset to start printing at
262
+
row_offset: u16 = 0,
263
/// horizontal offset to start printing at
264
+
col_offset: u16 = 0,
265
266
/// wrap behavior for printing
267
wrap: enum {
···
280
};
281
282
pub const PrintResult = struct {
283
+
col: u16,
284
+
row: u16,
285
overflow: bool,
286
};
287
288
/// prints segments to the window. returns true if the text overflowed with the
289
/// given wrap strategy and size.
290
+
pub fn print(self: Window, segments: []const Segment, opts: PrintOptions) PrintResult {
291
var row = opts.row_offset;
292
switch (opts.wrap) {
293
.grapheme => {
294
+
var col: u16 = opts.col_offset;
295
const overflow: bool = blk: for (segments) |segment| {
296
+
var iter = unicode.graphemeIterator(segment.text);
297
while (iter.next()) |grapheme| {
298
if (col >= self.width) {
299
row += 1;
···
311
if (opts.commit) self.writeCell(col, row, .{
312
.char = .{
313
.grapheme = s,
314
+
.width = @intCast(w),
315
},
316
.style = segment.style,
317
.link = segment.link,
···
331
};
332
},
333
.word => {
334
+
var col: u16 = opts.col_offset;
335
var overflow: bool = false;
336
var soft_wrapped: bool = false;
337
outer: for (segments) |segment| {
···
376
col = 0;
377
}
378
379
+
var grapheme_iterator = unicode.graphemeIterator(word);
380
while (grapheme_iterator.next()) |grapheme| {
381
soft_wrapped = false;
382
if (row >= self.height) {
···
388
if (opts.commit) self.writeCell(col, row, .{
389
.char = .{
390
.grapheme = s,
391
+
.width = @intCast(w),
392
},
393
.style = segment.style,
394
.link = segment.link,
···
413
};
414
},
415
.none => {
416
+
var col: u16 = opts.col_offset;
417
const overflow: bool = blk: for (segments) |segment| {
418
+
var iter = unicode.graphemeIterator(segment.text);
419
while (iter.next()) |grapheme| {
420
if (col >= self.width) break :blk true;
421
const s = grapheme.bytes(segment.text);
···
425
if (opts.commit) self.writeCell(col, row, .{
426
.char = .{
427
.grapheme = s,
428
+
.width = @intCast(w),
429
},
430
.style = segment.style,
431
.link = segment.link,
···
444
}
445
446
/// print a single segment. This is just a shortcut for print(&.{segment}, opts)
447
+
pub fn printSegment(self: Window, segment: Segment, opts: PrintOptions) PrintResult {
448
return self.print(&.{segment}, opts);
449
}
450
451
/// scrolls the window down one row (IE inserts a blank row at the bottom of the
452
/// screen and shifts all rows up one)
453
+
pub fn scroll(self: Window, n: u16) void {
454
if (n > self.height) return;
455
+
var row: u16 = @max(self.y_off, 0);
456
+
const first_col: u16 = @max(self.x_off, 0);
457
while (row < self.height - n) : (row += 1) {
458
+
const dst_start = (row * self.screen.width) + first_col;
459
const dst_end = dst_start + self.width;
460
461
+
const src_start = ((row + n) * self.screen.width) + first_col;
462
const src_end = src_start + self.width;
463
@memcpy(self.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
464
}
···
482
var parent = Window{
483
.x_off = 0,
484
.y_off = 0,
485
+
.parent_x_off = 0,
486
+
.parent_y_off = 0,
487
.width = 20,
488
.height = 20,
489
.screen = undefined,
490
};
491
492
+
const ch = parent.initChild(1, 1, null, null);
493
try std.testing.expectEqual(19, ch.width);
494
try std.testing.expectEqual(19, ch.height);
495
}
···
498
var parent = Window{
499
.x_off = 0,
500
.y_off = 0,
501
+
.parent_x_off = 0,
502
+
.parent_y_off = 0,
503
.width = 20,
504
.height = 20,
505
.screen = undefined,
506
};
507
508
+
const ch = parent.initChild(0, 0, 21, 21);
509
try std.testing.expectEqual(20, ch.width);
510
try std.testing.expectEqual(20, ch.height);
511
}
···
514
var parent = Window{
515
.x_off = 0,
516
.y_off = 0,
517
+
.parent_x_off = 0,
518
+
.parent_y_off = 0,
519
.width = 20,
520
.height = 20,
521
.screen = undefined,
522
};
523
524
+
const ch = parent.initChild(10, 10, 21, 21);
525
try std.testing.expectEqual(10, ch.width);
526
try std.testing.expectEqual(10, ch.height);
527
}
···
530
var parent = Window{
531
.x_off = 1,
532
.y_off = 1,
533
+
.parent_x_off = 0,
534
+
.parent_y_off = 0,
535
.width = 20,
536
.height = 20,
537
.screen = undefined,
538
};
539
540
+
const ch = parent.initChild(10, 10, 21, 21);
541
try std.testing.expectEqual(11, ch.x_off);
542
try std.testing.expectEqual(11, ch.y_off);
543
}
544
545
+
test "Window offsets" {
546
+
var parent = Window{
547
+
.x_off = 0,
548
+
.y_off = 0,
549
+
.parent_x_off = 0,
550
+
.parent_y_off = 0,
551
+
.width = 20,
552
+
.height = 20,
553
+
.screen = undefined,
554
+
};
555
+
556
+
const ch = parent.initChild(10, 10, 21, 21);
557
+
const ch2 = ch.initChild(-4, -4, null, null);
558
+
// Reading ch2 at row 0 should be null
559
+
try std.testing.expect(ch2.readCell(0, 0) == null);
560
+
// Should not panic us
561
+
ch2.writeCell(0, 0, undefined);
562
+
}
563
+
564
test "print: grapheme" {
565
+
var screen: Screen = .{ .width_method = .unicode };
566
const win: Window = .{
567
.x_off = 0,
568
.y_off = 0,
569
+
.parent_x_off = 0,
570
+
.parent_y_off = 0,
571
.width = 4,
572
.height = 2,
573
.screen = &screen,
···
581
var segments = [_]Segment{
582
.{ .text = "a" },
583
};
584
+
const result = win.print(&segments, opts);
585
try std.testing.expectEqual(1, result.col);
586
try std.testing.expectEqual(0, result.row);
587
try std.testing.expectEqual(false, result.overflow);
···
590
var segments = [_]Segment{
591
.{ .text = "abcd" },
592
};
593
+
const result = win.print(&segments, opts);
594
try std.testing.expectEqual(0, result.col);
595
try std.testing.expectEqual(1, result.row);
596
try std.testing.expectEqual(false, result.overflow);
···
599
var segments = [_]Segment{
600
.{ .text = "abcde" },
601
};
602
+
const result = win.print(&segments, opts);
603
try std.testing.expectEqual(1, result.col);
604
try std.testing.expectEqual(1, result.row);
605
try std.testing.expectEqual(false, result.overflow);
···
608
var segments = [_]Segment{
609
.{ .text = "abcdefgh" },
610
};
611
+
const result = win.print(&segments, opts);
612
try std.testing.expectEqual(0, result.col);
613
try std.testing.expectEqual(2, result.row);
614
try std.testing.expectEqual(false, result.overflow);
···
617
var segments = [_]Segment{
618
.{ .text = "abcdefghi" },
619
};
620
+
const result = win.print(&segments, opts);
621
try std.testing.expectEqual(0, result.col);
622
try std.testing.expectEqual(2, result.row);
623
try std.testing.expectEqual(true, result.overflow);
···
625
}
626
627
test "print: word" {
628
var screen: Screen = .{
629
.width_method = .unicode,
630
};
631
const win: Window = .{
632
.x_off = 0,
633
.y_off = 0,
634
+
.parent_x_off = 0,
635
+
.parent_y_off = 0,
636
.width = 4,
637
.height = 2,
638
.screen = &screen,
···
646
var segments = [_]Segment{
647
.{ .text = "a" },
648
};
649
+
const result = win.print(&segments, opts);
650
try std.testing.expectEqual(1, result.col);
651
try std.testing.expectEqual(0, result.row);
652
try std.testing.expectEqual(false, result.overflow);
···
655
var segments = [_]Segment{
656
.{ .text = " " },
657
};
658
+
const result = win.print(&segments, opts);
659
try std.testing.expectEqual(1, result.col);
660
try std.testing.expectEqual(0, result.row);
661
try std.testing.expectEqual(false, result.overflow);
···
664
var segments = [_]Segment{
665
.{ .text = " a" },
666
};
667
+
const result = win.print(&segments, opts);
668
try std.testing.expectEqual(2, result.col);
669
try std.testing.expectEqual(0, result.row);
670
try std.testing.expectEqual(false, result.overflow);
···
673
var segments = [_]Segment{
674
.{ .text = "a b" },
675
};
676
+
const result = win.print(&segments, opts);
677
try std.testing.expectEqual(3, result.col);
678
try std.testing.expectEqual(0, result.row);
679
try std.testing.expectEqual(false, result.overflow);
···
682
var segments = [_]Segment{
683
.{ .text = "a b c" },
684
};
685
+
const result = win.print(&segments, opts);
686
try std.testing.expectEqual(1, result.col);
687
try std.testing.expectEqual(1, result.row);
688
try std.testing.expectEqual(false, result.overflow);
···
691
var segments = [_]Segment{
692
.{ .text = "hello" },
693
};
694
+
const result = win.print(&segments, opts);
695
try std.testing.expectEqual(1, result.col);
696
try std.testing.expectEqual(1, result.row);
697
try std.testing.expectEqual(false, result.overflow);
···
700
var segments = [_]Segment{
701
.{ .text = "hi tim" },
702
};
703
+
const result = win.print(&segments, opts);
704
try std.testing.expectEqual(3, result.col);
705
try std.testing.expectEqual(1, result.row);
706
try std.testing.expectEqual(false, result.overflow);
···
709
var segments = [_]Segment{
710
.{ .text = "hello tim" },
711
};
712
+
const result = win.print(&segments, opts);
713
try std.testing.expectEqual(0, result.col);
714
try std.testing.expectEqual(2, result.row);
715
try std.testing.expectEqual(true, result.overflow);
···
718
var segments = [_]Segment{
719
.{ .text = "hello ti" },
720
};
721
+
const result = win.print(&segments, opts);
722
try std.testing.expectEqual(0, result.col);
723
try std.testing.expectEqual(2, result.row);
724
try std.testing.expectEqual(false, result.overflow);
···
728
.{ .text = "h" },
729
.{ .text = "e" },
730
};
731
+
const result = win.print(&segments, opts);
732
try std.testing.expectEqual(2, result.col);
733
try std.testing.expectEqual(0, result.row);
734
try std.testing.expectEqual(false, result.overflow);
···
741
.{ .text = "l" },
742
.{ .text = "o" },
743
};
744
+
const result = win.print(&segments, opts);
745
try std.testing.expectEqual(1, result.col);
746
try std.testing.expectEqual(1, result.row);
747
try std.testing.expectEqual(false, result.overflow);
···
750
var segments = [_]Segment{
751
.{ .text = "he\n" },
752
};
753
+
const result = win.print(&segments, opts);
754
try std.testing.expectEqual(0, result.col);
755
try std.testing.expectEqual(1, result.row);
756
try std.testing.expectEqual(false, result.overflow);
···
759
var segments = [_]Segment{
760
.{ .text = "he\n\n" },
761
};
762
+
const result = win.print(&segments, opts);
763
try std.testing.expectEqual(0, result.col);
764
try std.testing.expectEqual(2, result.row);
765
try std.testing.expectEqual(false, result.overflow);
···
768
var segments = [_]Segment{
769
.{ .text = "not now" },
770
};
771
+
const result = win.print(&segments, opts);
772
try std.testing.expectEqual(3, result.col);
773
try std.testing.expectEqual(1, result.row);
774
try std.testing.expectEqual(false, result.overflow);
···
777
var segments = [_]Segment{
778
.{ .text = "note now" },
779
};
780
+
const result = win.print(&segments, opts);
781
try std.testing.expectEqual(3, result.col);
782
try std.testing.expectEqual(1, result.row);
783
try std.testing.expectEqual(false, result.overflow);
···
787
.{ .text = "note" },
788
.{ .text = " now" },
789
};
790
+
const result = win.print(&segments, opts);
791
try std.testing.expectEqual(3, result.col);
792
try std.testing.expectEqual(1, result.row);
793
try std.testing.expectEqual(false, result.overflow);
···
797
.{ .text = "note " },
798
.{ .text = "now" },
799
};
800
+
const result = win.print(&segments, opts);
801
try std.testing.expectEqual(3, result.col);
802
try std.testing.expectEqual(1, result.row);
803
try std.testing.expectEqual(false, result.overflow);
···
883
}
884
}
885
};
886
+
887
+
test "refAllDecls" {
888
+
std.testing.refAllDecls(@This());
889
+
}
-207
src/aio.zig
-207
src/aio.zig
···
1
-
const build_options = @import("build_options");
2
-
const builtin = @import("builtin");
3
-
const std = @import("std");
4
-
const vaxis = @import("main.zig");
5
-
const handleEventGeneric = @import("Loop.zig").handleEventGeneric;
6
-
const log = std.log.scoped(.vaxis_aio);
7
-
8
-
const Yield = enum { no_state, took_event };
9
-
10
-
pub fn Loop(T: type) type {
11
-
if (!build_options.aio) {
12
-
@compileError(
13
-
\\build_options.aio is not enabled.
14
-
\\Use `LoopWithModules` instead to provide `aio` and `coro` modules from outside vaxis.
15
-
);
16
-
}
17
-
return LoopWithModules(T, @import("aio"), @import("coro"));
18
-
}
19
-
20
-
/// zig-aio based event loop
21
-
/// <https://github.com/Cloudef/zig-aio>
22
-
pub fn LoopWithModules(T: type, aio: type, coro: type) type {
23
-
return struct {
24
-
const Event = T;
25
-
26
-
winsize_task: ?coro.Task.Generic2(winsizeTask) = null,
27
-
reader_task: ?coro.Task.Generic2(ttyReaderTask) = null,
28
-
queue: std.BoundedArray(T, 512) = .{},
29
-
source: aio.EventSource,
30
-
fatal: bool = false,
31
-
32
-
pub fn init() !@This() {
33
-
return .{ .source = try aio.EventSource.init() };
34
-
}
35
-
36
-
pub fn deinit(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) void {
37
-
vx.deviceStatusReport(tty.anyWriter()) catch {};
38
-
if (self.winsize_task) |task| task.cancel();
39
-
if (self.reader_task) |task| task.cancel();
40
-
self.source.deinit();
41
-
self.* = undefined;
42
-
}
43
-
44
-
fn winsizeInner(self: *@This(), tty: *vaxis.Tty) !void {
45
-
const Context = struct {
46
-
loop: *@TypeOf(self.*),
47
-
tty: *vaxis.Tty,
48
-
winsize: ?vaxis.Winsize = null,
49
-
fn cb(ptr: *anyopaque) void {
50
-
std.debug.assert(coro.current() == null);
51
-
const ctx: *@This() = @ptrCast(@alignCast(ptr));
52
-
ctx.winsize = vaxis.Tty.getWinsize(ctx.tty.fd) catch return;
53
-
ctx.loop.source.notify();
54
-
}
55
-
};
56
-
57
-
// keep on stack
58
-
var ctx: Context = .{ .loop = self, .tty = tty };
59
-
if (builtin.target.os.tag != .windows) {
60
-
if (@hasField(Event, "winsize")) {
61
-
const handler: vaxis.Tty.SignalHandler = .{ .context = &ctx, .callback = Context.cb };
62
-
try vaxis.Tty.notifyWinsize(handler);
63
-
}
64
-
}
65
-
66
-
while (true) {
67
-
try coro.io.single(aio.WaitEventSource{ .source = &self.source });
68
-
if (ctx.winsize) |winsize| {
69
-
if (!@hasField(Event, "winsize")) unreachable;
70
-
ctx.loop.postEvent(.{ .winsize = winsize }) catch {};
71
-
ctx.winsize = null;
72
-
}
73
-
}
74
-
}
75
-
76
-
fn winsizeTask(self: *@This(), tty: *vaxis.Tty) void {
77
-
self.winsizeInner(tty) catch |err| {
78
-
if (err != error.Canceled) log.err("winsize: {}", .{err});
79
-
self.fatal = true;
80
-
};
81
-
}
82
-
83
-
fn windowsReadEvent(tty: *vaxis.Tty) !vaxis.Event {
84
-
var state: vaxis.Tty.EventState = .{};
85
-
while (true) {
86
-
var bytes_read: usize = 0;
87
-
var input_record: vaxis.Tty.INPUT_RECORD = undefined;
88
-
try coro.io.single(aio.ReadTty{
89
-
.tty = .{ .handle = tty.stdin },
90
-
.buffer = std.mem.asBytes(&input_record),
91
-
.out_read = &bytes_read,
92
-
});
93
-
94
-
if (try tty.eventFromRecord(&input_record, &state)) |ev| {
95
-
return ev;
96
-
}
97
-
}
98
-
}
99
-
100
-
fn ttyReaderWindows(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty) !void {
101
-
var cache: vaxis.GraphemeCache = .{};
102
-
while (true) {
103
-
const event = try windowsReadEvent(tty);
104
-
try handleEventGeneric(self, vx, &cache, Event, event, null);
105
-
}
106
-
}
107
-
108
-
fn ttyReaderPosix(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) !void {
109
-
// initialize a grapheme cache
110
-
var cache: vaxis.GraphemeCache = .{};
111
-
112
-
// get our initial winsize
113
-
const winsize = try vaxis.Tty.getWinsize(tty.fd);
114
-
if (@hasField(Event, "winsize")) {
115
-
try self.postEvent(.{ .winsize = winsize });
116
-
}
117
-
118
-
var parser: vaxis.Parser = .{
119
-
.grapheme_data = &vx.unicode.grapheme_data,
120
-
};
121
-
122
-
const file: std.fs.File = .{ .handle = tty.fd };
123
-
while (true) {
124
-
var buf: [4096]u8 = undefined;
125
-
var n: usize = undefined;
126
-
var read_start: usize = 0;
127
-
try coro.io.single(aio.ReadTty{ .tty = file, .buffer = buf[read_start..], .out_read = &n });
128
-
var seq_start: usize = 0;
129
-
while (seq_start < n) {
130
-
const result = try parser.parse(buf[seq_start..n], paste_allocator);
131
-
if (result.n == 0) {
132
-
// copy the read to the beginning. We don't use memcpy because
133
-
// this could be overlapping, and it's also rare
134
-
const initial_start = seq_start;
135
-
while (seq_start < n) : (seq_start += 1) {
136
-
buf[seq_start - initial_start] = buf[seq_start];
137
-
}
138
-
read_start = seq_start - initial_start + 1;
139
-
continue;
140
-
}
141
-
read_start = 0;
142
-
seq_start += result.n;
143
-
144
-
const event = result.event orelse continue;
145
-
try handleEventGeneric(self, vx, &cache, Event, event, paste_allocator);
146
-
}
147
-
}
148
-
}
149
-
150
-
fn ttyReaderTask(self: *@This(), vx: *vaxis.Vaxis, tty: *vaxis.Tty, paste_allocator: ?std.mem.Allocator) void {
151
-
return switch (builtin.target.os.tag) {
152
-
.windows => self.ttyReaderWindows(vx, tty),
153
-
else => self.ttyReaderPosix(vx, tty, paste_allocator),
154
-
} catch |err| {
155
-
if (err != error.Canceled) log.err("ttyReader: {}", .{err});
156
-
self.fatal = true;
157
-
};
158
-
}
159
-
160
-
/// Spawns tasks to handle winsize signal and tty
161
-
pub fn spawn(
162
-
self: *@This(),
163
-
scheduler: *coro.Scheduler,
164
-
vx: *vaxis.Vaxis,
165
-
tty: *vaxis.Tty,
166
-
paste_allocator: ?std.mem.Allocator,
167
-
spawn_options: coro.Scheduler.SpawnOptions,
168
-
) coro.Scheduler.SpawnError!void {
169
-
if (self.reader_task) |_| unreachable; // programming error
170
-
// This is required even if app doesn't care about winsize
171
-
// It is because it consumes the EventSource, so it can wakeup the scheduler
172
-
// Without that custom `postEvent`'s wouldn't wake up the scheduler and UI wouldn't update
173
-
self.winsize_task = try scheduler.spawn(winsizeTask, .{ self, tty }, spawn_options);
174
-
self.reader_task = try scheduler.spawn(ttyReaderTask, .{ self, vx, tty, paste_allocator }, spawn_options);
175
-
}
176
-
177
-
pub const PopEventError = error{TtyCommunicationSevered};
178
-
179
-
/// Call this in a while loop in the main event handler until it returns null
180
-
pub fn popEvent(self: *@This()) PopEventError!?T {
181
-
if (self.fatal) return error.TtyCommunicationSevered;
182
-
defer self.winsize_task.?.wakeupIf(Yield.took_event);
183
-
defer self.reader_task.?.wakeupIf(Yield.took_event);
184
-
return self.queue.popOrNull();
185
-
}
186
-
187
-
pub const PostEventError = error{Overflow};
188
-
189
-
pub fn postEvent(self: *@This(), event: T) !void {
190
-
if (coro.current()) |_| {
191
-
while (true) {
192
-
self.queue.insert(0, event) catch {
193
-
// wait for the app to take event
194
-
try coro.yield(Yield.took_event);
195
-
continue;
196
-
};
197
-
break;
198
-
}
199
-
} else {
200
-
// queue can be full, app could handle this error by spinning the scheduler
201
-
try self.queue.insert(0, event);
202
-
}
203
-
// wakes up the scheduler, so custom events update UI
204
-
self.source.notify();
205
-
}
206
-
};
207
-
}
···
+14
src/ctlseqs.zig
+14
src/ctlseqs.zig
···
11
pub const csi_u_query = "\x1b[?u";
12
pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\";
13
pub const sixel_geometry_query = "\x1b[?2;1;0S";
14
15
// mouse. We try for button motion and any motion. terminals will enable the
16
// last one we tried (any motion). This was added because zellij doesn't
···
31
// unicode
32
pub const unicode_set = "\x1b[?2027h";
33
pub const unicode_reset = "\x1b[?2027l";
34
35
// bracketed paste
36
pub const bp_set = "\x1b[?2004h";
···
87
pub const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m";
88
pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
89
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
90
91
// Underlines
92
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
···
113
114
// OSC sequences
115
pub const osc2_set_title = "\x1b]2;{s}\x1b\\";
116
pub const osc8 = "\x1b]8;{s};{s}\x1b\\";
117
pub const osc8_clear = "\x1b]8;;\x1b\\";
118
pub const osc9_notify = "\x1b]9;{s}\x1b\\";
···
130
pub const osc4_query = "\x1b]4;{d};?\x1b\\"; // color index {d}
131
pub const osc4_reset = "\x1b]104\x1b\\"; // this resets _all_ color indexes
132
pub const osc10_query = "\x1b]10;?\x1b\\"; // fg
133
pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
134
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
135
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
136
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
137
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default
···
11
pub const csi_u_query = "\x1b[?u";
12
pub const kitty_graphics_query = "\x1b_Gi=1,a=q\x1b\\";
13
pub const sixel_geometry_query = "\x1b[?2;1;0S";
14
+
pub const cursor_position_request = "\x1b[6n";
15
+
pub const explicit_width_query = "\x1b]66;w=1; \x1b\\";
16
+
pub const scaled_text_query = "\x1b]66;s=2; \x1b\\";
17
+
pub const multi_cursor_query = "\x1b[> q";
18
19
// mouse. We try for button motion and any motion. terminals will enable the
20
// last one we tried (any motion). This was added because zellij doesn't
···
35
// unicode
36
pub const unicode_set = "\x1b[?2027h";
37
pub const unicode_reset = "\x1b[?2027l";
38
+
pub const explicit_width = "\x1b]66;w={d};{s}\x1b\\";
39
+
40
+
// text sizing
41
+
pub const scaled_text = "\x1b]66;s={d}:w={d};{s}\x1b\\";
42
+
pub const scaled_text_with_fractions = "\x1b]66;s={d}:w={d}:n={d}:d={d}:v={d};{s}\x1b\\";
43
44
// bracketed paste
45
pub const bp_set = "\x1b[?2004h";
···
96
pub const fg_rgb_legacy = "\x1b[38;2;{d};{d};{d}m";
97
pub const bg_rgb_legacy = "\x1b[48;2;{d};{d};{d}m";
98
pub const ul_rgb_legacy = "\x1b[58;2;{d};{d};{d}m";
99
+
pub const ul_rgb_conpty = "\x1b[58:2::{d}:{d}:{d}m";
100
101
// Underlines
102
pub const ul_off = "\x1b[24m"; // NOTE: this could be \x1b[4:0m but is not as widely supported
···
123
124
// OSC sequences
125
pub const osc2_set_title = "\x1b]2;{s}\x1b\\";
126
+
pub const osc7 = "\x1b]7;{f}\x1b\\";
127
pub const osc8 = "\x1b]8;{s};{s}\x1b\\";
128
pub const osc8_clear = "\x1b]8;;\x1b\\";
129
pub const osc9_notify = "\x1b]9;{s}\x1b\\";
···
141
pub const osc4_query = "\x1b]4;{d};?\x1b\\"; // color index {d}
142
pub const osc4_reset = "\x1b]104\x1b\\"; // this resets _all_ color indexes
143
pub const osc10_query = "\x1b]10;?\x1b\\"; // fg
144
+
pub const osc10_set = "\x1b]10;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal fg
145
pub const osc10_reset = "\x1b]110\x1b\\"; // reset fg to terminal default
146
pub const osc11_query = "\x1b]11;?\x1b\\"; // bg
147
+
pub const osc11_set = "\x1b]11;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set default terminal bg
148
pub const osc11_reset = "\x1b]111\x1b\\"; // reset bg to terminal default
149
pub const osc12_query = "\x1b]12;?\x1b\\"; // cursor color
150
+
pub const osc12_set = "\x1b]12;rgb:{x:0>2}{x:0>2}/{x:0>2}{x:0>2}/{x:0>2}{x:0>2}\x1b\\"; // set terminal cursor color
151
pub const osc12_reset = "\x1b]112\x1b\\"; // reset cursor to terminal default
+2
src/event.zig
+2
src/event.zig
+178
-42
src/gwidth.zig
+178
-42
src/gwidth.zig
···
1
const std = @import("std");
2
const unicode = std.unicode;
3
const testing = std.testing;
4
-
const DisplayWidth = @import("DisplayWidth");
5
-
const code_point = @import("code_point");
6
7
/// the method to use when calculating the width of a grapheme
8
pub const Method = enum {
···
11
no_zwj,
12
};
13
14
/// returns the width of the provided string, as measured by the method chosen
15
-
pub fn gwidth(str: []const u8, method: Method, data: *const DisplayWidth.DisplayWidthData) !usize {
16
switch (method) {
17
.unicode => {
18
-
const dw: DisplayWidth = .{ .data = data };
19
-
return dw.strWidth(str);
20
},
21
.wcwidth => {
22
-
var total: usize = 0;
23
-
var iter: code_point.Iterator = .{ .bytes = str };
24
while (iter.next()) |cp| {
25
-
const w = switch (cp.code) {
26
// undo an override in zg for emoji skintone selectors
27
-
0x1f3fb...0x1f3ff,
28
-
=> 2,
29
-
else => data.codePointWidth(cp.code),
30
};
31
-
if (w < 0) continue;
32
-
total += @intCast(w);
33
}
34
return total;
35
},
36
.no_zwj => {
37
-
var out: [256]u8 = undefined;
38
-
if (str.len > out.len) return error.OutOfMemory;
39
-
const n = std.mem.replacementSize(u8, str, "\u{200D}", "");
40
-
_ = std.mem.replace(u8, str, "\u{200D}", "", &out);
41
-
return gwidth(out[0..n], .unicode, data);
42
},
43
}
44
}
45
46
test "gwidth: a" {
47
-
const alloc = testing.allocator_instance.allocator();
48
-
const data = try DisplayWidth.DisplayWidthData.init(alloc);
49
-
defer data.deinit();
50
-
try testing.expectEqual(1, try gwidth("a", .unicode, &data));
51
-
try testing.expectEqual(1, try gwidth("a", .wcwidth, &data));
52
-
try testing.expectEqual(1, try gwidth("a", .no_zwj, &data));
53
}
54
55
test "gwidth: emoji with ZWJ" {
56
-
const alloc = testing.allocator_instance.allocator();
57
-
const data = try DisplayWidth.DisplayWidthData.init(alloc);
58
-
defer data.deinit();
59
-
try testing.expectEqual(2, try gwidth("๐ฉโ๐", .unicode, &data));
60
-
try testing.expectEqual(4, try gwidth("๐ฉโ๐", .wcwidth, &data));
61
-
try testing.expectEqual(4, try gwidth("๐ฉโ๐", .no_zwj, &data));
62
}
63
64
test "gwidth: emoji with VS16 selector" {
65
-
const alloc = testing.allocator_instance.allocator();
66
-
const data = try DisplayWidth.DisplayWidthData.init(alloc);
67
-
defer data.deinit();
68
-
try testing.expectEqual(2, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode, &data));
69
-
try testing.expectEqual(1, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth, &data));
70
-
try testing.expectEqual(2, try gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj, &data));
71
}
72
73
test "gwidth: emoji with skin tone selector" {
74
-
const alloc = testing.allocator_instance.allocator();
75
-
const data = try DisplayWidth.DisplayWidthData.init(alloc);
76
-
defer data.deinit();
77
-
try testing.expectEqual(2, try gwidth("๐๐ฟ", .unicode, &data));
78
-
try testing.expectEqual(4, try gwidth("๐๐ฟ", .wcwidth, &data));
79
-
try testing.expectEqual(2, try gwidth("๐๐ฟ", .no_zwj, &data));
80
}
···
1
const std = @import("std");
2
const unicode = std.unicode;
3
const testing = std.testing;
4
+
const uucode = @import("uucode");
5
6
/// the method to use when calculating the width of a grapheme
7
pub const Method = enum {
···
10
no_zwj,
11
};
12
13
+
/// Calculate width from east asian width property and Unicode properties
14
+
fn eawToWidth(cp: u21, eaw: uucode.types.EastAsianWidth) i16 {
15
+
// Based on wcwidth implementation
16
+
// Control characters
17
+
if (cp == 0) return 0;
18
+
if (cp < 32 or (cp >= 0x7f and cp < 0xa0)) return -1;
19
+
20
+
// Use general category for comprehensive zero-width detection
21
+
const gc = uucode.get(.general_category, cp);
22
+
switch (gc) {
23
+
.mark_nonspacing, .mark_enclosing => return 0,
24
+
else => {},
25
+
}
26
+
27
+
// Additional zero-width characters not covered by general category
28
+
if (cp == 0x00ad) return 0; // soft hyphen
29
+
if (cp == 0x200b) return 0; // zero-width space
30
+
if (cp == 0x200c) return 0; // zero-width non-joiner
31
+
if (cp == 0x200d) return 0; // zero-width joiner
32
+
if (cp == 0x2060) return 0; // word joiner
33
+
if (cp == 0x034f) return 0; // combining grapheme joiner
34
+
if (cp == 0xfeff) return 0; // zero-width no-break space (BOM)
35
+
if (cp >= 0x180b and cp <= 0x180d) return 0; // Mongolian variation selectors
36
+
if (cp >= 0xfe00 and cp <= 0xfe0f) return 0; // variation selectors
37
+
if (cp >= 0xe0100 and cp <= 0xe01ef) return 0; // Plane-14 variation selectors
38
+
39
+
// East Asian Width: fullwidth or wide = 2
40
+
// ambiguous in East Asian context = 2, otherwise 1
41
+
// halfwidth, narrow, or neutral = 1
42
+
return switch (eaw) {
43
+
.fullwidth, .wide => 2,
44
+
else => 1,
45
+
};
46
+
}
47
+
48
/// returns the width of the provided string, as measured by the method chosen
49
+
pub fn gwidth(str: []const u8, method: Method) u16 {
50
switch (method) {
51
.unicode => {
52
+
var total: u16 = 0;
53
+
var grapheme_iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str));
54
+
55
+
var grapheme_start: usize = 0;
56
+
var prev_break: bool = true;
57
+
58
+
while (grapheme_iter.next()) |result| {
59
+
if (prev_break and !result.is_break) {
60
+
// Start of a new grapheme
61
+
const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1;
62
+
grapheme_start = grapheme_iter.i - cp_len;
63
+
}
64
+
65
+
if (result.is_break) {
66
+
// End of a grapheme - calculate its width
67
+
const grapheme_end = grapheme_iter.i;
68
+
const grapheme_bytes = str[grapheme_start..grapheme_end];
69
+
70
+
// Calculate grapheme width
71
+
var g_iter = uucode.utf8.Iterator.init(grapheme_bytes);
72
+
var width: i16 = 0;
73
+
var has_emoji_vs: bool = false;
74
+
var has_text_vs: bool = false;
75
+
var has_emoji_presentation: bool = false;
76
+
var ri_count: u8 = 0;
77
+
78
+
while (g_iter.next()) |cp| {
79
+
// Check for emoji variation selector (U+FE0F)
80
+
if (cp == 0xfe0f) {
81
+
has_emoji_vs = true;
82
+
continue;
83
+
}
84
+
85
+
// Check for text variation selector (U+FE0E)
86
+
if (cp == 0xfe0e) {
87
+
has_text_vs = true;
88
+
continue;
89
+
}
90
+
91
+
// Check if this codepoint has emoji presentation
92
+
if (uucode.get(.is_emoji_presentation, cp)) {
93
+
has_emoji_presentation = true;
94
+
}
95
+
96
+
// Count regional indicators (for flag emojis)
97
+
if (cp >= 0x1F1E6 and cp <= 0x1F1FF) {
98
+
ri_count += 1;
99
+
}
100
+
101
+
const eaw = uucode.get(.east_asian_width, cp);
102
+
const w = eawToWidth(cp, eaw);
103
+
// Take max of non-zero widths
104
+
if (w > 0 and w > width) width = w;
105
+
}
106
+
107
+
// Handle variation selectors and emoji presentation
108
+
if (has_text_vs) {
109
+
// Text presentation explicit - keep width as-is (usually 1)
110
+
width = @max(1, width);
111
+
} else if (has_emoji_vs or has_emoji_presentation or ri_count == 2) {
112
+
// Emoji presentation or flag pair - force width 2
113
+
width = @max(2, width);
114
+
}
115
+
116
+
total += @max(0, width);
117
+
118
+
grapheme_start = grapheme_end;
119
+
}
120
+
prev_break = result.is_break;
121
+
}
122
+
123
+
return total;
124
},
125
.wcwidth => {
126
+
var total: u16 = 0;
127
+
var iter = uucode.utf8.Iterator.init(str);
128
while (iter.next()) |cp| {
129
+
const w: i16 = switch (cp) {
130
// undo an override in zg for emoji skintone selectors
131
+
0x1f3fb...0x1f3ff => 2,
132
+
else => blk: {
133
+
const eaw = uucode.get(.east_asian_width, cp);
134
+
break :blk eawToWidth(cp, eaw);
135
+
},
136
};
137
+
total += @intCast(@max(0, w));
138
}
139
return total;
140
},
141
.no_zwj => {
142
+
var iter = std.mem.splitSequence(u8, str, "\u{200D}");
143
+
var result: u16 = 0;
144
+
while (iter.next()) |s| {
145
+
result += gwidth(s, .unicode);
146
+
}
147
+
return result;
148
},
149
}
150
}
151
152
test "gwidth: a" {
153
+
try testing.expectEqual(1, gwidth("a", .unicode));
154
+
try testing.expectEqual(1, gwidth("a", .wcwidth));
155
+
try testing.expectEqual(1, gwidth("a", .no_zwj));
156
}
157
158
test "gwidth: emoji with ZWJ" {
159
+
try testing.expectEqual(2, gwidth("๐ฉโ๐", .unicode));
160
+
try testing.expectEqual(4, gwidth("๐ฉโ๐", .wcwidth));
161
+
try testing.expectEqual(4, gwidth("๐ฉโ๐", .no_zwj));
162
}
163
164
test "gwidth: emoji with VS16 selector" {
165
+
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .unicode));
166
+
try testing.expectEqual(1, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .wcwidth));
167
+
try testing.expectEqual(2, gwidth("\xE2\x9D\xA4\xEF\xB8\x8F", .no_zwj));
168
}
169
170
test "gwidth: emoji with skin tone selector" {
171
+
try testing.expectEqual(2, gwidth("๐๐ฟ", .unicode));
172
+
try testing.expectEqual(4, gwidth("๐๐ฟ", .wcwidth));
173
+
try testing.expectEqual(2, gwidth("๐๐ฟ", .no_zwj));
174
+
}
175
+
176
+
test "gwidth: zero-width space" {
177
+
try testing.expectEqual(0, gwidth("\u{200B}", .unicode));
178
+
try testing.expectEqual(0, gwidth("\u{200B}", .wcwidth));
179
+
}
180
+
181
+
test "gwidth: zero-width non-joiner" {
182
+
try testing.expectEqual(0, gwidth("\u{200C}", .unicode));
183
+
try testing.expectEqual(0, gwidth("\u{200C}", .wcwidth));
184
+
}
185
+
186
+
test "gwidth: combining marks" {
187
+
// Hebrew combining mark
188
+
try testing.expectEqual(0, gwidth("\u{05B0}", .unicode));
189
+
// Devanagari combining mark
190
+
try testing.expectEqual(0, gwidth("\u{093C}", .unicode));
191
+
}
192
+
193
+
test "gwidth: flag emoji (regional indicators)" {
194
+
// US flag ๐บ๐ธ
195
+
try testing.expectEqual(2, gwidth("๐บ๐ธ", .unicode));
196
+
// UK flag ๐ฌ๐ง
197
+
try testing.expectEqual(2, gwidth("๐ฌ๐ง", .unicode));
198
+
}
199
+
200
+
test "gwidth: text variation selector" {
201
+
// U+2764 (heavy black heart) + U+FE0E (text variation selector)
202
+
// Should be width 1 with text presentation
203
+
try testing.expectEqual(1, gwidth("โค๏ธ", .unicode));
204
+
}
205
+
206
+
test "gwidth: keycap sequence" {
207
+
// Digit 1 + U+FE0F + U+20E3 (combining enclosing keycap)
208
+
// Should be width 2
209
+
try testing.expectEqual(2, gwidth("1๏ธโฃ", .unicode));
210
+
}
211
+
212
+
test "gwidth: base letter with combining mark" {
213
+
// 'a' + combining acute accent (NFD form)
214
+
// Should be width 1 (combining mark is zero-width)
215
+
try testing.expectEqual(1, gwidth("รก", .unicode));
216
}
+34
-32
src/main.zig
+34
-32
src/main.zig
···
1
const std = @import("std");
2
const builtin = @import("builtin");
3
-
const build_options = @import("build_options");
4
5
pub const Vaxis = @import("Vaxis.zig");
6
7
-
pub const Loop = @import("Loop.zig").Loop;
8
-
pub const xev = @import("xev.zig");
9
-
pub const aio = @import("aio.zig");
10
11
pub const zigimg = @import("zigimg");
12
···
27
pub const gwidth = @import("gwidth.zig");
28
pub const ctlseqs = @import("ctlseqs.zig");
29
pub const GraphemeCache = @import("GraphemeCache.zig");
30
-
pub const grapheme = @import("grapheme");
31
pub const Event = @import("event.zig").Event;
32
-
pub const Unicode = @import("Unicode.zig");
33
34
-
/// The target TTY implementation
35
-
pub const Tty = switch (builtin.os.tag) {
36
-
.windows => @import("windows/Tty.zig"),
37
-
else => @import("posix/Tty.zig"),
38
-
};
39
40
/// The size of the terminal screen
41
pub const Winsize = struct {
42
-
rows: usize,
43
-
cols: usize,
44
-
x_pixel: usize,
45
-
y_pixel: usize,
46
};
47
48
/// Initialize a Vaxis application.
···
50
return Vaxis.init(alloc, opts);
51
}
52
53
/// Resets terminal state on a panic, then calls the default zig panic handler
54
-
pub fn panic_handler(msg: []const u8, error_return_trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
55
-
if (Tty.global_tty) |gty| {
56
const reset: []const u8 = ctlseqs.csi_u_pop ++
57
ctlseqs.mouse_reset ++
58
ctlseqs.bp_reset ++
59
ctlseqs.rmcup;
60
61
-
gty.anyWriter().writeAll(reset) catch {};
62
-
63
gty.deinit();
64
}
65
-
66
-
std.builtin.default_panic(msg, error_return_trace, ret_addr);
67
}
68
69
pub const log_scopes = enum {
···
78
\\ โโโ โ โ โ โ โโโ โโโโโ
79
;
80
81
-
test {
82
-
_ = @import("gwidth.zig");
83
-
_ = @import("Cell.zig");
84
-
_ = @import("Key.zig");
85
-
_ = @import("Parser.zig");
86
-
_ = @import("Window.zig");
87
-
88
-
_ = @import("gwidth.zig");
89
-
_ = @import("queue.zig");
90
-
if (build_options.text_input)
91
-
_ = @import("widgets/TextInput.zig");
92
}
···
1
const std = @import("std");
2
const builtin = @import("builtin");
3
+
4
+
pub const tty = @import("tty.zig");
5
6
pub const Vaxis = @import("Vaxis.zig");
7
8
+
pub const loop = @import("Loop.zig");
9
+
pub const Loop = loop.Loop;
10
11
pub const zigimg = @import("zigimg");
12
···
27
pub const gwidth = @import("gwidth.zig");
28
pub const ctlseqs = @import("ctlseqs.zig");
29
pub const GraphemeCache = @import("GraphemeCache.zig");
30
pub const Event = @import("event.zig").Event;
31
+
pub const unicode = @import("unicode.zig");
32
+
33
+
pub const vxfw = @import("vxfw/vxfw.zig");
34
35
+
pub const Tty = tty.Tty;
36
37
/// The size of the terminal screen
38
pub const Winsize = struct {
39
+
rows: u16,
40
+
cols: u16,
41
+
x_pixel: u16,
42
+
y_pixel: u16,
43
};
44
45
/// Initialize a Vaxis application.
···
47
return Vaxis.init(alloc, opts);
48
}
49
50
+
pub const Panic = struct {
51
+
pub const call = panic_handler;
52
+
pub const sentinelMismatch = std.debug.FormattedPanic.sentinelMismatch;
53
+
pub const unwrapError = std.debug.FormattedPanic.unwrapError;
54
+
pub const outOfBounds = std.debug.FormattedPanic.outOfBounds;
55
+
pub const startGreaterThanEnd = std.debug.FormattedPanic.startGreaterThanEnd;
56
+
pub const inactiveUnionField = std.debug.FormattedPanic.inactiveUnionField;
57
+
pub const messages = std.debug.FormattedPanic.messages;
58
+
};
59
+
60
/// Resets terminal state on a panic, then calls the default zig panic handler
61
+
pub fn panic_handler(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
62
+
recover();
63
+
std.debug.defaultPanic(msg, ret_addr);
64
+
}
65
+
66
+
/// Resets the terminal state using the global tty instance. Use this only to recover during a panic
67
+
pub fn recover() void {
68
+
if (tty.global_tty) |*gty| {
69
const reset: []const u8 = ctlseqs.csi_u_pop ++
70
ctlseqs.mouse_reset ++
71
ctlseqs.bp_reset ++
72
ctlseqs.rmcup;
73
74
+
gty.writer().writeAll(reset) catch {};
75
+
gty.writer().flush() catch {};
76
gty.deinit();
77
}
78
}
79
80
pub const log_scopes = enum {
···
89
\\ โโโ โ โ โ โ โโโ โโโโโ
90
;
91
92
+
test "refAllDecls" {
93
+
std.testing.refAllDecls(@This());
94
}
-178
src/posix/Tty.zig
-178
src/posix/Tty.zig
···
1
-
//! TTY implementation conforming to posix standards
2
-
const Posix = @This();
3
-
4
-
const std = @import("std");
5
-
const builtin = @import("builtin");
6
-
7
-
const posix = std.posix;
8
-
const Winsize = @import("../main.zig").Winsize;
9
-
10
-
/// the original state of the terminal, prior to calling makeRaw
11
-
termios: posix.termios,
12
-
13
-
/// The file descriptor of the tty
14
-
fd: posix.fd_t,
15
-
16
-
pub const SignalHandler = struct {
17
-
context: *anyopaque,
18
-
callback: *const fn (context: *anyopaque) void,
19
-
};
20
-
21
-
/// global signal handlers
22
-
var handlers: [8]SignalHandler = undefined;
23
-
var handler_mutex: std.Thread.Mutex = .{};
24
-
var handler_idx: usize = 0;
25
-
26
-
/// global tty instance, used in case of a panic. Not guaranteed to work if
27
-
/// for some reason there are multiple TTYs open under a single vaxis
28
-
/// compilation unit - but this is better than nothing
29
-
pub var global_tty: ?Posix = null;
30
-
31
-
/// initializes a Tty instance by opening /dev/tty and "making it raw". A
32
-
/// signal handler is installed for SIGWINCH. No callbacks are installed, be
33
-
/// sure to register a callback when initializing the event loop
34
-
pub fn init() !Posix {
35
-
// Open our tty
36
-
const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0);
37
-
38
-
// Set the termios of the tty
39
-
const termios = try makeRaw(fd);
40
-
41
-
var act = posix.Sigaction{
42
-
.handler = .{ .handler = Posix.handleWinch },
43
-
.mask = switch (builtin.os.tag) {
44
-
.macos => 0,
45
-
.linux => posix.empty_sigset,
46
-
.freebsd => posix.empty_sigset,
47
-
else => @compileError("os not supported"),
48
-
},
49
-
.flags = 0,
50
-
};
51
-
try posix.sigaction(posix.SIG.WINCH, &act, null);
52
-
53
-
const self: Posix = .{
54
-
.fd = fd,
55
-
.termios = termios,
56
-
};
57
-
58
-
global_tty = self;
59
-
60
-
return self;
61
-
}
62
-
63
-
/// release resources associated with the Tty return it to its original state
64
-
pub fn deinit(self: Posix) void {
65
-
posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| {
66
-
std.log.err("couldn't restore terminal: {}", .{err});
67
-
};
68
-
if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos
69
-
posix.close(self.fd);
70
-
}
71
-
72
-
/// Write bytes to the tty
73
-
pub fn write(self: *const Posix, bytes: []const u8) !usize {
74
-
return posix.write(self.fd, bytes);
75
-
}
76
-
77
-
pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
78
-
const self: *const Posix = @ptrCast(@alignCast(ptr));
79
-
return posix.write(self.fd, bytes);
80
-
}
81
-
82
-
pub fn anyWriter(self: *const Posix) std.io.AnyWriter {
83
-
return .{
84
-
.context = self,
85
-
.writeFn = Posix.opaqueWrite,
86
-
};
87
-
}
88
-
89
-
pub fn read(self: *const Posix, buf: []u8) !usize {
90
-
return posix.read(self.fd, buf);
91
-
}
92
-
93
-
pub fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
94
-
const self: *const Posix = @ptrCast(@alignCast(ptr));
95
-
return posix.read(self.fd, buf);
96
-
}
97
-
98
-
pub fn anyReader(self: *const Posix) std.io.AnyReader {
99
-
return .{
100
-
.context = self,
101
-
.readFn = Posix.opaqueRead,
102
-
};
103
-
}
104
-
105
-
/// Install a signal handler for winsize. A maximum of 8 handlers may be
106
-
/// installed
107
-
pub fn notifyWinsize(handler: SignalHandler) !void {
108
-
handler_mutex.lock();
109
-
defer handler_mutex.unlock();
110
-
if (handler_idx == handlers.len) return error.OutOfMemory;
111
-
handlers[handler_idx] = handler;
112
-
handler_idx += 1;
113
-
}
114
-
115
-
fn handleWinch(_: c_int) callconv(.C) void {
116
-
handler_mutex.lock();
117
-
defer handler_mutex.unlock();
118
-
var i: usize = 0;
119
-
while (i < handler_idx) : (i += 1) {
120
-
const handler = handlers[i];
121
-
handler.callback(handler.context);
122
-
}
123
-
}
124
-
125
-
/// makeRaw enters the raw state for the terminal.
126
-
pub fn makeRaw(fd: posix.fd_t) !posix.termios {
127
-
const state = try posix.tcgetattr(fd);
128
-
var raw = state;
129
-
// see termios(3)
130
-
raw.iflag.IGNBRK = false;
131
-
raw.iflag.BRKINT = false;
132
-
raw.iflag.PARMRK = false;
133
-
raw.iflag.ISTRIP = false;
134
-
raw.iflag.INLCR = false;
135
-
raw.iflag.IGNCR = false;
136
-
raw.iflag.ICRNL = false;
137
-
raw.iflag.IXON = false;
138
-
139
-
raw.oflag.OPOST = false;
140
-
141
-
raw.lflag.ECHO = false;
142
-
raw.lflag.ECHONL = false;
143
-
raw.lflag.ICANON = false;
144
-
raw.lflag.ISIG = false;
145
-
raw.lflag.IEXTEN = false;
146
-
147
-
raw.cflag.CSIZE = .CS8;
148
-
raw.cflag.PARENB = false;
149
-
150
-
raw.cc[@intFromEnum(posix.V.MIN)] = 1;
151
-
raw.cc[@intFromEnum(posix.V.TIME)] = 0;
152
-
try posix.tcsetattr(fd, .FLUSH, raw);
153
-
return state;
154
-
}
155
-
156
-
/// Get the window size from the kernel
157
-
pub fn getWinsize(fd: posix.fd_t) !Winsize {
158
-
var winsize = posix.winsize{
159
-
.ws_row = 0,
160
-
.ws_col = 0,
161
-
.ws_xpixel = 0,
162
-
.ws_ypixel = 0,
163
-
};
164
-
165
-
const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
166
-
if (posix.errno(err) == .SUCCESS)
167
-
return Winsize{
168
-
.rows = winsize.ws_row,
169
-
.cols = winsize.ws_col,
170
-
.x_pixel = winsize.ws_xpixel,
171
-
.y_pixel = winsize.ws_ypixel,
172
-
};
173
-
return error.IoctlError;
174
-
}
175
-
176
-
pub fn bufferedWriter(self: *const Posix) std.io.BufferedWriter(4096, std.io.AnyWriter) {
177
-
return std.io.bufferedWriter(self.anyWriter());
178
-
}
···
+68
-46
src/queue.zig
+68
-46
src/queue.zig
···
30
self.not_empty.wait(&self.mutex);
31
}
32
std.debug.assert(!self.isEmptyLH());
33
-
if (self.isFullLH()) {
34
-
// If we are full, wake up a push that might be
35
-
// waiting here.
36
-
self.not_full.signal();
37
-
}
38
-
39
-
const result = self.buf[self.mask(self.read_index)];
40
-
self.read_index = self.mask2(self.read_index + 1);
41
-
return result;
42
}
43
44
/// Push an item into the queue. Blocks until an item has been
···
49
while (self.isFullLH()) {
50
self.not_full.wait(&self.mutex);
51
}
52
-
if (self.isEmptyLH()) {
53
-
// If we were empty, wake up a pop if it was waiting.
54
-
self.not_empty.signal();
55
-
}
56
std.debug.assert(!self.isFullLH());
57
-
58
-
self.buf[self.mask(self.write_index)] = item;
59
-
self.write_index = self.mask2(self.write_index + 1);
60
}
61
62
/// Push an item into the queue. Returns true when the item
···
64
/// was full.
65
pub fn tryPush(self: *Self, item: T) bool {
66
self.mutex.lock();
67
-
if (self.isFullLH()) {
68
-
self.mutex.unlock();
69
-
return false;
70
-
}
71
-
self.mutex.unlock();
72
-
self.push(item);
73
return true;
74
}
75
···
77
/// available.
78
pub fn tryPop(self: *Self) ?T {
79
self.mutex.lock();
80
-
if (self.isEmptyLH()) {
81
-
self.mutex.unlock();
82
-
return null;
83
-
}
84
-
self.mutex.unlock();
85
-
return self.pop();
86
}
87
88
/// Poll the queue. This call blocks until events are in the queue
···
93
self.not_empty.wait(&self.mutex);
94
}
95
std.debug.assert(!self.isEmptyLH());
96
}
97
98
fn isEmptyLH(self: Self) bool {
···
135
fn mask2(self: Self, index: usize) usize {
136
return index % (2 * self.buf.len);
137
}
138
};
139
}
140
···
184
thread.join();
185
}
186
187
-
fn sleepyPop(q: *Queue(u8, 2)) !void {
188
// First we wait for the queue to be full.
189
-
while (!q.isFull())
190
try Thread.yield();
191
192
// Then we spuriously wake it up, because that's a thing that can
···
200
// still full and the push in the other thread is still blocked
201
// waiting for space.
202
try Thread.yield();
203
-
std.time.sleep(std.time.ns_per_s);
204
// Finally, let that other thread go.
205
try std.testing.expectEqual(1, q.pop());
206
207
-
// This won't continue until the other thread has had a chance to
208
-
// put at least one item in the queue.
209
-
while (!q.isFull())
210
try Thread.yield();
211
// But we want to ensure that there's a second push waiting, so
212
// here's another sleep.
213
-
std.time.sleep(std.time.ns_per_s / 2);
214
215
// Another spurious wake...
216
q.not_full.signal();
···
218
// And another chance for the other thread to see that it's
219
// spurious and go back to sleep.
220
try Thread.yield();
221
-
std.time.sleep(std.time.ns_per_s / 2);
222
223
// Pop that thing and we're done.
224
try std.testing.expectEqual(2, q.pop());
···
232
// fails if the while loop in `push` is turned into an `if`.
233
234
var queue: Queue(u8, 2) = .{};
235
-
const thread = try Thread.spawn(cfg, sleepyPop, .{&queue});
236
queue.push(1);
237
queue.push(2);
238
const now = std.time.milliTimestamp();
239
queue.push(3); // This one should block.
240
const then = std.time.milliTimestamp();
241
242
// Just to make sure the sleeps are yielding to this thread, make
243
-
// sure it took at least 900ms to do the push.
244
-
try std.testing.expect(then - now > 900);
245
246
// This should block again, waiting for the other thread.
247
queue.push(4);
248
···
252
try std.testing.expectEqual(4, queue.pop());
253
}
254
255
-
fn sleepyPush(q: *Queue(u8, 1)) !void {
256
// Try to ensure the other thread has already started trying to pop.
257
try Thread.yield();
258
-
std.time.sleep(std.time.ns_per_s / 2);
259
260
// Spurious wake
261
q.not_full.signal();
262
q.not_empty.signal();
263
264
try Thread.yield();
265
-
std.time.sleep(std.time.ns_per_s / 2);
266
267
// Stick something in the queue so it can be popped.
268
q.push(1);
269
// Ensure it's been popped.
270
-
while (!q.isEmpty())
271
try Thread.yield();
272
// Give the other thread time to block again.
273
try Thread.yield();
274
-
std.time.sleep(std.time.ns_per_s / 2);
275
276
// Spurious wake
277
q.not_full.signal();
···
286
// `if`.
287
288
var queue: Queue(u8, 1) = .{};
289
-
const thread = try Thread.spawn(cfg, sleepyPush, .{&queue});
290
try std.testing.expectEqual(1, queue.pop());
291
try std.testing.expectEqual(2, queue.pop());
292
thread.join();
293
}
···
302
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
303
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
304
try Thread.yield();
305
-
std.time.sleep(std.time.ns_per_s / 2);
306
queue.push(1);
307
queue.push(1);
308
t1.join();
···
30
self.not_empty.wait(&self.mutex);
31
}
32
std.debug.assert(!self.isEmptyLH());
33
+
return self.popAndSignalLH();
34
}
35
36
/// Push an item into the queue. Blocks until an item has been
···
41
while (self.isFullLH()) {
42
self.not_full.wait(&self.mutex);
43
}
44
std.debug.assert(!self.isFullLH());
45
+
self.pushAndSignalLH(item);
46
}
47
48
/// Push an item into the queue. Returns true when the item
···
50
/// was full.
51
pub fn tryPush(self: *Self, item: T) bool {
52
self.mutex.lock();
53
+
defer self.mutex.unlock();
54
+
if (self.isFullLH()) return false;
55
+
self.pushAndSignalLH(item);
56
return true;
57
}
58
···
60
/// available.
61
pub fn tryPop(self: *Self) ?T {
62
self.mutex.lock();
63
+
defer self.mutex.unlock();
64
+
if (self.isEmptyLH()) return null;
65
+
return self.popAndSignalLH();
66
}
67
68
/// Poll the queue. This call blocks until events are in the queue
···
73
self.not_empty.wait(&self.mutex);
74
}
75
std.debug.assert(!self.isEmptyLH());
76
+
}
77
+
78
+
pub fn lock(self: *Self) void {
79
+
self.mutex.lock();
80
+
}
81
+
82
+
pub fn unlock(self: *Self) void {
83
+
self.mutex.unlock();
84
+
}
85
+
86
+
/// Used to efficiently drain the queue while the lock is externally held
87
+
pub fn drain(self: *Self) ?T {
88
+
if (self.isEmptyLH()) return null;
89
+
return self.popLH();
90
}
91
92
fn isEmptyLH(self: Self) bool {
···
129
fn mask2(self: Self, index: usize) usize {
130
return index % (2 * self.buf.len);
131
}
132
+
133
+
fn pushAndSignalLH(self: *Self, item: T) void {
134
+
const was_empty = self.isEmptyLH();
135
+
self.buf[self.mask(self.write_index)] = item;
136
+
self.write_index = self.mask2(self.write_index + 1);
137
+
if (was_empty) {
138
+
self.not_empty.signal();
139
+
}
140
+
}
141
+
142
+
fn popAndSignalLH(self: *Self) T {
143
+
const was_full = self.isFullLH();
144
+
const result = self.popLH();
145
+
if (was_full) {
146
+
self.not_full.signal();
147
+
}
148
+
return result;
149
+
}
150
+
151
+
fn popLH(self: *Self) T {
152
+
const result = self.buf[self.mask(self.read_index)];
153
+
self.read_index = self.mask2(self.read_index + 1);
154
+
return result;
155
+
}
156
};
157
}
158
···
202
thread.join();
203
}
204
205
+
fn sleepyPop(q: *Queue(u8, 2), state: *atomic.Value(u8)) !void {
206
// First we wait for the queue to be full.
207
+
while (state.load(.acquire) < 1)
208
try Thread.yield();
209
210
// Then we spuriously wake it up, because that's a thing that can
···
218
// still full and the push in the other thread is still blocked
219
// waiting for space.
220
try Thread.yield();
221
+
std.Thread.sleep(10 * std.time.ns_per_ms);
222
// Finally, let that other thread go.
223
try std.testing.expectEqual(1, q.pop());
224
225
+
// Wait for the other thread to signal it's ready for second push
226
+
while (state.load(.acquire) < 2)
227
try Thread.yield();
228
// But we want to ensure that there's a second push waiting, so
229
// here's another sleep.
230
+
std.Thread.sleep(10 * std.time.ns_per_ms);
231
232
// Another spurious wake...
233
q.not_full.signal();
···
235
// And another chance for the other thread to see that it's
236
// spurious and go back to sleep.
237
try Thread.yield();
238
+
std.Thread.sleep(10 * std.time.ns_per_ms);
239
240
// Pop that thing and we're done.
241
try std.testing.expectEqual(2, q.pop());
···
249
// fails if the while loop in `push` is turned into an `if`.
250
251
var queue: Queue(u8, 2) = .{};
252
+
var state = atomic.Value(u8).init(0);
253
+
const thread = try Thread.spawn(cfg, sleepyPop, .{ &queue, &state });
254
queue.push(1);
255
queue.push(2);
256
+
state.store(1, .release);
257
const now = std.time.milliTimestamp();
258
queue.push(3); // This one should block.
259
const then = std.time.milliTimestamp();
260
261
// Just to make sure the sleeps are yielding to this thread, make
262
+
// sure it took at least 5ms to do the push.
263
+
try std.testing.expect(then - now > 5);
264
265
+
state.store(2, .release);
266
// This should block again, waiting for the other thread.
267
queue.push(4);
268
···
272
try std.testing.expectEqual(4, queue.pop());
273
}
274
275
+
fn sleepyPush(q: *Queue(u8, 1), state: *atomic.Value(u8)) !void {
276
// Try to ensure the other thread has already started trying to pop.
277
try Thread.yield();
278
+
std.Thread.sleep(10 * std.time.ns_per_ms);
279
280
// Spurious wake
281
q.not_full.signal();
282
q.not_empty.signal();
283
284
try Thread.yield();
285
+
std.Thread.sleep(10 * std.time.ns_per_ms);
286
287
// Stick something in the queue so it can be popped.
288
q.push(1);
289
// Ensure it's been popped.
290
+
while (state.load(.acquire) < 1)
291
try Thread.yield();
292
// Give the other thread time to block again.
293
try Thread.yield();
294
+
std.Thread.sleep(10 * std.time.ns_per_ms);
295
296
// Spurious wake
297
q.not_full.signal();
···
306
// `if`.
307
308
var queue: Queue(u8, 1) = .{};
309
+
var state = atomic.Value(u8).init(0);
310
+
const thread = try Thread.spawn(cfg, sleepyPush, .{ &queue, &state });
311
try std.testing.expectEqual(1, queue.pop());
312
+
state.store(1, .release);
313
try std.testing.expectEqual(2, queue.pop());
314
thread.join();
315
}
···
324
const t1 = try Thread.spawn(cfg, readerThread, .{&queue});
325
const t2 = try Thread.spawn(cfg, readerThread, .{&queue});
326
try Thread.yield();
327
+
std.Thread.sleep(10 * std.time.ns_per_ms);
328
queue.push(1);
329
queue.push(1);
330
t1.join();
+750
src/tty.zig
+750
src/tty.zig
···
···
1
+
const std = @import("std");
2
+
const builtin = @import("builtin");
3
+
4
+
const vaxis = @import("main.zig");
5
+
6
+
const ctlseqs = vaxis.ctlseqs;
7
+
const posix = std.posix;
8
+
const windows = std.os.windows;
9
+
10
+
const Event = vaxis.Event;
11
+
const Key = vaxis.Key;
12
+
const Mouse = vaxis.Mouse;
13
+
const Parser = vaxis.Parser;
14
+
const Winsize = vaxis.Winsize;
15
+
16
+
/// The target TTY implementation
17
+
pub const Tty = if (builtin.is_test)
18
+
TestTty
19
+
else switch (builtin.os.tag) {
20
+
.windows => WindowsTty,
21
+
else => PosixTty,
22
+
};
23
+
24
+
/// global tty instance, used in case of a panic. Not guaranteed to work if
25
+
/// for some reason there are multiple TTYs open under a single vaxis
26
+
/// compilation unit - but this is better than nothing
27
+
pub var global_tty: ?Tty = null;
28
+
29
+
pub const PosixTty = struct {
30
+
/// the original state of the terminal, prior to calling makeRaw
31
+
termios: posix.termios,
32
+
33
+
/// The file descriptor of the tty
34
+
fd: posix.fd_t,
35
+
36
+
/// File.Writer for efficient buffered writing
37
+
tty_writer: std.fs.File.Writer,
38
+
39
+
pub const SignalHandler = struct {
40
+
context: *anyopaque,
41
+
callback: *const fn (context: *anyopaque) void,
42
+
};
43
+
44
+
/// global signal handlers
45
+
var handlers: [8]SignalHandler = undefined;
46
+
var handler_mutex: std.Thread.Mutex = .{};
47
+
var handler_idx: usize = 0;
48
+
49
+
var handler_installed: bool = false;
50
+
51
+
/// initializes a Tty instance by opening /dev/tty and "making it raw". A
52
+
/// signal handler is installed for SIGWINCH. No callbacks are installed, be
53
+
/// sure to register a callback when initializing the event loop
54
+
pub fn init(buffer: []u8) !PosixTty {
55
+
// Open our tty
56
+
const fd = try posix.open("/dev/tty", .{ .ACCMODE = .RDWR }, 0);
57
+
58
+
// Set the termios of the tty
59
+
const termios = try makeRaw(fd);
60
+
61
+
var act = posix.Sigaction{
62
+
.handler = .{ .handler = PosixTty.handleWinch },
63
+
.mask = switch (builtin.os.tag) {
64
+
.macos => 0,
65
+
else => posix.sigemptyset(),
66
+
},
67
+
.flags = 0,
68
+
};
69
+
posix.sigaction(posix.SIG.WINCH, &act, null);
70
+
handler_installed = true;
71
+
72
+
const file = std.fs.File{ .handle = fd };
73
+
74
+
const self: PosixTty = .{
75
+
.fd = fd,
76
+
.termios = termios,
77
+
.tty_writer = .initStreaming(file, buffer),
78
+
};
79
+
80
+
global_tty = self;
81
+
82
+
return self;
83
+
}
84
+
85
+
/// release resources associated with the Tty return it to its original state
86
+
pub fn deinit(self: PosixTty) void {
87
+
posix.tcsetattr(self.fd, .FLUSH, self.termios) catch |err| {
88
+
std.log.err("couldn't restore terminal: {}", .{err});
89
+
};
90
+
if (builtin.os.tag != .macos) // closing /dev/tty may block indefinitely on macos
91
+
posix.close(self.fd);
92
+
}
93
+
94
+
/// Resets the signal handler to it's default
95
+
pub fn resetSignalHandler() void {
96
+
if (!handler_installed) return;
97
+
handler_installed = false;
98
+
var act = posix.Sigaction{
99
+
.handler = .{ .handler = posix.SIG.DFL },
100
+
.mask = switch (builtin.os.tag) {
101
+
.macos => 0,
102
+
else => posix.sigemptyset(),
103
+
},
104
+
.flags = 0,
105
+
};
106
+
posix.sigaction(posix.SIG.WINCH, &act, null);
107
+
}
108
+
109
+
pub fn writer(self: *PosixTty) *std.Io.Writer {
110
+
return &self.tty_writer.interface;
111
+
}
112
+
113
+
pub fn read(self: *const PosixTty, buf: []u8) !usize {
114
+
return posix.read(self.fd, buf);
115
+
}
116
+
117
+
/// Install a signal handler for winsize. A maximum of 8 handlers may be
118
+
/// installed
119
+
pub fn notifyWinsize(handler: SignalHandler) !void {
120
+
handler_mutex.lock();
121
+
defer handler_mutex.unlock();
122
+
if (handler_idx == handlers.len) return error.OutOfMemory;
123
+
handlers[handler_idx] = handler;
124
+
handler_idx += 1;
125
+
}
126
+
127
+
fn handleWinch(_: c_int) callconv(.c) void {
128
+
handler_mutex.lock();
129
+
defer handler_mutex.unlock();
130
+
var i: usize = 0;
131
+
while (i < handler_idx) : (i += 1) {
132
+
const handler = handlers[i];
133
+
handler.callback(handler.context);
134
+
}
135
+
}
136
+
137
+
/// makeRaw enters the raw state for the terminal.
138
+
pub fn makeRaw(fd: posix.fd_t) !posix.termios {
139
+
const state = try posix.tcgetattr(fd);
140
+
var raw = state;
141
+
// see termios(3)
142
+
raw.iflag.IGNBRK = false;
143
+
raw.iflag.BRKINT = false;
144
+
raw.iflag.PARMRK = false;
145
+
raw.iflag.ISTRIP = false;
146
+
raw.iflag.INLCR = false;
147
+
raw.iflag.IGNCR = false;
148
+
raw.iflag.ICRNL = false;
149
+
raw.iflag.IXON = false;
150
+
151
+
raw.oflag.OPOST = false;
152
+
153
+
raw.lflag.ECHO = false;
154
+
raw.lflag.ECHONL = false;
155
+
raw.lflag.ICANON = false;
156
+
raw.lflag.ISIG = false;
157
+
raw.lflag.IEXTEN = false;
158
+
159
+
raw.cflag.CSIZE = .CS8;
160
+
raw.cflag.PARENB = false;
161
+
162
+
raw.cc[@intFromEnum(posix.V.MIN)] = 1;
163
+
raw.cc[@intFromEnum(posix.V.TIME)] = 0;
164
+
try posix.tcsetattr(fd, .FLUSH, raw);
165
+
return state;
166
+
}
167
+
168
+
/// Get the window size from the kernel
169
+
pub fn getWinsize(fd: posix.fd_t) !Winsize {
170
+
var winsize = posix.winsize{
171
+
.row = 0,
172
+
.col = 0,
173
+
.xpixel = 0,
174
+
.ypixel = 0,
175
+
};
176
+
177
+
const err = posix.system.ioctl(fd, posix.T.IOCGWINSZ, @intFromPtr(&winsize));
178
+
if (posix.errno(err) == .SUCCESS)
179
+
return Winsize{
180
+
.rows = winsize.row,
181
+
.cols = winsize.col,
182
+
.x_pixel = winsize.xpixel,
183
+
.y_pixel = winsize.ypixel,
184
+
};
185
+
return error.IoctlError;
186
+
}
187
+
};
188
+
189
+
pub const WindowsTty = struct {
190
+
stdin: windows.HANDLE,
191
+
stdout: windows.HANDLE,
192
+
193
+
initial_codepage: c_uint,
194
+
initial_input_mode: CONSOLE_MODE_INPUT,
195
+
initial_output_mode: CONSOLE_MODE_OUTPUT,
196
+
197
+
// a buffer to write key text into
198
+
buf: [4]u8 = undefined,
199
+
200
+
/// File.Writer for efficient buffered writing
201
+
tty_writer: std.fs.File.Writer,
202
+
203
+
/// The last mouse button that was pressed. We store the previous state of button presses on each
204
+
/// mouse event so we can detect which button was released
205
+
last_mouse_button_press: u16 = 0,
206
+
207
+
const utf8_codepage: c_uint = 65001;
208
+
209
+
/// The input mode set by init
210
+
pub const input_raw_mode: CONSOLE_MODE_INPUT = .{
211
+
.WINDOW_INPUT = 1, // resize events
212
+
.MOUSE_INPUT = 1,
213
+
.EXTENDED_FLAGS = 1, // allow mouse events
214
+
};
215
+
216
+
/// The output mode set by init
217
+
pub const output_raw_mode: CONSOLE_MODE_OUTPUT = .{
218
+
.PROCESSED_OUTPUT = 1, // handle control sequences
219
+
.VIRTUAL_TERMINAL_PROCESSING = 1, // handle ANSI sequences
220
+
.DISABLE_NEWLINE_AUTO_RETURN = 1, // disable inserting a new line when we write at the last column
221
+
.ENABLE_LVB_GRID_WORLDWIDE = 1, // enables reverse video and underline
222
+
};
223
+
224
+
pub fn init(buffer: []u8) !Tty {
225
+
const stdin: std.fs.File = .stdin();
226
+
const stdout: std.fs.File = .stdout();
227
+
228
+
// get initial modes
229
+
const initial_output_codepage = windows.kernel32.GetConsoleOutputCP();
230
+
const initial_input_mode = try getConsoleMode(CONSOLE_MODE_INPUT, stdin.handle);
231
+
const initial_output_mode = try getConsoleMode(CONSOLE_MODE_OUTPUT, stdout.handle);
232
+
233
+
// set new modes
234
+
try setConsoleMode(stdin.handle, input_raw_mode);
235
+
try setConsoleMode(stdout.handle, output_raw_mode);
236
+
if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
237
+
return windows.unexpectedError(windows.kernel32.GetLastError());
238
+
239
+
const self: Tty = .{
240
+
.stdin = stdin.handle,
241
+
.stdout = stdout.handle,
242
+
.initial_codepage = initial_output_codepage,
243
+
.initial_input_mode = initial_input_mode,
244
+
.initial_output_mode = initial_output_mode,
245
+
.tty_writer = .initStreaming(stdout, buffer),
246
+
};
247
+
248
+
// save a copy of this tty as the global_tty for panic handling
249
+
global_tty = self;
250
+
251
+
return self;
252
+
}
253
+
254
+
pub fn deinit(self: Tty) void {
255
+
_ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage);
256
+
setConsoleMode(self.stdin, self.initial_input_mode) catch {};
257
+
setConsoleMode(self.stdout, self.initial_output_mode) catch {};
258
+
windows.CloseHandle(self.stdin);
259
+
windows.CloseHandle(self.stdout);
260
+
}
261
+
262
+
pub const CONSOLE_MODE_INPUT = packed struct(u32) {
263
+
PROCESSED_INPUT: u1 = 0,
264
+
LINE_INPUT: u1 = 0,
265
+
ECHO_INPUT: u1 = 0,
266
+
WINDOW_INPUT: u1 = 0,
267
+
MOUSE_INPUT: u1 = 0,
268
+
INSERT_MODE: u1 = 0,
269
+
QUICK_EDIT_MODE: u1 = 0,
270
+
EXTENDED_FLAGS: u1 = 0,
271
+
AUTO_POSITION: u1 = 0,
272
+
VIRTUAL_TERMINAL_INPUT: u1 = 0,
273
+
_: u22 = 0,
274
+
};
275
+
pub const CONSOLE_MODE_OUTPUT = packed struct(u32) {
276
+
PROCESSED_OUTPUT: u1 = 0,
277
+
WRAP_AT_EOL_OUTPUT: u1 = 0,
278
+
VIRTUAL_TERMINAL_PROCESSING: u1 = 0,
279
+
DISABLE_NEWLINE_AUTO_RETURN: u1 = 0,
280
+
ENABLE_LVB_GRID_WORLDWIDE: u1 = 0,
281
+
_: u27 = 0,
282
+
};
283
+
284
+
pub fn getConsoleMode(comptime T: type, handle: windows.HANDLE) !T {
285
+
var mode: u32 = undefined;
286
+
if (windows.kernel32.GetConsoleMode(handle, &mode) == 0) return switch (windows.kernel32.GetLastError()) {
287
+
.INVALID_HANDLE => error.InvalidHandle,
288
+
else => |e| windows.unexpectedError(e),
289
+
};
290
+
return @bitCast(mode);
291
+
}
292
+
293
+
pub fn setConsoleMode(handle: windows.HANDLE, mode: anytype) !void {
294
+
if (windows.kernel32.SetConsoleMode(handle, @bitCast(mode)) == 0) return switch (windows.kernel32.GetLastError()) {
295
+
.INVALID_HANDLE => error.InvalidHandle,
296
+
else => |e| windows.unexpectedError(e),
297
+
};
298
+
}
299
+
300
+
pub fn writer(self: *Tty) *std.Io.Writer {
301
+
return &self.tty_writer.interface;
302
+
}
303
+
304
+
pub fn read(self: *const Tty, buf: []u8) !usize {
305
+
return posix.read(self.fd, buf);
306
+
}
307
+
308
+
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
309
+
// We use a loop so we can ignore certain events
310
+
var state: EventState = .{};
311
+
while (true) {
312
+
var event_count: u32 = 0;
313
+
var input_record: INPUT_RECORD = undefined;
314
+
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
315
+
return windows.unexpectedError(windows.kernel32.GetLastError());
316
+
317
+
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
318
+
return ev;
319
+
}
320
+
}
321
+
}
322
+
323
+
pub const EventState = struct {
324
+
ansi_buf: [128]u8 = undefined,
325
+
ansi_idx: usize = 0,
326
+
utf16_buf: [2]u16 = undefined,
327
+
utf16_half: bool = false,
328
+
};
329
+
330
+
pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
331
+
switch (record.EventType) {
332
+
0x0001 => { // Key event
333
+
const event = record.Event.KeyEvent;
334
+
335
+
if (state.utf16_half) half: {
336
+
state.utf16_half = false;
337
+
state.utf16_buf[1] = event.uChar.UnicodeChar;
338
+
const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
339
+
const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
340
+
341
+
const key: Key = .{
342
+
.codepoint = codepoint,
343
+
.base_layout_codepoint = codepoint,
344
+
.mods = translateMods(event.dwControlKeyState),
345
+
.text = self.buf[0..n],
346
+
};
347
+
348
+
switch (event.bKeyDown) {
349
+
0 => return .{ .key_release = key },
350
+
else => return .{ .key_press = key },
351
+
}
352
+
}
353
+
354
+
const base_layout: u16 = switch (event.wVirtualKeyCode) {
355
+
0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
356
+
if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
357
+
break :blk event.uChar.UnicodeChar;
358
+
state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
359
+
state.ansi_idx += 1;
360
+
if (state.ansi_idx <= 2) return null;
361
+
const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
362
+
return if (result.n == 0) null else evt: {
363
+
state.ansi_idx = 0;
364
+
break :evt result.event;
365
+
};
366
+
},
367
+
0x08 => Key.backspace,
368
+
0x09 => Key.tab,
369
+
0x0D => Key.enter,
370
+
0x13 => Key.pause,
371
+
0x14 => Key.caps_lock,
372
+
0x1B => Key.escape,
373
+
0x20 => Key.space,
374
+
0x21 => Key.page_up,
375
+
0x22 => Key.page_down,
376
+
0x23 => Key.end,
377
+
0x24 => Key.home,
378
+
0x25 => Key.left,
379
+
0x26 => Key.up,
380
+
0x27 => Key.right,
381
+
0x28 => Key.down,
382
+
0x2c => Key.print_screen,
383
+
0x2d => Key.insert,
384
+
0x2e => Key.delete,
385
+
0x30...0x39 => |k| k,
386
+
0x41...0x5a => |k| k + 0x20, // translate to lowercase
387
+
0x5b => Key.left_meta,
388
+
0x5c => Key.right_meta,
389
+
0x60 => Key.kp_0,
390
+
0x61 => Key.kp_1,
391
+
0x62 => Key.kp_2,
392
+
0x63 => Key.kp_3,
393
+
0x64 => Key.kp_4,
394
+
0x65 => Key.kp_5,
395
+
0x66 => Key.kp_6,
396
+
0x67 => Key.kp_7,
397
+
0x68 => Key.kp_8,
398
+
0x69 => Key.kp_9,
399
+
0x6a => Key.kp_multiply,
400
+
0x6b => Key.kp_add,
401
+
0x6c => Key.kp_separator,
402
+
0x6d => Key.kp_subtract,
403
+
0x6e => Key.kp_decimal,
404
+
0x6f => Key.kp_divide,
405
+
0x70 => Key.f1,
406
+
0x71 => Key.f2,
407
+
0x72 => Key.f3,
408
+
0x73 => Key.f4,
409
+
0x74 => Key.f5,
410
+
0x75 => Key.f6,
411
+
0x76 => Key.f8,
412
+
0x77 => Key.f8,
413
+
0x78 => Key.f9,
414
+
0x79 => Key.f10,
415
+
0x7a => Key.f11,
416
+
0x7b => Key.f12,
417
+
0x7c => Key.f13,
418
+
0x7d => Key.f14,
419
+
0x7e => Key.f15,
420
+
0x7f => Key.f16,
421
+
0x80 => Key.f17,
422
+
0x81 => Key.f18,
423
+
0x82 => Key.f19,
424
+
0x83 => Key.f20,
425
+
0x84 => Key.f21,
426
+
0x85 => Key.f22,
427
+
0x86 => Key.f23,
428
+
0x87 => Key.f24,
429
+
0x90 => Key.num_lock,
430
+
0x91 => Key.scroll_lock,
431
+
0xa0 => Key.left_shift,
432
+
0x10 => Key.left_shift,
433
+
0xa1 => Key.right_shift,
434
+
0xa2 => Key.left_control,
435
+
0x11 => Key.left_control,
436
+
0xa3 => Key.right_control,
437
+
0xa4 => Key.left_alt,
438
+
0x12 => Key.left_alt,
439
+
0xa5 => Key.right_alt,
440
+
0xad => Key.mute_volume,
441
+
0xae => Key.lower_volume,
442
+
0xaf => Key.raise_volume,
443
+
0xb0 => Key.media_track_next,
444
+
0xb1 => Key.media_track_previous,
445
+
0xb2 => Key.media_stop,
446
+
0xb3 => Key.media_play_pause,
447
+
0xba => ';',
448
+
0xbb => '+',
449
+
0xbc => ',',
450
+
0xbd => '-',
451
+
0xbe => '.',
452
+
0xbf => '/',
453
+
0xc0 => '`',
454
+
0xdb => '[',
455
+
0xdc => '\\',
456
+
0xdf => '\\',
457
+
0xe2 => '\\',
458
+
0xdd => ']',
459
+
0xde => '\'',
460
+
else => {
461
+
const log = std.log.scoped(.vaxis);
462
+
log.warn("unknown wVirtualKeyCode: 0x{x}", .{event.wVirtualKeyCode});
463
+
return null;
464
+
},
465
+
};
466
+
467
+
if (std.unicode.utf16IsHighSurrogate(base_layout)) {
468
+
state.utf16_buf[0] = base_layout;
469
+
state.utf16_half = true;
470
+
return null;
471
+
}
472
+
if (std.unicode.utf16IsLowSurrogate(base_layout)) {
473
+
return null;
474
+
}
475
+
476
+
var codepoint: u21 = base_layout;
477
+
var text: ?[]const u8 = null;
478
+
switch (event.uChar.UnicodeChar) {
479
+
0x00...0x1F => {},
480
+
else => |cp| {
481
+
codepoint = cp;
482
+
const n = try std.unicode.utf8Encode(codepoint, &self.buf);
483
+
text = self.buf[0..n];
484
+
},
485
+
}
486
+
487
+
const key: Key = .{
488
+
.codepoint = codepoint,
489
+
.base_layout_codepoint = base_layout,
490
+
.mods = translateMods(event.dwControlKeyState),
491
+
.text = text,
492
+
};
493
+
494
+
switch (event.bKeyDown) {
495
+
0 => return .{ .key_release = key },
496
+
else => return .{ .key_press = key },
497
+
}
498
+
},
499
+
0x0002 => { // Mouse event
500
+
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
501
+
502
+
const event = record.Event.MouseEvent;
503
+
504
+
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
505
+
// is wheel_down
506
+
// Low word represents button state
507
+
const mouse_wheel_direction: i16 = blk: {
508
+
const wheelu32: u32 = event.dwButtonState >> 16;
509
+
const wheelu16: u16 = @truncate(wheelu32);
510
+
break :blk @bitCast(wheelu16);
511
+
};
512
+
513
+
const buttons: u16 = @truncate(event.dwButtonState);
514
+
// save the current state when we are done
515
+
defer self.last_mouse_button_press = buttons;
516
+
const button_xor = self.last_mouse_button_press ^ buttons;
517
+
518
+
var event_type: Mouse.Type = .press;
519
+
const btn: Mouse.Button = switch (button_xor) {
520
+
0x0000 => blk: {
521
+
// Check wheel event
522
+
if (event.dwEventFlags & 0x0004 > 0) {
523
+
if (mouse_wheel_direction > 0)
524
+
break :blk .wheel_up
525
+
else
526
+
break :blk .wheel_down;
527
+
}
528
+
529
+
// If we have no change but one of the buttons is still pressed we have a
530
+
// drag event. Find out which button is held down
531
+
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
532
+
event_type = .drag;
533
+
if (buttons & 0x0001 > 0) break :blk .left;
534
+
if (buttons & 0x0002 > 0) break :blk .right;
535
+
if (buttons & 0x0004 > 0) break :blk .middle;
536
+
if (buttons & 0x0008 > 0) break :blk .button_8;
537
+
if (buttons & 0x0010 > 0) break :blk .button_9;
538
+
}
539
+
540
+
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
541
+
break :blk .none;
542
+
},
543
+
0x0001 => blk: {
544
+
if (buttons & 0x0001 == 0) event_type = .release;
545
+
break :blk .left;
546
+
},
547
+
0x0002 => blk: {
548
+
if (buttons & 0x0002 == 0) event_type = .release;
549
+
break :blk .right;
550
+
},
551
+
0x0004 => blk: {
552
+
if (buttons & 0x0004 == 0) event_type = .release;
553
+
break :blk .middle;
554
+
},
555
+
0x0008 => blk: {
556
+
if (buttons & 0x0008 == 0) event_type = .release;
557
+
break :blk .button_8;
558
+
},
559
+
0x0010 => blk: {
560
+
if (buttons & 0x0010 == 0) event_type = .release;
561
+
break :blk .button_9;
562
+
},
563
+
else => {
564
+
std.log.warn("unknown mouse event: {}", .{event});
565
+
return null;
566
+
},
567
+
};
568
+
569
+
const shift: u32 = 0x0010;
570
+
const alt: u32 = 0x0001 | 0x0002;
571
+
const ctrl: u32 = 0x0004 | 0x0008;
572
+
const mods: Mouse.Modifiers = .{
573
+
.shift = event.dwControlKeyState & shift > 0,
574
+
.alt = event.dwControlKeyState & alt > 0,
575
+
.ctrl = event.dwControlKeyState & ctrl > 0,
576
+
};
577
+
578
+
const mouse: Mouse = .{
579
+
.col = @as(i16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
580
+
.row = @as(i16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
581
+
.mods = mods,
582
+
.type = event_type,
583
+
.button = btn,
584
+
};
585
+
return .{ .mouse = mouse };
586
+
},
587
+
0x0004 => { // Screen resize events
588
+
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for
589
+
// the size directly when we get this event
590
+
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
591
+
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
592
+
return windows.unexpectedError(windows.kernel32.GetLastError());
593
+
}
594
+
const window_rect = console_info.srWindow;
595
+
const width = window_rect.Right - window_rect.Left + 1;
596
+
const height = window_rect.Bottom - window_rect.Top + 1;
597
+
return .{
598
+
.winsize = .{
599
+
.cols = @intCast(width),
600
+
.rows = @intCast(height),
601
+
.x_pixel = 0,
602
+
.y_pixel = 0,
603
+
},
604
+
};
605
+
},
606
+
0x0010 => { // Focus events
607
+
switch (record.Event.FocusEvent.bSetFocus) {
608
+
0 => return .focus_out,
609
+
else => return .focus_in,
610
+
}
611
+
},
612
+
else => {},
613
+
}
614
+
return null;
615
+
}
616
+
617
+
fn translateMods(mods: u32) Key.Modifiers {
618
+
const left_alt: u32 = 0x0002;
619
+
const right_alt: u32 = 0x0001;
620
+
const left_ctrl: u32 = 0x0008;
621
+
const right_ctrl: u32 = 0x0004;
622
+
623
+
const caps: u32 = 0x0080;
624
+
const num_lock: u32 = 0x0020;
625
+
const shift: u32 = 0x0010;
626
+
const alt: u32 = left_alt | right_alt;
627
+
const ctrl: u32 = left_ctrl | right_ctrl;
628
+
629
+
return .{
630
+
.shift = mods & shift > 0,
631
+
.alt = mods & alt > 0,
632
+
.ctrl = mods & ctrl > 0,
633
+
.caps_lock = mods & caps > 0,
634
+
.num_lock = mods & num_lock > 0,
635
+
};
636
+
}
637
+
638
+
// From gitub.com/ziglibs/zig-windows-console. Thanks :)
639
+
//
640
+
// Events
641
+
const union_unnamed_248 = extern union {
642
+
UnicodeChar: windows.WCHAR,
643
+
AsciiChar: windows.CHAR,
644
+
};
645
+
pub const KEY_EVENT_RECORD = extern struct {
646
+
bKeyDown: windows.BOOL,
647
+
wRepeatCount: windows.WORD,
648
+
wVirtualKeyCode: windows.WORD,
649
+
wVirtualScanCode: windows.WORD,
650
+
uChar: union_unnamed_248,
651
+
dwControlKeyState: windows.DWORD,
652
+
};
653
+
pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;
654
+
655
+
pub const MOUSE_EVENT_RECORD = extern struct {
656
+
dwMousePosition: windows.COORD,
657
+
dwButtonState: windows.DWORD,
658
+
dwControlKeyState: windows.DWORD,
659
+
dwEventFlags: windows.DWORD,
660
+
};
661
+
pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;
662
+
663
+
pub const WINDOW_BUFFER_SIZE_RECORD = extern struct {
664
+
dwSize: windows.COORD,
665
+
};
666
+
pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;
667
+
668
+
pub const MENU_EVENT_RECORD = extern struct {
669
+
dwCommandId: windows.UINT,
670
+
};
671
+
pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;
672
+
673
+
pub const FOCUS_EVENT_RECORD = extern struct {
674
+
bSetFocus: windows.BOOL,
675
+
};
676
+
pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;
677
+
678
+
const union_unnamed_249 = extern union {
679
+
KeyEvent: KEY_EVENT_RECORD,
680
+
MouseEvent: MOUSE_EVENT_RECORD,
681
+
WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
682
+
MenuEvent: MENU_EVENT_RECORD,
683
+
FocusEvent: FOCUS_EVENT_RECORD,
684
+
};
685
+
pub const INPUT_RECORD = extern struct {
686
+
EventType: windows.WORD,
687
+
Event: union_unnamed_249,
688
+
};
689
+
pub const PINPUT_RECORD = *INPUT_RECORD;
690
+
691
+
pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(.winapi) windows.BOOL;
692
+
};
693
+
694
+
pub const TestTty = struct {
695
+
/// Used for API compat
696
+
fd: posix.fd_t,
697
+
pipe_read: posix.fd_t,
698
+
pipe_write: posix.fd_t,
699
+
tty_writer: *std.Io.Writer.Allocating,
700
+
701
+
/// Initializes a TestTty.
702
+
pub fn init(buffer: []u8) !TestTty {
703
+
_ = buffer;
704
+
705
+
if (builtin.os.tag == .windows) return error.SkipZigTest;
706
+
const list = try std.testing.allocator.create(std.Io.Writer.Allocating);
707
+
list.* = .init(std.testing.allocator);
708
+
const r, const w = try posix.pipe();
709
+
return .{
710
+
.fd = r,
711
+
.pipe_read = r,
712
+
.pipe_write = w,
713
+
.tty_writer = list,
714
+
};
715
+
}
716
+
717
+
pub fn deinit(self: TestTty) void {
718
+
std.posix.close(self.pipe_read);
719
+
std.posix.close(self.pipe_write);
720
+
self.tty_writer.deinit();
721
+
std.testing.allocator.destroy(self.tty_writer);
722
+
}
723
+
724
+
pub fn writer(self: *TestTty) *std.Io.Writer {
725
+
return &self.tty_writer.writer;
726
+
}
727
+
728
+
pub fn read(self: *const TestTty, buf: []u8) !usize {
729
+
return posix.read(self.fd, buf);
730
+
}
731
+
732
+
/// Get the window size from the kernel
733
+
pub fn getWinsize(_: posix.fd_t) !Winsize {
734
+
return .{
735
+
.rows = 40,
736
+
.cols = 80,
737
+
.x_pixel = 40 * 8,
738
+
.y_pixel = 40 * 8 * 2,
739
+
};
740
+
}
741
+
742
+
/// Implemented for the Windows API
743
+
pub fn nextEvent(_: *Tty, _: *Parser, _: ?std.mem.Allocator) !Event {
744
+
return error.SkipZigTest;
745
+
}
746
+
747
+
pub fn resetSignalHandler() void {
748
+
return;
749
+
}
750
+
};
+64
src/unicode.zig
+64
src/unicode.zig
···
···
1
+
const std = @import("std");
2
+
const uucode = @import("uucode");
3
+
4
+
// Old API-compatible Grapheme value
5
+
pub const Grapheme = struct {
6
+
start: usize,
7
+
len: usize,
8
+
9
+
pub fn bytes(self: Grapheme, str: []const u8) []const u8 {
10
+
return str[self.start .. self.start + self.len];
11
+
}
12
+
};
13
+
14
+
// Old API-compatible iterator that yields Grapheme with .len and .bytes()
15
+
pub const GraphemeIterator = struct {
16
+
str: []const u8,
17
+
inner: uucode.grapheme.Iterator(uucode.utf8.Iterator),
18
+
start: usize = 0,
19
+
prev_break: bool = true,
20
+
21
+
pub fn init(str: []const u8) GraphemeIterator {
22
+
return .{
23
+
.str = str,
24
+
.inner = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(str)),
25
+
};
26
+
}
27
+
28
+
pub fn next(self: *GraphemeIterator) ?Grapheme {
29
+
while (self.inner.next()) |res| {
30
+
// When leaving a break and entering a non-break, set the start of a cluster
31
+
if (self.prev_break and !res.is_break) {
32
+
const cp_len: usize = std.unicode.utf8CodepointSequenceLength(res.cp) catch 1;
33
+
self.start = self.inner.i - cp_len;
34
+
}
35
+
36
+
// A break marks the end of the current grapheme
37
+
if (res.is_break) {
38
+
const end = self.inner.i;
39
+
const s = self.start;
40
+
self.start = end;
41
+
self.prev_break = true;
42
+
return .{ .start = s, .len = end - s };
43
+
}
44
+
45
+
self.prev_break = false;
46
+
}
47
+
48
+
// Flush the last grapheme if we ended mid-cluster
49
+
if (!self.prev_break and self.start < self.str.len) {
50
+
const s = self.start;
51
+
const len = self.str.len - s;
52
+
self.start = self.str.len;
53
+
self.prev_break = true;
54
+
return .{ .start = s, .len = len };
55
+
}
56
+
57
+
return null;
58
+
}
59
+
};
60
+
61
+
/// creates a grapheme iterator based on str
62
+
pub fn graphemeIterator(str: []const u8) GraphemeIterator {
63
+
return GraphemeIterator.init(str);
64
+
}
+604
src/vxfw/App.zig
+604
src/vxfw/App.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
const vxfw = @import("vxfw.zig");
4
+
5
+
const assert = std.debug.assert;
6
+
7
+
const Allocator = std.mem.Allocator;
8
+
9
+
const EventLoop = vaxis.Loop(vxfw.Event);
10
+
const Widget = vxfw.Widget;
11
+
12
+
const App = @This();
13
+
14
+
allocator: Allocator,
15
+
tty: vaxis.Tty,
16
+
vx: vaxis.Vaxis,
17
+
timers: std.ArrayList(vxfw.Tick),
18
+
wants_focus: ?vxfw.Widget,
19
+
buffer: [1024]u8,
20
+
21
+
/// Runtime options
22
+
pub const Options = struct {
23
+
/// Frames per second
24
+
framerate: u8 = 60,
25
+
};
26
+
27
+
/// Create an application. We require stable pointers to do the set up, so this will create an App
28
+
/// object on the heap. Call destroy when the app is complete to reset terminal state and release
29
+
/// resources
30
+
pub fn init(allocator: Allocator) !App {
31
+
var app: App = .{
32
+
.allocator = allocator,
33
+
.tty = undefined,
34
+
.vx = try vaxis.init(allocator, .{
35
+
.system_clipboard_allocator = allocator,
36
+
.kitty_keyboard_flags = .{
37
+
.report_events = true,
38
+
},
39
+
}),
40
+
.timers = std.ArrayList(vxfw.Tick){},
41
+
.wants_focus = null,
42
+
.buffer = undefined,
43
+
};
44
+
app.tty = try vaxis.Tty.init(&app.buffer);
45
+
return app;
46
+
}
47
+
48
+
pub fn deinit(self: *App) void {
49
+
self.timers.deinit(self.allocator);
50
+
self.vx.deinit(self.allocator, self.tty.writer());
51
+
self.tty.deinit();
52
+
}
53
+
54
+
pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void {
55
+
const tty = &self.tty;
56
+
const vx = &self.vx;
57
+
58
+
var loop: EventLoop = .{ .tty = tty, .vaxis = vx };
59
+
try loop.start();
60
+
defer loop.stop();
61
+
62
+
// Send the init event
63
+
loop.postEvent(.init);
64
+
// Also always initialize the app with a focus event
65
+
loop.postEvent(.focus_in);
66
+
67
+
try vx.enterAltScreen(tty.writer());
68
+
try vx.queryTerminal(tty.writer(), 1 * std.time.ns_per_s);
69
+
try vx.setBracketedPaste(tty.writer(), true);
70
+
try vx.subscribeToColorSchemeUpdates(tty.writer());
71
+
72
+
{
73
+
// This part deserves a comment. loop.init installs a signal handler for the tty. We wait to
74
+
// init the loop until we know if we need this handler. We don't need it if the terminal
75
+
// supports in-band-resize
76
+
if (!vx.state.in_band_resize) try loop.init();
77
+
}
78
+
79
+
// NOTE: We don't use pixel mouse anywhere
80
+
vx.caps.sgr_pixels = false;
81
+
try vx.setMouseMode(tty.writer(), true);
82
+
83
+
vxfw.DrawContext.init(vx.screen.width_method);
84
+
85
+
const framerate: u64 = if (opts.framerate > 0) opts.framerate else 60;
86
+
// Calculate tick rate
87
+
const tick_ms: u64 = @divFloor(std.time.ms_per_s, framerate);
88
+
89
+
// Set up arena and context
90
+
var arena = std.heap.ArenaAllocator.init(self.allocator);
91
+
defer arena.deinit();
92
+
93
+
var mouse_handler = MouseHandler.init(widget);
94
+
defer mouse_handler.deinit(self.allocator);
95
+
var focus_handler = FocusHandler.init(self.allocator, widget);
96
+
try focus_handler.path_to_focused.append(self.allocator, widget);
97
+
defer focus_handler.deinit(self.allocator);
98
+
99
+
// Timestamp of our next frame
100
+
var next_frame_ms: u64 = @intCast(std.time.milliTimestamp());
101
+
102
+
// Create our event context
103
+
var ctx: vxfw.EventContext = .{
104
+
.alloc = self.allocator,
105
+
.phase = .capturing,
106
+
.cmds = vxfw.CommandList{},
107
+
.consume_event = false,
108
+
.redraw = false,
109
+
.quit = false,
110
+
};
111
+
defer ctx.cmds.deinit(self.allocator);
112
+
113
+
while (true) {
114
+
const now_ms: u64 = @intCast(std.time.milliTimestamp());
115
+
if (now_ms >= next_frame_ms) {
116
+
// Deadline exceeded. Schedule the next frame
117
+
next_frame_ms = now_ms + tick_ms;
118
+
} else {
119
+
// Sleep until the deadline
120
+
std.Thread.sleep((next_frame_ms - now_ms) * std.time.ns_per_ms);
121
+
next_frame_ms += tick_ms;
122
+
}
123
+
124
+
try self.checkTimers(&ctx);
125
+
126
+
{
127
+
loop.queue.lock();
128
+
defer loop.queue.unlock();
129
+
while (loop.queue.drain()) |event| {
130
+
defer {
131
+
// Reset our context
132
+
ctx.consume_event = false;
133
+
ctx.phase = .capturing;
134
+
}
135
+
switch (event) {
136
+
.key_press => {
137
+
try focus_handler.handleEvent(&ctx, event);
138
+
try self.handleCommand(&ctx.cmds);
139
+
},
140
+
.focus_out => {
141
+
try mouse_handler.mouseExit(self, &ctx);
142
+
try focus_handler.handleEvent(&ctx, .focus_out);
143
+
try self.handleCommand(&ctx.cmds);
144
+
},
145
+
.focus_in => {
146
+
try focus_handler.handleEvent(&ctx, .focus_in);
147
+
try self.handleCommand(&ctx.cmds);
148
+
},
149
+
.mouse => |mouse| try mouse_handler.handleMouse(self, &ctx, mouse),
150
+
.winsize => |ws| {
151
+
try vx.resize(self.allocator, tty.writer(), ws);
152
+
ctx.redraw = true;
153
+
},
154
+
else => {
155
+
try focus_handler.handleEvent(&ctx, event);
156
+
try self.handleCommand(&ctx.cmds);
157
+
},
158
+
}
159
+
}
160
+
}
161
+
162
+
// If we have a focus change, handle that event before we layout
163
+
if (self.wants_focus) |wants_focus| {
164
+
try focus_handler.focusWidget(&ctx, wants_focus);
165
+
try self.handleCommand(&ctx.cmds);
166
+
self.wants_focus = null;
167
+
}
168
+
169
+
// Check if we should quit
170
+
if (ctx.quit) return;
171
+
172
+
// Check if we need a redraw
173
+
if (!ctx.redraw) continue;
174
+
ctx.redraw = false;
175
+
// Clear the arena.
176
+
_ = arena.reset(.free_all);
177
+
// Assert that we have handled all commands
178
+
assert(ctx.cmds.items.len == 0);
179
+
180
+
const surface: vxfw.Surface = blk: {
181
+
// Draw the root widget
182
+
const surface = try self.doLayout(widget, &arena);
183
+
184
+
// Check if any hover or mouse effects changed
185
+
try mouse_handler.updateMouse(self, surface, &ctx);
186
+
// Our focus may have changed. Handle that here
187
+
if (self.wants_focus) |wants_focus| {
188
+
try focus_handler.focusWidget(&ctx, wants_focus);
189
+
try self.handleCommand(&ctx.cmds);
190
+
self.wants_focus = null;
191
+
}
192
+
193
+
assert(ctx.cmds.items.len == 0);
194
+
if (!ctx.redraw) break :blk surface;
195
+
// If updating the mouse required a redraw, we do the layout again
196
+
break :blk try self.doLayout(widget, &arena);
197
+
};
198
+
199
+
// Store the last frame
200
+
mouse_handler.last_frame = surface;
201
+
// Update the focus handler list
202
+
try focus_handler.update(self.allocator, surface);
203
+
try self.render(surface, focus_handler.focused_widget);
204
+
}
205
+
}
206
+
207
+
fn doLayout(
208
+
self: *App,
209
+
widget: vxfw.Widget,
210
+
arena: *std.heap.ArenaAllocator,
211
+
) !vxfw.Surface {
212
+
const vx = &self.vx;
213
+
214
+
const draw_context: vxfw.DrawContext = .{
215
+
.arena = arena.allocator(),
216
+
.min = .{ .width = 0, .height = 0 },
217
+
.max = .{
218
+
.width = @intCast(vx.screen.width),
219
+
.height = @intCast(vx.screen.height),
220
+
},
221
+
.cell_size = .{
222
+
.width = vx.screen.width_pix / vx.screen.width,
223
+
.height = vx.screen.height_pix / vx.screen.height,
224
+
},
225
+
};
226
+
return widget.draw(draw_context);
227
+
}
228
+
229
+
fn render(
230
+
self: *App,
231
+
surface: vxfw.Surface,
232
+
focused_widget: vxfw.Widget,
233
+
) !void {
234
+
const vx = &self.vx;
235
+
const tty = &self.tty;
236
+
237
+
const win = vx.window();
238
+
win.clear();
239
+
win.hideCursor();
240
+
win.setCursorShape(.default);
241
+
242
+
const root_win = win.child(.{
243
+
.width = surface.size.width,
244
+
.height = surface.size.height,
245
+
});
246
+
surface.render(root_win, focused_widget);
247
+
248
+
try vx.render(tty.writer());
249
+
}
250
+
251
+
fn addTick(self: *App, tick: vxfw.Tick) Allocator.Error!void {
252
+
try self.timers.append(self.allocator, tick);
253
+
std.sort.insertion(vxfw.Tick, self.timers.items, {}, vxfw.Tick.lessThan);
254
+
}
255
+
256
+
fn handleCommand(self: *App, cmds: *vxfw.CommandList) Allocator.Error!void {
257
+
defer cmds.clearRetainingCapacity();
258
+
for (cmds.items) |cmd| {
259
+
switch (cmd) {
260
+
.tick => |tick| try self.addTick(tick),
261
+
.set_mouse_shape => |shape| self.vx.setMouseShape(shape),
262
+
.request_focus => |widget| self.wants_focus = widget,
263
+
.copy_to_clipboard => |content| {
264
+
defer self.allocator.free(content);
265
+
self.vx.copyToSystemClipboard(self.tty.writer(), content, self.allocator) catch |err| {
266
+
switch (err) {
267
+
error.OutOfMemory => return Allocator.Error.OutOfMemory,
268
+
else => std.log.err("copy error: {}", .{err}),
269
+
}
270
+
};
271
+
},
272
+
.set_title => |title| {
273
+
defer self.allocator.free(title);
274
+
self.vx.setTitle(self.tty.writer(), title) catch |err| {
275
+
std.log.err("set_title error: {}", .{err});
276
+
};
277
+
},
278
+
.queue_refresh => self.vx.queueRefresh(),
279
+
.notify => |notification| {
280
+
self.vx.notify(self.tty.writer(), notification.title, notification.body) catch |err| {
281
+
std.log.err("notify error: {}", .{err});
282
+
};
283
+
const alloc = self.allocator;
284
+
if (notification.title) |title| {
285
+
alloc.free(title);
286
+
}
287
+
alloc.free(notification.body);
288
+
},
289
+
.query_color => |kind| {
290
+
self.vx.queryColor(self.tty.writer(), kind) catch |err| {
291
+
std.log.err("queryColor error: {}", .{err});
292
+
};
293
+
},
294
+
}
295
+
}
296
+
}
297
+
298
+
fn checkTimers(self: *App, ctx: *vxfw.EventContext) anyerror!void {
299
+
const now_ms = std.time.milliTimestamp();
300
+
301
+
// timers are always sorted descending
302
+
while (self.timers.pop()) |tick| {
303
+
if (now_ms < tick.deadline_ms) {
304
+
// re-add the timer
305
+
try self.timers.append(self.allocator, tick);
306
+
break;
307
+
}
308
+
try tick.widget.handleEvent(ctx, .tick);
309
+
}
310
+
try self.handleCommand(&ctx.cmds);
311
+
}
312
+
313
+
const MouseHandler = struct {
314
+
last_frame: vxfw.Surface,
315
+
last_hit_list: []vxfw.HitResult,
316
+
mouse: ?vaxis.Mouse,
317
+
318
+
fn init(root: Widget) MouseHandler {
319
+
return .{
320
+
.last_frame = .{
321
+
.size = .{ .width = 0, .height = 0 },
322
+
.widget = root,
323
+
.buffer = &.{},
324
+
.children = &.{},
325
+
},
326
+
.last_hit_list = &.{},
327
+
.mouse = null,
328
+
};
329
+
}
330
+
331
+
fn deinit(self: MouseHandler, gpa: Allocator) void {
332
+
gpa.free(self.last_hit_list);
333
+
}
334
+
335
+
fn updateMouse(
336
+
self: *MouseHandler,
337
+
app: *App,
338
+
surface: vxfw.Surface,
339
+
ctx: *vxfw.EventContext,
340
+
) anyerror!void {
341
+
const mouse = self.mouse orelse return;
342
+
// For mouse events we store the last frame and use that for hit testing
343
+
const last_frame = surface;
344
+
345
+
var hits = std.ArrayList(vxfw.HitResult){};
346
+
defer hits.deinit(app.allocator);
347
+
const sub: vxfw.SubSurface = .{
348
+
.origin = .{ .row = 0, .col = 0 },
349
+
.surface = last_frame,
350
+
.z_index = 0,
351
+
};
352
+
const mouse_point: vxfw.Point = .{
353
+
.row = @intCast(mouse.row),
354
+
.col = @intCast(mouse.col),
355
+
};
356
+
if (sub.containsPoint(mouse_point)) {
357
+
try last_frame.hitTest(app.allocator, &hits, mouse_point);
358
+
}
359
+
360
+
// We store the hit list from the last mouse event to determine mouse_enter and mouse_leave
361
+
// events. If list a is the previous hit list, and list b is the current hit list:
362
+
// - Widgets in a but not in b get a mouse_leave event
363
+
// - Widgets in b but not in a get a mouse_enter event
364
+
// - Widgets in both receive nothing
365
+
const a = self.last_hit_list;
366
+
const b = hits.items;
367
+
368
+
// Find widgets in a but not b
369
+
for (a) |a_item| {
370
+
const a_widget = a_item.widget;
371
+
for (b) |b_item| {
372
+
const b_widget = b_item.widget;
373
+
if (a_widget.eql(b_widget)) break;
374
+
} else {
375
+
// a_item is not in b
376
+
try a_widget.handleEvent(ctx, .mouse_leave);
377
+
try app.handleCommand(&ctx.cmds);
378
+
}
379
+
}
380
+
381
+
// Widgets in b but not in a
382
+
for (b) |b_item| {
383
+
const b_widget = b_item.widget;
384
+
for (a) |a_item| {
385
+
const a_widget = a_item.widget;
386
+
if (b_widget.eql(a_widget)) break;
387
+
} else {
388
+
// b_item is not in a.
389
+
try b_widget.handleEvent(ctx, .mouse_enter);
390
+
try app.handleCommand(&ctx.cmds);
391
+
}
392
+
}
393
+
394
+
// Store a copy of this hit list for next frame
395
+
app.allocator.free(self.last_hit_list);
396
+
self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items);
397
+
}
398
+
399
+
fn handleMouse(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext, mouse: vaxis.Mouse) anyerror!void {
400
+
// For mouse events we store the last frame and use that for hit testing
401
+
const last_frame = self.last_frame;
402
+
self.mouse = mouse;
403
+
404
+
var hits = std.ArrayList(vxfw.HitResult){};
405
+
defer hits.deinit(app.allocator);
406
+
const sub: vxfw.SubSurface = .{
407
+
.origin = .{ .row = 0, .col = 0 },
408
+
.surface = last_frame,
409
+
.z_index = 0,
410
+
};
411
+
const mouse_point: vxfw.Point = .{
412
+
.row = @intCast(mouse.row),
413
+
.col = @intCast(mouse.col),
414
+
};
415
+
if (sub.containsPoint(mouse_point)) {
416
+
try last_frame.hitTest(app.allocator, &hits, mouse_point);
417
+
}
418
+
419
+
// Handle mouse_enter and mouse_leave events
420
+
{
421
+
// We store the hit list from the last mouse event to determine mouse_enter and mouse_leave
422
+
// events. If list a is the previous hit list, and list b is the current hit list:
423
+
// - Widgets in a but not in b get a mouse_leave event
424
+
// - Widgets in b but not in a get a mouse_enter event
425
+
// - Widgets in both receive nothing
426
+
const a = self.last_hit_list;
427
+
const b = hits.items;
428
+
429
+
// Find widgets in a but not b
430
+
for (a) |a_item| {
431
+
const a_widget = a_item.widget;
432
+
for (b) |b_item| {
433
+
const b_widget = b_item.widget;
434
+
if (a_widget.eql(b_widget)) break;
435
+
} else {
436
+
// a_item is not in b
437
+
try a_widget.handleEvent(ctx, .mouse_leave);
438
+
try app.handleCommand(&ctx.cmds);
439
+
}
440
+
}
441
+
442
+
// Widgets in b but not in a
443
+
for (b) |b_item| {
444
+
const b_widget = b_item.widget;
445
+
for (a) |a_item| {
446
+
const a_widget = a_item.widget;
447
+
if (b_widget.eql(a_widget)) break;
448
+
} else {
449
+
// b_item is not in a.
450
+
try b_widget.handleEvent(ctx, .mouse_enter);
451
+
try app.handleCommand(&ctx.cmds);
452
+
}
453
+
}
454
+
455
+
// Store a copy of this hit list for next frame
456
+
app.allocator.free(self.last_hit_list);
457
+
self.last_hit_list = try app.allocator.dupe(vxfw.HitResult, hits.items);
458
+
}
459
+
460
+
const target = hits.pop() orelse return;
461
+
462
+
// capturing phase
463
+
ctx.phase = .capturing;
464
+
for (hits.items) |item| {
465
+
var m_local = mouse;
466
+
m_local.col = item.local.col;
467
+
m_local.row = item.local.row;
468
+
try item.widget.captureEvent(ctx, .{ .mouse = m_local });
469
+
try app.handleCommand(&ctx.cmds);
470
+
471
+
if (ctx.consume_event) return;
472
+
}
473
+
474
+
// target phase
475
+
ctx.phase = .at_target;
476
+
{
477
+
var m_local = mouse;
478
+
m_local.col = target.local.col;
479
+
m_local.row = target.local.row;
480
+
try target.widget.handleEvent(ctx, .{ .mouse = m_local });
481
+
try app.handleCommand(&ctx.cmds);
482
+
483
+
if (ctx.consume_event) return;
484
+
}
485
+
486
+
// Bubbling phase
487
+
ctx.phase = .bubbling;
488
+
while (hits.pop()) |item| {
489
+
var m_local = mouse;
490
+
m_local.col = item.local.col;
491
+
m_local.row = item.local.row;
492
+
try item.widget.handleEvent(ctx, .{ .mouse = m_local });
493
+
try app.handleCommand(&ctx.cmds);
494
+
495
+
if (ctx.consume_event) return;
496
+
}
497
+
}
498
+
499
+
/// sends .mouse_leave to all of the widgets from the last_hit_list
500
+
fn mouseExit(self: *MouseHandler, app: *App, ctx: *vxfw.EventContext) anyerror!void {
501
+
for (self.last_hit_list) |item| {
502
+
try item.widget.handleEvent(ctx, .mouse_leave);
503
+
try app.handleCommand(&ctx.cmds);
504
+
}
505
+
}
506
+
};
507
+
508
+
/// Maintains a tree of focusable nodes. Delivers events to the currently focused node, walking up
509
+
/// the tree until the event is handled
510
+
const FocusHandler = struct {
511
+
root: Widget,
512
+
focused_widget: vxfw.Widget,
513
+
path_to_focused: std.ArrayList(Widget),
514
+
515
+
fn init(_: Allocator, root: Widget) FocusHandler {
516
+
return .{
517
+
.root = root,
518
+
.focused_widget = root,
519
+
.path_to_focused = std.ArrayList(Widget){},
520
+
};
521
+
}
522
+
523
+
fn deinit(self: *FocusHandler, allocator: Allocator) void {
524
+
self.path_to_focused.deinit(allocator);
525
+
}
526
+
527
+
/// Update the focus list
528
+
fn update(self: *FocusHandler, allocator: Allocator, surface: vxfw.Surface) Allocator.Error!void {
529
+
// clear path
530
+
self.path_to_focused.clearAndFree(allocator);
531
+
532
+
// Find the path to the focused widget. This builds a list that has the first element as the
533
+
// focused widget, and walks backward to the root. It's possible our focused widget is *not*
534
+
// in this tree. If this is the case, we refocus to the root widget
535
+
_ = try self.childHasFocus(allocator, surface);
536
+
537
+
if (!self.root.eql(surface.widget)) {
538
+
// If the root of surface is not the initial widget, we append the initial widget
539
+
try self.path_to_focused.append(allocator, self.root);
540
+
}
541
+
542
+
// reverse path_to_focused so that it is root first
543
+
std.mem.reverse(Widget, self.path_to_focused.items);
544
+
}
545
+
546
+
/// Returns true if a child of surface is the focused widget
547
+
fn childHasFocus(
548
+
self: *FocusHandler,
549
+
allocator: Allocator,
550
+
surface: vxfw.Surface,
551
+
) Allocator.Error!bool {
552
+
// Check if we are the focused widget
553
+
if (self.focused_widget.eql(surface.widget)) {
554
+
try self.path_to_focused.append(allocator, surface.widget);
555
+
return true;
556
+
}
557
+
for (surface.children) |child| {
558
+
// Add child to list if it is the focused widget or one of it's own children is
559
+
if (try self.childHasFocus(allocator, child.surface)) {
560
+
try self.path_to_focused.append(allocator, surface.widget);
561
+
return true;
562
+
}
563
+
}
564
+
return false;
565
+
}
566
+
567
+
fn focusWidget(self: *FocusHandler, ctx: *vxfw.EventContext, widget: vxfw.Widget) anyerror!void {
568
+
// Focusing a widget requires it to have an event handler
569
+
assert(widget.eventHandler != null);
570
+
if (self.focused_widget.eql(widget)) return;
571
+
572
+
ctx.phase = .at_target;
573
+
try self.focused_widget.handleEvent(ctx, .focus_out);
574
+
self.focused_widget = widget;
575
+
try self.focused_widget.handleEvent(ctx, .focus_in);
576
+
}
577
+
578
+
fn handleEvent(self: *FocusHandler, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
579
+
const path = self.path_to_focused.items;
580
+
assert(path.len > 0);
581
+
582
+
// Capturing phase. We send capture events from the root to the target (inclusive of target)
583
+
ctx.phase = .capturing;
584
+
for (path) |widget| {
585
+
try widget.captureEvent(ctx, event);
586
+
if (ctx.consume_event) return;
587
+
}
588
+
589
+
// Target phase. This is only sent to the target
590
+
ctx.phase = .at_target;
591
+
const target = self.path_to_focused.getLast();
592
+
try target.handleEvent(ctx, event);
593
+
if (ctx.consume_event) return;
594
+
595
+
// Bubbling phase. Bubbling phase moves from target (exclusive) to the root
596
+
ctx.phase = .bubbling;
597
+
const target_idx = path.len - 1;
598
+
var iter = std.mem.reverseIterator(path[0..target_idx]);
599
+
while (iter.next()) |widget| {
600
+
try widget.handleEvent(ctx, event);
601
+
if (ctx.consume_event) return;
602
+
}
603
+
}
604
+
};
+146
src/vxfw/Border.zig
+146
src/vxfw/Border.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
pub const BorderLabel = struct {
9
+
text: []const u8,
10
+
alignment: enum {
11
+
top_left,
12
+
top_center,
13
+
top_right,
14
+
bottom_left,
15
+
bottom_center,
16
+
bottom_right,
17
+
},
18
+
};
19
+
20
+
const Border = @This();
21
+
22
+
child: vxfw.Widget,
23
+
style: vaxis.Style = .{},
24
+
labels: []const BorderLabel = &[_]BorderLabel{},
25
+
26
+
pub fn widget(self: *const Border) vxfw.Widget {
27
+
return .{
28
+
.userdata = @constCast(self),
29
+
.drawFn = typeErasedDrawFn,
30
+
};
31
+
}
32
+
33
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
34
+
const self: *const Border = @ptrCast(@alignCast(ptr));
35
+
return self.draw(ctx);
36
+
}
37
+
38
+
/// If Border has a bounded maximum size, it will shrink the maximum size to account for the border
39
+
/// before drawing the child. If the size is unbounded, border will draw the child and then itself
40
+
/// around the childs size
41
+
pub fn draw(self: *const Border, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
42
+
const max_width: ?u16 = if (ctx.max.width) |width| width -| 2 else null;
43
+
const max_height: ?u16 = if (ctx.max.height) |height| height -| 2 else null;
44
+
45
+
const child_ctx = ctx.withConstraints(ctx.min, .{
46
+
.width = max_width,
47
+
.height = max_height,
48
+
});
49
+
const child = try self.child.draw(child_ctx);
50
+
51
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
52
+
children[0] = .{
53
+
.origin = .{ .col = 1, .row = 1 },
54
+
.z_index = 0,
55
+
.surface = child,
56
+
};
57
+
58
+
const size: vxfw.Size = .{ .width = child.size.width + 2, .height = child.size.height + 2 };
59
+
60
+
var surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), size, children);
61
+
62
+
// Draw the border
63
+
const right_edge = size.width -| 1;
64
+
const bottom_edge = size.height -| 1;
65
+
surf.writeCell(0, 0, .{ .char = .{ .grapheme = "โญ", .width = 1 }, .style = self.style });
66
+
surf.writeCell(right_edge, 0, .{ .char = .{ .grapheme = "โฎ", .width = 1 }, .style = self.style });
67
+
surf.writeCell(right_edge, bottom_edge, .{ .char = .{ .grapheme = "โฏ", .width = 1 }, .style = self.style });
68
+
surf.writeCell(0, bottom_edge, .{ .char = .{ .grapheme = "โฐ", .width = 1 }, .style = self.style });
69
+
70
+
var col: u16 = 1;
71
+
while (col < right_edge) : (col += 1) {
72
+
surf.writeCell(col, 0, .{ .char = .{ .grapheme = "โ", .width = 1 }, .style = self.style });
73
+
surf.writeCell(col, bottom_edge, .{ .char = .{ .grapheme = "โ", .width = 1 }, .style = self.style });
74
+
}
75
+
76
+
var row: u16 = 1;
77
+
while (row < bottom_edge) : (row += 1) {
78
+
surf.writeCell(0, row, .{ .char = .{ .grapheme = "โ", .width = 1 }, .style = self.style });
79
+
surf.writeCell(right_edge, row, .{ .char = .{ .grapheme = "โ", .width = 1 }, .style = self.style });
80
+
}
81
+
82
+
// Add border labels
83
+
for (self.labels) |label| {
84
+
const text_len: u16 = @intCast(ctx.stringWidth(label.text));
85
+
if (text_len == 0) continue;
86
+
87
+
const text_row: u16 = switch (label.alignment) {
88
+
.top_left, .top_center, .top_right => 0,
89
+
.bottom_left, .bottom_center, .bottom_right => bottom_edge,
90
+
};
91
+
92
+
var text_col: u16 = switch (label.alignment) {
93
+
.top_left, .bottom_left => 1,
94
+
.top_center, .bottom_center => @max((size.width - text_len) / 2, 1),
95
+
.top_right, .bottom_right => @max(size.width - 1 - text_len, 1),
96
+
};
97
+
98
+
var iter = ctx.graphemeIterator(label.text);
99
+
while (iter.next()) |grapheme| {
100
+
const text = grapheme.bytes(label.text);
101
+
const width: u16 = @intCast(ctx.stringWidth(text));
102
+
surf.writeCell(text_col, text_row, .{
103
+
.char = .{ .grapheme = text, .width = @intCast(width) },
104
+
.style = self.style,
105
+
});
106
+
text_col += width;
107
+
}
108
+
}
109
+
110
+
return surf;
111
+
}
112
+
113
+
test Border {
114
+
const Text = @import("Text.zig");
115
+
// Will be height=1, width=3
116
+
const text: Text = .{ .text = "abc" };
117
+
118
+
const border: Border = .{ .child = text.widget() };
119
+
120
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
121
+
defer arena.deinit();
122
+
vxfw.DrawContext.init(.unicode);
123
+
124
+
// Border will draw itself tightly around the child
125
+
const ctx: vxfw.DrawContext = .{
126
+
.arena = arena.allocator(),
127
+
.min = .{},
128
+
.max = .{ .width = 10, .height = 10 },
129
+
.cell_size = .{ .width = 10, .height = 20 },
130
+
};
131
+
132
+
const surface = try border.draw(ctx);
133
+
// Border should be the size of Text + 2
134
+
try std.testing.expectEqual(5, surface.size.width);
135
+
try std.testing.expectEqual(3, surface.size.height);
136
+
// Border has 1 child
137
+
try std.testing.expectEqual(1, surface.children.len);
138
+
const child = surface.children[0];
139
+
// The child is 1x3
140
+
try std.testing.expectEqual(3, child.surface.size.width);
141
+
try std.testing.expectEqual(1, child.surface.size.height);
142
+
}
143
+
144
+
test "refAllDecls" {
145
+
std.testing.refAllDecls(@This());
146
+
}
+214
src/vxfw/Button.zig
+214
src/vxfw/Button.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const vxfw = @import("vxfw.zig");
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const Center = @import("Center.zig");
9
+
const Text = @import("Text.zig");
10
+
11
+
const Button = @This();
12
+
13
+
// User supplied values
14
+
label: []const u8,
15
+
onClick: *const fn (?*anyopaque, ctx: *vxfw.EventContext) anyerror!void,
16
+
userdata: ?*anyopaque = null,
17
+
18
+
// Styles
19
+
style: struct {
20
+
default: vaxis.Style = .{ .reverse = true },
21
+
mouse_down: vaxis.Style = .{ .fg = .{ .index = 4 }, .reverse = true },
22
+
hover: vaxis.Style = .{ .fg = .{ .index = 3 }, .reverse = true },
23
+
focus: vaxis.Style = .{ .fg = .{ .index = 5 }, .reverse = true },
24
+
} = .{},
25
+
26
+
// State
27
+
mouse_down: bool = false,
28
+
has_mouse: bool = false,
29
+
focused: bool = false,
30
+
31
+
pub fn widget(self: *Button) vxfw.Widget {
32
+
return .{
33
+
.userdata = self,
34
+
.eventHandler = typeErasedEventHandler,
35
+
.drawFn = typeErasedDrawFn,
36
+
};
37
+
}
38
+
39
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
40
+
const self: *Button = @ptrCast(@alignCast(ptr));
41
+
return self.handleEvent(ctx, event);
42
+
}
43
+
44
+
pub fn handleEvent(self: *Button, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
45
+
switch (event) {
46
+
.key_press => |key| {
47
+
if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
48
+
return self.doClick(ctx);
49
+
}
50
+
},
51
+
.mouse => |mouse| {
52
+
if (self.mouse_down and mouse.type == .release) {
53
+
self.mouse_down = false;
54
+
return self.doClick(ctx);
55
+
}
56
+
if (mouse.type == .press and mouse.button == .left) {
57
+
self.mouse_down = true;
58
+
return ctx.consumeAndRedraw();
59
+
}
60
+
return ctx.consumeEvent();
61
+
},
62
+
.mouse_enter => {
63
+
// implicit redraw
64
+
self.has_mouse = true;
65
+
try ctx.setMouseShape(.pointer);
66
+
return ctx.consumeAndRedraw();
67
+
},
68
+
.mouse_leave => {
69
+
self.has_mouse = false;
70
+
self.mouse_down = false;
71
+
// implicit redraw
72
+
try ctx.setMouseShape(.default);
73
+
},
74
+
.focus_in => {
75
+
self.focused = true;
76
+
ctx.redraw = true;
77
+
},
78
+
.focus_out => {
79
+
self.focused = false;
80
+
ctx.redraw = true;
81
+
},
82
+
else => {},
83
+
}
84
+
}
85
+
86
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
87
+
const self: *Button = @ptrCast(@alignCast(ptr));
88
+
return self.draw(ctx);
89
+
}
90
+
91
+
pub fn draw(self: *Button, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
92
+
const style: vaxis.Style = if (self.mouse_down)
93
+
self.style.mouse_down
94
+
else if (self.has_mouse)
95
+
self.style.hover
96
+
else if (self.focused)
97
+
self.style.focus
98
+
else
99
+
self.style.default;
100
+
101
+
const text: Text = .{
102
+
.style = style,
103
+
.text = self.label,
104
+
.text_align = .center,
105
+
};
106
+
107
+
const center: Center = .{ .child = text.widget() };
108
+
const surf = try center.draw(ctx);
109
+
110
+
const button_surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), surf.size, surf.children);
111
+
@memset(button_surf.buffer, .{ .style = style });
112
+
return button_surf;
113
+
}
114
+
115
+
fn doClick(self: *Button, ctx: *vxfw.EventContext) anyerror!void {
116
+
try self.onClick(self.userdata, ctx);
117
+
ctx.consume_event = true;
118
+
}
119
+
120
+
test Button {
121
+
// Create some object which reacts to a button press
122
+
const Foo = struct {
123
+
count: u8,
124
+
125
+
fn onClick(ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
126
+
const foo: *@This() = @ptrCast(@alignCast(ptr));
127
+
foo.count +|= 1;
128
+
ctx.consumeAndRedraw();
129
+
}
130
+
};
131
+
var foo: Foo = .{ .count = 0 };
132
+
133
+
var button: Button = .{
134
+
.label = "Test Button",
135
+
.onClick = Foo.onClick,
136
+
.userdata = &foo,
137
+
};
138
+
139
+
// Event handlers need a context
140
+
var ctx: vxfw.EventContext = .{
141
+
.alloc = std.testing.allocator,
142
+
.cmds = .empty,
143
+
};
144
+
defer ctx.cmds.deinit(ctx.alloc);
145
+
146
+
// Get the widget interface
147
+
const b_widget = button.widget();
148
+
149
+
// Create a synthetic mouse event
150
+
var mouse_event: vaxis.Mouse = .{
151
+
.col = 0,
152
+
.row = 0,
153
+
.mods = .{},
154
+
.button = .left,
155
+
.type = .press,
156
+
};
157
+
// Send the button a mouse press event
158
+
try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
159
+
160
+
// A press alone doesn't trigger onClick
161
+
try std.testing.expectEqual(0, foo.count);
162
+
163
+
// Send the button a mouse release event. The onClick handler is called
164
+
mouse_event.type = .release;
165
+
try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
166
+
try std.testing.expectEqual(1, foo.count);
167
+
168
+
// Send it another press
169
+
mouse_event.type = .press;
170
+
try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
171
+
172
+
// Now the mouse leaves
173
+
try b_widget.handleEvent(&ctx, .mouse_leave);
174
+
175
+
// Then it comes back. We don't know it but the button was pressed outside of our widget. We
176
+
// receie the release event
177
+
mouse_event.type = .release;
178
+
try b_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
179
+
180
+
// But we didn't have the press registered, so we don't call onClick
181
+
try std.testing.expectEqual(1, foo.count);
182
+
183
+
// Now we receive an enter keypress. This also triggers the onClick handler
184
+
try b_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.enter } });
185
+
try std.testing.expectEqual(2, foo.count);
186
+
187
+
// Now we draw the button. Set up our context with some unicode data
188
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
189
+
defer arena.deinit();
190
+
vxfw.DrawContext.init(.unicode);
191
+
192
+
const draw_ctx: vxfw.DrawContext = .{
193
+
.arena = arena.allocator(),
194
+
.min = .{},
195
+
.max = .{ .width = 13, .height = 3 },
196
+
.cell_size = .{ .width = 10, .height = 20 },
197
+
};
198
+
const surface = try b_widget.draw(draw_ctx);
199
+
200
+
// The button should fill the available space.
201
+
try std.testing.expectEqual(surface.size.width, draw_ctx.max.width.?);
202
+
try std.testing.expectEqual(surface.size.height, draw_ctx.max.height.?);
203
+
204
+
// It should have one child, the label
205
+
try std.testing.expectEqual(1, surface.children.len);
206
+
207
+
// The label should be centered
208
+
try std.testing.expectEqual(1, surface.children[0].origin.row);
209
+
try std.testing.expectEqual(1, surface.children[0].origin.col);
210
+
}
211
+
212
+
test "refAllDecls" {
213
+
std.testing.refAllDecls(@This());
214
+
}
+113
src/vxfw/Center.zig
+113
src/vxfw/Center.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const Center = @This();
9
+
10
+
child: vxfw.Widget,
11
+
12
+
pub fn widget(self: *const Center) vxfw.Widget {
13
+
return .{
14
+
.userdata = @constCast(self),
15
+
.drawFn = typeErasedDrawFn,
16
+
};
17
+
}
18
+
19
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
20
+
const self: *const Center = @ptrCast(@alignCast(ptr));
21
+
return self.draw(ctx);
22
+
}
23
+
24
+
/// Cannot have unbounded constraints
25
+
pub fn draw(self: *const Center, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
26
+
const child_ctx = ctx.withConstraints(.{ .width = 0, .height = 0 }, ctx.max);
27
+
const max_size = ctx.max.size();
28
+
const child = try self.child.draw(child_ctx);
29
+
30
+
const x = (max_size.width - child.size.width) / 2;
31
+
const y = (max_size.height - child.size.height) / 2;
32
+
33
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
34
+
children[0] = .{
35
+
.origin = .{ .col = x, .row = y },
36
+
.z_index = 0,
37
+
.surface = child,
38
+
};
39
+
40
+
return .{
41
+
.size = max_size,
42
+
.widget = self.widget(),
43
+
.buffer = &.{},
44
+
.children = children,
45
+
};
46
+
}
47
+
48
+
test Center {
49
+
const Text = @import("Text.zig");
50
+
// Will be height=1, width=3
51
+
const text: Text = .{ .text = "abc" };
52
+
53
+
const center: Center = .{ .child = text.widget() };
54
+
55
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
56
+
defer arena.deinit();
57
+
vxfw.DrawContext.init(.unicode);
58
+
59
+
{
60
+
// Center expands to the max size. It must therefore have non-null max width and max height.
61
+
// These values are asserted in draw
62
+
const ctx: vxfw.DrawContext = .{
63
+
.arena = arena.allocator(),
64
+
.min = .{},
65
+
.max = .{ .width = 10, .height = 10 },
66
+
.cell_size = .{ .width = 10, .height = 20 },
67
+
};
68
+
69
+
const surface = try center.draw(ctx);
70
+
// Center does not produce any drawable cells
71
+
try std.testing.expectEqual(0, surface.buffer.len);
72
+
// Center has 1 child
73
+
try std.testing.expectEqual(1, surface.children.len);
74
+
// Center is the max size
75
+
try std.testing.expectEqual(surface.size, ctx.max.size());
76
+
const child = surface.children[0];
77
+
// The child is 1x3
78
+
try std.testing.expectEqual(3, child.surface.size.width);
79
+
try std.testing.expectEqual(1, child.surface.size.height);
80
+
// A centered 1x3 in 10x10 should be at origin 3, 4. The bias is toward the top left corner
81
+
try std.testing.expectEqual(4, child.origin.row);
82
+
try std.testing.expectEqual(3, child.origin.col);
83
+
}
84
+
{
85
+
// Center expands to the max size. It must therefore have non-null max width and max height.
86
+
// These values are asserted in draw
87
+
const ctx: vxfw.DrawContext = .{
88
+
.arena = arena.allocator(),
89
+
.min = .{},
90
+
.max = .{ .width = 5, .height = 3 },
91
+
.cell_size = .{ .width = 10, .height = 20 },
92
+
};
93
+
94
+
const surface = try center.draw(ctx);
95
+
// Center does not produce any drawable cells
96
+
try std.testing.expectEqual(0, surface.buffer.len);
97
+
// Center has 1 child
98
+
try std.testing.expectEqual(1, surface.children.len);
99
+
// Center is the max size
100
+
try std.testing.expectEqual(surface.size, ctx.max.size());
101
+
const child = surface.children[0];
102
+
// The child is 1x3
103
+
try std.testing.expectEqual(3, child.surface.size.width);
104
+
try std.testing.expectEqual(1, child.surface.size.height);
105
+
// A centered 1x3 in 3x5 should be at origin 1, 1. This is a perfectly centered child
106
+
try std.testing.expectEqual(1, child.origin.row);
107
+
try std.testing.expectEqual(1, child.origin.col);
108
+
}
109
+
}
110
+
111
+
test "refAllDecls" {
112
+
std.testing.refAllDecls(@This());
113
+
}
+162
src/vxfw/FlexColumn.zig
+162
src/vxfw/FlexColumn.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const FlexColumn = @This();
9
+
10
+
children: []const vxfw.FlexItem,
11
+
12
+
pub fn widget(self: *const FlexColumn) vxfw.Widget {
13
+
return .{
14
+
.userdata = @constCast(self),
15
+
.drawFn = typeErasedDrawFn,
16
+
};
17
+
}
18
+
19
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
20
+
const self: *const FlexColumn = @ptrCast(@alignCast(ptr));
21
+
return self.draw(ctx);
22
+
}
23
+
24
+
pub fn draw(self: *const FlexColumn, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
25
+
std.debug.assert(ctx.max.height != null);
26
+
std.debug.assert(ctx.max.width != null);
27
+
if (self.children.len == 0) return vxfw.Surface.init(ctx.arena, self.widget(), ctx.min);
28
+
29
+
// Store the inherent size of each widget
30
+
const size_list = try ctx.arena.alloc(u16, self.children.len);
31
+
32
+
var layout_arena = std.heap.ArenaAllocator.init(ctx.arena);
33
+
34
+
const layout_ctx: vxfw.DrawContext = .{
35
+
.min = .{ .width = 0, .height = 0 },
36
+
.max = .{ .width = ctx.max.width, .height = null },
37
+
.arena = layout_arena.allocator(),
38
+
.cell_size = ctx.cell_size,
39
+
};
40
+
41
+
// Store the inherent size of each widget
42
+
var first_pass_height: u16 = 0;
43
+
var total_flex: u16 = 0;
44
+
for (self.children, 0..) |child, i| {
45
+
const surf = try child.widget.draw(layout_ctx);
46
+
first_pass_height += surf.size.height;
47
+
total_flex += child.flex;
48
+
size_list[i] = surf.size.height;
49
+
}
50
+
51
+
// We are done with the layout arena
52
+
layout_arena.deinit();
53
+
54
+
// make our children list
55
+
var children: std.ArrayList(vxfw.SubSurface) = .empty;
56
+
57
+
// Draw again, but with distributed heights
58
+
var second_pass_height: u16 = 0;
59
+
var max_width: u16 = 0;
60
+
const remaining_space = ctx.max.height.? - first_pass_height;
61
+
for (self.children, 1..) |child, i| {
62
+
const inherent_height = size_list[i - 1];
63
+
const child_height = if (child.flex == 0)
64
+
inherent_height
65
+
else if (i == self.children.len)
66
+
// If we are the last one, we just get the remainder
67
+
ctx.max.height.? - second_pass_height
68
+
else
69
+
inherent_height + (remaining_space * child.flex) / total_flex;
70
+
71
+
// Create a context for the child
72
+
const child_ctx = ctx.withConstraints(
73
+
.{ .width = 0, .height = child_height },
74
+
.{ .width = ctx.max.width.?, .height = child_height },
75
+
);
76
+
const surf = try child.widget.draw(child_ctx);
77
+
78
+
try children.append(ctx.arena, .{
79
+
.origin = .{ .col = 0, .row = second_pass_height },
80
+
.surface = surf,
81
+
.z_index = 0,
82
+
});
83
+
max_width = @max(max_width, surf.size.width);
84
+
second_pass_height += surf.size.height;
85
+
}
86
+
87
+
const size: vxfw.Size = .{ .width = max_width, .height = second_pass_height };
88
+
return .{
89
+
.size = size,
90
+
.widget = self.widget(),
91
+
.buffer = &.{},
92
+
.children = children.items,
93
+
};
94
+
}
95
+
96
+
test FlexColumn {
97
+
// Create child widgets
98
+
const Text = @import("Text.zig");
99
+
// Will be height=1, width=3
100
+
const abc: Text = .{ .text = "abc" };
101
+
const def: Text = .{ .text = "def" };
102
+
const ghi: Text = .{ .text = "ghi" };
103
+
const jklmno: Text = .{ .text = "jkl\nmno" };
104
+
105
+
// Create the flex column
106
+
const flex_column: FlexColumn = .{
107
+
.children = &.{
108
+
.{ .widget = abc.widget(), .flex = 0 }, // flex=0 means we are our inherent size
109
+
.{ .widget = def.widget(), .flex = 1 },
110
+
.{ .widget = ghi.widget(), .flex = 1 },
111
+
.{ .widget = jklmno.widget(), .flex = 1 },
112
+
},
113
+
};
114
+
115
+
// Boiler plate draw context
116
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
117
+
defer arena.deinit();
118
+
vxfw.DrawContext.init(.unicode);
119
+
120
+
const flex_widget = flex_column.widget();
121
+
const ctx: vxfw.DrawContext = .{
122
+
.arena = arena.allocator(),
123
+
.min = .{},
124
+
.max = .{ .width = 16, .height = 16 },
125
+
.cell_size = .{ .width = 10, .height = 20 },
126
+
};
127
+
128
+
const surface = try flex_widget.draw(ctx);
129
+
// FlexColumn expands to max height and widest child
130
+
try std.testing.expectEqual(16, surface.size.height);
131
+
try std.testing.expectEqual(3, surface.size.width);
132
+
// We have four children
133
+
try std.testing.expectEqual(4, surface.children.len);
134
+
135
+
// We will track the row we are on to confirm the origins
136
+
var row: u16 = 0;
137
+
// First child has flex=0, it should be it's inherent height
138
+
try std.testing.expectEqual(1, surface.children[0].surface.size.height);
139
+
try std.testing.expectEqual(row, surface.children[0].origin.row);
140
+
// Add the child height each time
141
+
row += surface.children[0].surface.size.height;
142
+
// Let's do some math
143
+
// - We have 4 children to fit into 16 rows. 3 children will be 1 row tall, one will be 2 rows
144
+
// tall for a total height of 5 rows.
145
+
// - The first child is 1 row and no flex. The rest of the height gets distributed evenly among
146
+
// the remaining 3 children. The remainder height is 16 - 5 = 11, so each child should get 11 /
147
+
// 3 = 3 extra rows, and the last will receive the remainder
148
+
try std.testing.expectEqual(1 + 3, surface.children[1].surface.size.height);
149
+
try std.testing.expectEqual(row, surface.children[1].origin.row);
150
+
row += surface.children[1].surface.size.height;
151
+
152
+
try std.testing.expectEqual(1 + 3, surface.children[2].surface.size.height);
153
+
try std.testing.expectEqual(row, surface.children[2].origin.row);
154
+
row += surface.children[2].surface.size.height;
155
+
156
+
try std.testing.expectEqual(2 + 3 + 2, surface.children[3].surface.size.height);
157
+
try std.testing.expectEqual(row, surface.children[3].origin.row);
158
+
}
159
+
160
+
test "refAllDecls" {
161
+
std.testing.refAllDecls(@This());
162
+
}
+160
src/vxfw/FlexRow.zig
+160
src/vxfw/FlexRow.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const vxfw = @import("vxfw.zig");
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const FlexRow = @This();
9
+
10
+
children: []const vxfw.FlexItem,
11
+
12
+
pub fn widget(self: *const FlexRow) vxfw.Widget {
13
+
return .{
14
+
.userdata = @constCast(self),
15
+
.drawFn = typeErasedDrawFn,
16
+
};
17
+
}
18
+
19
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
20
+
const self: *const FlexRow = @ptrCast(@alignCast(ptr));
21
+
return self.draw(ctx);
22
+
}
23
+
24
+
pub fn draw(self: *const FlexRow, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
25
+
std.debug.assert(ctx.max.height != null);
26
+
std.debug.assert(ctx.max.width != null);
27
+
if (self.children.len == 0) return vxfw.Surface.init(ctx.arena, self.widget(), ctx.min);
28
+
29
+
// Store the inherent size of each widget
30
+
const size_list = try ctx.arena.alloc(u16, self.children.len);
31
+
32
+
var layout_arena = std.heap.ArenaAllocator.init(ctx.arena);
33
+
34
+
const layout_ctx: vxfw.DrawContext = .{
35
+
.min = .{ .width = 0, .height = 0 },
36
+
.max = .{ .width = null, .height = ctx.max.height },
37
+
.arena = layout_arena.allocator(),
38
+
.cell_size = ctx.cell_size,
39
+
};
40
+
41
+
var first_pass_width: u16 = 0;
42
+
var total_flex: u16 = 0;
43
+
for (self.children, 0..) |child, i| {
44
+
if (child.flex == 0) {
45
+
const surf = try child.widget.draw(layout_ctx);
46
+
first_pass_width += surf.size.width;
47
+
size_list[i] = surf.size.width;
48
+
}
49
+
total_flex += child.flex;
50
+
}
51
+
52
+
// We are done with the layout arena
53
+
layout_arena.deinit();
54
+
55
+
// make our children list
56
+
var children: std.ArrayList(vxfw.SubSurface) = .empty;
57
+
58
+
// Draw again, but with distributed widths
59
+
var second_pass_width: u16 = 0;
60
+
var max_height: u16 = 0;
61
+
const remaining_space = ctx.max.width.? -| first_pass_width;
62
+
for (self.children, 0..) |child, i| {
63
+
const child_width = if (child.flex == 0)
64
+
size_list[i]
65
+
else if (i == self.children.len - 1)
66
+
// If we are the last one, we just get the remainder
67
+
ctx.max.width.? -| second_pass_width
68
+
else
69
+
(remaining_space * child.flex) / total_flex;
70
+
71
+
// Create a context for the child
72
+
const child_ctx = ctx.withConstraints(
73
+
.{ .width = child_width, .height = 0 },
74
+
.{ .width = child_width, .height = ctx.max.height.? },
75
+
);
76
+
const surf = try child.widget.draw(child_ctx);
77
+
78
+
try children.append(ctx.arena, .{
79
+
.origin = .{ .col = second_pass_width, .row = 0 },
80
+
.surface = surf,
81
+
.z_index = 0,
82
+
});
83
+
max_height = @max(max_height, surf.size.height);
84
+
second_pass_width += surf.size.width;
85
+
}
86
+
const size: vxfw.Size = .{ .width = second_pass_width, .height = max_height };
87
+
return .{
88
+
.size = size,
89
+
.widget = self.widget(),
90
+
.buffer = &.{},
91
+
.children = children.items,
92
+
};
93
+
}
94
+
95
+
test FlexRow {
96
+
// Create child widgets
97
+
const Text = @import("Text.zig");
98
+
// Will be height=1, width=3
99
+
const abc: Text = .{ .text = "abc" };
100
+
const def: Text = .{ .text = "def" };
101
+
const ghi: Text = .{ .text = "ghi" };
102
+
const jklmno: Text = .{ .text = "jkl\nmno" };
103
+
104
+
// Create the flex row
105
+
const flex_row: FlexRow = .{
106
+
.children = &.{
107
+
.{ .widget = abc.widget(), .flex = 0 }, // flex=0 means we are our inherent size
108
+
.{ .widget = def.widget(), .flex = 1 },
109
+
.{ .widget = ghi.widget(), .flex = 1 },
110
+
.{ .widget = jklmno.widget(), .flex = 1 },
111
+
},
112
+
};
113
+
114
+
// Boiler plate draw context
115
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
116
+
defer arena.deinit();
117
+
vxfw.DrawContext.init(.unicode);
118
+
119
+
const flex_widget = flex_row.widget();
120
+
const ctx: vxfw.DrawContext = .{
121
+
.arena = arena.allocator(),
122
+
.min = .{},
123
+
.max = .{ .width = 16, .height = 16 },
124
+
.cell_size = .{ .width = 10, .height = 20 },
125
+
};
126
+
127
+
const surface = try flex_widget.draw(ctx);
128
+
// FlexRow expands to max width and tallest child
129
+
try std.testing.expectEqual(16, surface.size.width);
130
+
try std.testing.expectEqual(2, surface.size.height);
131
+
// We have four children
132
+
try std.testing.expectEqual(4, surface.children.len);
133
+
134
+
// We will track the column we are on to confirm the origins
135
+
var col: u16 = 0;
136
+
// First child has flex=0, it should be it's inherent width
137
+
try std.testing.expectEqual(3, surface.children[0].surface.size.width);
138
+
try std.testing.expectEqual(col, surface.children[0].origin.col);
139
+
// Add the child height each time
140
+
col += surface.children[0].surface.size.width;
141
+
// Let's do some math
142
+
// - We have 4 children to fit into 16 cols. All children will be 3 wide for a total width of 12
143
+
// - The first child is 3 cols and no flex. The rest of the width gets distributed evenly among
144
+
// the remaining 3 children. The remainder width is 16 - 12 = 4, so each child should get 4 /
145
+
// 3 = 1 extra cols, and the last will receive the remainder
146
+
try std.testing.expectEqual(1 + 3, surface.children[1].surface.size.width);
147
+
try std.testing.expectEqual(col, surface.children[1].origin.col);
148
+
col += surface.children[1].surface.size.width;
149
+
150
+
try std.testing.expectEqual(1 + 3, surface.children[2].surface.size.width);
151
+
try std.testing.expectEqual(col, surface.children[2].origin.col);
152
+
col += surface.children[2].surface.size.width;
153
+
154
+
try std.testing.expectEqual(1 + 3 + 1, surface.children[3].surface.size.width);
155
+
try std.testing.expectEqual(col, surface.children[3].origin.col);
156
+
}
157
+
158
+
test "refAllDecls" {
159
+
std.testing.refAllDecls(@This());
160
+
}
+767
src/vxfw/ListView.zig
+767
src/vxfw/ListView.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const assert = std.debug.assert;
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const vxfw = @import("vxfw.zig");
9
+
10
+
const ListView = @This();
11
+
12
+
pub const Builder = struct {
13
+
userdata: *const anyopaque,
14
+
buildFn: *const fn (*const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget,
15
+
16
+
inline fn itemAtIdx(self: Builder, idx: usize, cursor: usize) ?vxfw.Widget {
17
+
return self.buildFn(self.userdata, idx, cursor);
18
+
}
19
+
};
20
+
21
+
pub const Source = union(enum) {
22
+
slice: []const vxfw.Widget,
23
+
builder: Builder,
24
+
};
25
+
26
+
const Scroll = struct {
27
+
/// Index of the first fully-in-view widget
28
+
top: u32 = 0,
29
+
/// Line offset within the top widget.
30
+
offset: i17 = 0,
31
+
/// Pending scroll amount
32
+
pending_lines: i17 = 0,
33
+
/// If there is more room to scroll down
34
+
has_more: bool = true,
35
+
/// The cursor must be in the viewport
36
+
wants_cursor: bool = false,
37
+
38
+
fn linesDown(self: *Scroll, n: u8) bool {
39
+
if (!self.has_more) return false;
40
+
self.pending_lines += n;
41
+
return true;
42
+
}
43
+
44
+
fn linesUp(self: *Scroll, n: u8) bool {
45
+
if (self.top == 0 and self.offset == 0) return false;
46
+
self.pending_lines -= @intCast(n);
47
+
return true;
48
+
}
49
+
};
50
+
51
+
const cursor_indicator: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } };
52
+
53
+
children: Source,
54
+
cursor: u32 = 0,
55
+
/// When true, the widget will draw a cursor next to the widget which has the cursor
56
+
draw_cursor: bool = true,
57
+
/// Lines to scroll for a mouse wheel
58
+
wheel_scroll: u8 = 3,
59
+
/// Set this if the exact item count is known.
60
+
item_count: ?u32 = null,
61
+
62
+
/// scroll position
63
+
scroll: Scroll = .{},
64
+
65
+
pub fn widget(self: *const ListView) vxfw.Widget {
66
+
return .{
67
+
.userdata = @constCast(self),
68
+
.eventHandler = typeErasedEventHandler,
69
+
.drawFn = typeErasedDrawFn,
70
+
};
71
+
}
72
+
73
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
74
+
const self: *ListView = @ptrCast(@alignCast(ptr));
75
+
return self.handleEvent(ctx, event);
76
+
}
77
+
78
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
79
+
const self: *ListView = @ptrCast(@alignCast(ptr));
80
+
return self.draw(ctx);
81
+
}
82
+
83
+
pub fn handleEvent(self: *ListView, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
84
+
switch (event) {
85
+
.mouse => |mouse| {
86
+
if (mouse.button == .wheel_up) {
87
+
if (self.scroll.linesUp(self.wheel_scroll))
88
+
ctx.consumeAndRedraw();
89
+
}
90
+
if (mouse.button == .wheel_down) {
91
+
if (self.scroll.linesDown(self.wheel_scroll))
92
+
ctx.consumeAndRedraw();
93
+
}
94
+
},
95
+
.key_press => |key| {
96
+
if (key.matches('j', .{}) or
97
+
key.matches('n', .{ .ctrl = true }) or
98
+
key.matches(vaxis.Key.down, .{}))
99
+
{
100
+
return self.nextItem(ctx);
101
+
}
102
+
if (key.matches('k', .{}) or
103
+
key.matches('p', .{ .ctrl = true }) or
104
+
key.matches(vaxis.Key.up, .{}))
105
+
{
106
+
return self.prevItem(ctx);
107
+
}
108
+
if (key.matches(vaxis.Key.escape, .{})) {
109
+
self.ensureScroll();
110
+
return ctx.consumeAndRedraw();
111
+
}
112
+
},
113
+
else => {},
114
+
}
115
+
}
116
+
117
+
pub fn draw(self: *ListView, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
118
+
std.debug.assert(ctx.max.width != null);
119
+
std.debug.assert(ctx.max.height != null);
120
+
switch (self.children) {
121
+
.slice => |slice| {
122
+
self.item_count = @intCast(slice.len);
123
+
const builder: SliceBuilder = .{ .slice = slice };
124
+
return self.drawBuilder(ctx, .{ .userdata = &builder, .buildFn = SliceBuilder.build });
125
+
},
126
+
.builder => |b| return self.drawBuilder(ctx, b),
127
+
}
128
+
}
129
+
130
+
pub fn nextItem(self: *ListView, ctx: *vxfw.EventContext) void {
131
+
// If we have a count, we can handle this directly
132
+
if (self.item_count) |count| {
133
+
if (self.cursor >= count -| 1) {
134
+
return ctx.consumeEvent();
135
+
}
136
+
self.cursor += 1;
137
+
} else {
138
+
switch (self.children) {
139
+
.slice => |slice| {
140
+
self.item_count = @intCast(slice.len);
141
+
// If we are already at the end, don't do anything
142
+
if (self.cursor == slice.len - 1) {
143
+
return ctx.consumeEvent();
144
+
}
145
+
// Advance the cursor
146
+
self.cursor += 1;
147
+
},
148
+
.builder => |builder| {
149
+
// Save our current state
150
+
const prev = self.cursor;
151
+
// Advance the cursor
152
+
self.cursor += 1;
153
+
// Check the bounds, reversing until we get the last item
154
+
while (builder.itemAtIdx(self.cursor, self.cursor) == null) {
155
+
self.cursor -|= 1;
156
+
}
157
+
// If we didn't change state, we don't redraw
158
+
if (self.cursor == prev) {
159
+
return ctx.consumeEvent();
160
+
}
161
+
},
162
+
}
163
+
}
164
+
// Reset scroll
165
+
self.ensureScroll();
166
+
ctx.consumeAndRedraw();
167
+
}
168
+
169
+
pub fn prevItem(self: *ListView, ctx: *vxfw.EventContext) void {
170
+
if (self.cursor == 0) {
171
+
return ctx.consumeEvent();
172
+
}
173
+
174
+
if (self.item_count) |count| {
175
+
// If for some reason our count changed, we handle it here
176
+
self.cursor = @min(self.cursor - 1, count - 1);
177
+
} else {
178
+
switch (self.children) {
179
+
.slice => |slice| {
180
+
self.item_count = @intCast(slice.len);
181
+
self.cursor = @min(self.cursor - 1, slice.len - 1);
182
+
},
183
+
.builder => |builder| {
184
+
// Save our current state
185
+
const prev = self.cursor;
186
+
// Decrement the cursor
187
+
self.cursor -= 1;
188
+
// Check the bounds, reversing until we get the last item
189
+
while (builder.itemAtIdx(self.cursor, self.cursor) == null) {
190
+
self.cursor -|= 1;
191
+
}
192
+
// If we didn't change state, we don't redraw
193
+
if (self.cursor == prev) {
194
+
return ctx.consumeEvent();
195
+
}
196
+
},
197
+
}
198
+
}
199
+
200
+
// Reset scroll
201
+
self.ensureScroll();
202
+
return ctx.consumeAndRedraw();
203
+
}
204
+
205
+
// Only call when cursor state has changed, or we want to ensure the cursored item is in view
206
+
pub fn ensureScroll(self: *ListView) void {
207
+
if (self.cursor <= self.scroll.top) {
208
+
self.scroll.top = @intCast(self.cursor);
209
+
self.scroll.offset = 0;
210
+
} else {
211
+
self.scroll.wants_cursor = true;
212
+
}
213
+
}
214
+
215
+
/// Inserts children until add_height is < 0
216
+
fn insertChildren(
217
+
self: *ListView,
218
+
ctx: vxfw.DrawContext,
219
+
builder: Builder,
220
+
child_list: *std.ArrayList(vxfw.SubSurface),
221
+
add_height: i17,
222
+
) Allocator.Error!void {
223
+
assert(self.scroll.top > 0);
224
+
self.scroll.top -= 1;
225
+
var upheight = add_height;
226
+
while (self.scroll.top >= 0) : (self.scroll.top -= 1) {
227
+
// Get the child
228
+
const child = builder.itemAtIdx(self.scroll.top, self.cursor) orelse break;
229
+
230
+
const child_offset: u16 = if (self.draw_cursor) 2 else 0;
231
+
const max_size = ctx.max.size();
232
+
233
+
// Set up constraints. We let the child be the entire height if it wants
234
+
const child_ctx = ctx.withConstraints(
235
+
.{ .width = max_size.width - child_offset, .height = 0 },
236
+
.{ .width = max_size.width - child_offset, .height = null },
237
+
);
238
+
239
+
// Draw the child
240
+
const surf = try child.draw(child_ctx);
241
+
242
+
// Accumulate the height. Traversing backward so do this before setting origin
243
+
upheight -= surf.size.height;
244
+
245
+
// Insert the child to the beginning of the list
246
+
try child_list.insert(ctx.arena, 0, .{
247
+
.origin = .{ .col = if (self.draw_cursor) 2 else 0, .row = upheight },
248
+
.surface = surf,
249
+
.z_index = 0,
250
+
});
251
+
252
+
// Break if we went past the top edge, or are the top item
253
+
if (upheight <= 0 or self.scroll.top == 0) break;
254
+
}
255
+
256
+
// Our new offset is the "upheight"
257
+
self.scroll.offset = upheight;
258
+
259
+
// Reset origins if we overshot and put the top item too low
260
+
if (self.scroll.top == 0 and upheight > 0) {
261
+
self.scroll.offset = 0;
262
+
var row: i17 = 0;
263
+
for (child_list.items) |*child| {
264
+
child.origin.row = row;
265
+
row += child.surface.size.height;
266
+
}
267
+
}
268
+
// Our new offset is the "upheight"
269
+
self.scroll.offset = upheight;
270
+
}
271
+
272
+
fn totalHeight(list: *const std.ArrayList(vxfw.SubSurface)) usize {
273
+
var result: usize = 0;
274
+
for (list.items) |child| {
275
+
result += child.surface.size.height;
276
+
}
277
+
return result;
278
+
}
279
+
280
+
fn drawBuilder(self: *ListView, ctx: vxfw.DrawContext, builder: Builder) Allocator.Error!vxfw.Surface {
281
+
defer self.scroll.wants_cursor = false;
282
+
283
+
// Get the size. asserts neither constraint is null
284
+
const max_size = ctx.max.size();
285
+
// Set up surface.
286
+
var surface: vxfw.Surface = .{
287
+
.size = max_size,
288
+
.widget = self.widget(),
289
+
.buffer = &.{},
290
+
.children = &.{},
291
+
};
292
+
if (self.draw_cursor) {
293
+
// If we are drawing the cursor, we need to allocate a buffer so that we obscure anything
294
+
// underneath us
295
+
surface.buffer = try vxfw.Surface.createBuffer(ctx.arena, max_size);
296
+
}
297
+
298
+
// Set state
299
+
{
300
+
// Assume we have more. We only know we don't after drawing
301
+
self.scroll.has_more = true;
302
+
}
303
+
304
+
var child_list: std.ArrayList(vxfw.SubSurface) = .empty;
305
+
306
+
// Accumulated height tracks how much height we have drawn. It's initial state is
307
+
// (scroll.offset + scroll.pending_lines) lines _above_ the surface top edge.
308
+
// Example:
309
+
// 1. Scroll up 3 lines:
310
+
// pending_lines = -3
311
+
// offset = 0
312
+
// accumulated_height = -(0 + -3) = 3;
313
+
// Our first widget is placed at row 3, we will need to fill this in after the draw
314
+
// 2. Scroll up 3 lines, with an offset of 4
315
+
// pending_lines = -3
316
+
// offset = 4
317
+
// accumulated_height = -(4 + -3) = -1;
318
+
// Our first widget is placed at row -1
319
+
// 3. Scroll down 3 lines:
320
+
// pending_lines = 3
321
+
// offset = 0
322
+
// accumulated_height = -(0 + 3) = -3;
323
+
// Our first widget is placed at row -3. It's possible it consumes the entire widget. We
324
+
// will check for this at the end and only include visible children
325
+
var accumulated_height: i17 = -(self.scroll.offset + self.scroll.pending_lines);
326
+
327
+
// We handled the pending scroll by assigning accumulated_height. Reset it's state
328
+
self.scroll.pending_lines = 0;
329
+
330
+
// Set the initial index for our downard loop. We do this here because we might modify
331
+
// scroll.top before we traverse downward
332
+
var i: usize = self.scroll.top;
333
+
334
+
// If we are on the first item, and we have an upward scroll that consumed our offset, eg
335
+
// accumulated_height > 0, we reset state here. We can't scroll up anymore so we set
336
+
// accumulated_height to 0.
337
+
if (accumulated_height > 0 and self.scroll.top == 0) {
338
+
self.scroll.offset = 0;
339
+
accumulated_height = 0;
340
+
}
341
+
342
+
// If we are offset downward, insert widgets to the front of the list before traversing downard
343
+
if (accumulated_height > 0) {
344
+
try self.insertChildren(ctx, builder, &child_list, accumulated_height);
345
+
const last_child = child_list.items[child_list.items.len - 1];
346
+
accumulated_height = last_child.origin.row + last_child.surface.size.height;
347
+
}
348
+
349
+
const child_offset: u16 = if (self.draw_cursor) 2 else 0;
350
+
351
+
while (builder.itemAtIdx(i, self.cursor)) |child| {
352
+
// Defer the increment
353
+
defer i += 1;
354
+
355
+
// Set up constraints. We let the child be the entire height if it wants
356
+
const child_ctx = ctx.withConstraints(
357
+
.{ .width = max_size.width -| child_offset, .height = 0 },
358
+
.{ .width = max_size.width -| child_offset, .height = null },
359
+
);
360
+
361
+
// Draw the child
362
+
const surf = try child.draw(child_ctx);
363
+
364
+
// Add the child surface to our list. It's offset from parent is the accumulated height
365
+
try child_list.append(ctx.arena, .{
366
+
.origin = .{ .col = child_offset, .row = accumulated_height },
367
+
.surface = surf,
368
+
.z_index = 0,
369
+
});
370
+
371
+
// Accumulate the height
372
+
accumulated_height += surf.size.height;
373
+
374
+
if (self.scroll.wants_cursor and i < self.cursor)
375
+
continue // continue if we want the cursor and haven't gotten there yet
376
+
else if (accumulated_height >= max_size.height)
377
+
break; // Break if we drew enough
378
+
} else {
379
+
// This branch runs if we ran out of items. Set our state accordingly
380
+
self.scroll.has_more = false;
381
+
}
382
+
383
+
var total_height: usize = totalHeight(&child_list);
384
+
385
+
// If we reached the bottom, don't have enough height to fill the screen, and have room to add
386
+
// more, then we add more until out of items or filled the space. This can happen on a resize
387
+
if (!self.scroll.has_more and total_height < max_size.height and self.scroll.top > 0) {
388
+
try self.insertChildren(ctx, builder, &child_list, @intCast(max_size.height - total_height));
389
+
// Set the new total height
390
+
total_height = totalHeight(&child_list);
391
+
}
392
+
393
+
if (self.draw_cursor and self.cursor >= self.scroll.top) blk: {
394
+
// The index of the cursored widget in our child_list
395
+
const cursored_idx: u32 = self.cursor - self.scroll.top;
396
+
// Nothing to draw if our cursor is below our viewport
397
+
if (cursored_idx >= child_list.items.len) break :blk;
398
+
399
+
const sub = try ctx.arena.alloc(vxfw.SubSurface, 1);
400
+
const child = child_list.items[cursored_idx];
401
+
sub[0] = .{
402
+
.origin = .{ .col = child_offset, .row = 0 },
403
+
.surface = child.surface,
404
+
.z_index = 0,
405
+
};
406
+
const size = child.surface.size;
407
+
const cursor_surf = try vxfw.Surface.initWithChildren(
408
+
ctx.arena,
409
+
self.widget(),
410
+
.{ .width = child_offset + size.width, .height = size.height },
411
+
sub,
412
+
);
413
+
for (0..cursor_surf.size.height) |row| {
414
+
cursor_surf.writeCell(0, @intCast(row), cursor_indicator);
415
+
}
416
+
child_list.items[cursored_idx] = .{
417
+
.origin = .{ .col = 0, .row = child.origin.row },
418
+
.surface = cursor_surf,
419
+
.z_index = 0,
420
+
};
421
+
}
422
+
423
+
// If we want the cursor, we check that the cursored widget is fully in view. If it is too
424
+
// large, we position it so that it is the top item with a 0 offset
425
+
if (self.scroll.wants_cursor) {
426
+
const cursored_idx: u32 = self.cursor - self.scroll.top;
427
+
const sub = child_list.items[cursored_idx];
428
+
// The bottom row of the cursored widget
429
+
const bottom = sub.origin.row + sub.surface.size.height;
430
+
if (bottom > max_size.height) {
431
+
// Adjust the origin by the difference
432
+
// anchor bottom
433
+
var origin: i17 = max_size.height;
434
+
var idx: usize = cursored_idx + 1;
435
+
while (idx > 0) : (idx -= 1) {
436
+
var child = child_list.items[idx - 1];
437
+
origin -= child.surface.size.height;
438
+
child.origin.row = origin;
439
+
child_list.items[idx - 1] = child;
440
+
}
441
+
} else if (sub.surface.size.height >= max_size.height) {
442
+
// TODO: handle when the child is larger than our height.
443
+
// We need to change the max constraint to be optional sizes so that we can support
444
+
// unbounded drawing in scrollable areas
445
+
self.scroll.top = self.cursor;
446
+
self.scroll.offset = 0;
447
+
child_list.deinit(ctx.arena);
448
+
try child_list.append(ctx.arena, .{
449
+
.origin = .{ .col = 0, .row = 0 },
450
+
.surface = sub.surface,
451
+
.z_index = 0,
452
+
});
453
+
total_height = sub.surface.size.height;
454
+
}
455
+
}
456
+
457
+
// If we reached the bottom, we need to reset origins
458
+
if (!self.scroll.has_more and total_height < max_size.height) {
459
+
// anchor top
460
+
assert(self.scroll.top == 0);
461
+
self.scroll.offset = 0;
462
+
var origin: i17 = 0;
463
+
for (0..child_list.items.len) |idx| {
464
+
var child = child_list.items[idx];
465
+
child.origin.row = origin;
466
+
origin += child.surface.size.height;
467
+
child_list.items[idx] = child;
468
+
}
469
+
} else if (!self.scroll.has_more) {
470
+
// anchor bottom
471
+
var origin: i17 = max_size.height;
472
+
var idx: usize = child_list.items.len;
473
+
while (idx > 0) : (idx -= 1) {
474
+
var child = child_list.items[idx - 1];
475
+
origin -= child.surface.size.height;
476
+
child.origin.row = origin;
477
+
child_list.items[idx - 1] = child;
478
+
}
479
+
}
480
+
481
+
var start: usize = 0;
482
+
var end: usize = child_list.items.len;
483
+
484
+
for (child_list.items, 0..) |child, idx| {
485
+
if (child.origin.row <= 0 and child.origin.row + child.surface.size.height > 0) {
486
+
start = idx;
487
+
self.scroll.offset = -child.origin.row;
488
+
self.scroll.top += @intCast(idx);
489
+
}
490
+
if (child.origin.row > max_size.height) {
491
+
end = idx;
492
+
break;
493
+
}
494
+
}
495
+
496
+
surface.children = child_list.items[start..end];
497
+
return surface;
498
+
}
499
+
500
+
const SliceBuilder = struct {
501
+
slice: []const vxfw.Widget,
502
+
503
+
fn build(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
504
+
const self: *const SliceBuilder = @ptrCast(@alignCast(ptr));
505
+
if (idx >= self.slice.len) return null;
506
+
return self.slice[idx];
507
+
}
508
+
};
509
+
510
+
test ListView {
511
+
// Create child widgets
512
+
const Text = @import("Text.zig");
513
+
const abc: Text = .{ .text = "abc\n def\n ghi" };
514
+
const def: Text = .{ .text = "def" };
515
+
const ghi: Text = .{ .text = "ghi" };
516
+
const jklmno: Text = .{ .text = "jkl\n mno" };
517
+
// 0 |*abc
518
+
// 1 | def
519
+
// 2 | ghi
520
+
// 3 | def
521
+
// 4 ghi
522
+
// 5 jkl
523
+
// 6 mno
524
+
525
+
// Create the list view
526
+
const list_view: ListView = .{
527
+
.wheel_scroll = 1, // Set wheel scroll to one
528
+
.children = .{ .slice = &.{
529
+
abc.widget(),
530
+
def.widget(),
531
+
ghi.widget(),
532
+
jklmno.widget(),
533
+
} },
534
+
};
535
+
536
+
// Boiler plate draw context
537
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
538
+
defer arena.deinit();
539
+
vxfw.DrawContext.init(.unicode);
540
+
541
+
const list_widget = list_view.widget();
542
+
const draw_ctx: vxfw.DrawContext = .{
543
+
.arena = arena.allocator(),
544
+
.min = .{},
545
+
.max = .{ .width = 16, .height = 4 },
546
+
.cell_size = .{ .width = 10, .height = 20 },
547
+
};
548
+
549
+
var surface = try list_widget.draw(draw_ctx);
550
+
// ListView expands to max height and max width
551
+
try std.testing.expectEqual(4, surface.size.height);
552
+
try std.testing.expectEqual(16, surface.size.width);
553
+
// We have 2 children, because only visible children appear as a surface
554
+
try std.testing.expectEqual(2, surface.children.len);
555
+
556
+
var mouse_event: vaxis.Mouse = .{
557
+
.col = 0,
558
+
.row = 0,
559
+
.button = .wheel_up,
560
+
.mods = .{},
561
+
.type = .press,
562
+
};
563
+
// Event handlers need a context
564
+
var ctx: vxfw.EventContext = .{
565
+
.alloc = std.testing.allocator,
566
+
.cmds = .empty,
567
+
};
568
+
defer ctx.cmds.deinit(ctx.alloc);
569
+
570
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
571
+
// Wheel up doesn't adjust the scroll
572
+
try std.testing.expectEqual(0, list_view.scroll.top);
573
+
try std.testing.expectEqual(0, list_view.scroll.offset);
574
+
575
+
// Send a wheel down
576
+
mouse_event.button = .wheel_down;
577
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
578
+
// We have to draw the widget for scrolls to take effect
579
+
surface = try list_widget.draw(draw_ctx);
580
+
// 0 *abc
581
+
// 1 | def
582
+
// 2 | ghi
583
+
// 3 | def
584
+
// 4 | ghi
585
+
// 5 jkl
586
+
// 6 mno
587
+
// We should have gone down 1 line, and not changed our top widget
588
+
try std.testing.expectEqual(0, list_view.scroll.top);
589
+
try std.testing.expectEqual(1, list_view.scroll.offset);
590
+
// One more widget has scrolled into view
591
+
try std.testing.expectEqual(3, surface.children.len);
592
+
593
+
// Scroll down two more lines
594
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
595
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
596
+
surface = try list_widget.draw(draw_ctx);
597
+
// 0 *abc
598
+
// 1 def
599
+
// 2 ghi
600
+
// 3 | def
601
+
// 4 | ghi
602
+
// 5 | jkl
603
+
// 6 | mno
604
+
// We should have gone down 2 lines, which scrolls our top widget out of view
605
+
try std.testing.expectEqual(1, list_view.scroll.top);
606
+
try std.testing.expectEqual(0, list_view.scroll.offset);
607
+
try std.testing.expectEqual(3, surface.children.len);
608
+
609
+
// Scroll down again. We shouldn't advance anymore since we are at the bottom
610
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
611
+
surface = try list_widget.draw(draw_ctx);
612
+
try std.testing.expectEqual(1, list_view.scroll.top);
613
+
try std.testing.expectEqual(0, list_view.scroll.offset);
614
+
try std.testing.expectEqual(3, surface.children.len);
615
+
616
+
// Mouse wheel events don't change the cursor position. Let's press "escape" to reset the
617
+
// viewport and bring our cursor into view
618
+
try list_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.escape } });
619
+
surface = try list_widget.draw(draw_ctx);
620
+
try std.testing.expectEqual(0, list_view.scroll.top);
621
+
try std.testing.expectEqual(0, list_view.scroll.offset);
622
+
try std.testing.expectEqual(2, surface.children.len);
623
+
624
+
// Cursor down
625
+
try list_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'j' } });
626
+
surface = try list_widget.draw(draw_ctx);
627
+
// 0 | abc
628
+
// 1 | def
629
+
// 2 | ghi
630
+
// 3 |*def
631
+
// 4 ghi
632
+
// 5 jkl
633
+
// 6 mno
634
+
// Scroll doesn't change
635
+
try std.testing.expectEqual(0, list_view.scroll.top);
636
+
try std.testing.expectEqual(0, list_view.scroll.offset);
637
+
try std.testing.expectEqual(2, surface.children.len);
638
+
try std.testing.expectEqual(1, list_view.cursor);
639
+
640
+
// Cursor down
641
+
try list_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'j' } });
642
+
surface = try list_widget.draw(draw_ctx);
643
+
// 0 abc
644
+
// 1 | def
645
+
// 2 | ghi
646
+
// 3 | def
647
+
// 4 |*ghi
648
+
// 5 jkl
649
+
// 6 mno
650
+
// Scroll advances one row
651
+
try std.testing.expectEqual(0, list_view.scroll.top);
652
+
try std.testing.expectEqual(1, list_view.scroll.offset);
653
+
try std.testing.expectEqual(3, surface.children.len);
654
+
try std.testing.expectEqual(2, list_view.cursor);
655
+
656
+
// Cursor down
657
+
try list_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'j' } });
658
+
surface = try list_widget.draw(draw_ctx);
659
+
// 0 abc
660
+
// 1 def
661
+
// 2 ghi
662
+
// 3 | def
663
+
// 4 | ghi
664
+
// 5 |*jkl
665
+
// 6 | mno
666
+
// We are cursored onto the last item. The entire last item comes into view, effectively
667
+
// advancing the scroll by 2
668
+
try std.testing.expectEqual(1, list_view.scroll.top);
669
+
try std.testing.expectEqual(0, list_view.scroll.offset);
670
+
try std.testing.expectEqual(3, surface.children.len);
671
+
try std.testing.expectEqual(3, list_view.cursor);
672
+
}
673
+
674
+
// @reykjalin found an issue on mac with ghostty where the scroll up and scroll down were uneven.
675
+
// Ghostty has high precision scrolling and sends a lot of wheel events for each tick
676
+
test "ListView: uneven scroll" {
677
+
// Create child widgets
678
+
const Text = @import("Text.zig");
679
+
const zero: Text = .{ .text = "0" };
680
+
const one: Text = .{ .text = "1" };
681
+
const two: Text = .{ .text = "2" };
682
+
const three: Text = .{ .text = "3" };
683
+
const four: Text = .{ .text = "4" };
684
+
const five: Text = .{ .text = "5" };
685
+
const six: Text = .{ .text = "6" };
686
+
// 0 |
687
+
// 1 |
688
+
// 2 |
689
+
// 3 |
690
+
// 4
691
+
// 5
692
+
// 6
693
+
694
+
// Create the list view
695
+
const list_view: ListView = .{
696
+
.wheel_scroll = 1, // Set wheel scroll to one
697
+
.children = .{ .slice = &.{
698
+
zero.widget(),
699
+
one.widget(),
700
+
two.widget(),
701
+
three.widget(),
702
+
four.widget(),
703
+
five.widget(),
704
+
six.widget(),
705
+
} },
706
+
};
707
+
708
+
// Boiler plate draw context
709
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
710
+
defer arena.deinit();
711
+
vxfw.DrawContext.init(.unicode);
712
+
713
+
const list_widget = list_view.widget();
714
+
const draw_ctx: vxfw.DrawContext = .{
715
+
.arena = arena.allocator(),
716
+
.min = .{},
717
+
.max = .{ .width = 16, .height = 4 },
718
+
.cell_size = .{ .width = 10, .height = 20 },
719
+
};
720
+
721
+
var surface = try list_widget.draw(draw_ctx);
722
+
723
+
var mouse_event: vaxis.Mouse = .{
724
+
.col = 0,
725
+
.row = 0,
726
+
.button = .wheel_up,
727
+
.mods = .{},
728
+
.type = .press,
729
+
};
730
+
// Event handlers need a context
731
+
var ctx: vxfw.EventContext = .{
732
+
.alloc = std.testing.allocator,
733
+
.cmds = .empty,
734
+
};
735
+
defer ctx.cmds.deinit(ctx.alloc);
736
+
737
+
// Send a wheel down x 3
738
+
mouse_event.button = .wheel_down;
739
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
740
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
741
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
742
+
// We have to draw the widget for scrolls to take effect
743
+
surface = try list_widget.draw(draw_ctx);
744
+
// 0
745
+
// 1
746
+
// 2
747
+
// 3 |
748
+
// 4 |
749
+
// 5 |
750
+
// 6 |
751
+
try std.testing.expectEqual(3, list_view.scroll.top);
752
+
try std.testing.expectEqual(0, list_view.scroll.offset);
753
+
try std.testing.expectEqual(4, surface.children.len);
754
+
755
+
// Now wheel_up two times should move us two lines up
756
+
mouse_event.button = .wheel_up;
757
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
758
+
try list_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
759
+
surface = try list_widget.draw(draw_ctx);
760
+
try std.testing.expectEqual(1, list_view.scroll.top);
761
+
try std.testing.expectEqual(0, list_view.scroll.offset);
762
+
try std.testing.expectEqual(4, surface.children.len);
763
+
}
764
+
765
+
test "refAllDecls" {
766
+
std.testing.refAllDecls(@This());
767
+
}
+142
src/vxfw/Padding.zig
+142
src/vxfw/Padding.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const Padding = @This();
9
+
const PadValues = struct {
10
+
left: u16 = 0,
11
+
right: u16 = 0,
12
+
top: u16 = 0,
13
+
bottom: u16 = 0,
14
+
};
15
+
16
+
child: vxfw.Widget,
17
+
padding: PadValues = .{},
18
+
19
+
/// Vertical padding will be divided by 2 to approximate equal padding
20
+
pub fn all(padding: u16) PadValues {
21
+
return .{
22
+
.left = padding,
23
+
.right = padding,
24
+
.top = padding / 2,
25
+
.bottom = padding / 2,
26
+
};
27
+
}
28
+
29
+
pub fn horizontal(padding: u16) PadValues {
30
+
return .{
31
+
.left = padding,
32
+
.right = padding,
33
+
};
34
+
}
35
+
36
+
pub fn vertical(padding: u16) PadValues {
37
+
return .{
38
+
.top = padding,
39
+
.bottom = padding,
40
+
};
41
+
}
42
+
43
+
pub fn widget(self: *const Padding) vxfw.Widget {
44
+
return .{
45
+
.userdata = @constCast(self),
46
+
.drawFn = typeErasedDrawFn,
47
+
};
48
+
}
49
+
50
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
51
+
const self: *const Padding = @ptrCast(@alignCast(ptr));
52
+
return self.draw(ctx);
53
+
}
54
+
55
+
pub fn draw(self: *const Padding, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
56
+
const pad = self.padding;
57
+
if (pad.left > 0 or pad.right > 0)
58
+
std.debug.assert(ctx.max.width != null);
59
+
if (pad.top > 0 or pad.bottom > 0)
60
+
std.debug.assert(ctx.max.height != null);
61
+
const inner_min: vxfw.Size = .{
62
+
.width = ctx.min.width -| (pad.right + pad.left),
63
+
.height = ctx.min.height -| (pad.top + pad.bottom),
64
+
};
65
+
66
+
const max_width: ?u16 = if (ctx.max.width) |max|
67
+
max -| (pad.right + pad.left)
68
+
else
69
+
null;
70
+
const max_height: ?u16 = if (ctx.max.height) |max|
71
+
max -| (pad.top + pad.bottom)
72
+
else
73
+
null;
74
+
75
+
const inner_max: vxfw.MaxSize = .{
76
+
.width = max_width,
77
+
.height = max_height,
78
+
};
79
+
80
+
const child_surface = try self.child.draw(ctx.withConstraints(inner_min, inner_max));
81
+
82
+
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
83
+
children[0] = .{
84
+
.surface = child_surface,
85
+
.z_index = 0,
86
+
.origin = .{ .row = pad.top, .col = pad.left },
87
+
};
88
+
89
+
const size: vxfw.Size = .{
90
+
.width = child_surface.size.width + (pad.right + pad.left),
91
+
.height = child_surface.size.height + (pad.top + pad.bottom),
92
+
};
93
+
94
+
// Create the padding surface
95
+
return .{
96
+
.size = size,
97
+
.widget = self.widget(),
98
+
.buffer = &.{},
99
+
.children = children,
100
+
};
101
+
}
102
+
103
+
test Padding {
104
+
const Text = @import("Text.zig");
105
+
// Will be height=1, width=3
106
+
const text: Text = .{ .text = "abc" };
107
+
108
+
const padding: Padding = .{
109
+
.child = text.widget(),
110
+
.padding = horizontal(1),
111
+
};
112
+
113
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
114
+
defer arena.deinit();
115
+
vxfw.DrawContext.init(.unicode);
116
+
117
+
// Center expands to the max size. It must therefore have non-null max width and max height.
118
+
// These values are asserted in draw
119
+
const ctx: vxfw.DrawContext = .{
120
+
.arena = arena.allocator(),
121
+
.min = .{},
122
+
.max = .{ .width = 10, .height = 10 },
123
+
.cell_size = .{ .width = 10, .height = 20 },
124
+
};
125
+
126
+
const pad_widget = padding.widget();
127
+
128
+
const surface = try pad_widget.draw(ctx);
129
+
// Padding does not produce any drawable cells
130
+
try std.testing.expectEqual(0, surface.buffer.len);
131
+
// Padding has 1 child
132
+
try std.testing.expectEqual(1, surface.children.len);
133
+
const child = surface.children[0];
134
+
// Padding is the child size + padding
135
+
try std.testing.expectEqual(child.surface.size.width + 2, surface.size.width);
136
+
try std.testing.expectEqual(0, child.origin.row);
137
+
try std.testing.expectEqual(1, child.origin.col);
138
+
}
139
+
140
+
test "refAllDecls" {
141
+
std.testing.refAllDecls(@This());
142
+
}
+425
src/vxfw/RichText.zig
+425
src/vxfw/RichText.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const vxfw = @import("vxfw.zig");
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const RichText = @This();
9
+
10
+
pub const TextSpan = vaxis.Segment;
11
+
12
+
text: []const TextSpan,
13
+
text_align: enum { left, center, right } = .left,
14
+
base_style: vaxis.Style = .{},
15
+
softwrap: bool = true,
16
+
overflow: enum { ellipsis, clip } = .ellipsis,
17
+
width_basis: enum { parent, longest_line } = .longest_line,
18
+
19
+
pub fn widget(self: *const RichText) vxfw.Widget {
20
+
return .{
21
+
.userdata = @constCast(self),
22
+
.drawFn = typeErasedDrawFn,
23
+
};
24
+
}
25
+
26
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
27
+
const self: *const RichText = @ptrCast(@alignCast(ptr));
28
+
return self.draw(ctx);
29
+
}
30
+
31
+
pub fn draw(self: *const RichText, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
32
+
if (ctx.max.width != null and ctx.max.width.? == 0) {
33
+
return .{
34
+
.size = ctx.min,
35
+
.widget = self.widget(),
36
+
.buffer = &.{},
37
+
.children = &.{},
38
+
};
39
+
}
40
+
var iter = try SoftwrapIterator.init(self.text, ctx);
41
+
const container_size = self.findContainerSize(&iter);
42
+
43
+
// Create a surface of target width and max height. We'll trim the result after drawing
44
+
const surface = try vxfw.Surface.init(
45
+
ctx.arena,
46
+
self.widget(),
47
+
container_size,
48
+
);
49
+
const base: vaxis.Cell = .{ .style = self.base_style };
50
+
@memset(surface.buffer, base);
51
+
52
+
var row: u16 = 0;
53
+
if (self.softwrap) {
54
+
while (iter.next()) |line| {
55
+
if (ctx.max.outsideHeight(row)) break;
56
+
defer row += 1;
57
+
var col: u16 = switch (self.text_align) {
58
+
.left => 0,
59
+
.center => (container_size.width - line.width) / 2,
60
+
.right => container_size.width - line.width,
61
+
};
62
+
for (line.cells) |cell| {
63
+
surface.writeCell(col, row, cell);
64
+
col += cell.char.width;
65
+
}
66
+
}
67
+
} else {
68
+
while (iter.nextHardBreak()) |line| {
69
+
if (ctx.max.outsideHeight(row)) break;
70
+
const line_width = blk: {
71
+
var w: u16 = 0;
72
+
for (line) |cell| {
73
+
w +|= cell.char.width;
74
+
}
75
+
break :blk w;
76
+
};
77
+
defer row += 1;
78
+
var col: u16 = switch (self.text_align) {
79
+
.left => 0,
80
+
.center => (container_size.width -| line_width) / 2,
81
+
.right => container_size.width -| line_width,
82
+
};
83
+
for (line) |cell| {
84
+
if (col + cell.char.width >= container_size.width and
85
+
line_width > container_size.width and
86
+
self.overflow == .ellipsis)
87
+
{
88
+
surface.writeCell(col, row, .{
89
+
.char = .{ .grapheme = "โฆ", .width = 1 },
90
+
.style = cell.style,
91
+
});
92
+
col = container_size.width;
93
+
continue;
94
+
} else {
95
+
surface.writeCell(col, row, cell);
96
+
col += @intCast(cell.char.width);
97
+
}
98
+
}
99
+
}
100
+
}
101
+
return surface.trimHeight(@max(row, ctx.min.height));
102
+
}
103
+
104
+
/// Finds the widest line within the viewable portion of ctx
105
+
fn findContainerSize(self: RichText, iter: *SoftwrapIterator) vxfw.Size {
106
+
defer iter.reset();
107
+
var row: u16 = 0;
108
+
var max_width: u16 = iter.ctx.min.width;
109
+
if (self.softwrap) {
110
+
while (iter.next()) |line| {
111
+
if (iter.ctx.max.outsideHeight(row)) break;
112
+
defer row += 1;
113
+
max_width = @max(max_width, line.width);
114
+
}
115
+
} else {
116
+
while (iter.nextHardBreak()) |line| {
117
+
if (iter.ctx.max.outsideHeight(row)) break;
118
+
defer row += 1;
119
+
var w: u16 = 0;
120
+
for (line) |cell| {
121
+
w +|= cell.char.width;
122
+
}
123
+
max_width = @max(max_width, w);
124
+
}
125
+
}
126
+
const result_width = switch (self.width_basis) {
127
+
.longest_line => blk: {
128
+
if (iter.ctx.max.width) |max|
129
+
break :blk @min(max, max_width)
130
+
else
131
+
break :blk max_width;
132
+
},
133
+
.parent => blk: {
134
+
std.debug.assert(iter.ctx.max.width != null);
135
+
break :blk iter.ctx.max.width.?;
136
+
},
137
+
};
138
+
return .{ .width = result_width, .height = @max(row, iter.ctx.min.height) };
139
+
}
140
+
141
+
pub const SoftwrapIterator = struct {
142
+
arena: std.heap.ArenaAllocator,
143
+
ctx: vxfw.DrawContext,
144
+
text: []const vaxis.Cell,
145
+
line: []const vaxis.Cell,
146
+
index: usize = 0,
147
+
// Index of the hard iterator
148
+
hard_index: usize = 0,
149
+
150
+
const soft_breaks = " \t";
151
+
152
+
pub const Line = struct {
153
+
width: u16,
154
+
cells: []const vaxis.Cell,
155
+
};
156
+
157
+
fn init(spans: []const TextSpan, ctx: vxfw.DrawContext) Allocator.Error!SoftwrapIterator {
158
+
// Estimate the number of cells we need
159
+
var len: usize = 0;
160
+
for (spans) |span| {
161
+
len += span.text.len;
162
+
}
163
+
var arena = std.heap.ArenaAllocator.init(ctx.arena);
164
+
const alloc = arena.allocator();
165
+
var list: std.ArrayList(vaxis.Cell) = try .initCapacity(alloc, len);
166
+
167
+
for (spans) |span| {
168
+
var iter = ctx.graphemeIterator(span.text);
169
+
while (iter.next()) |grapheme| {
170
+
const char = grapheme.bytes(span.text);
171
+
if (std.mem.eql(u8, char, "\t")) {
172
+
const cell: vaxis.Cell = .{
173
+
.char = .{ .grapheme = " ", .width = 1 },
174
+
.style = span.style,
175
+
.link = span.link,
176
+
};
177
+
for (0..8) |_| {
178
+
try list.append(alloc, cell);
179
+
}
180
+
continue;
181
+
}
182
+
const width = ctx.stringWidth(char);
183
+
const cell: vaxis.Cell = .{
184
+
.char = .{ .grapheme = char, .width = @intCast(width) },
185
+
.style = span.style,
186
+
.link = span.link,
187
+
};
188
+
try list.append(alloc, cell);
189
+
}
190
+
}
191
+
return .{
192
+
.arena = arena,
193
+
.ctx = ctx,
194
+
.text = list.items,
195
+
.line = &.{},
196
+
};
197
+
}
198
+
199
+
fn reset(self: *SoftwrapIterator) void {
200
+
self.index = 0;
201
+
self.hard_index = 0;
202
+
self.line = &.{};
203
+
}
204
+
205
+
fn deinit(self: *SoftwrapIterator) void {
206
+
self.arena.deinit();
207
+
}
208
+
209
+
fn nextHardBreak(self: *SoftwrapIterator) ?[]const vaxis.Cell {
210
+
if (self.hard_index >= self.text.len) return null;
211
+
const start = self.hard_index;
212
+
var saw_cr: bool = false;
213
+
while (self.hard_index < self.text.len) : (self.hard_index += 1) {
214
+
const cell = self.text[self.hard_index];
215
+
if (std.mem.eql(u8, cell.char.grapheme, "\r")) {
216
+
saw_cr = true;
217
+
}
218
+
if (std.mem.eql(u8, cell.char.grapheme, "\n")) {
219
+
self.hard_index += 1;
220
+
if (saw_cr) {
221
+
return self.text[start .. self.hard_index - 2];
222
+
}
223
+
return self.text[start .. self.hard_index - 1];
224
+
}
225
+
if (saw_cr) {
226
+
// back up one
227
+
self.hard_index -= 1;
228
+
return self.text[start .. self.hard_index - 1];
229
+
}
230
+
} else return self.text[start..];
231
+
}
232
+
233
+
fn trimWSPRight(text: []const vaxis.Cell) []const vaxis.Cell {
234
+
// trim linear whitespace
235
+
var i: usize = text.len;
236
+
while (i > 0) : (i -= 1) {
237
+
if (std.mem.eql(u8, text[i - 1].char.grapheme, " ") or
238
+
std.mem.eql(u8, text[i - 1].char.grapheme, "\t"))
239
+
{
240
+
continue;
241
+
}
242
+
break;
243
+
}
244
+
return text[0..i];
245
+
}
246
+
247
+
fn trimWSPLeft(text: []const vaxis.Cell) []const vaxis.Cell {
248
+
// trim linear whitespace
249
+
var i: usize = 0;
250
+
while (i < text.len) : (i += 1) {
251
+
if (std.mem.eql(u8, text[i].char.grapheme, " ") or
252
+
std.mem.eql(u8, text[i].char.grapheme, "\t"))
253
+
{
254
+
continue;
255
+
}
256
+
break;
257
+
}
258
+
return text[i..];
259
+
}
260
+
261
+
fn next(self: *SoftwrapIterator) ?Line {
262
+
// Advance the hard iterator
263
+
if (self.index == self.line.len) {
264
+
self.line = self.nextHardBreak() orelse return null;
265
+
// trim linear whitespace
266
+
self.line = trimWSPRight(self.line);
267
+
self.index = 0;
268
+
}
269
+
270
+
const max_width = self.ctx.max.width orelse {
271
+
var width: u16 = 0;
272
+
for (self.line) |cell| {
273
+
width += cell.char.width;
274
+
}
275
+
self.index = self.line.len;
276
+
return .{
277
+
.width = width,
278
+
.cells = self.line,
279
+
};
280
+
};
281
+
282
+
const start = self.index;
283
+
var cur_width: u16 = 0;
284
+
while (self.index < self.line.len) {
285
+
// Find the width from current position to next word break
286
+
const idx = self.nextWrap();
287
+
const word = self.line[self.index..idx];
288
+
const next_width = blk: {
289
+
var w: usize = 0;
290
+
for (word) |ch| {
291
+
w += ch.char.width;
292
+
}
293
+
break :blk w;
294
+
};
295
+
296
+
if (cur_width + next_width > max_width) {
297
+
// Trim the word to see if it can fit on a line by itself
298
+
const trimmed = trimWSPLeft(word);
299
+
// New width is the previous width minus the number of cells we trimmed because we
300
+
// are only trimming cells that would have been 1 wide (' ' and '\t' both measure as
301
+
// 1 wide)
302
+
const trimmed_width = next_width -| (word.len - trimmed.len);
303
+
if (trimmed_width > max_width) {
304
+
// Won't fit on line by itself, so fit as much on this line as we can
305
+
for (word) |cell| {
306
+
if (cur_width + cell.char.width > max_width) {
307
+
const end = self.index;
308
+
return .{ .width = cur_width, .cells = self.line[start..end] };
309
+
}
310
+
cur_width += @intCast(cell.char.width);
311
+
self.index += 1;
312
+
}
313
+
}
314
+
const end = self.index;
315
+
// We are softwrapping, advance index to the start of the next word. This is equal
316
+
// to the difference in our word length and trimmed word length
317
+
self.index += (word.len - trimmed.len);
318
+
return .{ .width = cur_width, .cells = self.line[start..end] };
319
+
}
320
+
321
+
self.index = idx;
322
+
cur_width += @intCast(next_width);
323
+
}
324
+
return .{ .width = cur_width, .cells = self.line[start..] };
325
+
}
326
+
327
+
fn nextWrap(self: *SoftwrapIterator) usize {
328
+
var i: usize = self.index;
329
+
330
+
// Find the first non-whitespace character
331
+
while (i < self.line.len) : (i += 1) {
332
+
if (std.mem.eql(u8, self.line[i].char.grapheme, " ") or
333
+
std.mem.eql(u8, self.line[i].char.grapheme, "\t"))
334
+
{
335
+
continue;
336
+
}
337
+
break;
338
+
}
339
+
340
+
// Now find the first whitespace
341
+
while (i < self.line.len) : (i += 1) {
342
+
if (std.mem.eql(u8, self.line[i].char.grapheme, " ") or
343
+
std.mem.eql(u8, self.line[i].char.grapheme, "\t"))
344
+
{
345
+
return i;
346
+
}
347
+
continue;
348
+
}
349
+
350
+
return self.line.len;
351
+
}
352
+
};
353
+
354
+
test RichText {
355
+
var rich_text: RichText = .{
356
+
.text = &.{
357
+
.{ .text = "Hello, " },
358
+
.{ .text = "World", .style = .{ .bold = true } },
359
+
},
360
+
};
361
+
362
+
const rich_widget = rich_text.widget();
363
+
364
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
365
+
defer arena.deinit();
366
+
367
+
vxfw.DrawContext.init(.unicode);
368
+
369
+
// Center expands to the max size. It must therefore have non-null max width and max height.
370
+
// These values are asserted in draw
371
+
const ctx: vxfw.DrawContext = .{
372
+
.arena = arena.allocator(),
373
+
.min = .{},
374
+
.max = .{ .width = 7, .height = 2 },
375
+
.cell_size = .{ .width = 10, .height = 20 },
376
+
};
377
+
378
+
{
379
+
// RichText softwraps by default
380
+
const surface = try rich_widget.draw(ctx);
381
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 6, .height = 2 }), surface.size);
382
+
}
383
+
384
+
{
385
+
rich_text.softwrap = false;
386
+
rich_text.overflow = .ellipsis;
387
+
const surface = try rich_widget.draw(ctx);
388
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 7, .height = 1 }), surface.size);
389
+
// The last character will be an ellipsis
390
+
try std.testing.expectEqualStrings("โฆ", surface.buffer[surface.buffer.len - 1].char.grapheme);
391
+
}
392
+
}
393
+
394
+
test "long word wrapping" {
395
+
var rich_text: RichText = .{
396
+
.text = &.{
397
+
.{ .text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" },
398
+
},
399
+
};
400
+
401
+
const rich_widget = rich_text.widget();
402
+
403
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
404
+
defer arena.deinit();
405
+
406
+
vxfw.DrawContext.init(.unicode);
407
+
408
+
const len = rich_text.text[0].text.len;
409
+
const width: u16 = 8;
410
+
411
+
const ctx: vxfw.DrawContext = .{
412
+
.arena = arena.allocator(),
413
+
.min = .{},
414
+
.max = .{ .width = width, .height = null },
415
+
.cell_size = .{ .width = 10, .height = 20 },
416
+
};
417
+
418
+
const surface = try rich_widget.draw(ctx);
419
+
// Height should be length / width
420
+
try std.testing.expectEqual(len / width, surface.size.height);
421
+
}
422
+
423
+
test "refAllDecls" {
424
+
std.testing.refAllDecls(@This());
425
+
}
+632
src/vxfw/ScrollBars.zig
+632
src/vxfw/ScrollBars.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
const vxfw = @import("vxfw.zig");
4
+
5
+
const Allocator = std.mem.Allocator;
6
+
7
+
const ScrollBars = @This();
8
+
9
+
/// The ScrollBars widget must contain a ScrollView widget. The scroll bars drawn will be for the
10
+
/// scroll view contained in the ScrollBars widget.
11
+
scroll_view: vxfw.ScrollView,
12
+
/// If `true` a horizontal scroll bar will be drawn. Set to `false` to hide the horizontal scroll
13
+
/// bar. Defaults to `true`.
14
+
draw_horizontal_scrollbar: bool = true,
15
+
/// If `true` a vertical scroll bar will be drawn. Set to `false` to hide the vertical scroll bar.
16
+
/// Defaults to `true`.
17
+
draw_vertical_scrollbar: bool = true,
18
+
/// The estimated height of all the content in the ScrollView. When provided this height will be
19
+
/// used to calculate the size of the scrollbar's thumb. If this is not provided the widget will
20
+
/// make a best effort estimate of the size of the thumb using the number of elements rendered at
21
+
/// any given time. This will cause inconsistent thumb sizes - and possibly inconsistent
22
+
/// positioning - if different elements in the ScrollView have different heights. For the best user
23
+
/// experience, providing this estimate is strongly recommended.
24
+
///
25
+
/// Note that this doesn't necessarily have to be an accurate estimate and the tolerance for larger
26
+
/// views is quite forgiving, especially if you overshoot the estimate.
27
+
estimated_content_height: ?u32 = null,
28
+
/// The estimated width of all the content in the ScrollView. When provided this width will be used
29
+
/// to calculate the size of the scrollbar's thumb. If this is not provided the widget will make a
30
+
/// best effort estimate of the size of the thumb using the width of the elements rendered at any
31
+
/// given time. This will cause inconsistent thumb sizes - and possibly inconsistent positioning -
32
+
/// if different elements in the ScrollView have different widths. For the best user experience,
33
+
/// providing this estimate is strongly recommended.
34
+
///
35
+
/// Note that this doesn't necessarily have to be
36
+
/// an accurate estimate and the tolerance for larger views is quite forgiving, especially if you
37
+
/// overshoot the estimate.
38
+
estimated_content_width: ?u32 = null,
39
+
/// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must
40
+
/// have a 1 column width.
41
+
vertical_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } },
42
+
/// The cell drawn for the vertical scroll thumb while it's being hovered. Replace this to customize
43
+
/// the scroll thumb. Must have a 1 column width.
44
+
vertical_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } },
45
+
/// The cell drawn for the vertical scroll thumb while it's being dragged by the mouse. Replace this
46
+
/// to customize the scroll thumb. Must have a 1 column width.
47
+
vertical_scrollbar_drag_thumb: vaxis.Cell = .{
48
+
.char = .{ .grapheme = "โ", .width = 1 },
49
+
.style = .{ .fg = .{ .index = 4 } },
50
+
},
51
+
/// The cell drawn for the vertical scroll thumb. Replace this to customize the scroll thumb. Must
52
+
/// have a 1 column width.
53
+
horizontal_scrollbar_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } },
54
+
/// The cell drawn for the horizontal scroll thumb while it's being hovered. Replace this to
55
+
/// customize the scroll thumb. Must have a 1 column width.
56
+
horizontal_scrollbar_hover_thumb: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } },
57
+
/// The cell drawn for the horizontal scroll thumb while it's being dragged by the mouse. Replace
58
+
/// this to customize the scroll thumb. Must have a 1 column width.
59
+
horizontal_scrollbar_drag_thumb: vaxis.Cell = .{
60
+
.char = .{ .grapheme = "โ", .width = 1 },
61
+
.style = .{ .fg = .{ .index = 4 } },
62
+
},
63
+
64
+
/// You should not change this variable, treat it as private to the implementation. Used to track
65
+
/// the size of the widget so we can locate scroll bars for mouse interaction.
66
+
last_frame_size: vxfw.Size = .{ .width = 0, .height = 0 },
67
+
/// You should not change this variable, treat it as private to the implementation. Used to track
68
+
/// the width of the content so we map horizontal scroll thumb position to view position.
69
+
last_frame_max_content_width: u32 = 0,
70
+
/// You should not change this variable, treat it as private to the implementation. Used to track
71
+
/// the position of the mouse relative to the scroll thumb for mouse interaction.
72
+
mouse_offset_into_thumb: u8 = 0,
73
+
74
+
/// You should not change this variable, treat it as private to the implementation. Used to track
75
+
/// the position of the scroll thumb for mouse interaction.
76
+
vertical_thumb_top_row: u32 = 0,
77
+
/// You should not change this variable, treat it as private to the implementation. Used to track
78
+
/// the position of the scroll thumb for mouse interaction.
79
+
vertical_thumb_bottom_row: u32 = 0,
80
+
/// You should not change this variable, treat it as private to the implementation. Used to track
81
+
/// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb.
82
+
is_hovering_vertical_thumb: bool = false,
83
+
/// You should not change this variable, treat it as private to the implementation. Used to track
84
+
/// whether the thumb is currently being dragged, which is important to allowing the mouse to leave
85
+
/// the scroll thumb while it's being dragged.
86
+
is_dragging_vertical_thumb: bool = false,
87
+
88
+
/// You should not change this variable, treat it as private to the implementation. Used to track
89
+
/// the position of the scroll thumb for mouse interaction.
90
+
horizontal_thumb_start_col: u32 = 0,
91
+
/// You should not change this variable, treat it as private to the implementation. Used to track
92
+
/// the position of the scroll thumb for mouse interaction.
93
+
horizontal_thumb_end_col: u32 = 0,
94
+
/// You should not change this variable, treat it as private to the implementation. Used to track
95
+
/// whether the scroll thumb is hovered or not so we can set the right hover style for the thumb.
96
+
is_hovering_horizontal_thumb: bool = false,
97
+
/// You should not change this variable, treat it as private to the implementation. Used to track
98
+
/// whether the thumb is currently being dragged, which is important to allowing the mouse to leave
99
+
/// the scroll thumb while it's being dragged.
100
+
is_dragging_horizontal_thumb: bool = false,
101
+
102
+
pub fn widget(self: *const ScrollBars) vxfw.Widget {
103
+
return .{
104
+
.userdata = @constCast(self),
105
+
.eventHandler = typeErasedEventHandler,
106
+
.captureHandler = typeErasedCaptureHandler,
107
+
.drawFn = typeErasedDrawFn,
108
+
};
109
+
}
110
+
111
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
112
+
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
113
+
return self.handleEvent(ctx, event);
114
+
}
115
+
fn typeErasedCaptureHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
116
+
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
117
+
return self.handleCapture(ctx, event);
118
+
}
119
+
120
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
121
+
const self: *ScrollBars = @ptrCast(@alignCast(ptr));
122
+
return self.draw(ctx);
123
+
}
124
+
125
+
pub fn handleCapture(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
126
+
switch (event) {
127
+
.mouse => |mouse| {
128
+
if (self.is_dragging_vertical_thumb) {
129
+
// Stop dragging the thumb when the mouse is released.
130
+
if (mouse.type == .release and
131
+
mouse.button == .left and
132
+
self.is_dragging_vertical_thumb)
133
+
{
134
+
// If we just let the scroll thumb go after dragging we need to make sure we
135
+
// redraw so the right style is immediately applied to the thumb.
136
+
if (self.is_dragging_vertical_thumb) {
137
+
self.is_dragging_vertical_thumb = false;
138
+
ctx.redraw = true;
139
+
}
140
+
141
+
const is_mouse_over_vertical_thumb =
142
+
mouse.col == self.last_frame_size.width -| 1 and
143
+
mouse.row >= self.vertical_thumb_top_row and
144
+
mouse.row < self.vertical_thumb_bottom_row;
145
+
146
+
// If we're not hovering the scroll bar after letting it go, we should trigger a
147
+
// redraw so it goes back to its narrow, non-active, state immediately.
148
+
if (!is_mouse_over_vertical_thumb) {
149
+
self.is_hovering_vertical_thumb = false;
150
+
ctx.redraw = true;
151
+
}
152
+
153
+
// No need to redraw yet, but we must consume the event so ending the drag
154
+
// action doesn't trigger some other event handler.
155
+
return ctx.consumeEvent();
156
+
}
157
+
158
+
// Process dragging the vertical thumb.
159
+
if (mouse.type == .drag) {
160
+
// Make sure we consume the event if we're currently dragging the mouse so other
161
+
// events aren't sent in the mean time.
162
+
ctx.consumeEvent();
163
+
164
+
// New scroll thumb position.
165
+
const new_thumb_top = mouse.row -| self.mouse_offset_into_thumb;
166
+
167
+
// If the new thumb position is at the top we know we've scrolled to the top of
168
+
// the scroll view.
169
+
if (new_thumb_top == 0) {
170
+
self.scroll_view.scroll.top = 0;
171
+
return ctx.consumeAndRedraw();
172
+
}
173
+
174
+
const new_thumb_top_f: f32 = @floatFromInt(new_thumb_top);
175
+
const widget_height_f: f32 = @floatFromInt(self.last_frame_size.height);
176
+
const total_num_children_f: f32 = count: {
177
+
if (self.scroll_view.item_count) |c| break :count @floatFromInt(c);
178
+
179
+
switch (self.scroll_view.children) {
180
+
.slice => |slice| break :count @floatFromInt(slice.len),
181
+
.builder => |builder| {
182
+
var counter: usize = 0;
183
+
while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_|
184
+
counter += 1;
185
+
186
+
break :count @floatFromInt(counter);
187
+
},
188
+
}
189
+
};
190
+
191
+
const new_top_child_idx_f =
192
+
new_thumb_top_f *
193
+
total_num_children_f / widget_height_f;
194
+
self.scroll_view.scroll.top = @intFromFloat(new_top_child_idx_f);
195
+
196
+
return ctx.consumeAndRedraw();
197
+
}
198
+
}
199
+
200
+
if (self.is_dragging_horizontal_thumb) {
201
+
// Stop dragging the thumb when the mouse is released.
202
+
if (mouse.type == .release and
203
+
mouse.button == .left and
204
+
self.is_dragging_horizontal_thumb)
205
+
{
206
+
// If we just let the scroll thumb go after dragging we need to make sure we
207
+
// redraw so the right style is immediately applied to the thumb.
208
+
if (self.is_dragging_horizontal_thumb) {
209
+
self.is_dragging_horizontal_thumb = false;
210
+
ctx.redraw = true;
211
+
}
212
+
213
+
const is_mouse_over_horizontal_thumb =
214
+
mouse.row == self.last_frame_size.height -| 1 and
215
+
mouse.col >= self.horizontal_thumb_start_col and
216
+
mouse.col < self.horizontal_thumb_end_col;
217
+
218
+
// If we're not hovering the scroll bar after letting it go, we should trigger a
219
+
// redraw so it goes back to its narrow, non-active, state immediately.
220
+
if (!is_mouse_over_horizontal_thumb) {
221
+
self.is_hovering_horizontal_thumb = false;
222
+
ctx.redraw = true;
223
+
}
224
+
225
+
// No need to redraw yet, but we must consume the event so ending the drag
226
+
// action doesn't trigger some other event handler.
227
+
return ctx.consumeEvent();
228
+
}
229
+
230
+
// Process dragging the horizontal thumb.
231
+
if (mouse.type == .drag) {
232
+
// Make sure we consume the event if we're currently dragging the mouse so other
233
+
// events aren't sent in the mean time.
234
+
ctx.consumeEvent();
235
+
236
+
// New scroll thumb position.
237
+
const new_thumb_col_start = mouse.col -| self.mouse_offset_into_thumb;
238
+
239
+
// If the new thumb position is at the horizontal beginning of the current view
240
+
// we know we've scrolled to the beginning of the scroll view.
241
+
if (new_thumb_col_start == 0) {
242
+
self.scroll_view.scroll.left = 0;
243
+
return ctx.consumeAndRedraw();
244
+
}
245
+
246
+
const new_thumb_col_start_f: f32 = @floatFromInt(new_thumb_col_start);
247
+
const widget_width_f: f32 = @floatFromInt(self.last_frame_size.width);
248
+
249
+
const max_content_width_f: f32 =
250
+
@floatFromInt(self.last_frame_max_content_width);
251
+
252
+
const new_view_col_start_f =
253
+
new_thumb_col_start_f * max_content_width_f / widget_width_f;
254
+
const new_view_col_start: u32 = @intFromFloat(@ceil(new_view_col_start_f));
255
+
256
+
self.scroll_view.scroll.left =
257
+
@min(new_view_col_start, self.last_frame_max_content_width);
258
+
259
+
return ctx.consumeAndRedraw();
260
+
}
261
+
}
262
+
},
263
+
else => {},
264
+
}
265
+
}
266
+
267
+
pub fn handleEvent(self: *ScrollBars, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
268
+
switch (event) {
269
+
.mouse => |mouse| {
270
+
// 1. Process vertical scroll thumb hover.
271
+
const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col);
272
+
const mouse_row: u16 = if (mouse.row < 0) 0 else @intCast(mouse.row);
273
+
const is_mouse_over_vertical_thumb =
274
+
mouse_col == self.last_frame_size.width -| 1 and
275
+
mouse_row >= self.vertical_thumb_top_row and
276
+
mouse_row < self.vertical_thumb_bottom_row;
277
+
278
+
// Make sure we only update the state and redraw when it's necessary.
279
+
if (!self.is_hovering_vertical_thumb and is_mouse_over_vertical_thumb) {
280
+
self.is_hovering_vertical_thumb = true;
281
+
ctx.redraw = true;
282
+
} else if (self.is_hovering_vertical_thumb and !is_mouse_over_vertical_thumb) {
283
+
self.is_hovering_vertical_thumb = false;
284
+
ctx.redraw = true;
285
+
}
286
+
287
+
const did_start_dragging_vertical_thumb = is_mouse_over_vertical_thumb and
288
+
mouse.type == .press and mouse.button == .left;
289
+
290
+
if (did_start_dragging_vertical_thumb) {
291
+
self.is_dragging_vertical_thumb = true;
292
+
self.mouse_offset_into_thumb = @intCast(mouse_row -| self.vertical_thumb_top_row);
293
+
294
+
// No need to redraw yet, but we must consume the event.
295
+
return ctx.consumeEvent();
296
+
}
297
+
298
+
// 2. Process horizontal scroll thumb hover.
299
+
300
+
const is_mouse_over_horizontal_thumb =
301
+
mouse_row == self.last_frame_size.height -| 1 and
302
+
mouse_col >= self.horizontal_thumb_start_col and
303
+
mouse_col < self.horizontal_thumb_end_col;
304
+
305
+
// Make sure we only update the state and redraw when it's necessary.
306
+
if (!self.is_hovering_horizontal_thumb and is_mouse_over_horizontal_thumb) {
307
+
self.is_hovering_horizontal_thumb = true;
308
+
ctx.redraw = true;
309
+
} else if (self.is_hovering_horizontal_thumb and !is_mouse_over_horizontal_thumb) {
310
+
self.is_hovering_horizontal_thumb = false;
311
+
ctx.redraw = true;
312
+
}
313
+
314
+
const did_start_dragging_horizontal_thumb = is_mouse_over_horizontal_thumb and
315
+
mouse.type == .press and mouse.button == .left;
316
+
317
+
if (did_start_dragging_horizontal_thumb) {
318
+
self.is_dragging_horizontal_thumb = true;
319
+
self.mouse_offset_into_thumb = @intCast(
320
+
mouse_col -| self.horizontal_thumb_start_col,
321
+
);
322
+
323
+
// No need to redraw yet, but we must consume the event.
324
+
return ctx.consumeEvent();
325
+
}
326
+
},
327
+
.mouse_leave => self.is_dragging_vertical_thumb = false,
328
+
else => {},
329
+
}
330
+
}
331
+
332
+
pub fn draw(self: *ScrollBars, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
333
+
var children: std.ArrayList(vxfw.SubSurface) = .empty;
334
+
335
+
// 1. If we're not drawing the scrollbars we can just draw the ScrollView directly.
336
+
337
+
if (!self.draw_vertical_scrollbar and !self.draw_horizontal_scrollbar) {
338
+
try children.append(ctx.arena, .{
339
+
.origin = .{ .row = 0, .col = 0 },
340
+
.surface = try self.scroll_view.draw(ctx),
341
+
});
342
+
343
+
return .{
344
+
.size = ctx.max.size(),
345
+
.widget = self.widget(),
346
+
.buffer = &.{},
347
+
.children = children.items,
348
+
};
349
+
}
350
+
351
+
// 2. Otherwise we can draw the scrollbars.
352
+
353
+
const max = ctx.max.size();
354
+
self.last_frame_size = max;
355
+
356
+
// 3. Draw the scroll view itself.
357
+
358
+
const scroll_view_surface = try self.scroll_view.draw(ctx.withConstraints(
359
+
ctx.min,
360
+
.{
361
+
// We make sure to make room for the scrollbars if required.
362
+
.width = max.width -| @intFromBool(self.draw_vertical_scrollbar),
363
+
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar),
364
+
},
365
+
));
366
+
367
+
try children.append(ctx.arena, .{
368
+
.origin = .{ .row = 0, .col = 0 },
369
+
.surface = scroll_view_surface,
370
+
});
371
+
372
+
// 4. Draw the vertical scroll bar.
373
+
374
+
if (self.draw_vertical_scrollbar) vertical: {
375
+
// If we can't scroll, then there's no need to draw the scroll bar.
376
+
if (self.scroll_view.scroll.top == 0 and !self.scroll_view.scroll.has_more_vertical)
377
+
break :vertical;
378
+
379
+
// To draw the vertical scrollbar we need to know how big the scroll bar thumb should be.
380
+
// If we've been provided with an estimated height we use that to figure out how big the
381
+
// thumb should be, otherwise we estimate the size based on how many of the children were
382
+
// actually drawn in the ScrollView.
383
+
384
+
const widget_height_f: f32 = @floatFromInt(scroll_view_surface.size.height);
385
+
const total_num_children_f: f32 = count: {
386
+
if (self.scroll_view.item_count) |c| break :count @floatFromInt(c);
387
+
388
+
switch (self.scroll_view.children) {
389
+
.slice => |slice| break :count @floatFromInt(slice.len),
390
+
.builder => |builder| {
391
+
var counter: usize = 0;
392
+
while (builder.itemAtIdx(counter, self.scroll_view.cursor)) |_|
393
+
counter += 1;
394
+
395
+
break :count @floatFromInt(counter);
396
+
},
397
+
}
398
+
};
399
+
400
+
const thumb_height: u16 = height: {
401
+
// If we know the height, we can use the height of the current view to determine the
402
+
// size of the thumb.
403
+
if (self.estimated_content_height) |h| {
404
+
const content_height_f: f32 = @floatFromInt(h);
405
+
406
+
const thumb_height_f = widget_height_f * widget_height_f / content_height_f;
407
+
break :height @intFromFloat(@max(thumb_height_f, 1));
408
+
}
409
+
410
+
// Otherwise we estimate the size of the thumb based on the number of child elements
411
+
// drawn in the scroll view, and the number of total child elements.
412
+
413
+
const num_children_rendered_f: f32 = @floatFromInt(scroll_view_surface.children.len);
414
+
415
+
const thumb_height_f = widget_height_f * num_children_rendered_f / total_num_children_f;
416
+
break :height @intFromFloat(@max(thumb_height_f, 1));
417
+
};
418
+
419
+
// We also need to know the position of the thumb in the scroll bar. To find that we use the
420
+
// index of the top-most child widget rendered in the ScrollView.
421
+
422
+
const thumb_top: u32 = if (self.scroll_view.scroll.top == 0)
423
+
0
424
+
else if (self.scroll_view.scroll.has_more_vertical) pos: {
425
+
const top_child_idx_f: f32 = @floatFromInt(self.scroll_view.scroll.top);
426
+
const thumb_top_f = widget_height_f * top_child_idx_f / total_num_children_f;
427
+
428
+
break :pos @intFromFloat(thumb_top_f);
429
+
} else max.height -| thumb_height;
430
+
431
+
// Once we know the thumb height and its position we can draw the scroll bar.
432
+
433
+
const scroll_bar = try vxfw.Surface.init(
434
+
ctx.arena,
435
+
self.widget(),
436
+
.{
437
+
.width = 1,
438
+
// We make sure to make room for the horizontal scroll bar if it's being drawn.
439
+
.height = max.height -| @intFromBool(self.draw_horizontal_scrollbar),
440
+
},
441
+
);
442
+
443
+
const thumb_end_row = thumb_top + thumb_height;
444
+
for (thumb_top..thumb_end_row) |row| {
445
+
scroll_bar.writeCell(
446
+
0,
447
+
@intCast(row),
448
+
if (self.is_dragging_vertical_thumb)
449
+
self.vertical_scrollbar_drag_thumb
450
+
else if (self.is_hovering_vertical_thumb)
451
+
self.vertical_scrollbar_hover_thumb
452
+
else
453
+
self.vertical_scrollbar_thumb,
454
+
);
455
+
}
456
+
457
+
self.vertical_thumb_top_row = thumb_top;
458
+
self.vertical_thumb_bottom_row = thumb_end_row;
459
+
460
+
try children.append(ctx.arena, .{
461
+
.origin = .{ .row = 0, .col = max.width -| 1 },
462
+
.surface = scroll_bar,
463
+
});
464
+
}
465
+
466
+
// 5. Draw the horizontal scroll bar.
467
+
468
+
const is_horizontally_scrolled = self.scroll_view.scroll.left > 0;
469
+
const has_more_horizontal_content = self.scroll_view.scroll.has_more_horizontal;
470
+
471
+
const should_draw_scrollbar = is_horizontally_scrolled or has_more_horizontal_content;
472
+
473
+
if (self.draw_horizontal_scrollbar and should_draw_scrollbar) {
474
+
const scroll_bar = try vxfw.Surface.init(
475
+
ctx.arena,
476
+
self.widget(),
477
+
.{ .width = max.width, .height = 1 },
478
+
);
479
+
480
+
const widget_width_f: f32 = @floatFromInt(max.width);
481
+
482
+
const max_content_width: u32 = width: {
483
+
if (self.estimated_content_width) |w| break :width w;
484
+
485
+
var max_content_width: u32 = 0;
486
+
for (scroll_view_surface.children) |child| {
487
+
max_content_width = @max(max_content_width, child.surface.size.width);
488
+
}
489
+
break :width max_content_width;
490
+
};
491
+
const max_content_width_f: f32 =
492
+
if (self.scroll_view.scroll.left + max.width > max_content_width)
493
+
// If we've managed to overscroll horizontally for whatever reason - for example if the
494
+
// content changes - we make sure the scroll thumb doesn't disappear by increasing the
495
+
// max content width to match the current overscrolled position.
496
+
@floatFromInt(self.scroll_view.scroll.left + max.width)
497
+
else
498
+
@floatFromInt(max_content_width);
499
+
500
+
self.last_frame_max_content_width = max_content_width;
501
+
502
+
const thumb_width_f: f32 = widget_width_f * widget_width_f / max_content_width_f;
503
+
const thumb_width: u32 = @intFromFloat(@max(thumb_width_f, 1));
504
+
505
+
const view_start_col_f: f32 = @floatFromInt(self.scroll_view.scroll.left);
506
+
const thumb_start_f = view_start_col_f * widget_width_f / max_content_width_f;
507
+
508
+
const thumb_start: u32 = @intFromFloat(thumb_start_f);
509
+
const thumb_end = thumb_start + thumb_width;
510
+
for (thumb_start..thumb_end) |col| {
511
+
scroll_bar.writeCell(
512
+
@intCast(col),
513
+
0,
514
+
if (self.is_dragging_horizontal_thumb)
515
+
self.horizontal_scrollbar_drag_thumb
516
+
else if (self.is_hovering_horizontal_thumb)
517
+
self.horizontal_scrollbar_hover_thumb
518
+
else
519
+
self.horizontal_scrollbar_thumb,
520
+
);
521
+
}
522
+
self.horizontal_thumb_start_col = thumb_start;
523
+
self.horizontal_thumb_end_col = thumb_end;
524
+
try children.append(ctx.arena, .{
525
+
.origin = .{ .row = max.height -| 1, .col = 0 },
526
+
.surface = scroll_bar,
527
+
});
528
+
}
529
+
530
+
return .{
531
+
.size = ctx.max.size(),
532
+
.widget = self.widget(),
533
+
.buffer = &.{},
534
+
.children = children.items,
535
+
};
536
+
}
537
+
538
+
test ScrollBars {
539
+
// Create child widgets
540
+
const Text = @import("Text.zig");
541
+
const abc: Text = .{ .text = "abc\n def\n ghi" };
542
+
const def: Text = .{ .text = "def" };
543
+
const ghi: Text = .{ .text = "ghi" };
544
+
const jklmno: Text = .{ .text = "jkl\n mno" };
545
+
//
546
+
// 0 |abc|
547
+
// 1 | d|ef
548
+
// 2 | g|hi
549
+
// 3 |def|
550
+
// 4 ghi
551
+
// 5 jkl
552
+
// 6 mno
553
+
554
+
// Create the scroll view
555
+
const ScrollView = @import("ScrollView.zig");
556
+
const scroll_view: ScrollView = .{
557
+
.wheel_scroll = 1, // Set wheel scroll to one
558
+
.children = .{ .slice = &.{
559
+
abc.widget(),
560
+
def.widget(),
561
+
ghi.widget(),
562
+
jklmno.widget(),
563
+
} },
564
+
};
565
+
566
+
// Create the scroll bars.
567
+
var scroll_bars: ScrollBars = .{
568
+
.scroll_view = scroll_view,
569
+
.estimated_content_height = 7,
570
+
.estimated_content_width = 5,
571
+
};
572
+
573
+
// Boiler plate draw context
574
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
575
+
defer arena.deinit();
576
+
vxfw.DrawContext.init(.unicode);
577
+
578
+
const scroll_widget = scroll_bars.widget();
579
+
const draw_ctx: vxfw.DrawContext = .{
580
+
.arena = arena.allocator(),
581
+
.min = .{},
582
+
.max = .{ .width = 3, .height = 4 },
583
+
.cell_size = .{ .width = 10, .height = 20 },
584
+
};
585
+
586
+
var surface = try scroll_widget.draw(draw_ctx);
587
+
// Scroll bars should have 3 children: both scrollbars and the scroll view.
588
+
try std.testing.expectEqual(3, surface.children.len);
589
+
590
+
// Hide only the horizontal scroll bar.
591
+
scroll_bars.draw_horizontal_scrollbar = false;
592
+
surface = try scroll_widget.draw(draw_ctx);
593
+
// Scroll bars should have 2 children: vertical scroll bar and the scroll view.
594
+
try std.testing.expectEqual(2, surface.children.len);
595
+
596
+
// Hide only the vertical scroll bar.
597
+
scroll_bars.draw_horizontal_scrollbar = true;
598
+
scroll_bars.draw_vertical_scrollbar = false;
599
+
surface = try scroll_widget.draw(draw_ctx);
600
+
// Scroll bars should have 2 children: vertical scroll bar and the scroll view.
601
+
try std.testing.expectEqual(2, surface.children.len);
602
+
603
+
// Hide both scroll bars.
604
+
scroll_bars.draw_horizontal_scrollbar = false;
605
+
surface = try scroll_widget.draw(draw_ctx);
606
+
// Scroll bars should have 1 child: the scroll view.
607
+
try std.testing.expectEqual(1, surface.children.len);
608
+
609
+
// Re-enable scroll bars.
610
+
scroll_bars.draw_horizontal_scrollbar = true;
611
+
scroll_bars.draw_vertical_scrollbar = true;
612
+
613
+
// Even though the estimated size is smaller than the draw area, we still render the scroll
614
+
// bars if the scroll view knows we haven't rendered everything.
615
+
scroll_bars.estimated_content_height = 2;
616
+
scroll_bars.estimated_content_width = 1;
617
+
surface = try scroll_widget.draw(draw_ctx);
618
+
// Scroll bars should have 3 children: both scrollbars and the scroll view.
619
+
try std.testing.expectEqual(3, surface.children.len);
620
+
621
+
// The scroll view should be able to tell whether the scroll bars need to be rendered or not
622
+
// even if estimated content sizes aren't provided.
623
+
scroll_bars.estimated_content_height = null;
624
+
scroll_bars.estimated_content_width = null;
625
+
surface = try scroll_widget.draw(draw_ctx);
626
+
// Scroll bars should have 3 children: both scrollbars and the scroll view.
627
+
try std.testing.expectEqual(3, surface.children.len);
628
+
}
629
+
630
+
test "refAllDecls" {
631
+
std.testing.refAllDecls(@This());
632
+
}
+1087
src/vxfw/ScrollView.zig
+1087
src/vxfw/ScrollView.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const assert = std.debug.assert;
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const vxfw = @import("vxfw.zig");
9
+
10
+
const ScrollView = @This();
11
+
12
+
pub const Builder = struct {
13
+
userdata: *const anyopaque,
14
+
buildFn: *const fn (*const anyopaque, idx: usize, cursor: usize) ?vxfw.Widget,
15
+
16
+
pub inline fn itemAtIdx(self: Builder, idx: usize, cursor: usize) ?vxfw.Widget {
17
+
return self.buildFn(self.userdata, idx, cursor);
18
+
}
19
+
};
20
+
21
+
pub const Source = union(enum) {
22
+
slice: []const vxfw.Widget,
23
+
builder: Builder,
24
+
};
25
+
26
+
const Scroll = struct {
27
+
/// Index of the first fully-in-view widget.
28
+
top: u32 = 0,
29
+
/// Line offset within the top widget.
30
+
vertical_offset: i17 = 0,
31
+
/// Pending vertical scroll amount.
32
+
pending_lines: i17 = 0,
33
+
/// If there is more room to scroll down.
34
+
has_more_vertical: bool = true,
35
+
/// The column of the first in-view column.
36
+
left: u32 = 0,
37
+
/// If there is more room to scroll right.
38
+
has_more_horizontal: bool = true,
39
+
/// The cursor must be in the viewport.
40
+
wants_cursor: bool = false,
41
+
42
+
pub fn linesDown(self: *Scroll, n: u8) bool {
43
+
if (!self.has_more_vertical) return false;
44
+
self.pending_lines += n;
45
+
return true;
46
+
}
47
+
48
+
pub fn linesUp(self: *Scroll, n: u8) bool {
49
+
if (self.top == 0 and self.vertical_offset == 0) return false;
50
+
self.pending_lines -= @intCast(n);
51
+
return true;
52
+
}
53
+
54
+
pub fn colsLeft(self: *Scroll, n: u8) bool {
55
+
if (self.left == 0) return false;
56
+
self.left -|= n;
57
+
return true;
58
+
}
59
+
pub fn colsRight(self: *Scroll, n: u8) bool {
60
+
if (!self.has_more_horizontal) return false;
61
+
self.left +|= n;
62
+
return true;
63
+
}
64
+
};
65
+
66
+
children: Source,
67
+
cursor: u32 = 0,
68
+
last_height: u8 = 0,
69
+
/// When true, the widget will draw a cursor next to the widget which has the cursor
70
+
draw_cursor: bool = false,
71
+
/// The cell that will be drawn to represent the scroll view's cursor. Replace this to customize the
72
+
/// cursor indicator. Must have a 1 column width.
73
+
cursor_indicator: vaxis.Cell = .{ .char = .{ .grapheme = "โ", .width = 1 } },
74
+
/// Lines to scroll for a mouse wheel
75
+
wheel_scroll: u8 = 3,
76
+
/// Set this if the exact item count is known.
77
+
item_count: ?u32 = null,
78
+
79
+
/// scroll position
80
+
scroll: Scroll = .{},
81
+
82
+
pub fn widget(self: *const ScrollView) vxfw.Widget {
83
+
return .{
84
+
.userdata = @constCast(self),
85
+
.eventHandler = typeErasedEventHandler,
86
+
.drawFn = typeErasedDrawFn,
87
+
};
88
+
}
89
+
90
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
91
+
const self: *ScrollView = @ptrCast(@alignCast(ptr));
92
+
return self.handleEvent(ctx, event);
93
+
}
94
+
95
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
96
+
const self: *ScrollView = @ptrCast(@alignCast(ptr));
97
+
return self.draw(ctx);
98
+
}
99
+
100
+
pub fn handleEvent(self: *ScrollView, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
101
+
switch (event) {
102
+
.mouse => |mouse| {
103
+
if (mouse.button == .wheel_up) {
104
+
if (self.scroll.linesUp(self.wheel_scroll))
105
+
return ctx.consumeAndRedraw();
106
+
}
107
+
if (mouse.button == .wheel_down) {
108
+
if (self.scroll.linesDown(self.wheel_scroll))
109
+
return ctx.consumeAndRedraw();
110
+
}
111
+
if (mouse.button == .wheel_left) {
112
+
if (self.scroll.colsRight(self.wheel_scroll))
113
+
return ctx.consumeAndRedraw();
114
+
}
115
+
if (mouse.button == .wheel_right) {
116
+
if (self.scroll.colsLeft(self.wheel_scroll))
117
+
return ctx.consumeAndRedraw();
118
+
}
119
+
},
120
+
.key_press => |key| {
121
+
if (key.matches(vaxis.Key.down, .{}) or
122
+
key.matches('j', .{}) or
123
+
key.matches('n', .{ .ctrl = true }))
124
+
{
125
+
// If we're drawing the cursor, move it to the next item.
126
+
if (self.draw_cursor) return self.nextItem(ctx);
127
+
128
+
// Otherwise scroll the view down.
129
+
if (self.scroll.linesDown(1)) ctx.consumeAndRedraw();
130
+
}
131
+
if (key.matches(vaxis.Key.up, .{}) or
132
+
key.matches('k', .{}) or
133
+
key.matches('p', .{ .ctrl = true }))
134
+
{
135
+
// If we're drawing the cursor, move it to the previous item.
136
+
if (self.draw_cursor) return self.prevItem(ctx);
137
+
138
+
// Otherwise scroll the view up.
139
+
if (self.scroll.linesUp(1)) ctx.consumeAndRedraw();
140
+
}
141
+
if (key.matches(vaxis.Key.right, .{}) or
142
+
key.matches('l', .{}) or
143
+
key.matches('f', .{ .ctrl = true }))
144
+
{
145
+
if (self.scroll.colsRight(1)) ctx.consumeAndRedraw();
146
+
}
147
+
if (key.matches(vaxis.Key.left, .{}) or
148
+
key.matches('h', .{}) or
149
+
key.matches('b', .{ .ctrl = true }))
150
+
{
151
+
if (self.scroll.colsLeft(1)) ctx.consumeAndRedraw();
152
+
}
153
+
if (key.matches('d', .{ .ctrl = true })) {
154
+
const scroll_lines = @max(self.last_height / 2, 1);
155
+
if (self.scroll.linesDown(scroll_lines))
156
+
ctx.consumeAndRedraw();
157
+
}
158
+
if (key.matches('u', .{ .ctrl = true })) {
159
+
const scroll_lines = @max(self.last_height / 2, 1);
160
+
if (self.scroll.linesUp(scroll_lines))
161
+
ctx.consumeAndRedraw();
162
+
}
163
+
if (key.matches(vaxis.Key.escape, .{})) {
164
+
self.ensureScroll();
165
+
return ctx.consumeAndRedraw();
166
+
}
167
+
},
168
+
else => {},
169
+
}
170
+
}
171
+
172
+
pub fn draw(self: *ScrollView, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
173
+
std.debug.assert(ctx.max.width != null);
174
+
std.debug.assert(ctx.max.height != null);
175
+
switch (self.children) {
176
+
.slice => |slice| {
177
+
self.item_count = @intCast(slice.len);
178
+
const builder: SliceBuilder = .{ .slice = slice };
179
+
return self.drawBuilder(ctx, .{ .userdata = &builder, .buildFn = SliceBuilder.build });
180
+
},
181
+
.builder => |b| return self.drawBuilder(ctx, b),
182
+
}
183
+
}
184
+
185
+
pub fn nextItem(self: *ScrollView, ctx: *vxfw.EventContext) void {
186
+
// If we have a count, we can handle this directly
187
+
if (self.item_count) |count| {
188
+
if (self.cursor >= count - 1) {
189
+
return ctx.consumeEvent();
190
+
}
191
+
self.cursor += 1;
192
+
} else {
193
+
switch (self.children) {
194
+
.slice => |slice| {
195
+
self.item_count = @intCast(slice.len);
196
+
// If we are already at the end, don't do anything
197
+
if (self.cursor == slice.len - 1) {
198
+
return ctx.consumeEvent();
199
+
}
200
+
// Advance the cursor
201
+
self.cursor += 1;
202
+
},
203
+
.builder => |builder| {
204
+
// Save our current state
205
+
const prev = self.cursor;
206
+
// Advance the cursor
207
+
self.cursor += 1;
208
+
// Check the bounds, reversing until we get the last item
209
+
while (builder.itemAtIdx(self.cursor, self.cursor) == null) {
210
+
self.cursor -|= 1;
211
+
}
212
+
// If we didn't change state, we don't redraw
213
+
if (self.cursor == prev) {
214
+
return ctx.consumeEvent();
215
+
}
216
+
},
217
+
}
218
+
}
219
+
// Reset scroll
220
+
self.ensureScroll();
221
+
ctx.consumeAndRedraw();
222
+
}
223
+
224
+
pub fn prevItem(self: *ScrollView, ctx: *vxfw.EventContext) void {
225
+
if (self.cursor == 0) {
226
+
return ctx.consumeEvent();
227
+
}
228
+
229
+
if (self.item_count) |count| {
230
+
// If for some reason our count changed, we handle it here
231
+
self.cursor = @min(self.cursor - 1, count - 1);
232
+
} else {
233
+
switch (self.children) {
234
+
.slice => |slice| {
235
+
self.item_count = @intCast(slice.len);
236
+
self.cursor = @min(self.cursor - 1, slice.len - 1);
237
+
},
238
+
.builder => |builder| {
239
+
// Save our current state
240
+
const prev = self.cursor;
241
+
// Decrement the cursor
242
+
self.cursor -= 1;
243
+
// Check the bounds, reversing until we get the last item
244
+
while (builder.itemAtIdx(self.cursor, self.cursor) == null) {
245
+
self.cursor -|= 1;
246
+
}
247
+
// If we didn't change state, we don't redraw
248
+
if (self.cursor == prev) {
249
+
return ctx.consumeEvent();
250
+
}
251
+
},
252
+
}
253
+
}
254
+
255
+
// Reset scroll
256
+
self.ensureScroll();
257
+
return ctx.consumeAndRedraw();
258
+
}
259
+
260
+
// Only call when cursor state has changed, or we want to ensure the cursored item is in view
261
+
pub fn ensureScroll(self: *ScrollView) void {
262
+
if (self.cursor <= self.scroll.top) {
263
+
self.scroll.top = @intCast(self.cursor);
264
+
self.scroll.vertical_offset = 0;
265
+
} else {
266
+
self.scroll.wants_cursor = true;
267
+
}
268
+
}
269
+
270
+
/// Inserts children until add_height is < 0
271
+
fn insertChildren(
272
+
self: *ScrollView,
273
+
ctx: vxfw.DrawContext,
274
+
builder: Builder,
275
+
child_list: *std.ArrayList(vxfw.SubSurface),
276
+
add_height: i17,
277
+
) Allocator.Error!void {
278
+
assert(self.scroll.top > 0);
279
+
self.scroll.top -= 1;
280
+
var upheight = add_height;
281
+
while (self.scroll.top >= 0) : (self.scroll.top -= 1) {
282
+
// Get the child
283
+
const child = builder.itemAtIdx(self.scroll.top, self.cursor) orelse break;
284
+
285
+
const child_offset: u16 = if (self.draw_cursor) 2 else 0;
286
+
const max_size = ctx.max.size();
287
+
288
+
// Set up constraints. We let the child be the entire height if it wants
289
+
const child_ctx = ctx.withConstraints(
290
+
.{ .width = max_size.width - child_offset, .height = 0 },
291
+
.{ .width = null, .height = null },
292
+
);
293
+
294
+
// Draw the child
295
+
const surf = try child.draw(child_ctx);
296
+
297
+
// Accumulate the height. Traversing backward so do this before setting origin
298
+
upheight -= surf.size.height;
299
+
300
+
// Insert the child to the beginning of the list
301
+
const col_offset: i17 = if (self.draw_cursor) 2 else 0;
302
+
try child_list.insert(ctx.arena, 0, .{
303
+
.origin = .{ .col = col_offset - @as(i17, @intCast(self.scroll.left)), .row = upheight },
304
+
.surface = surf,
305
+
.z_index = 0,
306
+
});
307
+
308
+
// Break if we went past the top edge, or are the top item
309
+
if (upheight <= 0 or self.scroll.top == 0) break;
310
+
}
311
+
312
+
// Our new offset is the "upheight"
313
+
self.scroll.vertical_offset = upheight;
314
+
315
+
// Reset origins if we overshot and put the top item too low
316
+
if (self.scroll.top == 0 and upheight > 0) {
317
+
self.scroll.vertical_offset = 0;
318
+
var row: i17 = 0;
319
+
for (child_list.items) |*child| {
320
+
child.origin.row = row;
321
+
row += child.surface.size.height;
322
+
}
323
+
}
324
+
// Our new offset is the "upheight"
325
+
self.scroll.vertical_offset = upheight;
326
+
}
327
+
328
+
fn totalHeight(list: *const std.ArrayList(vxfw.SubSurface)) usize {
329
+
var result: usize = 0;
330
+
for (list.items) |child| {
331
+
result += child.surface.size.height;
332
+
}
333
+
return result;
334
+
}
335
+
336
+
fn drawBuilder(self: *ScrollView, ctx: vxfw.DrawContext, builder: Builder) Allocator.Error!vxfw.Surface {
337
+
defer self.scroll.wants_cursor = false;
338
+
339
+
// Get the size. asserts neither constraint is null
340
+
const max_size = ctx.max.size();
341
+
// Set up surface.
342
+
var surface: vxfw.Surface = .{
343
+
.size = max_size,
344
+
.widget = self.widget(),
345
+
.buffer = &.{},
346
+
.children = &.{},
347
+
};
348
+
349
+
// Set state
350
+
{
351
+
// Assume we have more. We only know we don't after drawing
352
+
self.scroll.has_more_vertical = true;
353
+
}
354
+
355
+
var child_list: std.ArrayList(vxfw.SubSurface) = .empty;
356
+
357
+
// Accumulated height tracks how much height we have drawn. It's initial state is
358
+
// -(scroll.vertical_offset + scroll.pending_lines) lines _above_ the surface top edge.
359
+
// Example:
360
+
// 1. Scroll up 3 lines:
361
+
// pending_lines = -3
362
+
// offset = 0
363
+
// accumulated_height = -(0 + -3) = 3;
364
+
// Our first widget is placed at row 3, we will need to fill this in after the draw
365
+
// 2. Scroll up 3 lines, with an offset of 4
366
+
// pending_lines = -3
367
+
// offset = 4
368
+
// accumulated_height = -(4 + -3) = -1;
369
+
// Our first widget is placed at row -1
370
+
// 3. Scroll down 3 lines:
371
+
// pending_lines = 3
372
+
// offset = 0
373
+
// accumulated_height = -(0 + 3) = -3;
374
+
// Our first widget is placed at row -3. It's possible it consumes the entire widget. We
375
+
// will check for this at the end and only include visible children
376
+
var accumulated_height: i17 = -(self.scroll.vertical_offset + self.scroll.pending_lines);
377
+
378
+
// We handled the pending scroll by assigning accumulated_height. Reset it's state
379
+
self.scroll.pending_lines = 0;
380
+
381
+
// Set the initial index for our downard loop. We do this here because we might modify
382
+
// scroll.top before we traverse downward
383
+
var i: usize = self.scroll.top;
384
+
385
+
// If we are on the first item, and we have an upward scroll that consumed our offset, eg
386
+
// accumulated_height > 0, we reset state here. We can't scroll up anymore so we set
387
+
// accumulated_height to 0.
388
+
if (accumulated_height > 0 and self.scroll.top == 0) {
389
+
self.scroll.vertical_offset = 0;
390
+
accumulated_height = 0;
391
+
}
392
+
393
+
// If we are offset downward, insert widgets to the front of the list before traversing downard
394
+
if (accumulated_height > 0) {
395
+
try self.insertChildren(ctx, builder, &child_list, accumulated_height);
396
+
const last_child = child_list.items[child_list.items.len - 1];
397
+
accumulated_height = last_child.origin.row + last_child.surface.size.height;
398
+
}
399
+
400
+
const child_offset: u16 = if (self.draw_cursor) 2 else 0;
401
+
402
+
while (builder.itemAtIdx(i, self.cursor)) |child| {
403
+
// Defer the increment
404
+
defer i += 1;
405
+
406
+
// Set up constraints. We let the child be the entire height if it wants
407
+
const child_ctx = ctx.withConstraints(
408
+
.{ .width = max_size.width - child_offset, .height = 0 },
409
+
.{ .width = null, .height = null },
410
+
);
411
+
412
+
// Draw the child
413
+
const surf = try child.draw(child_ctx);
414
+
415
+
// Add the child surface to our list. It's offset from parent is the accumulated height
416
+
try child_list.append(ctx.arena, .{
417
+
.origin = .{ .col = child_offset - @as(i17, @intCast(self.scroll.left)), .row = accumulated_height },
418
+
.surface = surf,
419
+
.z_index = 0,
420
+
});
421
+
422
+
// Accumulate the height
423
+
accumulated_height += surf.size.height;
424
+
425
+
if (self.scroll.wants_cursor and i < self.cursor)
426
+
continue // continue if we want the cursor and haven't gotten there yet
427
+
else if (accumulated_height >= max_size.height)
428
+
break; // Break if we drew enough
429
+
} else {
430
+
// This branch runs if we ran out of items. Set our state accordingly
431
+
self.scroll.has_more_vertical = false;
432
+
}
433
+
434
+
// If we've looped through all the items without hitting the end we check for one more item to
435
+
// see if we just drew the last item on the bottom of the screen. If we just drew the last item
436
+
// we can set `scroll.has_more` to false.
437
+
if (self.scroll.has_more_vertical and accumulated_height <= max_size.height) {
438
+
if (builder.itemAtIdx(i, self.cursor) == null) self.scroll.has_more_vertical = false;
439
+
}
440
+
441
+
var total_height: usize = totalHeight(&child_list);
442
+
443
+
// If we reached the bottom, don't have enough height to fill the screen, and have room to add
444
+
// more, then we add more until out of items or filled the space. This can happen on a resize
445
+
if (!self.scroll.has_more_vertical and total_height < max_size.height and self.scroll.top > 0) {
446
+
try self.insertChildren(ctx, builder, &child_list, @intCast(max_size.height - total_height));
447
+
// Set the new total height
448
+
total_height = totalHeight(&child_list);
449
+
}
450
+
451
+
if (self.draw_cursor and self.cursor >= self.scroll.top) blk: {
452
+
// The index of the cursored widget in our child_list
453
+
const cursored_idx: u32 = self.cursor - self.scroll.top;
454
+
// Nothing to draw if our cursor is below our viewport
455
+
if (cursored_idx >= child_list.items.len) break :blk;
456
+
457
+
const sub = try ctx.arena.alloc(vxfw.SubSurface, 1);
458
+
const child = child_list.items[cursored_idx];
459
+
sub[0] = .{
460
+
.origin = .{ .col = child_offset - @as(i17, @intCast(self.scroll.left)), .row = 0 },
461
+
.surface = child.surface,
462
+
.z_index = 0,
463
+
};
464
+
const cursor_surf = try vxfw.Surface.initWithChildren(
465
+
ctx.arena,
466
+
self.widget(),
467
+
.{ .width = child_offset, .height = child.surface.size.height },
468
+
sub,
469
+
);
470
+
for (0..cursor_surf.size.height) |row| {
471
+
cursor_surf.writeCell(0, @intCast(row), self.cursor_indicator);
472
+
}
473
+
child_list.items[cursored_idx] = .{
474
+
.origin = .{ .col = 0, .row = child.origin.row },
475
+
.surface = cursor_surf,
476
+
.z_index = 0,
477
+
};
478
+
}
479
+
480
+
// If we want the cursor, we check that the cursored widget is fully in view. If it is too
481
+
// large, we position it so that it is the top item with a 0 offset
482
+
if (self.scroll.wants_cursor) {
483
+
const cursored_idx: u32 = self.cursor - self.scroll.top;
484
+
const sub = child_list.items[cursored_idx];
485
+
// The bottom row of the cursored widget
486
+
const bottom = sub.origin.row + sub.surface.size.height;
487
+
if (bottom > max_size.height) {
488
+
// Adjust the origin by the difference
489
+
// anchor bottom
490
+
var origin: i17 = max_size.height;
491
+
var idx: usize = cursored_idx + 1;
492
+
while (idx > 0) : (idx -= 1) {
493
+
var child = child_list.items[idx - 1];
494
+
origin -= child.surface.size.height;
495
+
child.origin.row = origin;
496
+
child_list.items[idx - 1] = child;
497
+
}
498
+
} else if (sub.surface.size.height >= max_size.height) {
499
+
// TODO: handle when the child is larger than our height.
500
+
// We need to change the max constraint to be optional sizes so that we can support
501
+
// unbounded drawing in scrollable areas
502
+
self.scroll.top = self.cursor;
503
+
self.scroll.vertical_offset = 0;
504
+
child_list.deinit(ctx.arena);
505
+
try child_list.append(ctx.arena, .{
506
+
.origin = .{ .col = 0 - @as(i17, @intCast(self.scroll.left)), .row = 0 },
507
+
.surface = sub.surface,
508
+
.z_index = 0,
509
+
});
510
+
total_height = sub.surface.size.height;
511
+
}
512
+
}
513
+
514
+
// If we reached the bottom, we need to reset origins
515
+
if (!self.scroll.has_more_vertical and total_height < max_size.height) {
516
+
// anchor top
517
+
assert(self.scroll.top == 0);
518
+
self.scroll.vertical_offset = 0;
519
+
var origin: i17 = 0;
520
+
for (0..child_list.items.len) |idx| {
521
+
var child = child_list.items[idx];
522
+
child.origin.row = origin;
523
+
origin += child.surface.size.height;
524
+
child_list.items[idx] = child;
525
+
}
526
+
} else if (!self.scroll.has_more_vertical) {
527
+
// anchor bottom
528
+
var origin: i17 = max_size.height;
529
+
var idx: usize = child_list.items.len;
530
+
while (idx > 0) : (idx -= 1) {
531
+
var child = child_list.items[idx - 1];
532
+
origin -= child.surface.size.height;
533
+
child.origin.row = origin;
534
+
child_list.items[idx - 1] = child;
535
+
}
536
+
}
537
+
538
+
// Reset horizontal scroll info.
539
+
self.scroll.has_more_horizontal = false;
540
+
for (child_list.items) |child| {
541
+
if (child.surface.size.width -| self.scroll.left > max_size.width) {
542
+
self.scroll.has_more_horizontal = true;
543
+
break;
544
+
}
545
+
}
546
+
547
+
var start: usize = 0;
548
+
var end: usize = child_list.items.len;
549
+
550
+
for (child_list.items, 0..) |child, idx| {
551
+
if (child.origin.row <= 0 and child.origin.row + child.surface.size.height > 0) {
552
+
start = idx;
553
+
self.scroll.vertical_offset = -child.origin.row;
554
+
self.scroll.top += @intCast(idx);
555
+
}
556
+
if (child.origin.row > max_size.height) {
557
+
end = idx;
558
+
break;
559
+
}
560
+
}
561
+
562
+
surface.children = child_list.items;
563
+
564
+
// Update last known height.
565
+
// If the bits from total_height don't fit u8 we won't get the right value from @intCast or
566
+
// @truncate so we check manually.
567
+
self.last_height = if (total_height > 255) 255 else @intCast(total_height);
568
+
569
+
return surface;
570
+
}
571
+
572
+
const SliceBuilder = struct {
573
+
slice: []const vxfw.Widget,
574
+
575
+
fn build(ptr: *const anyopaque, idx: usize, _: usize) ?vxfw.Widget {
576
+
const self: *const SliceBuilder = @ptrCast(@alignCast(ptr));
577
+
if (idx >= self.slice.len) return null;
578
+
return self.slice[idx];
579
+
}
580
+
};
581
+
582
+
test ScrollView {
583
+
// Create child widgets
584
+
const Text = @import("Text.zig");
585
+
const abc: Text = .{ .text = "abc\n def\n ghi" };
586
+
const def: Text = .{ .text = "def" };
587
+
const ghi: Text = .{ .text = "ghi" };
588
+
const jklmno: Text = .{ .text = "jkl\n mno" };
589
+
//
590
+
// 0 |abc|
591
+
// 1 | d|ef
592
+
// 2 | g|hi
593
+
// 3 |def|
594
+
// 4 ghi
595
+
// 5 jkl
596
+
// 6 mno
597
+
598
+
// Create the scroll view
599
+
const scroll_view: ScrollView = .{
600
+
.wheel_scroll = 1, // Set wheel scroll to one
601
+
.children = .{ .slice = &.{
602
+
abc.widget(),
603
+
def.widget(),
604
+
ghi.widget(),
605
+
jklmno.widget(),
606
+
} },
607
+
};
608
+
609
+
// Boiler plate draw context
610
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
611
+
defer arena.deinit();
612
+
vxfw.DrawContext.init(.unicode);
613
+
614
+
const scroll_widget = scroll_view.widget();
615
+
const draw_ctx: vxfw.DrawContext = .{
616
+
.arena = arena.allocator(),
617
+
.min = .{},
618
+
.max = .{ .width = 3, .height = 4 },
619
+
.cell_size = .{ .width = 10, .height = 20 },
620
+
};
621
+
622
+
var surface = try scroll_widget.draw(draw_ctx);
623
+
// ScrollView expands to max height and max width
624
+
try std.testing.expectEqual(4, surface.size.height);
625
+
try std.testing.expectEqual(3, surface.size.width);
626
+
// We have 2 children, because only visible children appear as a surface
627
+
try std.testing.expectEqual(2, surface.children.len);
628
+
629
+
// ScrollView starts at the top and left.
630
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
631
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
632
+
633
+
// With the widgets provided the scroll view should have both more content to scroll vertically
634
+
// and horizontally.
635
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_vertical);
636
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_horizontal);
637
+
638
+
var mouse_event: vaxis.Mouse = .{
639
+
.col = 0,
640
+
.row = 0,
641
+
.button = .wheel_up,
642
+
.mods = .{},
643
+
.type = .press,
644
+
};
645
+
// Event handlers need a context
646
+
var ctx: vxfw.EventContext = .{
647
+
.alloc = std.testing.allocator,
648
+
.cmds = .empty,
649
+
};
650
+
defer ctx.cmds.deinit(ctx.alloc);
651
+
652
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
653
+
// Wheel up doesn't adjust the scroll
654
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
655
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
656
+
657
+
// Wheel right doesn't adjust the horizontal scroll
658
+
mouse_event.button = .wheel_right;
659
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
660
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
661
+
662
+
// Scroll right with 'h' doesn't adjust the horizontal scroll
663
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'h' } });
664
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
665
+
666
+
// Scroll right with '<c-b>' doesn't adjust the horizontal scroll
667
+
try scroll_widget.handleEvent(
668
+
&ctx,
669
+
.{ .key_press = .{ .codepoint = 'c', .mods = .{ .ctrl = true } } },
670
+
);
671
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
672
+
673
+
// === TEST SCROLL DOWN === //
674
+
675
+
// Send a wheel down to scroll down one line
676
+
mouse_event.button = .wheel_down;
677
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
678
+
// We have to draw the widget for scrolls to take effect
679
+
surface = try scroll_widget.draw(draw_ctx);
680
+
// 0 abc
681
+
// 1 | d|ef
682
+
// 2 | g|hi
683
+
// 3 |def|
684
+
// 4 |ghi|
685
+
// 5 jkl
686
+
// 6 mno
687
+
// We should have gone down 1 line, and not changed our top widget
688
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
689
+
try std.testing.expectEqual(1, scroll_view.scroll.vertical_offset);
690
+
// One more widget has scrolled into view
691
+
try std.testing.expectEqual(3, surface.children.len);
692
+
693
+
// Send a 'j' to scroll down one more line.
694
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'j' } });
695
+
surface = try scroll_widget.draw(draw_ctx);
696
+
// 0 abc
697
+
// 1 def
698
+
// 2 | g|hi
699
+
// 3 |def|
700
+
// 4 |ghi|
701
+
// 5 |jkl|
702
+
// 6 mno
703
+
// We should have gone down 1 line, and not changed our top widget
704
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
705
+
try std.testing.expectEqual(2, scroll_view.scroll.vertical_offset);
706
+
// One more widget has scrolled into view
707
+
try std.testing.expectEqual(4, surface.children.len);
708
+
709
+
// Send `<c-n> to scroll down one more line
710
+
try scroll_widget.handleEvent(
711
+
&ctx,
712
+
.{ .key_press = .{ .codepoint = 'n', .mods = .{ .ctrl = true } } },
713
+
);
714
+
surface = try scroll_widget.draw(draw_ctx);
715
+
// 0 abc
716
+
// 1 def
717
+
// 2 ghi
718
+
// 3 |def|
719
+
// 4 |ghi|
720
+
// 5 |jkl|
721
+
// 6 | m|no
722
+
// We should have gone down 1 line, which scrolls our top widget out of view
723
+
try std.testing.expectEqual(1, scroll_view.scroll.top);
724
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
725
+
// The top widget has now scrolled out of view, but is still rendered out of view because of
726
+
// how pending scroll events are handled.
727
+
try std.testing.expectEqual(4, surface.children.len);
728
+
729
+
// We've scrolled to the bottom.
730
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_vertical);
731
+
732
+
// Scroll down one more line, this shouldn't do anything.
733
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
734
+
surface = try scroll_widget.draw(draw_ctx);
735
+
// 0 abc
736
+
// 1 def
737
+
// 2 ghi
738
+
// 3 |def|
739
+
// 4 |ghi|
740
+
// 5 |jkl|
741
+
// 6 | m|no
742
+
try std.testing.expectEqual(1, scroll_view.scroll.top);
743
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
744
+
// The top widget was scrolled out of view on the last render, so we should no longer be
745
+
// drawing it right above the current view.
746
+
try std.testing.expectEqual(3, surface.children.len);
747
+
748
+
// We've scrolled to the bottom.
749
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_vertical);
750
+
751
+
// === TEST SCROLL UP === //
752
+
753
+
mouse_event.button = .wheel_up;
754
+
755
+
// Send mouse up, now the top widget is in view.
756
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
757
+
surface = try scroll_widget.draw(draw_ctx);
758
+
// 0 abc
759
+
// 1 def
760
+
// 2 | g|hi
761
+
// 3 |def|
762
+
// 4 |ghi|
763
+
// 5 |jkl|
764
+
// 6 mno
765
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
766
+
try std.testing.expectEqual(2, scroll_view.scroll.vertical_offset);
767
+
// The top widget was scrolled out of view on the last render, so we should no longer be
768
+
// drawing it right above the current view.
769
+
try std.testing.expectEqual(4, surface.children.len);
770
+
771
+
// We've scrolled away from the bottom.
772
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_vertical);
773
+
774
+
// Send 'k' to scroll up, now the bottom widget should be out of view.
775
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'k' } });
776
+
surface = try scroll_widget.draw(draw_ctx);
777
+
// 0 abc
778
+
// 1 | d|ef
779
+
// 2 | g|hi
780
+
// 3 |def|
781
+
// 4 |ghi|
782
+
// 5 jkl
783
+
// 6 mno
784
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
785
+
try std.testing.expectEqual(1, scroll_view.scroll.vertical_offset);
786
+
// The top widget was scrolled out of view on the last render, so we should no longer be
787
+
// drawing it right above the current view.
788
+
try std.testing.expectEqual(3, surface.children.len);
789
+
790
+
// Send '<c-p>' to scroll up, now we should be at the top.
791
+
try scroll_widget.handleEvent(
792
+
&ctx,
793
+
.{ .key_press = .{ .codepoint = 'p', .mods = .{ .ctrl = true } } },
794
+
);
795
+
surface = try scroll_widget.draw(draw_ctx);
796
+
// 0 |abc|
797
+
// 1 | d|ef
798
+
// 2 | g|hi
799
+
// 3 |def|
800
+
// 4 ghi
801
+
// 5 jkl
802
+
// 6 mno
803
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
804
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
805
+
// The top widget was scrolled out of view on the last render, so we should no longer be
806
+
// drawing it right above the current view.
807
+
try std.testing.expectEqual(2, surface.children.len);
808
+
809
+
// We should be at the top.
810
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
811
+
// We should still have no horizontal scroll.
812
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
813
+
814
+
// === TEST SCROLL LEFT - MOVES VIEW TO THE RIGHT === //
815
+
816
+
mouse_event.button = .wheel_left;
817
+
818
+
// Send `.wheel_left` to scroll the view to the right.
819
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
820
+
surface = try scroll_widget.draw(draw_ctx);
821
+
// 0 a|bc |
822
+
// 1 | de|f
823
+
// 2 | gh|i
824
+
// 3 d|ef |
825
+
// 4 ghi
826
+
// 5 jkl
827
+
// 6 mno
828
+
try std.testing.expectEqual(1, scroll_view.scroll.left);
829
+
// The number of children should be just the top 2 widgets.
830
+
try std.testing.expectEqual(2, surface.children.len);
831
+
// There is still more to draw horizontally.
832
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_horizontal);
833
+
834
+
// Send `l` to scroll the view to the right.
835
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l' } });
836
+
surface = try scroll_widget.draw(draw_ctx);
837
+
// 0 ab|c |
838
+
// 1 |def|
839
+
// 2 |ghi|
840
+
// 3 de|f |
841
+
// 4 ghi
842
+
// 5 jkl
843
+
// 6 mno
844
+
try std.testing.expectEqual(2, scroll_view.scroll.left);
845
+
// The number of children should be just the top 2 widgets.
846
+
try std.testing.expectEqual(2, surface.children.len);
847
+
// There is nothing more to draw horizontally.
848
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_horizontal);
849
+
850
+
// Send `<c-f>` to scroll the view to the right, this should do nothing.
851
+
try scroll_widget.handleEvent(
852
+
&ctx,
853
+
.{ .key_press = .{ .codepoint = 'f', .mods = .{ .ctrl = true } } },
854
+
);
855
+
surface = try scroll_widget.draw(draw_ctx);
856
+
// 0 ab|c |
857
+
// 1 |def|
858
+
// 2 |ghi|
859
+
// 3 de|f |
860
+
// 4 ghi
861
+
// 5 jkl
862
+
// 6 mno
863
+
try std.testing.expectEqual(2, scroll_view.scroll.left);
864
+
// The number of children should be just the top 2 widgets.
865
+
try std.testing.expectEqual(2, surface.children.len);
866
+
// There is nothing more to draw horizontally.
867
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_horizontal);
868
+
869
+
// Send `.wheel_right` to scroll the view to the left.
870
+
mouse_event.button = .wheel_right;
871
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
872
+
surface = try scroll_widget.draw(draw_ctx);
873
+
// 0 a|bc |
874
+
// 1 | de|f
875
+
// 2 | gh|i
876
+
// 3 d|ef |
877
+
// 4 ghi
878
+
// 5 jkl
879
+
// 6 mno
880
+
try std.testing.expectEqual(1, scroll_view.scroll.left);
881
+
// The number of children should be just the top 2 widgets.
882
+
try std.testing.expectEqual(2, surface.children.len);
883
+
// There is still more to draw horizontally.
884
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_horizontal);
885
+
886
+
// Processing 2 or more events before drawing may produce overscroll, because we need to draw
887
+
// the children to determine whether there's more horizontal scrolling available.
888
+
try scroll_widget.handleEvent(
889
+
&ctx,
890
+
.{ .key_press = .{ .codepoint = 'f', .mods = .{ .ctrl = true } } },
891
+
);
892
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l' } });
893
+
surface = try scroll_widget.draw(draw_ctx);
894
+
// 0 abc| |
895
+
// 1 d|ef |
896
+
// 2 g|hi |
897
+
// 3 def| |
898
+
// 4 ghi
899
+
// 5 jkl
900
+
// 6 mno
901
+
try std.testing.expectEqual(3, scroll_view.scroll.left);
902
+
// The number of children should be just the top 2 widgets.
903
+
try std.testing.expectEqual(2, surface.children.len);
904
+
// There is nothing more to draw horizontally.
905
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_horizontal);
906
+
907
+
// === TEST SCROLL RIGHT - MOVES VIEW TO THE LEFT === //
908
+
909
+
// Send `.wheel_right` to scroll the view to the left.
910
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
911
+
surface = try scroll_widget.draw(draw_ctx);
912
+
// 0 ab|c |
913
+
// 1 |def|
914
+
// 2 |ghi|
915
+
// 3 de|f |
916
+
// 4 ghi
917
+
// 5 jkl
918
+
// 6 mno
919
+
try std.testing.expectEqual(2, scroll_view.scroll.left);
920
+
// The number of children should be just the top 2 widgets.
921
+
try std.testing.expectEqual(2, surface.children.len);
922
+
// There is nothing more to draw horizontally.
923
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_horizontal);
924
+
925
+
// Send `h` to scroll the view to the left.
926
+
try scroll_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'h' } });
927
+
surface = try scroll_widget.draw(draw_ctx);
928
+
// 0 a|bc |
929
+
// 1 | de|f
930
+
// 2 | gh|i
931
+
// 3 d|ef |
932
+
// 4 ghi
933
+
// 5 jkl
934
+
// 6 mno
935
+
try std.testing.expectEqual(1, scroll_view.scroll.left);
936
+
// The number of children should be just the top 2 widgets.
937
+
try std.testing.expectEqual(2, surface.children.len);
938
+
// There is now more to draw horizontally.
939
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_horizontal);
940
+
941
+
// Send `<c-b>` to scroll the view to the left.
942
+
try scroll_widget.handleEvent(
943
+
&ctx,
944
+
.{ .key_press = .{ .codepoint = 'b', .mods = .{ .ctrl = true } } },
945
+
);
946
+
surface = try scroll_widget.draw(draw_ctx);
947
+
// 0 |abc|
948
+
// 1 | d|ef
949
+
// 2 | g|hi
950
+
// 3 |def|
951
+
// 4 ghi
952
+
// 5 jkl
953
+
// 6 mno
954
+
try std.testing.expectEqual(0, scroll_view.scroll.left);
955
+
// The number of children should be just the top 2 widgets.
956
+
try std.testing.expectEqual(2, surface.children.len);
957
+
// There is now more to draw horizontally.
958
+
try std.testing.expectEqual(true, scroll_view.scroll.has_more_horizontal);
959
+
960
+
// === TEST COMBINED HORIZONTAL AND VERTICAL SCROLL === //
961
+
962
+
// Scroll 3 columns to the right and 2 rows down.
963
+
mouse_event.button = .wheel_left;
964
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
965
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
966
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
967
+
mouse_event.button = .wheel_down;
968
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
969
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
970
+
surface = try scroll_widget.draw(draw_ctx);
971
+
// 0 abc
972
+
// 1 def
973
+
// 2 g|hi |
974
+
// 3 def| |
975
+
// 4 ghi| |
976
+
// 5 jkl| |
977
+
// 6 mno
978
+
try std.testing.expectEqual(3, scroll_view.scroll.left);
979
+
try std.testing.expectEqual(0, scroll_view.scroll.top);
980
+
try std.testing.expectEqual(2, scroll_view.scroll.vertical_offset);
981
+
// Even though only 1 child is visible, we still draw all 4 children in the view.
982
+
try std.testing.expectEqual(4, surface.children.len);
983
+
// There is nothing more to draw horizontally.
984
+
try std.testing.expectEqual(false, scroll_view.scroll.has_more_horizontal);
985
+
}
986
+
987
+
// @reykjalin found an issue on mac with ghostty where the scroll up and scroll down were uneven.
988
+
// Ghostty has high precision scrolling and sends a lot of wheel events for each tick
989
+
test "ScrollView: uneven scroll" {
990
+
// Create child widgets
991
+
const Text = @import("Text.zig");
992
+
const zero: Text = .{ .text = "0" };
993
+
const one: Text = .{ .text = "1" };
994
+
const two: Text = .{ .text = "2" };
995
+
const three: Text = .{ .text = "3" };
996
+
const four: Text = .{ .text = "4" };
997
+
const five: Text = .{ .text = "5" };
998
+
const six: Text = .{ .text = "6" };
999
+
// 0 |
1000
+
// 1 |
1001
+
// 2 |
1002
+
// 3 |
1003
+
// 4
1004
+
// 5
1005
+
// 6
1006
+
1007
+
// Create the list view
1008
+
const scroll_view: ScrollView = .{
1009
+
.wheel_scroll = 1, // Set wheel scroll to one
1010
+
.children = .{ .slice = &.{
1011
+
zero.widget(),
1012
+
one.widget(),
1013
+
two.widget(),
1014
+
three.widget(),
1015
+
four.widget(),
1016
+
five.widget(),
1017
+
six.widget(),
1018
+
} },
1019
+
};
1020
+
1021
+
// Boiler plate draw context
1022
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
1023
+
defer arena.deinit();
1024
+
vxfw.DrawContext.init(.unicode);
1025
+
1026
+
const scroll_widget = scroll_view.widget();
1027
+
const draw_ctx: vxfw.DrawContext = .{
1028
+
.arena = arena.allocator(),
1029
+
.min = .{},
1030
+
.max = .{ .width = 16, .height = 4 },
1031
+
.cell_size = .{ .width = 10, .height = 20 },
1032
+
};
1033
+
1034
+
var surface = try scroll_widget.draw(draw_ctx);
1035
+
1036
+
var mouse_event: vaxis.Mouse = .{
1037
+
.col = 0,
1038
+
.row = 0,
1039
+
.button = .wheel_up,
1040
+
.mods = .{},
1041
+
.type = .press,
1042
+
};
1043
+
// Event handlers need a context
1044
+
var ctx: vxfw.EventContext = .{
1045
+
.alloc = std.testing.allocator,
1046
+
.cmds = .empty,
1047
+
};
1048
+
defer ctx.cmds.deinit(ctx.alloc);
1049
+
1050
+
// Send a wheel down x 3
1051
+
mouse_event.button = .wheel_down;
1052
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
1053
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
1054
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
1055
+
// We have to draw the widget for scrolls to take effect
1056
+
surface = try scroll_widget.draw(draw_ctx);
1057
+
// 0
1058
+
// 1
1059
+
// 2
1060
+
// 3 |
1061
+
// 4 |
1062
+
// 5 |
1063
+
// 6 |
1064
+
try std.testing.expectEqual(3, scroll_view.scroll.top);
1065
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
1066
+
// The first time we draw again we still draw all 7 children due to how pending scroll events
1067
+
// work.
1068
+
try std.testing.expectEqual(7, surface.children.len);
1069
+
1070
+
surface = try scroll_widget.draw(draw_ctx);
1071
+
// By drawing again without any pending events there are now only the 4 visible elements
1072
+
// rendered.
1073
+
try std.testing.expectEqual(4, surface.children.len);
1074
+
1075
+
// Now wheel_up two times should move us two lines up
1076
+
mouse_event.button = .wheel_up;
1077
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
1078
+
try scroll_widget.handleEvent(&ctx, .{ .mouse = mouse_event });
1079
+
surface = try scroll_widget.draw(draw_ctx);
1080
+
try std.testing.expectEqual(1, scroll_view.scroll.top);
1081
+
try std.testing.expectEqual(0, scroll_view.scroll.vertical_offset);
1082
+
try std.testing.expectEqual(4, surface.children.len);
1083
+
}
1084
+
1085
+
test "refAllDecls" {
1086
+
std.testing.refAllDecls(@This());
1087
+
}
+103
src/vxfw/SizedBox.zig
+103
src/vxfw/SizedBox.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const SizedBox = @This();
9
+
10
+
child: vxfw.Widget,
11
+
size: vxfw.Size,
12
+
13
+
pub fn widget(self: *const SizedBox) vxfw.Widget {
14
+
return .{
15
+
.userdata = @constCast(self),
16
+
.drawFn = typeErasedDrawFn,
17
+
};
18
+
}
19
+
20
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
21
+
const self: *const SizedBox = @ptrCast(@alignCast(ptr));
22
+
const max: vxfw.MaxSize = .{
23
+
.width = if (ctx.max.width) |max_w| max_w else self.size.width,
24
+
.height = if (ctx.max.height) |max_h| max_h else self.size.height,
25
+
};
26
+
const min: vxfw.Size = .{
27
+
.width = @min(@max(ctx.min.width, self.size.width), max.width.?),
28
+
.height = @min(@max(ctx.min.height, self.size.height), max.height.?),
29
+
};
30
+
return self.child.draw(ctx.withConstraints(min, max));
31
+
}
32
+
33
+
test SizedBox {
34
+
// Create a test widget that saves the constraints it was given
35
+
const TestWidget = struct {
36
+
min: vxfw.Size,
37
+
max: vxfw.MaxSize,
38
+
39
+
pub fn widget(self: *@This()) vxfw.Widget {
40
+
return .{
41
+
.userdata = self,
42
+
.drawFn = @This().typeErasedDrawFn,
43
+
};
44
+
}
45
+
46
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
47
+
const self: *@This() = @ptrCast(@alignCast(ptr));
48
+
self.min = ctx.min;
49
+
self.max = ctx.max;
50
+
return .{
51
+
.size = ctx.min,
52
+
.widget = self.widget(),
53
+
.buffer = &.{},
54
+
.children = &.{},
55
+
};
56
+
}
57
+
};
58
+
59
+
// Boiler plate draw context
60
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
61
+
defer arena.deinit();
62
+
vxfw.DrawContext.init(.unicode);
63
+
64
+
var draw_ctx: vxfw.DrawContext = .{
65
+
.arena = arena.allocator(),
66
+
.min = .{},
67
+
.max = .{ .width = 16, .height = 16 },
68
+
.cell_size = .{ .width = 10, .height = 20 },
69
+
};
70
+
71
+
var test_widget: TestWidget = .{ .min = .{}, .max = .{} };
72
+
73
+
// SizedBox tries to draw the child widget at the specified size. It will shrink to fit within
74
+
// constraints
75
+
const sized_box: SizedBox = .{
76
+
.child = test_widget.widget(),
77
+
.size = .{ .width = 10, .height = 10 },
78
+
};
79
+
80
+
const box_widget = sized_box.widget();
81
+
{
82
+
const result = try box_widget.draw(draw_ctx);
83
+
// The sized box is smaller than the constraints, so we should be the desired size
84
+
try std.testing.expectEqual(sized_box.size, result.size);
85
+
}
86
+
87
+
{
88
+
draw_ctx.max.height = 8;
89
+
const result = try box_widget.draw(draw_ctx);
90
+
// The sized box is smaller than the constraints, so we should be that size
91
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 10, .height = 8 }), result.size);
92
+
}
93
+
94
+
draw_ctx.max.width = 8;
95
+
_ = try box_widget.draw(draw_ctx);
96
+
// The sized box is smaller than the constraints, so we should be that size
97
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 8, .height = 8 }), test_widget.min);
98
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 8, .height = 8 }), test_widget.max.size());
99
+
}
100
+
101
+
test "refAllDecls" {
102
+
std.testing.refAllDecls(@This());
103
+
}
+159
src/vxfw/Spinner.zig
+159
src/vxfw/Spinner.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const vxfw = @import("vxfw.zig");
5
+
6
+
const Allocator = std.mem.Allocator;
7
+
8
+
const Spinner = @This();
9
+
10
+
const frames: []const []const u8 = &.{ "โฃถ", "โฃง", "โฃ", "โก", "โ ฟ", "โขป", "โฃน", "โฃผ" };
11
+
const time_lapse: u32 = std.time.ms_per_s / 12; // 12 fps
12
+
13
+
count: std.atomic.Value(u16) = .{ .raw = 0 },
14
+
style: vaxis.Style = .{},
15
+
/// The frame index
16
+
frame: u4 = 0,
17
+
18
+
/// Turns true when we start the spinner. Only turns false during a draw if the count == 0. This
19
+
/// ensures we draw one more time past the spinner stopping to clear the state
20
+
was_spinning: std.atomic.Value(bool) = .{ .raw = false },
21
+
22
+
/// Start, or add one, to the spinner counter. Thread safe.
23
+
pub fn start(self: *Spinner) ?vxfw.Command {
24
+
self.was_spinning.store(true, .unordered);
25
+
const count = self.count.fetchAdd(1, .monotonic);
26
+
if (count == 0) {
27
+
return vxfw.Tick.in(time_lapse, self.widget());
28
+
}
29
+
return null;
30
+
}
31
+
32
+
/// Reduce one from the spinner counter. The spinner will stop when it reaches 0. Thread safe
33
+
pub fn stop(self: *Spinner) void {
34
+
self.count.store(self.count.load(.unordered) -| 1, .unordered);
35
+
}
36
+
37
+
pub fn wasSpinning(self: *Spinner) bool {
38
+
return self.was_spinning.load(.unordered);
39
+
}
40
+
41
+
pub fn widget(self: *Spinner) vxfw.Widget {
42
+
return .{
43
+
.userdata = self,
44
+
.eventHandler = typeErasedEventHandler,
45
+
.drawFn = typeErasedDrawFn,
46
+
};
47
+
}
48
+
49
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
50
+
const self: *Spinner = @ptrCast(@alignCast(ptr));
51
+
return self.handleEvent(ctx, event);
52
+
}
53
+
54
+
pub fn handleEvent(self: *Spinner, ctx: *vxfw.EventContext, event: vxfw.Event) Allocator.Error!void {
55
+
switch (event) {
56
+
.tick => {
57
+
const count = self.count.load(.unordered);
58
+
59
+
if (count == 0) {
60
+
if (self.wasSpinning()) {
61
+
ctx.redraw = true;
62
+
self.was_spinning.store(false, .unordered);
63
+
}
64
+
return;
65
+
}
66
+
// Update frame
67
+
self.frame += 1;
68
+
if (self.frame >= frames.len) self.frame = 0;
69
+
70
+
// Update rearm
71
+
try ctx.tick(time_lapse, self.widget());
72
+
},
73
+
else => {},
74
+
}
75
+
}
76
+
77
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
78
+
const self: *Spinner = @ptrCast(@alignCast(ptr));
79
+
return self.draw(ctx);
80
+
}
81
+
82
+
pub fn draw(self: *Spinner, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
83
+
const size: vxfw.Size = .{
84
+
.width = @max(1, ctx.min.width),
85
+
.height = @max(1, ctx.min.height),
86
+
};
87
+
88
+
const surface = try vxfw.Surface.init(ctx.arena, self.widget(), size);
89
+
@memset(surface.buffer, .{ .style = self.style });
90
+
91
+
if (self.count.load(.unordered) == 0) return surface;
92
+
93
+
surface.writeCell(0, 0, .{
94
+
.char = .{
95
+
.grapheme = frames[self.frame],
96
+
.width = 1,
97
+
},
98
+
.style = self.style,
99
+
});
100
+
return surface;
101
+
}
102
+
103
+
test Spinner {
104
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
105
+
defer arena.deinit();
106
+
// Create a spinner
107
+
var spinner: Spinner = .{};
108
+
// Get our widget interface
109
+
const spinner_widget = spinner.widget();
110
+
111
+
// Start the spinner. This (maybe) returns a Tick command to schedule the next frame. If the
112
+
// spinner is already running, no command is returned. Calling start is thread safe. The
113
+
// returned command can be added to an EventContext to schedule the frame
114
+
const maybe_cmd = spinner.start();
115
+
try std.testing.expect(maybe_cmd != null);
116
+
try std.testing.expect(maybe_cmd.? == .tick);
117
+
try std.testing.expectEqual(1, spinner.count.load(.unordered));
118
+
119
+
// If we call start again, we won't get another command but our counter will go up
120
+
const maybe_cmd2 = spinner.start();
121
+
try std.testing.expect(maybe_cmd2 == null);
122
+
try std.testing.expectEqual(2, spinner.count.load(.unordered));
123
+
124
+
// We are about to deliver the tick to the widget. We need an EventContext (the engine will
125
+
// provide this)
126
+
var ctx: vxfw.EventContext = .{
127
+
.alloc = arena.allocator(),
128
+
.cmds = .empty,
129
+
};
130
+
131
+
// The event loop handles the tick event and calls us back with a .tick event. If we should keep
132
+
// running, we will add a new tick event
133
+
try spinner_widget.handleEvent(&ctx, .tick);
134
+
135
+
// Receiving a .tick advances the frame
136
+
try std.testing.expectEqual(1, spinner.frame);
137
+
138
+
// Simulate a draw
139
+
const surface = try spinner_widget.draw(.{
140
+
.arena = arena.allocator(),
141
+
.min = .{},
142
+
.max = .{},
143
+
.cell_size = .{ .width = 10, .height = 20 },
144
+
});
145
+
146
+
// Spinner will try to be 1x1
147
+
try std.testing.expectEqual(1, surface.size.width);
148
+
try std.testing.expectEqual(1, surface.size.height);
149
+
150
+
// Stopping the spinner decrements our counter
151
+
spinner.stop();
152
+
try std.testing.expectEqual(1, spinner.count.load(.unordered));
153
+
spinner.stop();
154
+
try std.testing.expectEqual(0, spinner.count.load(.unordered));
155
+
}
156
+
157
+
test "refAllDecls" {
158
+
std.testing.refAllDecls(@This());
159
+
}
+251
src/vxfw/SplitView.zig
+251
src/vxfw/SplitView.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const SplitView = @This();
9
+
10
+
lhs: vxfw.Widget,
11
+
rhs: vxfw.Widget,
12
+
constrain: enum { lhs, rhs } = .lhs,
13
+
style: vaxis.Style = .{},
14
+
/// min width for the constrained side
15
+
min_width: u16 = 0,
16
+
/// max width for the constrained side
17
+
max_width: ?u16 = null,
18
+
/// Target width to draw at
19
+
width: u16,
20
+
21
+
/// Used to calculate mouse events when our constraint is rhs
22
+
last_max_width: ?u16 = null,
23
+
24
+
// State
25
+
pressed: bool = false,
26
+
mouse_set: bool = false,
27
+
28
+
pub fn widget(self: *const SplitView) vxfw.Widget {
29
+
return .{
30
+
.userdata = @constCast(self),
31
+
.eventHandler = typeErasedEventHandler,
32
+
.drawFn = typeErasedDrawFn,
33
+
};
34
+
}
35
+
36
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
37
+
const self: *SplitView = @ptrCast(@alignCast(ptr));
38
+
switch (event) {
39
+
.mouse_leave => {
40
+
self.pressed = false;
41
+
return;
42
+
},
43
+
.mouse => {},
44
+
else => return,
45
+
}
46
+
const mouse = event.mouse;
47
+
48
+
const separator_col: u16 = switch (self.constrain) {
49
+
.lhs => self.width,
50
+
.rhs => if (self.last_max_width) |max|
51
+
max -| self.width -| 1
52
+
else {
53
+
ctx.redraw = true;
54
+
return;
55
+
},
56
+
};
57
+
58
+
// If we are on the separator, we always set the mouse shape
59
+
if (mouse.col == separator_col) {
60
+
try ctx.setMouseShape(.@"ew-resize");
61
+
self.mouse_set = true;
62
+
// Set pressed state if we are a left click
63
+
if (mouse.type == .press and mouse.button == .left) {
64
+
self.pressed = true;
65
+
}
66
+
} else if (self.mouse_set) {
67
+
// If we have set the mouse state and *aren't* over the separator, default the mouse state
68
+
try ctx.setMouseShape(.default);
69
+
self.mouse_set = false;
70
+
}
71
+
72
+
// On release, we reset state
73
+
if (mouse.type == .release) {
74
+
self.pressed = false;
75
+
self.mouse_set = false;
76
+
try ctx.setMouseShape(.default);
77
+
}
78
+
79
+
// If pressed, we always keep the mouse shape and we update the width
80
+
if (self.pressed) {
81
+
try ctx.setMouseShape(.@"ew-resize");
82
+
switch (self.constrain) {
83
+
.lhs => {
84
+
self.width = @max(self.min_width, mouse.col);
85
+
if (self.max_width) |max| {
86
+
self.width = @min(self.width, max);
87
+
}
88
+
},
89
+
.rhs => {
90
+
const last_max = self.last_max_width orelse return;
91
+
const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col);
92
+
self.width = @min(last_max -| self.min_width, last_max -| mouse_col -| 1);
93
+
if (self.max_width) |max| {
94
+
self.width = @max(self.width, max);
95
+
}
96
+
},
97
+
}
98
+
ctx.consume_event = true;
99
+
}
100
+
}
101
+
102
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
103
+
const self: *SplitView = @ptrCast(@alignCast(ptr));
104
+
// Fills entire space
105
+
const max = ctx.max.size();
106
+
// Constrain width to the max
107
+
self.width = @min(self.width, max.width);
108
+
self.last_max_width = max.width;
109
+
110
+
// The constrained side is equal to the width
111
+
const constrained_min: vxfw.Size = .{ .width = self.width, .height = max.height };
112
+
const constrained_max = vxfw.MaxSize.fromSize(constrained_min);
113
+
114
+
const unconstrained_min: vxfw.Size = .{ .width = max.width -| self.width -| 1, .height = max.height };
115
+
const unconstrained_max = vxfw.MaxSize.fromSize(unconstrained_min);
116
+
117
+
var children = try std.ArrayList(vxfw.SubSurface).initCapacity(ctx.arena, 2);
118
+
119
+
switch (self.constrain) {
120
+
.lhs => {
121
+
if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
122
+
const lhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
123
+
const lhs_surface = try self.lhs.draw(lhs_ctx);
124
+
children.appendAssumeCapacity(.{
125
+
.surface = lhs_surface,
126
+
.origin = .{ .row = 0, .col = 0 },
127
+
});
128
+
}
129
+
if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
130
+
const rhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
131
+
const rhs_surface = try self.rhs.draw(rhs_ctx);
132
+
children.appendAssumeCapacity(.{
133
+
.surface = rhs_surface,
134
+
.origin = .{ .row = 0, .col = self.width + 1 },
135
+
});
136
+
}
137
+
var surface = try vxfw.Surface.initWithChildren(
138
+
ctx.arena,
139
+
self.widget(),
140
+
max,
141
+
children.items,
142
+
);
143
+
for (0..max.height) |row| {
144
+
surface.writeCell(self.width, @intCast(row), .{
145
+
.char = .{ .grapheme = "โ", .width = 1 },
146
+
.style = self.style,
147
+
});
148
+
}
149
+
return surface;
150
+
},
151
+
.rhs => {
152
+
if (unconstrained_max.width.? > 0 and unconstrained_max.height.? > 0) {
153
+
const lhs_ctx = ctx.withConstraints(unconstrained_min, unconstrained_max);
154
+
const lhs_surface = try self.lhs.draw(lhs_ctx);
155
+
children.appendAssumeCapacity(.{
156
+
.surface = lhs_surface,
157
+
.origin = .{ .row = 0, .col = 0 },
158
+
});
159
+
}
160
+
if (constrained_max.width.? > 0 and constrained_max.height.? > 0) {
161
+
const rhs_ctx = ctx.withConstraints(constrained_min, constrained_max);
162
+
const rhs_surface = try self.rhs.draw(rhs_ctx);
163
+
children.appendAssumeCapacity(.{
164
+
.surface = rhs_surface,
165
+
.origin = .{ .row = 0, .col = unconstrained_max.width.? + 1 },
166
+
});
167
+
}
168
+
var surface = try vxfw.Surface.initWithChildren(
169
+
ctx.arena,
170
+
self.widget(),
171
+
max,
172
+
children.items,
173
+
);
174
+
for (0..max.height) |row| {
175
+
surface.writeCell(max.width -| self.width -| 1, @intCast(row), .{
176
+
.char = .{ .grapheme = "โ", .width = 1 },
177
+
.style = self.style,
178
+
});
179
+
}
180
+
return surface;
181
+
},
182
+
}
183
+
}
184
+
185
+
test SplitView {
186
+
// Boiler plate draw context
187
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
188
+
defer arena.deinit();
189
+
vxfw.DrawContext.init(.unicode);
190
+
191
+
const draw_ctx: vxfw.DrawContext = .{
192
+
.arena = arena.allocator(),
193
+
.min = .{},
194
+
.max = .{ .width = 16, .height = 16 },
195
+
.cell_size = .{ .width = 10, .height = 20 },
196
+
};
197
+
198
+
// Create LHS and RHS widgets
199
+
const lhs: vxfw.Text = .{ .text = "Left hand side" };
200
+
const rhs: vxfw.Text = .{ .text = "Right hand side" };
201
+
202
+
var split_view: SplitView = .{
203
+
.lhs = lhs.widget(),
204
+
.rhs = rhs.widget(),
205
+
.width = 8,
206
+
};
207
+
208
+
const split_widget = split_view.widget();
209
+
{
210
+
const surface = try split_widget.draw(draw_ctx);
211
+
// SplitView expands to fill the space
212
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 16, .height = 16 }), surface.size);
213
+
// It has two children
214
+
try std.testing.expectEqual(2, surface.children.len);
215
+
// The left child should have a width = SplitView.width
216
+
try std.testing.expectEqual(split_view.width, surface.children[0].surface.size.width);
217
+
}
218
+
219
+
// Send the widget a mouse press on the separator
220
+
var mouse: vaxis.Mouse = .{
221
+
// The separator is at width
222
+
.col = @intCast(split_view.width),
223
+
.row = 0,
224
+
.type = .press,
225
+
.button = .left,
226
+
.mods = .{},
227
+
};
228
+
229
+
var ctx: vxfw.EventContext = .{
230
+
.alloc = arena.allocator(),
231
+
.cmds = .empty,
232
+
};
233
+
try split_widget.handleEvent(&ctx, .{ .mouse = mouse });
234
+
// We should get a command to change the mouse shape
235
+
try std.testing.expect(ctx.cmds.items[0] == .set_mouse_shape);
236
+
try std.testing.expect(ctx.redraw);
237
+
try std.testing.expect(split_view.pressed);
238
+
239
+
// If we move the mouse, we should update the width
240
+
mouse.col = 2;
241
+
mouse.type = .drag;
242
+
try split_widget.handleEvent(&ctx, .{ .mouse = mouse });
243
+
try std.testing.expect(ctx.redraw);
244
+
try std.testing.expect(split_view.pressed);
245
+
const mouse_col: u16 = if (mouse.col < 0) 0 else @intCast(mouse.col);
246
+
try std.testing.expectEqual(mouse_col, split_view.width);
247
+
}
248
+
249
+
test "refAllDecls" {
250
+
std.testing.refAllDecls(@This());
251
+
}
+508
src/vxfw/Text.zig
+508
src/vxfw/Text.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const Allocator = std.mem.Allocator;
5
+
6
+
const vxfw = @import("vxfw.zig");
7
+
8
+
const Text = @This();
9
+
10
+
text: []const u8,
11
+
style: vaxis.Style = .{},
12
+
text_align: enum { left, center, right } = .left,
13
+
softwrap: bool = true,
14
+
overflow: enum { ellipsis, clip } = .ellipsis,
15
+
width_basis: enum { parent, longest_line } = .longest_line,
16
+
17
+
pub fn widget(self: *const Text) vxfw.Widget {
18
+
return .{
19
+
.userdata = @constCast(self),
20
+
.drawFn = typeErasedDrawFn,
21
+
};
22
+
}
23
+
24
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
25
+
const self: *const Text = @ptrCast(@alignCast(ptr));
26
+
return self.draw(ctx);
27
+
}
28
+
29
+
pub fn draw(self: *const Text, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
30
+
if (ctx.max.width != null and ctx.max.width.? == 0) {
31
+
return .{
32
+
.size = ctx.min,
33
+
.widget = self.widget(),
34
+
.buffer = &.{},
35
+
.children = &.{},
36
+
};
37
+
}
38
+
const container_size = self.findContainerSize(ctx);
39
+
40
+
// Create a surface of target width and max height. We'll trim the result after drawing
41
+
const surface = try vxfw.Surface.init(
42
+
ctx.arena,
43
+
self.widget(),
44
+
container_size,
45
+
);
46
+
const base_style: vaxis.Style = .{
47
+
.fg = self.style.fg,
48
+
.bg = self.style.bg,
49
+
.reverse = self.style.reverse,
50
+
};
51
+
const base: vaxis.Cell = .{ .style = base_style };
52
+
@memset(surface.buffer, base);
53
+
54
+
var row: u16 = 0;
55
+
if (self.softwrap) {
56
+
var iter = SoftwrapIterator.init(self.text, ctx);
57
+
while (iter.next()) |line| {
58
+
if (row >= container_size.height) break;
59
+
defer row += 1;
60
+
var col: u16 = switch (self.text_align) {
61
+
.left => 0,
62
+
.center => (container_size.width - line.width) / 2,
63
+
.right => container_size.width - line.width,
64
+
};
65
+
var char_iter = ctx.graphemeIterator(line.bytes);
66
+
while (char_iter.next()) |char| {
67
+
const grapheme = char.bytes(line.bytes);
68
+
if (std.mem.eql(u8, grapheme, "\t")) {
69
+
for (0..8) |i| {
70
+
surface.writeCell(@intCast(col + i), row, .{
71
+
.char = .{ .grapheme = " ", .width = 1 },
72
+
.style = self.style,
73
+
});
74
+
}
75
+
col += 8;
76
+
continue;
77
+
}
78
+
const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme));
79
+
surface.writeCell(col, row, .{
80
+
.char = .{ .grapheme = grapheme, .width = grapheme_width },
81
+
.style = self.style,
82
+
});
83
+
col += grapheme_width;
84
+
}
85
+
}
86
+
} else {
87
+
var line_iter: LineIterator = .{ .buf = self.text };
88
+
while (line_iter.next()) |line| {
89
+
if (row >= container_size.height) break;
90
+
// \t is default 1 wide. We add 7x the count of tab characters to get the full width
91
+
const line_width = ctx.stringWidth(line) + 7 * std.mem.count(u8, line, "\t");
92
+
defer row += 1;
93
+
const resolved_line_width = @min(container_size.width, line_width);
94
+
var col: u16 = switch (self.text_align) {
95
+
.left => 0,
96
+
.center => (container_size.width - resolved_line_width) / 2,
97
+
.right => container_size.width - resolved_line_width,
98
+
};
99
+
var char_iter = ctx.graphemeIterator(line);
100
+
while (char_iter.next()) |char| {
101
+
if (col >= container_size.width) break;
102
+
const grapheme = char.bytes(line);
103
+
const grapheme_width: u8 = @intCast(ctx.stringWidth(grapheme));
104
+
105
+
if (col + grapheme_width >= container_size.width and
106
+
line_width > container_size.width and
107
+
self.overflow == .ellipsis)
108
+
{
109
+
surface.writeCell(col, row, .{
110
+
.char = .{ .grapheme = "โฆ", .width = 1 },
111
+
.style = self.style,
112
+
});
113
+
col = container_size.width;
114
+
} else {
115
+
surface.writeCell(col, row, .{
116
+
.char = .{ .grapheme = grapheme, .width = grapheme_width },
117
+
.style = self.style,
118
+
});
119
+
col += @intCast(grapheme_width);
120
+
}
121
+
}
122
+
}
123
+
}
124
+
return surface.trimHeight(@max(row, ctx.min.height));
125
+
}
126
+
127
+
/// Determines the container size by finding the widest line in the viewable area
128
+
fn findContainerSize(self: Text, ctx: vxfw.DrawContext) vxfw.Size {
129
+
var row: u16 = 0;
130
+
var max_width: u16 = ctx.min.width;
131
+
if (self.softwrap) {
132
+
var iter = SoftwrapIterator.init(self.text, ctx);
133
+
while (iter.next()) |line| {
134
+
if (ctx.max.outsideHeight(row))
135
+
break;
136
+
137
+
defer row += 1;
138
+
max_width = @max(max_width, line.width);
139
+
}
140
+
} else {
141
+
var line_iter: LineIterator = .{ .buf = self.text };
142
+
while (line_iter.next()) |line| {
143
+
if (ctx.max.outsideHeight(row))
144
+
break;
145
+
const line_width: u16 = @truncate(ctx.stringWidth(line));
146
+
defer row += 1;
147
+
const resolved_line_width = if (ctx.max.width) |max|
148
+
@min(max, line_width)
149
+
else
150
+
line_width;
151
+
max_width = @max(max_width, resolved_line_width);
152
+
}
153
+
}
154
+
const result_width = switch (self.width_basis) {
155
+
.longest_line => blk: {
156
+
if (ctx.max.width) |max|
157
+
break :blk @min(max, max_width)
158
+
else
159
+
break :blk max_width;
160
+
},
161
+
.parent => blk: {
162
+
std.debug.assert(ctx.max.width != null);
163
+
break :blk ctx.max.width.?;
164
+
},
165
+
};
166
+
return .{ .width = result_width, .height = @max(row, ctx.min.height) };
167
+
}
168
+
169
+
/// Iterates a slice of bytes by linebreaks. Lines are split by '\r', '\n', or '\r\n'
170
+
pub const LineIterator = struct {
171
+
buf: []const u8,
172
+
index: usize = 0,
173
+
174
+
fn next(self: *LineIterator) ?[]const u8 {
175
+
if (self.index >= self.buf.len) return null;
176
+
177
+
const start = self.index;
178
+
const end = std.mem.indexOfAnyPos(u8, self.buf, self.index, "\r\n") orelse {
179
+
self.index = self.buf.len;
180
+
return self.buf[start..];
181
+
};
182
+
183
+
self.index = end;
184
+
self.consumeCR();
185
+
self.consumeLF();
186
+
return self.buf[start..end];
187
+
}
188
+
189
+
// consumes a \n byte
190
+
fn consumeLF(self: *LineIterator) void {
191
+
if (self.index >= self.buf.len) return;
192
+
if (self.buf[self.index] == '\n') self.index += 1;
193
+
}
194
+
195
+
// consumes a \r byte
196
+
fn consumeCR(self: *LineIterator) void {
197
+
if (self.index >= self.buf.len) return;
198
+
if (self.buf[self.index] == '\r') self.index += 1;
199
+
}
200
+
};
201
+
202
+
pub const SoftwrapIterator = struct {
203
+
ctx: vxfw.DrawContext,
204
+
line: []const u8 = "",
205
+
index: usize = 0,
206
+
hard_iter: LineIterator,
207
+
208
+
pub const Line = struct {
209
+
width: u16,
210
+
bytes: []const u8,
211
+
};
212
+
213
+
const soft_breaks = " \t";
214
+
215
+
fn init(buf: []const u8, ctx: vxfw.DrawContext) SoftwrapIterator {
216
+
return .{
217
+
.ctx = ctx,
218
+
.hard_iter = .{ .buf = buf },
219
+
};
220
+
}
221
+
222
+
fn next(self: *SoftwrapIterator) ?Line {
223
+
// Advance the hard iterator
224
+
if (self.index == self.line.len) {
225
+
self.line = self.hard_iter.next() orelse return null;
226
+
self.line = std.mem.trimRight(u8, self.line, " \t");
227
+
self.index = 0;
228
+
}
229
+
230
+
const start = self.index;
231
+
var cur_width: u16 = 0;
232
+
while (self.index < self.line.len) {
233
+
const idx = self.nextWrap();
234
+
const word = self.line[self.index..idx];
235
+
const next_width = self.ctx.stringWidth(word);
236
+
237
+
if (self.ctx.max.width) |max| {
238
+
if (cur_width + next_width > max) {
239
+
// Trim the word to see if it can fit on a line by itself
240
+
const trimmed = std.mem.trimLeft(u8, word, " \t");
241
+
const trimmed_bytes = word.len - trimmed.len;
242
+
// The number of bytes we trimmed is equal to the reduction in length
243
+
const trimmed_width = next_width - trimmed_bytes;
244
+
if (trimmed_width > max) {
245
+
// Won't fit on line by itself, so fit as much on this line as we can
246
+
var iter = self.ctx.graphemeIterator(word);
247
+
while (iter.next()) |item| {
248
+
const grapheme = item.bytes(word);
249
+
const w = self.ctx.stringWidth(grapheme);
250
+
if (cur_width + w > max) {
251
+
const end = self.index;
252
+
return .{ .width = cur_width, .bytes = self.line[start..end] };
253
+
}
254
+
cur_width += @intCast(w);
255
+
self.index += grapheme.len;
256
+
}
257
+
}
258
+
// We are softwrapping, advance index to the start of the next word
259
+
const end = self.index;
260
+
self.index = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse self.line.len;
261
+
return .{ .width = cur_width, .bytes = self.line[start..end] };
262
+
}
263
+
}
264
+
265
+
self.index = idx;
266
+
cur_width += @intCast(next_width);
267
+
}
268
+
return .{ .width = cur_width, .bytes = self.line[start..] };
269
+
}
270
+
271
+
/// Determines the index of the end of the next word
272
+
fn nextWrap(self: *SoftwrapIterator) usize {
273
+
// Find the first linear whitespace char
274
+
const start_pos = std.mem.indexOfNonePos(u8, self.line, self.index, soft_breaks) orelse
275
+
return self.line.len;
276
+
if (std.mem.indexOfAnyPos(u8, self.line, start_pos, soft_breaks)) |idx| {
277
+
return idx;
278
+
}
279
+
return self.line.len;
280
+
}
281
+
282
+
// consumes a \n byte
283
+
fn consumeLF(self: *SoftwrapIterator) void {
284
+
if (self.index >= self.buf.len) return;
285
+
if (self.buf[self.index] == '\n') self.index += 1;
286
+
}
287
+
288
+
// consumes a \r byte
289
+
fn consumeCR(self: *SoftwrapIterator) void {
290
+
if (self.index >= self.buf.len) return;
291
+
if (self.buf[self.index] == '\r') self.index += 1;
292
+
}
293
+
};
294
+
295
+
test "SoftwrapIterator: LF breaks" {
296
+
vxfw.DrawContext.init(.unicode);
297
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
298
+
defer arena.deinit();
299
+
300
+
const ctx: vxfw.DrawContext = .{
301
+
.min = .{ .width = 0, .height = 0 },
302
+
.max = .{ .width = 20, .height = 10 },
303
+
.arena = arena.allocator(),
304
+
.cell_size = .{ .width = 10, .height = 20 },
305
+
};
306
+
var iter = SoftwrapIterator.init("Hello, \n world", ctx);
307
+
const first = iter.next();
308
+
try std.testing.expect(first != null);
309
+
try std.testing.expectEqualStrings("Hello,", first.?.bytes);
310
+
try std.testing.expectEqual(6, first.?.width);
311
+
312
+
const second = iter.next();
313
+
try std.testing.expect(second != null);
314
+
try std.testing.expectEqualStrings(" world", second.?.bytes);
315
+
try std.testing.expectEqual(6, second.?.width);
316
+
317
+
const end = iter.next();
318
+
try std.testing.expect(end == null);
319
+
}
320
+
321
+
test "SoftwrapIterator: soft breaks that fit" {
322
+
vxfw.DrawContext.init(.unicode);
323
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
324
+
defer arena.deinit();
325
+
326
+
const ctx: vxfw.DrawContext = .{
327
+
.min = .{ .width = 0, .height = 0 },
328
+
.max = .{ .width = 6, .height = 10 },
329
+
.arena = arena.allocator(),
330
+
.cell_size = .{ .width = 10, .height = 20 },
331
+
};
332
+
var iter = SoftwrapIterator.init("Hello, \nworld", ctx);
333
+
const first = iter.next();
334
+
try std.testing.expect(first != null);
335
+
try std.testing.expectEqualStrings("Hello,", first.?.bytes);
336
+
try std.testing.expectEqual(6, first.?.width);
337
+
338
+
const second = iter.next();
339
+
try std.testing.expect(second != null);
340
+
try std.testing.expectEqualStrings("world", second.?.bytes);
341
+
try std.testing.expectEqual(5, second.?.width);
342
+
343
+
const end = iter.next();
344
+
try std.testing.expect(end == null);
345
+
}
346
+
347
+
test "SoftwrapIterator: soft breaks that are longer than width" {
348
+
vxfw.DrawContext.init(.unicode);
349
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
350
+
defer arena.deinit();
351
+
352
+
const ctx: vxfw.DrawContext = .{
353
+
.min = .{ .width = 0, .height = 0 },
354
+
.max = .{ .width = 6, .height = 10 },
355
+
.arena = arena.allocator(),
356
+
.cell_size = .{ .width = 10, .height = 20 },
357
+
};
358
+
var iter = SoftwrapIterator.init("very-long-word \nworld", ctx);
359
+
const first = iter.next();
360
+
try std.testing.expect(first != null);
361
+
try std.testing.expectEqualStrings("very-l", first.?.bytes);
362
+
try std.testing.expectEqual(6, first.?.width);
363
+
364
+
const second = iter.next();
365
+
try std.testing.expect(second != null);
366
+
try std.testing.expectEqualStrings("ong-wo", second.?.bytes);
367
+
try std.testing.expectEqual(6, second.?.width);
368
+
369
+
const third = iter.next();
370
+
try std.testing.expect(third != null);
371
+
try std.testing.expectEqualStrings("rd", third.?.bytes);
372
+
try std.testing.expectEqual(2, third.?.width);
373
+
374
+
const fourth = iter.next();
375
+
try std.testing.expect(fourth != null);
376
+
try std.testing.expectEqualStrings("world", fourth.?.bytes);
377
+
try std.testing.expectEqual(5, fourth.?.width);
378
+
379
+
const end = iter.next();
380
+
try std.testing.expect(end == null);
381
+
}
382
+
383
+
test "SoftwrapIterator: soft breaks with leading spaces" {
384
+
vxfw.DrawContext.init(.unicode);
385
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
386
+
defer arena.deinit();
387
+
388
+
const ctx: vxfw.DrawContext = .{
389
+
.min = .{ .width = 0, .height = 0 },
390
+
.max = .{ .width = 6, .height = 10 },
391
+
.arena = arena.allocator(),
392
+
.cell_size = .{ .width = 10, .height = 20 },
393
+
};
394
+
var iter = SoftwrapIterator.init("Hello, \n world", ctx);
395
+
const first = iter.next();
396
+
try std.testing.expect(first != null);
397
+
try std.testing.expectEqualStrings("Hello,", first.?.bytes);
398
+
try std.testing.expectEqual(6, first.?.width);
399
+
400
+
const second = iter.next();
401
+
try std.testing.expect(second != null);
402
+
try std.testing.expectEqualStrings(" world", second.?.bytes);
403
+
try std.testing.expectEqual(6, second.?.width);
404
+
405
+
const end = iter.next();
406
+
try std.testing.expect(end == null);
407
+
}
408
+
409
+
test "LineIterator: LF breaks" {
410
+
const input = "Hello, \n world";
411
+
var iter: LineIterator = .{ .buf = input };
412
+
const first = iter.next();
413
+
try std.testing.expect(first != null);
414
+
try std.testing.expectEqualStrings("Hello, ", first.?);
415
+
416
+
const second = iter.next();
417
+
try std.testing.expect(second != null);
418
+
try std.testing.expectEqualStrings(" world", second.?);
419
+
420
+
const end = iter.next();
421
+
try std.testing.expect(end == null);
422
+
}
423
+
424
+
test "LineIterator: CR breaks" {
425
+
const input = "Hello, \r world";
426
+
var iter: LineIterator = .{ .buf = input };
427
+
const first = iter.next();
428
+
try std.testing.expect(first != null);
429
+
try std.testing.expectEqualStrings("Hello, ", first.?);
430
+
431
+
const second = iter.next();
432
+
try std.testing.expect(second != null);
433
+
try std.testing.expectEqualStrings(" world", second.?);
434
+
435
+
const end = iter.next();
436
+
try std.testing.expect(end == null);
437
+
}
438
+
439
+
test "LineIterator: CRLF breaks" {
440
+
const input = "Hello, \r\n world";
441
+
var iter: LineIterator = .{ .buf = input };
442
+
const first = iter.next();
443
+
try std.testing.expect(first != null);
444
+
try std.testing.expectEqualStrings("Hello, ", first.?);
445
+
446
+
const second = iter.next();
447
+
try std.testing.expect(second != null);
448
+
try std.testing.expectEqualStrings(" world", second.?);
449
+
450
+
const end = iter.next();
451
+
try std.testing.expect(end == null);
452
+
}
453
+
454
+
test "LineIterator: CRLF breaks with empty line" {
455
+
const input = "Hello, \r\n\r\n world";
456
+
var iter: LineIterator = .{ .buf = input };
457
+
const first = iter.next();
458
+
try std.testing.expect(first != null);
459
+
try std.testing.expectEqualStrings("Hello, ", first.?);
460
+
461
+
const second = iter.next();
462
+
try std.testing.expect(second != null);
463
+
try std.testing.expectEqualStrings("", second.?);
464
+
465
+
const third = iter.next();
466
+
try std.testing.expect(third != null);
467
+
try std.testing.expectEqualStrings(" world", third.?);
468
+
469
+
const end = iter.next();
470
+
try std.testing.expect(end == null);
471
+
}
472
+
473
+
test Text {
474
+
var text: Text = .{ .text = "Hello, world" };
475
+
const text_widget = text.widget();
476
+
477
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
478
+
defer arena.deinit();
479
+
vxfw.DrawContext.init(.unicode);
480
+
481
+
// Center expands to the max size. It must therefore have non-null max width and max height.
482
+
// These values are asserted in draw
483
+
const ctx: vxfw.DrawContext = .{
484
+
.arena = arena.allocator(),
485
+
.min = .{},
486
+
.max = .{ .width = 7, .height = 2 },
487
+
.cell_size = .{ .width = 10, .height = 20 },
488
+
};
489
+
490
+
{
491
+
// Text softwraps by default
492
+
const surface = try text_widget.draw(ctx);
493
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 6, .height = 2 }), surface.size);
494
+
}
495
+
496
+
{
497
+
text.softwrap = false;
498
+
text.overflow = .ellipsis;
499
+
const surface = try text_widget.draw(ctx);
500
+
try std.testing.expectEqual(@as(vxfw.Size, .{ .width = 7, .height = 1 }), surface.size);
501
+
// The last character will be an ellipsis
502
+
try std.testing.expectEqualStrings("โฆ", surface.buffer[surface.buffer.len - 1].char.grapheme);
503
+
}
504
+
}
505
+
506
+
test "refAllDecls" {
507
+
std.testing.refAllDecls(@This());
508
+
}
+598
src/vxfw/TextField.zig
+598
src/vxfw/TextField.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
4
+
const vxfw = @import("vxfw.zig");
5
+
6
+
const assert = std.debug.assert;
7
+
8
+
const Allocator = std.mem.Allocator;
9
+
const Key = vaxis.Key;
10
+
const Cell = vaxis.Cell;
11
+
const Window = vaxis.Window;
12
+
const unicode = vaxis.unicode;
13
+
14
+
const TextField = @This();
15
+
16
+
const ellipsis: Cell.Character = .{ .grapheme = "โฆ", .width = 1 };
17
+
18
+
// Index of our cursor
19
+
buf: Buffer,
20
+
21
+
/// Style to draw the TextField with
22
+
style: vaxis.Style = .{},
23
+
24
+
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
25
+
draw_offset: u16 = 0,
26
+
/// the column we placed the cursor the last time we drew
27
+
prev_cursor_col: u16 = 0,
28
+
/// the grapheme index of the cursor the last time we drew
29
+
prev_cursor_idx: u16 = 0,
30
+
/// approximate distance from an edge before we scroll
31
+
scroll_offset: u4 = 4,
32
+
/// Previous width we drew at
33
+
prev_width: u16 = 0,
34
+
35
+
previous_val: []const u8 = "",
36
+
37
+
userdata: ?*anyopaque = null,
38
+
onChange: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null,
39
+
onSubmit: ?*const fn (?*anyopaque, *vxfw.EventContext, []const u8) anyerror!void = null,
40
+
41
+
pub fn init(alloc: std.mem.Allocator) TextField {
42
+
return TextField{
43
+
.buf = Buffer.init(alloc),
44
+
};
45
+
}
46
+
47
+
pub fn deinit(self: *TextField) void {
48
+
self.buf.allocator.free(self.previous_val);
49
+
self.buf.deinit();
50
+
}
51
+
52
+
pub fn widget(self: *TextField) vxfw.Widget {
53
+
return .{
54
+
.userdata = self,
55
+
.eventHandler = typeErasedEventHandler,
56
+
.drawFn = typeErasedDrawFn,
57
+
};
58
+
}
59
+
60
+
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
61
+
const self: *TextField = @ptrCast(@alignCast(ptr));
62
+
return self.handleEvent(ctx, event);
63
+
}
64
+
65
+
pub fn handleEvent(self: *TextField, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
66
+
switch (event) {
67
+
.focus_out, .focus_in => ctx.redraw = true,
68
+
.key_press => |key| {
69
+
if (key.matches(Key.backspace, .{})) {
70
+
self.deleteBeforeCursor();
71
+
return self.checkChanged(ctx);
72
+
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
73
+
self.deleteAfterCursor();
74
+
return self.checkChanged(ctx);
75
+
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
76
+
self.cursorLeft();
77
+
return ctx.consumeAndRedraw();
78
+
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
79
+
self.cursorRight();
80
+
return ctx.consumeAndRedraw();
81
+
} else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) {
82
+
self.buf.moveGapLeft(self.buf.firstHalf().len);
83
+
return ctx.consumeAndRedraw();
84
+
} else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) {
85
+
self.buf.moveGapRight(self.buf.secondHalf().len);
86
+
return ctx.consumeAndRedraw();
87
+
} else if (key.matches('k', .{ .ctrl = true })) {
88
+
self.deleteToEnd();
89
+
return self.checkChanged(ctx);
90
+
} else if (key.matches('u', .{ .ctrl = true })) {
91
+
self.deleteToStart();
92
+
return self.checkChanged(ctx);
93
+
} else if (key.matches('b', .{ .alt = true }) or key.matches(Key.left, .{ .alt = true })) {
94
+
self.moveBackwardWordwise();
95
+
return ctx.consumeAndRedraw();
96
+
} else if (key.matches('f', .{ .alt = true }) or key.matches(Key.right, .{ .alt = true })) {
97
+
self.moveForwardWordwise();
98
+
return ctx.consumeAndRedraw();
99
+
} else if (key.matches('w', .{ .ctrl = true }) or key.matches(Key.backspace, .{ .alt = true })) {
100
+
self.deleteWordBefore();
101
+
return self.checkChanged(ctx);
102
+
} else if (key.matches('d', .{ .alt = true })) {
103
+
self.deleteWordAfter();
104
+
return self.checkChanged(ctx);
105
+
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
106
+
if (self.onSubmit) |onSubmit| {
107
+
const value = try self.toOwnedSlice();
108
+
// Get a ref to the allocator in case onSubmit deinits the TextField
109
+
const allocator = self.buf.allocator;
110
+
defer allocator.free(value);
111
+
try onSubmit(self.userdata, ctx, value);
112
+
return ctx.consumeAndRedraw();
113
+
}
114
+
} else if (key.text) |text| {
115
+
try self.insertSliceAtCursor(text);
116
+
return self.checkChanged(ctx);
117
+
}
118
+
},
119
+
else => {},
120
+
}
121
+
}
122
+
123
+
fn checkChanged(self: *TextField, ctx: *vxfw.EventContext) anyerror!void {
124
+
ctx.consumeAndRedraw();
125
+
const onChange = self.onChange orelse return;
126
+
const new = try self.buf.dupe();
127
+
defer {
128
+
self.buf.allocator.free(self.previous_val);
129
+
self.previous_val = new;
130
+
}
131
+
if (std.mem.eql(u8, new, self.previous_val)) return;
132
+
try onChange(self.userdata, ctx, new);
133
+
}
134
+
135
+
/// insert text at the cursor position
136
+
pub fn insertSliceAtCursor(self: *TextField, data: []const u8) std.mem.Allocator.Error!void {
137
+
var iter = unicode.graphemeIterator(data);
138
+
while (iter.next()) |text| {
139
+
try self.buf.insertSliceAtCursor(text.bytes(data));
140
+
}
141
+
}
142
+
143
+
pub fn sliceToCursor(self: *TextField, buf: []u8) []const u8 {
144
+
assert(buf.len >= self.buf.cursor);
145
+
@memcpy(buf[0..self.buf.cursor], self.buf.firstHalf());
146
+
return buf[0..self.buf.cursor];
147
+
}
148
+
149
+
/// calculates the display width from the draw_offset to the cursor
150
+
pub fn widthToCursor(self: *TextField, ctx: vxfw.DrawContext) u16 {
151
+
var width: u16 = 0;
152
+
const first_half = self.buf.firstHalf();
153
+
var first_iter = unicode.graphemeIterator(first_half);
154
+
var i: usize = 0;
155
+
while (first_iter.next()) |grapheme| {
156
+
defer i += 1;
157
+
if (i < self.draw_offset) {
158
+
continue;
159
+
}
160
+
const g = grapheme.bytes(first_half);
161
+
width += @intCast(ctx.stringWidth(g));
162
+
}
163
+
return width;
164
+
}
165
+
166
+
pub fn cursorLeft(self: *TextField) void {
167
+
// We need to find the size of the last grapheme in the first half
168
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
169
+
var len: usize = 0;
170
+
while (iter.next()) |grapheme| {
171
+
len = grapheme.len;
172
+
}
173
+
self.buf.moveGapLeft(len);
174
+
}
175
+
176
+
pub fn cursorRight(self: *TextField) void {
177
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
178
+
const grapheme = iter.next() orelse return;
179
+
self.buf.moveGapRight(grapheme.len);
180
+
}
181
+
182
+
pub fn graphemesBeforeCursor(self: *const TextField) u16 {
183
+
const first_half = self.buf.firstHalf();
184
+
var first_iter = unicode.graphemeIterator(first_half);
185
+
var i: u16 = 0;
186
+
while (first_iter.next()) |_| {
187
+
i += 1;
188
+
}
189
+
return i;
190
+
}
191
+
192
+
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
193
+
const self: *TextField = @ptrCast(@alignCast(ptr));
194
+
return self.draw(ctx);
195
+
}
196
+
197
+
pub fn draw(self: *TextField, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
198
+
std.debug.assert(ctx.max.width != null);
199
+
const max_width = ctx.max.width.?;
200
+
if (max_width != self.prev_width) {
201
+
self.prev_width = max_width;
202
+
self.draw_offset = 0;
203
+
self.prev_cursor_col = 0;
204
+
}
205
+
// Create a surface with max width and a minimum height of 1.
206
+
var surface = try vxfw.Surface.init(
207
+
ctx.arena,
208
+
self.widget(),
209
+
.{ .width = max_width, .height = @max(ctx.min.height, 1) },
210
+
);
211
+
212
+
const base: vaxis.Cell = .{ .style = self.style };
213
+
@memset(surface.buffer, base);
214
+
const style = self.style;
215
+
const cursor_idx = self.graphemesBeforeCursor();
216
+
if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx;
217
+
if (max_width == 0) return surface;
218
+
while (true) {
219
+
const width = self.widthToCursor(ctx);
220
+
if (width >= max_width) {
221
+
self.draw_offset +|= width - max_width + 1;
222
+
continue;
223
+
} else break;
224
+
}
225
+
226
+
self.prev_cursor_idx = cursor_idx;
227
+
self.prev_cursor_col = 0;
228
+
229
+
const first_half = self.buf.firstHalf();
230
+
var first_iter = unicode.graphemeIterator(first_half);
231
+
var col: u16 = 0;
232
+
var i: u16 = 0;
233
+
while (first_iter.next()) |grapheme| {
234
+
if (i < self.draw_offset) {
235
+
i += 1;
236
+
continue;
237
+
}
238
+
const g = grapheme.bytes(first_half);
239
+
const w: u8 = @intCast(ctx.stringWidth(g));
240
+
if (col + w >= max_width) {
241
+
surface.writeCell(max_width - 1, 0, .{
242
+
.char = ellipsis,
243
+
.style = style,
244
+
});
245
+
break;
246
+
}
247
+
surface.writeCell(@intCast(col), 0, .{
248
+
.char = .{
249
+
.grapheme = g,
250
+
.width = w,
251
+
},
252
+
.style = style,
253
+
});
254
+
col += w;
255
+
i += 1;
256
+
if (i == cursor_idx) self.prev_cursor_col = col;
257
+
}
258
+
const second_half = self.buf.secondHalf();
259
+
var second_iter = unicode.graphemeIterator(second_half);
260
+
while (second_iter.next()) |grapheme| {
261
+
if (i < self.draw_offset) {
262
+
i += 1;
263
+
continue;
264
+
}
265
+
const g = grapheme.bytes(second_half);
266
+
const w: u8 = @intCast(ctx.stringWidth(g));
267
+
if (col + w > max_width) {
268
+
surface.writeCell(max_width - 1, 0, .{
269
+
.char = ellipsis,
270
+
.style = style,
271
+
});
272
+
break;
273
+
}
274
+
surface.writeCell(@intCast(col), 0, .{
275
+
.char = .{
276
+
.grapheme = g,
277
+
.width = w,
278
+
},
279
+
.style = style,
280
+
});
281
+
col += w;
282
+
i += 1;
283
+
if (i == cursor_idx) self.prev_cursor_col = col;
284
+
}
285
+
if (self.draw_offset > 0) {
286
+
surface.writeCell(0, 0, .{
287
+
.char = ellipsis,
288
+
.style = style,
289
+
});
290
+
}
291
+
surface.cursor = .{ .col = @intCast(self.prev_cursor_col), .row = 0 };
292
+
return surface;
293
+
// win.showCursor(self.prev_cursor_col, 0);
294
+
}
295
+
296
+
pub fn clearAndFree(self: *TextField) void {
297
+
self.buf.clearAndFree();
298
+
self.reset();
299
+
}
300
+
301
+
pub fn clearRetainingCapacity(self: *TextField) void {
302
+
self.buf.clearRetainingCapacity();
303
+
self.reset();
304
+
}
305
+
306
+
pub fn toOwnedSlice(self: *TextField) ![]const u8 {
307
+
defer self.reset();
308
+
return self.buf.toOwnedSlice();
309
+
}
310
+
311
+
pub fn reset(self: *TextField) void {
312
+
self.draw_offset = 0;
313
+
self.prev_cursor_col = 0;
314
+
self.prev_cursor_idx = 0;
315
+
}
316
+
317
+
// returns the number of bytes before the cursor
318
+
pub fn byteOffsetToCursor(self: TextField) usize {
319
+
return self.buf.cursor;
320
+
}
321
+
322
+
pub fn deleteToEnd(self: *TextField) void {
323
+
self.buf.growGapRight(self.buf.secondHalf().len);
324
+
}
325
+
326
+
pub fn deleteToStart(self: *TextField) void {
327
+
self.buf.growGapLeft(self.buf.cursor);
328
+
}
329
+
330
+
pub fn deleteBeforeCursor(self: *TextField) void {
331
+
// We need to find the size of the last grapheme in the first half
332
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
333
+
var len: usize = 0;
334
+
while (iter.next()) |grapheme| {
335
+
len = grapheme.len;
336
+
}
337
+
self.buf.growGapLeft(len);
338
+
}
339
+
340
+
pub fn deleteAfterCursor(self: *TextField) void {
341
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
342
+
const grapheme = iter.next() orelse return;
343
+
self.buf.growGapRight(grapheme.len);
344
+
}
345
+
346
+
/// Moves the cursor backward by words. If the character before the cursor is a space, the cursor is
347
+
/// positioned just after the next previous space
348
+
pub fn moveBackwardWordwise(self: *TextField) void {
349
+
const trimmed = std.mem.trimRight(u8, self.buf.firstHalf(), " ");
350
+
const idx = if (std.mem.lastIndexOfScalar(u8, trimmed, ' ')) |last|
351
+
last + 1
352
+
else
353
+
0;
354
+
self.buf.moveGapLeft(self.buf.cursor - idx);
355
+
}
356
+
357
+
pub fn moveForwardWordwise(self: *TextField) void {
358
+
const second_half = self.buf.secondHalf();
359
+
var i: usize = 0;
360
+
while (i < second_half.len and second_half[i] == ' ') : (i += 1) {}
361
+
const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len;
362
+
self.buf.moveGapRight(idx);
363
+
}
364
+
365
+
pub fn deleteWordBefore(self: *TextField) void {
366
+
// Store current cursor position. Move one word backward. Delete after the cursor the bytes we
367
+
// moved
368
+
const pre = self.buf.cursor;
369
+
self.moveBackwardWordwise();
370
+
self.buf.growGapRight(pre - self.buf.cursor);
371
+
}
372
+
373
+
pub fn deleteWordAfter(self: *TextField) void {
374
+
// Store current cursor position. Move one word backward. Delete after the cursor the bytes we
375
+
// moved
376
+
const second_half = self.buf.secondHalf();
377
+
var i: usize = 0;
378
+
while (i < second_half.len and second_half[i] == ' ') : (i += 1) {}
379
+
const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len;
380
+
self.buf.growGapRight(idx);
381
+
}
382
+
383
+
test "sliceToCursor" {
384
+
var input = init(std.testing.allocator);
385
+
defer input.deinit();
386
+
try input.insertSliceAtCursor("hello, world");
387
+
input.cursorLeft();
388
+
input.cursorLeft();
389
+
input.cursorLeft();
390
+
var buf: [32]u8 = undefined;
391
+
try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf));
392
+
input.cursorRight();
393
+
try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf));
394
+
}
395
+
396
+
pub const Buffer = struct {
397
+
allocator: std.mem.Allocator,
398
+
buffer: []u8,
399
+
cursor: usize,
400
+
gap_size: usize,
401
+
402
+
pub fn init(allocator: std.mem.Allocator) Buffer {
403
+
return .{
404
+
.allocator = allocator,
405
+
.buffer = &.{},
406
+
.cursor = 0,
407
+
.gap_size = 0,
408
+
};
409
+
}
410
+
411
+
pub fn deinit(self: *Buffer) void {
412
+
self.allocator.free(self.buffer);
413
+
}
414
+
415
+
pub fn firstHalf(self: Buffer) []const u8 {
416
+
return self.buffer[0..self.cursor];
417
+
}
418
+
419
+
pub fn secondHalf(self: Buffer) []const u8 {
420
+
return self.buffer[self.cursor + self.gap_size ..];
421
+
}
422
+
423
+
pub fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void {
424
+
// Always grow by 512 bytes
425
+
const new_size = self.buffer.len + n + 512;
426
+
// Allocate the new memory
427
+
const new_memory = try self.allocator.alloc(u8, new_size);
428
+
// Copy the first half
429
+
@memcpy(new_memory[0..self.cursor], self.firstHalf());
430
+
// Copy the second half
431
+
const second_half = self.secondHalf();
432
+
@memcpy(new_memory[new_size - second_half.len ..], second_half);
433
+
self.allocator.free(self.buffer);
434
+
self.buffer = new_memory;
435
+
self.gap_size = new_size - second_half.len - self.cursor;
436
+
}
437
+
438
+
pub fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void {
439
+
if (slice.len == 0) return;
440
+
if (self.gap_size <= slice.len) try self.grow(slice.len);
441
+
@memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice);
442
+
self.cursor += slice.len;
443
+
self.gap_size -= slice.len;
444
+
}
445
+
446
+
/// Move the gap n bytes to the left
447
+
pub fn moveGapLeft(self: *Buffer, n: usize) void {
448
+
const new_idx = self.cursor -| n;
449
+
const dst = self.buffer[new_idx + self.gap_size ..];
450
+
const src = self.buffer[new_idx..self.cursor];
451
+
std.mem.copyForwards(u8, dst, src);
452
+
self.cursor = new_idx;
453
+
}
454
+
455
+
pub fn moveGapRight(self: *Buffer, n: usize) void {
456
+
const new_idx = self.cursor + n;
457
+
const dst = self.buffer[self.cursor..];
458
+
const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size];
459
+
std.mem.copyForwards(u8, dst, src);
460
+
self.cursor = new_idx;
461
+
}
462
+
463
+
/// grow the gap by moving the cursor n bytes to the left
464
+
pub fn growGapLeft(self: *Buffer, n: usize) void {
465
+
// gap grows by the delta
466
+
self.gap_size += n;
467
+
self.cursor -|= n;
468
+
}
469
+
470
+
/// grow the gap by removing n bytes after the cursor
471
+
pub fn growGapRight(self: *Buffer, n: usize) void {
472
+
self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor);
473
+
}
474
+
475
+
pub fn clearAndFree(self: *Buffer) void {
476
+
self.cursor = 0;
477
+
self.allocator.free(self.buffer);
478
+
self.buffer = &.{};
479
+
self.gap_size = 0;
480
+
}
481
+
482
+
pub fn clearRetainingCapacity(self: *Buffer) void {
483
+
self.cursor = 0;
484
+
self.gap_size = self.buffer.len;
485
+
}
486
+
487
+
pub fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 {
488
+
const slice = try self.dupe();
489
+
self.clearAndFree();
490
+
return slice;
491
+
}
492
+
493
+
pub fn realLength(self: *const Buffer) usize {
494
+
return self.firstHalf().len + self.secondHalf().len;
495
+
}
496
+
497
+
pub fn dupe(self: *const Buffer) std.mem.Allocator.Error![]const u8 {
498
+
const first_half = self.firstHalf();
499
+
const second_half = self.secondHalf();
500
+
const buf = try self.allocator.alloc(u8, first_half.len + second_half.len);
501
+
@memcpy(buf[0..first_half.len], first_half);
502
+
@memcpy(buf[first_half.len..], second_half);
503
+
return buf;
504
+
}
505
+
};
506
+
507
+
test "TextField.zig: Buffer" {
508
+
var gap_buf = Buffer.init(std.testing.allocator);
509
+
defer gap_buf.deinit();
510
+
511
+
try gap_buf.insertSliceAtCursor("abc");
512
+
try std.testing.expectEqualStrings("abc", gap_buf.firstHalf());
513
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
514
+
515
+
gap_buf.moveGapLeft(1);
516
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
517
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
518
+
519
+
try gap_buf.insertSliceAtCursor(" ");
520
+
try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf());
521
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
522
+
523
+
gap_buf.growGapLeft(1);
524
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
525
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
526
+
try std.testing.expectEqual(2, gap_buf.cursor);
527
+
528
+
gap_buf.growGapRight(1);
529
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
530
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
531
+
try std.testing.expectEqual(2, gap_buf.cursor);
532
+
}
533
+
534
+
test TextField {
535
+
// Boiler plate draw context init
536
+
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
537
+
defer arena.deinit();
538
+
vxfw.DrawContext.init(.unicode);
539
+
540
+
// Create some object which reacts to text field changes
541
+
const Foo = struct {
542
+
allocator: std.mem.Allocator,
543
+
text: []const u8,
544
+
545
+
fn onChange(ptr: ?*anyopaque, ctx: *vxfw.EventContext, str: []const u8) anyerror!void {
546
+
const foo: *@This() = @ptrCast(@alignCast(ptr));
547
+
foo.text = try foo.allocator.dupe(u8, str);
548
+
ctx.consumeAndRedraw();
549
+
}
550
+
};
551
+
var foo: Foo = .{ .text = "", .allocator = arena.allocator() };
552
+
553
+
// Text field expands to the width, so it can't be null. It is always 1 line tall
554
+
const draw_ctx: vxfw.DrawContext = .{
555
+
.arena = arena.allocator(),
556
+
.min = .{},
557
+
.max = .{ .width = 8, .height = 1 },
558
+
.cell_size = .{ .width = 10, .height = 20 },
559
+
};
560
+
_ = draw_ctx;
561
+
562
+
var ctx: vxfw.EventContext = .{
563
+
.alloc = arena.allocator(),
564
+
.cmds = .empty,
565
+
};
566
+
567
+
// Enough boiler plate...Create the text field
568
+
var text_field = TextField.init(std.testing.allocator);
569
+
defer text_field.deinit();
570
+
text_field.onChange = Foo.onChange;
571
+
text_field.onSubmit = Foo.onChange;
572
+
text_field.userdata = &foo;
573
+
574
+
const tf_widget = text_field.widget();
575
+
// Send some key events to the widget
576
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'H', .text = "H" } });
577
+
// The foo object stores the last text that we saw from an onChange call
578
+
try std.testing.expectEqualStrings("H", foo.text);
579
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'e', .text = "e" } });
580
+
try std.testing.expectEqualStrings("He", foo.text);
581
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } });
582
+
try std.testing.expectEqualStrings("Hel", foo.text);
583
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'l', .text = "l" } });
584
+
try std.testing.expectEqualStrings("Hell", foo.text);
585
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = 'o', .text = "o" } });
586
+
try std.testing.expectEqualStrings("Hello", foo.text);
587
+
588
+
// An arrow moves the cursor. The text doesn't change
589
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = vaxis.Key.left } });
590
+
try std.testing.expectEqualStrings("Hello", foo.text);
591
+
592
+
try tf_widget.handleEvent(&ctx, .{ .key_press = .{ .codepoint = '_', .text = "_" } });
593
+
try std.testing.expectEqualStrings("Hell_o", foo.text);
594
+
}
595
+
596
+
test "refAllDecls" {
597
+
std.testing.refAllDecls(@This());
598
+
}
+584
src/vxfw/vxfw.zig
+584
src/vxfw/vxfw.zig
···
···
1
+
const std = @import("std");
2
+
const vaxis = @import("../main.zig");
3
+
const uucode = @import("uucode");
4
+
5
+
const testing = std.testing;
6
+
7
+
const assert = std.debug.assert;
8
+
9
+
const Allocator = std.mem.Allocator;
10
+
11
+
pub const App = @import("App.zig");
12
+
13
+
// Widgets
14
+
pub const Border = @import("Border.zig");
15
+
pub const Button = @import("Button.zig");
16
+
pub const Center = @import("Center.zig");
17
+
pub const FlexColumn = @import("FlexColumn.zig");
18
+
pub const FlexRow = @import("FlexRow.zig");
19
+
pub const ListView = @import("ListView.zig");
20
+
pub const Padding = @import("Padding.zig");
21
+
pub const RichText = @import("RichText.zig");
22
+
pub const ScrollView = @import("ScrollView.zig");
23
+
pub const ScrollBars = @import("ScrollBars.zig");
24
+
pub const SizedBox = @import("SizedBox.zig");
25
+
pub const SplitView = @import("SplitView.zig");
26
+
pub const Spinner = @import("Spinner.zig");
27
+
pub const Text = @import("Text.zig");
28
+
pub const TextField = @import("TextField.zig");
29
+
30
+
pub const CommandList = std.ArrayList(Command);
31
+
32
+
pub const UserEvent = struct {
33
+
name: []const u8,
34
+
data: ?*const anyopaque = null,
35
+
};
36
+
37
+
pub const Event = union(enum) {
38
+
key_press: vaxis.Key,
39
+
key_release: vaxis.Key,
40
+
mouse: vaxis.Mouse,
41
+
focus_in, // window has gained focus
42
+
focus_out, // window has lost focus
43
+
paste_start, // bracketed paste start
44
+
paste_end, // bracketed paste end
45
+
paste: []const u8, // osc 52 paste, caller must free
46
+
color_report: vaxis.Color.Report, // osc 4, 10, 11, 12 response
47
+
color_scheme: vaxis.Color.Scheme, // light / dark OS theme changes
48
+
winsize: vaxis.Winsize, // the window size has changed. This event is always sent when the loop is started
49
+
app: UserEvent, // A custom event from the app
50
+
tick, // An event from a Tick command
51
+
init, // sent when the application starts
52
+
mouse_leave, // The mouse has left the widget
53
+
mouse_enter, // The mouse has enterred the widget
54
+
};
55
+
56
+
pub const Tick = struct {
57
+
deadline_ms: i64,
58
+
widget: Widget,
59
+
60
+
pub fn lessThan(_: void, lhs: Tick, rhs: Tick) bool {
61
+
return lhs.deadline_ms > rhs.deadline_ms;
62
+
}
63
+
64
+
pub fn in(ms: u32, widget: Widget) Command {
65
+
const now = std.time.milliTimestamp();
66
+
return .{ .tick = .{
67
+
.deadline_ms = now + ms,
68
+
.widget = widget,
69
+
} };
70
+
}
71
+
};
72
+
73
+
pub const Command = union(enum) {
74
+
/// Callback the event with a tick event at the specified deadlline
75
+
tick: Tick,
76
+
/// Change the mouse shape. This also has an implicit redraw
77
+
set_mouse_shape: vaxis.Mouse.Shape,
78
+
/// Request that this widget receives focus
79
+
request_focus: Widget,
80
+
81
+
/// Try to copy the provided text to the host clipboard. Uses OSC 52. Silently fails if terminal
82
+
/// doesn't support OSC 52
83
+
copy_to_clipboard: []const u8,
84
+
85
+
/// Set the title of the terminal
86
+
set_title: []const u8,
87
+
88
+
/// Queue a refresh of the entire screen. Implicitly sets redraw
89
+
queue_refresh,
90
+
91
+
/// Send a system notification
92
+
notify: struct {
93
+
title: ?[]const u8,
94
+
body: []const u8,
95
+
},
96
+
97
+
query_color: vaxis.Cell.Color.Kind,
98
+
};
99
+
100
+
pub const EventContext = struct {
101
+
phase: Phase = .at_target,
102
+
alloc: Allocator,
103
+
cmds: CommandList,
104
+
105
+
/// The event was handled, do not pass it on
106
+
consume_event: bool = false,
107
+
/// Tells the event loop to redraw the UI
108
+
redraw: bool = true,
109
+
/// Quit the application
110
+
quit: bool = false,
111
+
112
+
pub const Phase = enum {
113
+
capturing,
114
+
at_target,
115
+
bubbling,
116
+
};
117
+
118
+
pub fn addCmd(self: *EventContext, cmd: Command) Allocator.Error!void {
119
+
try self.cmds.append(self.alloc, cmd);
120
+
}
121
+
122
+
pub fn tick(self: *EventContext, ms: u32, widget: Widget) Allocator.Error!void {
123
+
try self.addCmd(Tick.in(ms, widget));
124
+
}
125
+
126
+
pub fn consumeAndRedraw(self: *EventContext) void {
127
+
self.consume_event = true;
128
+
self.redraw = true;
129
+
}
130
+
131
+
pub fn consumeEvent(self: *EventContext) void {
132
+
self.consume_event = true;
133
+
}
134
+
135
+
pub fn setMouseShape(self: *EventContext, shape: vaxis.Mouse.Shape) Allocator.Error!void {
136
+
try self.addCmd(.{ .set_mouse_shape = shape });
137
+
self.redraw = true;
138
+
}
139
+
140
+
pub fn requestFocus(self: *EventContext, widget: Widget) Allocator.Error!void {
141
+
try self.addCmd(.{ .request_focus = widget });
142
+
}
143
+
144
+
/// Copy content to clipboard.
145
+
/// content is duplicated using self.alloc.
146
+
/// Caller retains ownership of their copy of content.
147
+
pub fn copyToClipboard(self: *EventContext, content: []const u8) Allocator.Error!void {
148
+
try self.addCmd(.{ .copy_to_clipboard = try self.alloc.dupe(u8, content) });
149
+
}
150
+
151
+
/// Set window title.
152
+
/// title is duplicated using self.alloc.
153
+
/// Caller retains ownership of their copy of title.
154
+
pub fn setTitle(self: *EventContext, title: []const u8) Allocator.Error!void {
155
+
try self.addCmd(.{ .set_title = try self.alloc.dupe(u8, title) });
156
+
}
157
+
158
+
pub fn queueRefresh(self: *EventContext) Allocator.Error!void {
159
+
try self.addCmd(.queue_refresh);
160
+
self.redraw = true;
161
+
}
162
+
163
+
/// Send a system notification. This function dupes title and body using it's own allocator.
164
+
/// They will be freed once the notification has been sent
165
+
pub fn sendNotification(
166
+
self: *EventContext,
167
+
maybe_title: ?[]const u8,
168
+
body: []const u8,
169
+
) Allocator.Error!void {
170
+
const alloc = self.alloc;
171
+
if (maybe_title) |title| {
172
+
return self.addCmd(.{ .notify = .{
173
+
.title = try alloc.dupe(u8, title),
174
+
.body = try alloc.dupe(u8, body),
175
+
} });
176
+
}
177
+
return self.addCmd(.{ .notify = .{
178
+
.title = null,
179
+
.body = try alloc.dupe(u8, body),
180
+
} });
181
+
}
182
+
183
+
pub fn queryColor(self: *EventContext, kind: vaxis.Cell.Color.Kind) Allocator.Error!void {
184
+
try self.addCmd(.{ .query_color = kind });
185
+
}
186
+
};
187
+
188
+
pub const DrawContext = struct {
189
+
// Allocator backed by an arena. Widgets do not need to free their own resources, they will be
190
+
// freed after rendering
191
+
arena: std.mem.Allocator,
192
+
// Constraints
193
+
min: Size,
194
+
max: MaxSize,
195
+
196
+
// Size of a single cell, in pixels
197
+
cell_size: Size,
198
+
199
+
// Unicode stuff
200
+
var width_method: vaxis.gwidth.Method = .unicode;
201
+
202
+
pub fn init(method: vaxis.gwidth.Method) void {
203
+
DrawContext.width_method = method;
204
+
}
205
+
206
+
pub fn stringWidth(_: DrawContext, str: []const u8) usize {
207
+
return vaxis.gwidth.gwidth(
208
+
str,
209
+
DrawContext.width_method,
210
+
);
211
+
}
212
+
213
+
pub fn graphemeIterator(_: DrawContext, str: []const u8) vaxis.unicode.GraphemeIterator {
214
+
return vaxis.unicode.graphemeIterator(str);
215
+
}
216
+
217
+
pub fn withConstraints(self: DrawContext, min: Size, max: MaxSize) DrawContext {
218
+
return .{
219
+
.arena = self.arena,
220
+
.min = min,
221
+
.max = max,
222
+
.cell_size = self.cell_size,
223
+
};
224
+
}
225
+
};
226
+
227
+
pub const Size = struct {
228
+
width: u16 = 0,
229
+
height: u16 = 0,
230
+
};
231
+
232
+
pub const MaxSize = struct {
233
+
width: ?u16 = null,
234
+
height: ?u16 = null,
235
+
236
+
/// Returns true if the row would fall outside of this height. A null height value is infinite
237
+
/// and always returns false
238
+
pub fn outsideHeight(self: MaxSize, row: u16) bool {
239
+
const max = self.height orelse return false;
240
+
return row >= max;
241
+
}
242
+
243
+
/// Returns true if the col would fall outside of this width. A null width value is infinite
244
+
/// and always returns false
245
+
pub fn outsideWidth(self: MaxSize, col: u16) bool {
246
+
const max = self.width orelse return false;
247
+
return col >= max;
248
+
}
249
+
250
+
/// Asserts that neither height nor width are null
251
+
pub fn size(self: MaxSize) Size {
252
+
assert(self.width != null);
253
+
assert(self.height != null);
254
+
return .{
255
+
.width = self.width.?,
256
+
.height = self.height.?,
257
+
};
258
+
}
259
+
260
+
pub fn fromSize(other: Size) MaxSize {
261
+
return .{
262
+
.width = other.width,
263
+
.height = other.height,
264
+
};
265
+
}
266
+
};
267
+
268
+
/// The Widget interface
269
+
pub const Widget = struct {
270
+
userdata: *anyopaque,
271
+
captureHandler: ?*const fn (userdata: *anyopaque, ctx: *EventContext, event: Event) anyerror!void = null,
272
+
eventHandler: ?*const fn (userdata: *anyopaque, ctx: *EventContext, event: Event) anyerror!void = null,
273
+
drawFn: *const fn (userdata: *anyopaque, ctx: DrawContext) Allocator.Error!Surface,
274
+
275
+
pub fn captureEvent(self: Widget, ctx: *EventContext, event: Event) anyerror!void {
276
+
if (self.captureHandler) |handle| {
277
+
return handle(self.userdata, ctx, event);
278
+
}
279
+
}
280
+
281
+
pub fn handleEvent(self: Widget, ctx: *EventContext, event: Event) anyerror!void {
282
+
if (self.eventHandler) |handle| {
283
+
return handle(self.userdata, ctx, event);
284
+
}
285
+
}
286
+
287
+
pub fn draw(self: Widget, ctx: DrawContext) Allocator.Error!Surface {
288
+
return self.drawFn(self.userdata, ctx);
289
+
}
290
+
291
+
/// Returns true if the Widgets point to the same widget instance. To be considered the same,
292
+
/// the userdata and drawFn fields must point to the same values in both widgets
293
+
pub fn eql(self: Widget, other: Widget) bool {
294
+
return @intFromPtr(self.userdata) == @intFromPtr(other.userdata) and
295
+
@intFromPtr(self.drawFn) == @intFromPtr(other.drawFn);
296
+
}
297
+
};
298
+
299
+
pub const FlexItem = struct {
300
+
widget: Widget,
301
+
/// A value of zero means the child will have it's inherent size. Any value greater than zero
302
+
/// and the remaining space will be proportioned to each item
303
+
flex: u8 = 1,
304
+
305
+
pub fn init(child: Widget, flex: u8) FlexItem {
306
+
return .{ .widget = child, .flex = flex };
307
+
}
308
+
};
309
+
310
+
pub const Point = struct {
311
+
row: u16,
312
+
col: u16,
313
+
};
314
+
315
+
pub const RelativePoint = struct {
316
+
row: i17,
317
+
col: i17,
318
+
};
319
+
320
+
/// Result of a hit test
321
+
pub const HitResult = struct {
322
+
local: Point,
323
+
widget: Widget,
324
+
};
325
+
326
+
pub const CursorState = struct {
327
+
/// Local coordinates
328
+
row: u16,
329
+
/// Local coordinates
330
+
col: u16,
331
+
shape: vaxis.Cell.CursorShape = .default,
332
+
};
333
+
334
+
pub const Surface = struct {
335
+
/// Size of this surface
336
+
size: Size,
337
+
/// The widget this surface belongs to
338
+
widget: Widget,
339
+
340
+
/// Cursor state
341
+
cursor: ?CursorState = null,
342
+
343
+
/// Contents of this surface. Must be len == 0 or len == size.width * size.height
344
+
buffer: []vaxis.Cell,
345
+
346
+
children: []SubSurface,
347
+
348
+
pub fn empty(widget: Widget) Surface {
349
+
return .{
350
+
.size = .{},
351
+
.widget = widget,
352
+
.buffer = &.{},
353
+
.children = &.{},
354
+
};
355
+
}
356
+
357
+
/// Creates a slice of vaxis.Cell's equal to size.width * size.height
358
+
pub fn createBuffer(allocator: Allocator, size: Size) Allocator.Error![]vaxis.Cell {
359
+
const buffer = try allocator.alloc(vaxis.Cell, size.width * size.height);
360
+
@memset(buffer, .{ .default = true });
361
+
return buffer;
362
+
}
363
+
364
+
pub fn init(allocator: Allocator, widget: Widget, size: Size) Allocator.Error!Surface {
365
+
return .{
366
+
.size = size,
367
+
.widget = widget,
368
+
.buffer = try Surface.createBuffer(allocator, size),
369
+
.children = &.{},
370
+
};
371
+
}
372
+
373
+
pub fn initWithChildren(
374
+
allocator: Allocator,
375
+
widget: Widget,
376
+
size: Size,
377
+
children: []SubSurface,
378
+
) Allocator.Error!Surface {
379
+
return .{
380
+
.size = size,
381
+
.widget = widget,
382
+
.buffer = try Surface.createBuffer(allocator, size),
383
+
.children = children,
384
+
};
385
+
}
386
+
387
+
pub fn writeCell(self: Surface, col: u16, row: u16, cell: vaxis.Cell) void {
388
+
if (self.size.width <= col) return;
389
+
if (self.size.height <= row) return;
390
+
const i = (row * self.size.width) + col;
391
+
assert(i < self.buffer.len);
392
+
self.buffer[i] = cell;
393
+
}
394
+
395
+
pub fn readCell(self: Surface, col: usize, row: usize) vaxis.Cell {
396
+
assert(col < self.size.width and row < self.size.height);
397
+
const i = (row * self.size.width) + col;
398
+
assert(i < self.buffer.len);
399
+
return self.buffer[i];
400
+
}
401
+
402
+
/// Creates a new surface of the same width, with the buffer trimmed to a given height
403
+
pub fn trimHeight(self: Surface, height: u16) Surface {
404
+
assert(height <= self.size.height);
405
+
return .{
406
+
.size = .{ .width = self.size.width, .height = height },
407
+
.widget = self.widget,
408
+
.buffer = self.buffer[0 .. self.size.width * height],
409
+
.children = self.children,
410
+
};
411
+
}
412
+
413
+
/// Walks the Surface tree to produce a list of all widgets that intersect Point. Point will
414
+
/// always be translated to local Surface coordinates. Asserts that this Surface does contain Point
415
+
pub fn hitTest(self: Surface, allocator: Allocator, list: *std.ArrayList(HitResult), point: Point) Allocator.Error!void {
416
+
assert(point.col < self.size.width and point.row < self.size.height);
417
+
// Add this widget to the hit list if it has an event or capture handler
418
+
if (self.widget.eventHandler != null or self.widget.captureHandler != null)
419
+
try list.append(allocator, .{ .local = point, .widget = self.widget });
420
+
for (self.children) |child| {
421
+
if (!child.containsPoint(point)) continue;
422
+
const child_point: Point = .{
423
+
.row = @intCast(point.row - child.origin.row),
424
+
.col = @intCast(point.col - child.origin.col),
425
+
};
426
+
try child.surface.hitTest(allocator, list, child_point);
427
+
}
428
+
}
429
+
430
+
/// Copies all cells from Surface to Window
431
+
pub fn render(self: Surface, win: vaxis.Window, focused: Widget) void {
432
+
// render self first
433
+
if (self.buffer.len > 0) {
434
+
assert(self.buffer.len == self.size.width * self.size.height);
435
+
for (self.buffer, 0..) |cell, i| {
436
+
const row = i / self.size.width;
437
+
const col = i % self.size.width;
438
+
win.writeCell(@intCast(col), @intCast(row), cell);
439
+
}
440
+
}
441
+
442
+
if (self.cursor) |cursor| {
443
+
if (self.widget.eql(focused)) {
444
+
win.showCursor(cursor.col, cursor.row);
445
+
win.setCursorShape(cursor.shape);
446
+
}
447
+
}
448
+
449
+
// Sort children by z-index
450
+
std.mem.sort(SubSurface, self.children, {}, SubSurface.lessThan);
451
+
452
+
// for each child, we make a window and render to it
453
+
for (self.children) |child| {
454
+
const child_win = win.child(.{
455
+
.x_off = @intCast(child.origin.col),
456
+
.y_off = @intCast(child.origin.row),
457
+
.width = @intCast(child.surface.size.width),
458
+
.height = @intCast(child.surface.size.height),
459
+
});
460
+
child.surface.render(child_win, focused);
461
+
}
462
+
}
463
+
464
+
/// Returns true if the surface satisfies a set of constraints
465
+
pub fn satisfiesConstraints(self: Surface, min: Size, max: Size) bool {
466
+
return self.size.width < max.width and
467
+
self.size.width > min.width and
468
+
self.size.height < max.height and
469
+
self.size.height > min.height;
470
+
}
471
+
};
472
+
473
+
pub const SubSurface = struct {
474
+
/// Origin relative to parent
475
+
origin: RelativePoint,
476
+
/// This surface
477
+
surface: Surface,
478
+
/// z-index relative to siblings
479
+
z_index: u8 = 0,
480
+
481
+
pub fn lessThan(_: void, lhs: SubSurface, rhs: SubSurface) bool {
482
+
return lhs.z_index < rhs.z_index;
483
+
}
484
+
485
+
/// Returns true if this SubSurface contains Point. Point must be in parent local units
486
+
pub fn containsPoint(self: SubSurface, point: Point) bool {
487
+
return point.col >= self.origin.col and
488
+
point.row >= self.origin.row and
489
+
point.col < (self.origin.col + self.surface.size.width) and
490
+
point.row < (self.origin.row + self.surface.size.height);
491
+
}
492
+
};
493
+
494
+
test {
495
+
std.testing.refAllDecls(@This());
496
+
}
497
+
498
+
test "SubSurface: containsPoint" {
499
+
const surf: SubSurface = .{
500
+
.origin = .{ .row = 2, .col = 2 },
501
+
.surface = .{
502
+
.size = .{ .width = 10, .height = 10 },
503
+
.widget = undefined,
504
+
.children = &.{},
505
+
.buffer = &.{},
506
+
},
507
+
.z_index = 0,
508
+
};
509
+
510
+
try testing.expect(surf.containsPoint(.{ .row = 2, .col = 2 }));
511
+
try testing.expect(surf.containsPoint(.{ .row = 3, .col = 3 }));
512
+
try testing.expect(surf.containsPoint(.{ .row = 11, .col = 11 }));
513
+
514
+
try testing.expect(!surf.containsPoint(.{ .row = 1, .col = 1 }));
515
+
try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 12 }));
516
+
try testing.expect(!surf.containsPoint(.{ .row = 2, .col = 12 }));
517
+
try testing.expect(!surf.containsPoint(.{ .row = 12, .col = 2 }));
518
+
}
519
+
520
+
test "refAllDecls" {
521
+
std.testing.refAllDecls(@This());
522
+
}
523
+
524
+
test "Surface: satisfiesConstraints" {
525
+
const surf: Surface = .{
526
+
.size = .{ .width = 10, .height = 10 },
527
+
.widget = undefined,
528
+
.children = &.{},
529
+
.buffer = &.{},
530
+
};
531
+
532
+
try testing.expect(surf.satisfiesConstraints(.{ .width = 1, .height = 1 }, .{ .width = 20, .height = 20 }));
533
+
try testing.expect(!surf.satisfiesConstraints(.{ .width = 10, .height = 10 }, .{ .width = 20, .height = 20 }));
534
+
try testing.expect(!surf.satisfiesConstraints(.{ .width = 1, .height = 1 }, .{ .width = 10, .height = 10 }));
535
+
}
536
+
537
+
test "All widgets have a doctest and refAllDecls test" {
538
+
// This test goes through every file in src/ and checks that it has a doctest (the filename
539
+
// stripped of ".zig" matches a test name) and a test called "refAllDecls". It makes no
540
+
// guarantees about the quality of the test, but it does ensure it exists which at least makes
541
+
// it easy to fail CI early, or spot bad tests vs non-existant tests
542
+
const excludes = &[_][]const u8{ "vxfw.zig", "App.zig" };
543
+
544
+
var cwd = try std.fs.cwd().openDir("./src/vxfw", .{ .iterate = true });
545
+
var iter = cwd.iterate();
546
+
defer cwd.close();
547
+
outer: while (try iter.next()) |file| {
548
+
if (file.kind != .file) continue;
549
+
for (excludes) |ex| if (std.mem.eql(u8, ex, file.name)) continue :outer;
550
+
551
+
const container_name = if (std.mem.lastIndexOf(u8, file.name, ".zig")) |idx|
552
+
file.name[0..idx]
553
+
else
554
+
continue;
555
+
const data = try cwd.readFileAllocOptions(std.testing.allocator, file.name, 10_000_000, null, .of(u8), 0x00);
556
+
defer std.testing.allocator.free(data);
557
+
var ast = try std.zig.Ast.parse(std.testing.allocator, data, .zig);
558
+
defer ast.deinit(std.testing.allocator);
559
+
560
+
var has_doctest: bool = false;
561
+
var has_refAllDecls: bool = false;
562
+
for (ast.rootDecls()) |root_decl| {
563
+
const decl = ast.nodes.get(@intFromEnum(root_decl));
564
+
switch (decl.tag) {
565
+
.test_decl => {
566
+
const test_name = ast.tokenSlice(decl.main_token + 1);
567
+
if (std.mem.eql(u8, "\"refAllDecls\"", test_name))
568
+
has_refAllDecls = true
569
+
else if (std.mem.eql(u8, container_name, test_name))
570
+
has_doctest = true;
571
+
},
572
+
else => continue,
573
+
}
574
+
}
575
+
if (!has_doctest) {
576
+
std.log.err("file {s} has no doctest", .{file.name});
577
+
return error.TestExpectedDoctest;
578
+
}
579
+
if (!has_refAllDecls) {
580
+
std.log.err("file {s} has no 'refAllDecls' test", .{file.name});
581
+
return error.TestExpectedRefAllDecls;
582
+
}
583
+
}
584
+
}
+5
-5
src/widgets/CodeView.zig
+5
-5
src/widgets/CodeView.zig
···
4
const LineNumbers = vaxis.widgets.LineNumbers;
5
6
pub const DrawOptions = struct {
7
-
highlighted_line: usize = 0,
8
draw_line_numbers: bool = true,
9
-
indentation: usize = 0,
10
};
11
12
pub const Buffer = vaxis.widgets.TextView.Buffer;
···
39
nl.draw(win.child(.{
40
.x_off = 0,
41
.y_off = 0,
42
-
.width = .{ .limit = pad_left },
43
-
.height = .{ .limit = win.height },
44
}), self.scroll_view.scroll.y);
45
}
46
self.drawCode(win.child(.{ .x_off = pad_left }), buffer, opts);
···
98
self.scroll_view.writeCell(win, pos.x, pos.y, cell);
99
} else {
100
self.scroll_view.writeCell(win, pos.x, pos.y, .{
101
-
.char = .{ .grapheme = cluster, .width = width },
102
.style = style,
103
});
104
}
···
4
const LineNumbers = vaxis.widgets.LineNumbers;
5
6
pub const DrawOptions = struct {
7
+
highlighted_line: u16 = 0,
8
draw_line_numbers: bool = true,
9
+
indentation: u16 = 0,
10
};
11
12
pub const Buffer = vaxis.widgets.TextView.Buffer;
···
39
nl.draw(win.child(.{
40
.x_off = 0,
41
.y_off = 0,
42
+
.width = pad_left,
43
+
.height = win.height,
44
}), self.scroll_view.scroll.y);
45
}
46
self.drawCode(win.child(.{ .x_off = pad_left }), buffer, opts);
···
98
self.scroll_view.writeCell(win, pos.x, pos.y, cell);
99
} else {
100
self.scroll_view.writeCell(win, pos.x, pos.y, .{
101
+
.char = .{ .grapheme = cluster, .width = @intCast(width) },
102
.style = style,
103
});
104
}
+3
-3
src/widgets/LineNumbers.zig
+3
-3
src/widgets/LineNumbers.zig
···
12
return (v / (std.math.powi(usize, 10, n) catch unreachable)) % 10;
13
}
14
15
-
pub fn numDigits(v: usize) usize {
16
return switch (v) {
17
0...9 => 1,
18
10...99 => 2,
···
35
const num_digits = numDigits(line);
36
for (0..num_digits) |i| {
37
const digit = extractDigit(line, i);
38
-
win.writeCell(win.width -| (i + 2), line -| (y_scroll +| 1), .{
39
.char = .{
40
.width = 1,
41
.grapheme = digits[digit .. digit + 1],
···
45
}
46
if (highlighted) {
47
for (num_digits + 1..win.width) |i| {
48
-
win.writeCell(i, line -| (y_scroll +| 1), .{
49
.style = if (highlighted) self.highlighted_style else self.style,
50
});
51
}
···
12
return (v / (std.math.powi(usize, 10, n) catch unreachable)) % 10;
13
}
14
15
+
pub fn numDigits(v: usize) u8 {
16
return switch (v) {
17
0...9 => 1,
18
10...99 => 2,
···
35
const num_digits = numDigits(line);
36
for (0..num_digits) |i| {
37
const digit = extractDigit(line, i);
38
+
win.writeCell(@intCast(win.width -| (i + 2)), @intCast(line -| (y_scroll +| 1)), .{
39
.char = .{
40
.width = 1,
41
.grapheme = digits[digit .. digit + 1],
···
45
}
46
if (highlighted) {
47
for (num_digits + 1..win.width) |i| {
48
+
win.writeCell(@intCast(i), @intCast(line -| (y_scroll +| 1)), .{
49
.style = if (highlighted) self.highlighted_style else self.style,
50
});
51
}
+6
-6
src/widgets/ScrollView.zig
+6
-6
src/widgets/ScrollView.zig
···
65
};
66
const bg = parent.child(.{
67
.x_off = parent.width -| opts.character.width,
68
-
.width = .{ .limit = opts.character.width },
69
-
.height = .{ .limit = parent.height },
70
});
71
bg.fill(.{ .char = opts.character, .style = opts.bg });
72
vbar.draw(bg);
···
115
pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
116
const b = self.bounds(parent);
117
if (!b.inside(col, row)) return;
118
-
const win = parent.child(.{ .width = .{ .limit = b.x2 - b.x1 }, .height = .{ .limit = b.y2 - b.y1 } });
119
-
win.writeCell(col -| self.scroll.x, row -| self.scroll.y, cell);
120
}
121
122
/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
123
pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
124
const b = self.bounds(parent);
125
if (!b.inside(col, row)) return;
126
-
const win = parent.child(.{ .width = .{ .limit = b.width }, .height = .{ .limit = b.height } });
127
-
return win.readCell(col -| self.scroll.x, row -| self.scroll.y);
128
}
···
65
};
66
const bg = parent.child(.{
67
.x_off = parent.width -| opts.character.width,
68
+
.width = opts.character.width,
69
+
.height = parent.height,
70
});
71
bg.fill(.{ .char = opts.character, .style = opts.bg });
72
vbar.draw(bg);
···
115
pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
116
const b = self.bounds(parent);
117
if (!b.inside(col, row)) return;
118
+
const win = parent.child(.{ .width = @intCast(b.x2 - b.x1), .height = @intCast(b.y2 - b.y1) });
119
+
win.writeCell(@intCast(col -| self.scroll.x), @intCast(row -| self.scroll.y), cell);
120
}
121
122
/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
123
pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
124
const b = self.bounds(parent);
125
if (!b.inside(col, row)) return;
126
+
const win = parent.child(.{ .width = @intCast(b.x2 - b.x1), .height = @intCast(b.y2 - b.y1) });
127
+
return win.readCell(@intCast(col -| self.scroll.x), @intCast(row -| self.scroll.y));
128
}
+1
-1
src/widgets/Scrollbar.zig
+1
-1
src/widgets/Scrollbar.zig
+343
-95
src/widgets/Table.zig
+343
-95
src/widgets/Table.zig
···
8
9
/// Table Context for maintaining state and drawing Tables with `drawTable()`.
10
pub const TableContext = struct {
11
-
/// Current selected Row of the Table.
12
-
row: usize = 0,
13
-
/// Current selected Column of the Table.
14
-
col: usize = 0,
15
/// Starting point within the Data List.
16
-
start: usize = 0,
17
18
/// Active status of the Table.
19
active: bool = false,
20
21
-
/// The Background Color for Selected Rows and Column Headers.
22
selected_bg: vaxis.Cell.Color,
23
/// First Column Header Background Color
24
hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } },
25
/// Second Column Header Background Color
···
30
row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } },
31
32
/// Y Offset for drawing to the parent Window.
33
-
y_off: usize = 0,
34
35
/// Column Width
36
-
/// Note, this should be treated as Read Only. The Column Width will be calculated during `drawTable()`.
37
-
col_width: usize = 0,
38
};
39
40
/// Draw a Table for the TUI.
41
pub fn drawTable(
42
/// This should be an ArenaAllocator that can be deinitialized after each event call.
43
-
/// The Allocator is only used in two cases:
44
-
/// 1. If a cell is a non-String. If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".
45
-
/// 2. To show that a value is too large to fit into a cell. If the Allocator is not provided, they'll just be cutoff.
46
alloc: ?mem.Allocator,
47
/// The parent Window to draw to.
48
win: vaxis.Window,
49
-
/// Headers for the Table
50
-
headers: []const []const u8,
51
-
/// This must be an ArrayList.
52
data_list: anytype,
53
// The Table Context for this Table.
54
table_ctx: *TableContext,
55
) !void {
56
-
const table_win = win.initChild(
57
-
0,
58
-
table_ctx.y_off,
59
-
.{ .limit = win.width },
60
-
.{ .limit = win.height },
61
-
);
62
63
-
table_ctx.col_width = table_win.width / headers.len;
64
-
if (table_ctx.col_width % 2 != 0) table_ctx.col_width +|= 1;
65
-
while (table_ctx.col_width * headers.len < table_win.width - 1) table_ctx.col_width +|= 1;
66
67
-
if (table_ctx.col > headers.len - 1) table_ctx.*.col = headers.len - 1;
68
for (headers[0..], 0..) |hdr_txt, idx| {
69
-
const hdr_bg =
70
-
if (table_ctx.active and idx == table_ctx.col) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.hdr_bg_1 else table_ctx.hdr_bg_2;
71
-
const hdr_win = table_win.initChild(
72
-
idx * table_ctx.col_width,
73
-
0,
74
-
.{ .limit = table_ctx.col_width },
75
-
.{ .limit = 1 },
76
);
77
-
var hdr = vaxis.widgets.alignment.center(hdr_win, @min(table_ctx.col_width -| 1, hdr_txt.len +| 1), 1);
78
hdr_win.fill(.{ .style = .{ .bg = hdr_bg } });
79
var seg = [_]vaxis.Cell.Segment{.{
80
-
.text = if (hdr_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(table_ctx.col_width -| 4)]}) else hdr_txt,
81
.style = .{
82
.bg = hdr_bg,
83
.bold = true,
84
.ul_style = if (idx == table_ctx.col) .single else .dotted,
85
},
86
}};
87
-
_ = try hdr.print(seg[0..], .{ .wrap = .word });
88
}
89
90
-
const max_items = if (data_list.items.len > table_win.height -| 1) table_win.height -| 1 else data_list.items.len;
91
-
var end = table_ctx.*.start + max_items;
92
-
if (end > data_list.items.len) end = data_list.items.len;
93
-
table_ctx.*.start = tableStart: {
94
if (table_ctx.row == 0)
95
break :tableStart 0;
96
if (table_ctx.row < table_ctx.start)
97
break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row);
98
-
if (table_ctx.row >= data_list.items.len - 1)
99
-
table_ctx.*.row = data_list.items.len - 1;
100
if (table_ctx.row >= end)
101
break :tableStart table_ctx.start + (table_ctx.row - end + 1);
102
break :tableStart table_ctx.start;
103
};
104
-
end = table_ctx.*.start + max_items;
105
-
if (end > data_list.items.len) end = data_list.items.len;
106
-
for (data_list.items[table_ctx.start..end], 0..) |data, idx| {
107
-
const row_bg =
108
-
if (table_ctx.active and table_ctx.start + idx == table_ctx.row) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.row_bg_1 else table_ctx.row_bg_2;
109
-
110
-
const row_win = table_win.initChild(
111
-
0,
112
-
1 + idx,
113
-
.{ .limit = table_win.width },
114
-
.{ .limit = 1 },
115
-
);
116
-
const DataT = @TypeOf(data);
117
-
if (DataT == []const u8) {
118
-
row_win.fill(.{ .style = .{ .bg = row_bg } });
119
-
var seg = [_]vaxis.Cell.Segment{.{
120
-
.text = if (data.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{data[0..(table_ctx.col_width -| 4)]}) else data,
121
-
.style = .{ .bg = row_bg },
122
-
}};
123
-
_ = try row_win.print(seg[0..], .{ .wrap = .word });
124
-
return;
125
}
126
const item_fields = meta.fields(DataT);
127
-
inline for (item_fields[0..], 0..) |item_field, item_idx| {
128
-
const item = @field(data, item_field.name);
129
-
const ItemT = @TypeOf(item);
130
-
const item_win = row_win.initChild(
131
-
item_idx * table_ctx.col_width,
132
-
0,
133
-
.{ .limit = table_ctx.col_width },
134
-
.{ .limit = 1 },
135
-
);
136
-
const item_txt = switch (ItemT) {
137
-
[]const u8 => item,
138
-
else => nonStr: {
139
-
switch (@typeInfo(ItemT)) {
140
-
.Optional => {
141
-
const opt_item = item orelse break :nonStr "-";
142
-
switch (@typeInfo(ItemT).Optional.child) {
143
-
[]const u8 => break :nonStr opt_item,
144
-
else => {
145
-
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
146
-
},
147
-
}
148
},
149
-
else => {
150
-
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
151
-
},
152
-
}
153
-
},
154
-
};
155
-
item_win.fill(.{ .style = .{ .bg = row_bg } });
156
-
var seg = [_]vaxis.Cell.Segment{.{
157
-
.text = if (item_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(table_ctx.col_width -| 4)]}) else item_txt,
158
-
.style = .{ .bg = row_bg },
159
-
}};
160
-
_ = try item_win.print(seg[0..], .{ .wrap = .word });
161
}
162
}
163
}
···
8
9
/// Table Context for maintaining state and drawing Tables with `drawTable()`.
10
pub const TableContext = struct {
11
+
/// Current active Row of the Table.
12
+
row: u16 = 0,
13
+
/// Current active Column of the Table.
14
+
col: u16 = 0,
15
/// Starting point within the Data List.
16
+
start: u16 = 0,
17
+
/// Selected Rows.
18
+
sel_rows: ?[]u16 = null,
19
20
/// Active status of the Table.
21
active: bool = false,
22
+
/// Active Content Callback Function.
23
+
/// If available, this will be called to vertically expand the active row with additional info.
24
+
active_content_fn: ?*const fn (*vaxis.Window, *const anyopaque) anyerror!u16 = null,
25
+
/// Active Content Context
26
+
/// This will be provided to the `active_content` callback when called.
27
+
active_ctx: *const anyopaque = &{},
28
+
/// Y Offset for rows beyond the Active Content.
29
+
/// (This will be calculated automatically)
30
+
active_y_off: u16 = 0,
31
32
+
/// The Background Color for Selected Rows.
33
selected_bg: vaxis.Cell.Color,
34
+
/// The Foreground Color for Selected Rows.
35
+
selected_fg: vaxis.Cell.Color = .default,
36
+
/// The Background Color for the Active Row and Column Header.
37
+
active_bg: vaxis.Cell.Color,
38
+
/// The Foreground Color for the Active Row and Column Header.
39
+
active_fg: vaxis.Cell.Color = .default,
40
/// First Column Header Background Color
41
hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } },
42
/// Second Column Header Background Color
···
47
row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } },
48
49
/// Y Offset for drawing to the parent Window.
50
+
y_off: u16 = 0,
51
+
/// X Offset for printing each Cell/Item.
52
+
cell_x_off: u16 = 1,
53
54
/// Column Width
55
+
/// Note, if this is left `null` the Column Width will be dynamically calculated during `drawTable()`.
56
+
//col_width: ?usize = null,
57
+
col_width: WidthStyle = .dynamic_fill,
58
+
59
+
// Header Names
60
+
header_names: HeaderNames = .field_names,
61
+
// Column Indexes
62
+
col_indexes: ColumnIndexes = .all,
63
+
// Header Alignment
64
+
header_align: HorizontalAlignment = .center,
65
+
// Column Alignment
66
+
col_align: ColumnAlignment = .{ .all = .left },
67
+
68
+
// Header Borders
69
+
header_borders: bool = false,
70
+
// Row Borders
71
+
//row_borders: bool = false,
72
+
// Col Borders
73
+
col_borders: bool = false,
74
+
};
75
+
76
+
/// Width Styles for `col_width`.
77
+
pub const WidthStyle = union(enum) {
78
+
/// Dynamically calculate Column Widths such that the entire (or most) of the screen is filled horizontally.
79
+
dynamic_fill,
80
+
/// Dynamically calculate the Column Width for each Column based on its Header Length and the provided Padding length.
81
+
dynamic_header_len: u16,
82
+
/// Statically set all Column Widths to the same value.
83
+
static_all: u16,
84
+
/// Statically set individual Column Widths to specific values.
85
+
static_individual: []const u16,
86
+
};
87
+
88
+
/// Column Indexes
89
+
pub const ColumnIndexes = union(enum) {
90
+
/// Use all of the Columns.
91
+
all,
92
+
/// Use Columns from the specified indexes.
93
+
by_idx: []const usize,
94
+
};
95
+
96
+
/// Header Names
97
+
pub const HeaderNames = union(enum) {
98
+
/// Use Field Names as Headers
99
+
field_names,
100
+
/// Custom
101
+
custom: []const []const u8,
102
+
};
103
+
104
+
/// Horizontal Alignment
105
+
pub const HorizontalAlignment = enum {
106
+
left,
107
+
center,
108
+
};
109
+
/// Column Alignment
110
+
pub const ColumnAlignment = union(enum) {
111
+
all: HorizontalAlignment,
112
+
by_idx: []const HorizontalAlignment,
113
};
114
115
/// Draw a Table for the TUI.
116
pub fn drawTable(
117
/// This should be an ArenaAllocator that can be deinitialized after each event call.
118
+
/// The Allocator is only used in three cases:
119
+
/// 1. If a cell is a non-String. (If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".)
120
+
/// 2. To show that a value is too large to fit into a cell using '...'. (If the Allocator is not provided, they'll just be cutoff.)
121
+
/// 3. To copy a MultiArrayList into a normal slice. (Note, this is an expensive operation. Prefer to pass a Slice or ArrayList if possible.)
122
alloc: ?mem.Allocator,
123
/// The parent Window to draw to.
124
win: vaxis.Window,
125
+
/// This must be a Slice, ArrayList, or MultiArrayList.
126
+
/// Note, MultiArrayList support currently requires allocation.
127
data_list: anytype,
128
// The Table Context for this Table.
129
table_ctx: *TableContext,
130
) !void {
131
+
var di_is_mal = false;
132
+
const data_items = getData: {
133
+
const DataListT = @TypeOf(data_list);
134
+
const data_ti = @typeInfo(DataListT);
135
+
switch (data_ti) {
136
+
.pointer => |ptr| {
137
+
if (ptr.size != .slice) return error.UnsupportedTableDataType;
138
+
break :getData data_list;
139
+
},
140
+
.@"struct" => {
141
+
const di_fields = meta.fields(DataListT);
142
+
const al_fields = meta.fields(std.ArrayList([]const u8));
143
+
const mal_fields = meta.fields(std.MultiArrayList(struct { a: u8 = 0, b: u32 = 0 }));
144
+
// Probably an ArrayList
145
+
const is_al = comptime if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") == null and
146
+
mem.indexOf(u8, @typeName(DataListT), "ArrayList") != null and
147
+
al_fields.len == di_fields.len)
148
+
isAL: {
149
+
var is = true;
150
+
for (al_fields, di_fields) |al_field, di_field|
151
+
is = is and mem.eql(u8, al_field.name, di_field.name);
152
+
break :isAL is;
153
+
} else false;
154
+
if (is_al) break :getData data_list.items;
155
+
156
+
// Probably a MultiArrayList
157
+
const is_mal = if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") != null and
158
+
mal_fields.len == di_fields.len)
159
+
isMAL: {
160
+
var is = true;
161
+
inline for (mal_fields, di_fields) |mal_field, di_field|
162
+
is = is and mem.eql(u8, mal_field.name, di_field.name);
163
+
break :isMAL is;
164
+
} else false;
165
+
if (!is_mal) return error.UnsupportedTableDataType;
166
+
if (alloc) |_alloc| {
167
+
di_is_mal = true;
168
+
const mal_slice = data_list.slice();
169
+
const DataT = dataType: {
170
+
const fn_info = @typeInfo(@TypeOf(@field(@TypeOf(mal_slice), "get")));
171
+
break :dataType fn_info.@"fn".return_type orelse @panic("No Child Type");
172
+
};
173
+
var data_out_list = std.ArrayList(DataT){};
174
+
for (0..mal_slice.len) |idx| try data_out_list.append(_alloc, mal_slice.get(idx));
175
+
break :getData try data_out_list.toOwnedSlice(_alloc);
176
+
}
177
+
return error.UnsupportedTableDataType;
178
+
},
179
+
else => return error.UnsupportedTableDataType,
180
+
}
181
+
};
182
+
defer if (di_is_mal) alloc.?.free(data_items);
183
+
const DataT = @TypeOf(data_items[0]);
184
+
const fields = meta.fields(DataT);
185
+
const field_indexes = switch (table_ctx.col_indexes) {
186
+
.all => comptime allIdx: {
187
+
var indexes_buf: [fields.len]usize = undefined;
188
+
for (0..fields.len) |idx| indexes_buf[idx] = idx;
189
+
const indexes = indexes_buf;
190
+
break :allIdx indexes[0..];
191
+
},
192
+
.by_idx => |by_idx| by_idx,
193
+
};
194
195
+
// Headers for the Table
196
+
var hdrs_buf: [fields.len][]const u8 = undefined;
197
+
const headers = hdrs: {
198
+
switch (table_ctx.header_names) {
199
+
.field_names => {
200
+
for (field_indexes) |f_idx| {
201
+
inline for (fields, 0..) |field, idx| {
202
+
if (f_idx == idx)
203
+
hdrs_buf[idx] = field.name;
204
+
}
205
+
}
206
+
break :hdrs hdrs_buf[0..];
207
+
},
208
+
.custom => |hdrs| break :hdrs hdrs,
209
+
}
210
+
};
211
212
+
const table_win = win.child(.{
213
+
.y_off = table_ctx.y_off,
214
+
.width = win.width,
215
+
.height = win.height,
216
+
});
217
+
218
+
// Headers
219
+
if (table_ctx.col > headers.len - 1) table_ctx.col = @intCast(headers.len - 1);
220
+
var col_start: u16 = 0;
221
for (headers[0..], 0..) |hdr_txt, idx| {
222
+
const col_width = try calcColWidth(
223
+
@intCast(idx),
224
+
headers,
225
+
table_ctx.col_width,
226
+
table_win,
227
);
228
+
defer col_start += col_width;
229
+
const hdr_fg, const hdr_bg = hdrColors: {
230
+
if (table_ctx.active and idx == table_ctx.col)
231
+
break :hdrColors .{ table_ctx.active_fg, table_ctx.active_bg }
232
+
else if (idx % 2 == 0)
233
+
break :hdrColors .{ .default, table_ctx.hdr_bg_1 }
234
+
else
235
+
break :hdrColors .{ .default, table_ctx.hdr_bg_2 };
236
+
};
237
+
const hdr_win = table_win.child(.{
238
+
.x_off = col_start,
239
+
.y_off = 0,
240
+
.width = col_width,
241
+
.height = 1,
242
+
.border = .{ .where = if (table_ctx.header_borders and idx > 0) .left else .none },
243
+
});
244
+
var hdr = switch (table_ctx.header_align) {
245
+
.left => hdr_win,
246
+
.center => vaxis.widgets.alignment.center(hdr_win, @min(col_width -| 1, hdr_txt.len +| 1), 1),
247
+
};
248
hdr_win.fill(.{ .style = .{ .bg = hdr_bg } });
249
var seg = [_]vaxis.Cell.Segment{.{
250
+
.text = if (hdr_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(col_width -| 4)]}) else hdr_txt,
251
.style = .{
252
+
.fg = hdr_fg,
253
.bg = hdr_bg,
254
.bold = true,
255
.ul_style = if (idx == table_ctx.col) .single else .dotted,
256
},
257
}};
258
+
_ = hdr.print(seg[0..], .{ .wrap = .word });
259
}
260
261
+
// Rows
262
+
if (table_ctx.active_content_fn == null) table_ctx.active_y_off = 0;
263
+
const max_items: u16 =
264
+
if (data_items.len > table_win.height -| 1) table_win.height -| 1 else @intCast(data_items.len);
265
+
var end = table_ctx.start + max_items;
266
+
if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2)
267
+
end -|= table_ctx.active_y_off;
268
+
if (end > data_items.len) end = @intCast(data_items.len);
269
+
table_ctx.start = tableStart: {
270
if (table_ctx.row == 0)
271
break :tableStart 0;
272
if (table_ctx.row < table_ctx.start)
273
break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row);
274
+
if (table_ctx.row >= data_items.len - 1)
275
+
table_ctx.row = @intCast(data_items.len - 1);
276
if (table_ctx.row >= end)
277
break :tableStart table_ctx.start + (table_ctx.row - end + 1);
278
break :tableStart table_ctx.start;
279
};
280
+
end = table_ctx.start + max_items;
281
+
if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2)
282
+
end -|= table_ctx.active_y_off;
283
+
if (end > data_items.len) end = @intCast(data_items.len);
284
+
table_ctx.start = @min(table_ctx.start, end);
285
+
table_ctx.active_y_off = 0;
286
+
for (data_items[table_ctx.start..end], 0..) |data, row| {
287
+
const row_fg, const row_bg = rowColors: {
288
+
if (table_ctx.active and table_ctx.start + row == table_ctx.row)
289
+
break :rowColors .{ table_ctx.active_fg, table_ctx.active_bg };
290
+
if (table_ctx.sel_rows) |rows| {
291
+
if (mem.indexOfScalar(u16, rows, @intCast(table_ctx.start + row)) != null)
292
+
break :rowColors .{ table_ctx.selected_fg, table_ctx.selected_bg };
293
+
}
294
+
if (row % 2 == 0) break :rowColors .{ .default, table_ctx.row_bg_1 };
295
+
break :rowColors .{ .default, table_ctx.row_bg_2 };
296
+
};
297
+
var row_win = table_win.child(.{
298
+
.x_off = 0,
299
+
.y_off = @intCast(1 + row + table_ctx.active_y_off),
300
+
.width = table_win.width,
301
+
.height = 1,
302
+
//.border = .{ .where = if (table_ctx.row_borders) .top else .none },
303
+
});
304
+
if (table_ctx.start + row == table_ctx.row) {
305
+
table_ctx.active_y_off = if (table_ctx.active_content_fn) |content| try content(&row_win, table_ctx.active_ctx) else 0;
306
}
307
+
col_start = 0;
308
const item_fields = meta.fields(DataT);
309
+
var col_idx: usize = 0;
310
+
for (field_indexes) |f_idx| {
311
+
inline for (item_fields[0..], 0..) |item_field, item_idx| contFields: {
312
+
switch (table_ctx.col_indexes) {
313
+
.all => {},
314
+
.by_idx => {
315
+
if (item_idx != f_idx) break :contFields;
316
+
},
317
+
}
318
+
defer col_idx += 1;
319
+
const col_width = try calcColWidth(
320
+
item_idx,
321
+
headers,
322
+
table_ctx.col_width,
323
+
table_win,
324
+
);
325
+
defer col_start += col_width;
326
+
const item = @field(data, item_field.name);
327
+
const ItemT = @TypeOf(item);
328
+
const item_win = row_win.child(.{
329
+
.x_off = col_start,
330
+
.y_off = 0,
331
+
.width = col_width,
332
+
.height = 1,
333
+
.border = .{ .where = if (table_ctx.col_borders and col_idx > 0) .left else .none },
334
+
});
335
+
const item_txt = switch (ItemT) {
336
+
[]const u8 => item,
337
+
[][]const u8, []const []const u8 => strSlice: {
338
+
if (alloc) |_alloc| break :strSlice try fmt.allocPrint(_alloc, "{s}", .{item});
339
+
break :strSlice item;
340
+
},
341
+
else => nonStr: {
342
+
switch (@typeInfo(ItemT)) {
343
+
.@"enum" => break :nonStr @tagName(item),
344
+
.optional => {
345
+
const opt_item = item orelse break :nonStr "-";
346
+
switch (@typeInfo(ItemT).optional.child) {
347
+
[]const u8 => break :nonStr opt_item,
348
+
[][]const u8, []const []const u8 => {
349
+
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{s}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
350
+
},
351
+
else => {
352
+
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
353
+
},
354
+
}
355
+
},
356
+
else => {
357
+
break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
358
+
},
359
+
}
360
+
},
361
+
};
362
+
item_win.fill(.{ .style = .{ .bg = row_bg } });
363
+
const item_align_win = itemAlignWin: {
364
+
const col_align = switch (table_ctx.col_align) {
365
+
.all => |all| all,
366
+
.by_idx => |aligns| aligns[col_idx],
367
+
};
368
+
break :itemAlignWin switch (col_align) {
369
+
.left => item_win,
370
+
.center => center: {
371
+
const center = vaxis.widgets.alignment.center(item_win, @min(col_width -| 1, item_txt.len +| 1), 1);
372
+
center.fill(.{ .style = .{ .bg = row_bg } });
373
+
break :center center;
374
},
375
+
};
376
+
};
377
+
var seg = [_]vaxis.Cell.Segment{.{
378
+
.text = if (item_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(col_width -| 4)]}) else item_txt,
379
+
.style = .{ .fg = row_fg, .bg = row_bg },
380
+
}};
381
+
_ = item_align_win.print(seg[0..], .{ .wrap = .word, .col_offset = table_ctx.cell_x_off });
382
+
}
383
}
384
}
385
}
386
+
387
+
/// Calculate the Column Width of `col` using the provided Number of Headers (`num_hdrs`), Width Style (`style`), and Table Window (`table_win`).
388
+
pub fn calcColWidth(
389
+
col: u16,
390
+
headers: []const []const u8,
391
+
style: WidthStyle,
392
+
table_win: vaxis.Window,
393
+
) !u16 {
394
+
return switch (style) {
395
+
.dynamic_fill => dynFill: {
396
+
var cw: u16 = table_win.width / @as(u16, @intCast(headers.len));
397
+
if (cw % 2 != 0) cw +|= 1;
398
+
while (cw * headers.len < table_win.width - 1) cw +|= 1;
399
+
break :dynFill cw;
400
+
},
401
+
.dynamic_header_len => dynHdrs: {
402
+
if (col >= headers.len) break :dynHdrs error.NotEnoughStaticWidthsProvided;
403
+
break :dynHdrs @as(u16, @intCast(headers[col].len)) + (style.dynamic_header_len * 2);
404
+
},
405
+
.static_all => style.static_all,
406
+
.static_individual => statInd: {
407
+
if (col >= headers.len) break :statInd error.NotEnoughStaticWidthsProvided;
408
+
break :statInd style.static_individual[col];
409
+
},
410
+
};
411
+
}
+289
-175
src/widgets/TextInput.zig
+289
-175
src/widgets/TextInput.zig
···
3
const Key = @import("../Key.zig");
4
const Cell = @import("../Cell.zig");
5
const Window = @import("../Window.zig");
6
-
const GapBuffer = @import("gap_buffer").GapBuffer;
7
-
const Unicode = @import("../Unicode.zig");
8
9
const TextInput = @This();
10
···
16
const ellipsis: Cell.Character = .{ .grapheme = "โฆ", .width = 1 };
17
18
// Index of our cursor
19
-
cursor_idx: usize = 0,
20
-
grapheme_count: usize = 0,
21
-
buf: GapBuffer(u8),
22
23
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
24
-
draw_offset: usize = 0,
25
/// the column we placed the cursor the last time we drew
26
-
prev_cursor_col: usize = 0,
27
/// the grapheme index of the cursor the last time we drew
28
-
prev_cursor_idx: usize = 0,
29
/// approximate distance from an edge before we scroll
30
-
scroll_offset: usize = 4,
31
32
-
unicode: *const Unicode,
33
-
34
-
pub fn init(alloc: std.mem.Allocator, unicode: *const Unicode) TextInput {
35
return TextInput{
36
-
.buf = GapBuffer(u8).init(alloc),
37
-
.unicode = unicode,
38
};
39
}
40
···
46
switch (event) {
47
.key_press => |key| {
48
if (key.matches(Key.backspace, .{})) {
49
-
if (self.cursor_idx == 0) return;
50
-
try self.deleteBeforeCursor();
51
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
52
-
if (self.cursor_idx == self.grapheme_count) return;
53
-
try self.deleteAtCursor();
54
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
55
-
if (self.cursor_idx > 0) self.cursor_idx -= 1;
56
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
57
-
if (self.cursor_idx < self.grapheme_count) self.cursor_idx += 1;
58
-
} else if (key.matches('a', .{ .ctrl = true })) {
59
-
self.cursor_idx = 0;
60
-
} else if (key.matches('e', .{ .ctrl = true })) {
61
-
self.cursor_idx = self.grapheme_count;
62
} else if (key.matches('k', .{ .ctrl = true })) {
63
-
try self.deleteToEnd();
64
} else if (key.matches('u', .{ .ctrl = true })) {
65
-
try self.deleteToStart();
66
} else if (key.text) |text| {
67
-
try self.buf.insertSliceBefore(self.byteOffsetToCursor(), text);
68
-
self.cursor_idx += 1;
69
-
self.grapheme_count += 1;
70
}
71
},
72
}
73
}
74
75
/// insert text at the cursor position
76
-
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) !void {
77
-
var iter = self.unicode.graphemeIterator(data);
78
-
var byte_offset_to_cursor = self.byteOffsetToCursor();
79
while (iter.next()) |text| {
80
-
try self.buf.insertSliceBefore(byte_offset_to_cursor, text.bytes(data));
81
-
byte_offset_to_cursor += text.len;
82
-
self.cursor_idx += 1;
83
-
self.grapheme_count += 1;
84
}
85
}
86
87
pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 {
88
-
const offset = self.byteOffsetToCursor();
89
-
assert(offset <= buf.len); // provided buf was too small
90
-
91
-
if (offset <= self.buf.items.len) {
92
-
@memcpy(buf[0..offset], self.buf.items[0..offset]);
93
-
} else {
94
-
@memcpy(buf[0..self.buf.items.len], self.buf.items);
95
-
const second_half = self.buf.secondHalf();
96
-
const copy_len = offset - self.buf.items.len;
97
-
@memcpy(buf[self.buf.items.len .. self.buf.items.len + copy_len], second_half[0..copy_len]);
98
-
}
99
-
return buf[0..offset];
100
}
101
102
/// calculates the display width from the draw_offset to the cursor
103
-
fn widthToCursor(self: *TextInput, win: Window) usize {
104
-
var width: usize = 0;
105
-
var first_iter = self.unicode.graphemeIterator(self.buf.items);
106
var i: usize = 0;
107
while (first_iter.next()) |grapheme| {
108
defer i += 1;
109
if (i < self.draw_offset) {
110
continue;
111
}
112
-
if (i == self.cursor_idx) return width;
113
-
const g = grapheme.bytes(self.buf.items);
114
width += win.gwidth(g);
115
}
116
-
const second_half = self.buf.secondHalf();
117
-
var second_iter = self.unicode.graphemeIterator(second_half);
118
-
while (second_iter.next()) |grapheme| {
119
-
defer i += 1;
120
-
if (i < self.draw_offset) {
121
-
continue;
122
-
}
123
-
if (i == self.cursor_idx) return width;
124
-
const g = grapheme.bytes(second_half);
125
-
width += win.gwidth(g);
126
}
127
-
return width;
128
}
129
130
pub fn draw(self: *TextInput, win: Window) void {
131
-
if (self.cursor_idx < self.draw_offset) self.draw_offset = self.cursor_idx;
132
if (win.width == 0) return;
133
while (true) {
134
const width = self.widthToCursor(win);
···
138
} else break;
139
}
140
141
-
self.prev_cursor_idx = self.cursor_idx;
142
self.prev_cursor_col = 0;
143
144
// assumption!! the gap is never within a grapheme
145
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
146
-
var first_iter = self.unicode.graphemeIterator(self.buf.items);
147
-
var col: usize = 0;
148
-
var i: usize = 0;
149
while (first_iter.next()) |grapheme| {
150
if (i < self.draw_offset) {
151
i += 1;
152
continue;
153
}
154
-
const g = grapheme.bytes(self.buf.items);
155
const w = win.gwidth(g);
156
if (col + w >= win.width) {
157
-
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
158
break;
159
}
160
win.writeCell(col, 0, .{
161
.char = .{
162
.grapheme = g,
163
-
.width = w,
164
},
165
});
166
col += w;
167
i += 1;
168
-
if (i == self.cursor_idx) self.prev_cursor_col = col;
169
}
170
const second_half = self.buf.secondHalf();
171
-
var second_iter = self.unicode.graphemeIterator(second_half);
172
while (second_iter.next()) |grapheme| {
173
if (i < self.draw_offset) {
174
i += 1;
···
177
const g = grapheme.bytes(second_half);
178
const w = win.gwidth(g);
179
if (col + w > win.width) {
180
-
win.writeCell(win.width - 1, 0, .{ .char = ellipsis });
181
break;
182
}
183
win.writeCell(col, 0, .{
184
.char = .{
185
.grapheme = g,
186
-
.width = w,
187
},
188
});
189
col += w;
190
i += 1;
191
-
if (i == self.cursor_idx) self.prev_cursor_col = col;
192
}
193
if (self.draw_offset > 0) {
194
-
win.writeCell(0, 0, .{ .char = ellipsis });
195
}
196
win.showCursor(self.prev_cursor_col, 0);
197
}
···
211
return self.buf.toOwnedSlice();
212
}
213
214
-
fn reset(self: *TextInput) void {
215
-
self.cursor_idx = 0;
216
-
self.grapheme_count = 0;
217
self.draw_offset = 0;
218
self.prev_cursor_col = 0;
219
self.prev_cursor_idx = 0;
220
}
221
222
// returns the number of bytes before the cursor
223
-
// (since GapBuffers are strictly speaking not contiguous, this is a number in 0..realLength()
224
-
// which would need to be fed to realIndex() to get an actual offset into self.buf.items.ptr)
225
pub fn byteOffsetToCursor(self: TextInput) usize {
226
-
// assumption! the gap is never in the middle of a grapheme
227
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
228
-
var iter = self.unicode.graphemeIterator(self.buf.items);
229
-
var offset: usize = 0;
230
-
var i: usize = 0;
231
-
while (iter.next()) |grapheme| {
232
-
if (i == self.cursor_idx) break;
233
-
offset += grapheme.len;
234
-
i += 1;
235
-
} else {
236
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
237
-
while (second_iter.next()) |grapheme| {
238
-
if (i == self.cursor_idx) break;
239
-
offset += grapheme.len;
240
-
i += 1;
241
-
}
242
-
}
243
-
return offset;
244
}
245
246
-
fn deleteToEnd(self: *TextInput) !void {
247
-
const offset = self.byteOffsetToCursor();
248
-
try self.buf.replaceRangeAfter(offset, self.buf.realLength() - offset, &.{});
249
-
self.grapheme_count = self.cursor_idx;
250
}
251
252
-
fn deleteToStart(self: *TextInput) !void {
253
-
const offset = self.byteOffsetToCursor();
254
-
try self.buf.replaceRangeBefore(0, offset, &.{});
255
-
self.grapheme_count -= self.cursor_idx;
256
-
self.cursor_idx = 0;
257
}
258
259
-
fn deleteBeforeCursor(self: *TextInput) !void {
260
-
// assumption! the gap is never in the middle of a grapheme
261
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
262
-
var iter = self.unicode.graphemeIterator(self.buf.items);
263
-
var offset: usize = 0;
264
-
var i: usize = 1;
265
while (iter.next()) |grapheme| {
266
-
if (i == self.cursor_idx) {
267
-
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
268
-
self.cursor_idx -= 1;
269
-
self.grapheme_count -= 1;
270
-
return;
271
-
}
272
-
offset += grapheme.len;
273
-
i += 1;
274
-
} else {
275
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
276
-
while (second_iter.next()) |grapheme| {
277
-
if (i == self.cursor_idx) {
278
-
try self.buf.replaceRangeBefore(offset, grapheme.len, &.{});
279
-
self.cursor_idx -= 1;
280
-
self.grapheme_count -= 1;
281
-
return;
282
-
}
283
-
offset += grapheme.len;
284
-
i += 1;
285
-
}
286
}
287
}
288
289
-
fn deleteAtCursor(self: *TextInput) !void {
290
-
// assumption! the gap is never in the middle of a grapheme
291
-
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
292
-
var iter = self.unicode.graphemeIterator(self.buf.items);
293
-
var offset: usize = 0;
294
-
var i: usize = 1;
295
-
while (iter.next()) |grapheme| {
296
-
if (i == self.cursor_idx + 1) {
297
-
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
298
-
self.grapheme_count -= 1;
299
-
return;
300
-
}
301
-
offset += grapheme.len;
302
-
i += 1;
303
-
} else {
304
-
var second_iter = self.unicode.graphemeIterator(self.buf.secondHalf());
305
-
while (second_iter.next()) |grapheme| {
306
-
if (i == self.cursor_idx + 1) {
307
-
try self.buf.replaceRangeAfter(offset, grapheme.len, &.{});
308
-
self.grapheme_count -= 1;
309
-
return;
310
-
}
311
-
offset += grapheme.len;
312
-
i += 1;
313
-
}
314
-
}
315
}
316
317
test "assertion" {
318
-
const alloc = std.testing.allocator_instance.allocator();
319
-
const unicode = try Unicode.init(alloc);
320
-
defer unicode.deinit();
321
const astronaut = "๐ฉโ๐";
322
const astronaut_emoji: Key = .{
323
.text = astronaut,
324
.codepoint = try std.unicode.utf8Decode(astronaut[0..4]),
325
};
326
-
var input = TextInput.init(std.testing.allocator, &unicode);
327
defer input.deinit();
328
for (0..6) |_| {
329
try input.update(.{ .key_press = astronaut_emoji });
···
331
}
332
333
test "sliceToCursor" {
334
-
const alloc = std.testing.allocator_instance.allocator();
335
-
const unicode = try Unicode.init(alloc);
336
-
defer unicode.deinit();
337
-
var input = init(alloc, &unicode);
338
defer input.deinit();
339
try input.insertSliceAtCursor("hello, world");
340
-
input.cursor_idx = 2;
341
var buf: [32]u8 = undefined;
342
-
try std.testing.expectEqualStrings("he", input.sliceToCursor(&buf));
343
-
input.buf.moveGap(3);
344
-
input.cursor_idx = 5;
345
-
try std.testing.expectEqualStrings("hello", input.sliceToCursor(&buf));
346
}
···
3
const Key = @import("../Key.zig");
4
const Cell = @import("../Cell.zig");
5
const Window = @import("../Window.zig");
6
+
const unicode = @import("../unicode.zig");
7
8
const TextInput = @This();
9
···
15
const ellipsis: Cell.Character = .{ .grapheme = "โฆ", .width = 1 };
16
17
// Index of our cursor
18
+
buf: Buffer,
19
20
/// the number of graphemes to skip when drawing. Used for horizontal scrolling
21
+
draw_offset: u16 = 0,
22
/// the column we placed the cursor the last time we drew
23
+
prev_cursor_col: u16 = 0,
24
/// the grapheme index of the cursor the last time we drew
25
+
prev_cursor_idx: u16 = 0,
26
/// approximate distance from an edge before we scroll
27
+
scroll_offset: u16 = 4,
28
29
+
pub fn init(alloc: std.mem.Allocator) TextInput {
30
return TextInput{
31
+
.buf = Buffer.init(alloc),
32
};
33
}
34
···
40
switch (event) {
41
.key_press => |key| {
42
if (key.matches(Key.backspace, .{})) {
43
+
self.deleteBeforeCursor();
44
} else if (key.matches(Key.delete, .{}) or key.matches('d', .{ .ctrl = true })) {
45
+
self.deleteAfterCursor();
46
} else if (key.matches(Key.left, .{}) or key.matches('b', .{ .ctrl = true })) {
47
+
self.cursorLeft();
48
} else if (key.matches(Key.right, .{}) or key.matches('f', .{ .ctrl = true })) {
49
+
self.cursorRight();
50
+
} else if (key.matches('a', .{ .ctrl = true }) or key.matches(Key.home, .{})) {
51
+
self.buf.moveGapLeft(self.buf.firstHalf().len);
52
+
} else if (key.matches('e', .{ .ctrl = true }) or key.matches(Key.end, .{})) {
53
+
self.buf.moveGapRight(self.buf.secondHalf().len);
54
} else if (key.matches('k', .{ .ctrl = true })) {
55
+
self.deleteToEnd();
56
} else if (key.matches('u', .{ .ctrl = true })) {
57
+
self.deleteToStart();
58
+
} else if (key.matches('b', .{ .alt = true }) or key.matches(Key.left, .{ .alt = true })) {
59
+
self.moveBackwardWordwise();
60
+
} else if (key.matches('f', .{ .alt = true }) or key.matches(Key.right, .{ .alt = true })) {
61
+
self.moveForwardWordwise();
62
+
} else if (key.matches('w', .{ .ctrl = true }) or key.matches(Key.backspace, .{ .alt = true })) {
63
+
self.deleteWordBefore();
64
+
} else if (key.matches('d', .{ .alt = true })) {
65
+
self.deleteWordAfter();
66
} else if (key.text) |text| {
67
+
try self.insertSliceAtCursor(text);
68
}
69
},
70
}
71
}
72
73
/// insert text at the cursor position
74
+
pub fn insertSliceAtCursor(self: *TextInput, data: []const u8) std.mem.Allocator.Error!void {
75
+
var iter = unicode.graphemeIterator(data);
76
while (iter.next()) |text| {
77
+
try self.buf.insertSliceAtCursor(text.bytes(data));
78
}
79
}
80
81
pub fn sliceToCursor(self: *TextInput, buf: []u8) []const u8 {
82
+
assert(buf.len >= self.buf.cursor);
83
+
@memcpy(buf[0..self.buf.cursor], self.buf.firstHalf());
84
+
return buf[0..self.buf.cursor];
85
}
86
87
/// calculates the display width from the draw_offset to the cursor
88
+
pub fn widthToCursor(self: *TextInput, win: Window) u16 {
89
+
var width: u16 = 0;
90
+
const first_half = self.buf.firstHalf();
91
+
var first_iter = unicode.graphemeIterator(first_half);
92
var i: usize = 0;
93
while (first_iter.next()) |grapheme| {
94
defer i += 1;
95
if (i < self.draw_offset) {
96
continue;
97
}
98
+
const g = grapheme.bytes(first_half);
99
width += win.gwidth(g);
100
}
101
+
return width;
102
+
}
103
+
104
+
pub fn cursorLeft(self: *TextInput) void {
105
+
// We need to find the size of the last grapheme in the first half
106
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
107
+
var len: usize = 0;
108
+
while (iter.next()) |grapheme| {
109
+
len = grapheme.len;
110
}
111
+
self.buf.moveGapLeft(len);
112
+
}
113
+
114
+
pub fn cursorRight(self: *TextInput) void {
115
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
116
+
const grapheme = iter.next() orelse return;
117
+
self.buf.moveGapRight(grapheme.len);
118
+
}
119
+
120
+
pub fn graphemesBeforeCursor(self: *const TextInput) u16 {
121
+
const first_half = self.buf.firstHalf();
122
+
var first_iter = unicode.graphemeIterator(first_half);
123
+
var i: u16 = 0;
124
+
while (first_iter.next()) |_| {
125
+
i += 1;
126
+
}
127
+
return i;
128
}
129
130
pub fn draw(self: *TextInput, win: Window) void {
131
+
self.drawWithStyle(win, .{});
132
+
}
133
+
134
+
pub fn drawWithStyle(self: *TextInput, win: Window, style: Cell.Style) void {
135
+
const cursor_idx = self.graphemesBeforeCursor();
136
+
if (cursor_idx < self.draw_offset) self.draw_offset = cursor_idx;
137
if (win.width == 0) return;
138
while (true) {
139
const width = self.widthToCursor(win);
···
143
} else break;
144
}
145
146
+
self.prev_cursor_idx = cursor_idx;
147
self.prev_cursor_col = 0;
148
149
// assumption!! the gap is never within a grapheme
150
// one way to _ensure_ this is to move the gap... but that's a cost we probably don't want to pay.
151
+
const first_half = self.buf.firstHalf();
152
+
var first_iter = unicode.graphemeIterator(first_half);
153
+
var col: u16 = 0;
154
+
var i: u16 = 0;
155
while (first_iter.next()) |grapheme| {
156
if (i < self.draw_offset) {
157
i += 1;
158
continue;
159
}
160
+
const g = grapheme.bytes(first_half);
161
const w = win.gwidth(g);
162
if (col + w >= win.width) {
163
+
win.writeCell(win.width - 1, 0, .{
164
+
.char = ellipsis,
165
+
.style = style,
166
+
});
167
break;
168
}
169
win.writeCell(col, 0, .{
170
.char = .{
171
.grapheme = g,
172
+
.width = @intCast(w),
173
},
174
+
.style = style,
175
});
176
col += w;
177
i += 1;
178
+
if (i == cursor_idx) self.prev_cursor_col = col;
179
}
180
const second_half = self.buf.secondHalf();
181
+
var second_iter = unicode.graphemeIterator(second_half);
182
while (second_iter.next()) |grapheme| {
183
if (i < self.draw_offset) {
184
i += 1;
···
187
const g = grapheme.bytes(second_half);
188
const w = win.gwidth(g);
189
if (col + w > win.width) {
190
+
win.writeCell(win.width - 1, 0, .{
191
+
.char = ellipsis,
192
+
.style = style,
193
+
});
194
break;
195
}
196
win.writeCell(col, 0, .{
197
.char = .{
198
.grapheme = g,
199
+
.width = @intCast(w),
200
},
201
+
.style = style,
202
});
203
col += w;
204
i += 1;
205
+
if (i == cursor_idx) self.prev_cursor_col = col;
206
}
207
if (self.draw_offset > 0) {
208
+
win.writeCell(0, 0, .{
209
+
.char = ellipsis,
210
+
.style = style,
211
+
});
212
}
213
win.showCursor(self.prev_cursor_col, 0);
214
}
···
228
return self.buf.toOwnedSlice();
229
}
230
231
+
pub fn reset(self: *TextInput) void {
232
self.draw_offset = 0;
233
self.prev_cursor_col = 0;
234
self.prev_cursor_idx = 0;
235
}
236
237
// returns the number of bytes before the cursor
238
pub fn byteOffsetToCursor(self: TextInput) usize {
239
+
return self.buf.cursor;
240
}
241
242
+
pub fn deleteToEnd(self: *TextInput) void {
243
+
self.buf.growGapRight(self.buf.secondHalf().len);
244
}
245
246
+
pub fn deleteToStart(self: *TextInput) void {
247
+
self.buf.growGapLeft(self.buf.cursor);
248
}
249
250
+
pub fn deleteBeforeCursor(self: *TextInput) void {
251
+
// We need to find the size of the last grapheme in the first half
252
+
var iter = unicode.graphemeIterator(self.buf.firstHalf());
253
+
var len: usize = 0;
254
while (iter.next()) |grapheme| {
255
+
len = grapheme.len;
256
}
257
+
self.buf.growGapLeft(len);
258
}
259
260
+
pub fn deleteAfterCursor(self: *TextInput) void {
261
+
var iter = unicode.graphemeIterator(self.buf.secondHalf());
262
+
const grapheme = iter.next() orelse return;
263
+
self.buf.growGapRight(grapheme.len);
264
+
}
265
+
266
+
/// Moves the cursor backward by words. If the character before the cursor is a space, the cursor is
267
+
/// positioned just after the next previous space
268
+
pub fn moveBackwardWordwise(self: *TextInput) void {
269
+
const trimmed = std.mem.trimRight(u8, self.buf.firstHalf(), " ");
270
+
const idx = if (std.mem.lastIndexOfScalar(u8, trimmed, ' ')) |last|
271
+
last + 1
272
+
else
273
+
0;
274
+
self.buf.moveGapLeft(self.buf.cursor - idx);
275
+
}
276
+
277
+
pub fn moveForwardWordwise(self: *TextInput) void {
278
+
const second_half = self.buf.secondHalf();
279
+
var i: usize = 0;
280
+
while (i < second_half.len and second_half[i] == ' ') : (i += 1) {}
281
+
const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len;
282
+
self.buf.moveGapRight(idx);
283
+
}
284
+
285
+
pub fn deleteWordBefore(self: *TextInput) void {
286
+
// Store current cursor position. Move one word backward. Delete after the cursor the bytes we
287
+
// moved
288
+
const pre = self.buf.cursor;
289
+
self.moveBackwardWordwise();
290
+
self.buf.growGapRight(pre - self.buf.cursor);
291
+
}
292
+
293
+
pub fn deleteWordAfter(self: *TextInput) void {
294
+
// Store current cursor position. Move one word backward. Delete after the cursor the bytes we
295
+
// moved
296
+
const second_half = self.buf.secondHalf();
297
+
var i: usize = 0;
298
+
while (i < second_half.len and second_half[i] == ' ') : (i += 1) {}
299
+
const idx = std.mem.indexOfScalarPos(u8, second_half, i, ' ') orelse second_half.len;
300
+
self.buf.growGapRight(idx);
301
}
302
303
test "assertion" {
304
const astronaut = "๐ฉโ๐";
305
const astronaut_emoji: Key = .{
306
.text = astronaut,
307
.codepoint = try std.unicode.utf8Decode(astronaut[0..4]),
308
};
309
+
var input = TextInput.init(std.testing.allocator);
310
defer input.deinit();
311
for (0..6) |_| {
312
try input.update(.{ .key_press = astronaut_emoji });
···
314
}
315
316
test "sliceToCursor" {
317
+
var input = init(std.testing.allocator);
318
defer input.deinit();
319
try input.insertSliceAtCursor("hello, world");
320
+
input.cursorLeft();
321
+
input.cursorLeft();
322
+
input.cursorLeft();
323
var buf: [32]u8 = undefined;
324
+
try std.testing.expectEqualStrings("hello, wo", input.sliceToCursor(&buf));
325
+
input.cursorRight();
326
+
try std.testing.expectEqualStrings("hello, wor", input.sliceToCursor(&buf));
327
+
}
328
+
329
+
pub const Buffer = struct {
330
+
allocator: std.mem.Allocator,
331
+
buffer: []u8,
332
+
cursor: usize,
333
+
gap_size: usize,
334
+
335
+
pub fn init(allocator: std.mem.Allocator) Buffer {
336
+
return .{
337
+
.allocator = allocator,
338
+
.buffer = &.{},
339
+
.cursor = 0,
340
+
.gap_size = 0,
341
+
};
342
+
}
343
+
344
+
pub fn deinit(self: *Buffer) void {
345
+
self.allocator.free(self.buffer);
346
+
}
347
+
348
+
pub fn firstHalf(self: Buffer) []const u8 {
349
+
return self.buffer[0..self.cursor];
350
+
}
351
+
352
+
pub fn secondHalf(self: Buffer) []const u8 {
353
+
return self.buffer[self.cursor + self.gap_size ..];
354
+
}
355
+
356
+
pub fn grow(self: *Buffer, n: usize) std.mem.Allocator.Error!void {
357
+
// Always grow by 512 bytes
358
+
const new_size = self.buffer.len + n + 512;
359
+
// Allocate the new memory
360
+
const new_memory = try self.allocator.alloc(u8, new_size);
361
+
// Copy the first half
362
+
@memcpy(new_memory[0..self.cursor], self.firstHalf());
363
+
// Copy the second half
364
+
const second_half = self.secondHalf();
365
+
@memcpy(new_memory[new_size - second_half.len ..], second_half);
366
+
self.allocator.free(self.buffer);
367
+
self.buffer = new_memory;
368
+
self.gap_size = new_size - second_half.len - self.cursor;
369
+
}
370
+
371
+
pub fn insertSliceAtCursor(self: *Buffer, slice: []const u8) std.mem.Allocator.Error!void {
372
+
if (slice.len == 0) return;
373
+
if (self.gap_size <= slice.len) try self.grow(slice.len);
374
+
@memcpy(self.buffer[self.cursor .. self.cursor + slice.len], slice);
375
+
self.cursor += slice.len;
376
+
self.gap_size -= slice.len;
377
+
}
378
+
379
+
/// Move the gap n bytes to the left
380
+
pub fn moveGapLeft(self: *Buffer, n: usize) void {
381
+
const new_idx = self.cursor -| n;
382
+
const dst = self.buffer[new_idx + self.gap_size ..];
383
+
const src = self.buffer[new_idx..self.cursor];
384
+
std.mem.copyForwards(u8, dst, src);
385
+
self.cursor = new_idx;
386
+
}
387
+
388
+
pub fn moveGapRight(self: *Buffer, n: usize) void {
389
+
const new_idx = self.cursor + n;
390
+
const dst = self.buffer[self.cursor..];
391
+
const src = self.buffer[self.cursor + self.gap_size .. new_idx + self.gap_size];
392
+
std.mem.copyForwards(u8, dst, src);
393
+
self.cursor = new_idx;
394
+
}
395
+
396
+
/// grow the gap by moving the cursor n bytes to the left
397
+
pub fn growGapLeft(self: *Buffer, n: usize) void {
398
+
// gap grows by the delta
399
+
self.gap_size += n;
400
+
self.cursor -|= n;
401
+
}
402
+
403
+
/// grow the gap by removing n bytes after the cursor
404
+
pub fn growGapRight(self: *Buffer, n: usize) void {
405
+
self.gap_size = @min(self.gap_size + n, self.buffer.len - self.cursor);
406
+
}
407
+
408
+
pub fn clearAndFree(self: *Buffer) void {
409
+
self.cursor = 0;
410
+
self.allocator.free(self.buffer);
411
+
self.buffer = &.{};
412
+
self.gap_size = 0;
413
+
}
414
+
415
+
pub fn clearRetainingCapacity(self: *Buffer) void {
416
+
self.cursor = 0;
417
+
self.gap_size = self.buffer.len;
418
+
}
419
+
420
+
pub fn toOwnedSlice(self: *Buffer) std.mem.Allocator.Error![]const u8 {
421
+
const first_half = self.firstHalf();
422
+
const second_half = self.secondHalf();
423
+
const buf = try self.allocator.alloc(u8, first_half.len + second_half.len);
424
+
@memcpy(buf[0..first_half.len], first_half);
425
+
@memcpy(buf[first_half.len..], second_half);
426
+
self.clearAndFree();
427
+
return buf;
428
+
}
429
+
430
+
pub fn realLength(self: *const Buffer) usize {
431
+
return self.firstHalf().len + self.secondHalf().len;
432
+
}
433
+
};
434
+
435
+
test "TextInput.zig: Buffer" {
436
+
var gap_buf = Buffer.init(std.testing.allocator);
437
+
defer gap_buf.deinit();
438
+
439
+
try gap_buf.insertSliceAtCursor("abc");
440
+
try std.testing.expectEqualStrings("abc", gap_buf.firstHalf());
441
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
442
+
443
+
gap_buf.moveGapLeft(1);
444
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
445
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
446
+
447
+
try gap_buf.insertSliceAtCursor(" ");
448
+
try std.testing.expectEqualStrings("ab ", gap_buf.firstHalf());
449
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
450
+
451
+
gap_buf.growGapLeft(1);
452
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
453
+
try std.testing.expectEqualStrings("c", gap_buf.secondHalf());
454
+
try std.testing.expectEqual(2, gap_buf.cursor);
455
+
456
+
gap_buf.growGapRight(1);
457
+
try std.testing.expectEqualStrings("ab", gap_buf.firstHalf());
458
+
try std.testing.expectEqualStrings("", gap_buf.secondHalf());
459
+
try std.testing.expectEqual(2, gap_buf.cursor);
460
}
+58
-25
src/widgets/TextView.zig
+58
-25
src/widgets/TextView.zig
···
1
const std = @import("std");
2
const vaxis = @import("../main.zig");
3
-
const grapheme = @import("grapheme");
4
-
const DisplayWidth = @import("DisplayWidth");
5
const ScrollView = vaxis.widgets.ScrollView;
6
7
pub const BufferWriter = struct {
8
pub const Error = error{OutOfMemory};
9
pub const Writer = std.io.GenericWriter(@This(), Error, write);
10
11
allocator: std.mem.Allocator,
12
buffer: *Buffer,
13
-
gd: *const grapheme.GraphemeData,
14
-
wd: *const DisplayWidth.DisplayWidthData,
15
16
pub fn write(self: @This(), bytes: []const u8) Error!usize {
17
try self.buffer.append(self.allocator, .{
18
.bytes = bytes,
19
-
.gd = self.gd,
20
-
.wd = self.wd,
21
});
22
return bytes.len;
23
}
···
33
34
pub const Content = struct {
35
bytes: []const u8,
36
-
gd: *const grapheme.GraphemeData,
37
-
wd: *const DisplayWidth.DisplayWidthData,
38
};
39
40
pub const Style = struct {
···
45
46
pub const Error = error{OutOfMemory};
47
48
-
grapheme: std.MultiArrayList(grapheme.Grapheme) = .{},
49
content: std.ArrayListUnmanaged(u8) = .{},
50
style_list: StyleList = .{},
51
style_map: StyleMap = .{},
···
78
/// Appends content to the buffer.
79
pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void {
80
var cols: usize = self.last_cols;
81
-
var iter = grapheme.Iterator.init(content.bytes, content.gd);
82
-
const dw: DisplayWidth = .{ .data = content.wd };
83
-
while (iter.next()) |g| {
84
try self.grapheme.append(allocator, .{
85
-
.len = g.len,
86
-
.offset = @as(u32, @intCast(self.content.items.len)) + g.offset,
87
});
88
-
const cluster = g.bytes(content.bytes);
89
-
if (std.mem.eql(u8, cluster, "\n")) {
90
-
self.cols = @max(self.cols, cols);
91
-
cols = 0;
92
-
continue;
93
}
94
-
cols +|= dw.strWidth(cluster);
95
}
96
try self.content.appendSlice(allocator, content.bytes);
97
self.last_cols = cols;
98
self.cols = @max(self.cols, cols);
···
124
pub fn writer(
125
self: *@This(),
126
allocator: std.mem.Allocator,
127
-
gd: *const grapheme.GraphemeData,
128
-
wd: *const DisplayWidth.DisplayWidthData,
129
) BufferWriter.Writer {
130
return .{
131
.context = .{
132
.allocator = allocator,
133
.buffer = self,
134
-
.gd = gd,
135
-
.wd = wd,
136
},
137
};
138
}
···
184
};
185
186
self.scroll_view.writeCell(win, pos.x, pos.y, .{
187
-
.char = .{ .grapheme = cluster, .width = width },
188
.style = style,
189
});
190
}
···
1
const std = @import("std");
2
const vaxis = @import("../main.zig");
3
+
const uucode = @import("uucode");
4
const ScrollView = vaxis.widgets.ScrollView;
5
6
+
/// Simple grapheme representation to replace Graphemes.Grapheme
7
+
const Grapheme = struct {
8
+
len: u16,
9
+
offset: u32,
10
+
};
11
+
12
pub const BufferWriter = struct {
13
pub const Error = error{OutOfMemory};
14
pub const Writer = std.io.GenericWriter(@This(), Error, write);
15
16
allocator: std.mem.Allocator,
17
buffer: *Buffer,
18
19
pub fn write(self: @This(), bytes: []const u8) Error!usize {
20
try self.buffer.append(self.allocator, .{
21
.bytes = bytes,
22
});
23
return bytes.len;
24
}
···
34
35
pub const Content = struct {
36
bytes: []const u8,
37
};
38
39
pub const Style = struct {
···
44
45
pub const Error = error{OutOfMemory};
46
47
+
grapheme: std.MultiArrayList(Grapheme) = .{},
48
content: std.ArrayListUnmanaged(u8) = .{},
49
style_list: StyleList = .{},
50
style_map: StyleMap = .{},
···
77
/// Appends content to the buffer.
78
pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) Error!void {
79
var cols: usize = self.last_cols;
80
+
var iter = uucode.grapheme.Iterator(uucode.utf8.Iterator).init(.init(content.bytes));
81
+
82
+
var grapheme_start: usize = 0;
83
+
var prev_break: bool = true;
84
+
85
+
while (iter.next()) |result| {
86
+
if (prev_break and !result.is_break) {
87
+
// Start of a new grapheme
88
+
const cp_len: usize = std.unicode.utf8CodepointSequenceLength(result.cp) catch 1;
89
+
grapheme_start = iter.i - cp_len;
90
+
}
91
+
92
+
if (result.is_break) {
93
+
// End of a grapheme
94
+
const grapheme_end = iter.i;
95
+
const grapheme_len = grapheme_end - grapheme_start;
96
+
97
+
try self.grapheme.append(allocator, .{
98
+
.len = @intCast(grapheme_len),
99
+
.offset = @intCast(self.content.items.len + grapheme_start),
100
+
});
101
+
102
+
const cluster = content.bytes[grapheme_start..grapheme_end];
103
+
if (std.mem.eql(u8, cluster, "\n")) {
104
+
self.cols = @max(self.cols, cols);
105
+
cols = 0;
106
+
} else {
107
+
// Calculate width using gwidth
108
+
const w = vaxis.gwidth.gwidth(cluster, .unicode);
109
+
cols +|= w;
110
+
}
111
+
112
+
grapheme_start = grapheme_end;
113
+
}
114
+
prev_break = result.is_break;
115
+
}
116
+
117
+
// Flush the last grapheme if we ended mid-cluster
118
+
if (!prev_break and grapheme_start < content.bytes.len) {
119
+
const grapheme_len = content.bytes.len - grapheme_start;
120
+
121
try self.grapheme.append(allocator, .{
122
+
.len = @intCast(grapheme_len),
123
+
.offset = @intCast(self.content.items.len + grapheme_start),
124
});
125
+
126
+
const cluster = content.bytes[grapheme_start..];
127
+
if (!std.mem.eql(u8, cluster, "\n")) {
128
+
const w = vaxis.gwidth.gwidth(cluster, .unicode);
129
+
cols +|= w;
130
}
131
}
132
+
133
try self.content.appendSlice(allocator, content.bytes);
134
self.last_cols = cols;
135
self.cols = @max(self.cols, cols);
···
161
pub fn writer(
162
self: *@This(),
163
allocator: std.mem.Allocator,
164
) BufferWriter.Writer {
165
return .{
166
.context = .{
167
.allocator = allocator,
168
.buffer = self,
169
},
170
};
171
}
···
217
};
218
219
self.scroll_view.writeCell(win, pos.x, pos.y, .{
220
+
.char = .{ .grapheme = cluster, .width = @intCast(width) },
221
.style = style,
222
});
223
}
+157
src/widgets/View.zig
+157
src/widgets/View.zig
···
···
1
+
//! A View is effectively an "oversized" Window that can be written to and rendered in pieces.
2
+
3
+
const std = @import("std");
4
+
const mem = std.mem;
5
+
6
+
const View = @This();
7
+
8
+
const gw = @import("../gwidth.zig");
9
+
10
+
const Screen = @import("../Screen.zig");
11
+
const Window = @import("../Window.zig");
12
+
const unicode = @import("../unicode.zig");
13
+
const Cell = @import("../Cell.zig");
14
+
15
+
/// View Allocator
16
+
alloc: mem.Allocator,
17
+
18
+
/// Underlying Screen
19
+
screen: Screen,
20
+
21
+
/// View Initialization Config
22
+
pub const Config = struct {
23
+
width: u16,
24
+
height: u16,
25
+
};
26
+
27
+
/// Initialize a new View
28
+
pub fn init(alloc: mem.Allocator, config: Config) mem.Allocator.Error!View {
29
+
return .{
30
+
.alloc = alloc,
31
+
.screen = try Screen.init(alloc, .{
32
+
.cols = config.width,
33
+
.rows = config.height,
34
+
.x_pixel = 0,
35
+
.y_pixel = 0,
36
+
}),
37
+
};
38
+
}
39
+
40
+
pub fn window(self: *View) Window {
41
+
return .{
42
+
.x_off = 0,
43
+
.y_off = 0,
44
+
.parent_x_off = 0,
45
+
.parent_y_off = 0,
46
+
.width = self.screen.width,
47
+
.height = self.screen.height,
48
+
.screen = &self.screen,
49
+
};
50
+
}
51
+
52
+
/// Deinitialize this View
53
+
pub fn deinit(self: *View) void {
54
+
self.screen.deinit(self.alloc);
55
+
}
56
+
57
+
pub const DrawOptions = struct {
58
+
x_off: u16 = 0,
59
+
y_off: u16 = 0,
60
+
};
61
+
62
+
pub fn draw(self: *View, win: Window, opts: DrawOptions) void {
63
+
if (opts.x_off >= self.screen.width) return;
64
+
if (opts.y_off >= self.screen.height) return;
65
+
66
+
const width = @min(win.width, self.screen.width - opts.x_off);
67
+
const height = @min(win.height, self.screen.height - opts.y_off);
68
+
69
+
for (0..height) |_row| {
70
+
const row: i17 = @intCast(_row);
71
+
const src_start: usize = @intCast(opts.x_off + ((row + opts.y_off) * self.screen.width));
72
+
const src_end: usize = @intCast(src_start + width);
73
+
const dst_start: usize = @intCast(win.x_off + ((row + win.y_off) * win.screen.width));
74
+
const dst_end: usize = @intCast(dst_start + width);
75
+
@memcpy(win.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
76
+
}
77
+
}
78
+
79
+
/// Render Config for `toWin()`
80
+
pub const RenderConfig = struct {
81
+
x: u16 = 0,
82
+
y: u16 = 0,
83
+
width: Extent = .fit,
84
+
height: Extent = .fit,
85
+
86
+
pub const Extent = union(enum) {
87
+
fit,
88
+
max: u16,
89
+
};
90
+
};
91
+
92
+
/// Render a portion of this View to the provided Window (`win`).
93
+
/// This will return the bounded X (col), Y (row) coordinates based on the rendering.
94
+
pub fn toWin(self: *View, win: Window, config: RenderConfig) !struct { u16, u16 } {
95
+
var x = @min(self.screen.width - 1, config.x);
96
+
var y = @min(self.screen.height - 1, config.y);
97
+
const width = width: {
98
+
var width = switch (config.width) {
99
+
.fit => win.width,
100
+
.max => |w| @min(win.width, w),
101
+
};
102
+
width = @min(width, self.screen.width);
103
+
break :width @min(width, self.screen.width -| 1 -| x +| win.width);
104
+
};
105
+
const height = height: {
106
+
var height = switch (config.height) {
107
+
.fit => win.height,
108
+
.max => |h| @min(win.height, h),
109
+
};
110
+
height = @min(height, self.screen.height);
111
+
break :height @min(height, self.screen.height -| 1 -| y +| win.height);
112
+
};
113
+
x = @min(x, self.screen.width -| width);
114
+
y = @min(y, self.screen.height -| height);
115
+
const child = win.child(.{
116
+
.width = width,
117
+
.height = height,
118
+
});
119
+
self.draw(child, .{ .x_off = x, .y_off = y });
120
+
return .{ x, y };
121
+
}
122
+
123
+
/// Writes a cell to the location in the View
124
+
pub fn writeCell(self: *View, col: u16, row: u16, cell: Cell) void {
125
+
self.screen.writeCell(col, row, cell);
126
+
}
127
+
128
+
/// Reads a cell at the location in the View
129
+
pub fn readCell(self: *const View, col: u16, row: u16) ?Cell {
130
+
return self.screen.readCell(col, row);
131
+
}
132
+
133
+
/// Fills the View with the default cell
134
+
pub fn clear(self: View) void {
135
+
self.fill(.{ .default = true });
136
+
}
137
+
138
+
/// Returns the width of the grapheme. This depends on the terminal capabilities
139
+
pub fn gwidth(self: View, str: []const u8) u16 {
140
+
return gw.gwidth(str, self.screen.width_method);
141
+
}
142
+
143
+
/// Fills the View with the provided cell
144
+
pub fn fill(self: View, cell: Cell) void {
145
+
@memset(self.screen.buf, cell);
146
+
}
147
+
148
+
/// Prints segments to the View. Returns true if the text overflowed with the
149
+
/// given wrap strategy and size.
150
+
pub fn print(self: *View, segments: []const Cell.Segment, opts: Window.PrintOptions) Window.PrintResult {
151
+
return self.window().print(segments, opts);
152
+
}
153
+
154
+
/// Print a single segment. This is just a shortcut for print(&.{segment}, opts)
155
+
pub fn printSegment(self: *View, segment: Cell.Segment, opts: Window.PrintOptions) Window.PrintResult {
156
+
return self.print(&.{segment}, opts);
157
+
}
+31
-2
src/widgets/alignment.zig
+31
-2
src/widgets/alignment.zig
···
1
const Window = @import("../Window.zig");
2
3
-
pub fn center(parent: Window, cols: usize, rows: usize) Window {
4
const y_off = (parent.height / 2) -| (rows / 2);
5
const x_off = (parent.width / 2) -| (cols / 2);
6
-
return parent.initChild(x_off, y_off, .{ .limit = cols }, .{ .limit = rows });
7
}
···
1
const Window = @import("../Window.zig");
2
3
+
pub fn center(parent: Window, cols: u16, rows: u16) Window {
4
const y_off = (parent.height / 2) -| (rows / 2);
5
const x_off = (parent.width / 2) -| (cols / 2);
6
+
return parent.child(.{
7
+
.x_off = x_off,
8
+
.y_off = y_off,
9
+
.width = cols,
10
+
.height = rows,
11
+
});
12
+
}
13
+
14
+
pub fn topLeft(parent: Window, cols: u16, rows: u16) Window {
15
+
const y_off: u16 = 0;
16
+
const x_off: u16 = 0;
17
+
return parent.child(.{ .x_off = x_off, .y_off = y_off, .width = cols, .height = rows });
18
+
}
19
+
20
+
pub fn topRight(parent: Window, cols: u16, rows: u16) Window {
21
+
const y_off: u16 = 0;
22
+
const x_off = parent.width -| cols;
23
+
return parent.child(.{ .x_off = x_off, .y_off = y_off, .width = cols, .height = rows });
24
+
}
25
+
26
+
pub fn bottomLeft(parent: Window, cols: u16, rows: u16) Window {
27
+
const y_off = parent.height -| rows;
28
+
const x_off: u16 = 0;
29
+
return parent.child(.{ .x_off = x_off, .y_off = y_off, .width = cols, .height = rows });
30
+
}
31
+
32
+
pub fn bottomRight(parent: Window, cols: u16, rows: u16) Window {
33
+
const y_off = parent.height -| rows;
34
+
const x_off = parent.width -| cols;
35
+
return parent.child(.{ .x_off = x_off, .y_off = y_off, .width = cols, .height = rows });
36
}
-52
src/widgets/border.zig
-52
src/widgets/border.zig
···
1
-
const Cell = @import("../Cell.zig");
2
-
const Window = @import("../Window.zig");
3
-
4
-
const Style = Cell.Style;
5
-
const Character = Cell.Character;
6
-
7
-
const horizontal = Character{ .grapheme = "โ", .width = 1 };
8
-
const vertical = Character{ .grapheme = "โ", .width = 1 };
9
-
const top_left = Character{ .grapheme = "โญ", .width = 1 };
10
-
const top_right = Character{ .grapheme = "โฎ", .width = 1 };
11
-
const bottom_right = Character{ .grapheme = "โฏ", .width = 1 };
12
-
const bottom_left = Character{ .grapheme = "โฐ", .width = 1 };
13
-
14
-
pub fn all(win: Window, style: Style) Window {
15
-
const h = win.height;
16
-
const w = win.width;
17
-
win.writeCell(0, 0, .{ .char = top_left, .style = style });
18
-
win.writeCell(0, h -| 1, .{ .char = bottom_left, .style = style });
19
-
win.writeCell(w -| 1, 0, .{ .char = top_right, .style = style });
20
-
win.writeCell(w -| 1, h -| 1, .{ .char = bottom_right, .style = style });
21
-
var i: usize = 1;
22
-
while (i < (h -| 1)) : (i += 1) {
23
-
win.writeCell(0, i, .{ .char = vertical, .style = style });
24
-
win.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
25
-
}
26
-
i = 1;
27
-
while (i < w -| 1) : (i += 1) {
28
-
win.writeCell(i, 0, .{ .char = horizontal, .style = style });
29
-
win.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
30
-
}
31
-
return win.initChild(1, 1, .{ .limit = w -| 2 }, .{ .limit = h -| 2 });
32
-
}
33
-
34
-
pub fn right(win: Window, style: Style) Window {
35
-
const h = win.height;
36
-
const w = win.width;
37
-
var i: usize = 0;
38
-
while (i < h) : (i += 1) {
39
-
win.writeCell(w -| 1, i, .{ .char = vertical, .style = style });
40
-
}
41
-
return win.initChild(0, 0, .{ .limit = w -| 1 }, .expand);
42
-
}
43
-
44
-
pub fn bottom(win: Window, style: Style) Window {
45
-
const h = win.height;
46
-
const w = win.width;
47
-
var i: usize = 0;
48
-
while (i < w) : (i += 1) {
49
-
win.writeCell(i, h -| 1, .{ .char = horizontal, .style = style });
50
-
}
51
-
return win.initChild(0, 0, .expand, .{ .limit = h -| 1 });
52
-
}
···
+10
-10
src/widgets/terminal/Command.zig
+10
-10
src/widgets/terminal/Command.zig
···
36
37
// set the controlling terminal
38
var u: c_uint = std.posix.STDIN_FILENO;
39
-
if (posix.system.ioctl(self.pty.tty, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
40
41
// set up io
42
-
try posix.dup2(self.pty.tty, std.posix.STDIN_FILENO);
43
-
try posix.dup2(self.pty.tty, std.posix.STDOUT_FILENO);
44
-
try posix.dup2(self.pty.tty, std.posix.STDERR_FILENO);
45
46
-
posix.close(self.pty.tty);
47
-
if (self.pty.pty > 2) posix.close(self.pty.pty);
48
49
if (self.working_directory) |wd| {
50
try std.posix.chdir(wd);
···
64
.handler = .{ .handler = handleSigChild },
65
.mask = switch (builtin.os.tag) {
66
.macos => 0,
67
-
.linux => posix.empty_sigset,
68
else => @compileError("os not supported"),
69
},
70
.flags = 0,
71
};
72
-
try posix.sigaction(posix.SIG.CHLD, &act, null);
73
}
74
75
return;
76
}
77
78
-
fn handleSigChild(_: c_int) callconv(.C) void {
79
const result = std.posix.waitpid(-1, 0);
80
81
Terminal.global_vt_mutex.lock();
···
107
{
108
var it = map.iterator();
109
while (it.next()) |pair| {
110
-
envp_buf[i] = try std.fmt.allocPrintZ(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* });
111
i += 1;
112
}
113
}
···
36
37
// set the controlling terminal
38
var u: c_uint = std.posix.STDIN_FILENO;
39
+
if (posix.system.ioctl(self.pty.tty.handle, posix.T.IOCSCTTY, @intFromPtr(&u)) != 0) return error.IoctlError;
40
41
// set up io
42
+
try posix.dup2(self.pty.tty.handle, std.posix.STDIN_FILENO);
43
+
try posix.dup2(self.pty.tty.handle, std.posix.STDOUT_FILENO);
44
+
try posix.dup2(self.pty.tty.handle, std.posix.STDERR_FILENO);
45
46
+
self.pty.tty.close();
47
+
if (self.pty.pty.handle > 2) self.pty.pty.close();
48
49
if (self.working_directory) |wd| {
50
try std.posix.chdir(wd);
···
64
.handler = .{ .handler = handleSigChild },
65
.mask = switch (builtin.os.tag) {
66
.macos => 0,
67
+
.linux => posix.sigemptyset(),
68
else => @compileError("os not supported"),
69
},
70
.flags = 0,
71
};
72
+
posix.sigaction(posix.SIG.CHLD, &act, null);
73
}
74
75
return;
76
}
77
78
+
fn handleSigChild(_: c_int) callconv(.c) void {
79
const result = std.posix.waitpid(-1, 0);
80
81
Terminal.global_vt_mutex.lock();
···
107
{
108
var it = map.iterator();
109
while (it.next()) |pair| {
110
+
envp_buf[i] = try std.fmt.allocPrintSentinel(arena, "{s}={s}", .{ pair.key_ptr.*, pair.value_ptr.* }, 0);
111
i += 1;
112
}
113
}
+26
-28
src/widgets/terminal/Parser.zig
+26
-28
src/widgets/terminal/Parser.zig
···
2
const Parser = @This();
3
4
const std = @import("std");
5
-
const Reader = std.io.AnyReader;
6
const ansi = @import("ansi.zig");
7
-
const BufferedReader = std.io.BufferedReader(4096, std.io.AnyReader);
8
9
/// A terminal event
10
const Event = union(enum) {
···
18
apc: []const u8,
19
};
20
21
-
buf: std.ArrayList(u8),
22
/// a leftover byte from a ground event
23
pending_byte: ?u8 = null,
24
25
-
pub fn parseReader(self: *Parser, buffered: *BufferedReader) !Event {
26
-
const reader = buffered.reader().any();
27
self.buf.clearRetainingCapacity();
28
while (true) {
29
-
const b = if (self.pending_byte) |p| p else try reader.readByte();
30
self.pending_byte = null;
31
switch (b) {
32
// Escape sequence
33
0x1b => {
34
-
const next = try reader.readByte();
35
switch (next) {
36
-
0x4E => return .{ .ss2 = try reader.readByte() },
37
-
0x4F => return .{ .ss3 = try reader.readByte() },
38
0x50 => try skipUntilST(reader), // DCS
39
0x58 => try skipUntilST(reader), // SOS
40
0x5B => return self.parseCsi(reader), // CSI
···
58
=> return .{ .c0 = @enumFromInt(b) },
59
else => {
60
try self.buf.append(b);
61
-
return self.parseGround(buffered);
62
},
63
}
64
}
65
}
66
67
-
inline fn parseGround(self: *Parser, reader: *BufferedReader) !Event {
68
var buf: [1]u8 = undefined;
69
{
70
std.debug.assert(self.buf.items.len > 0);
···
72
const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]);
73
var i: usize = 1;
74
while (i < len) : (i += 1) {
75
-
const read = try reader.read(&buf);
76
if (read == 0) return error.EOF;
77
try self.buf.append(buf[0]);
78
}
79
}
80
while (true) {
81
-
if (reader.start == reader.end) return .{ .print = self.buf.items };
82
-
const n = try reader.read(&buf);
83
if (n == 0) return error.EOF;
84
const b = buf[0];
85
switch (b) {
···
92
const len = try std.unicode.utf8ByteSequenceLength(b);
93
var i: usize = 1;
94
while (i < len) : (i += 1) {
95
-
const read = try reader.read(&buf);
96
if (read == 0) return error.EOF;
97
98
try self.buf.append(buf[0]);
···
103
}
104
105
/// parse until b >= 0x30
106
-
inline fn parseEscape(self: *Parser, reader: Reader) !Event {
107
while (true) {
108
-
const b = try reader.readByte();
109
switch (b) {
110
0x20...0x2F => continue,
111
else => {
···
116
}
117
}
118
119
-
inline fn parseApc(self: *Parser, reader: Reader) !Event {
120
while (true) {
121
-
const b = try reader.readByte();
122
switch (b) {
123
0x00...0x17,
124
0x19,
125
0x1c...0x1f,
126
=> continue,
127
0x1b => {
128
-
try reader.skipBytes(1, .{ .buf_size = 1 });
129
return .{ .apc = self.buf.items };
130
},
131
else => try self.buf.append(b),
···
134
}
135
136
/// Skips sequences until we see an ST (String Terminator, ESC \)
137
-
inline fn skipUntilST(reader: Reader) !void {
138
-
try reader.skipUntilDelimiterOrEof('\x1b');
139
-
try reader.skipBytes(1, .{ .buf_size = 1 });
140
}
141
142
/// Parses an OSC sequence
143
-
inline fn parseOsc(self: *Parser, reader: Reader) !Event {
144
while (true) {
145
-
const b = try reader.readByte();
146
switch (b) {
147
0x00...0x06,
148
0x08...0x17,
···
150
0x1c...0x1f,
151
=> continue,
152
0x1b => {
153
-
try reader.skipBytes(1, .{ .buf_size = 1 });
154
return .{ .osc = self.buf.items };
155
},
156
0x07 => return .{ .osc = self.buf.items },
···
159
}
160
}
161
162
-
inline fn parseCsi(self: *Parser, reader: Reader) !Event {
163
var intermediate: ?u8 = null;
164
var pm: ?u8 = null;
165
166
while (true) {
167
-
const b = try reader.readByte();
168
switch (b) {
169
0x20...0x2F => intermediate = b,
170
0x30...0x3B => try self.buf.append(b),
···
2
const Parser = @This();
3
4
const std = @import("std");
5
+
const Reader = std.Io.Reader;
6
const ansi = @import("ansi.zig");
7
8
/// A terminal event
9
const Event = union(enum) {
···
17
apc: []const u8,
18
};
19
20
+
buf: std.array_list.Managed(u8),
21
/// a leftover byte from a ground event
22
pending_byte: ?u8 = null,
23
24
+
pub fn parseReader(self: *Parser, reader: *Reader) !Event {
25
self.buf.clearRetainingCapacity();
26
while (true) {
27
+
const b = if (self.pending_byte) |p| p else try reader.takeByte();
28
self.pending_byte = null;
29
switch (b) {
30
// Escape sequence
31
0x1b => {
32
+
const next = try reader.takeByte();
33
switch (next) {
34
+
0x4E => return .{ .ss2 = try reader.takeByte() },
35
+
0x4F => return .{ .ss3 = try reader.takeByte() },
36
0x50 => try skipUntilST(reader), // DCS
37
0x58 => try skipUntilST(reader), // SOS
38
0x5B => return self.parseCsi(reader), // CSI
···
56
=> return .{ .c0 = @enumFromInt(b) },
57
else => {
58
try self.buf.append(b);
59
+
return self.parseGround(reader);
60
},
61
}
62
}
63
}
64
65
+
inline fn parseGround(self: *Parser, reader: *Reader) !Event {
66
var buf: [1]u8 = undefined;
67
{
68
std.debug.assert(self.buf.items.len > 0);
···
70
const len = try std.unicode.utf8ByteSequenceLength(self.buf.items[0]);
71
var i: usize = 1;
72
while (i < len) : (i += 1) {
73
+
const read = try reader.readSliceShort(&buf);
74
if (read == 0) return error.EOF;
75
try self.buf.append(buf[0]);
76
}
77
}
78
while (true) {
79
+
if (reader.bufferedLen() == 0) return .{ .print = self.buf.items };
80
+
const n = try reader.readSliceShort(&buf);
81
if (n == 0) return error.EOF;
82
const b = buf[0];
83
switch (b) {
···
90
const len = try std.unicode.utf8ByteSequenceLength(b);
91
var i: usize = 1;
92
while (i < len) : (i += 1) {
93
+
const read = try reader.readSliceShort(&buf);
94
if (read == 0) return error.EOF;
95
96
try self.buf.append(buf[0]);
···
101
}
102
103
/// parse until b >= 0x30
104
+
inline fn parseEscape(self: *Parser, reader: *Reader) !Event {
105
while (true) {
106
+
const b = try reader.takeByte();
107
switch (b) {
108
0x20...0x2F => continue,
109
else => {
···
114
}
115
}
116
117
+
inline fn parseApc(self: *Parser, reader: *Reader) !Event {
118
while (true) {
119
+
const b = try reader.takeByte();
120
switch (b) {
121
0x00...0x17,
122
0x19,
123
0x1c...0x1f,
124
=> continue,
125
0x1b => {
126
+
_ = try reader.discard(std.Io.Limit.limited(1));
127
return .{ .apc = self.buf.items };
128
},
129
else => try self.buf.append(b),
···
132
}
133
134
/// Skips sequences until we see an ST (String Terminator, ESC \)
135
+
inline fn skipUntilST(reader: *Reader) !void {
136
+
_ = try reader.discardDelimiterExclusive('\x1b');
137
+
_ = try reader.discard(std.Io.Limit.limited(1));
138
}
139
140
/// Parses an OSC sequence
141
+
inline fn parseOsc(self: *Parser, reader: *Reader) !Event {
142
while (true) {
143
+
const b = try reader.takeByte();
144
switch (b) {
145
0x00...0x06,
146
0x08...0x17,
···
148
0x1c...0x1f,
149
=> continue,
150
0x1b => {
151
+
_ = try reader.discard(std.Io.Limit.limited(1));
152
return .{ .osc = self.buf.items };
153
},
154
0x07 => return .{ .osc = self.buf.items },
···
157
}
158
}
159
160
+
inline fn parseCsi(self: *Parser, reader: *Reader) !Event {
161
var intermediate: ?u8 = null;
162
var pm: ?u8 = null;
163
164
while (true) {
165
+
const b = try reader.takeByte();
166
switch (b) {
167
0x20...0x2F => intermediate = b,
168
0x30...0x3B => try self.buf.append(b),
+12
-12
src/widgets/terminal/Pty.zig
+12
-12
src/widgets/terminal/Pty.zig
···
7
8
const posix = std.posix;
9
10
-
pty: posix.fd_t,
11
-
tty: posix.fd_t,
12
13
/// opens a new tty/pty pair
14
pub fn init() !Pty {
···
20
21
/// closes the tty and pty
22
pub fn deinit(self: Pty) void {
23
-
posix.close(self.pty);
24
-
posix.close(self.tty);
25
}
26
27
/// sets the size of the pty
28
pub fn setSize(self: Pty, ws: Winsize) !void {
29
const _ws: posix.winsize = .{
30
-
.ws_row = @truncate(ws.rows),
31
-
.ws_col = @truncate(ws.cols),
32
-
.ws_xpixel = @truncate(ws.x_pixel),
33
-
.ws_ypixel = @truncate(ws.y_pixel),
34
};
35
-
if (posix.system.ioctl(self.pty, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
36
return error.SetWinsizeError;
37
}
38
···
48
if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError;
49
var buf: [16]u8 = undefined;
50
const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n});
51
-
std.log.err("pts: {s}", .{sname});
52
53
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
54
55
return .{
56
-
.pty = p,
57
-
.tty = t,
58
};
59
}
···
7
8
const posix = std.posix;
9
10
+
pty: std.fs.File,
11
+
tty: std.fs.File,
12
13
/// opens a new tty/pty pair
14
pub fn init() !Pty {
···
20
21
/// closes the tty and pty
22
pub fn deinit(self: Pty) void {
23
+
self.pty.close();
24
+
self.tty.close();
25
}
26
27
/// sets the size of the pty
28
pub fn setSize(self: Pty, ws: Winsize) !void {
29
const _ws: posix.winsize = .{
30
+
.row = @truncate(ws.rows),
31
+
.col = @truncate(ws.cols),
32
+
.xpixel = @truncate(ws.x_pixel),
33
+
.ypixel = @truncate(ws.y_pixel),
34
};
35
+
if (posix.system.ioctl(self.pty.handle, posix.T.IOCSWINSZ, @intFromPtr(&_ws)) != 0)
36
return error.SetWinsizeError;
37
}
38
···
48
if (posix.system.ioctl(p, posix.T.IOCGPTN, @intFromPtr(&n)) != 0) return error.IoctlError;
49
var buf: [16]u8 = undefined;
50
const sname = try std.fmt.bufPrint(&buf, "/dev/pts/{d}", .{n});
51
+
std.log.debug("pts: {s}", .{sname});
52
53
const t = try posix.open(sname, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0);
54
55
return .{
56
+
.pty = .{ .handle = p },
57
+
.tty = .{ .handle = t },
58
};
59
}
+47
-46
src/widgets/terminal/Screen.zig
+47
-46
src/widgets/terminal/Screen.zig
···
9
const Screen = @This();
10
11
pub const Cell = struct {
12
-
char: std.ArrayList(u8) = undefined,
13
style: vaxis.Style = .{},
14
-
uri: std.ArrayList(u8) = undefined,
15
-
uri_id: std.ArrayList(u8) = undefined,
16
width: u8 = 1,
17
18
wrapped: bool = false,
19
dirty: bool = true,
20
21
-
pub fn erase(self: *Cell, bg: vaxis.Color) void {
22
self.char.clearRetainingCapacity();
23
-
self.char.append(' ') catch unreachable; // we never completely free this list
24
self.style = .{};
25
self.style.bg = bg;
26
self.uri.clearRetainingCapacity();
···
30
self.dirty = true;
31
}
32
33
-
pub fn copyFrom(self: *Cell, src: Cell) !void {
34
self.char.clearRetainingCapacity();
35
-
try self.char.appendSlice(src.char.items);
36
self.style = src.style;
37
self.uri.clearRetainingCapacity();
38
-
try self.uri.appendSlice(src.uri.items);
39
self.uri_id.clearRetainingCapacity();
40
-
try self.uri_id.appendSlice(src.uri_id.items);
41
self.width = src.width;
42
self.wrapped = src.wrapped;
43
···
49
style: vaxis.Style = .{},
50
uri: std.ArrayList(u8) = undefined,
51
uri_id: std.ArrayList(u8) = undefined,
52
-
col: usize = 0,
53
-
row: usize = 0,
54
pending_wrap: bool = false,
55
shape: vaxis.Cell.CursorShape = .default,
56
visible: bool = true,
···
68
};
69
70
pub const ScrollingRegion = struct {
71
-
top: usize,
72
-
bottom: usize,
73
-
left: usize,
74
-
right: usize,
75
76
pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool {
77
return col >= self.left and
···
81
}
82
};
83
84
-
width: usize = 0,
85
-
height: usize = 0,
86
87
scrolling_region: ScrollingRegion,
88
···
93
csi_u_flags: vaxis.Key.KittyFlags = @bitCast(@as(u5, 0)),
94
95
/// sets each cell to the default cell
96
-
pub fn init(alloc: std.mem.Allocator, w: usize, h: usize) !Screen {
97
var screen = Screen{
98
-
.buf = try alloc.alloc(Cell, w * h),
99
.scrolling_region = .{
100
.top = 0,
101
.bottom = h - 1,
···
107
};
108
for (screen.buf, 0..) |_, i| {
109
screen.buf[i] = .{
110
-
.char = try std.ArrayList(u8).initCapacity(alloc, 1),
111
-
.uri = std.ArrayList(u8).init(alloc),
112
-
.uri_id = std.ArrayList(u8).init(alloc),
113
};
114
-
try screen.buf[i].char.append(' ');
115
}
116
return screen;
117
}
118
119
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
120
for (self.buf, 0..) |_, i| {
121
-
self.buf[i].char.deinit();
122
-
self.buf[i].uri.deinit();
123
-
self.buf[i].uri_id.deinit();
124
}
125
126
alloc.free(self.buf);
127
}
128
129
/// copies the visible area to the destination screen
130
-
pub fn copyTo(self: *Screen, dst: *Screen) !void {
131
dst.cursor = self.cursor;
132
for (self.buf, 0..) |cell, i| {
133
if (!cell.dirty) continue;
134
self.buf[i].dirty = false;
135
const grapheme = cell.char.items;
136
dst.buf[i].char.clearRetainingCapacity();
137
-
try dst.buf[i].char.appendSlice(grapheme);
138
dst.buf[i].width = cell.width;
139
dst.buf[i].style = cell.style;
140
}
···
182
const i = (row * self.width) + col;
183
assert(i < self.buf.len);
184
self.buf[i].char.clearRetainingCapacity();
185
-
self.buf[i].char.appendSlice(grapheme) catch {
186
log.warn("couldn't write grapheme", .{});
187
};
188
self.buf[i].uri.clearRetainingCapacity();
189
-
self.buf[i].uri.appendSlice(self.cursor.uri.items) catch {
190
log.warn("couldn't write uri", .{});
191
};
192
self.buf[i].uri_id.clearRetainingCapacity();
193
-
self.buf[i].uri_id.appendSlice(self.cursor.uri_id.items) catch {
194
log.warn("couldn't write uri_id", .{});
195
};
196
self.buf[i].style = self.cursor.style;
···
313
}
314
}
315
316
-
pub fn cursorUp(self: *Screen, n: usize) void {
317
self.cursor.pending_wrap = false;
318
if (self.withinScrollingRegion())
319
self.cursor.row = @max(
···
324
self.cursor.row -|= n;
325
}
326
327
-
pub fn cursorLeft(self: *Screen, n: usize) void {
328
self.cursor.pending_wrap = false;
329
if (self.withinScrollingRegion())
330
self.cursor.col = @max(
···
335
self.cursor.col = self.cursor.col -| n;
336
}
337
338
-
pub fn cursorRight(self: *Screen, n: usize) void {
339
self.cursor.pending_wrap = false;
340
if (self.withinScrollingRegion())
341
self.cursor.col = @min(
···
368
const end = (self.cursor.row * self.width) + (self.width);
369
var i = (self.cursor.row * self.width) + self.cursor.col;
370
while (i < end) : (i += 1) {
371
-
self.buf[i].erase(self.cursor.style.bg);
372
}
373
}
374
···
378
const end = start + self.cursor.col + 1;
379
var i = start;
380
while (i < end) : (i += 1) {
381
-
self.buf[i].erase(self.cursor.style.bg);
382
}
383
}
384
···
388
const end = start + self.width;
389
var i = start;
390
while (i < end) : (i += 1) {
391
-
self.buf[i].erase(self.cursor.style.bg);
392
}
393
}
394
···
411
while (col <= self.scrolling_region.right) : (col += 1) {
412
const i = (row * self.width) + col;
413
if (row + cnt > self.scrolling_region.bottom)
414
-
self.buf[i].erase(self.cursor.style.bg)
415
else
416
-
try self.buf[i].copyFrom(self.buf[i + stride]);
417
}
418
}
419
}
···
434
var col: usize = self.scrolling_region.left;
435
while (col <= self.scrolling_region.right) : (col += 1) {
436
const i = (row * self.width) + col;
437
-
try self.buf[i].copyFrom(self.buf[i - stride]);
438
}
439
}
440
···
443
var col: usize = self.scrolling_region.left;
444
while (col <= self.scrolling_region.right) : (col += 1) {
445
const i = (row * self.width) + col;
446
-
self.buf[i].erase(self.cursor.style.bg);
447
}
448
}
449
}
···
454
const start = (self.cursor.row * self.width) + (self.width);
455
var i = start;
456
while (i < self.buf.len) : (i += 1) {
457
-
self.buf[i].erase(self.cursor.style.bg);
458
}
459
}
460
···
465
const end = self.cursor.row * self.width;
466
var i = start;
467
while (i < end) : (i += 1) {
468
-
self.buf[i].erase(self.cursor.style.bg);
469
}
470
}
471
472
pub fn eraseAll(self: *Screen) void {
473
var i: usize = 0;
474
while (i < self.buf.len) : (i += 1) {
475
-
self.buf[i].erase(self.cursor.style.bg);
476
}
477
}
478
···
483
var col = self.cursor.col;
484
while (col <= self.scrolling_region.right) : (col += 1) {
485
if (col + n <= self.scrolling_region.right)
486
-
try self.buf[col].copyFrom(self.buf[col + n])
487
else
488
-
self.buf[col].erase(self.cursor.style.bg);
489
}
490
}
491
···
9
const Screen = @This();
10
11
pub const Cell = struct {
12
+
char: std.ArrayList(u8) = .empty,
13
style: vaxis.Style = .{},
14
+
uri: std.ArrayList(u8) = .empty,
15
+
uri_id: std.ArrayList(u8) = .empty,
16
width: u8 = 1,
17
18
wrapped: bool = false,
19
dirty: bool = true,
20
21
+
pub fn erase(self: *Cell, allocator: std.mem.Allocator, bg: vaxis.Color) void {
22
self.char.clearRetainingCapacity();
23
+
self.char.append(allocator, ' ') catch unreachable; // we never completely free this list
24
self.style = .{};
25
self.style.bg = bg;
26
self.uri.clearRetainingCapacity();
···
30
self.dirty = true;
31
}
32
33
+
pub fn copyFrom(self: *Cell, allocator: std.mem.Allocator, src: Cell) !void {
34
self.char.clearRetainingCapacity();
35
+
try self.char.appendSlice(allocator, src.char.items);
36
self.style = src.style;
37
self.uri.clearRetainingCapacity();
38
+
try self.uri.appendSlice(allocator, src.uri.items);
39
self.uri_id.clearRetainingCapacity();
40
+
try self.uri_id.appendSlice(allocator, src.uri_id.items);
41
self.width = src.width;
42
self.wrapped = src.wrapped;
43
···
49
style: vaxis.Style = .{},
50
uri: std.ArrayList(u8) = undefined,
51
uri_id: std.ArrayList(u8) = undefined,
52
+
col: u16 = 0,
53
+
row: u16 = 0,
54
pending_wrap: bool = false,
55
shape: vaxis.Cell.CursorShape = .default,
56
visible: bool = true,
···
68
};
69
70
pub const ScrollingRegion = struct {
71
+
top: u16,
72
+
bottom: u16,
73
+
left: u16,
74
+
right: u16,
75
76
pub fn contains(self: ScrollingRegion, col: usize, row: usize) bool {
77
return col >= self.left and
···
81
}
82
};
83
84
+
allocator: std.mem.Allocator,
85
+
86
+
width: u16 = 0,
87
+
height: u16 = 0,
88
89
scrolling_region: ScrollingRegion,
90
···
95
csi_u_flags: vaxis.Key.KittyFlags = @bitCast(@as(u5, 0)),
96
97
/// sets each cell to the default cell
98
+
pub fn init(alloc: std.mem.Allocator, w: u16, h: u16) !Screen {
99
var screen = Screen{
100
+
.allocator = alloc,
101
+
.buf = try alloc.alloc(Cell, @as(usize, @intCast(w)) * h),
102
.scrolling_region = .{
103
.top = 0,
104
.bottom = h - 1,
···
110
};
111
for (screen.buf, 0..) |_, i| {
112
screen.buf[i] = .{
113
+
.char = try .initCapacity(alloc, 1),
114
};
115
+
try screen.buf[i].char.append(alloc, ' ');
116
}
117
return screen;
118
}
119
120
pub fn deinit(self: *Screen, alloc: std.mem.Allocator) void {
121
for (self.buf, 0..) |_, i| {
122
+
self.buf[i].char.deinit(alloc);
123
+
self.buf[i].uri.deinit(alloc);
124
+
self.buf[i].uri_id.deinit(alloc);
125
}
126
127
alloc.free(self.buf);
128
}
129
130
/// copies the visible area to the destination screen
131
+
pub fn copyTo(self: *Screen, allocator: std.mem.Allocator, dst: *Screen) !void {
132
dst.cursor = self.cursor;
133
for (self.buf, 0..) |cell, i| {
134
if (!cell.dirty) continue;
135
self.buf[i].dirty = false;
136
const grapheme = cell.char.items;
137
dst.buf[i].char.clearRetainingCapacity();
138
+
try dst.buf[i].char.appendSlice(allocator, grapheme);
139
dst.buf[i].width = cell.width;
140
dst.buf[i].style = cell.style;
141
}
···
183
const i = (row * self.width) + col;
184
assert(i < self.buf.len);
185
self.buf[i].char.clearRetainingCapacity();
186
+
self.buf[i].char.appendSlice(self.allocator, grapheme) catch {
187
log.warn("couldn't write grapheme", .{});
188
};
189
self.buf[i].uri.clearRetainingCapacity();
190
+
self.buf[i].uri.appendSlice(self.allocator, self.cursor.uri.items) catch {
191
log.warn("couldn't write uri", .{});
192
};
193
self.buf[i].uri_id.clearRetainingCapacity();
194
+
self.buf[i].uri_id.appendSlice(self.allocator, self.cursor.uri_id.items) catch {
195
log.warn("couldn't write uri_id", .{});
196
};
197
self.buf[i].style = self.cursor.style;
···
314
}
315
}
316
317
+
pub fn cursorUp(self: *Screen, n: u16) void {
318
self.cursor.pending_wrap = false;
319
if (self.withinScrollingRegion())
320
self.cursor.row = @max(
···
325
self.cursor.row -|= n;
326
}
327
328
+
pub fn cursorLeft(self: *Screen, n: u16) void {
329
self.cursor.pending_wrap = false;
330
if (self.withinScrollingRegion())
331
self.cursor.col = @max(
···
336
self.cursor.col = self.cursor.col -| n;
337
}
338
339
+
pub fn cursorRight(self: *Screen, n: u16) void {
340
self.cursor.pending_wrap = false;
341
if (self.withinScrollingRegion())
342
self.cursor.col = @min(
···
369
const end = (self.cursor.row * self.width) + (self.width);
370
var i = (self.cursor.row * self.width) + self.cursor.col;
371
while (i < end) : (i += 1) {
372
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
373
}
374
}
375
···
379
const end = start + self.cursor.col + 1;
380
var i = start;
381
while (i < end) : (i += 1) {
382
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
383
}
384
}
385
···
389
const end = start + self.width;
390
var i = start;
391
while (i < end) : (i += 1) {
392
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
393
}
394
}
395
···
412
while (col <= self.scrolling_region.right) : (col += 1) {
413
const i = (row * self.width) + col;
414
if (row + cnt > self.scrolling_region.bottom)
415
+
self.buf[i].erase(self.allocator, self.cursor.style.bg)
416
else
417
+
try self.buf[i].copyFrom(self.allocator, self.buf[i + stride]);
418
}
419
}
420
}
···
435
var col: usize = self.scrolling_region.left;
436
while (col <= self.scrolling_region.right) : (col += 1) {
437
const i = (row * self.width) + col;
438
+
try self.buf[i].copyFrom(self.allocator, self.buf[i - stride]);
439
}
440
}
441
···
444
var col: usize = self.scrolling_region.left;
445
while (col <= self.scrolling_region.right) : (col += 1) {
446
const i = (row * self.width) + col;
447
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
448
}
449
}
450
}
···
455
const start = (self.cursor.row * self.width) + (self.width);
456
var i = start;
457
while (i < self.buf.len) : (i += 1) {
458
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
459
}
460
}
461
···
466
const end = self.cursor.row * self.width;
467
var i = start;
468
while (i < end) : (i += 1) {
469
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
470
}
471
}
472
473
pub fn eraseAll(self: *Screen) void {
474
var i: usize = 0;
475
while (i < self.buf.len) : (i += 1) {
476
+
self.buf[i].erase(self.allocator, self.cursor.style.bg);
477
}
478
}
479
···
484
var col = self.cursor.col;
485
while (col <= self.scrolling_region.right) : (col += 1) {
486
if (col + n <= self.scrolling_region.right)
487
+
try self.buf[col].copyFrom(self.allocator, self.buf[col + n])
488
else
489
+
self.buf[col].erase(self.allocator, self.cursor.style.bg);
490
}
491
}
492
+69
-79
src/widgets/terminal/Terminal.zig
+69
-79
src/widgets/terminal/Terminal.zig
···
10
const vaxis = @import("../../main.zig");
11
const Winsize = vaxis.Winsize;
12
const Screen = @import("Screen.zig");
13
-
const DisplayWidth = @import("DisplayWidth");
14
const Key = vaxis.Key;
15
const Queue = vaxis.Queue(Event, 16);
16
-
const code_point = @import("code_point");
17
const key = @import("key.zig");
18
19
pub const Event = union(enum) {
···
24
pwd_change: []const u8,
25
};
26
27
-
const grapheme = @import("grapheme");
28
-
29
const posix = std.posix;
30
31
const log = std.log.scoped(.terminal);
32
33
pub const Options = struct {
34
-
scrollback_size: usize = 500,
35
winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
36
initial_working_directory: ?[]const u8 = null,
37
};
···
52
pub var global_sigchild_installed: bool = false;
53
54
allocator: std.mem.Allocator,
55
-
scrollback_size: usize,
56
57
pty: Pty,
58
cmd: Command,
59
thread: ?std.Thread = null,
60
···
72
// dirty is protected by back_mutex. Only access this field when you hold that mutex
73
dirty: bool = false,
74
75
-
unicode: *const vaxis.Unicode,
76
should_quit: bool = false,
77
78
mode: Mode = .{},
79
80
tab_stops: std.ArrayList(u16),
81
-
title: std.ArrayList(u8),
82
-
working_directory: std.ArrayList(u8),
83
84
last_printed: []const u8 = "",
85
···
91
allocator: std.mem.Allocator,
92
argv: []const []const u8,
93
env: *const std.process.EnvMap,
94
-
unicode: *const vaxis.Unicode,
95
opts: Options,
96
) !Terminal {
97
// Verify we have an absolute path
98
if (opts.initial_working_directory) |pwd| {
···
106
.pty = pty,
107
.working_directory = opts.initial_working_directory,
108
};
109
-
var tabs = try std.ArrayList(u16).initCapacity(allocator, opts.winsize.cols / 8);
110
var col: u16 = 0;
111
while (col < opts.winsize.cols) : (col += 8) {
112
-
try tabs.append(col);
113
}
114
return .{
115
.allocator = allocator,
116
.pty = pty,
117
.cmd = cmd,
118
.scrollback_size = opts.scrollback_size,
119
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
120
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
121
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
122
-
.unicode = unicode,
123
.tab_stops = tabs,
124
-
.title = std.ArrayList(u8).init(allocator),
125
-
.working_directory = std.ArrayList(u8).init(allocator),
126
};
127
}
128
···
145
if (self.thread) |thread| {
146
// write an EOT into the tty to trigger a read on our thread
147
const EOT = "\x04";
148
-
_ = std.posix.write(self.pty.tty, EOT) catch {};
149
thread.join();
150
self.thread = null;
151
}
···
153
self.front_screen.deinit(self.allocator);
154
self.back_screen_pri.deinit(self.allocator);
155
self.back_screen_alt.deinit(self.allocator);
156
-
self.tab_stops.deinit();
157
-
self.title.deinit();
158
-
self.working_directory.deinit();
159
}
160
161
pub fn spawn(self: *Terminal) !void {
···
166
167
self.working_directory.clearRetainingCapacity();
168
if (self.cmd.working_directory) |pwd| {
169
-
try self.working_directory.appendSlice(pwd);
170
} else {
171
const pwd = std.fs.cwd();
172
-
var buffer: [std.fs.MAX_PATH_BYTES]u8 = undefined;
173
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
174
-
try self.working_directory.appendSlice(out_path);
175
}
176
177
{
···
210
try self.pty.setSize(ws);
211
}
212
213
-
pub fn draw(self: *Terminal, win: vaxis.Window) !void {
214
if (self.back_mutex.tryLock()) {
215
defer self.back_mutex.unlock();
216
// We keep this as a separate condition so we don't deadlock by obtaining the lock but not
217
// having sync
218
if (!self.mode.sync) {
219
-
try self.back_screen.copyTo(&self.front_screen);
220
self.dirty = false;
221
}
222
}
223
224
-
var row: usize = 0;
225
while (row < self.front_screen.height) : (row += 1) {
226
-
var col: usize = 0;
227
while (col < self.front_screen.width) {
228
const cell = self.front_screen.readCell(col, row) orelse continue;
229
win.writeCell(col, row, cell);
···
243
244
pub fn update(self: *Terminal, event: InputEvent) !void {
245
switch (event) {
246
-
.key_press => |k| try key.encode(self.anyWriter(), k, true, self.back_screen.csi_u_flags),
247
}
248
}
249
250
-
fn opaqueWrite(ptr: *const anyopaque, buf: []const u8) !usize {
251
-
const self: *const Terminal = @ptrCast(@alignCast(ptr));
252
-
return posix.write(self.pty.pty, buf);
253
}
254
255
-
pub fn anyWriter(self: *const Terminal) std.io.AnyWriter {
256
-
return .{
257
-
.context = self,
258
-
.writeFn = Terminal.opaqueWrite,
259
-
};
260
-
}
261
-
262
-
fn opaqueRead(ptr: *const anyopaque, buf: []u8) !usize {
263
-
const self: *const Terminal = @ptrCast(@alignCast(ptr));
264
-
return posix.read(self.pty.pty, buf);
265
-
}
266
-
267
-
fn anyReader(self: *const Terminal) std.io.AnyReader {
268
-
return .{
269
-
.context = self,
270
-
.readFn = Terminal.opaqueRead,
271
-
};
272
}
273
274
/// process the output from the command on the pty
275
fn run(self: *Terminal) !void {
276
var parser: Parser = .{
277
-
.buf = try std.ArrayList(u8).initCapacity(self.allocator, 128),
278
};
279
defer parser.buf.deinit();
280
281
-
// Use our anyReader to make a buffered reader, then get *that* any reader
282
-
var reader = std.io.bufferedReader(self.anyReader());
283
284
while (!self.should_quit) {
285
-
const event = try parser.parseReader(&reader);
286
self.back_mutex.lock();
287
defer self.back_mutex.unlock();
288
···
291
292
switch (event) {
293
.print => |str| {
294
-
var iter = grapheme.Iterator.init(str, &self.unicode.grapheme_data);
295
-
while (iter.next()) |g| {
296
-
const gr = g.bytes(str);
297
// TODO: use actual instead of .unicode
298
-
const w = try vaxis.gwidth.gwidth(gr, .unicode, &self.unicode.width_data);
299
try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
300
}
301
},
···
317
if (ts == self.back_screen.cursor.col) break true;
318
} else false;
319
if (already_set) continue;
320
-
try self.tab_stops.append(@truncate(self.back_screen.cursor.col));
321
std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
322
},
323
// Reverse Index
···
468
self.tab_stops.clearRetainingCapacity();
469
var col: u16 = 0;
470
while (col < self.back_screen.width) : (col += 8) {
471
-
try self.tab_stops.append(col);
472
}
473
}
474
},
···
484
);
485
var i: usize = start;
486
while (i < end) : (i += 1) {
487
-
self.back_screen.buf[i].erase(self.back_screen.cursor.style.bg);
488
}
489
},
490
'Z' => {
···
511
var iter = seq.iterator(u16);
512
const n = iter.next() orelse 1;
513
// TODO: maybe not .unicode
514
-
const w = try vaxis.gwidth.gwidth(self.last_printed, .unicode, &self.unicode.width_data);
515
var i: usize = 0;
516
while (i < n) : (i += 1) {
517
try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
···
519
},
520
// Device Attributes
521
'c' => {
522
if (seq.private_marker) |pm| {
523
switch (pm) {
524
// Secondary
525
-
'>' => try self.anyWriter().writeAll("\x1B[>1;69;0c"),
526
-
'=' => try self.anyWriter().writeAll("\x1B[=0000c"),
527
-
else => log.info("unhandled CSI: {}", .{seq}),
528
}
529
} else {
530
// Primary
531
-
try self.anyWriter().writeAll("\x1B[?62;22c");
532
}
533
},
534
// Cursor Vertical Position Absolute
···
562
const n = iter.next() orelse 0;
563
switch (n) {
564
0 => {
565
-
const current = try self.tab_stops.toOwnedSlice();
566
-
defer self.tab_stops.allocator.free(current);
567
self.tab_stops.clearRetainingCapacity();
568
for (current) |stop| {
569
if (stop == self.back_screen.cursor.col) continue;
570
-
try self.tab_stops.append(stop);
571
}
572
},
573
-
3 => self.tab_stops.clearAndFree(),
574
-
else => log.info("unhandled CSI: {}", .{seq}),
575
}
576
},
577
'h', 'l' => {
···
592
var iter = seq.iterator(u16);
593
const ps = iter.next() orelse 0;
594
if (seq.intermediate == null and seq.private_marker == null) {
595
switch (ps) {
596
-
5 => try self.anyWriter().writeAll("\x1b[0n"),
597
-
6 => try self.anyWriter().print("\x1b[{d};{d}R", .{
598
self.back_screen.cursor.row + 1,
599
self.back_screen.cursor.col + 1,
600
}),
601
-
else => log.info("unhandled CSI: {}", .{seq}),
602
}
603
}
604
},
···
609
switch (int) {
610
// report mode
611
'$' => {
612
switch (ps) {
613
-
2026 => try self.anyWriter().writeAll("\x1b[?2026;2$p"),
614
else => {
615
std.log.warn("unhandled mode: {}", .{ps});
616
-
try self.anyWriter().print("\x1b[?{d};0$p", .{ps});
617
},
618
}
619
},
620
-
else => log.info("unhandled CSI: {}", .{seq}),
621
}
622
}
623
},
···
633
}
634
}
635
if (seq.private_marker) |pm| {
636
switch (pm) {
637
// XTVERSION
638
-
'>' => try self.anyWriter().print(
639
"\x1bP>|libvaxis {s}\x1B\\",
640
.{"dev"},
641
),
642
-
else => log.info("unhandled CSI: {}", .{seq}),
643
}
644
}
645
},
···
667
self.back_screen.cursor.row = 0;
668
}
669
},
670
-
else => log.info("unhandled CSI: {}", .{seq}),
671
}
672
},
673
.osc => |osc| {
···
682
switch (ps) {
683
0 => {
684
self.title.clearRetainingCapacity();
685
-
try self.title.appendSlice(osc[semicolon + 1 ..]);
686
self.event_queue.push(.{ .title_change = self.title.items });
687
},
688
7 => {
···
701
defer i += 2;
702
break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
703
} else enc[i];
704
-
try self.working_directory.append(b);
705
}
706
self.event_queue.push(.{ .pwd_change = self.working_directory.items });
707
},
···
10
const vaxis = @import("../../main.zig");
11
const Winsize = vaxis.Winsize;
12
const Screen = @import("Screen.zig");
13
const Key = vaxis.Key;
14
const Queue = vaxis.Queue(Event, 16);
15
const key = @import("key.zig");
16
17
pub const Event = union(enum) {
···
22
pwd_change: []const u8,
23
};
24
25
const posix = std.posix;
26
27
const log = std.log.scoped(.terminal);
28
29
pub const Options = struct {
30
+
scrollback_size: u16 = 500,
31
winsize: Winsize = .{ .rows = 24, .cols = 80, .x_pixel = 0, .y_pixel = 0 },
32
initial_working_directory: ?[]const u8 = null,
33
};
···
48
pub var global_sigchild_installed: bool = false;
49
50
allocator: std.mem.Allocator,
51
+
scrollback_size: u16,
52
53
pty: Pty,
54
+
pty_writer: std.fs.File.Writer,
55
cmd: Command,
56
thread: ?std.Thread = null,
57
···
69
// dirty is protected by back_mutex. Only access this field when you hold that mutex
70
dirty: bool = false,
71
72
should_quit: bool = false,
73
74
mode: Mode = .{},
75
76
tab_stops: std.ArrayList(u16),
77
+
title: std.ArrayList(u8) = .empty,
78
+
working_directory: std.ArrayList(u8) = .empty,
79
80
last_printed: []const u8 = "",
81
···
87
allocator: std.mem.Allocator,
88
argv: []const []const u8,
89
env: *const std.process.EnvMap,
90
opts: Options,
91
+
write_buf: []u8,
92
) !Terminal {
93
// Verify we have an absolute path
94
if (opts.initial_working_directory) |pwd| {
···
102
.pty = pty,
103
.working_directory = opts.initial_working_directory,
104
};
105
+
var tabs: std.ArrayList(u16) = try .initCapacity(allocator, opts.winsize.cols / 8);
106
var col: u16 = 0;
107
while (col < opts.winsize.cols) : (col += 8) {
108
+
try tabs.append(allocator, col);
109
}
110
return .{
111
.allocator = allocator,
112
.pty = pty,
113
+
.pty_writer = pty.pty.writerStreaming(write_buf),
114
.cmd = cmd,
115
.scrollback_size = opts.scrollback_size,
116
.front_screen = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
117
.back_screen_pri = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows + opts.scrollback_size),
118
.back_screen_alt = try Screen.init(allocator, opts.winsize.cols, opts.winsize.rows),
119
.tab_stops = tabs,
120
};
121
}
122
···
139
if (self.thread) |thread| {
140
// write an EOT into the tty to trigger a read on our thread
141
const EOT = "\x04";
142
+
_ = self.pty.tty.write(EOT) catch {};
143
thread.join();
144
self.thread = null;
145
}
···
147
self.front_screen.deinit(self.allocator);
148
self.back_screen_pri.deinit(self.allocator);
149
self.back_screen_alt.deinit(self.allocator);
150
+
self.tab_stops.deinit(self.allocator);
151
+
self.title.deinit(self.allocator);
152
+
self.working_directory.deinit(self.allocator);
153
}
154
155
pub fn spawn(self: *Terminal) !void {
···
160
161
self.working_directory.clearRetainingCapacity();
162
if (self.cmd.working_directory) |pwd| {
163
+
try self.working_directory.appendSlice(self.allocator, pwd);
164
} else {
165
const pwd = std.fs.cwd();
166
+
var buffer: [std.fs.max_path_bytes]u8 = undefined;
167
const out_path = try std.os.getFdPath(pwd.fd, &buffer);
168
+
try self.working_directory.appendSlice(self.allocator, out_path);
169
}
170
171
{
···
204
try self.pty.setSize(ws);
205
}
206
207
+
pub fn draw(self: *Terminal, allocator: std.mem.Allocator, win: vaxis.Window) !void {
208
if (self.back_mutex.tryLock()) {
209
defer self.back_mutex.unlock();
210
// We keep this as a separate condition so we don't deadlock by obtaining the lock but not
211
// having sync
212
if (!self.mode.sync) {
213
+
try self.back_screen.copyTo(allocator, &self.front_screen);
214
self.dirty = false;
215
}
216
}
217
218
+
var row: u16 = 0;
219
while (row < self.front_screen.height) : (row += 1) {
220
+
var col: u16 = 0;
221
while (col < self.front_screen.width) {
222
const cell = self.front_screen.readCell(col, row) orelse continue;
223
win.writeCell(col, row, cell);
···
237
238
pub fn update(self: *Terminal, event: InputEvent) !void {
239
switch (event) {
240
+
.key_press => |k| {
241
+
const pty_writer = self.get_pty_writer();
242
+
defer pty_writer.flush() catch {};
243
+
try key.encode(pty_writer, k, true, self.back_screen.csi_u_flags);
244
+
},
245
}
246
}
247
248
+
pub fn get_pty_writer(self: *Terminal) *std.Io.Writer {
249
+
return &self.pty_writer.interface;
250
}
251
252
+
fn reader(self: *const Terminal, buf: []u8) std.fs.File.Reader {
253
+
return self.pty.pty.readerStreaming(buf);
254
}
255
256
/// process the output from the command on the pty
257
fn run(self: *Terminal) !void {
258
var parser: Parser = .{
259
+
.buf = try .initCapacity(self.allocator, 128),
260
};
261
defer parser.buf.deinit();
262
263
+
var reader_buf: [4096]u8 = undefined;
264
+
var reader_ = self.reader(&reader_buf);
265
266
while (!self.should_quit) {
267
+
const event = try parser.parseReader(&reader_.interface);
268
self.back_mutex.lock();
269
defer self.back_mutex.unlock();
270
···
273
274
switch (event) {
275
.print => |str| {
276
+
var iter = vaxis.unicode.graphemeIterator(str);
277
+
while (iter.next()) |grapheme| {
278
+
const gr = grapheme.bytes(str);
279
// TODO: use actual instead of .unicode
280
+
const w = vaxis.gwidth.gwidth(gr, .unicode);
281
try self.back_screen.print(gr, @truncate(w), self.mode.autowrap);
282
}
283
},
···
299
if (ts == self.back_screen.cursor.col) break true;
300
} else false;
301
if (already_set) continue;
302
+
try self.tab_stops.append(self.allocator, @truncate(self.back_screen.cursor.col));
303
std.mem.sort(u16, self.tab_stops.items, {}, std.sort.asc(u16));
304
},
305
// Reverse Index
···
450
self.tab_stops.clearRetainingCapacity();
451
var col: u16 = 0;
452
while (col < self.back_screen.width) : (col += 8) {
453
+
try self.tab_stops.append(self.allocator, col);
454
}
455
}
456
},
···
466
);
467
var i: usize = start;
468
while (i < end) : (i += 1) {
469
+
self.back_screen.buf[i].erase(self.allocator, self.back_screen.cursor.style.bg);
470
}
471
},
472
'Z' => {
···
493
var iter = seq.iterator(u16);
494
const n = iter.next() orelse 1;
495
// TODO: maybe not .unicode
496
+
const w = vaxis.gwidth.gwidth(self.last_printed, .unicode);
497
var i: usize = 0;
498
while (i < n) : (i += 1) {
499
try self.back_screen.print(self.last_printed, @truncate(w), self.mode.autowrap);
···
501
},
502
// Device Attributes
503
'c' => {
504
+
const pty_writer = self.get_pty_writer();
505
+
defer pty_writer.flush() catch {};
506
if (seq.private_marker) |pm| {
507
switch (pm) {
508
// Secondary
509
+
'>' => try pty_writer.writeAll("\x1B[>1;69;0c"),
510
+
'=' => try pty_writer.writeAll("\x1B[=0000c"),
511
+
else => log.info("unhandled CSI: {f}", .{seq}),
512
}
513
} else {
514
// Primary
515
+
try pty_writer.writeAll("\x1B[?62;22c");
516
}
517
},
518
// Cursor Vertical Position Absolute
···
546
const n = iter.next() orelse 0;
547
switch (n) {
548
0 => {
549
+
const current = try self.tab_stops.toOwnedSlice(self.allocator);
550
+
defer self.allocator.free(current);
551
self.tab_stops.clearRetainingCapacity();
552
for (current) |stop| {
553
if (stop == self.back_screen.cursor.col) continue;
554
+
try self.tab_stops.append(self.allocator, stop);
555
}
556
},
557
+
3 => self.tab_stops.clearAndFree(self.allocator),
558
+
else => log.info("unhandled CSI: {f}", .{seq}),
559
}
560
},
561
'h', 'l' => {
···
576
var iter = seq.iterator(u16);
577
const ps = iter.next() orelse 0;
578
if (seq.intermediate == null and seq.private_marker == null) {
579
+
const pty_writer = self.get_pty_writer();
580
+
defer pty_writer.flush() catch {};
581
switch (ps) {
582
+
5 => try pty_writer.writeAll("\x1b[0n"),
583
+
6 => try pty_writer.print("\x1b[{d};{d}R", .{
584
self.back_screen.cursor.row + 1,
585
self.back_screen.cursor.col + 1,
586
}),
587
+
else => log.info("unhandled CSI: {f}", .{seq}),
588
}
589
}
590
},
···
595
switch (int) {
596
// report mode
597
'$' => {
598
+
const pty_writer = self.get_pty_writer();
599
+
defer pty_writer.flush() catch {};
600
switch (ps) {
601
+
2026 => try pty_writer.writeAll("\x1b[?2026;2$p"),
602
else => {
603
std.log.warn("unhandled mode: {}", .{ps});
604
+
try pty_writer.print("\x1b[?{d};0$p", .{ps});
605
},
606
}
607
},
608
+
else => log.info("unhandled CSI: {f}", .{seq}),
609
}
610
}
611
},
···
621
}
622
}
623
if (seq.private_marker) |pm| {
624
+
const pty_writer = self.get_pty_writer();
625
+
defer pty_writer.flush() catch {};
626
switch (pm) {
627
// XTVERSION
628
+
'>' => try pty_writer.print(
629
"\x1bP>|libvaxis {s}\x1B\\",
630
.{"dev"},
631
),
632
+
else => log.info("unhandled CSI: {f}", .{seq}),
633
}
634
}
635
},
···
657
self.back_screen.cursor.row = 0;
658
}
659
},
660
+
else => log.info("unhandled CSI: {f}", .{seq}),
661
}
662
},
663
.osc => |osc| {
···
672
switch (ps) {
673
0 => {
674
self.title.clearRetainingCapacity();
675
+
try self.title.appendSlice(self.allocator, osc[semicolon + 1 ..]);
676
self.event_queue.push(.{ .title_change = self.title.items });
677
},
678
7 => {
···
691
defer i += 2;
692
break :blk try std.fmt.parseUnsigned(u8, enc[i + 1 .. i + 3], 16);
693
} else enc[i];
694
+
try self.working_directory.append(self.allocator, b);
695
}
696
self.event_queue.push(.{ .pwd_change = self.working_directory.items });
697
},
+5
-12
src/widgets/terminal/ansi.zig
+5
-12
src/widgets/terminal/ansi.zig
···
55
return .{ .bytes = self.params };
56
}
57
58
-
pub fn format(
59
-
self: CSI,
60
-
comptime layout: []const u8,
61
-
opts: std.fmt.FormatOptions,
62
-
writer: anytype,
63
-
) !void {
64
-
_ = layout;
65
-
_ = opts;
66
if (self.private_marker == null and self.intermediate == null)
67
-
try std.fmt.format(writer, "CSI {s} {c}", .{
68
self.params,
69
self.final,
70
})
71
else if (self.private_marker != null and self.intermediate == null)
72
-
try std.fmt.format(writer, "CSI {c} {s} {c}", .{
73
self.private_marker.?,
74
self.params,
75
self.final,
76
})
77
else if (self.private_marker == null and self.intermediate != null)
78
-
try std.fmt.format(writer, "CSI {s} {c} {c}", .{
79
self.params,
80
self.intermediate.?,
81
self.final,
82
})
83
else
84
-
try std.fmt.format(writer, "CSI {c} {s} {c} {c}", .{
85
self.private_marker.?,
86
self.params,
87
self.intermediate.?,
···
55
return .{ .bytes = self.params };
56
}
57
58
+
pub fn format(self: CSI, writer: anytype) !void {
59
if (self.private_marker == null and self.intermediate == null)
60
+
try writer.print("CSI {s} {c}", .{
61
self.params,
62
self.final,
63
})
64
else if (self.private_marker != null and self.intermediate == null)
65
+
try writer.print("CSI {c} {s} {c}", .{
66
self.private_marker.?,
67
self.params,
68
self.final,
69
})
70
else if (self.private_marker == null and self.intermediate != null)
71
+
try writer.print("CSI {s} {c} {c}", .{
72
self.params,
73
self.intermediate.?,
74
self.final,
75
})
76
else
77
+
try writer.print("CSI {c} {s} {c} {c}", .{
78
self.private_marker.?,
79
self.params,
80
self.intermediate.?,
+2
-2
src/widgets/terminal/key.zig
+2
-2
src/widgets/terminal/key.zig
···
2
const vaxis = @import("../../main.zig");
3
4
pub fn encode(
5
-
writer: std.io.AnyWriter,
6
key: vaxis.Key,
7
press: bool,
8
kitty_flags: vaxis.Key.KittyFlags,
···
19
}
20
}
21
22
-
fn legacy(writer: std.io.AnyWriter, key: vaxis.Key) !void {
23
// If we have text, we always write it directly
24
if (key.text) |text| {
25
try writer.writeAll(text);
···
2
const vaxis = @import("../../main.zig");
3
4
pub fn encode(
5
+
writer: *std.Io.Writer,
6
key: vaxis.Key,
7
press: bool,
8
kitty_flags: vaxis.Key.KittyFlags,
···
19
}
20
}
21
22
+
fn legacy(writer: *std.Io.Writer, key: vaxis.Key) !void {
23
// If we have text, we always write it directly
24
if (key.text) |text| {
25
try writer.writeAll(text);
+2
-7
src/widgets.zig
+2
-7
src/widgets.zig
···
1
//! Specialized TUI Widgets
2
3
-
const opts = @import("build_options");
4
-
5
-
pub const border = @import("widgets/border.zig");
6
pub const alignment = @import("widgets/alignment.zig");
7
pub const Scrollbar = @import("widgets/Scrollbar.zig");
8
pub const Table = @import("widgets/Table.zig");
···
11
pub const TextView = @import("widgets/TextView.zig");
12
pub const CodeView = @import("widgets/CodeView.zig");
13
pub const Terminal = @import("widgets/terminal/Terminal.zig");
14
-
15
-
// Widgets with dependencies
16
-
17
-
pub const TextInput = if (opts.text_input) @import("widgets/TextInput.zig") else undefined;
···
1
//! Specialized TUI Widgets
2
3
pub const alignment = @import("widgets/alignment.zig");
4
pub const Scrollbar = @import("widgets/Scrollbar.zig");
5
pub const Table = @import("widgets/Table.zig");
···
8
pub const TextView = @import("widgets/TextView.zig");
9
pub const CodeView = @import("widgets/CodeView.zig");
10
pub const Terminal = @import("widgets/terminal/Terminal.zig");
11
+
pub const TextInput = @import("widgets/TextInput.zig");
12
+
pub const View = @import("widgets/View.zig");
-497
src/windows/Tty.zig
-497
src/windows/Tty.zig
···
1
-
//! A Windows TTY implementation, using virtual terminal process output and
2
-
//! native windows input
3
-
const Tty = @This();
4
-
5
-
const std = @import("std");
6
-
const Event = @import("../event.zig").Event;
7
-
const Key = @import("../Key.zig");
8
-
const Mouse = @import("../Mouse.zig");
9
-
const Parser = @import("../Parser.zig");
10
-
const windows = std.os.windows;
11
-
12
-
stdin: windows.HANDLE,
13
-
stdout: windows.HANDLE,
14
-
15
-
initial_codepage: c_uint,
16
-
initial_input_mode: u32,
17
-
initial_output_mode: u32,
18
-
19
-
// a buffer to write key text into
20
-
buf: [4]u8 = undefined,
21
-
22
-
/// The last mouse button that was pressed. We store the previous state of button presses on each
23
-
/// mouse event so we can detect which button was released
24
-
last_mouse_button_press: u16 = 0,
25
-
26
-
pub var global_tty: ?Tty = null;
27
-
28
-
const utf8_codepage: c_uint = 65001;
29
-
30
-
const InputMode = struct {
31
-
const enable_window_input: u32 = 0x0008; // resize events
32
-
const enable_mouse_input: u32 = 0x0010;
33
-
const enable_extended_flags: u32 = 0x0080; // allows mouse events
34
-
35
-
pub fn rawMode() u32 {
36
-
return enable_window_input | enable_mouse_input | enable_extended_flags;
37
-
}
38
-
};
39
-
40
-
const OutputMode = struct {
41
-
const enable_processed_output: u32 = 0x0001; // handle control sequences
42
-
const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences
43
-
const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column
44
-
const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline
45
-
46
-
fn rawMode() u32 {
47
-
return enable_processed_output |
48
-
enable_virtual_terminal_processing |
49
-
disable_newline_auto_return |
50
-
enable_lvb_grid_worldwide;
51
-
}
52
-
};
53
-
54
-
pub fn init() !Tty {
55
-
const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE);
56
-
const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE);
57
-
58
-
// get initial modes
59
-
var initial_input_mode: windows.DWORD = undefined;
60
-
var initial_output_mode: windows.DWORD = undefined;
61
-
const initial_output_codepage = windows.kernel32.GetConsoleOutputCP();
62
-
{
63
-
if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) {
64
-
return windows.unexpectedError(windows.kernel32.GetLastError());
65
-
}
66
-
if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) {
67
-
return windows.unexpectedError(windows.kernel32.GetLastError());
68
-
}
69
-
}
70
-
71
-
// set new modes
72
-
{
73
-
if (SetConsoleMode(stdin, InputMode.rawMode()) == 0)
74
-
return windows.unexpectedError(windows.kernel32.GetLastError());
75
-
76
-
if (SetConsoleMode(stdout, OutputMode.rawMode()) == 0)
77
-
return windows.unexpectedError(windows.kernel32.GetLastError());
78
-
79
-
if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
80
-
return windows.unexpectedError(windows.kernel32.GetLastError());
81
-
}
82
-
83
-
const self: Tty = .{
84
-
.stdin = stdin,
85
-
.stdout = stdout,
86
-
.initial_codepage = initial_output_codepage,
87
-
.initial_input_mode = initial_input_mode,
88
-
.initial_output_mode = initial_output_mode,
89
-
};
90
-
91
-
// save a copy of this tty as the global_tty for panic handling
92
-
global_tty = self;
93
-
94
-
return self;
95
-
}
96
-
97
-
pub fn deinit(self: Tty) void {
98
-
_ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage);
99
-
_ = SetConsoleMode(self.stdin, self.initial_input_mode);
100
-
_ = SetConsoleMode(self.stdout, self.initial_output_mode);
101
-
windows.CloseHandle(self.stdin);
102
-
windows.CloseHandle(self.stdout);
103
-
}
104
-
105
-
pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
106
-
const self: *const Tty = @ptrCast(@alignCast(ptr));
107
-
return windows.WriteFile(self.stdout, bytes, null);
108
-
}
109
-
110
-
pub fn anyWriter(self: *const Tty) std.io.AnyWriter {
111
-
return .{
112
-
.context = self,
113
-
.writeFn = Tty.opaqueWrite,
114
-
};
115
-
}
116
-
117
-
pub fn bufferedWriter(self: *const Tty) std.io.BufferedWriter(4096, std.io.AnyWriter) {
118
-
return std.io.bufferedWriter(self.anyWriter());
119
-
}
120
-
121
-
pub fn nextEvent(self: *Tty, parser: *Parser, paste_allocator: ?std.mem.Allocator) !Event {
122
-
// We use a loop so we can ignore certain events
123
-
var state: EventState = .{};
124
-
while (true) {
125
-
var event_count: u32 = 0;
126
-
var input_record: INPUT_RECORD = undefined;
127
-
if (ReadConsoleInputW(self.stdin, &input_record, 1, &event_count) == 0)
128
-
return windows.unexpectedError(windows.kernel32.GetLastError());
129
-
130
-
if (try self.eventFromRecord(&input_record, &state, parser, paste_allocator)) |ev| {
131
-
return ev;
132
-
}
133
-
}
134
-
}
135
-
136
-
pub const EventState = struct {
137
-
ansi_buf: [128]u8 = undefined,
138
-
ansi_idx: usize = 0,
139
-
utf16_buf: [2]u16 = undefined,
140
-
utf16_half: bool = false,
141
-
};
142
-
143
-
pub fn eventFromRecord(self: *Tty, record: *const INPUT_RECORD, state: *EventState, parser: *Parser, paste_allocator: ?std.mem.Allocator) !?Event {
144
-
switch (record.EventType) {
145
-
0x0001 => { // Key event
146
-
const event = record.Event.KeyEvent;
147
-
148
-
if (state.utf16_half) half: {
149
-
state.utf16_half = false;
150
-
state.utf16_buf[1] = event.uChar.UnicodeChar;
151
-
const codepoint: u21 = std.unicode.utf16DecodeSurrogatePair(&state.utf16_buf) catch break :half;
152
-
const n = std.unicode.utf8Encode(codepoint, &self.buf) catch return null;
153
-
154
-
const key: Key = .{
155
-
.codepoint = codepoint,
156
-
.base_layout_codepoint = codepoint,
157
-
.mods = translateMods(event.dwControlKeyState),
158
-
.text = self.buf[0..n],
159
-
};
160
-
161
-
switch (event.bKeyDown) {
162
-
0 => return .{ .key_release = key },
163
-
else => return .{ .key_press = key },
164
-
}
165
-
}
166
-
167
-
const base_layout: u16 = switch (event.wVirtualKeyCode) {
168
-
0x00 => blk: { // delivered when we get an escape sequence or a unicode codepoint
169
-
if (state.ansi_idx == 0 and event.uChar.AsciiChar != 27)
170
-
break :blk event.uChar.UnicodeChar;
171
-
state.ansi_buf[state.ansi_idx] = event.uChar.AsciiChar;
172
-
state.ansi_idx += 1;
173
-
if (state.ansi_idx <= 2) return null;
174
-
const result = try parser.parse(state.ansi_buf[0..state.ansi_idx], paste_allocator);
175
-
return if (result.n == 0) null else evt: {
176
-
state.ansi_idx = 0;
177
-
break :evt result.event;
178
-
};
179
-
},
180
-
0x08 => Key.backspace,
181
-
0x09 => Key.tab,
182
-
0x0D => Key.enter,
183
-
0x13 => Key.pause,
184
-
0x14 => Key.caps_lock,
185
-
0x1B => Key.escape,
186
-
0x20 => Key.space,
187
-
0x21 => Key.page_up,
188
-
0x22 => Key.page_down,
189
-
0x23 => Key.end,
190
-
0x24 => Key.home,
191
-
0x25 => Key.left,
192
-
0x26 => Key.up,
193
-
0x27 => Key.right,
194
-
0x28 => Key.down,
195
-
0x2c => Key.print_screen,
196
-
0x2d => Key.insert,
197
-
0x2e => Key.delete,
198
-
0x30...0x39 => |k| k,
199
-
0x41...0x5a => |k| k + 0x20, // translate to lowercase
200
-
0x5b => Key.left_meta,
201
-
0x5c => Key.right_meta,
202
-
0x60 => Key.kp_0,
203
-
0x61 => Key.kp_1,
204
-
0x62 => Key.kp_2,
205
-
0x63 => Key.kp_3,
206
-
0x64 => Key.kp_4,
207
-
0x65 => Key.kp_5,
208
-
0x66 => Key.kp_6,
209
-
0x67 => Key.kp_7,
210
-
0x68 => Key.kp_8,
211
-
0x69 => Key.kp_9,
212
-
0x6a => Key.kp_multiply,
213
-
0x6b => Key.kp_add,
214
-
0x6c => Key.kp_separator,
215
-
0x6d => Key.kp_subtract,
216
-
0x6e => Key.kp_decimal,
217
-
0x6f => Key.kp_divide,
218
-
0x70 => Key.f1,
219
-
0x71 => Key.f2,
220
-
0x72 => Key.f3,
221
-
0x73 => Key.f4,
222
-
0x74 => Key.f5,
223
-
0x75 => Key.f6,
224
-
0x76 => Key.f8,
225
-
0x77 => Key.f8,
226
-
0x78 => Key.f9,
227
-
0x79 => Key.f10,
228
-
0x7a => Key.f11,
229
-
0x7b => Key.f12,
230
-
0x7c => Key.f13,
231
-
0x7d => Key.f14,
232
-
0x7e => Key.f15,
233
-
0x7f => Key.f16,
234
-
0x80 => Key.f17,
235
-
0x81 => Key.f18,
236
-
0x82 => Key.f19,
237
-
0x83 => Key.f20,
238
-
0x84 => Key.f21,
239
-
0x85 => Key.f22,
240
-
0x86 => Key.f23,
241
-
0x87 => Key.f24,
242
-
0x90 => Key.num_lock,
243
-
0x91 => Key.scroll_lock,
244
-
0xa0 => Key.left_shift,
245
-
0xa1 => Key.right_shift,
246
-
0xa2 => Key.left_control,
247
-
0xa3 => Key.right_control,
248
-
0xa4 => Key.left_alt,
249
-
0xa5 => Key.right_alt,
250
-
0xad => Key.mute_volume,
251
-
0xae => Key.lower_volume,
252
-
0xaf => Key.raise_volume,
253
-
0xb0 => Key.media_track_next,
254
-
0xb1 => Key.media_track_previous,
255
-
0xb2 => Key.media_stop,
256
-
0xb3 => Key.media_play_pause,
257
-
0xba => ';',
258
-
0xbb => '+',
259
-
0xbc => ',',
260
-
0xbd => '-',
261
-
0xbe => '.',
262
-
0xbf => '/',
263
-
0xc0 => '`',
264
-
0xdb => '[',
265
-
0xdc => '\\',
266
-
0xdd => ']',
267
-
0xde => '\'',
268
-
else => return null,
269
-
};
270
-
271
-
if (std.unicode.utf16IsHighSurrogate(base_layout)) {
272
-
state.utf16_buf[0] = base_layout;
273
-
state.utf16_half = true;
274
-
return null;
275
-
}
276
-
if (std.unicode.utf16IsLowSurrogate(base_layout)) {
277
-
return null;
278
-
}
279
-
280
-
var codepoint: u21 = base_layout;
281
-
var text: ?[]const u8 = null;
282
-
switch (event.uChar.UnicodeChar) {
283
-
0x00...0x1F => {},
284
-
else => |cp| {
285
-
codepoint = cp;
286
-
const n = try std.unicode.utf8Encode(codepoint, &self.buf);
287
-
text = self.buf[0..n];
288
-
},
289
-
}
290
-
291
-
const key: Key = .{
292
-
.codepoint = codepoint,
293
-
.base_layout_codepoint = base_layout,
294
-
.mods = translateMods(event.dwControlKeyState),
295
-
.text = text,
296
-
};
297
-
298
-
switch (event.bKeyDown) {
299
-
0 => return .{ .key_release = key },
300
-
else => return .{ .key_press = key },
301
-
}
302
-
},
303
-
0x0002 => { // Mouse event
304
-
// see https://learn.microsoft.com/en-us/windows/console/mouse-event-record-str
305
-
306
-
const event = record.Event.MouseEvent;
307
-
308
-
// High word of dwButtonState represents mouse wheel. Positive is wheel_up, negative
309
-
// is wheel_down
310
-
// Low word represents button state
311
-
const mouse_wheel_direction: i16 = blk: {
312
-
const wheelu32: u32 = event.dwButtonState >> 16;
313
-
const wheelu16: u16 = @truncate(wheelu32);
314
-
break :blk @bitCast(wheelu16);
315
-
};
316
-
317
-
const buttons: u16 = @truncate(event.dwButtonState);
318
-
// save the current state when we are done
319
-
defer self.last_mouse_button_press = buttons;
320
-
const button_xor = self.last_mouse_button_press ^ buttons;
321
-
322
-
var event_type: Mouse.Type = .press;
323
-
const btn: Mouse.Button = switch (button_xor) {
324
-
0x0000 => blk: {
325
-
// Check wheel event
326
-
if (event.dwEventFlags & 0x0004 > 0) {
327
-
if (mouse_wheel_direction > 0)
328
-
break :blk .wheel_up
329
-
else
330
-
break :blk .wheel_down;
331
-
}
332
-
333
-
// If we have no change but one of the buttons is still pressed we have a
334
-
// drag event. Find out which button is held down
335
-
if (buttons > 0 and event.dwEventFlags & 0x0001 > 0) {
336
-
event_type = .drag;
337
-
if (buttons & 0x0001 > 0) break :blk .left;
338
-
if (buttons & 0x0002 > 0) break :blk .right;
339
-
if (buttons & 0x0004 > 0) break :blk .middle;
340
-
if (buttons & 0x0008 > 0) break :blk .button_8;
341
-
if (buttons & 0x0010 > 0) break :blk .button_9;
342
-
}
343
-
344
-
if (event.dwEventFlags & 0x0001 > 0) event_type = .motion;
345
-
break :blk .none;
346
-
},
347
-
0x0001 => blk: {
348
-
if (buttons & 0x0001 == 0) event_type = .release;
349
-
break :blk .left;
350
-
},
351
-
0x0002 => blk: {
352
-
if (buttons & 0x0002 == 0) event_type = .release;
353
-
break :blk .right;
354
-
},
355
-
0x0004 => blk: {
356
-
if (buttons & 0x0004 == 0) event_type = .release;
357
-
break :blk .middle;
358
-
},
359
-
0x0008 => blk: {
360
-
if (buttons & 0x0008 == 0) event_type = .release;
361
-
break :blk .button_8;
362
-
},
363
-
0x0010 => blk: {
364
-
if (buttons & 0x0010 == 0) event_type = .release;
365
-
break :blk .button_9;
366
-
},
367
-
else => {
368
-
std.log.warn("unknown mouse event: {}", .{event});
369
-
return null;
370
-
},
371
-
};
372
-
373
-
const shift: u32 = 0x0010;
374
-
const alt: u32 = 0x0001 | 0x0002;
375
-
const ctrl: u32 = 0x0004 | 0x0008;
376
-
const mods: Mouse.Modifiers = .{
377
-
.shift = event.dwControlKeyState & shift > 0,
378
-
.alt = event.dwControlKeyState & alt > 0,
379
-
.ctrl = event.dwControlKeyState & ctrl > 0,
380
-
};
381
-
382
-
const mouse: Mouse = .{
383
-
.col = @as(u16, @bitCast(event.dwMousePosition.X)), // Windows reports with 0 index
384
-
.row = @as(u16, @bitCast(event.dwMousePosition.Y)), // Windows reports with 0 index
385
-
.mods = mods,
386
-
.type = event_type,
387
-
.button = btn,
388
-
};
389
-
return .{ .mouse = mouse };
390
-
},
391
-
0x0004 => { // Screen resize events
392
-
// NOTE: Even though the event comes with a size, it may not be accurate. We ask for
393
-
// the size directly when we get this event
394
-
var console_info: windows.CONSOLE_SCREEN_BUFFER_INFO = undefined;
395
-
if (windows.kernel32.GetConsoleScreenBufferInfo(self.stdout, &console_info) == 0) {
396
-
return windows.unexpectedError(windows.kernel32.GetLastError());
397
-
}
398
-
const window_rect = console_info.srWindow;
399
-
const width = window_rect.Right - window_rect.Left + 1;
400
-
const height = window_rect.Bottom - window_rect.Top + 1;
401
-
return .{
402
-
.winsize = .{
403
-
.cols = @intCast(width),
404
-
.rows = @intCast(height),
405
-
.x_pixel = 0,
406
-
.y_pixel = 0,
407
-
},
408
-
};
409
-
},
410
-
0x0010 => { // Focus events
411
-
switch (record.Event.FocusEvent.bSetFocus) {
412
-
0 => return .focus_out,
413
-
else => return .focus_in,
414
-
}
415
-
},
416
-
else => {},
417
-
}
418
-
return null;
419
-
}
420
-
421
-
fn translateMods(mods: u32) Key.Modifiers {
422
-
const left_alt: u32 = 0x0002;
423
-
const right_alt: u32 = 0x0001;
424
-
const left_ctrl: u32 = 0x0008;
425
-
const right_ctrl: u32 = 0x0004;
426
-
427
-
const caps: u32 = 0x0080;
428
-
const num_lock: u32 = 0x0020;
429
-
const shift: u32 = 0x0010;
430
-
const alt: u32 = left_alt | right_alt;
431
-
const ctrl: u32 = left_ctrl | right_ctrl;
432
-
433
-
return .{
434
-
.shift = mods & shift > 0,
435
-
.alt = mods & alt > 0,
436
-
.ctrl = mods & ctrl > 0,
437
-
.caps_lock = mods & caps > 0,
438
-
.num_lock = mods & num_lock > 0,
439
-
};
440
-
}
441
-
442
-
// From gitub.com/ziglibs/zig-windows-console. Thanks :)
443
-
//
444
-
// Events
445
-
const union_unnamed_248 = extern union {
446
-
UnicodeChar: windows.WCHAR,
447
-
AsciiChar: windows.CHAR,
448
-
};
449
-
pub const KEY_EVENT_RECORD = extern struct {
450
-
bKeyDown: windows.BOOL,
451
-
wRepeatCount: windows.WORD,
452
-
wVirtualKeyCode: windows.WORD,
453
-
wVirtualScanCode: windows.WORD,
454
-
uChar: union_unnamed_248,
455
-
dwControlKeyState: windows.DWORD,
456
-
};
457
-
pub const PKEY_EVENT_RECORD = *KEY_EVENT_RECORD;
458
-
459
-
pub const MOUSE_EVENT_RECORD = extern struct {
460
-
dwMousePosition: windows.COORD,
461
-
dwButtonState: windows.DWORD,
462
-
dwControlKeyState: windows.DWORD,
463
-
dwEventFlags: windows.DWORD,
464
-
};
465
-
pub const PMOUSE_EVENT_RECORD = *MOUSE_EVENT_RECORD;
466
-
467
-
pub const WINDOW_BUFFER_SIZE_RECORD = extern struct {
468
-
dwSize: windows.COORD,
469
-
};
470
-
pub const PWINDOW_BUFFER_SIZE_RECORD = *WINDOW_BUFFER_SIZE_RECORD;
471
-
472
-
pub const MENU_EVENT_RECORD = extern struct {
473
-
dwCommandId: windows.UINT,
474
-
};
475
-
pub const PMENU_EVENT_RECORD = *MENU_EVENT_RECORD;
476
-
477
-
pub const FOCUS_EVENT_RECORD = extern struct {
478
-
bSetFocus: windows.BOOL,
479
-
};
480
-
pub const PFOCUS_EVENT_RECORD = *FOCUS_EVENT_RECORD;
481
-
482
-
const union_unnamed_249 = extern union {
483
-
KeyEvent: KEY_EVENT_RECORD,
484
-
MouseEvent: MOUSE_EVENT_RECORD,
485
-
WindowBufferSizeEvent: WINDOW_BUFFER_SIZE_RECORD,
486
-
MenuEvent: MENU_EVENT_RECORD,
487
-
FocusEvent: FOCUS_EVENT_RECORD,
488
-
};
489
-
pub const INPUT_RECORD = extern struct {
490
-
EventType: windows.WORD,
491
-
Event: union_unnamed_249,
492
-
};
493
-
pub const PINPUT_RECORD = *INPUT_RECORD;
494
-
495
-
pub extern "kernel32" fn ReadConsoleInputW(hConsoleInput: windows.HANDLE, lpBuffer: PINPUT_RECORD, nLength: windows.DWORD, lpNumberOfEventsRead: *windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
496
-
// TODO: remove this in zig 0.13.0
497
-
pub extern "kernel32" fn SetConsoleMode(in_hConsoleHandle: windows.HANDLE, in_dwMode: windows.DWORD) callconv(windows.WINAPI) windows.BOOL;
···
-271
src/xev.zig
-271
src/xev.zig
···
1
-
const std = @import("std");
2
-
const xev = @import("xev");
3
-
4
-
const Tty = @import("main.zig").Tty;
5
-
const Winsize = @import("main.zig").Winsize;
6
-
const Vaxis = @import("Vaxis.zig");
7
-
const Parser = @import("Parser.zig");
8
-
const Key = @import("Key.zig");
9
-
const Mouse = @import("Mouse.zig");
10
-
const Color = @import("Cell.zig").Color;
11
-
12
-
const log = std.log.scoped(.vaxis_xev);
13
-
14
-
pub const Event = union(enum) {
15
-
key_press: Key,
16
-
key_release: Key,
17
-
mouse: Mouse,
18
-
focus_in,
19
-
focus_out,
20
-
paste_start, // bracketed paste start
21
-
paste_end, // bracketed paste end
22
-
paste: []const u8, // osc 52 paste, caller must free
23
-
color_report: Color.Report, // osc 4, 10, 11, 12 response
24
-
color_scheme: Color.Scheme,
25
-
winsize: Winsize,
26
-
};
27
-
28
-
pub fn TtyWatcher(comptime Userdata: type) type {
29
-
return struct {
30
-
const Self = @This();
31
-
32
-
file: xev.File,
33
-
tty: *Tty,
34
-
35
-
read_buf: [4096]u8,
36
-
read_buf_start: usize,
37
-
read_cmp: xev.Completion,
38
-
39
-
winsize_wakeup: xev.Async,
40
-
winsize_cmp: xev.Completion,
41
-
42
-
callback: *const fn (
43
-
ud: ?*Userdata,
44
-
loop: *xev.Loop,
45
-
watcher: *Self,
46
-
event: Event,
47
-
) xev.CallbackAction,
48
-
49
-
ud: ?*Userdata,
50
-
vx: *Vaxis,
51
-
parser: Parser,
52
-
53
-
pub fn init(
54
-
self: *Self,
55
-
tty: *Tty,
56
-
vaxis: *Vaxis,
57
-
loop: *xev.Loop,
58
-
userdata: ?*Userdata,
59
-
callback: *const fn (
60
-
ud: ?*Userdata,
61
-
loop: *xev.Loop,
62
-
watcher: *Self,
63
-
event: Event,
64
-
) xev.CallbackAction,
65
-
) !void {
66
-
self.* = .{
67
-
.tty = tty,
68
-
.file = xev.File.initFd(tty.fd),
69
-
.read_buf = undefined,
70
-
.read_buf_start = 0,
71
-
.read_cmp = .{},
72
-
73
-
.winsize_wakeup = try xev.Async.init(),
74
-
.winsize_cmp = .{},
75
-
76
-
.callback = callback,
77
-
.ud = userdata,
78
-
.vx = vaxis,
79
-
.parser = .{ .grapheme_data = &vaxis.unicode.grapheme_data },
80
-
};
81
-
82
-
self.file.read(
83
-
loop,
84
-
&self.read_cmp,
85
-
.{ .slice = &self.read_buf },
86
-
Self,
87
-
self,
88
-
Self.ttyReadCallback,
89
-
);
90
-
self.winsize_wakeup.wait(
91
-
loop,
92
-
&self.winsize_cmp,
93
-
Self,
94
-
self,
95
-
winsizeCallback,
96
-
);
97
-
const handler: Tty.SignalHandler = .{
98
-
.context = self,
99
-
.callback = Self.signalCallback,
100
-
};
101
-
try Tty.notifyWinsize(handler);
102
-
}
103
-
104
-
fn signalCallback(ptr: *anyopaque) void {
105
-
const self: *Self = @ptrCast(@alignCast(ptr));
106
-
self.winsize_wakeup.notify() catch |err| {
107
-
log.warn("couldn't wake up winsize callback: {}", .{err});
108
-
};
109
-
}
110
-
111
-
fn ttyReadCallback(
112
-
ud: ?*Self,
113
-
loop: *xev.Loop,
114
-
c: *xev.Completion,
115
-
_: xev.File,
116
-
buf: xev.ReadBuffer,
117
-
r: xev.ReadError!usize,
118
-
) xev.CallbackAction {
119
-
const n = r catch |err| {
120
-
log.err("read error: {}", .{err});
121
-
return .disarm;
122
-
};
123
-
const self = ud orelse unreachable;
124
-
125
-
// reset read start state
126
-
self.read_buf_start = 0;
127
-
128
-
var seq_start: usize = 0;
129
-
parse_loop: while (seq_start < n) {
130
-
const result = self.parser.parse(buf.slice[seq_start..n], null) catch |err| {
131
-
log.err("couldn't parse input: {}", .{err});
132
-
return .disarm;
133
-
};
134
-
if (result.n == 0) {
135
-
// copy the read to the beginning. We don't use memcpy because
136
-
// this could be overlapping, and it's also rare
137
-
const initial_start = seq_start;
138
-
while (seq_start < n) : (seq_start += 1) {
139
-
self.read_buf[seq_start - initial_start] = self.read_buf[seq_start];
140
-
}
141
-
self.read_buf_start = seq_start - initial_start + 1;
142
-
return .rearm;
143
-
}
144
-
seq_start += n;
145
-
const event_inner = result.event orelse {
146
-
log.debug("unknown event: {s}", .{self.read_buf[seq_start - n + 1 .. seq_start]});
147
-
continue :parse_loop;
148
-
};
149
-
150
-
// Capture events we want to bubble up
151
-
const event: ?Event = switch (event_inner) {
152
-
.key_press => |key| .{ .key_press = key },
153
-
.key_release => |key| .{ .key_release = key },
154
-
.mouse => |mouse| .{ .mouse = mouse },
155
-
.focus_in => .focus_in,
156
-
.focus_out => .focus_out,
157
-
.paste_start => .paste_start,
158
-
.paste_end => .paste_end,
159
-
.paste => |paste| .{ .paste = paste },
160
-
.color_report => |report| .{ .color_report = report },
161
-
.color_scheme => |scheme| .{ .color_scheme = scheme },
162
-
.winsize => |ws| .{ .winsize = ws },
163
-
164
-
// capability events which we handle below
165
-
.cap_kitty_keyboard,
166
-
.cap_kitty_graphics,
167
-
.cap_rgb,
168
-
.cap_unicode,
169
-
.cap_sgr_pixels,
170
-
.cap_color_scheme_updates,
171
-
.cap_da1,
172
-
=> null, // handled below
173
-
};
174
-
175
-
if (event) |ev| {
176
-
const action = self.callback(self.ud, loop, self, ev);
177
-
switch (action) {
178
-
.disarm => return .disarm,
179
-
else => continue :parse_loop,
180
-
}
181
-
}
182
-
183
-
switch (event_inner) {
184
-
.key_press,
185
-
.key_release,
186
-
.mouse,
187
-
.focus_in,
188
-
.focus_out,
189
-
.paste_start,
190
-
.paste_end,
191
-
.paste,
192
-
.color_report,
193
-
.color_scheme,
194
-
.winsize,
195
-
=> unreachable, // handled above
196
-
197
-
.cap_kitty_keyboard => {
198
-
log.info("kitty keyboard capability detected", .{});
199
-
self.vx.caps.kitty_keyboard = true;
200
-
},
201
-
.cap_kitty_graphics => {
202
-
if (!self.vx.caps.kitty_graphics) {
203
-
log.info("kitty graphics capability detected", .{});
204
-
self.vx.caps.kitty_graphics = true;
205
-
}
206
-
},
207
-
.cap_rgb => {
208
-
log.info("rgb capability detected", .{});
209
-
self.vx.caps.rgb = true;
210
-
},
211
-
.cap_unicode => {
212
-
log.info("unicode capability detected", .{});
213
-
self.vx.caps.unicode = .unicode;
214
-
self.vx.screen.width_method = .unicode;
215
-
},
216
-
.cap_sgr_pixels => {
217
-
log.info("pixel mouse capability detected", .{});
218
-
self.vx.caps.sgr_pixels = true;
219
-
},
220
-
.cap_color_scheme_updates => {
221
-
log.info("color_scheme_updates capability detected", .{});
222
-
self.vx.caps.color_scheme_updates = true;
223
-
},
224
-
.cap_da1 => {
225
-
self.vx.enableDetectedFeatures(self.tty.anyWriter()) catch |err| {
226
-
log.err("couldn't enable features: {}", .{err});
227
-
};
228
-
},
229
-
}
230
-
}
231
-
232
-
self.file.read(
233
-
loop,
234
-
c,
235
-
.{ .slice = &self.read_buf },
236
-
Self,
237
-
self,
238
-
Self.ttyReadCallback,
239
-
);
240
-
return .disarm;
241
-
}
242
-
243
-
fn winsizeCallback(
244
-
ud: ?*Self,
245
-
l: *xev.Loop,
246
-
c: *xev.Completion,
247
-
r: xev.Async.WaitError!void,
248
-
) xev.CallbackAction {
249
-
_ = r catch |err| {
250
-
log.err("async error: {}", .{err});
251
-
return .disarm;
252
-
};
253
-
const self = ud orelse unreachable; // no userdata
254
-
const winsize = Tty.getWinsize(self.tty.fd) catch |err| {
255
-
log.err("couldn't get winsize: {}", .{err});
256
-
return .disarm;
257
-
};
258
-
const ret = self.callback(self.ud, l, self, .{ .winsize = winsize });
259
-
if (ret == .disarm) return .disarm;
260
-
261
-
self.winsize_wakeup.wait(
262
-
l,
263
-
c,
264
-
Self,
265
-
self,
266
-
winsizeCallback,
267
-
);
268
-
return .disarm;
269
-
}
270
-
};
271
-
}
···