地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
at main 240 lines 8.3 kB view raw
1const std = @import("std"); 2const builtin = @import("builtin"); 3 4const vaxis = @import("vaxis"); 5 6const App = @import("./app.zig"); 7const environment = @import("./environment.zig"); 8const Notification = @import("./notification.zig"); 9const FileLogger = @import("file_logger.zig"); 10 11const CONFIG_NAME = "config.json"; 12const TRASH_DIR_NAME = "trash"; 13const HOME_DIR_NAME = ".jido"; 14const XDG_CONFIG_HOME_DIR_NAME = "jido"; 15 16const Config = struct { 17 show_hidden: bool = true, 18 sort_dirs: bool = true, 19 show_images: bool = true, 20 preview_file: bool = true, 21 empty_trash_on_exit: bool = false, 22 true_dir_size: bool = false, 23 entry_dir: ?[]const u8 = null, 24 archive_traversal_limit: usize = 100, 25 keep_partial_extraction: bool = false, 26 styles: Styles = .{}, 27 keybinds: Keybinds = .{}, 28 29 config_dir: ?std.fs.Dir = null, 30 31 ///Returned dir needs to be closed by user. 32 pub fn configDir(self: Config) !?std.fs.Dir { 33 if (self.config_dir) |dir| { 34 return try dir.openDir(".", .{ .iterate = true }); 35 } else return null; 36 } 37 38 ///Returned dir needs to be closed by user. 39 pub fn trashDir(self: Config) !?std.fs.Dir { 40 var parent = try self.configDir() orelse return null; 41 defer parent.close(); 42 if (!environment.dirExists(parent, TRASH_DIR_NAME)) { 43 try parent.makeDir(TRASH_DIR_NAME); 44 } 45 46 return try parent.openDir(TRASH_DIR_NAME, .{ .iterate = true }); 47 } 48 49 pub fn parse(self: *Config, alloc: std.mem.Allocator, app: *App) !void { 50 var dir = lbl: { 51 if (try environment.getXdgConfigHomeDir()) |home_dir| { 52 defer { 53 var dir = home_dir; 54 dir.close(); 55 } 56 57 if (!environment.dirExists(home_dir, XDG_CONFIG_HOME_DIR_NAME)) { 58 try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME); 59 } 60 61 const jido_dir = try home_dir.openDir( 62 XDG_CONFIG_HOME_DIR_NAME, 63 .{ .iterate = true }, 64 ); 65 self.config_dir = jido_dir; 66 67 if (environment.fileExists(jido_dir, CONFIG_NAME)) { 68 break :lbl jido_dir; 69 } 70 return; 71 } 72 73 if (try environment.getHomeDir()) |home_dir| { 74 defer { 75 var dir = home_dir; 76 dir.close(); 77 } 78 79 if (!environment.dirExists(home_dir, HOME_DIR_NAME)) { 80 try home_dir.makeDir(HOME_DIR_NAME); 81 } 82 83 const jido_dir = try home_dir.openDir( 84 HOME_DIR_NAME, 85 .{ .iterate = true }, 86 ); 87 self.config_dir = jido_dir; 88 89 if (environment.fileExists(jido_dir, CONFIG_NAME)) { 90 break :lbl jido_dir; 91 } 92 return; 93 } 94 95 return; 96 }; 97 98 const config_file = try dir.openFile(CONFIG_NAME, .{}); 99 defer config_file.close(); 100 101 const config_str = try config_file.readToEndAlloc(alloc, 1024 * 1024 * 1024); 102 defer alloc.free(config_str); 103 104 const parsed_config = try std.json.parseFromSlice(Config, alloc, config_str, .{}); 105 defer parsed_config.deinit(); 106 107 self.* = parsed_config.value; 108 self.config_dir = dir; 109 110 // Check duplicate keybinds 111 { 112 var file_logger = FileLogger.init(dir); 113 defer file_logger.deinit(); 114 115 var key_map = std.AutoHashMap(u21, []const u8).init(alloc); 116 defer { 117 var it = key_map.iterator(); 118 while (it.next()) |entry| { 119 alloc.free(entry.value_ptr.*); 120 } 121 key_map.deinit(); 122 } 123 124 inline for (std.meta.fields(Keybinds)) |field| { 125 if (@field(self.keybinds, field.name)) |field_value| { 126 const codepoint = @intFromEnum(field_value); 127 128 const res = try key_map.getOrPut(codepoint); 129 if (res.found_existing) { 130 var keybind_str: [1024]u8 = undefined; 131 const keybind_str_bytes = try std.unicode.utf8Encode(codepoint, &keybind_str); 132 133 const message = try std.fmt.allocPrint( 134 alloc, 135 "'{s}' and '{s}' have the same keybind: '{s}'. This can cause undefined behaviour.", 136 .{ res.value_ptr.*, field.name, keybind_str[0..keybind_str_bytes] }, 137 ); 138 defer alloc.free(message); 139 140 app.notification.write(message, .err) catch {}; 141 file_logger.write(message, .err) catch {}; 142 143 return error.DuplicateKeybind; 144 } 145 res.value_ptr.* = try alloc.dupe(u8, field.name); 146 } 147 } 148 } 149 150 return; 151 } 152}; 153 154const Colours = struct { 155 const RGB = [3]u8; 156 const red: RGB = .{ 227, 23, 10 }; 157 const orange: RGB = .{ 251, 139, 36 }; 158 const blue: RGB = .{ 82, 209, 220 }; 159 const grey: RGB = .{ 39, 39, 39 }; 160 const black: RGB = .{ 0, 0, 0 }; 161 const snow_white: RGB = .{ 254, 252, 253 }; 162}; 163 164const NotificationStyles = struct { 165 box: vaxis.Style = vaxis.Style{ 166 .fg = .{ .rgb = Colours.snow_white }, 167 .bg = .{ .rgb = Colours.grey }, 168 }, 169 err: vaxis.Style = vaxis.Style{ 170 .fg = .{ .rgb = Colours.red }, 171 .bg = .{ .rgb = Colours.grey }, 172 }, 173 warn: vaxis.Style = vaxis.Style{ 174 .fg = .{ .rgb = Colours.orange }, 175 .bg = .{ .rgb = Colours.grey }, 176 }, 177 info: vaxis.Style = vaxis.Style{ 178 .fg = .{ .rgb = Colours.blue }, 179 .bg = .{ .rgb = Colours.grey }, 180 }, 181}; 182 183pub const Keybinds = struct { 184 pub const Char = enum(u21) { 185 _, 186 pub fn jsonParse(alloc: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() { 187 const parsed = try std.json.innerParse([]const u8, alloc, source, options); 188 if (std.mem.eql(u8, parsed, "")) return error.InvalidCharacter; 189 190 const utf8_byte_sequence_len = std.unicode.utf8ByteSequenceLength(parsed[0]) catch return error.InvalidCharacter; 191 if (parsed.len != utf8_byte_sequence_len) return error.InvalidCharacter; 192 const unicode = switch (utf8_byte_sequence_len) { 193 1 => parsed[0], 194 2 => std.unicode.utf8Decode2(parsed[0..2].*), 195 3 => std.unicode.utf8Decode3(parsed[0..3].*), 196 4 => std.unicode.utf8Decode4(parsed[0..4].*), 197 else => return error.InvalidCharacter, 198 } catch return error.InvalidCharacter; 199 200 return @enumFromInt(unicode); 201 } 202 }; 203 204 toggle_hidden_files: ?Char = @enumFromInt('.'), 205 delete: ?Char = @enumFromInt('D'), 206 rename: ?Char = @enumFromInt('R'), 207 create_dir: ?Char = @enumFromInt('d'), 208 create_file: ?Char = @enumFromInt('%'), 209 fuzzy_find: ?Char = @enumFromInt('/'), 210 change_dir: ?Char = @enumFromInt('c'), 211 enter_command_mode: ?Char = @enumFromInt(':'), 212 jump_top: ?Char = @enumFromInt('g'), 213 jump_bottom: ?Char = @enumFromInt('G'), 214 toggle_verbose_file_information: ?Char = @enumFromInt('v'), 215 force_delete: ?Char = null, 216 paste: ?Char = @enumFromInt('p'), 217 yank: ?Char = @enumFromInt('y'), 218 extract_archive: ?Char = @enumFromInt('x'), 219}; 220 221const Styles = struct { 222 selected_list_item: vaxis.Style = vaxis.Style{ 223 .bg = .{ .rgb = Colours.grey }, 224 .bold = true, 225 }, 226 notification: NotificationStyles = NotificationStyles{}, 227 text_input: vaxis.Style = vaxis.Style{}, 228 text_input_err: vaxis.Style = vaxis.Style{ .bg = .{ .rgb = Colours.red } }, 229 list_item: vaxis.Style = vaxis.Style{}, 230 file_name: vaxis.Style = vaxis.Style{}, 231 file_information: vaxis.Style = vaxis.Style{ 232 .fg = .{ .rgb = Colours.black }, 233 .bg = .{ .rgb = Colours.snow_white }, 234 }, 235 git_branch: vaxis.Style = vaxis.Style{ 236 .fg = .{ .rgb = Colours.blue }, 237 }, 238}; 239 240pub var config: Config = Config{};