// This file is part of nfm, the neat file manager.
//
// Copyright © 2021 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 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 Self = @This();
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.ArrayList(File),
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.cursor = 0;
self.scroll_offset = 0;
self.dirty = false;
// Get Directories.
var it = dir.iterate();
while (try it.next()) |entry| {
const file = try self.files.addOne();
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(context: void, left: File, right: File) bool {
_ = context;
// 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) = .{},
arena: heap.ArenaAllocator,
pub fn new(alloc: mem.Allocator) !Self {
return Self{
.arena = heap.ArenaAllocator.init(alloc),
};
}
pub fn deinit(self: *Self) void {
self.arena.deinit();
}
/// 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();
// 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(alloc, ".");
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);
}
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;
}