~leon_plickat/nfm

2965e3f68f3566dc7882fadd5b4a0cb541cb84e2 — Leon Henrik Plickat 4 months ago 771b65c inotify
[WIP] inotify
2 files changed, 140 insertions(+), 26 deletions(-)

M src/DirMap.zig
M src/nfm.zig
M src/DirMap.zig => src/DirMap.zig +116 -22
@@ 1,6 1,6 @@
// This file is part of nfm, the neat file manager.
//
// Copyright © 2021 Leon Henrik Plickat
// Copyright © 2021 - 2022 Leon Henrik Plickat
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by


@@ 15,6 15,7 @@
// 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 builtin = @import("builtin");
const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;


@@ 25,8 26,12 @@ const mem = std.mem;
const os = std.os;
const unicode = std.unicode;

const Context = @import("nfm.zig").Context;

const Self = @This();

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

pub const Dir = struct {
    pub const File = struct {
        dir: *Dir,


@@ 104,22 109,69 @@ pub const Dir = struct {
    };

    name: []const u8,
    files: std.ArrayList(File),
    files: std.ArrayListUnmanaged(File),
    files_arena: heap.ArenaAllocator,
    cursor: usize,
    scroll_offset: usize,
    dirty: bool, // if true file list needs to be refreshed
    inotify_wd: i32 = undefined,

    pub fn init(self: *Dir, dirmap: *Self, dir: fs.Dir, name: []const u8) !void {
        self.files_arena = heap.ArenaAllocator.init(context.gpa);
        errdefer self.files_arena.deinit();
        const alloc = self.files_arena.allocator();
        self.files = try std.ArrayListUnmanaged(File).initCapacity(alloc, 16);
        try self.getFiles(alloc, dir);

    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.cursor = 0;
        self.scroll_offset = 0;
        self.dirty = false;

        // Get Directories.
        self.inotify_wd = try os.inotify_add_watch(
            dirmap.inotify_fd,
            self.name,
            os.linux.IN.CREATE | os.linux.IN.DELETE | os.linux.IN.DELETE_SELF | os.linux.IN.MOVE_SELF | os.linux.IN.MOVE | os.linux.IN.EXCL_UNLINK | os.linux.IN.ONLYDIR,
        );
    }

    pub fn deinit(self: *Dir) void {
        self.files_arena.deinit();
    }

    pub fn refresh(self: *Dir) !void {
        self.dirty = false;

        const old_cursor = self.cursor;
        const old_cursor_file_name = try context.gpa.dupe(u8, self.files.items[self.cursor].name);
        defer context.gpa.free(old_cursor_file_name);

        self.files_arena.deinit();

        self.files_arena = heap.ArenaAllocator.init(context.gpa);
        errdefer self.files_arena.deinit();
        const alloc = self.files_arena.allocator();
        self.files = try std.ArrayListUnmanaged(File).initCapacity(alloc, 16);
        var dir = try fs.cwd().openDir(self.name, .{});
        defer dir.close();
        try self.getFiles(alloc, dir);

        for (self.files.items) |fl, i| {
            if (mem.eql(u8, fl.name, old_cursor_file_name)) {
                self.cursor = i;
                return;
            }
        }

        if (old_cursor < self.files.items.len) {
            self.cursor = old_cursor;
        }
    }

    fn getFiles(self: *Dir, alloc: mem.Allocator, dir: fs.Dir) !void {
        var it = dir.iterate();
        while (try it.next()) |entry| {
            const file = try self.files.addOne();
            const file = try self.files.addOne(alloc);
            file.dir = self;
            file.name = try alloc.dupeZ(u8, entry.name);
            file.kind = entry.kind;


@@ 141,9 193,7 @@ pub const Dir = struct {
        std.sort.sort(File, self.files.items, {}, Dir.lessThan);
    }

    fn lessThan(context: void, left: File, right: File) bool {
        _ = context;

    fn lessThan(_: void, left: File, right: File) bool {
        // Directories come first.
        if (right.kind == .Directory) {
            if (left.kind != .Directory) return false;


@@ 163,22 213,60 @@ pub const Dir = struct {
};

dirs: std.StringHashMapUnmanaged(*Dir) = .{},
arena: heap.ArenaAllocator,

pub fn new(alloc: mem.Allocator) !Self {
    return Self{
        .arena = heap.ArenaAllocator.init(alloc),
    };
inotify_fd: os.fd_t = undefined,
inotify_ev: os.linux.epoll_event = undefined,

pub fn init(self: *Self) !void {
    if (builtin.os.tag == .linux) {
        self.inotify_fd = try os.inotify_init1(os.linux.IN.NONBLOCK | os.linux.IN.CLOEXEC);
        errdefer os.close(self.inotify_fd);

        self.inotify_ev = .{
            .events = os.linux.EPOLL.IN,
            .data = .{ .@"u32" = @enumToInt(Context.PollIndex.dirmap) },
        };
        try os.epoll_ctl(
            context.epoll_fd,
            os.linux.EPOLL.CTL_ADD,
            self.inotify_fd,
            &self.inotify_ev,
        );
    } else {
        // TODO support other operating systems
    }
}

pub fn deinit(self: *Self) void {
    self.arena.deinit();
    var it = self.dirs.iterator();
    while (it.next()) |entry| {
        entry.value_ptr.*.deinit();
        context.gpa.free(entry.value_ptr);
        context.gpa.free(entry.key_ptr);
    }
    self.dirs.deinit();
    os.close(self.inotify_fd);
}

pub fn handleInotify(self: *Self) !void {
    const buf_size = @sizeOf(os.linux.inotify_event);
    var buf: [buf_size]u8 = undefined;
    const read = try os.read(self.inotify_fd, &buf);
    if (read == 0) return;
    const ev = @ptrCast(*const os.linux.inotify_event, &buf);
    var it = self.dirs.iterator();
    while (it.next()) |dir| {
        if (dir.inotify_wd == ev.wd) {
            dir.dirty = true;
            break;
        }
    } else {
        return error.UnknownInotifyWatchDescriptor;
    }
}

/// Add or find a dir for a given relative path
pub fn getDirAndSetCwd(self: *Self, relative_path: []const u8) !*Dir {
    const alloc = self.arena.allocator();

    var dir = try fs.cwd().openDir(relative_path, .{ .iterate = true });
    defer dir.close();



@@ 188,12 276,18 @@ pub fn getDirAndSetCwd(self: *Self, relative_path: []const u8) !*Dir {
    try dir.setAsCwd();

    // TODO: there must be a better way to get the name / path of a Dir.
    const dir_path = try dir.realpathAlloc(alloc, ".");
    const dir_path = try dir.realpathAlloc(context.gpa, ".");
    errdefer context.gpa.free(dir_path);

    var mapentry = try self.dirs.getOrPut(alloc, dir_path);
    if (!mapentry.found_existing) {
        mapentry.value_ptr.* = try alloc.create(Dir);
        try mapentry.value_ptr.*.init(alloc, dir, dir_path);
    var mapentry = try self.dirs.getOrPut(context.gpa, dir_path);
    if (mapentry.found_existing) {
        context.gpa.free(dir_path);
    } else {
        mapentry.value_ptr.* = try context.gpa.create(Dir);
        try mapentry.value_ptr.*.init(self, dir, dir_path);
    }
    if (mapentry.value_ptr.*.dirty) {
        try mapentry.value_ptr.*.refresh();
    }
    return mapentry.value_ptr.*;
}

M src/nfm.zig => src/nfm.zig +24 -4
@@ 43,10 43,11 @@ const UserInterface = @import("UserInterface.zig");
pub const Context = struct {
    pub const PollIndex = enum(u32) {
        ui = 1,
        dirmap = 2,
    };

    epoll_fd: i32 = undefined,
    fds: [1]os.pollfd = undefined,
    fds: [2]os.pollfd = undefined,

    config: Config = .{},
    ui: UserInterface = undefined,


@@ 132,8 133,9 @@ fn nfm() !void {
        context.epoll_fd = try os.epoll_create1(0);
    }

    context.dirmap = try DirMap.new(context.gpa);
    try context.dirmap.init();
    defer context.dirmap.deinit();

    context.cwd = try context.dirmap.getDirAndSetCwd(".");
    context.initial_cwd_path = context.cwd.name;



@@ 150,9 152,10 @@ fn nfm() !void {
    }

    while (context.loop) {
        var epoll_ev: [8]os.linux.epoll_event = undefined;
        var epoll_ev_len: usize = 0;
        if (builtin.os.tag == .linux) {
            var ev: [4]os.linux.epoll_event = undefined;
            _ = os.epoll_wait(context.epoll_fd, &ev, -1);
            epoll_ev_len = os.epoll_wait(context.epoll_fd, &epoll_ev, -1);
        } else {
            _ = try os.poll(&context.fds, -1);
        }


@@ 160,6 163,15 @@ fn nfm() !void {
        // The .dirty UI event does not set POLLIN, it just interrupts poll(),
        // so let's just always try to get UI events.
        try handleUiEvents();

        if (builtin.os.tag == .linux) {
            for (epoll_ev[0..epoll_ev_len]) |e| {
                switch (@intToEnum(Context.PollIndex, e.data.@"u32")) {
                    .ui => {}, // See above.
                    .dirmap => try handleDirmapEvents(),
                }
            }
        }
    }

    if (builtin.os.tag == .linux) {


@@ 174,6 186,14 @@ fn nfm() !void {
    }
}

fn handleDirmapEvents() !void {
    try context.dirmap.handleInotify();
    if (context.cwd.dirty) {
        try context.cwd.refresh();
        try context.ui.render(true, false);
    }
}

fn handleUiEvents() !void {
    while (try context.ui.nextEvent()) |ev| {
        switch (context.mode) {