地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.
1const std = @import("std");
2const App = @import("./app.zig");
3const environment = @import("./environment.zig");
4const zuid = @import("zuid");
5const vaxis = @import("vaxis");
6const Key = vaxis.Key;
7const config = &@import("./config.zig").config;
8const commands = @import("./commands.zig");
9const Keybinds = @import("./config.zig").Keybinds;
10const events = @import("./events.zig");
11
12pub fn handleGlobalEvent(
13 app: *App,
14 event: App.Event,
15) error{OutOfMemory}!void {
16 switch (event) {
17 .key_press => |key| {
18 if ((key.codepoint == 'c' and key.mods.ctrl)) {
19 app.should_quit = true;
20 return;
21 }
22
23 if ((key.codepoint == 'r' and key.mods.ctrl)) {
24 if (config.parse(app.alloc, app)) {
25 app.notification.write("Reloaded configuration file.", .info) catch {};
26 } else |err| switch (err) {
27 error.SyntaxError => {
28 app.notification.write("Encountered a syntax error while parsing the config file.", .err) catch {
29 std.log.err("Encountered a syntax error while parsing the config file.", .{});
30 };
31 },
32 error.InvalidCharacter => {
33 app.notification.write("One or more overriden keybinds are invalid.", .err) catch {
34 std.log.err("One or more overriden keybinds are invalid.", .{});
35 };
36 },
37 error.DuplicateKeybind => {
38 // Error logged in function
39 },
40 else => {
41 const message = try std.fmt.allocPrint(app.alloc, "Encountend an unknown error while parsing the config file - {}", .{err});
42 defer app.alloc.free(message);
43
44 app.notification.write(message, .err) catch {
45 std.log.err("Encountend an unknown error while parsing the config file - {}", .{err});
46 };
47 },
48 }
49 }
50 },
51 else => {},
52 }
53}
54
55pub fn handleNormalEvent(
56 app: *App,
57 event: App.Event,
58) !void {
59 switch (event) {
60 .key_press => |key| {
61 @setEvalBranchQuota(
62 std.meta.fields(Keybinds).len * 1000,
63 );
64
65 const maybe_remap: ?std.meta.FieldEnum(Keybinds) = lbl: {
66 inline for (std.meta.fields(Keybinds)) |field| {
67 if (@field(config.keybinds, field.name)) |field_value| {
68 if (key.codepoint == @intFromEnum(field_value)) {
69 break :lbl comptime std.meta.stringToEnum(std.meta.FieldEnum(Keybinds), field.name) orelse unreachable;
70 }
71 }
72 }
73 break :lbl null;
74 };
75
76 if (maybe_remap) |action| {
77 switch (action) {
78 .toggle_hidden_files => try events.toggleHiddenFiles(app),
79 .delete => try events.delete(app),
80 .rename => {
81 const entry = (app.directories.getSelected() catch {
82 app.notification.write("Can not rename item - no item selected.", .warn) catch {};
83 return;
84 }) orelse return;
85
86 app.text_input.clearAndFree();
87
88 // Try insert entry name into text input for a nicer experience.
89 // This failing shouldn't stop the user from entering a new name.
90 app.text_input.insertSliceAtCursor(entry.name) catch {};
91 app.state = .rename;
92 },
93
94 .create_dir => {
95 try app.repopulateDirectory("");
96 app.text_input.clearAndFree();
97 app.state = .new_dir;
98 },
99 .create_file => {
100 try app.repopulateDirectory("");
101 app.text_input.clearAndFree();
102 app.state = .new_file;
103 },
104 .fuzzy_find => {
105 app.text_input.clearAndFree();
106 app.state = .fuzzy;
107 },
108 .change_dir => {
109 app.text_input.clearAndFree();
110 app.state = .change_dir;
111 },
112 .enter_command_mode => {
113 app.text_input.clearAndFree();
114 app.text_input.insertSliceAtCursor(":") catch {};
115 app.state = .command;
116 },
117 .jump_bottom => app.directories.entries.selectLast(),
118 .jump_top => app.directories.entries.selectFirst(),
119 .toggle_verbose_file_information => app.drawer.verbose = !app.drawer.verbose,
120 .force_delete => try events.forceDelete(app),
121 .yank => try events.yank(app),
122 .paste => try events.paste(app),
123 }
124 } else {
125 switch (key.codepoint) {
126 '-', 'h', Key.left => try events.traverseLeft(app),
127 Key.enter, 'l', Key.right => try events.traverseRight(app),
128 'j', Key.down => app.directories.entries.next(),
129 'k', Key.up => app.directories.entries.previous(),
130 'u' => try events.undo(app),
131 else => {},
132 }
133 }
134 },
135 .image_ready => {},
136 .notification => {},
137 .winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
138 }
139}
140
141pub fn handleInputEvent(app: *App, event: App.Event) !void {
142 switch (event) {
143 .key_press => |key| {
144 switch (key.codepoint) {
145 Key.escape => {
146 switch (app.state) {
147 .fuzzy => {
148 try app.repopulateDirectory("");
149 app.text_input.clearAndFree();
150 },
151 .command => app.command_history.cursor = null,
152 else => {},
153 }
154
155 app.text_input.clearAndFree();
156 app.state = .normal;
157 },
158 Key.enter => {
159 const selected = app.directories.entries.selected;
160 switch (app.state) {
161 .new_dir => try events.createNewDir(app),
162 .new_file => try events.createNewFile(app),
163 .rename => try events.rename(app),
164 .change_dir => {
165 const path = app.inputToSlice();
166 try commands.cd(app, path);
167 app.text_input.clearAndFree();
168 },
169 .command => {
170 const command = app.inputToSlice();
171
172 // Push command to history if it's not empty.
173 if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) {
174 app.command_history.add(command, app.alloc) catch |err| {
175 const message = try std.fmt.allocPrint(app.alloc, "Failed to add command to history - {}.", .{err});
176 defer app.alloc.free(message);
177 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
178 };
179 }
180
181 supported: {
182 if (std.mem.eql(u8, command, ":q")) {
183 app.should_quit = true;
184 return;
185 }
186
187 if (std.mem.eql(u8, command, ":config")) {
188 try commands.config(app);
189 break :supported;
190 }
191
192 if (std.mem.eql(u8, command, ":trash")) {
193 try commands.trash(app);
194 break :supported;
195 }
196
197 if (std.mem.startsWith(u8, command, ":cd ")) {
198 try commands.cd(app, command[":cd ".len..]);
199 break :supported;
200 }
201
202 if (std.mem.eql(u8, command, ":empty_trash")) {
203 try commands.emptyTrash(app);
204 break :supported;
205 }
206
207 if (std.mem.eql(u8, command, ":h")) {
208 app.state = .help_menu;
209 break :supported;
210 }
211
212 app.text_input.clearAndFree();
213 try app.text_input.insertSliceAtCursor(":UnsupportedCommand");
214 }
215
216 app.command_history.cursor = null;
217 },
218 else => {},
219 }
220
221 if (app.state != .help_menu) app.state = .normal;
222 app.directories.entries.selected = selected;
223 },
224 Key.left => app.text_input.cursorLeft(),
225 Key.right => app.text_input.cursorRight(),
226 Key.up => {
227 if (app.state == .command) {
228 if (app.command_history.previous()) |command| {
229 app.text_input.clearAndFree();
230 app.text_input.insertSliceAtCursor(command) catch |err| {
231 const message = try std.fmt.allocPrint(app.alloc, "Failed to get previous command history - {}.", .{err});
232 defer app.alloc.free(message);
233 app.notification.write(message, .err) catch {};
234 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
235 };
236 }
237 }
238 },
239 Key.down => {
240 if (app.state == .command) {
241 app.text_input.clearAndFree();
242 if (app.command_history.next()) |command| {
243 app.text_input.insertSliceAtCursor(command) catch |err| {
244 const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err});
245 defer app.alloc.free(message);
246 app.notification.write(message, .err) catch {};
247 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
248 };
249 } else {
250 app.text_input.insertSliceAtCursor(":") catch |err| {
251 const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err});
252 defer app.alloc.free(message);
253 app.notification.write(message, .err) catch {};
254 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
255 };
256 }
257 }
258 },
259 else => {
260 try app.text_input.update(.{ .key_press = key });
261
262 switch (app.state) {
263 .fuzzy => {
264 const fuzzy = app.inputToSlice();
265 try app.repopulateDirectory(fuzzy);
266 },
267 .command => {
268 const command = app.inputToSlice();
269 if (!std.mem.startsWith(u8, command, ":")) {
270 app.text_input.clearAndFree();
271 app.text_input.insertSliceAtCursor(":") catch |err| {
272 app.state = .normal;
273
274 const message = try std.fmt.allocPrint(app.alloc, "An input error occurred while attempting to enter a command - {}.", .{err});
275 defer app.alloc.free(message);
276 app.notification.write(message, .err) catch {};
277 if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
278 };
279 }
280 },
281 else => {},
282 }
283 },
284 }
285 },
286 .image_ready => {},
287 .notification => {},
288 .winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
289 }
290}
291
292pub fn handleHelpMenuEvent(app: *App, event: App.Event) !void {
293 switch (event) {
294 .key_press => |key| {
295 switch (key.codepoint) {
296 Key.escape, 'q' => app.state = .normal,
297 'j', Key.down => app.help_menu.next(),
298 'k', Key.up => app.help_menu.previous(),
299 else => {},
300 }
301 },
302 .image_ready => {},
303 .notification => {},
304 .winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
305 }
306}