feat(event): add focus in/out event to SystemEvents
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 3m54s
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 3m54s
feat(app): add minimal size argument for App.start Read more corresponding inputs from stdin and convert them correctly (i.e. in band window resizing), further keys (arrow keys, F-keys, etc.). Respect the provided minimal size for the application which posts an error message in case the size is smaller than the requested minimal size.
This commit is contained in:
189
src/app.zig
189
src/app.zig
@@ -54,13 +54,18 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
quit_event: std.Thread.ResetEvent = .{},
|
||||
termios: ?std.posix.termios = null,
|
||||
attached_handler: bool = false,
|
||||
min_size: ?terminal.Size = null,
|
||||
prev_size: terminal.Size = .{ .cols = 0, .rows = 0 },
|
||||
|
||||
pub const SignalHandler = struct {
|
||||
context: *anyopaque,
|
||||
callback: *const fn (context: *anyopaque) void,
|
||||
};
|
||||
|
||||
pub fn start(this: *@This()) !void {
|
||||
pub fn start(this: *@This(), min_size: ?terminal.Size) !void {
|
||||
if (fullscreen) { // a minimal size only really makes sense if the application is rendered fullscreen
|
||||
this.min_size = min_size;
|
||||
}
|
||||
if (this.thread) |_| return;
|
||||
|
||||
if (!this.attached_handler) {
|
||||
@@ -137,7 +142,20 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
|
||||
fn winsizeCallback(ptr: *anyopaque) void {
|
||||
const this: *@This() = @ptrCast(@alignCast(ptr));
|
||||
this.postEvent(.{ .resize = terminal.getTerminalSize() });
|
||||
const size = terminal.getTerminalSize();
|
||||
// check for minimal size (if any was provided)
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
}
|
||||
|
||||
var winch_handler: ?SignalHandler = null;
|
||||
@@ -159,7 +177,19 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
// send initial terminal size
|
||||
// changes are handled by the winch signal handler
|
||||
// see `App.start` and `App.registerWinch` for details
|
||||
this.postEvent(.{ .resize = terminal.getTerminalSize() });
|
||||
{
|
||||
// TODO: what should happen if the initial window size is too small?
|
||||
// -> currently the first render call will then crash the application (which happens anyway)
|
||||
const size = terminal.getTerminalSize();
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
}
|
||||
}
|
||||
this.postEvent(.{ .resize = size });
|
||||
}
|
||||
|
||||
// thread to read user inputs
|
||||
var buf: [256]u8 = undefined;
|
||||
@@ -170,6 +200,159 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
// escape key presses
|
||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||
switch (buf[1]) {
|
||||
0x4F => { // ss3
|
||||
if (read_bytes < 3) {
|
||||
continue;
|
||||
}
|
||||
const key: ?Key = switch (buf[2]) {
|
||||
0x1B => null,
|
||||
'A' => .{ .cp = Key.up },
|
||||
'B' => .{ .cp = Key.down },
|
||||
'C' => .{ .cp = Key.right },
|
||||
'D' => .{ .cp = Key.left },
|
||||
'E' => .{ .cp = Key.kp_begin },
|
||||
'F' => .{ .cp = Key.end },
|
||||
'H' => .{ .cp = Key.home },
|
||||
'P' => .{ .cp = Key.f1 },
|
||||
'Q' => .{ .cp = Key.f2 },
|
||||
'R' => .{ .cp = Key.f3 },
|
||||
'S' => .{ .cp = Key.f4 },
|
||||
else => null,
|
||||
};
|
||||
if (key) |k| {
|
||||
this.postEvent(.{ .key = k });
|
||||
}
|
||||
},
|
||||
0x5B => { // csi
|
||||
if (read_bytes < 3) {
|
||||
continue;
|
||||
}
|
||||
// We start iterating at index 2 to get past the '['
|
||||
const sequence = for (buf[2..], 2..) |b, i| {
|
||||
switch (b) {
|
||||
0x40...0xFF => break buf[0 .. i + 1],
|
||||
else => continue,
|
||||
}
|
||||
} else continue;
|
||||
|
||||
const final = sequence[sequence.len - 1];
|
||||
switch (final) {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'H', 'P', 'Q', 'R', 'S' => {
|
||||
// Legacy keys
|
||||
// CSI {ABCDEFHPQS}
|
||||
// CSI 1 ; modifier:event_type {ABCDEFHPQS}
|
||||
const key: Key = .{
|
||||
.cp = switch (final) {
|
||||
'A' => Key.up,
|
||||
'B' => Key.down,
|
||||
'C' => Key.right,
|
||||
'D' => Key.left,
|
||||
'E' => Key.kp_begin,
|
||||
'F' => Key.end,
|
||||
'H' => Key.home,
|
||||
'P' => Key.f1,
|
||||
'Q' => Key.f2,
|
||||
'R' => Key.f3,
|
||||
'S' => Key.f4,
|
||||
else => unreachable, // switch case prevents in this case form ever happening
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
'~' => {
|
||||
// Legacy keys
|
||||
// CSI number ~
|
||||
// CSI number ; modifier ~
|
||||
// CSI number ; modifier:event_type ; text_as_codepoint ~
|
||||
var field_iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const number_buf = field_iter.next() orelse unreachable; // always will have one field
|
||||
const number = std.fmt.parseUnsigned(u16, number_buf, 10) catch break;
|
||||
|
||||
const key: Key = .{
|
||||
.cp = switch (number) {
|
||||
2 => Key.insert,
|
||||
3 => Key.delete,
|
||||
5 => Key.page_up,
|
||||
6 => Key.page_down,
|
||||
7 => Key.home,
|
||||
8 => Key.end,
|
||||
11 => Key.f1,
|
||||
12 => Key.f2,
|
||||
13 => Key.f3,
|
||||
14 => Key.f4,
|
||||
15 => Key.f5,
|
||||
17 => Key.f6,
|
||||
18 => Key.f7,
|
||||
19 => Key.f8,
|
||||
20 => Key.f9,
|
||||
21 => Key.f10,
|
||||
23 => Key.f11,
|
||||
24 => Key.f12,
|
||||
// 200 => return .{ .event = .paste_start, .n = sequence.len },
|
||||
// 201 => return .{ .event = .paste_end, .n = sequence.len },
|
||||
57427 => Key.kp_begin,
|
||||
else => unreachable,
|
||||
},
|
||||
};
|
||||
this.postEvent(.{ .key = key });
|
||||
},
|
||||
|
||||
'I' => this.postEvent(.{ .focus = true }),
|
||||
'O' => this.postEvent(.{ .focus = false }),
|
||||
// 'M', 'm' => return parseMouse(sequence), // TODO: parse mouse inputs
|
||||
'c' => {
|
||||
// Primary DA (CSI ? Pm c)
|
||||
},
|
||||
'n' => {
|
||||
// Device Status Report
|
||||
// CSI Ps n
|
||||
// CSI ? Ps n
|
||||
std.debug.assert(sequence.len >= 3);
|
||||
},
|
||||
't' => {
|
||||
// XTWINOPS
|
||||
// Split first into fields delimited by ';'
|
||||
var iter = std.mem.splitScalar(u8, sequence[2 .. sequence.len - 1], ';');
|
||||
const ps = iter.first();
|
||||
if (std.mem.eql(u8, "48", ps)) {
|
||||
// in band window resize
|
||||
// CSI 48 ; height ; width ; height_pix ; width_pix t
|
||||
const height_char = iter.next() orelse break;
|
||||
const width_char = iter.next() orelse break;
|
||||
|
||||
// TODO: only post the event if the size has changed?
|
||||
// because there might be too many resize events (which force a re-draw of the entire screen)
|
||||
const size: terminal.Size = .{
|
||||
.rows = std.fmt.parseUnsigned(u16, height_char, 10) catch break,
|
||||
.cols = std.fmt.parseUnsigned(u16, width_char, 10) catch break,
|
||||
};
|
||||
// check for minimal size (if any was provided)
|
||||
if (this.min_size) |min_size| {
|
||||
if (size.cols < min_size.cols or size.rows < min_size.rows) {
|
||||
this.postEvent(.{
|
||||
.err = .{ .err = error.InsufficientSize, .msg = "Terminal size is too small for the requested minimal size" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (size.cols != this.prev_size.cols or size.rows != this.prev_size.rows) {
|
||||
this.postEvent(.{ .resize = size });
|
||||
this.prev_size = size;
|
||||
}
|
||||
}
|
||||
},
|
||||
'u' => {
|
||||
// Kitty keyboard
|
||||
// CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u
|
||||
// Not all fields will be present. Only unicode-key-code is
|
||||
// mandatory
|
||||
},
|
||||
'y' => {
|
||||
// DECRPM (CSI ? Ps ; Pm $ y)
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
},
|
||||
// TODO: parse corresponding codes
|
||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||
else => {},
|
||||
|
||||
@@ -17,6 +17,7 @@ pub const SystemEvent = union(enum) {
|
||||
err: Error,
|
||||
resize: Size,
|
||||
key: Key,
|
||||
focus: bool,
|
||||
};
|
||||
|
||||
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
|
||||
|
||||
@@ -89,6 +89,7 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: implement a minimal size requirement for Widgets to render correctly?
|
||||
// import and export of `Widget` implementations
|
||||
pub const Text = @import("widget/Text.zig").Widget(Event, Renderer);
|
||||
pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer);
|
||||
|
||||
@@ -62,28 +62,28 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
},
|
||||
.key => |key| {
|
||||
var require_render = true;
|
||||
if (key.matches(.{ .cp = 'g' })) {
|
||||
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = terminal.Key.home })) {
|
||||
// top
|
||||
if (this.idx != 0) {
|
||||
this.idx = 0;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'G' })) {
|
||||
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = terminal.Key.end })) {
|
||||
// bottom
|
||||
if (this.idx < this.contents.items.len -| 1) {
|
||||
this.idx = this.contents.items.len -| 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'j' })) {
|
||||
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = terminal.Key.down })) {
|
||||
// down
|
||||
if (this.idx < this.contents.items.len -| 1) {
|
||||
this.idx += 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'k' })) {
|
||||
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = terminal.Key.up })) {
|
||||
// up
|
||||
if (this.idx > 0) {
|
||||
this.idx -= 1;
|
||||
|
||||
@@ -53,28 +53,28 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type {
|
||||
}
|
||||
},
|
||||
.key => |key| {
|
||||
if (key.matches(.{ .cp = 'g' })) {
|
||||
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = Key.home })) {
|
||||
// top
|
||||
if (this.line != 0) {
|
||||
this.line = 0;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'G' })) {
|
||||
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = Key.end })) {
|
||||
// bottom
|
||||
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
|
||||
this.line = this.line_index.items.len -| 1 -| this.size.rows;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'j' })) {
|
||||
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = Key.down })) {
|
||||
// down
|
||||
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
|
||||
this.line += 1;
|
||||
} else {
|
||||
require_render = false;
|
||||
}
|
||||
} else if (key.matches(.{ .cp = 'k' })) {
|
||||
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = Key.up })) {
|
||||
// up
|
||||
if (this.line > 0) {
|
||||
this.line -= 1;
|
||||
|
||||
Reference in New Issue
Block a user