const std = @import("std");
const linput = @import("shims/linput.zig");
const time = @import("shims/time.zig");
const stat = @cImport({
@cInclude("sys/types.h");
@cInclude("sys/stat.h");
});
// Interfaces - e.g. holds some state (structs)
const framebuffer = @import("interfaces/framebuffer.zig");
const Keyhandler = @import("interfaces/Keyhandler.zig");
const Uinputkb = @import("interfaces/Uinputkb.zig");
// Utils - e.g. helper (pure) fns
const utilbacklight = @import("util/utilbacklight.zig");
const utilbin = @import("util/utilbin.zig");
const utildevfs = @import("util/utildevfs.zig");
const utileviocgrab = @import("util/utileviocgrab.zig");
const utilfiles = @import("util/utilfiles.zig");
const utilgrid = @import("util/utilgrid.zig");
const utilioctl = @import("util/utilioctl.zig");
const utilkeycode = @import("util/utilkeycode.zig");
const utilled = @import("util/utilled.zig");
const utiltime = @import("util/utiltime.zig");
const utiltty = @import("util/utiltty.zig");
const utilwakeupsources = @import("util/utilwakeupsources.zig");
const config = @import("config.zig");
const types = @import("types.zig");
allocator: std.mem.Allocator,
data_keyboard: []types.GridItem,
datalist_strings: std.ArrayList(types.GridItem),
fifo_data: std.fifo.LinearFifo(u8, std.fifo.LinearFifoBufferType{ .Static = config.fifo_size }),
fifo_file: std.fs.File,
keyboard_offset: usize,
keyboard_selected_index: usize,
keyhandler: Keyhandler,
uinputkb: Uinputkb,
mode: types.Mode = .Normal,
touch_pending: ?struct { x: c_int = 0, y: c_int = 0 } = .{ .x = 0, .y = 0 },
redraw: bool,
status: []const u8,
status_last_update: ?time.timeval,
strings_base: [][]const u8,
strings_nl_enabled: bool,
strings_offset: usize,
strings_selected_index: ?usize,
submode_lock: types.SubmodeLock = .ScreenOn,
submode_normal: types.SubmodeNormal = .Keyboard,
pub fn fds_poll_loop(runtime: *@This()) !void {
var ev_key_files = try utildevfs.get_devfs_files(runtime.allocator, "/dev/input", linput.EV_KEY);
var ev_touch_files = try utildevfs.get_devfs_files(runtime.allocator, "/dev/input", linput.EV_ABS);
var poll_fds: std.ArrayList(std.os.pollfd) = std.ArrayList(std.os.pollfd).init(runtime.allocator);
var poll_files: std.ArrayList(std.fs.File) = std.ArrayList(std.fs.File).init(runtime.allocator);
try poll_fds.append(.{ .fd = runtime.fifo_file.handle, .events = std.os.POLL.IN, .revents = undefined });
try poll_files.append(runtime.fifo_file);
for (ev_key_files) |file| {
try poll_fds.append(.{ .fd = file.handle, .events = std.os.POLL.IN, .revents = undefined });
try poll_files.append(file);
}
for (ev_touch_files) |file| {
try poll_fds.append(.{ .fd = file.handle, .events = std.os.POLL.IN, .revents = undefined });
try poll_files.append(file);
}
std.debug.print("Key files: {any}\n", .{ev_key_files});
std.debug.print("Touch files: {any}\n", .{ev_touch_files});
try utileviocgrab.exclusive_grab_files(ev_key_files);
while (true) {
poll_fds.items[0].events = std.os.POLL.IN;
poll_fds.items[0].revents = 0;
const n_events = try std.os.poll(poll_fds.items, config.timeout_poll_ms);
_ = n_events;
try runtime.refresh_status();
try runtime.event_key_handle(null);
if (runtime.redraw) {
try runtime.redraw_ui();
runtime.redraw = false;
}
for (poll_fds.items) |fd, fd_index| {
if (fd.revents & std.os.POLL.IN != 0) {
if (fd_index == 0) {
try runtime.load_fifo_data();
} else {
const evt_data = try poll_files.items[fd_index].reader().readStruct(linput.input_event);
if (evt_data.type == linput.EV_ABS and runtime.mode != .Lock) {
try runtime.event_touch_handle(evt_data);
} else if (evt_data.type == linput.EV_KEY) {
try runtime.event_key_handle(evt_data);
}
}
}
}
}
}
fn load_fifo_data(runtime: *@This()) !void {
var scratch_buffer: [50000]u8 = undefined;
var read_bytes = try runtime.fifo_file.read(scratch_buffer[0..]);
try runtime.fifo_data.write(scratch_buffer[0..read_bytes]);
load_strings: while (true) {
for (runtime.fifo_data.readableSlice(0)) |char, index| {
// https://en.wikipedia.org/wiki/C0_and_C1_control_codes
if (char == 0x07 or
char == 0x08 or
char == 0x09 or
char == 0x0b or
char == 0x0c or
char == 0x0d or
char == 0x1b)
{
switch (char) {
0x07 => runtime.submode_normal = .Keyboard, // \a (bell)
0x08 => runtime.submode_normal = .Strings, // \b (backspace)
0x09 => try runtime.restore_base_strings(), // \t (tab)
0x0b => framebuffer.sigwinch(undefined), // \v (tabfeed)
0x0c => try runtime.clear_strings(), // \f (formfeed)
0x0d => runtime.strings_nl_enabled = true, // \r (carriage return)
0x1b => runtime.strings_nl_enabled = false, // \e (esc)
else => unreachable,
}
runtime.fifo_data.discard(1);
continue :load_strings;
} else if (char == '\n') {
const str = std.mem.trim(u8, runtime.fifo_data.readableSlice(0)[0..index], " \n");
if (str.len > 0) {
std.debug.print("String append: <{s}>\n", .{str});
try runtime.append_string(str);
}
runtime.fifo_data.discard(index + 1);
continue :load_strings;
}
}
break :load_strings;
}
runtime.redraw = true;
}
fn event_touch_handle(runtime: *@This(), evt_data: linput.input_event) !void {
const h = @floatToInt(u32, @intToFloat(f64, framebuffer.fb_vinfo.yres) * config.height_proportion) - config.status_height;
const w = framebuffer.fb_vinfo.xres;
var run : enum { FingerUp, FingerDown, Invalid } = run: {
if (evt_data.code == linput.ABS_MT_POSITION_X or evt_data.code == linput.ABS_MT_POSITION_Y) {
if (runtime.touch_pending == null) runtime.touch_pending = .{ .x = 0, .y = 0 };
if (evt_data.code == linput.ABS_MT_POSITION_X) runtime.touch_pending.?.x = evt_data.value;
if (evt_data.code == linput.ABS_MT_POSITION_Y) runtime.touch_pending.?.y = evt_data.value;
break :run .FingerDown;
} else if (evt_data.code == linput.ABS_MT_TRACKING_ID and evt_data.value == -1 and runtime.touch_pending != null) {
break :run .FingerUp;
}
break :run .Invalid;
};
if (run == .FingerDown or run == .FingerUp) {
// Invalidate out of bound events
if (runtime.touch_pending.?.y < framebuffer.fb_vinfo.yres - h or runtime.touch_pending.?.y > framebuffer.fb_vinfo.yres) return;
// Select item on grid
try utilgrid.grid_calculate(runtime.grid(), runtime.dims_cols(), runtime.dims_rows(), 0, config.status_height, w, h, grid_calculate_callback_register_click, runtime);
// Select item, deslect in grid and invalid pending touch
if (run == .FingerUp) {
runtime.grid_select();
if (runtime.grid_current_index()) |current_index| runtime.grid()[current_index].touched = false;
runtime.touch_pending = null;
runtime.redraw = true;
}
}
}
fn event_key_handle(runtime: *@This(), evt_data: ?linput.input_event) !void {
if (try runtime.keyhandler.register_and_determine_event_dispatch(
evt_data,
if (runtime.mode == .Normal) .{ .keys = &[_]types.Key{ .Up, .Down }, .n_taps = 1 } else .{},
)) |k| {
const kb = k;
//std.debug.print("Got keyhandler kb {}\n", .{kb});
for (config.keybinding_actions) |config_entry| {
const match_key = @bitCast(u7, config_entry.keybinding) == @bitCast(u7, kb);
const match_mode = config_entry.mode == runtime.mode;
const match_submode = match_submode: {
if (config_entry.submode) |config_entry_submode| {
break :match_submode switch (config_entry_submode) {
.Lock => config_entry.mode == .Lock and config_entry_submode.Lock == runtime.submode_lock,
.Normal => config_entry.mode == .Normal and config_entry_submode.Normal == runtime.submode_normal,
};
} else {
break :match_submode true;
}
};
if (match_key and match_mode and match_submode) {
config_entry.handler(runtime, config_entry.arg);
runtime.redraw = true;
return;
}
}
}
}
fn grid_calculate_callback_register_click(x: u32, y: u32, w: u32, h: u32, _: types.GridItem, item_i: usize, runtime: anytype) !bool {
const y_off = @floatToInt(u32, @intToFloat(f64, framebuffer.fb_vinfo.yres) * (1.0 - config.height_proportion));
if (runtime.grid_current_index() != null and
runtime.touch_pending != null and
runtime.touch_pending.?.x > x and
runtime.touch_pending.?.x < x + w and
runtime.touch_pending.?.y > y_off + y and
runtime.touch_pending.?.y < y_off + y + h)
{
if (item_i != runtime.grid_current_index().? or !runtime.grid()[runtime.grid_current_index().?].touched) {
runtime.redraw = true;
}
runtime.grid_activate_index(item_i, true);
return false;
}
return true;
}
pub fn grid_select(runtime: *@This()) void {
if (runtime.grid_current_index()) |current_index| {
runtime.keyboard_key_hit(&runtime.grid()[current_index]) catch |x| {
std.debug.print("failed to write: {}\n", .{x});
};
}
}
pub fn keyboard_key_hit(runtime: *@This(), item: *types.GridItem) !void {
if (item.set_offset) |offset| {
try runtime.clear_grid_state(.All);
if (runtime.submode_normal == .Strings) {
runtime.strings_offset = offset;
runtime.strings_selected_index = null;
} else {
runtime.keyboard_offset = offset;
runtime.strings_selected_index = null;
}
runtime.grid_activate_index(0, false);
} else if (item.set_normalsubmode) |new_submode| {
runtime.submode_normal = new_submode;
try runtime.clear_grid_state(.All);
runtime.grid_activate_index(0, false);
runtime.redraw = true;
} else if (item.sticky) {
if (item.stickied) {
try runtime.uinputkb.uinput_type_keycode(item.v.?, .up);
} else {
try runtime.uinputkb.uinput_type_keycode(item.v.?, .down);
}
item.stickied = !item.stickied;
} else {
if (item.v) |value| {
try runtime.uinputkb.uinput_type_keycode(value, .downup);
} else if (item.t.len == 2 and item.t[0] == '\\') {
if (item.t[1] == 'n') try runtime.uinputkb.uinput_type_keycode((try utilkeycode.ascii_to_keycode('\n')).value, .downup);
if (item.t[1] == 't') try runtime.uinputkb.uinput_type_keycode((try utilkeycode.ascii_to_keycode('\t')).value, .downup);
if (item.t[1] == 'b') try runtime.uinputkb.uinput_type_keycode((try utilkeycode.ascii_to_keycode(0x08)).value, .downup); // \b
} else for (item.t) |c| {
const keycode = try utilkeycode.ascii_to_keycode(c);
if (keycode.shift) try runtime.uinputkb.uinput_type_keycode(linput.KEY_LEFTSHIFT, .down);
try runtime.uinputkb.uinput_type_keycode(keycode.value, .downup);
if (keycode.shift) try runtime.uinputkb.uinput_type_keycode(linput.KEY_LEFTSHIFT, .up);
}
if (runtime.submode_normal == .Strings and runtime.strings_nl_enabled) {
try runtime.uinputkb.uinput_type_keycode((try utilkeycode.ascii_to_keycode('\n')).value, .downup);
}
try runtime.clear_grid_state(.Stickied);
}
}
fn refresh_status(runtime: *@This()) !void {
if (runtime.status_last_update != null and
@divTrunc(utiltime.timeval_delta_ms(utiltime.now(), runtime.status_last_update.?), 1000) < 60)
{
return;
}
runtime.status_last_update = utiltime.now();
if (utilfiles.first_existing_path(config.files_status[0..], true, false)) |status_file_path| {
const status_file = try std.fs.openFileAbsolute(status_file_path, .{ .read = true });
defer status_file.close();
runtime.allocator.free(runtime.status);
runtime.status = try status_file.readToEndAlloc(runtime.allocator, 999);
} else {
runtime.status = "Statusfile unconfigured for device!";
}
}
fn dims_cols(runtime: *@This()) u32 {
return if (runtime.submode_normal == .Strings) config.strings_dims_cols else config.keyboard_dims_cols;
}
fn dims_rows(runtime: *@This()) u32 {
return if (runtime.submode_normal == .Strings) config.strings_dims_rows else config.keyboard_dims_rows;
}
fn redraw_ui(runtime: *@This()) !void {
comptime {
if (config.strings_paginate > config.strings_dims_cols * config.strings_dims_rows) {
@panic("Pagination for datalist_strings invalid");
}
if (config.keyboard_paginate > config.keyboard_dims_cols * config.keyboard_dims_rows) {
@panic("Pagination for data_keyboard invalid");
}
}
if (runtime.mode == .Lock) {
const s = try std.fmt.allocPrint(runtime.allocator, "/dev/tty{d}: L | {s}", .{ utiltty.current_vt(), runtime.status });
defer runtime.allocator.free(s);
try framebuffer.draw_grid(
&[_]types.GridItem{.{ .t = "Locked" }},
1,
1,
0xffcfd4,
s,
);
} else if (runtime.submode_normal == .Strings) {
const nl = if (runtime.strings_nl_enabled) "NE" else "ND";
const s = try std.fmt.allocPrint(runtime.allocator, "/dev/tty{d}: S:{s} | {s}", .{ utiltty.current_vt(), nl, runtime.status });
defer runtime.allocator.free(s);
try framebuffer.draw_grid(
runtime.grid(),
runtime.dims_cols(),
runtime.dims_rows(),
0xc2d7ff,
s,
);
} else if (runtime.submode_normal == .Keyboard) {
const layer = if (runtime.keyboard_offset == 0) "L1" else "L2";
const s = try std.fmt.allocPrint(runtime.allocator, "/dev/tty{d}: K:{s} | {s}", .{ utiltty.current_vt(), layer, runtime.status });
defer runtime.allocator.free(s);
try framebuffer.draw_grid(
runtime.grid(),
runtime.dims_cols(),
runtime.dims_rows(),
0xc7ffdf,
s,
);
}
}
pub fn grid_current_index(runtime: *@This()) ?usize {
if (runtime.submode_normal == .Strings) {
return runtime.strings_selected_index;
} else {
return runtime.keyboard_selected_index;
}
}
pub fn grid_activate_index(runtime: *@This(), index: usize, touched: bool) void {
if (runtime.grid_current_index()) |current_index| {
runtime.grid()[current_index].touched = false;
runtime.grid()[current_index].selected = false;
}
if (runtime.submode_normal == .Strings) {
runtime.strings_selected_index = index;
} else {
runtime.keyboard_selected_index = index;
}
runtime.grid()[index].selected = true;
if (touched) {
runtime.grid()[index].touched = true;
}
}
pub fn grid(runtime: *@This()) []types.GridItem {
if (runtime.submode_normal == .Keyboard) {
const start = runtime.keyboard_offset;
const end = std.math.min(runtime.data_keyboard.len, runtime.keyboard_offset + config.keyboard_paginate);
return runtime.data_keyboard[start..end];
} else {
const start = runtime.strings_offset;
const end = std.math.min(runtime.datalist_strings.items.len, runtime.strings_offset + config.strings_paginate);
return runtime.datalist_strings.items[start..end];
}
}
pub fn clear_grid_state(runtime: *@This(), mods: enum { All, Stickied }) !void {
for (runtime.grid()) |*g| {
if ((mods == .All or mods == .Stickied) and g.stickied) {
try runtime.uinputkb.uinput_type_keycode(g.v.?, .up);
g.stickied = false;
}
if (mods == .All) {
g.touched = false;
g.selected = false;
}
}
}
fn append_string(runtime: *@This(), string: []const u8) !void {
const curr_len = runtime.datalist_strings.items.len;
if (curr_len < config.grid_batch_size and (curr_len + 2) % config.grid_batch_size == 0) {
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, "Kb"), .set_normalsubmode = .Keyboard });
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, "->"), .set_offset = curr_len + 2 });
} else if (curr_len > config.grid_batch_size and (curr_len + 3) % config.grid_batch_size == 0) {
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, "<-"), .set_offset = (curr_len + 3) - (config.grid_batch_size * 2) });
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, "Kb"), .set_normalsubmode = .Keyboard });
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, "->"), .set_offset = curr_len + 3 });
}
try runtime.datalist_strings.append(.{ .t = try runtime.allocator.dupe(u8, string) });
if (runtime.strings_selected_index == null) {
runtime.grid_activate_index(0, false);
}
}
pub fn sync_led_color(runtime: *@This()) void {
if (runtime.mode == .Lock and runtime.submode_lock == .ScreenOn) {
utilled.set_led(.Blue, true) catch |e| std.debug.print("{}", .{e});
utilled.set_led(.Red, false) catch |e| std.debug.print("{}", .{e});
} else if (runtime.mode == .Lock and runtime.submode_lock == .ScreenOff) {
utilled.set_led(.Blue, true) catch |e| std.debug.print("{}", .{e});
utilled.set_led(.Red, true) catch |e| std.debug.print("{}", .{e});
} else {
utilled.set_led(.Blue, false) catch |e| std.debug.print("{}", .{e});
utilled.set_led(.Red, false) catch |e| std.debug.print("{}", .{e});
}
}
fn clear_strings(runtime: *@This()) !void {
// Free raw
for (runtime.datalist_strings.items) |item| {
runtime.allocator.free(item.t);
}
runtime.datalist_strings.clearAndFree();
runtime.strings_selected_index = null;
runtime.strings_offset = 0;
}
pub fn restore_base_strings(runtime: *@This()) !void {
try runtime.clear_strings();
for (runtime.strings_base) |s| {
try runtime.append_string(s);
}
runtime.strings_nl_enabled = true;
}
pub fn sigterm(_: c_int) callconv(.C) void {
utiltty.resize_tty(1) catch unreachable;
std.fs.deleteFileAbsolute(config.fifo_path) catch {};
std.os.exit(0);
}
pub fn init(allocator: std.mem.Allocator) !@This() {
var runtime = @This(){
.allocator = allocator,
.redraw = true,
.status = try allocator.alloc(u8, 0),
.status_last_update = null,
.keyhandler = Keyhandler.init(),
.uinputkb = try Uinputkb.init(),
.fifo_file = undefined,
.fifo_data = undefined,
.data_keyboard = try allocator.alloc(types.GridItem, config.keyboard.len),
.keyboard_selected_index = 0,
.keyboard_offset = 0,
.strings_base = try utilbin.path_strings_sorted(allocator),
.datalist_strings = std.ArrayList(types.GridItem).init(allocator),
.strings_selected_index = 0,
.strings_offset = 0,
.strings_nl_enabled = true,
};
// Setup Fifo
{
runtime.fifo_data = std.fifo.LinearFifo(u8, std.fifo.LinearFifoBufferType{ .Static = config.fifo_size }).init();
std.fs.deleteFileAbsolute(config.fifo_path) catch {};
if (0 != stat.mkfifo(config.fifo_path, 0)) {
return error.FailedToCreateFifo;
} else if (0 != stat.chmod(config.fifo_path, 0o777)) {
return error.FailedToChmodFifo;
} else {
runtime.fifo_file = try std.fs.openFileAbsolute(config.fifo_path, .{ .read = true, .write = true });
}
}
// Setup Framebuffer
try framebuffer.init(allocator);
// Setup signals
if (0 != std.os.linux.sigaction(std.os.SIG.WINCH, &.{
.handler = .{ .handler = framebuffer.sigwinch },
.mask = [_]u32{0} ** 32,
.flags = @as(c_uint, 0),
}, null)) {
return error.FailedToSetupSigwinchSignal;
}
if (0 != std.os.linux.sigaction(std.os.SIG.TERM, &.{
.handler = .{ .handler = sigterm },
.mask = [_]u32{0} ** 32,
.flags = @as(c_uint, 0),
}, null)) {
return error.FailedToSetupSigtermSignal;
}
// Reset LEDS
{
try utilled.set_led(.Blue, false);
try utilled.set_led(.Red, false);
try utilled.set_led(.Green, false);
}
// Setup Strings
{
runtime.submode_normal = .Strings;
try runtime.restore_base_strings();
}
runtime.grid_activate_index(0, false);
try runtime.refresh_status();
// Setup data_keyboard
runtime.submode_normal = .Keyboard;
runtime.data_keyboard = try allocator.alloc(types.GridItem, config.keyboard.len);
std.mem.copy(types.GridItem, runtime.data_keyboard[0..], config.keyboard[0..]);
runtime.grid_activate_index(0, false);
try runtime.redraw_ui();
try utiltty.screen_blank(.Unblank);
return runtime;
}