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