~leon_plickat/nfm

a9d58fd75153c14606c65fd28837da75e80a3920 — Leon Henrik Plickat 4 months ago 2949af3
Update terminology: refer to the cursor instead of "selection"

Selection is a vague word that is already used in different contexts.
As such, replacing it with a different word makes things a bit clearer.
5 files changed, 81 insertions(+), 84 deletions(-)

M README.md
M doc/nfm.1
M src/DirMap.zig
M src/UserInterface.zig
M src/nfm.zig
M README.md => README.md +0 -1
@@ 18,7 18,6 @@ to be used as a file selector for other programs.

## TODO:

* Maybe rename "selection" to "cursor" to not conflict with the "select prompt"?
* refresh dirmap when directory content change
  - inotify?
* command/search history

M doc/nfm.1 => doc/nfm.1 +37 -38
@@ 33,17 33,15 @@ operations via the command mode.
Only opening files and changing directories is implemented directly for
convenience.
.P
nfm operates using two concepts, \fBselection\fR and \fBmarks\fR.
In nfm you can use two methods of selecting files, via \fBcursor\fR and via
\fBmarks\fR.
The cursor is the highlighted line you can move with keybinds and allows you to
select a single file in the current directory.
If you want to operate on multiple files at once, perhaps even across
multiple directories, you can use marks.
.P
Selection simply is the file or directory currently selected by the cursor.
As such there can only ever be one selected file.
.P
Marks are for operations on multiple files.
You can mark multiple files in different directories and apply operations to
them collectively.
.P
Selection and marks do not conflict.
You can operate on the selection even if marked files exist.
The cursor and marks do not conflict.
You can operate on the file selected by the cursor even if marked files exist.
.
.
.SH OPTIONS


@@ 79,13 77,13 @@ The following keybinds are available when navigating the filesystem.
.P
\fBj\fR, \fBdown\fR
.RS
Move the selection down.
Move the cursor down.
.RE
.
.P
\fBk\fR, \fBup\fR
.RS
Move the selection up.
Move the cursor up.
.RE
.
.P


@@ 97,32 95,32 @@ Go to the parent directory.
.P
\fBl\fR, \fBright\fR
.RS
If selection is a directory, enter it.
If selection is a file, try to open it.
If the cursor selects a directory, enter it.
If it selects a file, try to open it.
.RE
.
.P
\fBg\fR, \fBK\fR, \fBhome\fR
.RS
Move the selection to the top of the list.
Move the cursor to the top of the list.
.RE
.
.P
\fBG\fR, \fBJ\fR, \fBend\fR
.RS
Move the selection to the bottom of the list.
Move the cursor to the bottom of the list.
.RE
.
.P
\fBpage-down\fR
.RS
Move the selection down by 80% of the terminal height.
Move the cursor down by 80% of the terminal height.
.RE
.
.P
\fBpage-up\fR
.RS
Move the selection up by 80% of the terminal height.
Move the cursor up by 80% of the terminal height.
.RE
.
.P


@@ 135,22 133,22 @@ If there are none, same behaviour as for \fBl\fR.
.P
\fBm\fR
.RS
Toggle the mark of the selection.
Toggle the mark of the file selected by the cursor.
.RE
.
.P
\fB/\fR
.RS
Open the search prompt, where a regular expression can be entered. 
When enter is pressed, the next file in the current directory matching the
search pattern is selected.
When enter is pressed, the cursor will jump next file in the current directory
matching the search pattern.
The search pattern remains active until a new one is entered.
.RE
.
.P
\fBn\fR
.RS
Move selection to the next file matching the search pattern.
Move cursor to the next file matching the search pattern.
.RE
.
.P


@@ 164,13 162,13 @@ pattern are marked.
.P
\fBN\fR
.RS
Move selection to the next marked file.
Move cursor to the next marked file.
.RE
.
.P
\fBi\fR
.RS 
Invert marks for all entries of the current directory.
Invert marks for all files in the current directory.
.RE
.
.P


@@ 208,8 206,8 @@ Open command prompt.
.P
\fBo\fR
.RS
Open command prompt, prefilled with the name of the selected file and the cursor
at the start.
Open command prompt, prefilled with the name of the file selected by the cursor
with the prompt cursor at the start.
This allows to quickly open files with specific programs, like alternative
editors or viewers.
.RE


