// This file is part of nfm, the neat file manager.
//
// 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
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// 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 builtin = @import("builtin");
const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;
const fs = std.fs;
const heap = std.heap;
const math = std.math;
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,
name: [:0]const u8,
kind: fs.Dir.Entry.Kind,
mark: bool,
text: ?bool,
r_user: bool = false,
w_user: bool = false,
x_user: bool = false,
r_group: bool = false,
w_group: bool = false,
x_group: bool = false,
r_other: bool = false,
w_other: bool = false,
x_other: bool = false,
/// Check if the file is a text file by checking if its first 16 bytes
/// are valid unicode. Also check against magic number of common false
/// positives.
/// TODO: maybe find a better detection method?
pub fn isTextFile(self: *File) !bool {
if (self.text) |text| return text;
std.debug.assert(self.kind == .File);
var dir = try fs.cwd().openDir(self.dir.name, .{});
defer dir.close();
var file = try dir.openFile(self.name, .{});
defer file.close();
try file.seekTo(0);
var bytes: [16]u8 = undefined;
const bytes_read = try file.readAll(&bytes);
// Treat empty files as text files, but don't cache the type,
// because the file may get filled later with non-text data.
if (bytes_read == 0) return true;
// The magic values of some false positives.
const magic = [_][]const u8{
"\x25PDF", // PDF
"\x7FELF", // ELF
"\x00\x00\x00\x20ftypisom\x00\x00", // mp4
"\xFF\xFB", // mp3 per wikipedia
"ID3", // mp3 per reality, because metatags
"OggS", // ogg
"fLaC", // flac
"RIFF", // wav / riff
"gimpxcf", // gimps XCF format
"IWAD", // DOOM WAD file
};
for (magic) |mg| {
if (mem.eql(u8, mg, bytes[0..mg.len])) {
self.text = false;
return false;
}
}
// TODO: Some files are technically text files, but we usually don't
// want them to open in our editors. So check file extension
// as well, for example for ".svg".
self.text = unicode.utf8ValidateSlice(&bytes);
return self.text.?;
}
/// Write path
pub fn dumpPath(self: *const File, writer: anytype) !void {
_ = try writer.write(self.dir.name);
_ = try writer.write("/");
_ = try writer.write(self.name);
if (self.kind == .Directory) _ = try writer.write("/");
_ = try writer.write("\n");
}
};
name: []const u8,
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);
self.name = name;
self.cursor = 0;
self.scroll_offset = 0;
self.dirty = false;
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(alloc);
file.dir = self;
file.name = try alloc.dupeZ(u8, entry.name);
file.kind = entry.kind;
file.mark = false;
file.text = null;
var stat: os.system.Stat = undefined;
_ = os.system.stat(file.name, &stat);
if ((stat.mode & os.system.S.IRUSR) > 0) file.r_user = true;
if ((stat.mode & os.system.S.IWUSR) > 0) file.w_user = true;
if ((stat.mode & os.system.S.IXUSR) > 0) file.x_user = true;
if ((stat.mode & os.system.S.IRGRP) > 0) file.r_group = true;
if ((stat.mode & os.system.S.IWGRP) > 0) file.w_group = true;
if ((stat.mode & os.system.S.IXGRP) > 0) file.x_group = true;
if ((stat.mode & os.system.S.IROTH) > 0) file.r_other = true;
if ((stat.mode & os.system.S.IWOTH) > 0) file.w_other = true;
if ((stat.mode & os.system.S.IXOTH) > 0) file.x_other = true;
}
std.sort.sort(File, self.files.items, {}, Dir.lessThan);
}
fn lessThan(_: void, left: File, right: File) bool {
// Directories come first.
if (right.kind == .Directory) {
if (left.kind != .Directory) return false;
} else {
if (left.kind == .Directory) return true;
}
// TODO fancier sorting (although this accidentalloc already also does
// numbers mostly right)
var i: usize = 0;
while (i < left.name.len and i < right.name.len) : (i += 1) {
if (right.name[i] == left.name[i]) continue;
return ascii.toLower(right.name[i]) > ascii.toLower(left.name[i]);
}
return false;
}
};
dirs: std.StringHashMapUnmanaged(*Dir) = .{},
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 {
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 {
var dir = try fs.cwd().openDir(relative_path, .{ .iterate = true });
defer dir.close();
// This does not belong here, as it breaks the abstraction, but I can't
// seem to find a way to get it out of here while still keeping the
// interface nice and only opening the directory once.
try dir.setAsCwd();
// TODO: there must be a better way to get the name / path of a Dir.
const dir_path = try dir.realpathAlloc(context.gpa, ".");
errdefer context.gpa.free(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.*;
}
const MarkIterator = struct {
map: *Self,
map_it: std.StringHashMapUnmanaged(*Dir).Iterator,
dir_item_index: usize = 0,
current_dir: *Dir = undefined,
pub fn next(it: *MarkIterator) ?*Dir.File {
while (true) {
for (it.current_dir.files.items[it.dir_item_index..]) |*file| {
defer it.dir_item_index += 1;
if (file.mark) return file;
}
const entry = it.map_it.next() orelse return null;
it.current_dir = entry.value_ptr.*;
it.dir_item_index = 0;
}
}
};
/// Iterate over all marked files in all directories.
pub fn markIterator(self: *Self) MarkIterator {
var ret = MarkIterator{
.map = self,
.map_it = self.dirs.iterator(),
};
const entry = ret.map_it.next() orelse unreachable;
ret.current_dir = entry.value_ptr.*;
return ret;
}