~leon_plickat/public-inbox

nfm: Implement command/search/select history v2 APPLIED

Hugo Machet: 1
 Implement command/search/select history

 4 files changed, 160 insertions(+), 20 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~leon_plickat/public-inbox/patches/30758/mbox | git am -3
Learn more about email & git

[PATCH nfm v2] Implement command/search/select history Export this patch

References: https://todo.sr.ht/~leon_plickat/nfm/9
---
 src/History.zig       | 55 ++++++++++++++++++++++++++++
 src/UserInterface.zig | 38 ++++++++++++++-----
 src/mode.zig          |  2 +
 src/nfm.zig           | 85 ++++++++++++++++++++++++++++++++++++++-----
 4 files changed, 160 insertions(+), 20 deletions(-)
 create mode 100644 src/History.zig

diff --git a/src/History.zig b/src/History.zig
new file mode 100644
index 000000000000..3e5f10a0a531
--- /dev/null
+++ b/src/History.zig
@@ -0,0 +1,55 @@
// This file is part of nfm, the neat file manager.
//
// Copyright © 2022 Hugo Machet
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 3 as published
// by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const assert = std.debug.assert;
const heap = std.heap;
const mem = std.mem;

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

const Self = @This();

arena: heap.ArenaAllocator,

command: std.ArrayListUnmanaged([:0]u8) = .{},
search: std.ArrayListUnmanaged([:0]u8) = .{},

pub fn init(self: *Self) void {
    self.arena = heap.ArenaAllocator.init(context.gpa);
    errdefer self.arena.deinit();
}

pub fn deinit(self: *Self) void {
    self.command.deinit(self.arena.allocator());
    self.search.deinit(self.arena.allocator());
    self.arena.deinit();
}

