+29
-11
README.md
+29
-11
README.md
···
28
- A terminal supporting the `kitty image protocol` to view images.
29
30
## Key manual
31
```
32
Global:
33
<CTRL-c> :Exit.
···
65
:cd <path> :Change directory via path. Will enter input mode.
66
```
67
68
-
69
## Configuration
70
Configure `jido` by editing the external configuration file located at either:
71
- `$HOME/.jido/config.json`
···
84
.show_images: bool,
85
.preview_file: bool,
86
.empty_trash_on_exit: bool,
87
-
.styles: Styles,
88
}
89
90
NotificationStyles = struct {
91
-
box: vaxis.Style,
92
-
err: vaxis.Style,
93
-
warn: vaxis.Style,
94
-
info: vaxis.Style,
95
-
};
96
97
Styles = struct {
98
.selected_list_item: Style,
···
100
.file_name: Style,
101
.file_information: Style
102
.notification: NotificationStyles,
103
-
.git_branch: Style,
104
}
105
106
Style = struct {
···
113
double,
114
curly,
115
dotted,
116
-
dashed,
117
}
118
.bold: bool,
119
.dim: bool,
···
121
.blink: bool,
122
.reverse: bool,
123
.invisible: bool,
124
-
.strikethrough: bool,
125
}
126
127
Color = enum{
128
default,
129
index: u8,
130
-
rgb: [3]u8,
131
}
132
```
133
134
## Contributing
···
28
- A terminal supporting the `kitty image protocol` to view images.
29
30
## Key manual
31
+
Below are the default keybinds. Keybinds can be overwritten via the `Keybinds`
32
+
config option. See [Configuration](#configuration) for more information.
33
+
34
```
35
Global:
36
<CTRL-c> :Exit.
···
68
:cd <path> :Change directory via path. Will enter input mode.
69
```
70
71
## Configuration
72
Configure `jido` by editing the external configuration file located at either:
73
- `$HOME/.jido/config.json`
···
86
.show_images: bool,
87
.preview_file: bool,
88
.empty_trash_on_exit: bool,
89
+
.keybinds: Keybinds,
90
+
.styles: Styles
91
+
}
92
+
93
+
Keybinds = struct {
94
+
.toggle_hidden_files: Char,
95
+
.delete: Char,
96
+
.rename: Char,
97
+
.create_dir: Char,
98
+
.create_file: Char,
99
+
.fuzzy_find: Char,
100
+
.change_dir: Char,
101
+
.enter_command_mode: Char
102
+
.jump_top: Char
103
+
.jump_bottom: Char
104
}
105
106
NotificationStyles = struct {
107
+
.box: vaxis.Style,
108
+
.err: vaxis.Style,
109
+
.warn: vaxis.Style,
110
+
.info: vaxis.Style
111
+
}
112
113
Styles = struct {
114
.selected_list_item: Style,
···
116
.file_name: Style,
117
.file_information: Style
118
.notification: NotificationStyles,
119
+
.git_branch: Style
120
}
121
122
Style = struct {
···
129
double,
130
curly,
131
dotted,
132
+
dashed
133
}
134
.bold: bool,
135
.dim: bool,
···
137
.blink: bool,
138
.reverse: bool,
139
.invisible: bool,
140
+
.strikethrough: bool
141
}
142
143
Color = enum{
144
default,
145
index: u8,
146
+
rgb: [3]u8
147
}
148
+
149
+
Char = enum(u21)
150
```
151
152
## Contributing
+3
example-config.json
+3
example-config.json
+57
-4
src/config.zig
+57
-4
src/config.zig
···
17
empty_trash_on_exit: bool = false,
18
// TODO(10-01-25): This needs to be implemented.
19
// command_history_len: usize = 10,
20
-
styles: Styles = Styles{},
21
22
config_dir: ?std.fs.Dir = null,
23
···
51
try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME);
52
}
53
54
-
const jido_dir = try home_dir.openDir(XDG_CONFIG_HOME_DIR_NAME, .{ .iterate = true });
55
self.config_dir = jido_dir;
56
57
if (environment.fileExists(jido_dir, CONFIG_NAME)) {
···
70
try home_dir.makeDir(HOME_DIR_NAME);
71
}
72
73
-
const jido_dir = try home_dir.openDir(HOME_DIR_NAME, .{ .iterate = true });
74
self.config_dir = jido_dir;
75
76
if (environment.fileExists(jido_dir, CONFIG_NAME)) {
···
93
94
self.* = parsed_config.value;
95
self.config_dir = dir;
96
return;
97
}
98
};
···
125
},
126
};
127
128
const Styles = struct {
129
selected_list_item: vaxis.Style = vaxis.Style{
130
.bg = .{ .rgb = Colours.grey },
···
144
},
145
};
146
147
-
pub var config: Config = Config{ .styles = Styles{} };
···
17
empty_trash_on_exit: bool = false,
18
// TODO(10-01-25): This needs to be implemented.
19
// command_history_len: usize = 10,
20
+
styles: Styles = .{},
21
+
keybinds: Keybinds = .{},
22
23
config_dir: ?std.fs.Dir = null,
24
···
52
try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME);
53
}
54
55
+
const jido_dir = try home_dir.openDir(
56
+
XDG_CONFIG_HOME_DIR_NAME,
57
+
.{ .iterate = true },
58
+
);
59
self.config_dir = jido_dir;
60
61
if (environment.fileExists(jido_dir, CONFIG_NAME)) {
···
74
try home_dir.makeDir(HOME_DIR_NAME);
75
}
76
77
+
const jido_dir = try home_dir.openDir(
78
+
HOME_DIR_NAME,
79
+
.{ .iterate = true },
80
+
);
81
self.config_dir = jido_dir;
82
83
if (environment.fileExists(jido_dir, CONFIG_NAME)) {
···
100
101
self.* = parsed_config.value;
102
self.config_dir = dir;
103
+
104
+
// Check duplicate keybinds
105
+
{
106
+
var key_map = std.AutoHashMap(u21, []const u8).init(alloc);
107
+
defer {
108
+
var it = key_map.iterator();
109
+
while (it.next()) |entry| {
110
+
alloc.free(entry.value_ptr.*);
111
+
}
112
+
key_map.deinit();
113
+
}
114
+
115
+
inline for (std.meta.fields(Keybinds)) |field| {
116
+
const codepoint = @intFromEnum(@field(self.keybinds, field.name));
117
+
118
+
const res = try key_map.getOrPut(codepoint);
119
+
if (res.found_existing) {
120
+
return error.DuplicateKeybind;
121
+
}
122
+
res.value_ptr.* = try alloc.dupe(u8, field.name);
123
+
}
124
+
}
125
+
126
return;
127
}
128
};
···
155
},
156
};
157
158
+
pub const Keybinds = struct {
159
+
pub const Char = enum(u21) {
160
+
_,
161
+
pub fn jsonParse(alloc: std.mem.Allocator, source: anytype, options: std.json.ParseOptions) !@This() {
162
+
const parsed = try std.json.innerParse([]const u8, alloc, source, options);
163
+
if (std.mem.eql(u8, parsed, "")) return error.InvalidCharacter;
164
+
const unicode = std.unicode.utf8Decode(parsed) catch return error.InvalidCharacter;
165
+
return @enumFromInt(unicode);
166
+
}
167
+
};
168
+
169
+
toggle_hidden_files: Char = @enumFromInt('.'),
170
+
delete: Char = @enumFromInt('D'),
171
+
rename: Char = @enumFromInt('R'),
172
+
create_dir: Char = @enumFromInt('d'),
173
+
create_file: Char = @enumFromInt('%'),
174
+
fuzzy_find: Char = @enumFromInt('/'),
175
+
change_dir: Char = @enumFromInt('c'),
176
+
enter_command_mode: Char = @enumFromInt(':'),
177
+
jump_top: Char = @enumFromInt('g'),
178
+
jump_bottom: Char = @enumFromInt('G'),
179
+
};
180
+
181
const Styles = struct {
182
selected_list_item: vaxis.Style = vaxis.Style{
183
.bg = .{ .rgb = Colours.grey },
···
197
},
198
};
199
200
+
pub var config: Config = Config{};
+257
-237
src/event_handlers.zig
+257
-237
src/event_handlers.zig
···
6
const Key = vaxis.Key;
7
const config = &@import("./config.zig").config;
8
const commands = @import("./commands.zig");
9
10
pub fn inputToSlice(self: *App) []const u8 {
11
self.text_input.buf.cursor = self.text_input.buf.realLength();
···
19
) !void {
20
switch (event) {
21
.key_press => |key| {
22
-
switch (key.codepoint) {
23
-
'-', 'h', Key.left => {
24
-
app.text_input.clearAndFree();
25
26
-
if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| {
27
-
app.directories.dir.close();
28
-
app.directories.dir = dir;
29
30
app.directories.clearEntries();
31
-
const fuzzy = inputToSlice(app);
32
-
app.directories.populateEntries(fuzzy) catch |err| {
33
switch (err) {
34
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
35
else => try app.notification.writeErr(.UnknownError),
36
}
37
};
38
39
-
if (app.directories.history.pop()) |history| {
40
-
if (history.selected < app.directories.entries.len()) {
41
-
app.directories.entries.selected = history.selected;
42
-
app.directories.entries.offset = history.offset;
43
-
}
44
}
45
-
} else |err| {
46
-
switch (err) {
47
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
48
-
else => try app.notification.writeErr(.UnknownError),
49
-
}
50
-
}
51
-
},
52
-
Key.enter, 'l', Key.right => {
53
-
const entry = lbl: {
54
-
const entry = app.directories.getSelected() catch return;
55
-
if (entry) |e| break :lbl e else return;
56
-
};
57
58
-
switch (entry.kind) {
59
-
.directory => {
60
-
app.text_input.clearAndFree();
61
62
-
if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| {
63
-
app.directories.dir.close();
64
-
app.directories.dir = dir;
65
66
-
_ = app.directories.history.push(.{
67
-
.selected = app.directories.entries.selected,
68
-
.offset = app.directories.entries.offset,
69
-
});
70
71
-
app.directories.clearEntries();
72
-
const fuzzy = inputToSlice(app);
73
-
app.directories.populateEntries(fuzzy) catch |err| {
74
-
switch (err) {
75
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
76
-
else => try app.notification.writeErr(.UnknownError),
77
-
}
78
-
};
79
-
} else |err| {
80
-
switch (err) {
81
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
82
-
else => try app.notification.writeErr(.UnknownError),
83
-
}
84
}
85
-
},
86
-
.file => {
87
-
if (environment.getEditor()) |editor| {
88
-
try app.vx.exitAltScreen(app.tty.anyWriter());
89
-
try app.vx.resetState(app.tty.anyWriter());
90
-
loop.stop();
91
92
-
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch {
93
-
try app.notification.writeErr(.UnableToOpenFile);
94
-
};
95
96
-
try loop.start();
97
-
try app.vx.enterAltScreen(app.tty.anyWriter());
98
-
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
99
-
app.vx.queueRefresh();
100
-
} else {
101
-
try app.notification.writeErr(.EditorNotSet);
102
}
103
-
},
104
-
else => {},
105
-
}
106
-
},
107
-
'j', Key.down => {
108
-
app.directories.entries.next(app.last_known_height);
109
-
},
110
-
'k', Key.up => {
111
-
app.directories.entries.previous(app.last_known_height);
112
-
},
113
-
'G' => {
114
-
app.directories.entries.selectLast(app.last_known_height);
115
-
},
116
-
'g' => app.directories.entries.selectFirst(),
117
-
'D' => {
118
-
const entry = lbl: {
119
-
const entry = app.directories.getSelected() catch {
120
-
try app.notification.writeErr(.UnableToDelete);
121
return;
122
};
123
-
if (entry) |e| break :lbl e else return;
124
-
};
125
-
126
-
var old_path_buf: [std.fs.max_path_bytes]u8 = undefined;
127
-
const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf));
128
-
129
-
var trash_dir = dir: {
130
-
notfound: {
131
-
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
132
-
}
133
-
app.alloc.free(old_path);
134
-
try app.notification.writeErr(.ConfigPathNotFound);
135
-
return;
136
-
};
137
-
defer trash_dir.close();
138
-
var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined;
139
-
const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf);
140
141
-
if (std.mem.eql(u8, old_path, trash_dir_path)) {
142
-
try app.notification.writeErr(.CannotDeleteTrashDir);
143
-
app.alloc.free(old_path);
144
-
return;
145
-
}
146
147
-
var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
148
-
const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4().toArray() }));
149
150
-
if (app.directories.dir.rename(entry.name, tmp_path)) {
151
-
if (app.actions.push(.{
152
-
.delete = .{ .old = old_path, .new = tmp_path },
153
-
})) |prev_elem| {
154
-
app.alloc.free(prev_elem.delete.old);
155
-
app.alloc.free(prev_elem.delete.new);
156
}
157
158
-
try app.notification.writeInfo(.Deleted);
159
-
app.directories.removeSelected();
160
-
} else |err| {
161
-
switch (err) {
162
-
error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints),
163
-
else => try app.notification.writeErr(.UnableToDelete),
164
-
}
165
-
app.alloc.free(old_path);
166
-
app.alloc.free(tmp_path);
167
-
}
168
-
},
169
-
'd' => {
170
-
app.text_input.clearAndFree();
171
-
app.directories.clearEntries();
172
-
app.directories.populateEntries("") catch |err| {
173
-
switch (err) {
174
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
175
-
else => try app.notification.writeErr(.UnknownError),
176
-
}
177
-
};
178
-
app.state = .new_dir;
179
-
},
180
-
'%' => {
181
-
app.text_input.clearAndFree();
182
-
app.directories.clearEntries();
183
-
app.directories.populateEntries("") catch |err| {
184
-
switch (err) {
185
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
186
-
else => try app.notification.writeErr(.UnknownError),
187
-
}
188
-
};
189
-
app.state = .new_file;
190
-
},
191
-
'u' => {
192
-
if (app.actions.pop()) |action| {
193
-
const selected = app.directories.entries.selected;
194
195
-
switch (action) {
196
-
.delete => |a| {
197
-
defer app.alloc.free(a.new);
198
-
defer app.alloc.free(a.old);
199
200
-
// TODO: Will overwrite an item if it has the same name.
201
-
if (app.directories.dir.rename(a.new, a.old)) {
202
app.directories.clearEntries();
203
const fuzzy = inputToSlice(app);
204
app.directories.populateEntries(fuzzy) catch |err| {
···
207
else => try app.notification.writeErr(.UnknownError),
208
}
209
};
210
-
try app.notification.writeInfo(.RestoredDelete);
211
-
} else |_| {
212
-
try app.notification.writeErr(.UnableToUndo);
213
}
214
},
215
-
.rename => |a| {
216
-
defer app.alloc.free(a.new);
217
-
defer app.alloc.free(a.old);
218
219
-
// TODO: Will overwrite an item if it has the same name.
220
-
if (app.directories.dir.rename(a.new, a.old)) {
221
-
app.directories.clearEntries();
222
-
const fuzzy = inputToSlice(app);
223
-
app.directories.populateEntries(fuzzy) catch |err| {
224
-
switch (err) {
225
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
226
-
else => try app.notification.writeErr(.UnknownError),
227
-
}
228
};
229
-
try app.notification.writeInfo(.RestoredRename);
230
-
} else |_| {
231
-
try app.notification.writeErr(.UnableToUndo);
232
}
233
},
234
}
235
236
-
app.directories.entries.selected = selected;
237
-
} else {
238
-
try app.notification.writeInfo(.EmptyUndo);
239
-
}
240
-
},
241
-
'/' => {
242
-
app.text_input.clearAndFree();
243
-
app.state = .fuzzy;
244
-
},
245
-
'R' => {
246
-
app.text_input.clearAndFree();
247
-
app.state = .rename;
248
249
-
const entry = lbl: {
250
-
const entry = app.directories.getSelected() catch {
251
-
app.state = .normal;
252
-
try app.notification.writeErr(.UnableToRename);
253
-
return;
254
-
};
255
-
if (entry) |e| break :lbl e else {
256
-
app.state = .normal;
257
-
return;
258
-
}
259
-
};
260
261
-
app.text_input.insertSliceAtCursor(entry.name) catch {
262
-
app.state = .normal;
263
-
try app.notification.writeErr(.UnableToRename);
264
-
return;
265
-
};
266
-
},
267
-
'c' => {
268
-
app.text_input.clearAndFree();
269
-
app.state = .change_dir;
270
-
},
271
-
':' => {
272
-
app.text_input.clearAndFree();
273
-
app.text_input.insertSliceAtCursor(":") catch {};
274
-
app.state = .command;
275
-
},
276
-
'.' => {
277
-
config.show_hidden = !config.show_hidden;
278
279
-
const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: {
280
-
const selected = app.directories.getSelected() catch break :lbl .{ "", true };
281
-
if (selected == null) break :lbl .{ "", true };
282
-
283
-
break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false };
284
-
};
285
-
defer if (!prev_selected_err) app.alloc.free(prev_selected_name);
286
-
287
-
app.directories.clearEntries();
288
-
app.directories.populateEntries("") catch |err| {
289
-
switch (err) {
290
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
291
-
else => try app.notification.writeErr(.UnknownError),
292
}
293
-
};
294
-
295
-
for (app.directories.entries.all()) |entry| {
296
-
// Update offset as we search for last selected entry.
297
-
app.directories.entries.updateOffset(app.last_known_height, .next);
298
-
if (std.mem.eql(u8, entry.name, prev_selected_name)) return;
299
-
app.directories.entries.selected += 1;
300
-
}
301
-
302
-
// If it didn't find entry, reset selected.
303
-
app.directories.entries.selected = 0;
304
-
},
305
-
else => {},
306
}
307
},
308
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
···
6
const Key = vaxis.Key;
7
const config = &@import("./config.zig").config;
8
const commands = @import("./commands.zig");
9
+
const Keybinds = @import("./config.zig").Keybinds;
10
11
pub fn inputToSlice(self: *App) []const u8 {
12
self.text_input.buf.cursor = self.text_input.buf.realLength();
···
20
) !void {
21
switch (event) {
22
.key_press => |key| {
23
+
@setEvalBranchQuota(
24
+
std.meta.fields(Keybinds).len * 1000,
25
+
);
26
+
27
+
const maybe_remap: ?std.meta.FieldEnum(Keybinds) = lbl: {
28
+
inline for (std.meta.fields(Keybinds)) |field| {
29
+
if (key.codepoint == @intFromEnum(@field(config.keybinds, field.name))) {
30
+
break :lbl comptime std.meta.stringToEnum(std.meta.FieldEnum(Keybinds), field.name) orelse unreachable;
31
+
}
32
+
}
33
+
break :lbl null;
34
+
};
35
+
36
+
if (maybe_remap) |action| {
37
+
switch (action) {
38
+
.toggle_hidden_files => {
39
+
config.show_hidden = !config.show_hidden;
40
+
41
+
const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: {
42
+
const selected = app.directories.getSelected() catch break :lbl .{ "", true };
43
+
if (selected == null) break :lbl .{ "", true };
44
45
+
break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false };
46
+
};
47
+
defer if (!prev_selected_err) app.alloc.free(prev_selected_name);
48
49
app.directories.clearEntries();
50
+
app.text_input.clearAndFree();
51
+
app.directories.populateEntries("") catch |err| {
52
switch (err) {
53
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
54
else => try app.notification.writeErr(.UnknownError),
55
}
56
};
57
58
+
for (app.directories.entries.all()) |entry| {
59
+
// Update offset as we search for last selected entry.
60
+
app.directories.entries.updateOffset(app.last_known_height, .next);
61
+
if (std.mem.eql(u8, entry.name, prev_selected_name)) return;
62
+
app.directories.entries.selected += 1;
63
}
64
65
+
// If it didn't find entry, reset selected.
66
+
app.directories.entries.selected = 0;
67
+
},
68
+
.delete => {
69
+
const entry = lbl: {
70
+
const entry = app.directories.getSelected() catch {
71
+
try app.notification.writeErr(.UnableToDelete);
72
+
return;
73
+
};
74
+
if (entry) |e| break :lbl e else return;
75
+
};
76
77
+
var old_path_buf: [std.fs.max_path_bytes]u8 = undefined;
78
+
const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf));
79
80
+
var trash_dir = dir: {
81
+
notfound: {
82
+
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
83
+
}
84
+
app.alloc.free(old_path);
85
+
try app.notification.writeErr(.ConfigPathNotFound);
86
+
return;
87
+
};
88
+
defer trash_dir.close();
89
+
var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined;
90
+
const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf);
91
92
+
if (std.mem.eql(u8, old_path, trash_dir_path)) {
93
+
try app.notification.writeErr(.CannotDeleteTrashDir);
94
+
app.alloc.free(old_path);
95
+
return;
96
+
}
97
+
98
+
var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
99
+
const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4() }));
100
+
101
+
if (app.directories.dir.rename(entry.name, tmp_path)) {
102
+
if (app.actions.push(.{
103
+
.delete = .{ .old = old_path, .new = tmp_path },
104
+
})) |prev_elem| {
105
+
app.alloc.free(prev_elem.delete.old);
106
+
app.alloc.free(prev_elem.delete.new);
107
}
108
109
+
try app.notification.writeInfo(.Deleted);
110
+
app.directories.removeSelected();
111
+
} else |err| {
112
+
switch (err) {
113
+
error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints),
114
+
else => try app.notification.writeErr(.UnableToDelete),
115
+
}
116
+
app.alloc.free(old_path);
117
+
app.alloc.free(tmp_path);
118
+
}
119
+
},
120
+
.rename => {
121
+
app.text_input.clearAndFree();
122
+
app.state = .rename;
123
124
+
const entry = lbl: {
125
+
const entry = app.directories.getSelected() catch {
126
+
app.state = .normal;
127
+
try app.notification.writeErr(.UnableToRename);
128
+
return;
129
+
};
130
+
if (entry) |e| break :lbl e else {
131
+
app.state = .normal;
132
+
return;
133
}
134
+
};
135
+
136
+
app.text_input.insertSliceAtCursor(entry.name) catch {
137
+
app.state = .normal;
138
+
try app.notification.writeErr(.UnableToRename);
139
return;
140
};
141
+
},
142
+
.create_dir => {
143
+
app.text_input.clearAndFree();
144
+
app.directories.clearEntries();
145
+
app.directories.populateEntries("") catch |err| {
146
+
switch (err) {
147
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
148
+
else => try app.notification.writeErr(.UnknownError),
149
+
}
150
+
};
151
+
app.state = .new_dir;
152
+
},
153
+
.create_file => {
154
+
app.text_input.clearAndFree();
155
+
app.directories.clearEntries();
156
+
app.directories.populateEntries("") catch |err| {
157
+
switch (err) {
158
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
159
+
else => try app.notification.writeErr(.UnknownError),
160
+
}
161
+
};
162
+
app.state = .new_file;
163
+
},
164
+
.fuzzy_find => {
165
+
app.text_input.clearAndFree();
166
+
app.state = .fuzzy;
167
+
},
168
+
.change_dir => {
169
+
app.text_input.clearAndFree();
170
+
app.state = .change_dir;
171
+
},
172
+
.enter_command_mode => {
173
+
app.text_input.clearAndFree();
174
+
app.text_input.insertSliceAtCursor(":") catch {};
175
+
app.state = .command;
176
+
},
177
+
.jump_bottom => {
178
+
app.directories.entries.selectLast(app.last_known_height);
179
+
},
180
+
.jump_top => app.directories.entries.selectFirst(),
181
+
}
182
+
} else {
183
+
switch (key.codepoint) {
184
+
'-', 'h', Key.left => {
185
+
app.text_input.clearAndFree();
186
187
+
if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| {
188
+
app.directories.dir.close();
189
+
app.directories.dir = dir;
190
191
+
app.directories.clearEntries();
192
+
const fuzzy = inputToSlice(app);
193
+
app.directories.populateEntries(fuzzy) catch |err| {
194
+
switch (err) {
195
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
196
+
else => try app.notification.writeErr(.UnknownError),
197
+
}
198
+
};
199
200
+
if (app.directories.history.pop()) |history| {
201
+
if (history.selected < app.directories.entries.len()) {
202
+
app.directories.entries.selected = history.selected;
203
+
app.directories.entries.offset = history.offset;
204
+
}
205
+
}
206
+
} else |err| {
207
+
switch (err) {
208
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
209
+
else => try app.notification.writeErr(.UnknownError),
210
+
}
211
}
212
+
},
213
+
Key.enter, 'l', Key.right => {
214
+
const entry = lbl: {
215
+
const entry = app.directories.getSelected() catch return;
216
+
if (entry) |e| break :lbl e else return;
217
+
};
218
219
+
switch (entry.kind) {
220
+
.directory => {
221
+
app.text_input.clearAndFree();
222
+
223
+
if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| {
224
+
app.directories.dir.close();
225
+
app.directories.dir = dir;
226
227
+
_ = app.directories.history.push(.{
228
+
.selected = app.directories.entries.selected,
229
+
.offset = app.directories.entries.offset,
230
+
});
231
232
app.directories.clearEntries();
233
const fuzzy = inputToSlice(app);
234
app.directories.populateEntries(fuzzy) catch |err| {
···
237
else => try app.notification.writeErr(.UnknownError),
238
}
239
};
240
+
} else |err| {
241
+
switch (err) {
242
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
243
+
else => try app.notification.writeErr(.UnknownError),
244
+
}
245
}
246
},
247
+
.file => {
248
+
if (environment.getEditor()) |editor| {
249
+
try app.vx.exitAltScreen(app.tty.anyWriter());
250
+
try app.vx.resetState(app.tty.anyWriter());
251
+
loop.stop();
252
253
+
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch {
254
+
try app.notification.writeErr(.UnableToOpenFile);
255
};
256
+
257
+
try loop.start();
258
+
try app.vx.enterAltScreen(app.tty.anyWriter());
259
+
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
260
+
app.vx.queueRefresh();
261
+
} else {
262
+
try app.notification.writeErr(.EditorNotSet);
263
}
264
},
265
+
else => {},
266
}
267
+
},
268
+
'j', Key.down => {
269
+
app.directories.entries.next(app.last_known_height);
270
+
},
271
+
'k', Key.up => {
272
+
app.directories.entries.previous(app.last_known_height);
273
+
},
274
+
'u' => {
275
+
if (app.actions.pop()) |action| {
276
+
const selected = app.directories.entries.selected;
277
278
+
switch (action) {
279
+
.delete => |a| {
280
+
defer app.alloc.free(a.new);
281
+
defer app.alloc.free(a.old);
282
283
+
// TODO: Will overwrite an item if it has the same name.
284
+
if (app.directories.dir.rename(a.new, a.old)) {
285
+
app.directories.clearEntries();
286
+
const fuzzy = inputToSlice(app);
287
+
app.directories.populateEntries(fuzzy) catch |err| {
288
+
switch (err) {
289
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
290
+
else => try app.notification.writeErr(.UnknownError),
291
+
}
292
+
};
293
+
try app.notification.writeInfo(.RestoredDelete);
294
+
} else |_| {
295
+
try app.notification.writeErr(.UnableToUndo);
296
+
}
297
+
},
298
+
.rename => |a| {
299
+
defer app.alloc.free(a.new);
300
+
defer app.alloc.free(a.old);
301
302
+
// TODO: Will overwrite an item if it has the same name.
303
+
if (app.directories.dir.rename(a.new, a.old)) {
304
+
app.directories.clearEntries();
305
+
const fuzzy = inputToSlice(app);
306
+
app.directories.populateEntries(fuzzy) catch |err| {
307
+
switch (err) {
308
+
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
309
+
else => try app.notification.writeErr(.UnknownError),
310
+
}
311
+
};
312
+
try app.notification.writeInfo(.RestoredRename);
313
+
} else |_| {
314
+
try app.notification.writeErr(.UnableToUndo);
315
+
}
316
+
},
317
+
}
318
319
+
app.directories.entries.selected = selected;
320
+
} else {
321
+
try app.notification.writeInfo(.EmptyUndo);
322
}
323
+
},
324
+
else => {},
325
+
}
326
}
327
},
328
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
+6
src/main.zig
+6
src/main.zig
···
30
error.SyntaxError => {
31
try app.notification.writeErr(.ConfigSyntaxError);
32
},
33
+
error.InvalidCharacter => {
34
+
try app.notification.writeErr(.InvalidKeybind);
35
+
},
36
+
error.DuplicateKeybind => {
37
+
try app.notification.writeErr(.DuplicateKeybinds);
38
+
},
39
else => {
40
try app.notification.writeErr(.ConfigUnknownError);
41
},
+4
src/notification.zig
+4
src/notification.zig
···
28
ConfigUnknownError,
29
ConfigPathNotFound,
30
CannotDeleteTrashDir,
31
NotADir,
32
};
33
···
82
.ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err),
83
.ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err),
84
.CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err),
85
};
86
}
87
···
28
ConfigUnknownError,
29
ConfigPathNotFound,
30
CannotDeleteTrashDir,
31
+
DuplicateKeybinds,
32
+
InvalidKeybind,
33
NotADir,
34
};
35
···
84
.ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err),
85
.ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err),
86
.CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err),
87
+
.DuplicateKeybinds => self.write("Config has keybinds with the same key. This can lead to undefined behaviour. Check log file for more information.", .err),
88
+
.InvalidKeybind => self.write("Config has keybind(s) with invalid key(s).", .err),
89
};
90
}
91