地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
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{};