+9
.gila/todo/grave_zone_233/grave_zone_233.md
+9
.gila/todo/grave_zone_233/grave_zone_233.md
···
1
+
---
2
+
title: feat: support cursor placement in input
3
+
status: todo
4
+
priority_value: 50
5
+
priority: high
6
+
owner: bjeyn
7
+
created: 2026-01-11T21:53:52Z
8
+
---
9
+
Currently the user cannot move their cursor left or right when typing in the input field. This is painful when renaming files or changing directories.
+9
.gila/todo/intelligent_dino_17y/intelligent_dino_17y.md
+9
.gila/todo/intelligent_dino_17y/intelligent_dino_17y.md
+1
-1
.github/workflows/create-draft-release.yml
+1
-1
.github/workflows/create-draft-release.yml
+29
.github/workflows/mirror.yml
+29
.github/workflows/mirror.yml
···
1
+
name: Mirror to tangled
2
+
3
+
on:
4
+
push:
5
+
branches:
6
+
- main
7
+
8
+
jobs:
9
+
mirror:
10
+
runs-on: ubuntu-latest
11
+
12
+
steps:
13
+
- name: Checkout source repo
14
+
uses: actions/checkout@v4
15
+
with:
16
+
fetch-depth: 0
17
+
18
+
- name: Set up SSH key
19
+
run: |
20
+
mkdir -p ~/.ssh
21
+
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
22
+
chmod 600 ~/.ssh/id_ed25519
23
+
ssh-keyscan -H tangled.sh >> ~/.ssh/known_hosts
24
+
shell: bash
25
+
26
+
- name: Mirror
27
+
run: |
28
+
git remote add tangled git@tangled.sh:brookjeynes.dev/jido
29
+
git push --mirror tangled
+230
CHANGELOG.md
+230
CHANGELOG.md
···
1
+
# Changelog
2
+
3
+
## v1.3.0 (2025-05-26)
4
+
- feat: add `--choose-dir` arg
5
+
- feat: add `--entry-dir=PATH` arg
6
+
7
+
## v1.2.0 (2025-05-26)
8
+
- feat(images): Cache images to avoid unecessary re-processing
9
+
10
+
## v1.1.0 (2025-05-21)
11
+
- fix(images): Improve performance by only locking critical parts of image loading
12
+
- fix(images): Thread the image loading process as not to block user input
13
+
14
+
## v1.0.1 (2025-04-14)
15
+
- fix(errors): Ensure logged enums are wrapped in `@tagName()` for readability.
16
+
17
+
## v1.0.0 (2025-04-06)
18
+
- New Keybinds:
19
+
- Added ability to copy files.
20
+
This is done by (y)anking the file, then (p)asting in the desired directory.
21
+
This action can be (u)ndone and behind the scenes is a deletion.
22
+
Currently this feature only supports files, folders, and symlinks.
23
+
- Added force delete keybind. It's unbound by default.
24
+
- Added keybind `v` to view additional information about the selected entry.
25
+
- A huge audit of `try` usages was conducted. As a result of this, Jido is much
26
+
more resiliant to errors and should crash less often in known cases.
27
+
- Added `:h` command to view help / keybind menu.
28
+
- Added config option `true_dir_size` to see the true size of directories.
29
+
- Added [-v | --version] and [-h | --help] args.
30
+
- File permissions are now displayed in the file information bar to the bottom
31
+
of Jido.
32
+
- Keybinds can now be unbound. Some keybinds are now unbound by default.
33
+
See [Configuration](https://github.com/BrookJeynes/jido?tab=readme-ov-file#configuration)
34
+
for more information.
35
+
- Fixes:
36
+
- fix: Scrolling command history now provides the correct values.
37
+
- fix: Ensure complete Git branch is displayed. Previously if the branch
38
+
contained slashes, it would only retrieve the ending split.
39
+
- fix: Allow the cursor to be moved left and right on text input.
40
+
- fix: The keybind " " (spacebar) is now accepted by the config.
41
+
- fix: Multi-char keybinds now throw errors instead of crashing.
42
+
- fix: Undoing a delete/rename wont overwrite an item with the same name now.
43
+
44
+
## v0.9.9 (2025-04-06)
45
+
- feat: Added ability to copy folders.
46
+
- fix: Scrolling command history now provides the correct values.
47
+
48
+
## v0.9.8 (2025-04-04)
49
+
- fix: Ensure complete Git branch is displayed.
50
+
- refactor: Audit try usage to improve system resiliance.
51
+
- refactor: Removed need for enum based notifications.
52
+
53
+
## v0.9.7 (2025-04-01)
54
+
- feat: Added ability to copy files.
55
+
This is done by (y)anking the file, then (p)asting in the desired directory.
56
+
This action can be (u)ndone and behind the scenes is a deletion.
57
+
- fix: Allow the cursor to be moved left and right.
58
+
- refactor: Changed action struct field names to be more clear.
59
+
- refactor: Better ergonomics around writing to the log file.
60
+
61
+
## v0.9.6 (2025-03-31)
62
+
- feat: Added ability to unbound keybinds.
63
+
- feat: Added force delete keybind. It's unbound by default.
64
+
65
+
## v0.9.5 (2025-03-29)
66
+
- feat: Added [-v | --version] and [-h | --help] args.
67
+
68
+
## v0.9.4 (2025-03-29)
69
+
- feat: Added keybind `h` to view help / keybind menu.
70
+
- refactor: `List` drawing logic is now handled by the `Drawer{}`.
71
+
72
+
## v0.9.3 (2025-03-27)
73
+
- feat: The keybind " " is now accepted. This allows spacebar to be bound.
74
+
- feat: Duplicate keybind notification now includes additional information.
75
+
- fix: Multi-char keybinds now throw errors instead of crashing.
76
+
- fix: Remove need to init notification handler. This fixes many issues with
77
+
the places in the code notifications could be produced.
78
+
79
+
## v0.9.2 (2025-03-25)
80
+
- feat: Added keybind `v` to view additional information about the selected entry.
81
+
- feat: Added config option `true_dir_size` to see the true size of directories.
82
+
- fix: Undoing a delete/rename wont overwrite an item with the same name now.
83
+
84
+
## v0.9.1 (2025-03-23)
85
+
- feat: File permissions are now displayed in the file information bar to the
86
+
bottom of Jido.
87
+
88
+
## v0.9.0 (2025-03-21)
89
+
- New Keybinds:
90
+
- Added keybind `<CTRL-r>` to reload config while Jido is running.
91
+
- Added keybind `.` to hide/show hidden files at runtime.
92
+
Default behaviour is still read from the config file if set.
93
+
- Added keybind rebinding.
94
+
Jido now allows you to rebind certain keys. These can be rebound via the config
95
+
file. See [Configuration](https://github.com/BrookJeynes/jido?tab=readme-ov-file#configuration)
96
+
for more information.
97
+
- Added file logger.
98
+
This file logger allows Jido to provide users with more detailed log messages
99
+
the notification system cannot. The log file can be found within the config
100
+
directory under the file `log.txt`.
101
+
- Jido is now built with the latest stable version of Zig, v0.14.0.
102
+
- Fixes:
103
+
- Hiding/showing hidden files after cd would cause all the files to visually
104
+
disappear.
105
+
- Off by one error when traversing command history causing the list to skip
106
+
some entries.
107
+
- Empty commands are no longer added to the command history. This now means
108
+
commands are whitespace trimmed.
109
+
- Move logic to hide dot files from renderer to directory reader.
110
+
This moves the logic to hide dot files out from the renderer to the
111
+
directory reader. This means if hidden files are turned off, they aren't
112
+
even stored.
113
+
- Default styling didn't specify styling for notification box text. This
114
+
would cause visual issues for light mode users.
115
+
116
+
## v0.8.3 (2025-03-19)
117
+
- feat: Added keybind `<CTRL-r>` to reload config while Jido is running.
118
+
- fix: Hiding/showing hidden files after cd would display no files.
119
+
- fix: Off by one error when traversing command history...
120
+
- fix: Dont add empty commands to command history.
121
+
- docs: Updated readme to mention new keybind.
122
+
- docs: Reordered keybinds section to add "Global" section.
123
+
124
+
## v0.8.2 (2025-03-18)
125
+
- fix: Move logic to hide dot files from renderer to directory reader.
126
+
this moves the logic to hide dot files out from the renderer to the
127
+
directory reader. This means if hidden files are turned off, they aren't
128
+
even stored.
129
+
- feat: Added keybind `.` to hide/show hidden files at runtime.
130
+
Default behaviour is still read from the config file if set.
131
+
132
+
## v0.8.1 (2025-03-11)
133
+
- feat: Jido is now built with zig 0.14.0.
134
+
- chore: Update packages.
135
+
136
+
## v0.8.0 (2025-01-07)
137
+
- Rebrand from zfe to Jido by @BrookJeynes in #16
138
+
I felt that I wanted this project to have more of its own identity so I
139
+
decided now that this project is getting closer to a v1.0 release, it's time
140
+
to give it a proper name.
141
+
- Added command mode by @BrookJeynes in #14
142
+
Command mode is a way for users to enter Jido commands.
143
+
Currently supported commands:
144
+
```
145
+
Command mode:
146
+
:q :Exit.
147
+
:config :Navigate to config directory if it exists.
148
+
:trash :Navigate to trash directory if it exists.
149
+
:empty_trash :Empty trash if it exists. This action cannot be undone.
150
+
```
151
+
- Deletes are now sent to `<config>/trash` instead of `/tmp`. by @BrookJeynes in #15
152
+
Previously, deletes were sent to `/tmp`. This made it convenient for cleanup
153
+
however caused issues on certain distros. This was because the `/tmp` dir was
154
+
on a separate mount point and therefore the file was unable to be moved there.
155
+
Tying into this, there is now a new `empty_trash_on_exit` config option set to
156
+
false by default.
157
+
- Reworked the notification stylings. Notification stylings are now under the
158
+
notification namespace within the config file.
159
+
- The code used to detect the git branch no longer needs git installed on the
160
+
system.
161
+
- Displayed file size now shows the correct file size for files.
162
+
163
+
## v0.7.0 (2025-01-01)
164
+
- Fix notification segfaults by @BrookJeynes in #9
165
+
- Conform codebase styling by @BrookJeynes in #10
166
+
- Create release action by @BrookJeynes in #11
167
+
- Separate event and draw logic by @BrookJeynes in #12
168
+
- Updated config location from `$HOME/.config/zfe` to `$HOME/.zfe` by @BrookJeynes in 3cb9bb2
169
+
- This means that the config can be found at either `$HOME/.zfe/` or
170
+
`$XDG_CONFIG_HOME/zfe/config/`. The old path will continue to work for
171
+
the meantime but has been deprecated.
172
+
- Show git branch when available by @BrookJeynes in #13
173
+
174
+
## v0.6.1 (2024-12-03)
175
+
- Updated libvaxis and refactored build.zig by @BrookJeynes in #7
176
+
- Notifications are now their own windows that appear to the right by @BrookJeynes in #8
177
+
- Notifications are now their own windows that appear to the right of the
178
+
screen. they disappear after 3 seconds but note that renders only occur
179
+
after an action has been polled. this means that if you wait for 3 seconds
180
+
without an action, the notification wont disappear until an action occurs.
181
+
- Added info notifications on actions such as renaming, deleting, changing
182
+
dir, etc.
183
+
- Added notification_box colour setting to config.
184
+
185
+
## v0.5.0 (2024-06-05)
186
+
- Updated libvaxis dependency.
187
+
- Fixed an issue where viewing a PDF would freeze zfe. This fixes issue #5
188
+
- Added additional "Optional Dependencies" section to README to specify optional
189
+
dependencies for zfe (such as pdftotext for PDF viewing).
190
+
- Updated the way images are streamed in. This should help with #4 but I don't
191
+
think it ultimately fixes the issue at hand.
192
+
193
+
## v0.4.0 (2024-06-05)
194
+
- Fixed bug where cursor would jump back to the top after deleting, renaming,
195
+
creating, or undoing.
196
+
- Added new keybind `c` to change directory via path.
197
+
- Previous positions are saved when entering a new directory.
198
+
- PDFs can now be read if `pdftotext` is installed.
199
+
- Undo history can now only store the last 100 events.
200
+
- List scrolling is now squeaky smooth.
201
+
- Other general refactors and bug fixes.
202
+
203
+
## v0.3.0 (2024-05-30)
204
+
- Moved render and event handling logic to their own functions. This will make
205
+
it a more pleasant experience for contributors.
206
+
- Added issue templates for easier and more concise bug reports and feature
207
+
requests.
208
+
- Fixed issue where images would stop rendering if an event was emitted without
209
+
changing selected item.
210
+
- Implemented ability to delete files and folders.
211
+
- Implemented ability to rename files and folders.
212
+
- Implemented ability to undo deletions and renames within a session.
213
+
- Implemented ability to create folders and directories.
214
+
- Updated README with new keybinds.
215
+
- Added config option for styling info bar.
216
+
217
+
## v0.2.0 (2024-05-26)
218
+
- Implemented fuzzy search for items in a directory.
219
+
- Files can now be opened with `$EDITOR`.
220
+
- Error messages now displayed in app.
221
+
- Better errors when failing to read config.
222
+
- Stopped supporting Windows.
223
+
224
+
## v0.1.1 (2024-05-25)
225
+
- Added better error handling.
226
+
- Added new config style for error bar.
227
+
- Updated README to include config schema.
228
+
- Added MIT license.
229
+
230
+
## v0.1.0 (2024-05-24)
+66
-25
README.md
+66
-25
README.md
···
1
1
# 地圖 (Jido)
2
2
3
-

4
-
5
-
> **Note:** Previously known as **zfe**, this project has been renamed to
6
-
**Jido** to better reflect its purpose and functionality.
3
+

7
4
8
5
**Jido** is a lightweight Unix TUI file explorer designed for speed and
9
6
simplicity.
···
13
10
Vim-like bindings and a minimalist interface, Jido focuses on speed and
14
11
simplicity.
15
12
13
+
Jido is built with Zig v`0.15.2`.
14
+
16
15
- [Installation](#installation)
17
16
- [Integrations](#integrations)
18
17
- [Key manual](#key-manual)
···
28
27
- A terminal supporting the `kitty image protocol` to view images.
29
28
30
29
## Key manual
30
+
Below are the default keybinds. Keybinds can be overwritten via the `Keybinds`
31
+
config option. Some keybinds are unbound by default, see [Configuration](#configuration)
32
+
for more information.
33
+
31
34
```
32
-
Normal mode:
35
+
Global:
33
36
<CTRL-c> :Exit.
37
+
<CTRL-r> :Reload config.
38
+
39
+
Normal mode:
34
40
j / <Down> :Go down.
35
41
k / <Up> :Go up.
36
42
h / <Left> / - :Go to the parent directory.
···
44
50
d :Create directory. Will enter input mode.
45
51
% :Create file. Will enter input mode.
46
52
/ :Fuzzy search directory. Will enter input mode.
53
+
. :Toggle hidden files.
47
54
: :Allows for Jido commands to be entered. Please refer to the
48
55
"Command mode" section for available commands. Will enter
49
56
input mode.
57
+
v :Verbose mode. Provides more information about selected entry.
58
+
y :Yank selected item.
59
+
p :Past yanked item.
50
60
51
61
Input mode:
52
62
<Esc> :Cancel input.
53
63
<CR> :Confirm input.
54
64
55
65
Command mode:
66
+
<Up> / <Down> :Cycle previous commands.
56
67
:q :Exit.
68
+
:h :View available keybinds. 'q' to return to app.
57
69
:config :Navigate to config directory if it exists.
58
70
:trash :Navigate to trash directory if it exists.
59
71
:empty_trash :Empty trash if it exists. This action cannot be undone.
72
+
:cd <path> :Change directory via path. Will enter input mode.
60
73
```
61
74
62
-
63
75
## Configuration
64
76
Configure `jido` by editing the external configuration file located at either:
65
77
- `$HOME/.jido/config.json`
66
78
- `$XDG_CONFIG_HOME/jido/config.json`.
67
79
68
-
Jido will look for these env variables specifically. If they are not set, Jido will
69
-
not be able to find the config file.
80
+
Jido will look for these env variables specifically. If they are not set, Jido
81
+
will not be able to find the config file.
70
82
71
83
An example config file can be found [here](https://github.com/BrookJeynes/jido/blob/main/example-config.json).
72
84
73
85
Config schema:
74
86
```
75
87
Config = struct {
76
-
.show_hidden: bool,
77
-
.sort_dirs: bool,
78
-
.show_images: bool,
79
-
.preview_file: bool,
80
-
.empty_trash_on_exit: bool,
81
-
.styles: Styles,
88
+
.show_hidden: bool = true,
89
+
.sort_dirs: bool = true,
90
+
.show_images: bool = true, -- Images are only supported in a terminal
91
+
supporting the `kitty image protocol`.
92
+
.preview_file: bool = true,
93
+
.empty_trash_on_exit: bool = false, -- Emptying the trash permanently deletes
94
+
all files within the trash. These
95
+
files are not recoverable past this
96
+
point.
97
+
.true_dir_size: bool = false, -- Display size of directory including
98
+
all its children. This can and will
99
+
cause lag on deeply nested directories.
100
+
.keybinds: Keybinds,
101
+
.styles: Styles
102
+
}
103
+
104
+
Keybinds = struct {
105
+
.toggle_hidden_files: ?Char = '.',
106
+
.delete: ?Char = 'D',
107
+
.rename: ?Char = 'R',
108
+
.create_dir: ?Char = 'd',
109
+
.create_file: ?Char = '%',
110
+
.fuzzy_find: ?Char = '/',
111
+
.change_dir: ?Char = 'c',
112
+
.enter_command_mode: ?Char = ':',
113
+
.jump_top: ?Char = 'g',
114
+
.jump_bottom: ?Char = 'G',
115
+
.toggle_verbose_file_information: ?Char = 'v',
116
+
.force_delete: ?Char = null -- Files deleted this way are
117
+
not recoverable
118
+
.yank: ?Char = 'y'
119
+
.paste: ?Char = 'p'
82
120
}
83
121
84
122
NotificationStyles = struct {
85
-
box: vaxis.Style,
86
-
err: vaxis.Style,
87
-
warn: vaxis.Style,
88
-
info: vaxis.Style,
89
-
};
123
+
.box: vaxis.Style,
124
+
.err: vaxis.Style,
125
+
.warn: vaxis.Style,
126
+
.info: vaxis.Style
127
+
}
90
128
91
129
Styles = struct {
92
130
.selected_list_item: Style,
···
94
132
.file_name: Style,
95
133
.file_information: Style
96
134
.notification: NotificationStyles,
97
-
.git_branch: Style,
135
+
.git_branch: Style
98
136
}
99
137
100
138
Style = struct {
···
107
145
double,
108
146
curly,
109
147
dotted,
110
-
dashed,
148
+
dashed
111
149
}
112
150
.bold: bool,
113
151
.dim: bool,
···
115
153
.blink: bool,
116
154
.reverse: bool,
117
155
.invisible: bool,
118
-
.strikethrough: bool,
156
+
.strikethrough: bool
119
157
}
120
158
121
159
Color = enum{
122
160
default,
123
161
index: u8,
124
-
rgb: [3]u8,
162
+
rgb: [3]u8
125
163
}
164
+
165
+
Char = enum(u21)
126
166
```
127
167
128
168
## Contributing
129
-
Contributions, issues, and feature requests are always welcome! This project is
130
-
currently using the latest stable release of Zig (0.13.0).
169
+
Contributions, issues, and feature requests are always welcome via
170
+
[GitHub](https://github.com/brookjeynes/jido) or
171
+
[tangled](https://tangled.sh/@brookjeynes.dev/jido).
assets/preview.gif
assets/preview.gif
This is a binary file and will not be displayed.
+30
-11
build.zig
+30
-11
build.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
3
4
+
///Must match the `version` in `build.zig.zon`.
5
+
const version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 };
6
+
4
7
const targets: []const std.Target.Query = &.{
5
8
.{ .cpu_arch = .aarch64, .os_tag = .macos },
6
9
.{ .cpu_arch = .aarch64, .os_tag = .linux },
···
8
11
.{ .cpu_arch = .x86_64, .os_tag = .macos },
9
12
};
10
13
11
-
fn createExe(b: *std.Build, exe_name: []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !*std.Build.Step.Compile {
12
-
const libvaxis = b.dependency("vaxis", .{ .target = target }).module("vaxis");
13
-
const fuzzig = b.dependency("fuzzig", .{ .target = target }).module("fuzzig");
14
-
const zuid = b.dependency("zuid", .{ .target = target }).module("zuid");
14
+
fn createExe(
15
+
b: *std.Build,
16
+
exe_name: []const u8,
17
+
target: std.Build.ResolvedTarget,
18
+
optimize: std.builtin.OptimizeMode,
19
+
build_options: *std.Build.Module,
20
+
) !*std.Build.Step.Compile {
21
+
const libvaxis = b.dependency("vaxis", .{ .target = target, .optimize = optimize }).module("vaxis");
22
+
const fuzzig = b.dependency("fuzzig", .{ .target = target, .optimize = optimize }).module("fuzzig");
23
+
const zeit = b.dependency("zeit", .{ .target = target, .optimize = optimize }).module("zeit");
24
+
const zuid = b.dependency("zuid", .{ .target = target, .optimize = optimize }).module("zuid");
15
25
16
26
const exe = b.addExecutable(.{
17
27
.name = exe_name,
18
-
.root_source_file = b.path("src/main.zig"),
19
-
.target = target,
20
-
.optimize = optimize,
28
+
.root_module = b.createModule(.{
29
+
.root_source_file = b.path("src/main.zig"),
30
+
.target = target,
31
+
.optimize = optimize,
32
+
}),
21
33
});
22
34
35
+
exe.root_module.addImport("options", build_options);
23
36
exe.root_module.addImport("vaxis", libvaxis);
24
37
exe.root_module.addImport("fuzzig", fuzzig);
38
+
exe.root_module.addImport("zeit", zeit);
25
39
exe.root_module.addImport("zuid", zuid);
26
40
27
41
return exe;
···
31
45
const target = b.standardTargetOptions(.{});
32
46
const optimize = b.standardOptimizeOption(.{});
33
47
48
+
const build_options = b.addOptions();
49
+
build_options.step.name = "build options";
50
+
build_options.addOption(std.SemanticVersion, "version", version);
51
+
const build_options_module = build_options.createModule();
52
+
34
53
// Building targets for release.
35
54
const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false;
36
55
if (build_all) {
37
-
try buildTargets(b);
56
+
try buildTargets(b, build_options_module);
38
57
return;
39
58
}
40
59
41
-
const exe = try createExe(b, "jido", target, optimize);
60
+
const exe = try createExe(b, "jido", target, optimize, build_options_module);
42
61
b.installArtifact(exe);
43
62
44
63
const run_cmd = b.addRunArtifact(exe);
···
50
69
run_step.dependOn(&run_cmd.step);
51
70
}
52
71
53
-
fn buildTargets(b: *std.Build) !void {
72
+
fn buildTargets(b: *std.Build, build_options: *std.Build.Module) !void {
54
73
for (targets) |t| {
55
74
const target = b.resolveTargetQuery(t);
56
75
57
-
const exe = try createExe(b, "jido", target, .ReleaseSafe);
76
+
const exe = try createExe(b, "jido", target, .ReleaseSafe, build_options);
58
77
b.installArtifact(exe);
59
78
60
79
const target_output = b.addInstallArtifact(exe, .{
+25
-10
build.zig.zon
+25
-10
build.zig.zon
···
1
1
.{
2
-
.name = "jido",
3
-
.version = "0.8.0",
4
-
.minimum_zig_version = "0.13.0",
2
+
.name = .jido,
3
+
.fingerprint = 0xee45eabe36cafb57,
4
+
.version = "1.3.0",
5
+
.minimum_zig_version = "0.15.2",
5
6
6
7
.dependencies = .{
8
+
// Replace with rockorager/libvaxis once https://github.com/rockorager/libvaxis/pull/293 is merged
7
9
.vaxis = .{
8
-
.url = "git+https://github.com/rockorager/libvaxis#77f5795892b08cd64ad6a103f0c53a7d1db50b18",
9
-
.hash = "1220d587525255e734670ae74f38cb09d75df936c7889b07a6eab739c066dc736f85",
10
+
.url = "git+https://github.com/rob9315/libvaxis.git#8d04cffd9137b4a8c56b356de98b32023ae752f3",
11
+
.hash = "vaxis-0.5.1-BWNV_OA-CQDeFBHIx9ryyASogr2GE3FsAm-l5Ii5-HZT",
10
12
},
11
13
.fuzzig = .{
12
-
.url = "git+https://github.com/fjebaker/fuzzig#0fd156d5097365151e85a85eef9d8cf0eebe7b00",
13
-
.hash = "122019f077d09686b1ec47928ca2b4bf264422f3a27afc5b49dafb0129a4ceca0d01",
14
+
.url = "git+https://github.com/fjebaker/fuzzig#4251fe4230d38e721514394a485db62ee1667ff3",
15
+
.hash = "fuzzig-0.1.1-Ji0xivxIAQBD0g8O_NV_0foqoPf3elsg9Sc3pNfdVH4D",
16
+
},
17
+
.zeit = .{
18
+
.url = "git+https://github.com/rockorager/zeit#7ac64d72dbfb1a4ad549102e7d4e232a687d32d8",
19
+
.hash = "zeit-0.6.0-5I6bk36tAgATpSl9wjFmRPMqYN2Mn0JQHgIcRNcqDpJA",
14
20
},
21
+
// Replace with KeithBrown39423/zuid once https://github.com/KeithBrown39423/zuid/pull/4 is merged
15
22
.zuid = .{
16
-
.url = "git+https://github.com/KeithBrown39423/zuid#49e5980ba83f7d9ae967fa7ce4d54384c1c0f82b",
17
-
.hash = "1220e05a3f459c0adbf2b09b4764838833e3e716a712852aec6ef1636f4d8e9f646e",
23
+
.url = "https://github.com/BrookJeynes/zuid/archive/refs/heads/bj/2025-12-31/feat/0.15.1.tar.gz",
24
+
.hash = "zuid-3.0.0-l7aPyUlXAAAk9BLSDm2roA3i78Sy6_GvQI4hwe0PHI_m",
25
+
},
26
+
// Replace with zigimg/zigimg once https://github.com/zigimg/zigimg/pull/305 is merged
27
+
.zigimg = .{
28
+
.url = "git+https://github.com/brookjeynes/zigimg.git#9714df09f76891323c7fdbbbf23a17b79024fffb",
29
+
.hash = "zigimg-0.1.0-8_eo2j4mFwCU7tWnqvkYtzqe-OPRn_bxEql_IJhW85LT",
18
30
},
19
31
},
20
32
21
33
.paths = .{
22
-
"./src/",
34
+
"LICENSE",
35
+
"build.zig",
36
+
"build.zig.zon",
37
+
"src",
23
38
},
24
39
}
+4
example-config.json
+4
example-config.json
+186
-45
src/app.zig
+186
-45
src/app.zig
···
6
6
const config = &@import("./config.zig").config;
7
7
const List = @import("./list.zig").List;
8
8
const Directories = @import("./directories.zig");
9
+
const FileLogger = @import("./file_logger.zig");
9
10
const CircStack = @import("./circ_stack.zig").CircularStack;
10
11
const zuid = @import("zuid");
11
12
const vaxis = @import("vaxis");
12
13
const Key = vaxis.Key;
13
14
const EventHandlers = @import("./event_handlers.zig");
15
+
const CommandHistory = @import("./commands.zig").CommandHistory;
16
+
17
+
const help_menu_items = [_][]const u8{
18
+
"Global:",
19
+
"<CTRL-c> :Exit.",
20
+
"<CTRL-r> :Reload config.",
21
+
"",
22
+
"Normal mode:",
23
+
"j / <Down> :Go down.",
24
+
"k / <Up> :Go up.",
25
+
"h / <Left> / - :Go to the parent directory.",
26
+
"l / <Right> :Open item or change directory.",
27
+
"g :Go to the top.",
28
+
"G :Go to the bottom.",
29
+
"c :Change directory via path. Will enter input mode.",
30
+
"R :Rename item. Will enter input mode.",
31
+
"D :Delete item.",
32
+
"u :Undo delete/rename.",
33
+
"d :Create directory. Will enter input mode.",
34
+
"% :Create file. Will enter input mode.",
35
+
"/ :Fuzzy search directory. Will enter input mode.",
36
+
". :Toggle hidden files.",
37
+
": :Allows for Jido commands to be entered. Please refer to the ",
38
+
" \"Command mode\" section for available commands. Will enter ",
39
+
" input mode.",
40
+
"v :Verbose mode. Provides more information about selected entry. ",
41
+
"y :Yank selected item.",
42
+
"p :Past yanked item.",
43
+
"",
44
+
"Input mode:",
45
+
"<Esc> :Cancel input.",
46
+
"<CR> :Confirm input.",
47
+
"",
48
+
"Command mode:",
49
+
"<Up> / <Down> :Cycle previous commands.",
50
+
":q :Exit.",
51
+
":h :View available keybinds. 'q' to return to app.",
52
+
":config :Navigate to config directory if it exists.",
53
+
":trash :Navigate to trash directory if it exists.",
54
+
":empty_trash :Empty trash if it exists. This action cannot be undone.",
55
+
":cd <path> :Change directory via path. Will enter input mode.",
56
+
};
14
57
15
58
pub const State = enum {
16
59
normal,
···
20
63
change_dir,
21
64
rename,
22
65
command,
23
-
};
24
-
25
-
const ActionPaths = struct {
26
-
/// Allocated.
27
-
old: []const u8,
28
-
/// Allocated.
29
-
new: []const u8,
66
+
help_menu,
30
67
};
31
68
32
69
pub const Action = union(enum) {
33
-
delete: ActionPaths,
34
-
rename: ActionPaths,
70
+
delete: struct { prev_path: []const u8, new_path: []const u8 },
71
+
rename: struct { prev_path: []const u8, new_path: []const u8 },
72
+
paste: []const u8,
35
73
};
36
74
37
75
pub const Event = union(enum) {
76
+
image_ready,
77
+
notification,
38
78
key_press: Key,
39
79
winsize: vaxis.Winsize,
40
80
};
41
81
82
+
pub const Image = struct {
83
+
const Status = enum {
84
+
ready,
85
+
processing,
86
+
failed,
87
+
};
88
+
89
+
///Only use on first transmission. Subsequent draws should use
90
+
///`Image.image`.
91
+
data: ?vaxis.zigimg.Image = null,
92
+
image: ?vaxis.Image = null,
93
+
path: ?[]const u8 = null,
94
+
status: Status = .processing,
95
+
96
+
pub fn deinit(self: @This(), alloc: std.mem.Allocator, vx: vaxis.Vaxis, tty: *vaxis.Tty) void {
97
+
if (self.image) |image| {
98
+
vx.freeImage(tty.writer(), image.id);
99
+
}
100
+
if (self.data) |data| {
101
+
var d = data;
102
+
d.deinit(alloc);
103
+
}
104
+
if (self.path) |path| alloc.free(path);
105
+
}
106
+
};
107
+
42
108
const actions_len = 100;
109
+
const image_cache_cap = 100;
43
110
44
111
const App = @This();
45
112
46
113
alloc: std.mem.Allocator,
47
114
should_quit: bool,
48
115
vx: vaxis.Vaxis = undefined,
116
+
tty_buffer: [1024]u8 = undefined,
49
117
tty: vaxis.Tty = undefined,
118
+
loop: vaxis.Loop(Event) = undefined,
50
119
state: State = .normal,
51
120
actions: CircStack(Action, actions_len),
121
+
command_history: CommandHistory = CommandHistory{},
122
+
drawer: Drawer = Drawer{},
52
123
124
+
help_menu: List([]const u8),
53
125
directories: Directories,
54
-
notification: Notification,
126
+
notification: Notification = Notification{},
127
+
file_logger: ?FileLogger = null,
55
128
56
129
text_input: vaxis.widgets.TextInput,
57
130
text_input_buf: [std.fs.max_path_bytes]u8 = undefined,
58
131
59
-
image: ?vaxis.Image = null,
132
+
yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null,
60
133
last_known_height: usize,
61
134
62
-
pub fn init(alloc: std.mem.Allocator) !App {
135
+
images: struct {
136
+
mutex: std.Thread.Mutex = .{},
137
+
cache: std.StringHashMap(Image),
138
+
},
139
+
140
+
pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !App {
63
141
var vx = try vaxis.init(alloc, .{
64
142
.kitty_keyboard_flags = .{
65
143
.report_text = false,
···
70
148
},
71
149
});
72
150
73
-
var notification = Notification{};
74
-
notification.init();
151
+
var help_menu = List([]const u8).init(alloc);
152
+
try help_menu.fromArray(&help_menu_items);
75
153
76
-
return App{
154
+
var app: App = .{
77
155
.alloc = alloc,
78
156
.should_quit = false,
79
157
.vx = vx,
80
-
.tty = try vaxis.Tty.init(),
81
-
.directories = try Directories.init(alloc),
82
-
.text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode),
83
-
.notification = notification,
158
+
.directories = try Directories.init(alloc, entry_dir),
159
+
.help_menu = help_menu,
160
+
.text_input = vaxis.widgets.TextInput.init(alloc),
84
161
.actions = CircStack(Action, actions_len).init(),
85
162
.last_known_height = vx.window().height,
163
+
.images = .{ .cache = .init(alloc) },
86
164
};
165
+
app.tty = try vaxis.Tty.init(&app.tty_buffer);
166
+
app.loop = vaxis.Loop(Event){
167
+
.vaxis = &app.vx,
168
+
.tty = &app.tty,
169
+
};
170
+
171
+
return app;
87
172
}
88
173
89
174
pub fn deinit(self: *App) void {
90
-
for (self.actions.buf[0..self.actions.count]) |action| {
175
+
while (self.actions.pop()) |action| {
91
176
switch (action) {
92
-
.delete, .rename => |a| {
93
-
self.alloc.free(a.new);
94
-
self.alloc.free(a.old);
177
+
.delete => |a| {
178
+
self.alloc.free(a.new_path);
179
+
self.alloc.free(a.prev_path);
180
+
},
181
+
.rename => |a| {
182
+
self.alloc.free(a.new_path);
183
+
self.alloc.free(a.prev_path);
95
184
},
185
+
.paste => |a| self.alloc.free(a),
96
186
}
97
187
}
98
188
189
+
if (self.yanked) |yanked| {
190
+
self.alloc.free(yanked.dir);
191
+
self.alloc.free(yanked.entry.name);
192
+
}
193
+
194
+
self.command_history.deinit(self.alloc);
195
+
196
+
self.help_menu.deinit();
99
197
self.directories.deinit();
100
198
self.text_input.deinit();
101
-
self.vx.deinit(self.alloc, self.tty.anyWriter());
199
+
self.vx.deinit(self.alloc, self.tty.writer());
102
200
self.tty.deinit();
201
+
if (self.file_logger) |file_logger| file_logger.deinit();
202
+
203
+
var image_iter = self.images.cache.iterator();
204
+
while (image_iter.next()) |img| {
205
+
img.value_ptr.deinit(self.alloc, self.vx, &self.tty);
206
+
}
207
+
self.images.cache.deinit();
103
208
}
104
209
105
-
pub fn run(self: *App) !void {
106
-
var drawer = Drawer{};
107
-
try self.directories.populateEntries("");
210
+
pub fn inputToSlice(self: *App) []const u8 {
211
+
self.text_input.buf.cursor = self.text_input.buf.realLength();
212
+
return self.text_input.sliceToCursor(&self.text_input_buf);
213
+
}
108
214
109
-
var loop: vaxis.Loop(Event) = .{
110
-
.vaxis = &self.vx,
111
-
.tty = &self.tty,
215
+
pub fn repopulateDirectory(self: *App, fuzzy: []const u8) error{OutOfMemory}!void {
216
+
self.directories.clearEntries();
217
+
self.directories.populateEntries(fuzzy) catch |err| {
218
+
const message = try std.fmt.allocPrint(self.alloc, "Failed to read directory entries - {}.", .{err});
219
+
defer self.alloc.free(message);
220
+
self.notification.write(message, .err) catch {};
221
+
if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {};
112
222
};
113
-
try loop.start();
114
-
defer loop.stop();
223
+
}
224
+
225
+
pub fn run(self: *App) !void {
226
+
try self.repopulateDirectory("");
227
+
try self.loop.start();
228
+
defer self.loop.stop();
115
229
116
-
try self.vx.enterAltScreen(self.tty.anyWriter());
117
-
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
230
+
try self.vx.enterAltScreen(self.tty.writer());
231
+
try self.vx.queryTerminal(self.tty.writer(), 1 * std.time.ns_per_s);
232
+
self.vx.caps.kitty_graphics = true;
118
233
119
234
while (!self.should_quit) {
120
-
loop.pollEvent();
121
-
while (loop.tryEvent()) |event| {
235
+
self.loop.pollEvent();
236
+
while (self.loop.tryEvent()) |event| {
237
+
// Global keybinds.
238
+
try EventHandlers.handleGlobalEvent(self, event);
239
+
240
+
// State specific keybinds.
122
241
switch (self.state) {
123
242
.normal => {
124
-
try EventHandlers.handleNormalEvent(self, event, &loop);
243
+
try EventHandlers.handleNormalEvent(self, event);
244
+
},
245
+
.help_menu => {
246
+
try EventHandlers.handleHelpMenuEvent(self, event);
125
247
},
126
248
else => {
127
249
try EventHandlers.handleInputEvent(self, event);
···
129
251
}
130
252
}
131
253
132
-
try drawer.draw(self);
254
+
try self.drawer.draw(self);
133
255
134
-
var buffered = self.tty.bufferedWriter();
135
-
try self.vx.render(buffered.writer().any());
136
-
try buffered.flush();
256
+
try self.vx.render(self.tty.writer());
137
257
}
138
258
139
259
if (config.empty_trash_on_exit) {
140
-
if (try config.trashDir()) |dir| {
141
-
var trash_dir = dir;
142
-
defer trash_dir.close();
143
-
_ = try environment.deleteContents(trash_dir);
260
+
var trash_dir = dir: {
261
+
notfound: {
262
+
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
263
+
}
264
+
if (self.file_logger) |file_logger| file_logger.write("Failed to open trash directory.", .err) catch {
265
+
std.log.err("Failed to open trash directory.", .{});
266
+
};
267
+
return;
268
+
};
269
+
defer trash_dir.close();
270
+
271
+
const failed = environment.deleteContents(trash_dir) catch |err| {
272
+
const message = try std.fmt.allocPrint(self.alloc, "Failed to empty trash - {}.", .{err});
273
+
defer self.alloc.free(message);
274
+
if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {
275
+
std.log.err("Failed to empty trash - {}.", .{err});
276
+
};
277
+
return;
278
+
};
279
+
if (failed > 0) {
280
+
const message = try std.fmt.allocPrint(self.alloc, "Failed to empty {d} items from the trash.", .{failed});
281
+
defer self.alloc.free(message);
282
+
if (self.file_logger) |file_logger| file_logger.write(message, .err) catch {
283
+
std.log.err("Failed to empty {d} items from the trash.", .{failed});
284
+
};
144
285
}
145
286
}
146
287
}
+142
-35
src/commands.zig
+142
-35
src/commands.zig
···
1
+
const std = @import("std");
1
2
const App = @import("app.zig");
2
3
const environment = @import("environment.zig");
3
-
const _config = &@import("./config.zig").config;
4
+
const user_config = &@import("./config.zig").config;
5
+
6
+
pub const CommandHistory = struct {
7
+
const history_len = 10;
8
+
9
+
history: [history_len][]const u8 = undefined,
10
+
count: usize = 0,
11
+
///Points to the oldest entry.
12
+
start: usize = 0,
13
+
cursor: ?usize = null,
14
+
15
+
pub fn deinit(self: *CommandHistory, allocator: std.mem.Allocator) void {
16
+
for (self.history[0..self.count]) |entry| {
17
+
allocator.free(entry);
18
+
}
19
+
}
20
+
21
+
pub fn add(self: *CommandHistory, cmd: []const u8, allocator: std.mem.Allocator) error{OutOfMemory}!void {
22
+
const index = (self.start + self.count) % history_len;
23
+
24
+
if (self.count < history_len) {
25
+
self.count += 1;
26
+
} else {
27
+
// Overwriting the oldest entry.
28
+
allocator.free(self.history[self.start]);
29
+
self.start = (self.start + 1) % history_len;
30
+
}
31
+
32
+
self.history[index] = try allocator.dupe(u8, cmd);
33
+
self.cursor = null;
34
+
}
35
+
36
+
pub fn previous(self: *CommandHistory) ?[]const u8 {
37
+
if (self.count == 0) return null;
38
+
39
+
if (self.cursor == null) {
40
+
self.cursor = self.count - 1;
41
+
} else if (self.cursor.? > 0) {
42
+
self.cursor.? -= 1;
43
+
}
44
+
45
+
return self.getAtCursor();
46
+
}
47
+
48
+
pub fn next(self: *CommandHistory) ?[]const u8 {
49
+
if (self.count == 0 or self.cursor == null) return null;
50
+
51
+
if (self.cursor.? < self.count - 1) {
52
+
self.cursor.? += 1;
53
+
return self.getAtCursor();
54
+
}
55
+
56
+
self.cursor = null;
57
+
return null;
58
+
}
59
+
60
+
fn getAtCursor(self: *CommandHistory) ?[]const u8 {
61
+
if (self.cursor == null) return null;
62
+
const index = (self.start + self.cursor.?) % history_len;
63
+
return self.history[index];
64
+
}
65
+
};
4
66
5
67
///Navigate the user to the config dir.
6
-
pub fn config(app: *App) !void {
68
+
pub fn config(app: *App) error{OutOfMemory}!void {
7
69
const dir = dir: {
8
70
notfound: {
9
-
break :dir (_config.configDir() catch break :notfound) orelse break :notfound;
71
+
break :dir (user_config.configDir() catch break :notfound) orelse break :notfound;
10
72
}
11
-
try app.notification.writeErr(.ConfigPathNotFound);
73
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to config directory - unable to retrieve config directory.", .{});
74
+
defer app.alloc.free(message);
75
+
app.notification.write(message, .err) catch {};
76
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
12
77
return;
13
78
};
14
-
app.directories.clearEntries();
79
+
15
80
app.directories.dir.close();
16
81
app.directories.dir = dir;
17
-
app.directories.populateEntries("") catch |err| {
18
-
switch (err) {
19
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
20
-
else => try app.notification.writeErr(.UnknownError),
21
-
}
22
-
};
82
+
try app.repopulateDirectory("");
23
83
}
24
84
25
85
///Navigate the user to the trash dir.
26
-
pub fn trash(app: *App) !void {
86
+
pub fn trash(app: *App) error{OutOfMemory}!void {
27
87
const dir = dir: {
28
88
notfound: {
29
-
break :dir (_config.trashDir() catch break :notfound) orelse break :notfound;
89
+
break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
30
90
}
31
-
try app.notification.writeErr(.ConfigPathNotFound);
91
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{});
92
+
defer app.alloc.free(message);
93
+
app.notification.write(message, .err) catch {};
94
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
32
95
return;
33
96
};
34
-
app.directories.clearEntries();
97
+
35
98
app.directories.dir.close();
36
99
app.directories.dir = dir;
37
-
app.directories.populateEntries("") catch |err| {
38
-
switch (err) {
39
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
40
-
else => try app.notification.writeErr(.UnknownError),
41
-
}
42
-
};
100
+
try app.repopulateDirectory("");
43
101
}
44
102
45
103
///Empty the trash.
46
-
pub fn emptyTrash(app: *App) !void {
47
-
const dir = dir: {
104
+
pub fn emptyTrash(app: *App) error{OutOfMemory}!void {
105
+
var message: ?[]const u8 = null;
106
+
defer if (message) |msg| app.alloc.free(msg);
107
+
108
+
var dir = dir: {
48
109
notfound: {
49
-
break :dir (_config.trashDir() catch break :notfound) orelse break :notfound;
110
+
break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
50
111
}
51
-
try app.notification.writeErr(.ConfigPathNotFound);
112
+
message = try std.fmt.allocPrint(app.alloc, "Failed to navigate to trash directory - unable to retrieve trash directory.", .{});
113
+
app.notification.write(message.?, .err) catch {};
114
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
52
115
return;
53
116
};
117
+
defer dir.close();
54
118
55
-
var trash_dir = dir;
56
-
defer trash_dir.close();
57
-
const failed = try environment.deleteContents(trash_dir);
58
-
if (failed > 0) try app.notification.writeErr(.FailedToDeleteSomeItems);
119
+
const failed = environment.deleteContents(dir) catch |err| lbl: {
120
+
message = try std.fmt.allocPrint(app.alloc, "Failed to empty trash - {}.", .{err});
121
+
app.notification.write(message.?, .err) catch {};
122
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
123
+
break :lbl 0;
124
+
};
125
+
if (failed > 0) {
126
+
message = try std.fmt.allocPrint(app.alloc, "Failed to empty {d} items from the trash.", .{failed});
127
+
app.notification.write(message.?, .err) catch {};
128
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
129
+
}
59
130
60
-
app.directories.clearEntries();
61
-
app.directories.populateEntries("") catch |err| {
62
-
switch (err) {
63
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
64
-
else => try app.notification.writeErr(.UnknownError),
65
-
}
131
+
try app.repopulateDirectory("");
132
+
}
133
+
134
+
pub fn resolvePath(buf: *[std.fs.max_path_bytes]u8, path: []const u8, dir: std.fs.Dir) []const u8 {
135
+
const resolved_path = if (std.mem.startsWith(u8, path, "~")) path: {
136
+
var home_dir = (environment.getHomeDir() catch break :path path) orelse break :path path;
137
+
defer home_dir.close();
138
+
const relative = std.mem.trim(u8, path[1..], std.fs.path.sep_str);
139
+
return home_dir.realpath(
140
+
if (relative.len == 0) "." else relative,
141
+
buf,
142
+
) catch path;
143
+
} else path;
144
+
145
+
return dir.realpath(resolved_path, buf) catch path;
146
+
}
147
+
148
+
///Change directory.
149
+
pub fn cd(app: *App, path: []const u8) error{OutOfMemory}!void {
150
+
var message: ?[]const u8 = null;
151
+
defer if (message) |msg| app.alloc.free(msg);
152
+
153
+
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
154
+
const resolved_path = resolvePath(&path_buf, path, app.directories.dir);
155
+
156
+
const dir = app.directories.dir.openDir(resolved_path, .{ .iterate = true }) catch |err| {
157
+
message = switch (err) {
158
+
error.FileNotFound => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - directory does not exist.", .{resolved_path}),
159
+
error.NotDir => try std.fmt.allocPrint(app.alloc, "Failed to navigate to '{s}' - item is not a directory.", .{resolved_path}),
160
+
else => try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err}),
161
+
};
162
+
app.notification.write(message.?, .err) catch {};
163
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
164
+
return;
66
165
};
166
+
app.directories.dir.close();
167
+
app.directories.dir = dir;
168
+
169
+
message = try std.fmt.allocPrint(app.alloc, "Navigated to directory '{s}'.", .{resolved_path});
170
+
app.notification.write(message.?, .info) catch {};
171
+
172
+
try app.repopulateDirectory("");
173
+
app.directories.history.reset();
67
174
}
+134
-47
src/config.zig
+134
-47
src/config.zig
···
2
2
const builtin = @import("builtin");
3
3
const environment = @import("./environment.zig");
4
4
const vaxis = @import("vaxis");
5
+
const FileLogger = @import("file_logger.zig");
5
6
const Notification = @import("./notification.zig");
7
+
const App = @import("./app.zig");
6
8
7
-
pub const ParseRes = struct { deprecated: bool };
9
+
const CONFIG_NAME = "config.json";
10
+
const TRASH_DIR_NAME = "trash";
11
+
const HOME_DIR_NAME = ".jido";
12
+
const XDG_CONFIG_HOME_DIR_NAME = "jido";
8
13
9
14
const Config = struct {
10
15
show_hidden: bool = true,
···
12
17
show_images: bool = true,
13
18
preview_file: bool = true,
14
19
empty_trash_on_exit: bool = false,
15
-
styles: Styles,
20
+
true_dir_size: bool = false,
21
+
entry_dir: ?[]const u8 = null,
22
+
styles: Styles = .{},
23
+
keybinds: Keybinds = .{},
16
24
17
-
config_path_buf: [std.fs.max_path_bytes]u8 = undefined,
18
-
config_path: ?[]u8 = null,
25
+
config_dir: ?std.fs.Dir = null,
19
26
20
27
///Returned dir needs to be closed by user.
21
28
pub fn configDir(self: Config) !?std.fs.Dir {
22
-
if (self.config_path) |path| {
23
-
return try std.fs.openDirAbsolute(std.mem.trimRight(u8, path, "config.json"), .{ .iterate = true });
29
+
if (self.config_dir) |dir| {
30
+
return try dir.openDir(".", .{ .iterate = true });
24
31
} else return null;
25
32
}
26
33
···
28
35
pub fn trashDir(self: Config) !?std.fs.Dir {
29
36
var parent = try self.configDir() orelse return null;
30
37
defer parent.close();
31
-
if (!environment.dirExists(parent, "trash")) {
32
-
try parent.makeDir("trash");
38
+
if (!environment.dirExists(parent, TRASH_DIR_NAME)) {
39
+
try parent.makeDir(TRASH_DIR_NAME);
33
40
}
34
41
35
-
return try parent.openDir("trash", .{ .iterate = true });
42
+
return try parent.openDir(TRASH_DIR_NAME, .{ .iterate = true });
36
43
}
37
44
38
-
pub fn parse(self: *Config, alloc: std.mem.Allocator) !ParseRes {
39
-
var deprecated = false;
40
-
var config_location: struct {
41
-
home_dir: std.fs.Dir,
42
-
path: []const u8,
43
-
} = lbl: {
45
+
pub fn parse(self: *Config, alloc: std.mem.Allocator, app: *App) !void {
46
+
var dir = lbl: {
44
47
if (try environment.getXdgConfigHomeDir()) |home_dir| {
45
-
const path = "jido" ++ std.fs.path.sep_str ++ "config.json";
46
-
if (environment.fileExists(home_dir, path)) {
47
-
break :lbl .{
48
-
.home_dir = home_dir,
49
-
.path = path,
50
-
};
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);
51
55
}
52
56
53
-
var dir = home_dir;
54
-
dir.close();
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;
55
67
}
56
68
57
69
if (try environment.getHomeDir()) |home_dir| {
58
-
const path = ".jido" ++ std.fs.path.sep_str ++ "config.json";
59
-
if (environment.fileExists(home_dir, path)) {
60
-
break :lbl .{
61
-
.home_dir = home_dir,
62
-
.path = path,
63
-
};
70
+
defer {
71
+
var dir = home_dir;
72
+
dir.close();
64
73
}
65
74
66
-
const deprecated_path = ".config" ++ std.fs.path.sep_str ++ "jido" ++ std.fs.path.sep_str ++ "config.json";
67
-
if (environment.fileExists(home_dir, deprecated_path)) {
68
-
deprecated = true;
69
-
break :lbl .{
70
-
.home_dir = home_dir,
71
-
.path = deprecated_path,
72
-
};
75
+
if (!environment.dirExists(home_dir, HOME_DIR_NAME)) {
76
+
try home_dir.makeDir(HOME_DIR_NAME);
73
77
}
74
78
75
-
var dir = home_dir;
76
-
dir.close();
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;
77
89
}
78
90
79
-
return .{ .deprecated = deprecated };
91
+
return;
80
92
};
81
-
defer config_location.home_dir.close();
82
93
83
-
const config_file = try config_location.home_dir.openFile(config_location.path, .{});
94
+
const config_file = try dir.openFile(CONFIG_NAME, .{});
84
95
defer config_file.close();
85
96
86
97
const config_str = try config_file.readToEndAlloc(alloc, 1024 * 1024 * 1024);
···
90
101
defer parsed_config.deinit();
91
102
92
103
self.* = parsed_config.value;
93
-
self.config_path = config_location.home_dir.realpath(
94
-
config_location.path,
95
-
&self.config_path_buf,
96
-
) catch null;
97
-
return .{ .deprecated = deprecated };
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;
98
147
}
99
148
};
100
149
···
110
159
111
160
const NotificationStyles = struct {
112
161
box: vaxis.Style = vaxis.Style{
162
+
.fg = .{ .rgb = Colours.snow_white },
113
163
.bg = .{ .rgb = Colours.grey },
114
164
},
115
165
err: vaxis.Style = vaxis.Style{
···
126
176
},
127
177
};
128
178
179
+
pub 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
+
129
216
const Styles = struct {
130
217
selected_list_item: vaxis.Style = vaxis.Style{
131
218
.bg = .{ .rgb = Colours.grey },
···
145
232
},
146
233
};
147
234
148
-
pub var config: Config = Config{ .styles = Styles{} };
235
+
pub var config: Config = Config{};
+45
-58
src/directories.zig
+45
-58
src/directories.zig
···
5
5
const vaxis = @import("vaxis");
6
6
const fuzzig = @import("fuzzig");
7
7
8
-
const History = struct {
9
-
selected: usize,
10
-
offset: usize,
11
-
};
12
-
13
8
const history_len: usize = 100;
14
9
15
10
const Self = @This();
···
20
15
file_contents: [4096]u8 = undefined,
21
16
pdf_contents: ?[]u8 = null,
22
17
entries: List(std.fs.Dir.Entry),
23
-
history: CircStack(History, history_len),
18
+
history: CircStack(usize, history_len),
24
19
child_entries: List([]const u8),
25
20
searcher: fuzzig.Ascii,
26
21
27
-
pub fn init(alloc: std.mem.Allocator) !Self {
22
+
pub fn init(alloc: std.mem.Allocator, entry_dir: ?[]const u8) !Self {
23
+
const dir_path = if (entry_dir) |dir| dir else ".";
24
+
const dir = std.fs.cwd().openDir(dir_path, .{ .iterate = true }) catch |err| {
25
+
switch (err) {
26
+
error.FileNotFound => {
27
+
std.log.err("path '{s}' could not be found.", .{dir_path});
28
+
return err;
29
+
},
30
+
else => {
31
+
std.log.err("{}", .{err});
32
+
return err;
33
+
},
34
+
}
35
+
};
36
+
28
37
return Self{
29
38
.alloc = alloc,
30
-
.dir = try std.fs.cwd().openDir(".", .{ .iterate = true }),
39
+
.dir = dir,
31
40
.entries = List(std.fs.Dir.Entry).init(alloc),
32
-
.history = CircStack(History, history_len).init(),
41
+
.history = CircStack(usize, history_len).init(),
33
42
.child_entries = List([]const u8).init(alloc),
34
43
.searcher = try fuzzig.Ascii.init(
35
44
alloc,
···
71
80
return try self.dir.realpath(relative_path, &self.path_buf);
72
81
}
73
82
83
+
pub fn getDirSize(self: Self, dir: std.fs.Dir) !usize {
84
+
var total_size: usize = 0;
85
+
86
+
var walker = try dir.walk(self.alloc);
87
+
defer walker.deinit();
88
+
89
+
while (try walker.next()) |entry| {
90
+
switch (entry.kind) {
91
+
.file => {
92
+
const stat = try entry.dir.statFile(entry.basename);
93
+
total_size += stat.size;
94
+
},
95
+
else => {},
96
+
}
97
+
}
98
+
99
+
return total_size;
100
+
}
101
+
74
102
pub fn populateChildEntries(
75
103
self: *Self,
76
104
relative_path: []const u8,
···
80
108
81
109
var it = dir.iterate();
82
110
while (try it.next()) |entry| {
111
+
if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) {
112
+
continue;
113
+
}
114
+
83
115
try self.child_entries.append(try self.alloc.dupe(u8, entry.name));
84
116
}
85
117
···
88
120
}
89
121
}
90
122
91
-
pub fn writeChildEntries(
92
-
self: *Self,
93
-
window: vaxis.Window,
94
-
style: vaxis.Style,
95
-
) !void {
96
-
for (self.child_entries.all(), 0..) |item, i| {
97
-
if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) {
98
-
continue;
99
-
}
100
-
101
-
if (i > window.height) continue;
102
-
103
-
const w = window.child(.{ .y_off = @intCast(i), .height = 1 });
104
-
w.fill(vaxis.Cell{ .style = style });
105
-
106
-
_ = w.print(&.{.{ .text = item, .style = style }}, .{});
107
-
}
108
-
}
109
-
110
123
pub fn populateEntries(self: *Self, fuzzy_search: []const u8) !void {
111
124
var it = self.dir.iterate();
112
125
while (try it.next()) |entry| {
···
115
128
continue;
116
129
}
117
130
131
+
if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) {
132
+
continue;
133
+
}
134
+
118
135
try self.entries.append(.{
119
136
.kind = entry.kind,
120
137
.name = try self.alloc.dupe(u8, entry.name),
···
123
140
124
141
if (config.sort_dirs == true) {
125
142
std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sortEntry);
126
-
}
127
-
}
128
-
129
-
pub fn writeEntries(
130
-
self: *Self,
131
-
window: vaxis.Window,
132
-
selected_list_item_style: vaxis.Style,
133
-
list_item_style: vaxis.Style,
134
-
) !void {
135
-
for (self.entries.all()[self.entries.offset..], 0..) |item, i| {
136
-
const selected = self.entries.selected - self.entries.offset;
137
-
const is_selected = selected == i;
138
-
139
-
if (std.mem.startsWith(u8, item.name, ".") and config.show_hidden == false) {
140
-
continue;
141
-
}
142
-
143
-
if (i > window.height) continue;
144
-
145
-
const w = window.child(.{ .y_off = @intCast(i), .height = 1 });
146
-
w.fill(vaxis.Cell{
147
-
.style = if (is_selected) selected_list_item_style else list_item_style,
148
-
});
149
-
150
-
_ = w.print(&.{
151
-
.{
152
-
.text = item.name,
153
-
.style = if (is_selected) selected_list_item_style else list_item_style,
154
-
},
155
-
}, .{});
156
143
}
157
144
}
158
145
+416
-102
src/drawer.zig
+416
-102
src/drawer.zig
···
1
1
const std = @import("std");
2
2
const App = @import("./app.zig");
3
+
const FileLogger = @import("./file_logger.zig");
3
4
const Notification = @import("./notification.zig");
4
5
const Directories = @import("./directories.zig");
5
6
const config = &@import("./config.zig").config;
6
7
const vaxis = @import("vaxis");
7
8
const Git = @import("./git.zig");
8
-
const inputToSlice = @import("./event_handlers.zig").inputToSlice;
9
+
const List = @import("./list.zig").List;
10
+
const zeit = @import("zeit");
9
11
10
12
const Drawer = @This();
11
13
12
14
const top_div: u16 = 1;
13
15
const info_div: u16 = 1;
14
-
const bottom_div: u16 = 1;
15
16
16
17
// Used to detect whether to re-render an image.
17
18
current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
···
21
22
file_info_buf: [std.fs.max_path_bytes]u8 = undefined,
22
23
file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [<file_name>]
23
24
git_branch: [1024]u8 = undefined,
25
+
verbose: bool = false,
24
26
25
-
pub fn draw(self: *Drawer, app: *App) !void {
27
+
pub fn draw(self: *Drawer, app: *App) error{ OutOfMemory, NoSpaceLeft }!void {
26
28
const win = app.vx.window();
27
29
win.clear();
28
30
29
-
const abs_file_path_bar = try self.drawAbsFilePath(app.alloc, &app.directories, win);
30
-
const file_info_bar = try self.drawFileInfo(&app.directories, win);
31
-
app.last_known_height = try drawDirList(
32
-
&app.directories,
31
+
if (app.state == .help_menu) {
32
+
win.hideCursor();
33
+
const offset: usize = app.help_menu.selected;
34
+
for (app.help_menu.all()[offset..], 0..) |item, i| {
35
+
if (i > win.height) continue;
36
+
37
+
const w = win.child(.{ .y_off = @intCast(i), .height = 1 });
38
+
w.fill(vaxis.Cell{
39
+
.style = config.styles.list_item,
40
+
});
41
+
42
+
_ = w.print(&.{.{
43
+
.text = item,
44
+
.style = config.styles.list_item,
45
+
}}, .{});
46
+
}
47
+
48
+
return;
49
+
}
50
+
51
+
const abs_file_path_bar = try self.drawAbsFilePath(app, win);
52
+
const file_info_bar = try self.drawFileInfo(app.alloc, &app.directories, win);
53
+
app.last_known_height = drawDirList(
33
54
win,
55
+
app.directories.entries,
34
56
abs_file_path_bar,
35
57
file_info_bar,
36
58
);
37
59
38
-
if (config.preview_file == true) {
60
+
if (config.preview_file) {
39
61
const file_name_bar = try self.drawFileName(&app.directories, win);
40
62
try self.drawFilePreview(app, win, file_name_bar);
41
63
}
42
64
43
-
const input = inputToSlice(app);
44
-
try drawUserInput(app.state, &app.text_input, input, win);
45
-
try drawNotification(&app.notification, win);
65
+
const input = app.inputToSlice();
66
+
drawUserInput(app.state, &app.text_input, input, win);
67
+
68
+
// Notification should be drawn last.
69
+
drawNotification(&app.notification, &app.file_logger, win);
46
70
}
47
71
48
72
fn drawFileName(
49
73
self: *Drawer,
50
74
directories: *Directories,
51
75
win: vaxis.Window,
52
-
) !vaxis.Window {
76
+
) error{NoSpaceLeft}!vaxis.Window {
53
77
const file_name_bar = win.child(.{
54
78
.x_off = win.width / 2,
55
79
.y_off = 0,
···
57
81
.height = top_div,
58
82
});
59
83
60
-
lbl: {
61
-
const entry = directories.getSelected() catch break :lbl;
62
-
if (entry) |e| {
63
-
const file_name = try std.fmt.bufPrint(
64
-
&self.file_name_buf,
65
-
"[{s}]",
66
-
.{e.name},
67
-
);
68
-
_ = file_name_bar.print(&.{vaxis.Segment{
69
-
.text = file_name,
70
-
.style = config.styles.file_name,
71
-
}}, .{});
72
-
}
73
-
}
84
+
const entry = lbl: {
85
+
const entry = directories.getSelected() catch return file_name_bar;
86
+
if (entry) |e| break :lbl e else return file_name_bar;
87
+
};
88
+
89
+
const file_name = try std.fmt.bufPrint(&self.file_name_buf, "[{s}]", .{entry.name});
90
+
_ = file_name_bar.printSegment(.{ .text = file_name, .style = config.styles.file_name }, .{});
74
91
75
92
return file_name_bar;
76
93
}
···
80
97
app: *App,
81
98
win: vaxis.Window,
82
99
file_name_win: vaxis.Window,
83
-
) !void {
100
+
) error{ OutOfMemory, NoSpaceLeft }!void {
101
+
const bottom_div: u16 = 1;
102
+
84
103
const preview_win = win.child(.{
85
104
.x_off = win.width / 2,
86
105
.y_off = top_div + 1,
···
100
119
self.current_item_path = try std.fmt.bufPrint(
101
120
&self.current_item_path_buf,
102
121
"{s}/{s}",
103
-
.{ try app.directories.fullPath("."), entry.name },
122
+
.{ app.directories.fullPath(".") catch {
123
+
const message = try std.fmt.allocPrint(app.alloc, "Can not display file - unable to retrieve directory path.", .{});
124
+
defer app.alloc.free(message);
125
+
app.notification.write(message, .err) catch {};
126
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
127
+
128
+
_ = preview_win.print(&.{
129
+
.{ .text = "Can not display file - unable to retrieve directory path. No preview available." },
130
+
}, .{});
131
+
return;
132
+
}, entry.name },
104
133
);
105
134
106
135
switch (entry.kind) {
107
136
.directory => {
108
137
app.directories.clearChildEntries();
109
-
if (app.directories.populateChildEntries(entry.name)) {
110
-
try app.directories.writeChildEntries(preview_win, config.styles.list_item);
111
-
} else |err| {
112
-
switch (err) {
113
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
114
-
else => try app.notification.writeErr(.UnknownError),
138
+
app.directories.populateChildEntries(entry.name) catch |err| {
139
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to populate child directory entries - {}.", .{err});
140
+
defer app.alloc.free(message);
141
+
app.notification.write(message, .err) catch {};
142
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
143
+
144
+
_ = preview_win.print(&.{
145
+
.{ .text = "Failed to populate child directory entries. No preview available." },
146
+
}, .{});
147
+
148
+
return;
149
+
};
150
+
151
+
for (app.directories.child_entries.all(), 0..) |item, i| {
152
+
if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) {
153
+
continue;
115
154
}
155
+
if (i > preview_win.height) continue;
156
+
const w = preview_win.child(.{ .y_off = @intCast(i), .height = 1 });
157
+
w.fill(vaxis.Cell{ .style = config.styles.list_item });
158
+
_ = w.print(&.{.{ .text = item, .style = config.styles.list_item }}, .{});
116
159
}
117
160
},
118
161
.file => file: {
119
-
var file = app.directories.dir.openFile(entry.name, .{ .mode = .read_only }) catch |err| {
120
-
switch (err) {
121
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
122
-
else => try app.notification.writeErr(.UnknownError),
123
-
}
162
+
var file = app.directories.dir.openFile(
163
+
entry.name,
164
+
.{ .mode = .read_only },
165
+
) catch |err| {
166
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to open file - {}.", .{err});
167
+
defer app.alloc.free(message);
168
+
app.notification.write(message, .err) catch {};
169
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
124
170
125
171
_ = preview_win.print(&.{
126
-
.{ .text = "No preview available." },
172
+
.{ .text = "Failed to open file. No preview available." },
127
173
}, .{});
128
174
129
175
break :file;
130
176
};
131
177
defer file.close();
132
-
const bytes = try file.readAll(&app.directories.file_contents);
178
+
const bytes = file.readAll(&app.directories.file_contents) catch |err| {
179
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to read file contents - {}.", .{err});
180
+
defer app.alloc.free(message);
181
+
app.notification.write(message, .err) catch {};
182
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
183
+
184
+
_ = preview_win.print(&.{
185
+
.{ .text = "Failed to read file contents. No preview available." },
186
+
}, .{});
187
+
188
+
break :file;
189
+
};
133
190
134
191
// Handle image.
135
192
if (config.show_images == true) unsupported: {
136
193
var match = false;
137
-
inline for (@typeInfo(vaxis.zigimg.Image.Format).Enum.fields) |field| {
138
-
const entry_ext = std.mem.trimLeft(
139
-
u8,
140
-
std.fs.path.extension(entry.name),
141
-
".",
142
-
);
143
-
if (std.mem.eql(u8, entry_ext, field.name)) {
144
-
match = true;
145
-
}
194
+
inline for (@typeInfo(vaxis.zigimg.Image.Format).@"enum".fields) |field| {
195
+
const entry_ext = std.mem.trimLeft(u8, std.fs.path.extension(entry.name), ".");
196
+
if (std.mem.eql(u8, entry_ext, field.name)) match = true;
146
197
}
147
198
if (!match) break :unsupported;
148
199
149
-
if (std.mem.eql(u8, self.last_item_path, self.current_item_path)) break :unsupported;
200
+
app.images.mutex.lock();
201
+
defer app.images.mutex.unlock();
202
+
203
+
if (app.images.cache.getPtr(self.current_item_path)) |cache_entry| {
204
+
if (cache_entry.status == .processing) {
205
+
_ = preview_win.print(&.{
206
+
.{ .text = "Image still processing." },
207
+
}, .{});
208
+
break :file;
209
+
}
210
+
211
+
if (cache_entry.status == .failed) {
212
+
_ = preview_win.print(&.{
213
+
.{ .text = "Failed to process image." },
214
+
}, .{});
215
+
break :file;
216
+
}
217
+
218
+
if (cache_entry.image) |img| {
219
+
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
220
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
221
+
defer app.alloc.free(message);
222
+
app.notification.write(message, .err) catch {};
223
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
224
+
225
+
_ = preview_win.print(&.{
226
+
.{ .text = "Failed to draw image to screen. No preview available." },
227
+
}, .{});
228
+
cache_entry.image = null;
229
+
break :file;
230
+
};
231
+
} else {
232
+
if (cache_entry.data == null) {
233
+
const path = try app.alloc.dupe(u8, self.current_item_path);
234
+
processImage(app, path) catch {
235
+
app.alloc.free(path);
236
+
break :unsupported;
237
+
};
238
+
_ = preview_win.print(&.{
239
+
.{ .text = "Image still processing." },
240
+
}, .{});
241
+
break :file;
242
+
}
150
243
151
-
var image = vaxis.zigimg.Image.fromFilePath(
152
-
app.alloc,
153
-
self.current_item_path,
154
-
) catch {
155
-
break :unsupported;
156
-
};
157
-
defer image.deinit();
244
+
if (app.vx.transmitImage(app.alloc, app.tty.writer(), &cache_entry.data.?, .rgba)) |img| {
245
+
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
246
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
247
+
defer app.alloc.free(message);
248
+
app.notification.write(message, .err) catch {};
249
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
158
250
159
-
if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &image, .rgba)) |img| {
160
-
app.image = img;
161
-
} else |_| {
162
-
if (app.image) |img| {
163
-
app.vx.freeImage(app.tty.anyWriter(), img.id);
251
+
_ = preview_win.print(&.{
252
+
.{ .text = "Failed to draw image to screen. No preview available." },
253
+
}, .{});
254
+
break :file;
255
+
};
256
+
cache_entry.image = img;
257
+
if (cache_entry.data) |data| {
258
+
var d = data;
259
+
d.deinit(app.alloc);
260
+
}
261
+
cache_entry.data = null;
262
+
} else |_| {
263
+
break :unsupported;
264
+
}
164
265
}
165
-
app.image = null;
166
-
break :unsupported;
167
-
}
168
266
169
-
if (app.image) |img| {
170
-
try img.draw(preview_win, .{ .scale = .contain });
267
+
break :file;
268
+
} else {
269
+
_ = preview_win.print(&.{
270
+
.{ .text = "Processing image." },
271
+
}, .{});
272
+
273
+
const path = try app.alloc.dupe(u8, self.current_item_path);
274
+
processImage(app, path) catch {
275
+
app.alloc.free(path);
276
+
break :unsupported;
277
+
};
171
278
}
172
279
173
280
break :file;
···
233
340
234
341
fn drawFileInfo(
235
342
self: *Drawer,
343
+
alloc: std.mem.Allocator,
236
344
directories: *Directories,
237
345
win: vaxis.Window,
238
-
) !vaxis.Window {
346
+
) error{NoSpaceLeft}!vaxis.Window {
347
+
const bottom_div: u16 = if (self.verbose) 6 else 1;
348
+
239
349
const file_info_win = win.child(.{
240
350
.x_off = 0,
241
351
.y_off = win.height - bottom_div,
···
250
360
};
251
361
252
362
var fbs = std.io.fixedBufferStream(&self.file_info_buf);
363
+
364
+
// Selected entry.
253
365
try fbs.writer().print(
254
-
"{d}/{d} ",
255
-
.{ directories.entries.selected + 1, directories.entries.len() },
366
+
"{s}{d}/{d}{s}",
367
+
.{
368
+
if (self.verbose) "Entry: " else "",
369
+
directories.entries.selected + 1,
370
+
directories.entries.len(),
371
+
if (self.verbose) "\n" else " ",
372
+
},
256
373
);
257
374
258
-
if (entry.kind == .directory) {
259
-
_ = file_info_win.printSegment(.{
260
-
.text = fbs.getWritten(),
261
-
.style = config.styles.file_information,
262
-
}, .{});
263
-
return file_info_win;
375
+
// Time created / last modified
376
+
if (self.verbose) lbl: {
377
+
var maybe_meta: ?std.fs.File.Stat = null;
378
+
if (entry.kind == .directory) {
379
+
maybe_meta = directories.dir.stat() catch break :lbl;
380
+
} else if (entry.kind == .file) {
381
+
var file = directories.dir.openFile(entry.name, .{}) catch break :lbl;
382
+
maybe_meta = file.stat() catch break :lbl;
383
+
}
384
+
385
+
const meta = maybe_meta orelse break :lbl;
386
+
var env = std.process.getEnvMap(alloc) catch break :lbl;
387
+
defer env.deinit();
388
+
const local = zeit.local(alloc, &env) catch break :lbl;
389
+
defer local.deinit();
390
+
391
+
const ctime_instant = zeit.instant(.{
392
+
.source = .{ .unix_nano = meta.ctime },
393
+
.timezone = &local,
394
+
}) catch break :lbl;
395
+
const ctime = ctime_instant.time();
396
+
ctime.strftime(fbs.writer().any(), "Created: %Y-%m-%d %H:%M:%S\n") catch break :lbl;
397
+
398
+
const mtime_instant = zeit.instant(.{
399
+
.source = .{ .unix_nano = meta.mtime },
400
+
.timezone = &local,
401
+
}) catch break :lbl;
402
+
const mtime = mtime_instant.time();
403
+
mtime.strftime(fbs.writer().any(), "Last modified: %Y-%m-%d %H:%M:%S\n") catch break :lbl;
264
404
}
265
405
266
-
const file_size: u64 = lbl: {
267
-
const formatted_size = directories.dir.statFile(entry.name) catch break :lbl 0;
268
-
break :lbl formatted_size.size;
406
+
// File permissions.
407
+
var file_perm_buf: [11]u8 = undefined;
408
+
const file_perms: usize = lbl: {
409
+
if (self.verbose) try fbs.writer().writeAll("Permissions: ");
410
+
var file_perm_fbs = std.io.fixedBufferStream(&file_perm_buf);
411
+
412
+
if (entry.kind == .directory) {
413
+
_ = try file_perm_fbs.write("d");
414
+
}
415
+
416
+
const perm_strings = [_][]const u8{
417
+
"---", "--x", "-w-", "-wx",
418
+
"r--", "r-x", "rw-", "rwx",
419
+
};
420
+
421
+
const stat = directories.dir.statFile(entry.name) catch {
422
+
_ = try file_perm_fbs.write("---------\n");
423
+
break :lbl 10;
424
+
};
425
+
// Ignore upper bytes as they represent file type.
426
+
const perms = @as(u9, @truncate(stat.mode));
427
+
428
+
for (0..3) |group| {
429
+
const shift: u4 = @truncate((2 - group) * 3); // Extract from left to right
430
+
const perm = @as(u3, @truncate((perms >> shift) & 0b111));
431
+
_ = try file_perm_fbs.write(perm_strings[perm]);
432
+
}
433
+
434
+
if (self.verbose) {
435
+
_ = try file_perm_fbs.write("\n");
436
+
} else {
437
+
_ = try file_perm_fbs.write(" ");
438
+
}
439
+
440
+
if (entry.kind == .directory) {
441
+
break :lbl 11;
442
+
} else {
443
+
break :lbl 10;
444
+
}
269
445
};
446
+
try fbs.writer().writeAll(file_perm_buf[0..file_perms]);
270
447
271
-
const extension = std.fs.path.extension(entry.name);
272
-
if (extension.len > 0) try fbs.writer().print("{s} ", .{extension});
448
+
// Size.
449
+
const size: ?usize = lbl: {
450
+
const stat = directories.dir.statFile(entry.name) catch break :lbl null;
451
+
if (entry.kind == .file) {
452
+
break :lbl stat.size;
453
+
} else if (entry.kind == .directory) {
454
+
if (config.true_dir_size) {
455
+
var dir = directories.dir.openDir(
456
+
entry.name,
457
+
.{ .iterate = true },
458
+
) catch break :lbl null;
459
+
defer dir.close();
460
+
break :lbl directories.getDirSize(dir) catch break :lbl null;
461
+
} else {
462
+
break :lbl stat.size;
463
+
}
464
+
}
465
+
466
+
break :lbl 0;
467
+
};
468
+
if (size) |s| try fbs.writer().print("{s}{B:.2}\n", .{
469
+
if (self.verbose) "Size: " else "",
470
+
s,
471
+
});
273
472
274
-
try fbs.writer().print("{:.2}", .{std.fmt.fmtIntSizeDec(file_size)});
473
+
// Extension.
474
+
const extension = std.fs.path.extension(entry.name);
475
+
if (self.verbose) {
476
+
try fbs.writer().print(
477
+
"Extension: {s}\n",
478
+
.{if (entry.kind == .directory) "Dir" else extension},
479
+
);
480
+
} else {
481
+
try fbs.writer().print(
482
+
"{s} ",
483
+
.{if (entry.kind == .directory) "dir" else extension},
484
+
);
485
+
}
275
486
276
487
_ = file_info_win.printSegment(.{
277
488
.text = fbs.getWritten(),
···
282
493
}
283
494
284
495
fn drawDirList(
285
-
directories: *Directories,
286
496
win: vaxis.Window,
497
+
list: List(std.fs.Dir.Entry),
287
498
abs_file_path: vaxis.Window,
288
499
file_information: vaxis.Window,
289
-
) !u16 {
500
+
) u16 {
501
+
const bottom_div: u16 = 1;
502
+
290
503
const current_dir_list_win = win.child(.{
291
504
.x_off = 0,
292
505
.y_off = top_div + 1,
293
506
.width = if (config.preview_file) win.width / 2 else win.width,
294
507
.height = win.height - (abs_file_path.height + file_information.height + top_div + bottom_div),
295
508
});
296
-
try directories.writeEntries(
297
-
current_dir_list_win,
298
-
config.styles.selected_list_item,
299
-
config.styles.list_item,
300
-
);
509
+
510
+
const win_height = current_dir_list_win.height;
511
+
var offset: usize = 0;
512
+
513
+
while (list.all()[offset..].len > win_height and
514
+
list.selected >= offset + (win_height / 2))
515
+
{
516
+
offset += 1;
517
+
}
518
+
519
+
for (list.all()[offset..], 0..) |item, i| {
520
+
const selected = list.selected - offset;
521
+
const is_selected = selected == i;
522
+
523
+
if (i > win_height) continue;
524
+
525
+
const w = current_dir_list_win.child(.{ .y_off = @intCast(i), .height = 1 });
526
+
w.fill(vaxis.Cell{
527
+
.style = if (is_selected) config.styles.selected_list_item else config.styles.list_item,
528
+
});
529
+
530
+
_ = w.print(&.{
531
+
.{
532
+
.text = item.name,
533
+
.style = if (is_selected) config.styles.selected_list_item else config.styles.list_item,
534
+
},
535
+
}, .{});
536
+
}
301
537
302
-
return current_dir_list_win.height;
538
+
return win_height;
303
539
}
304
540
305
541
fn drawAbsFilePath(
306
542
self: *Drawer,
307
-
alloc: std.mem.Allocator,
308
-
directories: *Directories,
543
+
app: *App,
309
544
win: vaxis.Window,
310
-
) !vaxis.Window {
545
+
) error{ OutOfMemory, NoSpaceLeft }!vaxis.Window {
311
546
const abs_file_path_bar = win.child(.{
312
547
.x_off = 0,
313
548
.y_off = 0,
···
315
550
.height = top_div,
316
551
});
317
552
318
-
const branch_alloc = try Git.getGitBranch(alloc, directories.dir);
319
-
defer if (branch_alloc) |b| alloc.free(b);
553
+
const branch_alloc = Git.getGitBranch(app.alloc, app.directories.dir) catch null;
554
+
defer if (branch_alloc) |b| app.alloc.free(b);
320
555
const branch = if (branch_alloc) |b|
321
556
try std.fmt.bufPrint(
322
557
&self.git_branch,
···
327
562
"";
328
563
329
564
_ = abs_file_path_bar.print(&.{
330
-
vaxis.Segment{ .text = try directories.fullPath(".") },
565
+
vaxis.Segment{ .text = app.directories.fullPath(".") catch {
566
+
const message = try std.fmt.allocPrint(app.alloc, "Can not display absolute file path - unable to retrieve full path.", .{});
567
+
defer app.alloc.free(message);
568
+
app.notification.write(message, .err) catch {};
569
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
570
+
return abs_file_path_bar;
571
+
} },
331
572
vaxis.Segment{ .text = if (branch_alloc != null) " on " else "" },
332
573
vaxis.Segment{ .text = branch, .style = config.styles.git_branch },
333
574
}, .{});
···
340
581
text_input: *vaxis.widgets.TextInput,
341
582
input: []const u8,
342
583
win: vaxis.Window,
343
-
) !void {
584
+
) void {
344
585
const user_input_win = win.child(.{
345
586
.x_off = 0,
346
587
.y_off = top_div,
···
351
592
352
593
switch (current_state) {
353
594
.fuzzy, .new_file, .new_dir, .rename, .change_dir, .command => {
354
-
text_input.draw(user_input_win);
595
+
text_input.drawWithStyle(user_input_win, config.styles.text_input);
355
596
},
356
597
.normal => {
357
598
if (text_input.buf.realLength() > 0) {
358
599
text_input.drawWithStyle(
359
600
user_input_win,
360
-
if (std.mem.eql(u8, input, ":UnsupportedCommand")) config.styles.text_input_err else .{},
601
+
if (std.mem.eql(u8, input, ":UnsupportedCommand"))
602
+
config.styles.text_input_err
603
+
else
604
+
config.styles.text_input,
361
605
);
362
606
}
363
607
364
608
win.hideCursor();
365
609
},
610
+
.help_menu => {
611
+
win.hideCursor();
612
+
},
366
613
}
367
614
}
368
615
369
616
fn drawNotification(
370
617
notification: *Notification,
618
+
file_logger: *?FileLogger,
371
619
win: vaxis.Window,
372
-
) !void {
620
+
) void {
373
621
if (notification.len() == 0) return;
374
622
if (notification.clearIfEnded()) return;
375
623
···
380
628
const max_width = win.width / 4;
381
629
const width = notification.len() + width_padding;
382
630
const calculated_width = if (width > max_width) max_width else width;
383
-
const height = try std.math.divCeil(usize, notification.len(), calculated_width) + height_padding;
631
+
const height = (std.math.divCeil(usize, notification.len(), calculated_width) catch {
632
+
if (file_logger.*) |fl| fl.write("Unable to display notification - failed to calculate notification height.", .err) catch {};
633
+
return;
634
+
}) + height_padding;
384
635
385
636
const notification_win = win.child(.{
386
637
.x_off = @intCast(win.width - (calculated_width + screen_pos_padding)),
···
400
651
.style = config.styles.notification.box,
401
652
}, .{ .wrap = .word });
402
653
}
654
+
655
+
fn processImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void {
656
+
app.images.cache.put(path, .{ .path = path, .status = .processing }) catch {
657
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path});
658
+
defer app.alloc.free(message);
659
+
app.notification.write(message, .err) catch {};
660
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
661
+
return error.Unsupported;
662
+
};
663
+
664
+
const load_img_thread = std.Thread.spawn(.{}, loadImage, .{
665
+
app,
666
+
path,
667
+
}) catch {
668
+
app.images.mutex.lock();
669
+
if (app.images.cache.getPtr(path)) |entry| {
670
+
entry.status = .failed;
671
+
}
672
+
app.images.mutex.unlock();
673
+
674
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to spawn processing thread.", .{path});
675
+
defer app.alloc.free(message);
676
+
app.notification.write(message, .err) catch {};
677
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
678
+
679
+
return error.Unsupported;
680
+
};
681
+
load_img_thread.detach();
682
+
}
683
+
684
+
fn loadImage(app: *App, path: []const u8) error{OutOfMemory}!void {
685
+
var buf: [(1024 * 1024) * 5]u8 = undefined; // 5mb
686
+
const data = vaxis.zigimg.Image.fromFilePath(app.alloc, path, &buf) catch {
687
+
app.images.mutex.lock();
688
+
if (app.images.cache.getPtr(path)) |entry| {
689
+
entry.status = .failed;
690
+
}
691
+
app.images.mutex.unlock();
692
+
693
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to read image from path.", .{path});
694
+
defer app.alloc.free(message);
695
+
app.notification.write(message, .err) catch {};
696
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
697
+
698
+
return;
699
+
};
700
+
701
+
app.images.mutex.lock();
702
+
if (app.images.cache.getPtr(path)) |entry| {
703
+
entry.status = .ready;
704
+
entry.data = data;
705
+
entry.path = path;
706
+
} else {
707
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to load image '{s}' - error occurred while attempting to add image to cache.", .{path});
708
+
defer app.alloc.free(message);
709
+
app.notification.write(message, .err) catch {};
710
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
711
+
return;
712
+
}
713
+
app.images.mutex.unlock();
714
+
715
+
app.loop.postEvent(.image_ready);
716
+
}
+26
src/environment.zig
+26
src/environment.zig
···
1
1
const std = @import("std");
2
+
const zuid = @import("zuid");
2
3
const builtin = @import("builtin");
3
4
4
5
pub fn getHomeDir() !?std.fs.Dir {
···
23
24
return null;
24
25
}
25
26
27
+
pub fn checkDuplicatePath(
28
+
buf: []u8,
29
+
dir: std.fs.Dir,
30
+
relative_path: []const u8,
31
+
) error{NoSpaceLeft}!struct {
32
+
path: []const u8,
33
+
had_duplicate: bool,
34
+
} {
35
+
var had_duplicate = false;
36
+
const new_path = if (fileExists(dir, relative_path)) lbl: {
37
+
had_duplicate = true;
38
+
const extension = std.fs.path.extension(relative_path);
39
+
break :lbl try std.fmt.bufPrint(
40
+
buf,
41
+
"{s}-{f}{s}",
42
+
.{ relative_path[0 .. relative_path.len - extension.len], zuid.new.v4(), extension },
43
+
);
44
+
} else lbl: {
45
+
break :lbl try std.fmt.bufPrint(buf, "{s}", .{relative_path});
46
+
};
47
+
48
+
return .{ .path = new_path, .had_duplicate = had_duplicate };
49
+
}
50
+
26
51
pub fn openFile(
27
52
alloc: std.mem.Allocator,
28
53
dir: std.fs.Dir,
···
68
93
return result;
69
94
}
70
95
96
+
///Deletes the contents of a directory but not the directory itself.
71
97
///Returns the amount of files failed to be delete.
72
98
pub fn deleteContents(dir: std.fs.Dir) !usize {
73
99
var failed: usize = 0;
+202
-390
src/event_handlers.zig
+202
-390
src/event_handlers.zig
···
6
6
const Key = vaxis.Key;
7
7
const config = &@import("./config.zig").config;
8
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();
12
-
return self.text_input.sliceToCursor(&self.text_input_buf);
13
-
}
9
+
const Keybinds = @import("./config.zig").Keybinds;
10
+
const events = @import("./events.zig");
14
11
15
-
pub fn handleNormalEvent(
12
+
pub fn handleGlobalEvent(
16
13
app: *App,
17
14
event: App.Event,
18
-
loop: *vaxis.Loop(App.Event),
19
-
) !void {
15
+
) error{OutOfMemory}!void {
20
16
switch (event) {
21
17
.key_press => |key| {
22
18
if ((key.codepoint == 'c' and key.mods.ctrl)) {
···
24
20
return;
25
21
}
26
22
27
-
switch (key.codepoint) {
28
-
'-', 'h', Key.left => {
29
-
app.text_input.clearAndFree();
30
-
31
-
if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| {
32
-
app.directories.dir.close();
33
-
app.directories.dir = dir;
34
-
35
-
app.directories.clearEntries();
36
-
const fuzzy = inputToSlice(app);
37
-
app.directories.populateEntries(fuzzy) catch |err| {
38
-
switch (err) {
39
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
40
-
else => try app.notification.writeErr(.UnknownError),
41
-
}
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.", .{});
42
30
};
43
-
44
-
if (app.directories.history.pop()) |history| {
45
-
app.directories.entries.selected = history.selected;
46
-
app.directories.entries.offset = history.offset;
47
-
}
48
-
} else |err| {
49
-
switch (err) {
50
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
51
-
else => try app.notification.writeErr(.UnknownError),
52
-
}
53
-
}
54
-
},
55
-
Key.enter, 'l', Key.right => {
56
-
const entry = lbl: {
57
-
const entry = app.directories.getSelected() catch return;
58
-
if (entry) |e| break :lbl e else return;
59
-
};
60
-
61
-
switch (entry.kind) {
62
-
.directory => {
63
-
app.text_input.clearAndFree();
64
-
65
-
if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| {
66
-
app.directories.dir.close();
67
-
app.directories.dir = dir;
68
-
69
-
_ = app.directories.history.push(.{
70
-
.selected = app.directories.entries.selected,
71
-
.offset = app.directories.entries.offset,
72
-
});
73
-
74
-
app.directories.clearEntries();
75
-
const fuzzy = inputToSlice(app);
76
-
app.directories.populateEntries(fuzzy) catch |err| {
77
-
switch (err) {
78
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
79
-
else => try app.notification.writeErr(.UnknownError),
80
-
}
81
-
};
82
-
} else |err| {
83
-
switch (err) {
84
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
85
-
else => try app.notification.writeErr(.UnknownError),
86
-
}
87
-
}
88
-
},
89
-
.file => {
90
-
if (environment.getEditor()) |editor| {
91
-
try app.vx.exitAltScreen(app.tty.anyWriter());
92
-
try app.vx.resetState(app.tty.anyWriter());
93
-
loop.stop();
94
-
95
-
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch {
96
-
try app.notification.writeErr(.UnableToOpenFile);
97
-
};
98
-
99
-
try loop.start();
100
-
try app.vx.enterAltScreen(app.tty.anyWriter());
101
-
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
102
-
app.vx.queueRefresh();
103
-
} else {
104
-
try app.notification.writeErr(.EditorNotSet);
105
-
}
106
-
},
107
-
else => {},
108
-
}
109
-
},
110
-
'j', Key.down => {
111
-
app.directories.entries.next(app.last_known_height);
112
-
},
113
-
'k', Key.up => {
114
-
app.directories.entries.previous(app.last_known_height);
115
-
},
116
-
'G' => {
117
-
app.directories.entries.selectLast(app.last_known_height);
118
-
},
119
-
'g' => app.directories.entries.selectFirst(),
120
-
'D' => {
121
-
const entry = lbl: {
122
-
const entry = app.directories.getSelected() catch {
123
-
try app.notification.writeErr(.UnableToDelete);
124
-
return;
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.", .{});
125
35
};
126
-
if (entry) |e| break :lbl e else return;
127
-
};
128
-
129
-
var old_path_buf: [std.fs.max_path_bytes]u8 = undefined;
130
-
const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf));
131
-
132
-
var trash_dir = dir: {
133
-
notfound: {
134
-
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
135
-
}
136
-
app.alloc.free(old_path);
137
-
try app.notification.writeErr(.ConfigPathNotFound);
138
-
return;
139
-
};
140
-
defer trash_dir.close();
141
-
var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined;
142
-
const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf);
143
-
144
-
if (std.mem.eql(u8, old_path, trash_dir_path)) {
145
-
try app.notification.writeErr(.CannotDeleteTrashDir);
146
-
app.alloc.free(old_path);
147
-
return;
148
-
}
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);
149
43
150
-
var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
151
-
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().toString() }));
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
+
}
152
54
153
-
if (app.directories.dir.rename(entry.name, tmp_path)) {
154
-
if (app.actions.push(.{
155
-
.delete = .{ .old = old_path, .new = tmp_path },
156
-
})) |prev_elem| {
157
-
app.alloc.free(prev_elem.delete.old);
158
-
app.alloc.free(prev_elem.delete.new);
159
-
}
55
+
pub 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
+
);
160
64
161
-
try app.notification.writeInfo(.Deleted);
162
-
app.directories.removeSelected();
163
-
} else |err| {
164
-
switch (err) {
165
-
error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints),
166
-
else => try app.notification.writeErr(.UnableToDelete),
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;
167
70
}
168
-
app.alloc.free(old_path);
169
-
app.alloc.free(tmp_path);
170
71
}
171
-
},
172
-
'd' => {
173
-
app.text_input.clearAndFree();
174
-
app.directories.clearEntries();
175
-
app.directories.populateEntries("") catch |err| {
176
-
switch (err) {
177
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
178
-
else => try app.notification.writeErr(.UnknownError),
179
-
}
180
-
};
181
-
app.state = .new_dir;
182
-
},
183
-
'%' => {
184
-
app.text_input.clearAndFree();
185
-
app.directories.clearEntries();
186
-
app.directories.populateEntries("") catch |err| {
187
-
switch (err) {
188
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
189
-
else => try app.notification.writeErr(.UnknownError),
190
-
}
191
-
};
192
-
app.state = .new_file;
193
-
},
194
-
'u' => {
195
-
if (app.actions.pop()) |action| {
196
-
const selected = app.directories.entries.selected;
197
-
198
-
switch (action) {
199
-
.delete => |a| {
200
-
defer app.alloc.free(a.new);
201
-
defer app.alloc.free(a.old);
72
+
}
73
+
break :lbl null;
74
+
};
202
75
203
-
// TODO: Will overwrite an item if it has the same name.
204
-
if (app.directories.dir.rename(a.new, a.old)) {
205
-
app.directories.clearEntries();
206
-
const fuzzy = inputToSlice(app);
207
-
app.directories.populateEntries(fuzzy) catch |err| {
208
-
switch (err) {
209
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
210
-
else => try app.notification.writeErr(.UnknownError),
211
-
}
212
-
};
213
-
try app.notification.writeInfo(.RestoredDelete);
214
-
} else |_| {
215
-
try app.notification.writeErr(.UnableToUndo);
216
-
}
217
-
},
218
-
.rename => |a| {
219
-
defer app.alloc.free(a.new);
220
-
defer app.alloc.free(a.old);
221
-
222
-
// TODO: Will overwrite an item if it has the same name.
223
-
if (app.directories.dir.rename(a.new, a.old)) {
224
-
app.directories.clearEntries();
225
-
const fuzzy = inputToSlice(app);
226
-
app.directories.populateEntries(fuzzy) catch |err| {
227
-
switch (err) {
228
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
229
-
else => try app.notification.writeErr(.UnknownError),
230
-
}
231
-
};
232
-
try app.notification.writeInfo(.RestoredRename);
233
-
} else |_| {
234
-
try app.notification.writeErr(.UnableToUndo);
235
-
}
236
-
},
237
-
}
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;
238
85
239
-
app.directories.entries.selected = selected;
240
-
} else {
241
-
try app.notification.writeInfo(.EmptyUndo);
242
-
}
243
-
},
244
-
'/' => {
245
-
app.text_input.clearAndFree();
246
-
app.state = .fuzzy;
247
-
},
248
-
'R' => {
249
-
app.text_input.clearAndFree();
250
-
app.state = .rename;
86
+
app.text_input.clearAndFree();
251
87
252
-
const entry = lbl: {
253
-
const entry = app.directories.getSelected() catch {
254
-
app.state = .normal;
255
-
try app.notification.writeErr(.UnableToRename);
256
-
return;
257
-
};
258
-
if (entry) |e| break :lbl e else {
259
-
app.state = .normal;
260
-
return;
261
-
}
262
-
};
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
+
},
263
93
264
-
app.text_input.insertSliceAtCursor(entry.name) catch {
265
-
app.state = .normal;
266
-
try app.notification.writeErr(.UnableToRename);
267
-
return;
268
-
};
269
-
},
270
-
'c' => {
271
-
app.text_input.clearAndFree();
272
-
app.state = .change_dir;
273
-
},
274
-
':' => {
275
-
app.text_input.clearAndFree();
276
-
app.text_input.insertSliceAtCursor(":") catch {};
277
-
app.state = .command;
278
-
},
279
-
else => {},
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
+
}
280
133
}
281
134
},
282
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
135
+
.image_ready => {},
136
+
.notification => {},
137
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
283
138
}
284
139
}
285
140
286
141
pub fn handleInputEvent(app: *App, event: App.Event) !void {
287
142
switch (event) {
288
143
.key_press => |key| {
289
-
if ((key.codepoint == 'c' and key.mods.ctrl)) {
290
-
app.should_quit = true;
291
-
return;
292
-
}
293
-
294
144
switch (key.codepoint) {
295
145
Key.escape => {
296
146
switch (app.state) {
297
147
.fuzzy => {
298
-
app.directories.clearEntries();
299
-
app.directories.populateEntries("") catch |err| {
300
-
switch (err) {
301
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
302
-
else => try app.notification.writeErr(.UnknownError),
303
-
}
304
-
};
148
+
try app.repopulateDirectory("");
149
+
app.text_input.clearAndFree();
305
150
},
151
+
.command => app.command_history.cursor = null,
306
152
else => {},
307
153
}
308
154
···
312
158
Key.enter => {
313
159
const selected = app.directories.entries.selected;
314
160
switch (app.state) {
315
-
.new_dir => {
316
-
const dir = inputToSlice(app);
317
-
if (app.directories.dir.makeDir(dir)) {
318
-
try app.notification.writeInfo(.CreatedFolder);
319
-
320
-
app.directories.clearEntries();
321
-
app.directories.populateEntries("") catch |err| {
322
-
switch (err) {
323
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
324
-
else => try app.notification.writeErr(.UnknownError),
325
-
}
326
-
};
327
-
} else |err| {
328
-
switch (err) {
329
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
330
-
error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists),
331
-
else => try app.notification.writeErr(.UnknownError),
332
-
}
333
-
}
334
-
app.text_input.clearAndFree();
335
-
},
336
-
.new_file => {
337
-
const file = inputToSlice(app);
338
-
if (environment.fileExists(app.directories.dir, file)) {
339
-
try app.notification.writeErr(.ItemAlreadyExists);
340
-
} else {
341
-
if (app.directories.dir.createFile(file, .{})) |f| {
342
-
f.close();
343
-
344
-
try app.notification.writeInfo(.CreatedFile);
345
-
346
-
app.directories.clearEntries();
347
-
app.directories.populateEntries("") catch |err| {
348
-
switch (err) {
349
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
350
-
else => try app.notification.writeErr(.UnknownError),
351
-
}
352
-
};
353
-
} else |err| {
354
-
switch (err) {
355
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
356
-
else => try app.notification.writeErr(.UnknownError),
357
-
}
358
-
}
359
-
}
360
-
app.text_input.clearAndFree();
361
-
},
362
-
.rename => {
363
-
var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined;
364
-
const dir_prefix = try app.directories.dir.realpath(".", &dir_prefix_buf);
365
-
366
-
const old = lbl: {
367
-
const entry = app.directories.getSelected() catch {
368
-
try app.notification.writeErr(.UnableToRename);
369
-
return;
370
-
};
371
-
if (entry) |e| break :lbl e else return;
372
-
};
373
-
const new = inputToSlice(app);
374
-
375
-
if (environment.fileExists(app.directories.dir, new)) {
376
-
try app.notification.writeErr(.ItemAlreadyExists);
377
-
} else {
378
-
app.directories.dir.rename(old.name, new) catch |err| switch (err) {
379
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
380
-
error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists),
381
-
else => try app.notification.writeErr(.UnknownError),
382
-
};
383
-
if (app.actions.push(.{
384
-
.rename = .{
385
-
.old = try std.fs.path.join(app.alloc, &.{ dir_prefix, old.name }),
386
-
.new = try std.fs.path.join(app.alloc, &.{ dir_prefix, new }),
387
-
},
388
-
})) |prev_elem| {
389
-
app.alloc.free(prev_elem.rename.old);
390
-
app.alloc.free(prev_elem.rename.new);
391
-
}
392
-
393
-
try app.notification.writeInfo(.Renamed);
394
-
395
-
app.directories.clearEntries();
396
-
app.directories.populateEntries("") catch |err| {
397
-
switch (err) {
398
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
399
-
else => try app.notification.writeErr(.UnknownError),
400
-
}
401
-
};
402
-
}
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);
403
167
app.text_input.clearAndFree();
404
168
},
405
-
.change_dir => {
406
-
const path = inputToSlice(app);
407
-
if (app.directories.dir.openDir(path, .{ .iterate = true })) |dir| {
408
-
app.directories.dir.close();
409
-
app.directories.dir = dir;
410
-
411
-
try app.notification.writeInfo(.ChangedDir);
169
+
.command => {
170
+
const command = app.inputToSlice();
412
171
413
-
app.directories.clearEntries();
414
-
app.directories.populateEntries("") catch |err| {
415
-
switch (err) {
416
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
417
-
else => try app.notification.writeErr(.UnknownError),
418
-
}
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 {};
419
178
};
420
-
app.directories.history.reset();
421
-
} else |err| {
422
-
switch (err) {
423
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
424
-
error.FileNotFound => try app.notification.writeErr(.IncorrectPath),
425
-
error.NotDir => try app.notification.writeErr(.IncorrectPath),
426
-
else => try app.notification.writeErr(.UnknownError),
427
-
}
428
179
}
429
-
430
-
app.text_input.clearAndFree();
431
-
},
432
-
.command => {
433
-
const command = inputToSlice(app);
434
180
435
181
supported: {
436
182
if (std.mem.eql(u8, command, ":q")) {
···
448
194
break :supported;
449
195
}
450
196
451
-
// TODO(06-01-25): Add a confirmation for this.
197
+
if (std.mem.startsWith(u8, command, ":cd ")) {
198
+
try commands.cd(app, command[":cd ".len..]);
199
+
break :supported;
200
+
}
201
+
452
202
if (std.mem.eql(u8, command, ":empty_trash")) {
453
203
try commands.emptyTrash(app);
454
204
break :supported;
455
205
}
456
206
207
+
if (std.mem.eql(u8, command, ":h")) {
208
+
app.state = .help_menu;
209
+
break :supported;
210
+
}
211
+
457
212
app.text_input.clearAndFree();
458
213
try app.text_input.insertSliceAtCursor(":UnsupportedCommand");
459
214
}
215
+
216
+
app.command_history.cursor = null;
460
217
},
461
218
else => {},
462
219
}
463
-
app.state = .normal;
220
+
221
+
if (app.state != .help_menu) app.state = .normal;
464
222
app.directories.entries.selected = selected;
465
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
+
},
466
259
else => {
467
260
try app.text_input.update(.{ .key_press = key });
468
261
469
262
switch (app.state) {
470
263
.fuzzy => {
471
-
app.directories.clearEntries();
472
-
const fuzzy = inputToSlice(app);
473
-
app.directories.populateEntries(fuzzy) catch |err| {
474
-
switch (err) {
475
-
error.AccessDenied => try app.notification.writeErr(.PermissionDenied),
476
-
else => try app.notification.writeErr(.UnknownError),
477
-
}
478
-
};
264
+
const fuzzy = app.inputToSlice();
265
+
try app.repopulateDirectory(fuzzy);
479
266
},
480
267
.command => {
481
-
const command = inputToSlice(app);
268
+
const command = app.inputToSlice();
482
269
if (!std.mem.startsWith(u8, command, ":")) {
483
270
app.text_input.clearAndFree();
484
-
try app.text_input.insertSliceAtCursor(":");
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
+
};
485
279
}
486
280
},
487
281
else => {},
···
489
283
},
490
284
}
491
285
},
492
-
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
286
+
.image_ready => {},
287
+
.notification => {},
288
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
289
+
}
290
+
}
291
+
292
+
pub 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),
493
305
}
494
306
}
+571
src/events.zig
+571
src/events.zig
···
1
+
const std = @import("std");
2
+
const App = @import("./app.zig");
3
+
const config = &@import("./config.zig").config;
4
+
const zuid = @import("zuid");
5
+
const environment = @import("./environment.zig");
6
+
const vaxis = @import("vaxis");
7
+
8
+
pub fn delete(app: *App) error{OutOfMemory}!void {
9
+
var message: ?[]const u8 = null;
10
+
defer if (message) |msg| app.alloc.free(msg);
11
+
12
+
const entry = (app.directories.getSelected() catch {
13
+
app.notification.write("Can not to delete item - no item selected.", .warn) catch {};
14
+
return;
15
+
}) orelse return;
16
+
17
+
var prev_path_buf: [std.fs.max_path_bytes]u8 = undefined;
18
+
const prev_path = app.directories.dir.realpath(entry.name, &prev_path_buf) catch {
19
+
message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve absolute path.", .{entry.name});
20
+
app.notification.write(message.?, .err) catch {};
21
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
22
+
return;
23
+
};
24
+
const prev_path_alloc = try app.alloc.dupe(u8, prev_path);
25
+
26
+
var trash_dir = dir: {
27
+
notfound: {
28
+
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
29
+
}
30
+
app.alloc.free(prev_path_alloc);
31
+
message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve trash directory.", .{entry.name});
32
+
app.notification.write(message.?, .err) catch {};
33
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
34
+
return;
35
+
};
36
+
defer trash_dir.close();
37
+
38
+
var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined;
39
+
const trash_dir_path = trash_dir.realpath(".", &trash_dir_path_buf) catch {
40
+
message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - unable to retrieve absolute path for trash directory.", .{entry.name});
41
+
app.notification.write(message.?, .err) catch {};
42
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
43
+
return;
44
+
};
45
+
46
+
if (std.mem.eql(u8, prev_path_alloc, trash_dir_path)) {
47
+
app.notification.write("Can not delete trash directory.", .warn) catch {};
48
+
app.alloc.free(prev_path_alloc);
49
+
return;
50
+
}
51
+
52
+
const tmp_path = try std.fmt.allocPrint(app.alloc, "{s}/{s}-{f}", .{ trash_dir_path, entry.name, zuid.new.v4() });
53
+
if (app.directories.dir.rename(entry.name, tmp_path)) {
54
+
if (app.actions.push(.{
55
+
.delete = .{ .prev_path = prev_path_alloc, .new_path = tmp_path },
56
+
})) |prev_elem| {
57
+
app.alloc.free(prev_elem.delete.prev_path);
58
+
app.alloc.free(prev_elem.delete.new_path);
59
+
}
60
+
message = try std.fmt.allocPrint(app.alloc, "Deleted '{s}'.", .{entry.name});
61
+
app.notification.write(message.?, .info) catch {};
62
+
63
+
app.directories.removeSelected();
64
+
} else |err| {
65
+
app.alloc.free(prev_path_alloc);
66
+
app.alloc.free(tmp_path);
67
+
68
+
message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - {}.", .{ entry.name, err });
69
+
app.notification.write(message.?, .err) catch {};
70
+
}
71
+
}
72
+
73
+
pub fn rename(app: *App) error{OutOfMemory}!void {
74
+
var message: ?[]const u8 = null;
75
+
defer if (message) |msg| app.alloc.free(msg);
76
+
77
+
const entry = (app.directories.getSelected() catch {
78
+
app.notification.write("Can not to rename item - no item selected.", .warn) catch {};
79
+
return;
80
+
}) orelse return;
81
+
82
+
var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined;
83
+
const dir_prefix = app.directories.dir.realpath(".", &dir_prefix_buf) catch {
84
+
message = try std.fmt.allocPrint(app.alloc, "Failed to rename '{s}' - unable to retrieve absolute path.", .{entry.name});
85
+
app.notification.write(message.?, .err) catch {};
86
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
87
+
return;
88
+
};
89
+
90
+
const new_path = app.inputToSlice();
91
+
92
+
if (environment.fileExists(app.directories.dir, new_path)) {
93
+
message = try std.fmt.allocPrint(app.alloc, "Can not rename file - '{s}' already exists.", .{new_path});
94
+
app.notification.write(message.?, .warn) catch {};
95
+
} else {
96
+
app.directories.dir.rename(entry.name, new_path) catch |err| {
97
+
message = try std.fmt.allocPrint(app.alloc, "Failed to rename '{s}' - {}.", .{ new_path, err });
98
+
app.notification.write(message.?, .err) catch {};
99
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
100
+
return;
101
+
};
102
+
103
+
if (app.actions.push(.{
104
+
.rename = .{
105
+
.prev_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, entry.name }),
106
+
.new_path = try std.fs.path.join(app.alloc, &.{ dir_prefix, new_path }),
107
+
},
108
+
})) |prev_elem| {
109
+
app.alloc.free(prev_elem.rename.prev_path);
110
+
app.alloc.free(prev_elem.rename.new_path);
111
+
}
112
+
113
+
try app.repopulateDirectory("");
114
+
app.text_input.clearAndFree();
115
+
116
+
message = try std.fmt.allocPrint(app.alloc, "Renamed '{s}' to '{s}'.", .{ entry.name, new_path });
117
+
app.notification.write(message.?, .info) catch {};
118
+
}
119
+
120
+
app.text_input.clearAndFree();
121
+
}
122
+
123
+
pub fn forceDelete(app: *App) error{OutOfMemory}!void {
124
+
const entry = (app.directories.getSelected() catch {
125
+
app.notification.write("Can not force delete item - no item selected.", .warn) catch {};
126
+
return;
127
+
}) orelse return;
128
+
129
+
app.directories.dir.deleteTree(entry.name) catch |err| {
130
+
const error_message = try std.fmt.allocPrint(app.alloc, "Failed to force delete '{s}' - {}.", .{ entry.name, err });
131
+
app.notification.write(error_message, .err) catch {};
132
+
return;
133
+
};
134
+
135
+
app.directories.removeSelected();
136
+
}
137
+
138
+
pub fn toggleHiddenFiles(app: *App) error{OutOfMemory}!void {
139
+
config.show_hidden = !config.show_hidden;
140
+
141
+
const prev_selected_name: []const u8, const prev_selected_err: bool = lbl: {
142
+
const selected = app.directories.getSelected() catch break :lbl .{ "", true };
143
+
if (selected == null) break :lbl .{ "", true };
144
+
145
+
break :lbl .{ try app.alloc.dupe(u8, selected.?.name), false };
146
+
};
147
+
defer if (!prev_selected_err) app.alloc.free(prev_selected_name);
148
+
149
+
try app.repopulateDirectory("");
150
+
app.text_input.clearAndFree();
151
+
152
+
for (app.directories.entries.all()) |entry| {
153
+
if (std.mem.eql(u8, entry.name, prev_selected_name)) return;
154
+
app.directories.entries.selected += 1;
155
+
}
156
+
157
+
// If it didn't find entry, reset selected.
158
+
app.directories.entries.selected = 0;
159
+
}
160
+
161
+
pub fn yank(app: *App) error{OutOfMemory}!void {
162
+
var message: ?[]const u8 = null;
163
+
defer if (message) |msg| app.alloc.free(msg);
164
+
165
+
if (app.yanked) |yanked| {
166
+
app.alloc.free(yanked.dir);
167
+
app.alloc.free(yanked.entry.name);
168
+
}
169
+
170
+
app.yanked = lbl: {
171
+
const entry = (app.directories.getSelected() catch {
172
+
app.notification.write("Can not yank item - no item selected.", .warn) catch {};
173
+
break :lbl null;
174
+
}) orelse break :lbl null;
175
+
176
+
switch (entry.kind) {
177
+
.file, .directory, .sym_link => {
178
+
break :lbl .{
179
+
.dir = try app.alloc.dupe(u8, app.directories.fullPath(".") catch {
180
+
message = try std.fmt.allocPrint(
181
+
app.alloc,
182
+
"Failed to yank '{s}' - unable to retrieve directory path.",
183
+
.{entry.name},
184
+
);
185
+
app.notification.write(message.?, .err) catch {};
186
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
187
+
break :lbl null;
188
+
}),
189
+
.entry = .{
190
+
.kind = entry.kind,
191
+
.name = try app.alloc.dupe(u8, entry.name),
192
+
},
193
+
};
194
+
},
195
+
else => {
196
+
message = try std.fmt.allocPrint(app.alloc, "Can not yank '{s}' - unsupported file type '{s}'.", .{ entry.name, @tagName(entry.kind) });
197
+
app.notification.write(message.?, .warn) catch {};
198
+
break :lbl null;
199
+
},
200
+
}
201
+
};
202
+
203
+
if (app.yanked) |y| {
204
+
message = try std.fmt.allocPrint(app.alloc, "Yanked '{s}'.", .{y.entry.name});
205
+
app.notification.write(message.?, .info) catch {};
206
+
}
207
+
}
208
+
209
+
pub fn paste(app: *App) error{ OutOfMemory, NoSpaceLeft }!void {
210
+
var message: ?[]const u8 = null;
211
+
defer if (message) |msg| app.alloc.free(msg);
212
+
213
+
const yanked = if (app.yanked) |y| y else return;
214
+
215
+
var new_path_buf: [std.fs.max_path_bytes]u8 = undefined;
216
+
const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, yanked.entry.name) catch {
217
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - path too long.", .{yanked.entry.name});
218
+
app.notification.write(message.?, .err) catch {};
219
+
return;
220
+
};
221
+
222
+
switch (yanked.entry.kind) {
223
+
.directory => {
224
+
var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch {
225
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir });
226
+
app.notification.write(message.?, .err) catch {};
227
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
228
+
return;
229
+
};
230
+
defer source_dir.close();
231
+
232
+
var selected_dir = source_dir.openDir(yanked.entry.name, .{ .iterate = true }) catch {
233
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.entry.name });
234
+
app.notification.write(message.?, .err) catch {};
235
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
236
+
return;
237
+
};
238
+
defer selected_dir.close();
239
+
240
+
var walker = selected_dir.walk(app.alloc) catch |err| {
241
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to walk directory tree due to {}.", .{ yanked.entry.name, err });
242
+
app.notification.write(message.?, .err) catch {};
243
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
244
+
return;
245
+
};
246
+
defer walker.deinit();
247
+
248
+
// Make initial dir.
249
+
app.directories.dir.makeDir(new_path_res.path) catch |err| {
250
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create new directory due to {}.", .{ yanked.entry.name, err });
251
+
app.notification.write(message.?, .err) catch {};
252
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
253
+
return;
254
+
};
255
+
256
+
var errored = false;
257
+
var inner_path_buf: [std.fs.max_path_bytes]u8 = undefined;
258
+
while (walker.next() catch |err| {
259
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy one or more files - {}. A partial copy may have taken place.", .{err});
260
+
app.notification.write(message.?, .err) catch {};
261
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
262
+
return;
263
+
}) |entry| {
264
+
const path = try std.fmt.bufPrint(&inner_path_buf, "{s}{s}{s}", .{ new_path_res.path, std.fs.path.sep_str, entry.path });
265
+
switch (entry.kind) {
266
+
.directory => {
267
+
app.directories.dir.makeDir(path) catch {
268
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to create containing directory '{s}'.", .{ entry.basename, path });
269
+
app.notification.write(message.?, .err) catch {};
270
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
271
+
errored = true;
272
+
};
273
+
},
274
+
.file, .sym_link => {
275
+
entry.dir.copyFile(entry.basename, app.directories.dir, path, .{}) catch |err| switch (err) {
276
+
error.FileNotFound => {
277
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{entry.path});
278
+
app.notification.write(message.?, .err) catch {};
279
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
280
+
errored = true;
281
+
},
282
+
else => {
283
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ entry.path, err });
284
+
app.notification.write(message.?, .err) catch {};
285
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
286
+
errored = true;
287
+
},
288
+
};
289
+
},
290
+
else => {
291
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unsupported file type '{s}'.", .{ entry.path, @tagName(entry.kind) });
292
+
app.notification.write(message.?, .err) catch {};
293
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
294
+
errored = true;
295
+
},
296
+
}
297
+
}
298
+
299
+
if (errored) {
300
+
app.notification.write("Failed to copy some items, check the log file for more details.", .err) catch {};
301
+
} else {
302
+
message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name});
303
+
app.notification.write(message.?, .info) catch {};
304
+
}
305
+
},
306
+
.file, .sym_link => {
307
+
var source_dir = std.fs.openDirAbsolute(yanked.dir, .{ .iterate = true }) catch {
308
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - unable to open directory '{s}'.", .{ yanked.entry.name, yanked.dir });
309
+
app.notification.write(message.?, .err) catch {};
310
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
311
+
return;
312
+
};
313
+
defer source_dir.close();
314
+
315
+
std.fs.Dir.copyFile(
316
+
source_dir,
317
+
yanked.entry.name,
318
+
app.directories.dir,
319
+
new_path_res.path,
320
+
.{},
321
+
) catch |err| switch (err) {
322
+
error.FileNotFound => {
323
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - the original file was deleted or moved.", .{yanked.entry.name});
324
+
app.notification.write(message.?, .err) catch {};
325
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
326
+
return;
327
+
},
328
+
else => {
329
+
message = try std.fmt.allocPrint(app.alloc, "Failed to copy '{s}' - {}.", .{ yanked.entry.name, err });
330
+
app.notification.write(message.?, .err) catch {};
331
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
332
+
return;
333
+
},
334
+
};
335
+
336
+
message = try std.fmt.allocPrint(app.alloc, "Copied '{s}'.", .{yanked.entry.name});
337
+
app.notification.write(message.?, .info) catch {};
338
+
},
339
+
else => {
340
+
message = try std.fmt.allocPrint(app.alloc, "Can not copy '{s}' - unsupported file type '{s}'.", .{ yanked.entry.name, @tagName(yanked.entry.kind) });
341
+
app.notification.write(message.?, .warn) catch {};
342
+
return;
343
+
},
344
+
}
345
+
346
+
// Append action to undo history.
347
+
var new_path_abs_buf: [std.fs.max_path_bytes]u8 = undefined;
348
+
const new_path_abs = app.directories.dir.realpath(new_path_res.path, &new_path_abs_buf) catch {
349
+
message = try std.fmt.allocPrint(
350
+
app.alloc,
351
+
"Failed to push copy action for '{s}' to undo history - unable to retrieve absolute directory path for '{s}'. This action will not be able to be undone via the `undo` keybind.",
352
+
.{ new_path_res.path, yanked.entry.name },
353
+
);
354
+
app.notification.write(message.?, .err) catch {};
355
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
356
+
return;
357
+
};
358
+
359
+
if (app.actions.push(.{
360
+
.paste = try app.alloc.dupe(u8, new_path_abs),
361
+
})) |prev_elem| {
362
+
app.alloc.free(prev_elem.delete.prev_path);
363
+
app.alloc.free(prev_elem.delete.new_path);
364
+
}
365
+
366
+
try app.repopulateDirectory("");
367
+
app.text_input.clearAndFree();
368
+
}
369
+
370
+
pub fn traverseLeft(app: *App) error{OutOfMemory}!void {
371
+
app.text_input.clearAndFree();
372
+
373
+
const dir = app.directories.dir.openDir("../", .{ .iterate = true }) catch |err| {
374
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err});
375
+
defer app.alloc.free(message);
376
+
app.notification.write(message, .err) catch {};
377
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
378
+
return;
379
+
};
380
+
381
+
app.directories.dir.close();
382
+
app.directories.dir = dir;
383
+
384
+
try app.repopulateDirectory("");
385
+
app.text_input.clearAndFree();
386
+
387
+
if (app.directories.history.pop()) |history| {
388
+
if (history < app.directories.entries.len()) {
389
+
app.directories.entries.selected = history;
390
+
}
391
+
}
392
+
}
393
+
394
+
pub fn traverseRight(app: *App) !void {
395
+
var message: ?[]const u8 = null;
396
+
defer if (message) |msg| app.alloc.free(msg);
397
+
398
+
const entry = (app.directories.getSelected() catch {
399
+
app.notification.write("Can not rename item - no item selected.", .warn) catch {};
400
+
return;
401
+
}) orelse return;
402
+
403
+
switch (entry.kind) {
404
+
.directory => {
405
+
app.text_input.clearAndFree();
406
+
407
+
const dir = app.directories.dir.openDir(entry.name, .{ .iterate = true }) catch |err| {
408
+
message = try std.fmt.allocPrint(app.alloc, "Failed to read directory entries - {}.", .{err});
409
+
app.notification.write(message.?, .err) catch {};
410
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
411
+
return;
412
+
};
413
+
414
+
app.directories.dir.close();
415
+
app.directories.dir = dir;
416
+
_ = app.directories.history.push(app.directories.entries.selected);
417
+
try app.repopulateDirectory("");
418
+
app.text_input.clearAndFree();
419
+
},
420
+
.file => {
421
+
if (environment.getEditor()) |editor| {
422
+
try app.vx.exitAltScreen(app.tty.writer());
423
+
try app.vx.resetState(app.tty.writer());
424
+
app.loop.stop();
425
+
426
+
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| {
427
+
message = try std.fmt.allocPrint(app.alloc, "Failed to open file '{s}' - {}.", .{ entry.name, err });
428
+
app.notification.write(message.?, .err) catch {};
429
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
430
+
};
431
+
432
+
try app.loop.start();
433
+
try app.vx.enterAltScreen(app.tty.writer());
434
+
try app.vx.enableDetectedFeatures(app.tty.writer());
435
+
app.vx.queueRefresh();
436
+
} else {
437
+
app.notification.write("Can not open file - $EDITOR not set.", .warn) catch {};
438
+
}
439
+
},
440
+
else => {},
441
+
}
442
+
}
443
+
444
+
pub fn createNewDir(app: *App) error{OutOfMemory}!void {
445
+
var message: ?[]const u8 = null;
446
+
defer if (message) |msg| app.alloc.free(msg);
447
+
448
+
const dir = app.inputToSlice();
449
+
450
+
app.directories.dir.makeDir(dir) catch |err| {
451
+
message = try std.fmt.allocPrint(app.alloc, "Failed to create directory '{s}' - {}", .{ dir, err });
452
+
app.notification.write(message.?, .err) catch {};
453
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
454
+
app.text_input.clearAndFree();
455
+
return;
456
+
};
457
+
458
+
try app.repopulateDirectory("");
459
+
app.text_input.clearAndFree();
460
+
461
+
message = try std.fmt.allocPrint(app.alloc, "Created new directory '{s}'.", .{dir});
462
+
app.notification.write(message.?, .info) catch {};
463
+
}
464
+
465
+
pub fn createNewFile(app: *App) error{OutOfMemory}!void {
466
+
var message: ?[]const u8 = null;
467
+
defer if (message) |msg| app.alloc.free(msg);
468
+
469
+
const file = app.inputToSlice();
470
+
471
+
if (environment.fileExists(app.directories.dir, file)) {
472
+
message = try std.fmt.allocPrint(app.alloc, "Can not create file - '{s}' already exists.", .{file});
473
+
app.notification.write(message.?, .warn) catch {};
474
+
} else {
475
+
_ = app.directories.dir.createFile(file, .{}) catch |err| {
476
+
message = try std.fmt.allocPrint(app.alloc, "Failed to create file '{s}' - {}", .{ file, err });
477
+
app.notification.write(message.?, .err) catch {};
478
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
479
+
app.text_input.clearAndFree();
480
+
return;
481
+
};
482
+
483
+
try app.repopulateDirectory("");
484
+
app.text_input.clearAndFree();
485
+
486
+
message = try std.fmt.allocPrint(app.alloc, "Created new file '{s}'.", .{file});
487
+
app.notification.write(message.?, .info) catch {};
488
+
}
489
+
490
+
app.text_input.clearAndFree();
491
+
}
492
+
493
+
pub fn undo(app: *App) error{OutOfMemory}!void {
494
+
var message: ?[]const u8 = null;
495
+
defer if (message) |msg| app.alloc.free(msg);
496
+
497
+
const action = app.actions.pop() orelse {
498
+
app.notification.write("There is nothing to undo.", .info) catch {};
499
+
return;
500
+
};
501
+
502
+
const selected = app.directories.entries.selected;
503
+
504
+
switch (action) {
505
+
.delete => |a| {
506
+
defer app.alloc.free(a.new_path);
507
+
defer app.alloc.free(a.prev_path);
508
+
509
+
var new_path_buf: [std.fs.max_path_bytes]u8 = undefined;
510
+
const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path) catch {
511
+
message = try std.fmt.allocPrint(app.alloc, "Failed to undo delete '{s}' - path too long.", .{a.prev_path});
512
+
app.notification.write(message.?, .err) catch {};
513
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
514
+
return;
515
+
};
516
+
517
+
app.directories.dir.rename(a.new_path, new_path_res.path) catch |err| {
518
+
message = try std.fmt.allocPrint(app.alloc, "Failed to undo delete for '{s}' - {}.", .{ a.prev_path, err });
519
+
app.notification.write(message.?, .err) catch {};
520
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
521
+
return;
522
+
};
523
+
524
+
try app.repopulateDirectory("");
525
+
app.text_input.clearAndFree();
526
+
527
+
message = try std.fmt.allocPrint(app.alloc, "Restored '{s}' as '{s}'.", .{ a.prev_path, new_path_res.path });
528
+
app.notification.write(message.?, .info) catch {};
529
+
},
530
+
.rename => |a| {
531
+
defer app.alloc.free(a.new_path);
532
+
defer app.alloc.free(a.prev_path);
533
+
534
+
var new_path_buf: [std.fs.max_path_bytes]u8 = undefined;
535
+
const new_path_res = environment.checkDuplicatePath(&new_path_buf, app.directories.dir, a.prev_path) catch {
536
+
message = try std.fmt.allocPrint(app.alloc, "Failed to undo rename '{s}' - path too long.", .{a.prev_path});
537
+
app.notification.write(message.?, .err) catch {};
538
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
539
+
return;
540
+
};
541
+
542
+
app.directories.dir.rename(a.new_path, new_path_res.path) catch |err| {
543
+
message = try std.fmt.allocPrint(app.alloc, "Failed to undo rename for '{s}' - {}.", .{ a.new_path, err });
544
+
app.notification.write(message.?, .err) catch {};
545
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
546
+
return;
547
+
};
548
+
549
+
try app.repopulateDirectory("");
550
+
app.text_input.clearAndFree();
551
+
552
+
message = try std.fmt.allocPrint(app.alloc, "Reverted renaming of '{s}', now '{s}'.", .{ a.new_path, new_path_res.path });
553
+
app.notification.write(message.?, .info) catch {};
554
+
},
555
+
.paste => |path| {
556
+
defer app.alloc.free(path);
557
+
558
+
app.directories.dir.deleteTree(path) catch |err| {
559
+
message = try std.fmt.allocPrint(app.alloc, "Failed to delete '{s}' - {}.", .{ path, err });
560
+
app.notification.write(message.?, .err) catch {};
561
+
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
562
+
return;
563
+
};
564
+
565
+
try app.repopulateDirectory("");
566
+
app.text_input.clearAndFree();
567
+
},
568
+
}
569
+
570
+
app.directories.entries.selected = selected;
571
+
}
+59
src/file_logger.zig
+59
src/file_logger.zig
···
1
+
const std = @import("std");
2
+
const environment = @import("environment.zig");
3
+
const config = &@import("./config.zig").config;
4
+
5
+
pub const LOG_PATH = "log.txt";
6
+
7
+
const LogLevel = enum {
8
+
err,
9
+
info,
10
+
warn,
11
+
12
+
pub fn toString(level: LogLevel) []const u8 {
13
+
return switch (level) {
14
+
.err => "ERROR",
15
+
.info => "INFO",
16
+
.warn => "WARN",
17
+
};
18
+
}
19
+
};
20
+
21
+
const FileLogger = @This();
22
+
23
+
dir: std.fs.Dir,
24
+
file: ?std.fs.File,
25
+
26
+
pub fn init(dir: std.fs.Dir) FileLogger {
27
+
const file = dir.createFile(LOG_PATH, .{ .truncate = false, .read = true }) catch |err| {
28
+
std.log.err("Failed to create/open log file: {s}", .{@errorName(err)});
29
+
return .{ .dir = dir, .file = null };
30
+
};
31
+
32
+
return .{ .dir = dir, .file = file };
33
+
}
34
+
35
+
pub fn deinit(self: FileLogger) void {
36
+
if (self.file) |file| {
37
+
var f = file;
38
+
f.close();
39
+
}
40
+
}
41
+
42
+
pub fn write(self: FileLogger, msg: []const u8, level: LogLevel) !void {
43
+
const file = if (self.file) |file| file else return error.NoLogFile;
44
+
45
+
if (try file.tryLock(.exclusive)) {
46
+
defer file.unlock();
47
+
48
+
var buffer: [1024]u8 = undefined;
49
+
var file_writer_impl = file.writer(&buffer);
50
+
const file_writer = &file_writer_impl.interface;
51
+
try file_writer_impl.seekTo(file.getEndPos() catch 0);
52
+
53
+
try file_writer.print(
54
+
"({d}) {s}: {s}\n",
55
+
.{ std.time.timestamp(), LogLevel.toString(level), msg },
56
+
);
57
+
try file_writer.flush();
58
+
}
59
+
}
+5
-6
src/git.zig
+5
-6
src/git.zig
···
2
2
3
3
/// Callers owns memory returned.
4
4
pub fn getGitBranch(alloc: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 {
5
-
var file = dir.openFile(".git/HEAD", .{}) catch return null;
5
+
var file = try dir.openFile(".git/HEAD", .{});
6
6
defer file.close();
7
7
8
8
var buf: [1024]u8 = undefined;
9
-
const bytes = file.readAll(&buf) catch return null;
9
+
const bytes = try file.readAll(&buf);
10
+
if (bytes == 0) return null;
10
11
11
-
var it = std.mem.splitBackwardsSequence(u8, buf[0..bytes], "/");
12
-
const branch = it.next() orelse return null;
13
-
if (std.mem.eql(u8, branch, "")) return null;
12
+
const preamble = "ref: refs/heads/";
14
13
15
-
return try alloc.dupe(u8, branch);
14
+
return try alloc.dupe(u8, buf[preamble.len..]);
16
15
}
+13
-22
src/list.zig
+13
-22
src/list.zig
···
8
8
alloc: std.mem.Allocator,
9
9
items: std.ArrayList(T),
10
10
selected: usize,
11
-
offset: usize,
12
11
13
12
pub fn init(alloc: std.mem.Allocator) Self {
14
13
return Self{
15
14
.alloc = alloc,
16
-
.items = std.ArrayList(T).init(alloc),
15
+
.items = .empty,
17
16
.selected = 0,
18
-
.offset = 0,
19
17
};
20
18
}
21
19
22
20
pub fn deinit(self: *Self) void {
23
-
self.items.deinit();
21
+
self.items.deinit(self.alloc);
24
22
}
25
23
26
24
pub fn append(self: *Self, item: T) !void {
27
-
try self.items.append(item);
25
+
try self.items.append(self.alloc, item);
28
26
}
29
27
30
28
pub fn clear(self: *Self) void {
31
-
self.items.clearAndFree();
29
+
self.items.clearAndFree(self.alloc);
32
30
self.selected = 0;
33
-
self.offset = 0;
31
+
}
32
+
33
+
pub fn fromArray(self: *Self, array: []const T) !void {
34
+
for (array) |item| {
35
+
try self.append(item);
36
+
}
34
37
}
35
38
36
39
pub fn get(self: Self, index: usize) !T {
···
61
64
return self.items.items.len;
62
65
}
63
66
64
-
pub fn next(self: *Self, win_height: usize) void {
67
+
pub fn next(self: *Self) void {
65
68
if (self.selected + 1 < self.len()) {
66
69
self.selected += 1;
67
-
68
-
if (self.all()[self.offset..].len > win_height and self.selected >= self.offset + (win_height / 2)) {
69
-
self.offset += 1;
70
-
}
71
70
}
72
71
}
73
72
74
-
pub fn previous(self: *Self, win_height: usize) void {
73
+
pub fn previous(self: *Self) void {
75
74
if (self.selected > 0) {
76
75
self.selected -= 1;
77
-
78
-
if (self.offset > 0 and self.selected < self.offset + (win_height / 2)) {
79
-
self.offset -= 1;
80
-
}
81
76
}
82
77
}
83
78
84
-
pub fn selectLast(self: *Self, win_height: usize) void {
79
+
pub fn selectLast(self: *Self) void {
85
80
self.selected = self.len() - 1;
86
-
if (self.selected >= win_height) {
87
-
self.offset = self.selected - (win_height - 1);
88
-
}
89
81
}
90
82
91
83
pub fn selectFirst(self: *Self) void {
92
84
self.selected = 0;
93
-
self.offset = 0;
94
85
}
95
86
};
96
87
}
+132
-11
src/main.zig
+132
-11
src/main.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
+
const options = @import("options");
3
4
const App = @import("app.zig");
5
+
const FileLogger = @import("file_logger.zig");
4
6
const vaxis = @import("vaxis");
5
-
const ConfigParseRes = @import("./config.zig").ParseRes;
6
7
const config = &@import("./config.zig").config;
8
+
const resolvePath = @import("./commands.zig").resolvePath;
7
9
8
10
pub const panic = vaxis.panic_handler;
11
+
const help_menu =
12
+
\\Usage: jido
13
+
\\
14
+
\\a lightweight Unix TUI file explorer
15
+
\\
16
+
\\Flags:
17
+
\\ -h, --help Show help information and exit.
18
+
\\ -v, --version Print version information and exit.
19
+
\\ --entry-dir=PATH Open jido at chosen dir.
20
+
\\ --choose-dir Makes jido act like a directory chooser. When jido
21
+
\\ quits, it will write the name of the last visited
22
+
\\ directory to STDOUT.
23
+
\\
24
+
;
9
25
10
26
pub const std_options: std.Options = .{
11
27
.log_scope_levels = &.{
···
14
30
},
15
31
};
16
32
33
+
const Options = struct {
34
+
help: bool = false,
35
+
version: bool = false,
36
+
@"choose-dir": bool = false,
37
+
@"entry-path": []const u8 = ".",
38
+
39
+
fn optKind(a: []const u8) enum { short, long, positional } {
40
+
if (std.mem.startsWith(u8, a, "--")) return .long;
41
+
if (std.mem.startsWith(u8, a, "-")) return .short;
42
+
return .positional;
43
+
}
44
+
};
45
+
17
46
pub fn main() !void {
18
47
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
19
48
defer {
···
24
53
}
25
54
const alloc = gpa.allocator();
26
55
27
-
var app = try App.init(alloc);
28
-
defer app.deinit();
56
+
var last_dir: ?[]const u8 = null;
57
+
var entry_path_buf: [std.fs.max_path_bytes]u8 = undefined;
58
+
59
+
var opts = Options{};
60
+
var args = std.process.args();
61
+
_ = args.skip();
62
+
while (args.next()) |arg| {
63
+
switch (Options.optKind(arg)) {
64
+
.short => {
65
+
const str = arg[1..];
66
+
for (str) |b| {
67
+
switch (b) {
68
+
'v' => opts.version = true,
69
+
'h' => opts.help = true,
70
+
else => {
71
+
std.log.err("Invalid opt: '{c}'", .{b});
72
+
std.process.exit(1);
73
+
},
74
+
}
75
+
}
76
+
},
77
+
.long => {
78
+
var split = std.mem.splitScalar(u8, arg[2..], '=');
79
+
const opt = split.first();
80
+
const val = split.rest();
81
+
if (std.mem.eql(u8, opt, "version")) {
82
+
opts.version = true;
83
+
} else if (std.mem.eql(u8, opt, "help")) {
84
+
opts.help = true;
85
+
} else if (std.mem.eql(u8, opt, "choose-dir")) {
86
+
opts.@"choose-dir" = true;
87
+
} else if (std.mem.eql(u8, opt, "entry-dir")) {
88
+
const path = if (std.mem.eql(u8, val, "")) "." else val;
89
+
var dir = try std.fs.cwd().openDir(".", .{ .iterate = true });
90
+
defer dir.close();
91
+
opts.@"entry-path" = resolvePath(&entry_path_buf, path, dir);
92
+
}
93
+
},
94
+
.positional => {
95
+
std.log.err("Invalid opt: '{s}'. Jido does not take positional arguments.", .{arg});
96
+
std.process.exit(1);
97
+
},
98
+
}
99
+
}
29
100
30
-
const config_parse_res = config.parse(alloc) catch |err| lbl: {
31
-
switch (err) {
101
+
if (opts.help) {
102
+
std.debug.print(help_menu, .{});
103
+
return;
104
+
}
105
+
106
+
if (opts.version) {
107
+
std.debug.print("jido v{f}\n", .{options.version});
108
+
return;
109
+
}
110
+
111
+
{
112
+
var app = App.init(alloc, opts.@"entry-path") catch {
113
+
vaxis.recover();
114
+
std.process.exit(1);
115
+
};
116
+
defer app.deinit();
117
+
118
+
config.parse(alloc, &app) catch |err| switch (err) {
32
119
error.SyntaxError => {
33
-
try app.notification.writeErr(.ConfigSyntaxError);
120
+
app.notification.write("Encountered a syntax error while parsing the config file.", .err) catch {
121
+
std.log.err("Encountered a syntax error while parsing the config file.", .{});
122
+
};
123
+
},
124
+
error.InvalidCharacter => {
125
+
app.notification.write("One or more overriden keybinds are invalid.", .err) catch {
126
+
std.log.err("One or more overriden keybinds are invalid.", .{});
127
+
};
128
+
},
129
+
error.DuplicateKeybind => {
130
+
// Error logged in function
34
131
},
35
132
else => {
36
-
try app.notification.writeErr(.ConfigUnknownError);
133
+
const message = try std.fmt.allocPrint(alloc, "Encountend an unknown error while parsing the config file - {}", .{err});
134
+
defer alloc.free(message);
135
+
136
+
app.notification.write(message, .err) catch {
137
+
std.log.err("Encountend an unknown error while parsing the config file - {}", .{err});
138
+
};
37
139
},
140
+
};
141
+
142
+
app.file_logger = if (config.config_dir) |dir| FileLogger.init(dir) else logger: {
143
+
std.log.err("Failed to initialise file logger - no config directory found", .{});
144
+
break :logger null;
145
+
};
146
+
app.notification.loop = &app.loop;
147
+
148
+
try app.run();
149
+
150
+
if (opts.@"choose-dir") {
151
+
last_dir = alloc.dupe(u8, try app.directories.fullPath(".")) catch null;
38
152
}
153
+
}
39
154
40
-
break :lbl ConfigParseRes{ .deprecated = false };
41
-
};
42
-
if (config_parse_res.deprecated) try app.notification.writeWarn(.DeprecatedConfigPath);
155
+
// Must be printed after app has deinit as part of that process clears
156
+
// the screen.
157
+
if (last_dir) |path| {
158
+
var stdout_buffer: [std.fs.max_path_bytes]u8 = undefined;
159
+
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
160
+
const stdout = &stdout_writer.interface;
161
+
stdout.print("{s}\n", .{path}) catch {};
162
+
stdout.flush() catch {};
43
163
44
-
try app.run();
164
+
alloc.free(path);
165
+
}
45
166
}
+10
-77
src/notification.zig
+10
-77
src/notification.zig
···
1
1
const std = @import("std");
2
+
const vaxis = @import("vaxis");
3
+
const Event = @import("app.zig").Event;
4
+
5
+
const FileLogger = @import("file_logger.zig");
2
6
3
7
const Self = @This();
4
8
···
11
15
warn,
12
16
};
13
17
14
-
const Error = enum {
15
-
PermissionDenied,
16
-
UnknownError,
17
-
UnableToUndo,
18
-
UnableToOpenFile,
19
-
UnableToDelete,
20
-
FailedToDeleteSomeItems,
21
-
UnableToDeleteAcrossMountPoints,
22
-
UnsupportedImageFormat,
23
-
EditorNotSet,
24
-
ItemAlreadyExists,
25
-
UnableToRename,
26
-
IncorrectPath,
27
-
ConfigSyntaxError,
28
-
ConfigUnknownError,
29
-
ConfigPathNotFound,
30
-
CannotDeleteTrashDir,
31
-
};
32
-
33
-
const Info = enum {
34
-
CreatedFile,
35
-
CreatedFolder,
36
-
Deleted,
37
-
Renamed,
38
-
RestoredDelete,
39
-
RestoredRename,
40
-
EmptyUndo,
41
-
ChangedDir,
42
-
};
43
-
44
-
const Warn = enum { DeprecatedConfigPath };
18
+
var buf: [1024]u8 = undefined;
45
19
46
-
buf: [1024]u8 = undefined,
47
20
style: Style = Style.info,
48
-
fbs: std.io.FixedBufferStream([]u8) = undefined,
21
+
fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(&buf),
49
22
/// How long until the notification disappears in seconds.
50
23
timer: i64 = 0,
51
-
52
-
pub fn init(self: *Self) void {
53
-
self.fbs = std.io.fixedBufferStream(&self.buf);
54
-
self.timer = std.time.timestamp();
55
-
}
24
+
loop: ?*vaxis.Loop(Event) = null,
56
25
57
26
pub fn write(self: *Self, text: []const u8, style: Style) !void {
58
27
self.fbs.reset();
59
28
_ = try self.fbs.write(text);
60
29
self.timer = std.time.timestamp();
61
30
self.style = style;
62
-
}
63
31
64
-
pub fn writeErr(self: *Self, err: Error) !void {
65
-
try switch (err) {
66
-
.PermissionDenied => self.write("Permission denied.", .err),
67
-
.UnknownError => self.write("An unknown error occurred.", .err),
68
-
.UnableToOpenFile => self.write("Unable to open file.", .err),
69
-
.UnableToDelete => self.write("Unable to delete item.", .err),
70
-
.FailedToDeleteSomeItems => self.write("Failed to delete some items..", .err),
71
-
.UnableToDeleteAcrossMountPoints => self.write("Unable to move item to /tmp. Failed to delete.", .err),
72
-
.UnableToUndo => self.write("Unable to undo previous action.", .err),
73
-
.ItemAlreadyExists => self.write("Item already exists.", .err),
74
-
.UnableToRename => self.write("Unable to rename item.", .err),
75
-
.IncorrectPath => self.write("Unable to find path.", .err),
76
-
.EditorNotSet => self.write("$EDITOR is not set.", .err),
77
-
.UnsupportedImageFormat => self.write("Unsupported image format.", .err),
78
-
.ConfigSyntaxError => self.write("Could not read config due to a syntax error.", .err),
79
-
.ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err),
80
-
.ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err),
81
-
.CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err),
82
-
};
83
-
}
84
-
85
-
pub fn writeInfo(self: *Self, info: Info) !void {
86
-
try switch (info) {
87
-
.CreatedFile => self.write("Successfully created file.", .info),
88
-
.CreatedFolder => self.write("Successfully created folder.", .info),
89
-
.Deleted => self.write("Successfully deleted item.", .info),
90
-
.Renamed => self.write("Successfully renamed item.", .info),
91
-
.RestoredDelete => self.write("Successfully restored deleted item.", .info),
92
-
.RestoredRename => self.write("Successfully restored renamed item.", .info),
93
-
.EmptyUndo => self.write("Nothing to undo.", .info),
94
-
.ChangedDir => self.write("Successfully changed directory.", .info),
95
-
};
96
-
}
97
-
98
-
pub fn writeWarn(self: *Self, warning: Warn) !void {
99
-
try switch (warning) {
100
-
.DeprecatedConfigPath => self.write("You are using a deprecated config path. Please move your config to either `$XDG_CONFIG_HOME/jido` or `$HOME/.jido`", .warn),
101
-
};
32
+
if (self.loop) |loop| {
33
+
loop.postEvent(.notification);
34
+
}
102
35
}
103
36
104
37
pub fn reset(self: *Self) void {