~leon_plickat/nfm

5a21ea527d6048dd733f3a8c7d89d106e0ee11d0 — Leon Henrik Plickat 4 months ago fe64471
Improve UI Event interface
2 files changed, 169 insertions(+), 96 deletions(-)

M src/UserInterface.zig
M src/nfm.zig
M src/UserInterface.zig => src/UserInterface.zig +123 -52
@@ 35,36 35,32 @@ const Self = @This();

const context = &@import("nfm.zig").context;

pub const EscapeKey = union(enum) {
    unknown,
    escape,
    arrow_up,
    arrow_down,
    arrow_left,
    arrow_right,
    end,
    home,
    page_up,
    page_down,
    delete,
    insert,
    f1,
    f2,
    f3,
    f4,
    f5,
    f6,
    f7,
    f8,
    f9,
    f10,
    f11,
    f12,
pub const Event = union(enum) {
    sigwinch: void,
    unknown: void,
    escape: void,
    arrow_up: void,
    arrow_down: void,
    arrow_left: void,
    arrow_right: void,
    end: void,
    home: void,
    page_up: void,
    page_down: void,
    delete: void,
    insert: void,
    function: u8,
    ctrl: u8,
    alt: u8,
    ascii: u8,
};

/// The various escape sequences that represent special keys.
/// TODO[zig] This currently exceeds the operation limit for comptime, so unused
///           values are commented out. Hopefully in the future comptime can
///           handle this completely.
const escape_key_codes = std.ComptimeStringMap(
    EscapeKey,
    Event,
    .{
        // Legacy
        .{ "[A", .arrow_up },


@@ 87,31 83,102 @@ const escape_key_codes = std.ComptimeStringMap(
        .{ "[1~", .home },
        .{ "[7~", .home },
        .{ "[H~", .home },
        .{ "OP", .f1 },
        .{ "OQ", .f2 },
        .{ "OR", .f3 },
        .{ "OS", .f4 },
        .{ "[15~", .f5 },
        .{ "[17~", .f6 },
        .{ "[18~", .f7 },
        .{ "[19~", .f8 },
        .{ "[20~", .f9 },
        .{ "[21~", .f10 },
        .{ "[23~", .f11 },
        .{ "[24~", .f12 },
        //.{ "OP", .{ .function = 1 } },
        //.{ "OQ", .{ .function = 2 } },
        //.{ "OR", .{ .function = 3 } },
        //.{ "OS", .{ .function = 4 } },
        //.{ "[15~", .{ .function = 5 } },
        //.{ "[17~", .{ .function = 6 } },
        //.{ "[18~", .{ .function = 7 } },
        //.{ "[19~", .{ .function = 8 } },
        //.{ "[20~", .{ .function = 9 } },
        //.{ "[21~", .{ .function = 10 } },
        //.{ "[23~", .{ .function = 11 } },
        //.{ "[24~", .{ .function = 12 } },
        //.{ "a", .{ .alt = 'a' } },
        //.{ "b", .{ .alt = 'b' } },
        //.{ "c", .{ .alt = 'c' } },
        //.{ "d", .{ .alt = 'd' } },
        //.{ "e", .{ .alt = 'e' } },
        //.{ "f", .{ .alt = 'f' } },
        //.{ "g", .{ .alt = 'g' } },
        //.{ "h", .{ .alt = 'h' } },
        //.{ "i", .{ .alt = 'i' } },
        //.{ "j", .{ .alt = 'j' } },
        //.{ "k", .{ .alt = 'k' } },
        //.{ "l", .{ .alt = 'l' } },
        //.{ "m", .{ .alt = 'm' } },
        //.{ "n", .{ .alt = 'n' } },
        //.{ "o", .{ .alt = 'o' } },
        //.{ "p", .{ .alt = 'p' } },
        //.{ "q", .{ .alt = 'q' } },
        //.{ "r", .{ .alt = 'r' } },
        //.{ "s", .{ .alt = 's' } },
        //.{ "t", .{ .alt = 't' } },
        //.{ "u", .{ .alt = 'u' } },
        //.{ "v", .{ .alt = 'v' } },
        //.{ "w", .{ .alt = 'w' } },
        //.{ "x", .{ .alt = 'x' } },
        //.{ "y", .{ .alt = 'y' } },
        //.{ "z", .{ .alt = 'z' } },

        // Kitty
        .{ "[27u", .escape },
        //.{ "[97;5u", .{ .ctrl = 'a' } },
        //.{ "[98;5u", .{ .ctrl = 'b' } },
        .{ "[99;5u", .{ .ctrl = 'c' } },
        //.{ "[100;5u", .{ .ctrl = 'd' } },
        //.{ "[101;5u", .{ .ctrl = 'e' } },
        //.{ "[102;5u", .{ .ctrl = 'f' } },
        //.{ "[103;5u", .{ .ctrl = 'g' } },
        //.{ "[104;5u", .{ .ctrl = 'h' } },
        //.{ "[105;5u", .{ .ctrl = 'i' } },
        //.{ "[106;5u", .{ .ctrl = 'j' } },
        //.{ "[107;5u", .{ .ctrl = 'k' } },
        //.{ "[108;5u", .{ .ctrl = 'l' } },
        //.{ "[109;5u", .{ .ctrl = 'm' } },
        //.{ "[110;5u", .{ .ctrl = 'n' } },
        //.{ "[111;5u", .{ .ctrl = 'o' } },
        //.{ "[112;5u", .{ .ctrl = 'p' } },
        //.{ "[113;5u", .{ .ctrl = 'q' } },
        //.{ "[114;5u", .{ .ctrl = 'r' } },
        //.{ "[115;5u", .{ .ctrl = 's' } },
        //.{ "[116;5u", .{ .ctrl = 't' } },
        //.{ "[117;5u", .{ .ctrl = 'u' } },
        //.{ "[118;5u", .{ .ctrl = 'v' } },
        //.{ "[119;5u", .{ .ctrl = 'w' } },
        //.{ "[120;5u", .{ .ctrl = 'x' } },
        //.{ "[121;5u", .{ .ctrl = 'y' } },
        //.{ "[122;5u", .{ .ctrl = 'z' } },
        //.{ "[97;3u", .{ .alt = 'a' } },
        //.{ "[98;3u", .{ .alt = 'b' } },
        //.{ "[99;3u", .{ .alt = 'c' } },
        //.{ "[100;3u", .{ .alt = 'd' } },
        //.{ "[101;3u", .{ .alt = 'e' } },
        //.{ "[102;3u", .{ .alt = 'f' } },
        //.{ "[103;3u", .{ .alt = 'g' } },
        //.{ "[104;3u", .{ .alt = 'h' } },
        //.{ "[105;3u", .{ .alt = 'i' } },
        //.{ "[106;3u", .{ .alt = 'j' } },
        //.{ "[107;3u", .{ .alt = 'k' } },
        //.{ "[108;3u", .{ .alt = 'l' } },
        //.{ "[109;3u", .{ .alt = 'm' } },
        //.{ "[110;3u", .{ .alt = 'n' } },
        //.{ "[111;3u", .{ .alt = 'o' } },
        //.{ "[112;3u", .{ .alt = 'p' } },
        //.{ "[113;3u", .{ .alt = 'q' } },
        //.{ "[114;3u", .{ .alt = 'r' } },
        //.{ "[115;3u", .{ .alt = 's' } },
        //.{ "[116;3u", .{ .alt = 't' } },
        //.{ "[117;3u", .{ .alt = 'u' } },
        //.{ "[118;3u", .{ .alt = 'v' } },
        //.{ "[119;3u", .{ .alt = 'w' } },
        //.{ "[120;3u", .{ .alt = 'x' } },
        //.{ "[121;3u", .{ .alt = 'y' } },
        //.{ "[122;3u", .{ .alt = 'z' } },
    },
);

pub const Event = union(enum) {
    sigwinch: void,
    ascii_key: u8,
    ctrl_key: u8,
    escape_key: EscapeKey,
};

const Colour = enum {
    none,
    black,


@@ 292,7 359,6 @@ fn handleSigWinch(_: c_int) callconv(.C) void {
    context.ui.render(true, true) catch {};
}

// TODO .ctrl_key
pub fn nextEvent(self: *Self) !?Event {
    if (self.sigwinch) {
        self.sigwinch = false;


@@ 322,7 388,7 @@ pub fn nextEvent(self: *Self) !?Event {
        termios.cc[vmin] = 0;
        try os.tcsetattr(self.tty.handle, .NOW, termios);

        var esc_buffer: [4]u8 = undefined;
        var esc_buffer: [8]u8 = undefined;
        const esc_read = try self.tty.read(&esc_buffer);

        termios.cc[vtime] = 0;


@@ 338,13 404,18 @@ pub fn nextEvent(self: *Self) !?Event {
        //      probably do the same. However this is a low priority goal, as
        //      this problem does not occur when using kitty keyboard mode.

        if (esc_read == 0) return Event{ .escape_key = .escape };
        return Event{ .escape_key = escape_key_codes.get(esc_buffer[0..esc_read]) orelse .unknown };
    } else {
        return Event{ .ascii_key = buffer[0] };
        if (esc_read == 0) return .escape;
        return escape_key_codes.get(esc_buffer[0..esc_read]) orelse .unknown;
    }

    // Legacy codes for Ctrl-[a-z].
    // TODO Is there something in the std to take care of this for us?
    const chars = [_]u8{ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' };
    for (chars) |char| {
        if (buffer[0] == char & '\x1f') return Event{ .ctrl = char };
    }

    unreachable;
    return Event{ .ascii = buffer[0] };
}

pub fn deinit(self: *Self) void {

M src/nfm.zig => src/nfm.zig +46 -44
@@ 190,8 190,7 @@ fn handleEventUserInput(ev: UserInterface.Event) !void {
            try context.ui.resize();
            try context.ui.render(true, true);
        },
        .ctrl_key => {}, // TODO support the typical readline-esque keybinds for user_input mode
        .ascii_key => |key| switch (key) {
        .ascii => |key| switch (key) {
            '\n', '\r' => try handleReturnUserInput(),
            127 => { // Backspace
                user_input.deleteLeft();


@@ 202,29 201,30 @@ fn handleEventUserInput(ev: UserInterface.Event) !void {
                try context.ui.render(false, true);
            },
        },
        .escape_key => |key| switch (key) {
            .escape => {
                context.mode.setNav();
                try context.ui.render(false, true);
            },
            .arrow_left => {
                user_input.moveCursorLeft();
                try context.ui.render(false, true);
            },
            .arrow_right => {
                user_input.moveCursorRight();
                try context.ui.render(false, true);
            },
            .delete => {
                user_input.deleteRight();
                try context.ui.render(false, true);
            },
            else => {},
        .escape => {
            context.mode.setNav();
            try context.ui.render(false, true);
        },
        .arrow_left => {
            user_input.moveCursorLeft();
            try context.ui.render(false, true);
        },
        .arrow_right => {
            user_input.moveCursorRight();
            try context.ui.render(false, true);
        },
        .delete => {
            user_input.deleteRight();
            try context.ui.render(false, true);
        },
        .ctrl => {}, // TODO support the typical readline-esque keybinds for user_input mode
        else => {},
    }
}

fn handleReturnUserInput() !void {
    // Keybinds for the user input prompt are hardcoded, as configuration makes
    // little sense here.
    const user_input = &context.mode.user_input;
    switch (user_input.operation) {
        .command => {


@@ 270,7 270,7 @@ fn handleReturnUserInput() !void {
                try context.ui.render(false, true);
                return;
            };
            try handleKeyNav('n');
            try handleAsciiKeyNav('n');
        },
        .select => {
            var select = Regex.compile(context.gpa, user_input.buffer.items) catch {


@@ 298,34 298,36 @@ fn handleReturnUserInput() !void {
}

fn handleEventNav(ev: UserInterface.Event) !void {
    // TODO make navigation mode keybind configurable
    switch (ev) {
        .sigwinch => {
            try context.ui.resize();
            try context.ui.render(true, true);
        },
        .ascii_key => |ch| try handleKeyNav(ch),
        .ctrl_key => {}, // TODO
        .escape_key => |key| switch (key) {
            // These will never return false, so we can safely
            // discard the return value.
            .arrow_up => try handleKeyNav('k'),
            .arrow_down => try handleKeyNav('j'),
            .arrow_left => try handleKeyNav('h'),
            .arrow_right => try handleKeyNav('l'),
            .end => try handleKeyNav('G'),
            .home => try handleKeyNav('g'),
            .escape => try handleEscapeNav(),
            .page_up => {
                // Scroll 80% of the terminal height.
                context.selection_delta -|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
                try context.ui.render(true, false);
            },
            .page_down => {
                context.selection_delta +|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
                try context.ui.render(true, false);
            },
            else => {}, // TODO maybe find useful features for these keys
        .arrow_up => try handleAsciiKeyNav('k'),
        .arrow_down => try handleAsciiKeyNav('j'),
        .arrow_left => try handleAsciiKeyNav('h'),
        .arrow_right => try handleAsciiKeyNav('l'),
        .end => try handleAsciiKeyNav('G'),
        .home => try handleAsciiKeyNav('g'),
        .escape => try handleEscapeNav(),
        .page_up => {
            // Scroll 80% of the terminal height.
            context.selection_delta -|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
            try context.ui.render(true, false);
        },
        .page_down => {
            context.selection_delta +|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
            try context.ui.render(true, false);
        },
        .ctrl => |ch| {
            if (ch == 'c') {
                context.mode.setMessage(.info, "Press q to exit");
                try context.ui.render(false, true);
            }
        },
        .ascii => |ch| try handleAsciiKeyNav(ch),
        else => {}, // TODO maybe find useful features for these keys
    }
}



@@ 372,7 374,7 @@ fn timespecDiffLessThanOneSecond(a: os.timespec, b: os.timespec) bool {
    return diff <= time.ns_per_s;
}

fn handleKeyNav(key: u8) !void {
fn handleAsciiKeyNav(key: u8) !void {
    const title_dirty = context.mode != .nav;
    context.mode.setNav();
    switch (key) {