mirror of
https://github.com/fairyglade/ly.git
synced 2026-03-25 09:46:06 +00:00
Move the event loop to a separate function
Signed-off-by: AnErrupTion <anerruption@disroot.org>
This commit is contained in:
304
src/main.zig
304
src/main.zig
@@ -66,11 +66,7 @@ fn ttyControlTransferSignalHandler(_: c_int) callconv(.c) void {
|
|||||||
|
|
||||||
const UiState = struct {
|
const UiState = struct {
|
||||||
allocator: Allocator,
|
allocator: Allocator,
|
||||||
active_widget_index: usize,
|
|
||||||
handlable_widgets: std.ArrayList(*Widget),
|
|
||||||
auth_fails: u64,
|
auth_fails: u64,
|
||||||
run: bool,
|
|
||||||
update: bool,
|
|
||||||
is_autologin: bool,
|
is_autologin: bool,
|
||||||
use_kmscon_vt: bool,
|
use_kmscon_vt: bool,
|
||||||
active_tty: u8,
|
active_tty: u8,
|
||||||
@@ -966,10 +962,7 @@ pub fn main() !void {
|
|||||||
state.config.auth_fails,
|
state.config.auth_fails,
|
||||||
);
|
);
|
||||||
|
|
||||||
state.active_widget_index = 0;
|
|
||||||
state.auth_fails = 0;
|
state.auth_fails = 0;
|
||||||
state.run = true;
|
|
||||||
state.update = true;
|
|
||||||
state.animate = state.config.animation != .none;
|
state.animate = state.config.animation != .none;
|
||||||
state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert;
|
state.insert_mode = !state.config.vi_mode or state.config.vi_default_mode == .insert;
|
||||||
state.edge_margin = Position.init(
|
state.edge_margin = Position.init(
|
||||||
@@ -1056,36 +1049,24 @@ pub fn main() !void {
|
|||||||
try widgets.append(state.allocator, cascade.widget());
|
try widgets.append(state.allocator, cascade.widget());
|
||||||
}
|
}
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Esc", &disableInsertMode);
|
try state.buffer.registerKeybind("Esc", &disableInsertMode, &state);
|
||||||
try state.buffer.registerKeybind("I", &enableInsertMode);
|
try state.buffer.registerKeybind("I", &enableInsertMode, &state);
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Ctrl+C", &quit);
|
try state.buffer.registerKeybind("Ctrl+C", &quit, &state);
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Ctrl+U", &clearPassword);
|
try state.buffer.registerKeybind("Ctrl+U", &clearPassword, &state);
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Ctrl+K", &moveCursorUp);
|
try state.buffer.registerKeybind("K", &viMoveCursorUp, &state);
|
||||||
try state.buffer.registerKeybind("Up", &moveCursorUp);
|
try state.buffer.registerKeybind("J", &viMoveCursorDown, &state);
|
||||||
try state.buffer.registerKeybind("K", &viMoveCursorUp);
|
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Ctrl+J", &moveCursorDown);
|
try state.buffer.registerKeybind("Enter", &authenticate, &state);
|
||||||
try state.buffer.registerKeybind("Down", &moveCursorDown);
|
|
||||||
try state.buffer.registerKeybind("J", &viMoseCursorDown);
|
|
||||||
|
|
||||||
try state.buffer.registerKeybind("Tab", &wrapCursor);
|
try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd, &state);
|
||||||
try state.buffer.registerKeybind("Shift+Tab", &wrapCursorReverse);
|
try state.buffer.registerKeybind(state.config.restart_key, &restartCmd, &state);
|
||||||
|
if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd, &state);
|
||||||
try state.buffer.registerKeybind("Enter", &authenticate);
|
if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd, &state);
|
||||||
|
if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd, &state);
|
||||||
try state.buffer.registerKeybind(state.config.shutdown_key, &shutdownCmd);
|
if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd, &state);
|
||||||
try state.buffer.registerKeybind(state.config.restart_key, &restartCmd);
|
|
||||||
if (state.config.sleep_cmd != null) try state.buffer.registerKeybind(state.config.sleep_key, &sleepCmd);
|
|
||||||
if (state.config.hibernate_cmd != null) try state.buffer.registerKeybind(state.config.hibernate_key, &hibernateCmd);
|
|
||||||
if (state.config.brightness_down_key) |key| try state.buffer.registerKeybind(key, &decreaseBrightnessCmd);
|
|
||||||
if (state.config.brightness_up_key) |key| try state.buffer.registerKeybind(key, &increaseBrightnessCmd);
|
|
||||||
|
|
||||||
var event: termbox.tb_event = undefined;
|
|
||||||
var inactivity_time_start = try interop.getTimeOfDay();
|
|
||||||
var inactivity_cmd_ran = false;
|
|
||||||
|
|
||||||
if (state.config.initial_info_text) |text| {
|
if (state.config.initial_info_text) |text| {
|
||||||
try state.info_line.addMessage(text, state.config.bg, state.config.fg);
|
try state.info_line.addMessage(text, state.config.bg, state.config.fg);
|
||||||
@@ -1122,159 +1103,20 @@ pub fn main() !void {
|
|||||||
.password => state.password_widget,
|
.password => state.password_widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run the event loop
|
var shared_error = try SharedError.init();
|
||||||
state.handlable_widgets = .empty;
|
defer shared_error.deinit();
|
||||||
defer state.handlable_widgets.deinit(state.allocator);
|
|
||||||
|
|
||||||
var i: usize = 0;
|
try state.buffer.runEventLoop(
|
||||||
for (widgets.items) |*widget| {
|
|
||||||
if (widget.vtable.handle_fn != null) {
|
|
||||||
try state.handlable_widgets.append(state.allocator, widget);
|
|
||||||
|
|
||||||
if (widget.id == active_widget.id) state.active_widget_index = i;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (widgets.items) |*widget| try widget.update(&state);
|
|
||||||
positionComponents(&state);
|
|
||||||
|
|
||||||
while (state.run) {
|
|
||||||
if (state.update) {
|
|
||||||
for (widgets.items) |*widget| try widget.update(&state);
|
|
||||||
|
|
||||||
// Reset cursor
|
|
||||||
const current_widget = getActiveWidget(&state);
|
|
||||||
current_widget.handle(null, state.insert_mode) catch |err| {
|
|
||||||
try state.info_line.addMessage(
|
|
||||||
state.lang.err_alloc,
|
|
||||||
state.config.error_bg,
|
|
||||||
state.config.error_fg,
|
|
||||||
);
|
|
||||||
try state.log_file.err(
|
|
||||||
"tui",
|
|
||||||
"failed to set cursor in active widget '{s}': {s}",
|
|
||||||
.{ current_widget.display_name, @errorName(err) },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
try TerminalBuffer.clearScreen(false);
|
|
||||||
|
|
||||||
for (widgets.items) |*widget| widget.draw();
|
|
||||||
|
|
||||||
TerminalBuffer.presentBuffer();
|
|
||||||
}
|
|
||||||
|
|
||||||
var maybe_timeout: ?usize = null;
|
|
||||||
for (widgets.items) |*widget| {
|
|
||||||
if (try widget.calculateTimeout(&state)) |widget_timeout| {
|
|
||||||
if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.config.inactivity_cmd) |inactivity_cmd| {
|
|
||||||
const time = try interop.getTimeOfDay();
|
|
||||||
|
|
||||||
if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > state.config.inactivity_delay) {
|
|
||||||
var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator);
|
|
||||||
inactivity.stdout_behavior = .Ignore;
|
|
||||||
inactivity.stderr_behavior = .Ignore;
|
|
||||||
|
|
||||||
handle_inactivity_cmd: {
|
|
||||||
const process_result = inactivity.spawnAndWait() catch {
|
|
||||||
break :handle_inactivity_cmd;
|
|
||||||
};
|
|
||||||
if (process_result.Exited != 0) {
|
|
||||||
try state.info_line.addMessage(
|
|
||||||
state.lang.err_inactivity,
|
|
||||||
state.config.error_bg,
|
|
||||||
state.config.error_fg,
|
|
||||||
);
|
|
||||||
try state.log_file.err(
|
|
||||||
"sys",
|
|
||||||
"failed to execute inactivity command: exit code {d}",
|
|
||||||
.{process_result.Exited},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inactivity_cmd_ran = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event);
|
|
||||||
|
|
||||||
state.update = maybe_timeout != null;
|
|
||||||
|
|
||||||
if (event_error < 0) continue;
|
|
||||||
|
|
||||||
// Input of some kind was detected, so reset the inactivity timer
|
|
||||||
inactivity_time_start = try interop.getTimeOfDay();
|
|
||||||
|
|
||||||
if (event.type == termbox.TB_EVENT_RESIZE) {
|
|
||||||
state.buffer.width = TerminalBuffer.getWidth();
|
|
||||||
state.buffer.height = TerminalBuffer.getHeight();
|
|
||||||
|
|
||||||
try state.log_file.info("tui", "screen resolution updated to {d}x{d}", .{ state.buffer.width, state.buffer.height });
|
|
||||||
|
|
||||||
for (widgets.items) |*widget| {
|
|
||||||
widget.realloc() catch |err| {
|
|
||||||
try state.info_line.addMessage(
|
|
||||||
state.lang.err_alloc,
|
|
||||||
state.config.error_bg,
|
|
||||||
state.config.error_fg,
|
|
||||||
);
|
|
||||||
try state.log_file.err(
|
|
||||||
"tui",
|
|
||||||
"failed to reallocate widget '{s}': {s}",
|
|
||||||
.{ widget.display_name, @errorName(err) },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
positionComponents(&state);
|
|
||||||
|
|
||||||
state.update = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var maybe_keys = try state.buffer.handleKeybind(
|
|
||||||
state.allocator,
|
state.allocator,
|
||||||
event,
|
shared_error,
|
||||||
|
widgets.items,
|
||||||
|
active_widget,
|
||||||
|
state.config.inactivity_delay,
|
||||||
|
&state.insert_mode, // FIXME: Hack
|
||||||
|
positionWidgets,
|
||||||
|
handleInactivity,
|
||||||
&state,
|
&state,
|
||||||
);
|
);
|
||||||
if (maybe_keys) |*keys| {
|
|
||||||
defer keys.deinit(state.allocator);
|
|
||||||
|
|
||||||
const current_widget = getActiveWidget(&state);
|
|
||||||
for (keys.items) |key| {
|
|
||||||
current_widget.handle(key, state.insert_mode) catch |err| {
|
|
||||||
try state.info_line.addMessage(
|
|
||||||
state.lang.err_alloc,
|
|
||||||
state.config.error_bg,
|
|
||||||
state.config.error_fg,
|
|
||||||
);
|
|
||||||
try state.log_file.err(
|
|
||||||
"tui",
|
|
||||||
"failed to handle active widget '{s}': {s}",
|
|
||||||
.{ current_widget.display_name, @errorName(err) },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
state.update = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn getActiveWidget(state: *UiState) *Widget {
|
|
||||||
return state.handlable_widgets.items[state.active_widget_index];
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setActiveWidget(state: *UiState, widget: *Widget) void {
|
|
||||||
for (state.handlable_widgets.items, 0..) |widg, i| {
|
|
||||||
if (widg.id == widget.id) state.active_widget_index = i;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disableInsertMode(ptr: *anyopaque) !bool {
|
fn disableInsertMode(ptr: *anyopaque) !bool {
|
||||||
@@ -1282,7 +1124,7 @@ fn disableInsertMode(ptr: *anyopaque) !bool {
|
|||||||
|
|
||||||
if (state.config.vi_mode and state.insert_mode) {
|
if (state.config.vi_mode and state.insert_mode) {
|
||||||
state.insert_mode = false;
|
state.insert_mode = false;
|
||||||
state.update = true;
|
state.buffer.drawNextFrame(true);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1292,78 +1134,38 @@ fn enableInsertMode(ptr: *anyopaque) !bool {
|
|||||||
if (state.insert_mode) return true;
|
if (state.insert_mode) return true;
|
||||||
|
|
||||||
state.insert_mode = true;
|
state.insert_mode = true;
|
||||||
state.update = true;
|
state.buffer.drawNextFrame(true);
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn clearPassword(ptr: *anyopaque) !bool {
|
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
|
||||||
|
|
||||||
if (getActiveWidget(state) == &state.password_widget) {
|
|
||||||
state.password.clear();
|
|
||||||
state.update = true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn moveCursorUp(ptr: *anyopaque) !bool {
|
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
|
||||||
if (state.active_widget_index == 0) return false;
|
|
||||||
|
|
||||||
state.active_widget_index -= 1;
|
|
||||||
state.update = true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn viMoveCursorUp(ptr: *anyopaque) !bool {
|
fn viMoveCursorUp(ptr: *anyopaque) !bool {
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
if (state.insert_mode) return true;
|
if (state.insert_mode) return true;
|
||||||
if (state.active_widget_index == 0) return false;
|
|
||||||
|
|
||||||
state.active_widget_index -= 1;
|
return try state.buffer.simulateKeybind("Up");
|
||||||
state.update = true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn moveCursorDown(ptr: *anyopaque) !bool {
|
fn viMoveCursorDown(ptr: *anyopaque) !bool {
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
|
||||||
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
|
|
||||||
|
|
||||||
state.active_widget_index += 1;
|
|
||||||
state.update = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn viMoseCursorDown(ptr: *anyopaque) !bool {
|
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
if (state.insert_mode) return true;
|
if (state.insert_mode) return true;
|
||||||
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
|
|
||||||
|
|
||||||
state.active_widget_index += 1;
|
return try state.buffer.simulateKeybind("Down");
|
||||||
state.update = true;
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrapCursor(ptr: *anyopaque) !bool {
|
fn clearPassword(ptr: *anyopaque) !bool {
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len;
|
if (state.buffer.getActiveWidget().id == state.password_widget.id) {
|
||||||
state.update = true;
|
state.password.clear();
|
||||||
return false;
|
state.buffer.drawNextFrame(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wrapCursorReverse(ptr: *anyopaque) !bool {
|
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
|
||||||
|
|
||||||
state.active_widget_index = (state.active_widget_index - 1) % state.handlable_widgets.items.len;
|
|
||||||
state.update = true;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn quit(ptr: *anyopaque) !bool {
|
fn quit(ptr: *anyopaque) !bool {
|
||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
state.run = false;
|
state.buffer.stopEventLoop();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,7 +1326,7 @@ fn authenticate(ptr: *anyopaque) !bool {
|
|||||||
const auth_err = shared_err.readError();
|
const auth_err = shared_err.readError();
|
||||||
if (auth_err) |err| {
|
if (auth_err) |err| {
|
||||||
state.auth_fails += 1;
|
state.auth_fails += 1;
|
||||||
setActiveWidget(state, &state.password_widget);
|
state.buffer.setActiveWidget(state.password_widget);
|
||||||
|
|
||||||
try state.info_line.addMessage(
|
try state.info_line.addMessage(
|
||||||
getAuthErrorMsg(err, state.lang),
|
getAuthErrorMsg(err, state.lang),
|
||||||
@@ -1556,7 +1358,7 @@ fn authenticate(ptr: *anyopaque) !bool {
|
|||||||
|
|
||||||
if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) {
|
if (state.config.auth_fails == 0 or state.auth_fails < state.config.auth_fails) {
|
||||||
try TerminalBuffer.clearScreen(true);
|
try TerminalBuffer.clearScreen(true);
|
||||||
state.update = true;
|
state.buffer.drawNextFrame(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore the cursor
|
// Restore the cursor
|
||||||
@@ -1569,7 +1371,7 @@ fn shutdownCmd(ptr: *anyopaque) !bool {
|
|||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
shutdown = true;
|
shutdown = true;
|
||||||
state.run = false;
|
state.buffer.stopEventLoop();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1577,7 +1379,7 @@ fn restartCmd(ptr: *anyopaque) !bool {
|
|||||||
var state: *UiState = @ptrCast(@alignCast(ptr));
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
restart = true;
|
restart = true;
|
||||||
state.run = false;
|
state.buffer.stopEventLoop();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1810,7 +1612,9 @@ fn updateSessionSpecifier(self: *Label, ptr: *anyopaque) !void {
|
|||||||
self.setText(env.environment.specifier);
|
self.setText(env.environment.specifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn positionComponents(state: *UiState) void {
|
fn positionWidgets(ptr: *anyopaque) !void {
|
||||||
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
if (!state.config.hide_key_hints) {
|
if (!state.config.hide_key_hints) {
|
||||||
state.shutdown_label.positionX(state.edge_margin
|
state.shutdown_label.positionX(state.edge_margin
|
||||||
.add(TerminalBuffer.START_POSITION));
|
.add(TerminalBuffer.START_POSITION));
|
||||||
@@ -1895,6 +1699,34 @@ fn positionComponents(state: *UiState) void {
|
|||||||
.invertY(state.buffer.height - 1));
|
.invertY(state.buffer.height - 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handleInactivity(ptr: *anyopaque) !void {
|
||||||
|
var state: *UiState = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
if (state.config.inactivity_cmd) |inactivity_cmd| {
|
||||||
|
var inactivity = std.process.Child.init(&[_][]const u8{ "/bin/sh", "-c", inactivity_cmd }, state.allocator);
|
||||||
|
inactivity.stdout_behavior = .Ignore;
|
||||||
|
inactivity.stderr_behavior = .Ignore;
|
||||||
|
|
||||||
|
handle_inactivity_cmd: {
|
||||||
|
const process_result = inactivity.spawnAndWait() catch {
|
||||||
|
break :handle_inactivity_cmd;
|
||||||
|
};
|
||||||
|
if (process_result.Exited != 0) {
|
||||||
|
try state.info_line.addMessage(
|
||||||
|
state.lang.err_inactivity,
|
||||||
|
state.config.error_bg,
|
||||||
|
state.config.error_fg,
|
||||||
|
);
|
||||||
|
try state.log_file.err(
|
||||||
|
"sys",
|
||||||
|
"failed to execute inactivity command: exit code {d}",
|
||||||
|
.{process_result.Exited},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void {
|
fn addOtherEnvironment(session: *Session, lang: Lang, display_server: DisplayServer, exec: ?[]const u8) !void {
|
||||||
const name = switch (display_server) {
|
const name = switch (display_server) {
|
||||||
.shell => lang.shell,
|
.shell => lang.shell,
|
||||||
|
|||||||
@@ -5,16 +5,21 @@ const Random = std.Random;
|
|||||||
const ly_core = @import("ly-core");
|
const ly_core = @import("ly-core");
|
||||||
const interop = ly_core.interop;
|
const interop = ly_core.interop;
|
||||||
const LogFile = ly_core.LogFile;
|
const LogFile = ly_core.LogFile;
|
||||||
|
const SharedError = ly_core.SharedError;
|
||||||
pub const termbox = @import("termbox2");
|
pub const termbox = @import("termbox2");
|
||||||
|
|
||||||
const Cell = @import("Cell.zig");
|
const Cell = @import("Cell.zig");
|
||||||
const keyboard = @import("keyboard.zig");
|
const keyboard = @import("keyboard.zig");
|
||||||
const Position = @import("Position.zig");
|
const Position = @import("Position.zig");
|
||||||
|
const Widget = @import("Widget.zig");
|
||||||
|
|
||||||
const TerminalBuffer = @This();
|
const TerminalBuffer = @This();
|
||||||
|
|
||||||
const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool;
|
const KeybindCallbackFn = *const fn (*anyopaque) anyerror!bool;
|
||||||
const KeybindMap = std.AutoHashMap(keyboard.Key, KeybindCallbackFn);
|
const KeybindMap = std.AutoHashMap(keyboard.Key, struct {
|
||||||
|
callback: KeybindCallbackFn,
|
||||||
|
context: *anyopaque,
|
||||||
|
});
|
||||||
|
|
||||||
pub const InitOptions = struct {
|
pub const InitOptions = struct {
|
||||||
fg: u32,
|
fg: u32,
|
||||||
@@ -85,8 +90,17 @@ blank_cell: Cell,
|
|||||||
full_color: bool,
|
full_color: bool,
|
||||||
termios: ?std.posix.termios,
|
termios: ?std.posix.termios,
|
||||||
keybinds: KeybindMap,
|
keybinds: KeybindMap,
|
||||||
|
handlable_widgets: std.ArrayList(*Widget),
|
||||||
|
run: bool,
|
||||||
|
update: bool,
|
||||||
|
active_widget_index: usize,
|
||||||
|
|
||||||
pub fn init(allocator: Allocator, options: InitOptions, log_file: *LogFile, random: Random) !TerminalBuffer {
|
pub fn init(
|
||||||
|
allocator: Allocator,
|
||||||
|
options: InitOptions,
|
||||||
|
log_file: *LogFile,
|
||||||
|
random: Random,
|
||||||
|
) !TerminalBuffer {
|
||||||
// Initialize termbox
|
// Initialize termbox
|
||||||
_ = termbox.tb_init();
|
_ = termbox.tb_init();
|
||||||
|
|
||||||
@@ -139,6 +153,10 @@ pub fn init(allocator: Allocator, options: InitOptions, log_file: *LogFile, rand
|
|||||||
// Needed to reclaim the TTY after giving up its control
|
// Needed to reclaim the TTY after giving up its control
|
||||||
.termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO),
|
.termios = try std.posix.tcgetattr(std.posix.STDIN_FILENO),
|
||||||
.keybinds = KeybindMap.init(allocator),
|
.keybinds = KeybindMap.init(allocator),
|
||||||
|
.handlable_widgets = .empty,
|
||||||
|
.run = true,
|
||||||
|
.update = true,
|
||||||
|
.active_widget_index = 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,6 +165,159 @@ pub fn deinit(self: *TerminalBuffer) void {
|
|||||||
TerminalBuffer.shutdown();
|
TerminalBuffer.shutdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn runEventLoop(
|
||||||
|
self: *TerminalBuffer,
|
||||||
|
allocator: Allocator,
|
||||||
|
shared_error: SharedError,
|
||||||
|
widgets: []Widget,
|
||||||
|
active_widget: Widget,
|
||||||
|
inactivity_delay: u16,
|
||||||
|
insert_mode: *bool,
|
||||||
|
position_widgets_fn: *const fn (*anyopaque) anyerror!void,
|
||||||
|
inactivity_event_fn: ?*const fn (*anyopaque) anyerror!void,
|
||||||
|
context: *anyopaque,
|
||||||
|
) !void {
|
||||||
|
try self.registerKeybind("Ctrl+K", &moveCursorUp, self);
|
||||||
|
try self.registerKeybind("Up", &moveCursorUp, self);
|
||||||
|
|
||||||
|
try self.registerKeybind("Ctrl+J", &moveCursorDown, self);
|
||||||
|
try self.registerKeybind("Down", &moveCursorDown, self);
|
||||||
|
|
||||||
|
try self.registerKeybind("Tab", &wrapCursor, self);
|
||||||
|
try self.registerKeybind("Shift+Tab", &wrapCursorReverse, self);
|
||||||
|
|
||||||
|
defer self.handlable_widgets.deinit(allocator);
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
for (widgets) |*widget| {
|
||||||
|
if (widget.vtable.handle_fn != null) {
|
||||||
|
try self.handlable_widgets.append(allocator, widget);
|
||||||
|
|
||||||
|
if (widget.id == active_widget.id) self.active_widget_index = i;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (widgets) |*widget| try widget.update(context);
|
||||||
|
try @call(.auto, position_widgets_fn, .{context});
|
||||||
|
|
||||||
|
var event: termbox.tb_event = undefined;
|
||||||
|
var inactivity_cmd_ran = false;
|
||||||
|
var inactivity_time_start = try interop.getTimeOfDay();
|
||||||
|
|
||||||
|
while (self.run) {
|
||||||
|
if (self.update) {
|
||||||
|
for (widgets) |*widget| try widget.update(context);
|
||||||
|
|
||||||
|
// Reset cursor
|
||||||
|
const current_widget = self.getActiveWidget();
|
||||||
|
current_widget.handle(null, insert_mode.*) catch |err| {
|
||||||
|
shared_error.writeError(error.SetCursorFailed);
|
||||||
|
try self.log_file.err(
|
||||||
|
"tui",
|
||||||
|
"failed to set cursor in active widget '{s}': {s}",
|
||||||
|
.{ current_widget.display_name, @errorName(err) },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
try TerminalBuffer.clearScreen(false);
|
||||||
|
|
||||||
|
for (widgets) |*widget| widget.draw();
|
||||||
|
|
||||||
|
TerminalBuffer.presentBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybe_timeout: ?usize = null;
|
||||||
|
for (widgets) |*widget| {
|
||||||
|
if (try widget.calculateTimeout(context)) |widget_timeout| {
|
||||||
|
if (maybe_timeout == null or widget_timeout < maybe_timeout.?) maybe_timeout = widget_timeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inactivity_event_fn) |inactivity_fn| {
|
||||||
|
const time = try interop.getTimeOfDay();
|
||||||
|
|
||||||
|
if (!inactivity_cmd_ran and time.seconds - inactivity_time_start.seconds > inactivity_delay) {
|
||||||
|
try @call(.auto, inactivity_fn, .{context});
|
||||||
|
inactivity_cmd_ran = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const event_error = if (maybe_timeout) |timeout| termbox.tb_peek_event(&event, @intCast(timeout)) else termbox.tb_poll_event(&event);
|
||||||
|
|
||||||
|
self.update = maybe_timeout != null;
|
||||||
|
|
||||||
|
if (event_error < 0) continue;
|
||||||
|
|
||||||
|
// Input of some kind was detected, so reset the inactivity timer
|
||||||
|
inactivity_time_start = try interop.getTimeOfDay();
|
||||||
|
|
||||||
|
if (event.type == termbox.TB_EVENT_RESIZE) {
|
||||||
|
self.width = TerminalBuffer.getWidth();
|
||||||
|
self.height = TerminalBuffer.getHeight();
|
||||||
|
|
||||||
|
try self.log_file.info(
|
||||||
|
"tui",
|
||||||
|
"screen resolution updated to {d}x{d}",
|
||||||
|
.{ self.width, self.height },
|
||||||
|
);
|
||||||
|
|
||||||
|
for (widgets) |*widget| {
|
||||||
|
widget.realloc() catch |err| {
|
||||||
|
shared_error.writeError(error.WidgetReallocationFailed);
|
||||||
|
try self.log_file.err(
|
||||||
|
"tui",
|
||||||
|
"failed to reallocate widget '{s}': {s}",
|
||||||
|
.{ widget.display_name, @errorName(err) },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
try @call(.auto, position_widgets_fn, .{context});
|
||||||
|
|
||||||
|
self.update = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var maybe_keys = try self.handleKeybind(allocator, event);
|
||||||
|
if (maybe_keys) |*keys| {
|
||||||
|
defer keys.deinit(allocator);
|
||||||
|
|
||||||
|
const current_widget = self.getActiveWidget();
|
||||||
|
for (keys.items) |key| {
|
||||||
|
current_widget.handle(key, insert_mode.*) catch |err| {
|
||||||
|
shared_error.writeError(error.CurrentWidgetHandlingFailed);
|
||||||
|
try self.log_file.err(
|
||||||
|
"tui",
|
||||||
|
"failed to handle active widget '{s}': {s}",
|
||||||
|
.{ current_widget.display_name, @errorName(err) },
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stopEventLoop(self: *TerminalBuffer) void {
|
||||||
|
self.run = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drawNextFrame(self: *TerminalBuffer, value: bool) void {
|
||||||
|
self.update = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getActiveWidget(self: *TerminalBuffer) *Widget {
|
||||||
|
return self.handlable_widgets.items[self.active_widget_index];
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn setActiveWidget(self: *TerminalBuffer, widget: Widget) void {
|
||||||
|
for (self.handlable_widgets.items, 0..) |widg, i| {
|
||||||
|
if (widg.id == widget.id) self.active_widget_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getWidth() usize {
|
pub fn getWidth() usize {
|
||||||
return @intCast(termbox.tb_width());
|
return @intCast(termbox.tb_width());
|
||||||
}
|
}
|
||||||
@@ -211,31 +382,18 @@ pub fn reclaim(self: TerminalBuffer) !void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn registerKeybind(self: *TerminalBuffer, keybind: []const u8, callback: KeybindCallbackFn) !void {
|
pub fn registerKeybind(
|
||||||
var key = std.mem.zeroes(keyboard.Key);
|
self: *TerminalBuffer,
|
||||||
var iterator = std.mem.splitScalar(u8, keybind, '+');
|
keybind: []const u8,
|
||||||
|
callback: KeybindCallbackFn,
|
||||||
|
context: *anyopaque,
|
||||||
|
) !void {
|
||||||
|
const key = try self.parseKeybind(keybind);
|
||||||
|
|
||||||
while (iterator.next()) |item| {
|
self.keybinds.put(key, .{
|
||||||
var found = false;
|
.callback = callback,
|
||||||
|
.context = context,
|
||||||
inline for (std.meta.fields(keyboard.Key)) |field| {
|
}) catch |err| {
|
||||||
if (std.ascii.eqlIgnoreCase(field.name, item)) {
|
|
||||||
@field(key, field.name) = true;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!found) {
|
|
||||||
try self.log_file.err(
|
|
||||||
"tui",
|
|
||||||
"failed to parse key {s} of keybind {s}",
|
|
||||||
.{ item, keybind },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.keybinds.put(key, callback) catch |err| {
|
|
||||||
try self.log_file.err(
|
try self.log_file.err(
|
||||||
"tui",
|
"tui",
|
||||||
"failed to register keybind {s}: {s}",
|
"failed to register keybind {s}: {s}",
|
||||||
@@ -244,27 +402,18 @@ pub fn registerKeybind(self: *TerminalBuffer, keybind: []const u8, callback: Key
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handleKeybind(
|
pub fn simulateKeybind(self: *TerminalBuffer, keybind: []const u8) !bool {
|
||||||
self: *TerminalBuffer,
|
const key = try self.parseKeybind(keybind);
|
||||||
allocator: Allocator,
|
|
||||||
tb_event: termbox.tb_event,
|
|
||||||
context: *anyopaque,
|
|
||||||
) !?std.ArrayList(keyboard.Key) {
|
|
||||||
var keys = try keyboard.getKeyList(allocator, tb_event);
|
|
||||||
|
|
||||||
for (keys.items) |key| {
|
if (self.keybinds.get(key)) |binding| {
|
||||||
if (self.keybinds.get(key)) |callback| {
|
return try @call(
|
||||||
const passthrough_event = try @call(.auto, callback, .{context});
|
.auto,
|
||||||
if (!passthrough_event) {
|
binding.callback,
|
||||||
keys.deinit(allocator);
|
.{binding.context},
|
||||||
return null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return keys;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return keys;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn drawText(
|
pub fn drawText(
|
||||||
@@ -335,3 +484,91 @@ fn clearBackBuffer() !void {
|
|||||||
const capability_slice = std.mem.span(capability);
|
const capability_slice = std.mem.span(capability);
|
||||||
_ = try std.posix.write(termbox.global.ttyfd, capability_slice);
|
_ = try std.posix.write(termbox.global.ttyfd, capability_slice);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parseKeybind(self: *TerminalBuffer, keybind: []const u8) !keyboard.Key {
|
||||||
|
var key = std.mem.zeroes(keyboard.Key);
|
||||||
|
var iterator = std.mem.splitScalar(u8, keybind, '+');
|
||||||
|
|
||||||
|
while (iterator.next()) |item| {
|
||||||
|
var found = false;
|
||||||
|
|
||||||
|
inline for (std.meta.fields(keyboard.Key)) |field| {
|
||||||
|
if (std.ascii.eqlIgnoreCase(field.name, item)) {
|
||||||
|
@field(key, field.name) = true;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
try self.log_file.err(
|
||||||
|
"tui",
|
||||||
|
"failed to parse key {s} of keybind {s}",
|
||||||
|
.{ item, keybind },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handleKeybind(
|
||||||
|
self: *TerminalBuffer,
|
||||||
|
allocator: Allocator,
|
||||||
|
tb_event: termbox.tb_event,
|
||||||
|
) !?std.ArrayList(keyboard.Key) {
|
||||||
|
var keys = try keyboard.getKeyList(allocator, tb_event);
|
||||||
|
|
||||||
|
for (keys.items) |key| {
|
||||||
|
if (self.keybinds.get(key)) |binding| {
|
||||||
|
const passthrough_event = try @call(
|
||||||
|
.auto,
|
||||||
|
binding.callback,
|
||||||
|
.{binding.context},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!passthrough_event) {
|
||||||
|
keys.deinit(allocator);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn moveCursorUp(ptr: *anyopaque) !bool {
|
||||||
|
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||||
|
if (state.active_widget_index == 0) return false;
|
||||||
|
|
||||||
|
state.active_widget_index -= 1;
|
||||||
|
state.update = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn moveCursorDown(ptr: *anyopaque) !bool {
|
||||||
|
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||||
|
if (state.active_widget_index == state.handlable_widgets.items.len - 1) return false;
|
||||||
|
|
||||||
|
state.active_widget_index += 1;
|
||||||
|
state.update = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrapCursor(ptr: *anyopaque) !bool {
|
||||||
|
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
state.active_widget_index = (state.active_widget_index + 1) % state.handlable_widgets.items.len;
|
||||||
|
state.update = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrapCursorReverse(ptr: *anyopaque) !bool {
|
||||||
|
var state: *TerminalBuffer = @ptrCast(@alignCast(ptr));
|
||||||
|
|
||||||
|
state.active_widget_index = if (state.active_widget_index == 0) state.handlable_widgets.items.len - 1 else state.active_widget_index - 1;
|
||||||
|
state.update = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user