@@ 218,7 216,7 @@ editors or viewers.
\fBx\fR
.RS
Open command prompt, prefilled with the 'rm' command and the name of the
selected file.
file selected by the cursor.
This allows to quickly delete the selected file.
.RE
.


@@ 226,7 224,7 @@ This allows to quickly delete the selected file.
\fBc\fR
.RS
Open command prompt, prefilled with the 'mv' command and the name of the
selected file.
file selected by the cursor.
This allows to quickly move or rename the selected file.
.RE
.


@@ 239,8 237,8 @@ exceptions.
.P
\fBl\fR
.RS
If selection is a directory, enter it.
If selection is a file, print its full path to stdout and exit.
If the cursor selects a directory, enter it.
If it selects a file, print its full path to stdout and exit.
.RE
.
.P 


@@ 248,7 246,8 @@ If selection is a file, print its full path to stdout and exit.
.RS
If there are marked files or directories, write their full paths to stdout and
exit.
Otherwise write the full path of the selection to stdout and exit.
Otherwise write the full path of the file selected by the cursor to stdout and
exit.
.RE
.
.


@@ 268,19 267,19 @@ Commit operation.
.P
\fBCtrl-a\fR
.RS
Move the cursor to the beginning of the line.
Move the prompt cursor to the beginning of the line.
.RE
.
.P
\fBCtrl-e\fR
.RS
Move the cursor to the end of the line.
Move the prompt cursor to the end of the line.
.RE
.
.P
\fBCtrl-s\fR
.RS
Insert the name of the selected file.
Insert the name of the file selected by the cursor.
.RE
.
.P


@@ 292,25 291,25 @@ Insert the paths of all marked files.
.P
\fBCtrl-w\fR
.RS
Delete all characters from the cursor to the start of the previous word.
Delete all characters from the prompt cursor to the start of the previous word.
.RE
.
.P
\fBCtrl-k\fR
.RS
Delete all characters from the cursor to the end of the line.
Delete all characters from the prompt cursor to the end of the line.
.RE
.
.P
\fBAlt-f\fR
.RS
Move cursor to the start of the previous word.
Move prompt cursor to the start of the previous word.
.RE
.
.P
\fBAlt-f\fR
.RS
Move cursor to the start of the next word.
Move prompt cursor to the start of the next word.
.RE
.
.