pub fn append(self: *Self, item: []const u8) !void {
    assert(context.mode == .user_input);
    if (item.len == 0) return;

    const list = switch (context.mode.user_input.operation) {
        .command => &self.command,
        .search, .select => &self.search,
    };

    const alloc = self.arena.allocator();

    if (list.items.len > 1 and mem.eql(u8, list.items[list.items.len - 1], item)) return;
    try list.append(alloc, try alloc.dupeZ(u8, item));
}
diff --git a/src/UserInterface.zig b/src/UserInterface.zig
index b331cd500aa4..6fd7a25a040f 100644
--- a/src/UserInterface.zig
+++ b/src/UserInterface.zig
@@ -310,8 +310,14 @@ fn drawTitle(self: *Self, title: []const u8) !void {
    } else {
        try self.term.hideCursor();
    }

    switch (context.mode) {
        .user_input => {
            const history = switch (context.mode.user_input.operation) {
                .command => &context.history.command,
                .search, .select => &context.history.search,
            };

            const prefix = @tagName(context.mode.user_input.operation);
            const prefix_len = prefix.len + 4;
            debug.assert(prefix_len < min_width);
@@ -321,17 +327,29 @@ fn drawTitle(self: *Self, title: []const u8) !void {
            try self.term.writeAll(prefix);
            try self.term.writeByte(':');
            try self.term.writeByte(' ');
            try self.term.setAttribute(.{ .bg = .black, .fg = .white });
            try self.term.writeByte(' ');
            try self.term.moveCursorTo(0, prefix_len);
            // TODO input line scrolling
            const rest = try self.term.writeLine(
                self.term.width - prefix_len,
                context.mode.user_input.buffer.buffer.items,
            );
            if (rest > 0) try self.term.writeByteNTimes(' ', rest);

            try self.term.moveCursorTo(0, prefix_len + context.mode.user_input.buffer.cursor);
            if (context.mode.user_input.history_index) |index| {
                try self.term.hideCursor();
                try self.term.setAttribute(.{ .bg = .black, .fg = .magenta, .italic = true });
                try self.term.moveCursorTo(0, prefix_len);
                const rest = try self.term.writeLine(
                    self.term.width - prefix_len,
                    history.items[history.items.len - index],
                );
                if (rest > 0) try self.term.writeByteNTimes(' ', rest);
            } else {
                try self.term.setAttribute(.{ .bg = .black, .fg = .white });
                try self.term.writeByte(' ');
                try self.term.moveCursorTo(0, prefix_len);
                // TODO input line scrolling
                const rest = try self.term.writeLine(
                    self.term.width - prefix_len,
                    context.mode.user_input.buffer.buffer.items,
                );
                if (rest > 0) try self.term.writeByteNTimes(' ', rest);

                try self.term.moveCursorTo(0, prefix_len + context.mode.user_input.buffer.cursor);
            }
        },
        .message => {
            try self.term.setAttribute(context.mode.message.level.getAttribute());
diff --git a/src/mode.zig b/src/mode.zig
index b0b0fe965b52..426c99897f7c 100644
--- a/src/mode.zig
+++ b/src/mode.zig
@@ -63,6 +63,7 @@ pub const Mode = union(enum) {
        const UserInput = @This();
        operation: UserInputOperation,
        buffer: InputBuffer,
        history_index: ?usize,
    },

    pub fn setNav(self: *Self) void {
@@ -84,6 +85,7 @@ pub const Mode = union(enum) {
        self.* = .{ .user_input = .{
            .operation = operation,
            .buffer = try InputBuffer.init(context.gpa),
            .history_index = null,
        } };
    }

diff --git a/src/nfm.zig b/src/nfm.zig
index b83d5dcee5c6..711cec626033 100644
--- a/src/nfm.zig
+++ b/src/nfm.zig
@@ -33,6 +33,7 @@ const log = std.log;
const CommandTokenizer = @import("CommandTokenizer.zig");
const Config = @import("Config.zig");
const DirMap = @import("DirMap.zig");
const History = @import("History.zig");
const Mode = @import("mode.zig").Mode;
const UserInterface = @import("UserInterface.zig");
const View = @import("view.zig").View;
@@ -57,6 +58,7 @@ pub const Context = struct {
    cwd: *DirMap.Dir = undefined, // TODO should this be an attribute of DirMap?
    initial_cwd_path: []const u8 = undefined,
    search: ?Regex = null,
    history: History = undefined,
    show_hidden: bool = false,
    child: ?os.pid_t = null,
    killed: bool = false,
@@ -377,6 +379,9 @@ fn nfm() !void {
    defer context.ui.deinit();
    try context.ui.start();

    context.history.init();
    defer context.history.deinit();

    defer {
        if (context.search) |*search| {
            search.deinit();
@@ -444,14 +449,26 @@ fn handleEventNav(ev: spoon.Event) !void {

fn handleEventUserInput(ev: spoon.Event) !void {
    const buffer = &context.mode.user_input.buffer;

    const history = switch (context.mode.user_input.operation) {
        .command => &context.history.command,
        .search, .select => &context.history.search,
    };

    switch (ev) {
        .ascii => |key| switch (key) {
            '\n', '\r' => {
                try handleReturnUserInput();
                return;
            },
            127 => buffer.delete(.left, 1), // Backspace
            else => try buffer.insertChar(key),
            127 => {
                if (context.mode.user_input.history_index != null) return;
                buffer.delete(.left, 1); // Backspace
            },
            else => {
                if (context.mode.user_input.history_index != null) return;
                try buffer.insertChar(key);
            },
        },
        .ctrl => |key| switch (key) {
            'c', 'g' => context.mode.setNav(),
@@ -497,6 +514,27 @@ fn handleEventUserInput(ev: spoon.Event) !void {
        .escape => context.mode.setNav(),
        .arrow_left => buffer.moveCursor(.left, 1),
        .arrow_right => buffer.moveCursor(.right, 1),
        .arrow_up => {
            if (history.items.len == 0) return;
            if (context.mode.user_input.history_index == null) {
                context.mode.user_input.history_index = 0;
            }

            if (context.mode.user_input.history_index.? + 1 <= history.items.len) {
                context.mode.user_input.history_index.? += 1;
            }
        },
        .arrow_down => {
            if (history.items.len == 0 or context.mode.user_input.history_index == null) {
                return;
            }

            if (context.mode.user_input.history_index.? - 1 > 0) {
                context.mode.user_input.history_index.? -= 1;
            } else if (context.mode.user_input.history_index.? - 1 == 0) {
                context.mode.user_input.history_index = null;
            }
        },
        .delete => buffer.delete(.right, 1),
        else => return,
    }
@@ -509,35 +547,62 @@ fn handleReturnUserInput() !void {
    const user_input = &context.mode.user_input;
    switch (user_input.operation) {
        .command => {
            if (user_input.buffer.buffer.items.len == 0) {
                context.mode.setNav();
                return;
            if (context.mode.user_input.history_index) |index| {
                const cmd = context.history.command.items[context.history.command.items.len - index];
                context.mode.user_input.history_index = null;
                try context.run(cmd, "/bin/sh or subprocess");
            } else {
                if (user_input.buffer.buffer.items.len == 0) {
                    context.mode.setNav();
                    return;
                }

                const cmd = try user_input.buffer.buffer.toOwnedSliceSentinel(0);
                try context.history.append(cmd);
                defer context.gpa.free(cmd);
                try context.run(cmd, "/bin/sh or subprocess");
            }
            const cmd = try user_input.buffer.buffer.toOwnedSliceSentinel(0);
            defer context.gpa.free(cmd);
            try context.run(cmd, "/bin/sh or subprocess");
        },
        .search => {
            if (context.search) |*search| {
                search.deinit();
                context.search = null;
            }
            context.search = Regex.compile(context.gpa, user_input.buffer.buffer.items) catch {

            const search = blk: {
                if (context.mode.user_input.history_index) |index| {
                    break :blk context.history.search.items[context.history.search.items.len - index];
                }
                break :blk user_input.buffer.buffer.items;
            };
            context.mode.user_input.history_index = null;

            context.search = Regex.compile(context.gpa, search) catch {
                // TODO find out the exact errors that indicate a bad regex
                context.mode.setMessage(.err, "Invalid regex");
                return;
            };
            try context.history.append(user_input.buffer.buffer.items);

            // This function will never need this parameter, so just input
            // garbage to make the compiler happy.
            try Config.impl.@"cursor-move-to-next-match"(&Config.KeyOperation{});
        },
        .select => {
            var select = Regex.compile(context.gpa, user_input.buffer.buffer.items) catch {
            const search = blk: {
                if (context.mode.user_input.history_index) |index| {
                    break :blk context.history.search.items[context.history.search.items.len - index];
                }
                break :blk user_input.buffer.buffer.items;
            };
            context.mode.user_input.history_index = null;

            var select = Regex.compile(context.gpa, search) catch {
                // TODO find out the exact errors that indicate a bad regex
                context.mode.setMessage(.err, "Invalid regex");
                return;
            };
            try context.history.append(user_input.buffer.buffer.items);
            defer select.deinit();
            var mark = false;
            for (context.cwd.files.items) |*file| {
-- 
2.35.1
Just some minor things I quickly cleaned up myself.
Thanks!


Friendly greetings,
Leon Henrik Plickat