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