M src/DirMap.zig => src/DirMap.zig +2 -2
@@ 105,14 105,14 @@ pub const Dir = struct {

    name: []const u8,
    files: std.ArrayList(File),
    selection: usize,
    cursor: usize,
    scroll_offset: usize,
    dirty: bool, // if true file list needs to be refreshed

    pub fn init(self: *Dir, alloc: mem.Allocator, dir: fs.Dir, name: []const u8) !void {
        self.files = std.ArrayList(File).init(alloc);
        self.name = name;
        self.selection = 0;
        self.cursor = 0;
        self.scroll_offset = 0;
        self.dirty = false;


M src/UserInterface.zig => src/UserInterface.zig +15 -15
@@ 625,26 625,26 @@ fn drawList(self: *Self, writer: anytype) !void {
        try setAttr(writer, .{ .fg = .red, .bold = true });
        try writer.writeAll(msg);
    } else {
        // Move selection
        if (context.selection_delta > 0) {
            if (context.cwd.selection +| @intCast(usize, context.selection_delta) >= context.cwd.files.items.len - 1) {
                context.cwd.selection = context.cwd.files.items.len - 1;
        // Move cursor.
        if (context.cursor_delta > 0) {
            if (context.cwd.cursor +| @intCast(usize, context.cursor_delta) >= context.cwd.files.items.len - 1) {
                context.cwd.cursor = context.cwd.files.items.len - 1;
            } else {
                context.cwd.selection +|= @intCast(usize, context.selection_delta);
                context.cwd.cursor +|= @intCast(usize, context.cursor_delta);
            }
        } else if (context.selection_delta < 0) {
            context.cwd.selection -|= @intCast(usize, try math.absInt(context.selection_delta));
        } else if (context.cursor_delta < 0) {
            context.cwd.cursor -|= @intCast(usize, try math.absInt(context.cursor_delta));
        }

        // Move scroll offset
        if (context.cwd.selection < context.cwd.scroll_offset) {
            context.cwd.scroll_offset = context.cwd.selection;
        } else if (context.cwd.selection > context.cwd.scroll_offset +| self.height - 2) {
            context.cwd.scroll_offset +|= context.cwd.selection - ((self.height - 2) +| context.cwd.scroll_offset);
        // Move scroll offset.
        if (context.cwd.cursor < context.cwd.scroll_offset) {
            context.cwd.scroll_offset = context.cwd.cursor;
        } else if (context.cwd.cursor > context.cwd.scroll_offset +| self.height - 2) {
            context.cwd.scroll_offset +|= context.cwd.cursor - ((self.height - 2) +| context.cwd.scroll_offset);
        }

        // TODO In most cases, only two list entries need to be drawn, the
        //      previous selection and the next selection. The full list only
        //      previous cursor and the next cursor. The full list only
        //      needs to be draws when either the scroll_offset changes or when
        //      the cwd has changed.
        var i: usize = 0;


@@ 653,12 653,12 @@ fn drawList(self: *Self, writer: anytype) !void {
                writer,
                i + 1,
                context.cwd.files.items[i + context.cwd.scroll_offset],
                i + context.cwd.scroll_offset == context.cwd.selection,
                i + context.cwd.scroll_offset == context.cwd.cursor,
                self.width,
            );
        }
    }
    context.selection_delta = 0;
    context.cursor_delta = 0;
}

fn drawFileName(writer: anytype, y: usize, file: DirMap.Dir.File, selected: bool, _width: usize) !void {

M src/nfm.zig => src/nfm.zig +27 -28
@@ 58,9 58,9 @@ pub const Context = struct {
    initial_cwd_path: []const u8 = undefined,
    search: ?Regex = null,

    /// A change in the selection index, likely caused by keybind. It will be
    /// A change in the cursor index, likely caused by keybind. It will be
    /// applied whenever ui.render() is called.
    selection_delta: isize = 0,
    cursor_delta: isize = 0,

    loop: bool = true,



@@ 204,7 204,7 @@ fn handleEventUserInput(ev: UserInterface.Event) !void {
            'a' => buffer.cursor = 0,
            'e' => buffer.cursor = buffer.buffer.items.len,
            's' => {
                const file = &context.cwd.files.items[context.cwd.selection];
                const file = &context.cwd.files.items[context.cwd.cursor];
                try buffer.insertSlice(file.name, .single_word);
                try buffer.insertChar(' ');
            },


@@ 340,11 340,11 @@ fn handleEventNav(ev: UserInterface.Event) !void {
        .escape => try handleEscapeNav(),
        .page_up => {
            // Scroll 80% of the terminal height.
            context.selection_delta -|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
            context.cursor_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));
            context.cursor_delta +|= 4 * @intCast(isize, @divFloor(context.ui.height, 5));
            try context.ui.render(true, false);
        },
        .ctrl => |ch| {


@@ 407,25 407,25 @@ fn handleAsciiKeyNav(key: u8) !void {
    switch (key) {
        'j' => {
            if (context.cwd.files.items.len != 0) {
                context.selection_delta += 1;
                context.cursor_delta += 1;
                try context.ui.render(true, title_dirty);
            }
        },
        'k' => {
            if (context.cwd.files.items.len != 0) {
                context.selection_delta -= 1;
                context.cursor_delta -= 1;
                try context.ui.render(true, title_dirty);
            }
        },
        'h' => try setCwd(".."),
        'l' => try enterOrOpen(),
        'g', 'K' => {
            context.cwd.selection = 0;
            context.cwd.cursor = 0;
            context.cwd.scroll_offset = 0;
            try context.ui.render(true, title_dirty);
        },
        'G', 'J' => {
            context.cwd.selection = context.cwd.files.items.len - 1;
            context.cwd.cursor = context.cwd.files.items.len - 1;
            try context.ui.render(true, title_dirty);
        },
        // TODO find out why enter is suddenly carriage return instead of newline.


@@ 436,7 436,7 @@ fn handleAsciiKeyNav(key: u8) !void {
        },
        'm' => {
            if (context.cwd.files.items.len == 0) return;
            const file = &context.cwd.files.items[context.cwd.selection];
            const file = &context.cwd.files.items[context.cwd.cursor];
            file.mark = !file.mark;
            try context.ui.render(true, title_dirty);
        },


@@ 457,7 457,7 @@ fn handleAsciiKeyNav(key: u8) !void {
            //      own command prompt commands, with some sort of special
            //      format syntax to insert file names and set the cursor
            //      position.
            const file = &context.cwd.files.items[context.cwd.selection];
            const file = &context.cwd.files.items[context.cwd.cursor];
            try context.mode.setUserInput(.command);
            try context.mode.user_input.buffer.insertChar(' ');
            try context.mode.user_input.buffer.insertSlice(file.name, .single_word);


@@ 465,7 465,7 @@ fn handleAsciiKeyNav(key: u8) !void {
            try context.ui.render(false, true);
        },
        'x' => {
            const file = &context.cwd.files.items[context.cwd.selection];
            const file = &context.cwd.files.items[context.cwd.cursor];
            try context.mode.setUserInput(.command);
            try context.mode.user_input.buffer.insertSlice("rm ", .pure);
            try context.mode.user_input.buffer.insertSlice(file.name, .single_word);


@@ 477,7 477,7 @@ fn handleAsciiKeyNav(key: u8) !void {
            try context.ui.render(false, true);
        },
        'c' => {
            const file = &context.cwd.files.items[context.cwd.selection];
            const file = &context.cwd.files.items[context.cwd.cursor];
            try context.mode.setUserInput(.command);
            try context.mode.user_input.buffer.insertSlice("mv ", .pure);
            try context.mode.user_input.buffer.insertSlice(file.name, .single_word);


@@ 504,19 504,19 @@ fn handleAsciiKeyNav(key: u8) !void {
                return;
            };

            var i = context.cwd.selection;
            for (context.cwd.files.items[context.cwd.selection + 1 ..]) |file| {
            var i = context.cwd.cursor;
            for (context.cwd.files.items[context.cwd.cursor + 1 ..]) |file| {
                i +|= 1;
                if (try search.partialMatch(file.name)) {
                    context.cwd.selection = i;
                    context.cwd.cursor = i;
                    try context.ui.render(true, title_dirty);
                    return;
                }
            }
            i = 0;
            for (context.cwd.files.items[0 .. context.cwd.selection + 1]) |file| {
            for (context.cwd.files.items[0 .. context.cwd.cursor + 1]) |file| {
                if (try search.partialMatch(file.name)) {
                    context.cwd.selection = i;
                    context.cwd.cursor = i;
                    try context.ui.render(true, title_dirty);
                    return;
                }


@@ 527,19 527,19 @@ fn handleAsciiKeyNav(key: u8) !void {
            try context.ui.render(false, true);
        },
        'N' => {
            var i = context.cwd.selection;
            for (context.cwd.files.items[context.cwd.selection + 1 ..]) |file| {
            var i = context.cwd.cursor;
            for (context.cwd.files.items[context.cwd.cursor + 1 ..]) |file| {
                i +|= 1;
                if (file.mark) {
                    context.cwd.selection = i;
                    context.cwd.cursor = i;
                    try context.ui.render(true, false);
                    return;
                }
            }
            i = 0;
            for (context.cwd.files.items[0 .. context.cwd.selection + 1]) |file| {
            for (context.cwd.files.items[0 .. context.cwd.cursor + 1]) |file| {
                if (file.mark) {
                    context.cwd.selection = i;
                    context.cwd.cursor = i;
                    try context.ui.render(true, false);
                    return;
                }


@@ 616,7 616,7 @@ fn handleReturnNav() !void {

fn enterOrOpen() !void {
    if (context.cwd.files.items.len == 0) return;
    const file = &context.cwd.files.items[context.cwd.selection];
    const file = &context.cwd.files.items[context.cwd.cursor];
    if (file.kind == .Directory) {
        try setCwd(file.name);
    } else if (file.kind == .File) {


@@ 685,13 685,12 @@ fn setCwd(dirname: []const u8) !void {
        }
    };

    // If we go up one directory, ensure that the selection is on the previous
    // cwd.
    // If we go up one directory, ensure that the cursor is on the previous cwd.
    if (dirname[0] == '.' and dirname[1] == '.') {
        var i: usize = 0;
        for (context.cwd.files.items) |*dir| {
            if (mem.eql(u8, prev_basename, dir.name)) {
                context.cwd.selection = i;
                context.cwd.cursor = i;
                break;
            }
            i += 1;


@@ 713,7 712,7 @@ fn dumpSelection() !void {
    // If there were no marked files, try to output the selected file.
    if (!mark) {
        if (context.cwd.files.items.len == 0) return;
        const file = context.cwd.files.items[context.cwd.selection];
        const file = context.cwd.files.items[context.cwd.cursor];
        try file.dumpPath(writer);
    }
}