+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
+67
.github/workflows/create-draft-release.yml
+67
.github/workflows/create-draft-release.yml
···
1
+
name: Create draft release
2
+
3
+
on:
4
+
workflow_dispatch:
5
+
6
+
env:
7
+
# https://github.com/cli/cli/issues/9514#issuecomment-2311517523
8
+
GH_TOKEN: ${{ secrets.TOKEN }}
9
+
10
+
jobs:
11
+
build:
12
+
runs-on: ubuntu-latest
13
+
14
+
steps:
15
+
- name: Checkout code
16
+
uses: actions/checkout@v4
17
+
18
+
- name: Set up Zig
19
+
uses: korandoru/setup-zig@v1
20
+
with:
21
+
zig-version: "0.14.0"
22
+
23
+
- name: Build application
24
+
run: |
25
+
zig build -Dall-targets
26
+
27
+
- name: Upload artifact
28
+
uses: actions/upload-artifact@v4
29
+
with:
30
+
name: builds
31
+
path: zig-out/
32
+
33
+
release:
34
+
needs: build
35
+
runs-on: ubuntu-latest
36
+
37
+
steps:
38
+
- name: Checkout code
39
+
uses: actions/checkout@v4
40
+
41
+
- name: Download artifacts
42
+
uses: actions/download-artifact@v4
43
+
with:
44
+
path: artifacts
45
+
46
+
- name: Create tarballs
47
+
run: |
48
+
mkdir -p tarballs
49
+
for dir in artifacts/builds/*; do
50
+
if [ -d "$dir" ] && [ $(basename "$dir") != "bin" ]; then
51
+
tar -czf "tarballs/$(basename "$dir").tar.gz" -C "$dir" .
52
+
fi
53
+
done
54
+
55
+
- name: gh log
56
+
run: |
57
+
gh --version
58
+
gh auth status
59
+
60
+
- name: Create release
61
+
run: |
62
+
gh release create ${{ github.ref_name }} tarballs/* \
63
+
--title "Release ${{ github.ref_name }}" \
64
+
--notes "Automated release with build artifacts." \
65
+
--draft
66
+
env:
67
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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)
+122
-57
README.md
+122
-57
README.md
···
1
-
<h1 align="center">
2
-
zfe
3
-
</h1>
1
+
# 地圖 (Jido)
2
+
3
+

4
4
5
-
<div align="center">Unix terminal file explorer, written in Zig</div>
5
+
**Jido** is a lightweight Unix TUI file explorer designed for speed and
6
+
simplicity.
7
+
8
+
The name 地圖 (지도) translates to "map" in English, reflecting Jido's
9
+
purpose: helping you navigate and explore your file system with ease. With
10
+
Vim-like bindings and a minimalist interface, Jido focuses on speed and
11
+
simplicity.
6
12
7
-
<br>
13
+
Jido is built with Zig v`0.15.2`.
8
14
9
-
**zfe** is a small unix terminal file explorer written in Zig.
15
+
- [Installation](#installation)
16
+
- [Integrations](#integrations)
17
+
- [Key manual](#key-manual)
18
+
- [Configuration](#configuration)
19
+
- [Contributing](#contributing)
10
20
11
-

21
+
## Installation
22
+
To install Jido, check the "Releases" page or build locally
23
+
via `zig build --release=safe`.
12
24
13
-
## Features
14
-
- **Simple to use**: Minimal and customizable keymaps with vim binding support.
15
-
- **Image Previews**: Preview images with Kitty terminal.
16
-
- **File Previews**: Preview contents of files directly in the terminal.
17
-
- **Configurable Options**: Customize settings via an external configuration file.
18
-
- **Fuzzy Search**: Fuzzy search within directories.
25
+
## Integrations
26
+
- `pdftotext` to view PDF text previews.
27
+
- A terminal supporting the `kitty image protocol` to view images.
19
28
20
-
## Install
21
-
To install zfe, check the "Releases" section in Github and download the
22
-
appropriate version or build locally via `zig build -Doptimize=ReleaseSafe`.
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
+
34
+
```
35
+
Global:
36
+
<CTRL-c> :Exit.
37
+
<CTRL-r> :Reload config.
38
+
39
+
Normal mode:
40
+
j / <Down> :Go down.
41
+
k / <Up> :Go up.
42
+
h / <Left> / - :Go to the parent directory.
43
+
l / <Right> :Open item or change directory.
44
+
g :Go to the top.
45
+
G :Go to the bottom.
46
+
c :Change directory via path. Will enter input mode.
47
+
R :Rename item. Will enter input mode.
48
+
D :Delete item.
49
+
u :Undo delete/rename.
50
+
d :Create directory. Will enter input mode.
51
+
% :Create file. Will enter input mode.
52
+
/ :Fuzzy search directory. Will enter input mode.
53
+
. :Toggle hidden files.
54
+
: :Allows for Jido commands to be entered. Please refer to the
55
+
"Command mode" section for available commands. Will enter
56
+
input mode.
57
+
v :Verbose mode. Provides more information about selected entry.
58
+
y :Yank selected item.
59
+
p :Past yanked item.
60
+
61
+
Input mode:
62
+
<Esc> :Cancel input.
63
+
<CR> :Confirm input.
64
+
65
+
Command mode:
66
+
<Up> / <Down> :Cycle previous commands.
67
+
:q :Exit.
68
+
:h :View available keybinds. 'q' to return to app.
69
+
:config :Navigate to config directory if it exists.
70
+
:trash :Navigate to trash directory if it exists.
71
+
:empty_trash :Empty trash if it exists. This action cannot be undone.
72
+
:cd <path> :Change directory via path. Will enter input mode.
73
+
```
23
74
24
75
## Configuration
25
-
Configure `zfe` by editing the external configuration file located at either:
26
-
- `$HOME/.config/zfe/config.json`
27
-
- `$XDG_CONFIG_HOME/zfe/config.json`.
76
+
Configure `jido` by editing the external configuration file located at either:
77
+
- `$HOME/.jido/config.json`
78
+
- `$XDG_CONFIG_HOME/jido/config.json`.
79
+
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.
28
82
29
-
An example config file can be found [here](https://github.com/BrookJeynes/zfe/blob/main/example-config.json).
83
+
An example config file can be found [here](https://github.com/BrookJeynes/jido/blob/main/example-config.json).
30
84
31
85
Config schema:
32
86
```
33
87
Config = struct {
34
-
.show_hidden: bool,
35
-
.sort_dirs: bool,
36
-
.show_images: bool,
37
-
.preview_file: bool,
38
-
.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'
120
+
}
121
+
122
+
NotificationStyles = struct {
123
+
.box: vaxis.Style,
124
+
.err: vaxis.Style,
125
+
.warn: vaxis.Style,
126
+
.info: vaxis.Style
39
127
}
40
128
41
129
Styles = struct {
···
43
131
.list_item: Style,
44
132
.file_name: Style,
45
133
.file_information: Style
46
-
.error_bar: Style,
47
-
.info_bar: Style,
134
+
.notification: NotificationStyles,
135
+
.git_branch: Style
48
136
}
49
137
50
138
Style = struct {
···
57
145
double,
58
146
curly,
59
147
dotted,
60
-
dashed,
148
+
dashed
61
149
}
62
150
.bold: bool,
63
151
.dim: bool,
···
65
153
.blink: bool,
66
154
.reverse: bool,
67
155
.invisible: bool,
68
-
.strikethrough: bool,
156
+
.strikethrough: bool
69
157
}
70
158
71
159
Color = enum{
72
160
default,
73
161
index: u8,
74
-
rgb: [3]u8,
162
+
rgb: [3]u8
75
163
}
76
-
```
77
164
78
-
## Keybinds
79
-
```
80
-
Normal mode:
81
-
q / <CTRL-c> :Exit.
82
-
83
-
j / <Down> :Go down.
84
-
k / <Up> :Go up.
85
-
h / <Left> / - :Go to the parent directory if exists.
86
-
l / <Right> :Open item or change directory.
87
-
gg :Go to the top.
88
-
G :Go to the bottom.
89
-
c :Change directory via path. Will enter input mode.
90
-
91
-
R :Rename item. Will enter input mode.
92
-
D :Delete item.
93
-
u :Undo delete/rename.
94
-
95
-
d :Create directory. Will enter input mode.
96
-
% :Create file. Will enter input mode.
97
-
/ :Fuzzy search directory. Will enter input mode.
98
-
99
-
Input mode:
100
-
<Esc> :Cancel input.
101
-
<Enter> :Confirm input.
165
+
Char = enum(u21)
102
166
```
103
167
104
168
## Contributing
105
-
Contributions, issues, and feature requests are always welcome! This project is
106
-
currently using the latest stable release of Zig (0.12.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.
assets/preview.png
assets/preview.png
This is a binary file and will not be displayed.
assets/style_guide.png
assets/style_guide.png
This is a binary file and will not be displayed.
+49
-58
build.zig
+49
-58
build.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
3
4
-
/// Must match the `minimum_zig_version` in `build.zig.zon`.
5
-
const minimum_zig_version = "0.12.0";
6
-
/// Must match the `version` in `build.zig.zon`.
7
-
const version = std.SemanticVersion{ .major = 0, .minor = 4, .patch = 0 };
4
+
///Must match the `version` in `build.zig.zon`.
5
+
const version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 };
8
6
9
-
const Build = blk: {
10
-
const current_zig = builtin.zig_version;
11
-
const min_zig = std.SemanticVersion.parse(minimum_zig_version) catch unreachable;
12
-
if (current_zig.order(min_zig) == .lt) {
13
-
@compileError(std.fmt.comptimePrint("Your Zig version v{} does not meet the minimum build requirement of v{}", .{ current_zig, min_zig }));
14
-
}
15
-
break :blk std.Build;
7
+
const targets: []const std.Target.Query = &.{
8
+
.{ .cpu_arch = .aarch64, .os_tag = .macos },
9
+
.{ .cpu_arch = .aarch64, .os_tag = .linux },
10
+
.{ .cpu_arch = .x86_64, .os_tag = .linux },
11
+
.{ .cpu_arch = .x86_64, .os_tag = .macos },
16
12
};
17
13
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");
25
+
26
+
const exe = b.addExecutable(.{
27
+
.name = exe_name,
28
+
.root_module = b.createModule(.{
29
+
.root_source_file = b.path("src/main.zig"),
30
+
.target = target,
31
+
.optimize = optimize,
32
+
}),
33
+
});
34
+
35
+
exe.root_module.addImport("options", build_options);
36
+
exe.root_module.addImport("vaxis", libvaxis);
37
+
exe.root_module.addImport("fuzzig", fuzzig);
38
+
exe.root_module.addImport("zeit", zeit);
39
+
exe.root_module.addImport("zuid", zuid);
40
+
41
+
return exe;
42
+
}
43
+
18
44
pub fn build(b: *std.Build) !void {
45
+
const target = b.standardTargetOptions(.{});
46
+
const optimize = b.standardOptimizeOption(.{});
47
+
19
48
const build_options = b.addOptions();
20
49
build_options.step.name = "build options";
50
+
build_options.addOption(std.SemanticVersion, "version", version);
21
51
const build_options_module = build_options.createModule();
22
-
build_options.addOption([]const u8, "minimum_zig_string", minimum_zig_version);
23
-
build_options.addOption(std.SemanticVersion, "zfe_version", version);
24
52
25
53
// Building targets for release.
26
-
const build_all = b.option(bool, "build-all-targets", "Build all targets in ReleaseSafe mode.") orelse false;
54
+
const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false;
27
55
if (build_all) {
28
-
try build_targets(b, build_options_module);
56
+
try buildTargets(b, build_options_module);
29
57
return;
30
58
}
31
59
32
-
const target = b.standardTargetOptions(.{});
33
-
const optimize = b.standardOptimizeOption(.{});
34
-
35
-
const libvaxis = b.dependency("vaxis", .{ .target = target }).module("vaxis");
36
-
const fuzzig = b.dependency("fuzzig", .{ .target = target }).module("fuzzig");
37
-
const zuid = b.dependency("zuid", .{ .target = target }).module("zuid");
38
-
39
-
const exe = b.addExecutable(.{
40
-
.name = "zfe",
41
-
.root_source_file = b.path("src/main.zig"),
42
-
.target = target,
43
-
.optimize = optimize,
44
-
});
45
-
exe.root_module.addImport("vaxis", libvaxis);
46
-
exe.root_module.addImport("fuzzig", fuzzig);
47
-
exe.root_module.addImport("zuid", zuid);
48
-
exe.root_module.addImport("options", build_options_module);
60
+
const exe = try createExe(b, "jido", target, optimize, build_options_module);
49
61
b.installArtifact(exe);
50
62
51
63
const run_cmd = b.addRunArtifact(exe);
···
57
69
run_step.dependOn(&run_cmd.step);
58
70
}
59
71
60
-
fn build_targets(b: *std.Build, build_options_module: *std.Build.Module) !void {
61
-
const target = b.standardTargetOptions(.{});
62
-
const targets: []const std.Target.Query = &.{
63
-
.{ .cpu_arch = .aarch64, .os_tag = .macos },
64
-
.{ .cpu_arch = .aarch64, .os_tag = .linux },
65
-
.{ .cpu_arch = .x86_64, .os_tag = .linux },
66
-
.{ .cpu_arch = .x86_64, .os_tag = .macos },
67
-
};
68
-
72
+
fn buildTargets(b: *std.Build, build_options: *std.Build.Module) !void {
69
73
for (targets) |t| {
70
-
const build_target = b.resolveTargetQuery(t);
74
+
const target = b.resolveTargetQuery(t);
71
75
72
-
const libvaxis = b.dependency("vaxis", .{ .target = target }).module("vaxis");
73
-
const fuzzig = b.dependency("fuzzig", .{ .target = target }).module("fuzzig");
74
-
const zuid = b.dependency("zuid", .{ .target = target }).module("zuid");
75
-
76
-
const build_exe = b.addExecutable(.{
77
-
.name = "zfe",
78
-
.root_source_file = b.path("src/main.zig"),
79
-
.target = build_target,
80
-
.optimize = .ReleaseSafe,
81
-
});
82
-
build_exe.root_module.addImport("fuzzig", fuzzig);
83
-
build_exe.root_module.addImport("vaxis", libvaxis);
84
-
build_exe.root_module.addImport("zuid", zuid);
85
-
build_exe.root_module.addImport("options", build_options_module);
86
-
b.installArtifact(build_exe);
76
+
const exe = try createExe(b, "jido", target, .ReleaseSafe, build_options);
77
+
b.installArtifact(exe);
87
78
88
-
const target_output = b.addInstallArtifact(build_exe, .{
79
+
const target_output = b.addInstallArtifact(exe, .{
89
80
.dest_dir = .{
90
81
.override = .{
91
82
.custom = try t.zigTriple(b.allocator),
+25
-12
build.zig.zon
+25
-12
build.zig.zon
···
1
1
.{
2
-
.name = "zfe",
3
-
// Must match the `version` in `build.zig`
4
-
.version = "0.4.0",
5
-
// Must match the `minimum_zig_version` in `build.zig`
6
-
.minimum_zig_version = "0.12.0",
2
+
.name = .jido,
3
+
.fingerprint = 0xee45eabe36cafb57,
4
+
.version = "1.3.0",
5
+
.minimum_zig_version = "0.15.2",
7
6
8
7
.dependencies = .{
8
+
// Replace with rockorager/libvaxis once https://github.com/rockorager/libvaxis/pull/293 is merged
9
9
.vaxis = .{
10
-
.url = "git+https://github.com/rockorager/libvaxis#ef028bfe72f5a26a3d9eb225bb7cf307d467f01c",
11
-
.hash = "12203bec1ec84db62b9c4436620d5572ff10d53c89775c43de2d626b48ecbd263fe4",
10
+
.url = "git+https://github.com/rob9315/libvaxis.git#8d04cffd9137b4a8c56b356de98b32023ae752f3",
11
+
.hash = "vaxis-0.5.1-BWNV_OA-CQDeFBHIx9ryyASogr2GE3FsAm-l5Ii5-HZT",
12
12
},
13
13
.fuzzig = .{
14
-
.url = "git+https://github.com/fjebaker/fuzzig#c6a0e0ca1a24e55ebdce51c83b918d4325ca7032",
15
-
.hash = "1220214dfb9a0806d9c8a059beb9e3b07811fd138cd5baeb9d1da432588920a084bf",
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",
16
20
},
21
+
// Replace with KeithBrown39423/zuid once https://github.com/KeithBrown39423/zuid/pull/4 is merged
17
22
.zuid = .{
18
-
.url = "git+https://github.com/KeithBrown39423/zuid#49e5980ba83f7d9ae967fa7ce4d54384c1c0f82b",
19
-
.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",
20
30
},
21
31
},
22
32
23
33
.paths = .{
24
-
"./src/",
34
+
"LICENSE",
35
+
"build.zig",
36
+
"build.zig.zon",
37
+
"src",
25
38
},
26
39
}
+14
-6
example-config.json
+14
-6
example-config.json
···
3
3
"sort_dirs": false,
4
4
"show_images": true,
5
5
"preview_file": true,
6
+
"keybinds": {
7
+
"toggle_hidden_files": "h",
8
+
"force_delete": null
9
+
},
6
10
"styles": {
7
11
"selected_list_item": {
8
12
"bg": {
···
10
14
},
11
15
"bold": true
12
16
},
13
-
"file_information": {
14
-
"fg": {
15
-
"rgb": [0, 0, 0]
16
-
},
17
-
"bg": {
18
-
"rgb": [255, 255, 255]
17
+
"notification": {
18
+
"info": {
19
+
"fg": {
20
+
"rgb": [127, 255, 0]
21
+
}
19
22
}
23
+
},
24
+
"git_branch": {
25
+
"fg": {
26
+
"rgb": [127, 255, 0]
27
+
}
20
28
}
21
29
}
22
30
}
-6
release_zip.sh
-6
release_zip.sh
+191
-685
src/app.zig
+191
-685
src/app.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
-
4
-
const Logger = @import("./log.zig").Logger;
5
3
const environment = @import("./environment.zig");
4
+
const Drawer = @import("./drawer.zig");
6
5
const Notification = @import("./notification.zig");
7
6
const config = &@import("./config.zig").config;
8
7
const List = @import("./list.zig").List;
9
8
const Directories = @import("./directories.zig");
9
+
const FileLogger = @import("./file_logger.zig");
10
10
const CircStack = @import("./circ_stack.zig").CircularStack;
11
-
12
11
const zuid = @import("zuid");
13
-
14
12
const vaxis = @import("vaxis");
15
-
const TextInput = @import("vaxis").widgets.TextInput;
16
-
const Cell = vaxis.Cell;
17
13
const Key = vaxis.Key;
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
+
};
18
57
19
58
pub const State = enum {
20
59
normal,
···
23
62
new_file,
24
63
change_dir,
25
64
rename,
65
+
command,
66
+
help_menu,
26
67
};
27
68
28
-
const InputReturnStatus = enum {
29
-
exit,
30
-
default,
69
+
pub const Action = union(enum) {
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,
31
73
};
32
74
33
-
const ActionPaths = struct {
34
-
/// Allocated.
35
-
old: []const u8,
36
-
/// Allocated.
37
-
new: []const u8,
75
+
pub const Event = union(enum) {
76
+
image_ready,
77
+
notification,
78
+
key_press: Key,
79
+
winsize: vaxis.Winsize,
38
80
};
39
81
40
-
pub const Action = union(enum) {
41
-
delete: ActionPaths,
42
-
rename: ActionPaths,
43
-
};
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,
44
95
45
-
const Event = union(enum) {
46
-
key_press: vaxis.Key,
47
-
winsize: vaxis.Winsize,
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
+
}
48
106
};
49
107
50
-
const top_div = 1;
51
-
const info_div = 1;
52
-
const bottom_div = 1;
53
108
const actions_len = 100;
109
+
const image_cache_cap = 100;
54
110
55
111
const App = @This();
56
112
57
113
alloc: std.mem.Allocator,
114
+
should_quit: bool,
58
115
vx: vaxis.Vaxis = undefined,
116
+
tty_buffer: [1024]u8 = undefined,
59
117
tty: vaxis.Tty = undefined,
60
-
logger: Logger,
118
+
loop: vaxis.Loop(Event) = undefined,
61
119
state: State = .normal,
62
120
actions: CircStack(Action, actions_len),
121
+
command_history: CommandHistory = CommandHistory{},
122
+
drawer: Drawer = Drawer{},
63
123
64
-
// Used to detect whether to re-render an image.
65
-
current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
66
-
current_item_path: []u8 = "",
67
-
last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
68
-
last_item_path: []u8 = "",
69
-
124
+
help_menu: List([]const u8),
70
125
directories: Directories,
71
-
notification: Notification,
126
+
notification: Notification = Notification{},
127
+
file_logger: ?FileLogger = null,
72
128
73
-
text_input: TextInput,
129
+
text_input: vaxis.widgets.TextInput,
74
130
text_input_buf: [std.fs.max_path_bytes]u8 = undefined,
75
131
76
-
image: ?vaxis.Image = null,
132
+
yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null,
77
133
last_known_height: usize,
78
134
79
-
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 {
80
141
var vx = try vaxis.init(alloc, .{
81
142
.kitty_keyboard_flags = .{
82
143
.report_text = false,
···
87
148
},
88
149
});
89
150
90
-
return App{
151
+
var help_menu = List([]const u8).init(alloc);
152
+
try help_menu.fromArray(&help_menu_items);
153
+
154
+
var app: App = .{
91
155
.alloc = alloc,
156
+
.should_quit = false,
92
157
.vx = vx,
93
-
.tty = try vaxis.Tty.init(),
94
-
.directories = try Directories.init(alloc),
95
-
.logger = Logger{},
96
-
.text_input = TextInput.init(alloc, &vx.unicode),
97
-
.notification = Notification{},
158
+
.directories = try Directories.init(alloc, entry_dir),
159
+
.help_menu = help_menu,
160
+
.text_input = vaxis.widgets.TextInput.init(alloc),
98
161
.actions = CircStack(Action, actions_len).init(),
99
162
.last_known_height = vx.window().height,
163
+
.images = .{ .cache = .init(alloc) },
100
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;
101
172
}
102
173
103
174
pub fn deinit(self: *App) void {
104
-
for (self.actions.buf[0..self.actions.count]) |action| {
175
+
while (self.actions.pop()) |action| {
105
176
switch (action) {
106
-
.delete, .rename => |a| {
107
-
self.alloc.free(a.new);
108
-
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);
109
184
},
185
+
.paste => |a| self.alloc.free(a),
110
186
}
111
187
}
112
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();
113
197
self.directories.deinit();
114
198
self.text_input.deinit();
115
-
self.vx.deinit(self.alloc, self.tty.anyWriter());
199
+
self.vx.deinit(self.alloc, self.tty.writer());
116
200
self.tty.deinit();
117
-
}
118
-
119
-
pub fn run(self: *App) !void {
120
-
self.logger.init();
121
-
self.notification.init();
122
-
123
-
try self.directories.populate_entries("");
124
-
125
-
var loop: vaxis.Loop(Event) = .{ .vaxis = &self.vx, .tty = &self.tty };
126
-
try loop.start();
127
-
defer loop.stop();
128
-
129
-
try self.vx.enterAltScreen(self.tty.anyWriter());
130
-
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
131
-
self.vx.queueRefresh();
132
-
133
-
while (true) {
134
-
self.notification.reset();
135
-
const event = loop.nextEvent();
201
+
if (self.file_logger) |file_logger| file_logger.deinit();
136
202
137
-
switch (self.state) {
138
-
.normal => {
139
-
switch (try self.handle_normal_event(event, &loop)) {
140
-
.exit => return,
141
-
.default => {},
142
-
}
143
-
},
144
-
.fuzzy, .new_file, .new_dir, .rename, .change_dir => {
145
-
switch (try self.handle_input_event(event)) {
146
-
.exit => return,
147
-
.default => {},
148
-
}
149
-
},
150
-
}
151
-
152
-
try self.draw();
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);
153
206
}
207
+
self.images.cache.deinit();
154
208
}
155
209
156
210
pub fn inputToSlice(self: *App) []const u8 {
157
-
self.text_input.cursor_idx = self.text_input.grapheme_count;
211
+
self.text_input.buf.cursor = self.text_input.buf.realLength();
158
212
return self.text_input.sliceToCursor(&self.text_input_buf);
159
213
}
160
214
161
-
pub fn handle_normal_event(self: *App, event: Event, loop: *vaxis.Loop(Event)) !InputReturnStatus {
162
-
switch (event) {
163
-
.key_press => |key| {
164
-
if ((key.codepoint == 'c' and key.mods.ctrl) or key.codepoint == 'q') {
165
-
return .exit;
166
-
}
167
-
168
-
switch (key.codepoint) {
169
-
'-', 'h', Key.left => {
170
-
self.text_input.clearAndFree();
171
-
172
-
if (self.directories.dir.openDir("../", .{ .iterate = true })) |dir| {
173
-
self.directories.dir = dir;
174
-
175
-
self.directories.cleanup();
176
-
const fuzzy = self.inputToSlice();
177
-
self.directories.populate_entries(fuzzy) catch |err| {
178
-
switch (err) {
179
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
180
-
else => try self.notification.write_err(.UnknownError),
181
-
}
182
-
};
183
-
184
-
if (self.directories.history.pop()) |history| {
185
-
self.directories.entries.selected = history.selected;
186
-
self.directories.entries.offset = history.offset;
187
-
}
188
-
} else |err| {
189
-
switch (err) {
190
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
191
-
else => try self.notification.write_err(.UnknownError),
192
-
}
193
-
}
194
-
},
195
-
Key.enter, 'l', Key.right => {
196
-
const entry = try self.directories.get_selected();
197
-
198
-
switch (entry.kind) {
199
-
.directory => {
200
-
self.text_input.clearAndFree();
201
-
202
-
if (self.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| {
203
-
self.directories.dir = dir;
204
-
205
-
self.directories.history.push(.{
206
-
.selected = self.directories.entries.selected,
207
-
.offset = self.directories.entries.offset,
208
-
});
209
-
210
-
self.directories.cleanup();
211
-
const fuzzy = self.inputToSlice();
212
-
self.directories.populate_entries(fuzzy) catch |err| {
213
-
switch (err) {
214
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
215
-
else => try self.notification.write_err(.UnknownError),
216
-
}
217
-
};
218
-
} else |err| {
219
-
switch (err) {
220
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
221
-
else => try self.notification.write_err(.UnknownError),
222
-
}
223
-
}
224
-
},
225
-
.file => {
226
-
if (environment.get_editor()) |editor| {
227
-
try self.vx.exitAltScreen(self.tty.anyWriter());
228
-
try self.vx.resetState(self.tty.anyWriter());
229
-
loop.stop();
230
-
231
-
environment.open_file(self.alloc, self.directories.dir, entry.name, editor) catch {
232
-
try self.notification.write_err(.UnableToOpenFile);
233
-
};
234
-
235
-
try loop.start();
236
-
try self.vx.enterAltScreen(self.tty.anyWriter());
237
-
try self.vx.enableDetectedFeatures(self.tty.anyWriter());
238
-
self.vx.queueRefresh();
239
-
} else {
240
-
try self.notification.write_err(.EditorNotSet);
241
-
}
242
-
},
243
-
else => {},
244
-
}
245
-
},
246
-
'j', Key.down => {
247
-
self.directories.entries.next(self.last_known_height);
248
-
},
249
-
'k', Key.up => {
250
-
self.directories.entries.previous(self.last_known_height);
251
-
},
252
-
'G' => {
253
-
self.directories.entries.select_last(self.last_known_height);
254
-
},
255
-
'g' => {
256
-
self.directories.entries.select_first();
257
-
},
258
-
'D' => {
259
-
const entry = self.directories.get_selected() catch {
260
-
try self.notification.write_err(.UnableToDeleteItem);
261
-
return .default;
262
-
};
263
-
264
-
var old_path_buf: [std.fs.max_path_bytes]u8 = undefined;
265
-
const old_path = try self.alloc.dupe(u8, try self.directories.dir.realpath(entry.name, &old_path_buf));
266
-
var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined;
267
-
const tmp_path = try self.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "/tmp/{s}-{s}", .{ entry.name, zuid.new.v4().toString() }));
268
-
269
-
try self.notification.write("Deleting item...", .info);
270
-
if (self.directories.dir.rename(entry.name, tmp_path)) {
271
-
// TODO: Will leak memory if pushing to a full stack.
272
-
self.actions.push(.{
273
-
.delete = .{ .old = old_path, .new = tmp_path },
274
-
});
275
-
try self.notification.write("Deleted item.", .info);
276
-
277
-
self.directories.remove_selected();
278
-
} else |_| {
279
-
try self.notification.write_err(.UnableToDeleteItem);
280
-
}
281
-
},
282
-
'd' => {
283
-
self.state = .new_dir;
284
-
},
285
-
'%' => {
286
-
self.state = .new_file;
287
-
},
288
-
'u' => {
289
-
if (self.actions.pop()) |action| {
290
-
const selected = self.directories.entries.selected;
291
-
292
-
switch (action) {
293
-
.delete => |a| {
294
-
// TODO: Will overwrite an item if it has the same name.
295
-
if (self.directories.dir.rename(a.new, a.old)) {
296
-
defer self.alloc.free(a.new);
297
-
defer self.alloc.free(a.old);
298
-
299
-
self.directories.cleanup();
300
-
const fuzzy = self.inputToSlice();
301
-
self.directories.populate_entries(fuzzy) catch |err| {
302
-
switch (err) {
303
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
304
-
else => try self.notification.write_err(.UnknownError),
305
-
}
306
-
};
307
-
try self.notification.write("Restored deleted item.", .info);
308
-
} else |_| {
309
-
try self.notification.write_err(.UnableToUndo);
310
-
}
311
-
},
312
-
.rename => |a| {
313
-
// TODO: Will overwrite an item if it has the same name.
314
-
if (self.directories.dir.rename(a.new, a.old)) {
315
-
defer self.alloc.free(a.new);
316
-
defer self.alloc.free(a.old);
317
-
318
-
self.directories.cleanup();
319
-
const fuzzy = self.inputToSlice();
320
-
self.directories.populate_entries(fuzzy) catch |err| {
321
-
switch (err) {
322
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
323
-
else => try self.notification.write_err(.UnknownError),
324
-
}
325
-
};
326
-
try self.notification.write("Restored previous item name.", .info);
327
-
} else |_| {
328
-
try self.notification.write_err(.UnableToUndo);
329
-
}
330
-
},
331
-
}
332
-
333
-
self.directories.entries.selected = selected;
334
-
} else {
335
-
try self.notification.write("Nothing to undo.", .info);
336
-
}
337
-
},
338
-
'/' => {
339
-
self.state = .fuzzy;
340
-
},
341
-
'R' => {
342
-
self.state = .rename;
343
-
344
-
const entry = try self.directories.get_selected();
345
-
self.text_input.insertSliceAtCursor(entry.name) catch {
346
-
self.state = .normal;
347
-
try self.notification.write_err(.UnableToRename);
348
-
};
349
-
},
350
-
'c' => {
351
-
self.state = .change_dir;
352
-
},
353
-
else => {
354
-
// log.debug("codepoint: {d}\n", .{key.codepoint});
355
-
},
356
-
}
357
-
},
358
-
.winsize => |ws| {
359
-
try self.vx.resize(self.alloc, self.tty.anyWriter(), ws);
360
-
},
361
-
}
362
-
363
-
return .default;
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 {};
222
+
};
364
223
}
365
224
366
-
pub fn handle_input_event(self: *App, event: Event) !InputReturnStatus {
367
-
switch (event) {
368
-
.key_press => |key| {
369
-
if ((key.codepoint == 'c' and key.mods.ctrl) or key.codepoint == 'q') {
370
-
return .exit;
371
-
}
372
-
373
-
switch (key.codepoint) {
374
-
Key.escape => {
375
-
switch (self.state) {
376
-
.fuzzy => {
377
-
self.directories.cleanup();
378
-
self.directories.populate_entries("") catch |err| {
379
-
switch (err) {
380
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
381
-
else => try self.notification.write_err(.UnknownError),
382
-
}
383
-
};
384
-
},
385
-
else => {},
386
-
}
387
-
388
-
self.text_input.clearAndFree();
389
-
self.state = .normal;
390
-
},
391
-
Key.enter => {
392
-
const selected = self.directories.entries.selected;
393
-
switch (self.state) {
394
-
.new_dir => {
395
-
const dir = self.inputToSlice();
396
-
if (self.directories.dir.makeDir(dir)) {
397
-
self.directories.cleanup();
398
-
self.directories.populate_entries("") catch |err| {
399
-
switch (err) {
400
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
401
-
else => try self.notification.write_err(.UnknownError),
402
-
}
403
-
};
404
-
} else |err| {
405
-
switch (err) {
406
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
407
-
error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists),
408
-
else => try self.notification.write_err(.UnknownError),
409
-
}
410
-
}
411
-
self.text_input.clearAndFree();
412
-
},
413
-
.new_file => {
414
-
const file = self.inputToSlice();
415
-
if (environment.file_exists(self.directories.dir, file)) {
416
-
try self.notification.write_err(.ItemAlreadyExists);
417
-
} else {
418
-
if (self.directories.dir.createFile(file, .{})) |f| {
419
-
f.close();
420
-
self.directories.cleanup();
421
-
self.directories.populate_entries("") catch |err| {
422
-
switch (err) {
423
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
424
-
else => try self.notification.write_err(.UnknownError),
425
-
}
426
-
};
427
-
} else |err| {
428
-
switch (err) {
429
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
430
-
else => try self.notification.write_err(.UnknownError),
431
-
}
432
-
}
433
-
}
434
-
self.text_input.clearAndFree();
435
-
},
436
-
.rename => {
437
-
var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined;
438
-
const dir_prefix = try self.directories.dir.realpath(".", &dir_prefix_buf);
225
+
pub fn run(self: *App) !void {
226
+
try self.repopulateDirectory("");
227
+
try self.loop.start();
228
+
defer self.loop.stop();
439
229
440
-
const old = try self.directories.get_selected();
441
-
const new = self.inputToSlice();
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;
442
233
443
-
if (environment.file_exists(self.directories.dir, new)) {
444
-
try self.notification.write_err(.ItemAlreadyExists);
445
-
} else {
446
-
self.directories.dir.rename(old.name, new) catch |err| switch (err) {
447
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
448
-
error.PathAlreadyExists => try self.notification.write_err(.ItemAlreadyExists),
449
-
else => try self.notification.write_err(.UnknownError),
450
-
};
451
-
// TODO: Will leak memory if pushing to a full stack.
452
-
self.actions.push(.{
453
-
.rename = .{
454
-
.old = try std.fs.path.join(self.alloc, &.{ dir_prefix, old.name }),
455
-
.new = try std.fs.path.join(self.alloc, &.{ dir_prefix, new }),
456
-
},
457
-
});
234
+
while (!self.should_quit) {
235
+
self.loop.pollEvent();
236
+
while (self.loop.tryEvent()) |event| {
237
+
// Global keybinds.
238
+
try EventHandlers.handleGlobalEvent(self, event);
458
239
459
-
self.directories.cleanup();
460
-
self.directories.populate_entries("") catch |err| {
461
-
switch (err) {
462
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
463
-
else => try self.notification.write_err(.UnknownError),
464
-
}
465
-
};
466
-
}
467
-
self.text_input.clearAndFree();
468
-
},
469
-
.change_dir => {
470
-
const path = self.inputToSlice();
471
-
if (self.directories.dir.openDir(path, .{ .iterate = true })) |dir| {
472
-
self.directories.dir = dir;
473
-
474
-
self.directories.cleanup();
475
-
self.directories.populate_entries("") catch |err| {
476
-
switch (err) {
477
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
478
-
else => try self.notification.write_err(.UnknownError),
479
-
}
480
-
};
481
-
self.directories.history.reset();
482
-
} else |err| {
483
-
switch (err) {
484
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
485
-
error.FileNotFound => try self.notification.write_err(.IncorrectPath),
486
-
error.NotDir => try self.notification.write_err(.IncorrectPath),
487
-
else => try self.notification.write_err(.UnknownError),
488
-
}
489
-
}
490
-
491
-
self.text_input.clearAndFree();
492
-
},
493
-
else => {},
494
-
}
495
-
self.state = .normal;
496
-
self.directories.entries.selected = selected;
240
+
// State specific keybinds.
241
+
switch (self.state) {
242
+
.normal => {
243
+
try EventHandlers.handleNormalEvent(self, event);
244
+
},
245
+
.help_menu => {
246
+
try EventHandlers.handleHelpMenuEvent(self, event);
497
247
},
498
248
else => {
499
-
try self.text_input.update(.{ .key_press = key });
500
-
501
-
switch (self.state) {
502
-
.fuzzy => {
503
-
self.directories.cleanup();
504
-
const fuzzy = self.inputToSlice();
505
-
self.directories.populate_entries(fuzzy) catch |err| {
506
-
switch (err) {
507
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
508
-
else => try self.notification.write_err(.UnknownError),
509
-
}
510
-
};
511
-
},
512
-
else => {},
513
-
}
249
+
try EventHandlers.handleInputEvent(self, event);
514
250
},
515
251
}
516
-
},
517
-
.winsize => |ws| {
518
-
try self.vx.resize(self.alloc, self.tty.anyWriter(), ws);
519
-
},
520
-
}
521
-
522
-
return .default;
523
-
}
524
-
525
-
pub fn draw(self: *App) !void {
526
-
const win = self.vx.window();
527
-
win.clear();
528
-
529
-
const abs_file_path_bar = try self.draw_abs_file_path(win);
530
-
var file_info_buf: [1024]u8 = undefined;
531
-
const file_info_bar = try self.draw_file_info(win, &file_info_buf);
532
-
try self.draw_current_dir_list(win, abs_file_path_bar, file_info_bar);
533
-
534
-
if (config.preview_file == true) {
535
-
var file_name_buf: [std.fs.MAX_NAME_BYTES + 2]u8 = undefined;
536
-
const file_name_bar = try self.draw_file_name(win, &file_name_buf);
537
-
try self.draw_preview(win, file_name_bar);
538
-
}
539
-
540
-
try self.draw_info(win);
541
-
542
-
try self.vx.render(self.tty.anyWriter());
543
-
}
544
-
545
-
fn draw_file_name(self: *App, win: vaxis.Window, buf: []u8) !vaxis.Window {
546
-
const file_name_bar = win.child(.{
547
-
.x_off = win.width / 2,
548
-
.y_off = 0,
549
-
.width = .{ .limit = win.width },
550
-
.height = .{ .limit = top_div },
551
-
});
552
-
553
-
if (self.directories.get_selected()) |entry| {
554
-
const file_name = try std.fmt.bufPrint(buf, "[{s}]", .{entry.name});
555
-
_ = try file_name_bar.print(&.{vaxis.Segment{
556
-
.text = file_name,
557
-
.style = config.styles.file_name,
558
-
}}, .{});
559
-
} else |_| {}
560
-
561
-
return file_name_bar;
562
-
}
563
-
564
-
fn draw_preview(self: *App, win: vaxis.Window, file_name_win: vaxis.Window) !void {
565
-
const preview_win = win.child(.{
566
-
.x_off = win.width / 2,
567
-
.y_off = top_div + 1,
568
-
.width = .{ .limit = win.width / 2 },
569
-
.height = .{ .limit = win.height - (file_name_win.height + top_div + bottom_div) },
570
-
});
571
-
572
-
// Populate preview bar
573
-
if (self.directories.entries.len() > 0 and config.preview_file == true) {
574
-
const entry = try self.directories.get_selected();
575
-
576
-
@memcpy(&self.last_item_path_buf, &self.current_item_path_buf);
577
-
self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len];
578
-
self.current_item_path = try std.fmt.bufPrint(&self.current_item_path_buf, "{s}/{s}", .{ try self.directories.full_path("."), entry.name });
579
-
580
-
switch (entry.kind) {
581
-
.directory => {
582
-
self.directories.cleanup_sub();
583
-
if (self.directories.populate_sub_entries(entry.name)) {
584
-
try self.directories.write_sub_entries(preview_win, config.styles.list_item);
585
-
} else |err| {
586
-
switch (err) {
587
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
588
-
else => try self.notification.write_err(.UnknownError),
589
-
}
590
-
}
591
-
},
592
-
.file => file: {
593
-
var file = self.directories.dir.openFile(entry.name, .{ .mode = .read_only }) catch |err| {
594
-
switch (err) {
595
-
error.AccessDenied => try self.notification.write_err(.PermissionDenied),
596
-
else => try self.notification.write_err(.UnknownError),
597
-
}
598
-
599
-
_ = try preview_win.print(&.{
600
-
.{
601
-
.text = "No preview available.",
602
-
},
603
-
}, .{});
604
-
605
-
break :file;
606
-
};
607
-
defer file.close();
608
-
const bytes = try file.readAll(&self.directories.file_contents);
609
-
610
-
// Handle image.
611
-
if (config.show_images == true) unsupported_terminal: {
612
-
const supported: [1][]const u8 = .{".png"};
613
-
614
-
for (supported) |ext| {
615
-
if (std.mem.eql(u8, std.fs.path.extension(entry.name), ext)) {
616
-
if (!std.mem.eql(u8, self.last_item_path, self.current_item_path)) {
617
-
if (self.vx.loadImage(self.alloc, self.tty.anyWriter(), .{ .path = self.current_item_path })) |img| {
618
-
self.image = img;
619
-
} else |_| {
620
-
self.image = null;
621
-
break :unsupported_terminal;
622
-
}
623
-
}
624
-
625
-
if (self.image) |img| {
626
-
try img.draw(preview_win, .{ .scale = .fit });
627
-
}
628
-
629
-
break :file;
630
-
} else {
631
-
// Free any image we might have already.
632
-
if (self.image) |img| {
633
-
self.vx.freeImage(self.tty.anyWriter(), img.id);
634
-
}
635
-
}
636
-
}
637
-
}
638
-
639
-
// Handle pdf.
640
-
if (std.mem.eql(u8, std.fs.path.extension(entry.name), ".pdf")) {
641
-
var child = std.process.Child.init(&.{ "pdftotext", self.current_item_path, "-" }, self.alloc);
642
-
child.stdout_behavior = .Pipe;
643
-
child.stderr_behavior = .Pipe;
644
-
try child.spawn();
645
-
646
-
var stdout = std.ArrayList(u8).init(self.alloc);
647
-
defer stdout.deinit();
648
-
var stderr = std.ArrayList(u8).init(self.alloc);
649
-
defer stderr.deinit();
650
-
try child.collectOutput(&stdout, &stderr, 4096);
651
-
652
-
const term = try child.wait();
653
-
if (term.Exited != 0) {
654
-
_ = try preview_win.print(&.{
655
-
.{
656
-
.text = "No preview available. Install pdftotext to get PDF previews.",
657
-
},
658
-
}, .{});
659
-
break :file;
660
-
}
661
-
662
-
if (self.directories.pdf_contents) |pdf_contents| {
663
-
self.directories.alloc.free(pdf_contents);
664
-
}
665
-
self.directories.pdf_contents = try stdout.toOwnedSlice();
666
-
_ = try preview_win.print(&.{
667
-
.{
668
-
.text = self.directories.pdf_contents.?,
669
-
},
670
-
}, .{});
671
-
break :file;
672
-
}
673
-
674
-
// Handle utf-8.
675
-
if (std.unicode.utf8ValidateSlice(self.directories.file_contents[0..bytes])) {
676
-
_ = try preview_win.print(&.{
677
-
.{
678
-
.text = self.directories.file_contents[0..bytes],
679
-
},
680
-
}, .{});
681
-
break :file;
682
-
}
683
-
684
-
// Fallback to no preview.
685
-
_ = try preview_win.print(&.{
686
-
.{
687
-
.text = "No preview available.",
688
-
},
689
-
}, .{});
690
-
},
691
-
else => {
692
-
_ = try preview_win.print(&.{vaxis.Segment{ .text = self.current_item_path }}, .{});
693
-
},
694
252
}
695
-
}
696
-
}
697
253
698
-
fn draw_file_info(self: *App, win: vaxis.Window, file_info_buf: []u8) !vaxis.Window {
699
-
const file_info = try std.fmt.bufPrint(file_info_buf, "{d}/{d} {s} {s}", .{
700
-
self.directories.entries.selected + 1,
701
-
self.directories.entries.len(),
702
-
std.fs.path.extension(if (self.directories.get_selected()) |entry| entry.name else |_| ""),
703
-
std.fmt.fmtIntSizeDec((try self.directories.dir.metadata()).size()),
704
-
});
705
-
706
-
const file_info_win = win.child(.{
707
-
.x_off = 0,
708
-
.y_off = win.height - bottom_div,
709
-
.width = if (config.preview_file) .{ .limit = win.width / 2 } else .{ .limit = win.width },
710
-
.height = .{ .limit = bottom_div },
711
-
});
712
-
file_info_win.fill(vaxis.Cell{ .style = config.styles.file_information });
713
-
_ = try file_info_win.print(&.{vaxis.Segment{ .text = file_info, .style = config.styles.file_information }}, .{});
254
+
try self.drawer.draw(self);
714
255
715
-
return file_info_win;
716
-
}
717
-
718
-
fn draw_current_dir_list(self: *App, win: vaxis.Window, abs_file_path: vaxis.Window, file_information: vaxis.Window) !void {
719
-
const current_dir_list_win = win.child(.{
720
-
.x_off = 0,
721
-
.y_off = top_div + 1,
722
-
.width = if (config.preview_file) .{ .limit = win.width / 2 } else .{ .limit = win.width },
723
-
.height = .{ .limit = win.height - (abs_file_path.height + file_information.height + top_div + bottom_div) },
724
-
});
725
-
try self.directories.write_entries(current_dir_list_win, config.styles.selected_list_item, config.styles.list_item);
726
-
727
-
self.last_known_height = current_dir_list_win.height;
728
-
}
729
-
730
-
fn draw_abs_file_path(self: *App, win: vaxis.Window) !vaxis.Window {
731
-
const abs_file_path_bar = win.child(.{
732
-
.x_off = 0,
733
-
.y_off = 0,
734
-
.width = .{ .limit = win.width },
735
-
.height = .{ .limit = top_div },
736
-
});
737
-
_ = try abs_file_path_bar.print(&.{vaxis.Segment{ .text = try self.directories.full_path(".") }}, .{});
738
-
739
-
return abs_file_path_bar;
740
-
}
741
-
742
-
fn draw_info(self: *App, win: vaxis.Window) !void {
743
-
const info_win = win.child(.{
744
-
.x_off = 0,
745
-
.y_off = top_div,
746
-
.width = .{ .limit = win.width },
747
-
.height = .{ .limit = info_div },
748
-
});
749
-
750
-
// Display info box.
751
-
if (self.notification.len > 0) {
752
-
if (self.text_input.grapheme_count > 0) {
753
-
self.text_input.clearAndFree();
754
-
}
755
-
756
-
_ = try info_win.print(&.{
757
-
.{
758
-
.text = self.notification.slice(),
759
-
.style = switch (self.notification.style) {
760
-
.info => config.styles.info_bar,
761
-
.err => config.styles.error_bar,
762
-
},
763
-
},
764
-
}, .{});
256
+
try self.vx.render(self.tty.writer());
765
257
}
766
258
767
-
// Display user input box.
768
-
switch (self.state) {
769
-
.fuzzy, .new_file, .new_dir, .rename, .change_dir => {
770
-
self.notification.reset();
771
-
self.text_input.draw(info_win);
772
-
},
773
-
.normal => {
774
-
if (self.text_input.grapheme_count > 0) {
775
-
self.text_input.draw(info_win);
259
+
if (config.empty_trash_on_exit) {
260
+
var trash_dir = dir: {
261
+
notfound: {
262
+
break :dir (config.trashDir() catch break :notfound) orelse break :notfound;
776
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();
777
270
778
-
win.hideCursor();
779
-
},
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
+
};
285
+
}
780
286
}
781
287
}
+5
-1
src/circ_stack.zig
+5
-1
src/circ_stack.zig
···
17
17
self.count = 0;
18
18
}
19
19
20
-
pub fn push(self: *Self, v: T) void {
20
+
pub fn push(self: *Self, v: T) ?T {
21
+
const prev_elem = if (self.count == capacity) self.buf[self.head] else null;
22
+
21
23
self.buf[self.head] = v;
22
24
self.head = (self.head + 1) % capacity;
23
25
if (self.count != capacity) self.count += 1;
26
+
27
+
return prev_elem;
24
28
}
25
29
26
30
pub fn pop(self: *Self) ?T {
+174
src/commands.zig
+174
src/commands.zig
···
1
+
const std = @import("std");
2
+
const App = @import("app.zig");
3
+
const environment = @import("environment.zig");
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
+
};
66
+
67
+
///Navigate the user to the config dir.
68
+
pub fn config(app: *App) error{OutOfMemory}!void {
69
+
const dir = dir: {
70
+
notfound: {
71
+
break :dir (user_config.configDir() catch break :notfound) orelse break :notfound;
72
+
}
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 {};
77
+
return;
78
+
};
79
+
80
+
app.directories.dir.close();
81
+
app.directories.dir = dir;
82
+
try app.repopulateDirectory("");
83
+
}
84
+
85
+
///Navigate the user to the trash dir.
86
+
pub fn trash(app: *App) error{OutOfMemory}!void {
87
+
const dir = dir: {
88
+
notfound: {
89
+
break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
90
+
}
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 {};
95
+
return;
96
+
};
97
+
98
+
app.directories.dir.close();
99
+
app.directories.dir = dir;
100
+
try app.repopulateDirectory("");
101
+
}
102
+
103
+
///Empty the trash.
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: {
109
+
notfound: {
110
+
break :dir (user_config.trashDir() catch break :notfound) orelse break :notfound;
111
+
}
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 {};
115
+
return;
116
+
};
117
+
defer dir.close();
118
+
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
+
}
130
+
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;
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();
174
+
}
+201
-33
src/config.zig
+201
-33
src/config.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
+
const environment = @import("./environment.zig");
3
4
const vaxis = @import("vaxis");
4
-
const environment = @import("./environment.zig");
5
+
const FileLogger = @import("file_logger.zig");
6
+
const Notification = @import("./notification.zig");
7
+
const App = @import("./app.zig");
8
+
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";
5
13
6
14
const Config = struct {
7
15
show_hidden: bool = true,
8
16
sort_dirs: bool = true,
9
17
show_images: bool = true,
10
18
preview_file: bool = true,
11
-
styles: Styles,
19
+
empty_trash_on_exit: bool = false,
20
+
true_dir_size: bool = false,
21
+
entry_dir: ?[]const u8 = null,
22
+
styles: Styles = .{},
23
+
keybinds: Keybinds = .{},
24
+
25
+
config_dir: ?std.fs.Dir = null,
26
+
27
+
///Returned dir needs to be closed by user.
28
+
pub fn configDir(self: Config) !?std.fs.Dir {
29
+
if (self.config_dir) |dir| {
30
+
return try dir.openDir(".", .{ .iterate = true });
31
+
} else return null;
32
+
}
33
+
34
+
///Returned dir needs to be closed by user.
35
+
pub fn trashDir(self: Config) !?std.fs.Dir {
36
+
var parent = try self.configDir() orelse return null;
37
+
defer parent.close();
38
+
if (!environment.dirExists(parent, TRASH_DIR_NAME)) {
39
+
try parent.makeDir(TRASH_DIR_NAME);
40
+
}
41
+
42
+
return try parent.openDir(TRASH_DIR_NAME, .{ .iterate = true });
43
+
}
44
+
45
+
pub fn parse(self: *Config, alloc: std.mem.Allocator, app: *App) !void {
46
+
var dir = lbl: {
47
+
if (try environment.getXdgConfigHomeDir()) |home_dir| {
48
+
defer {
49
+
var dir = home_dir;
50
+
dir.close();
51
+
}
52
+
53
+
if (!environment.dirExists(home_dir, XDG_CONFIG_HOME_DIR_NAME)) {
54
+
try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME);
55
+
}
56
+
57
+
const jido_dir = try home_dir.openDir(
58
+
XDG_CONFIG_HOME_DIR_NAME,
59
+
.{ .iterate = true },
60
+
);
61
+
self.config_dir = jido_dir;
62
+
63
+
if (environment.fileExists(jido_dir, CONFIG_NAME)) {
64
+
break :lbl jido_dir;
65
+
}
66
+
return;
67
+
}
12
68
13
-
pub fn parse(self: *Config, alloc: std.mem.Allocator) !void {
14
-
var config_path: []u8 = undefined;
15
-
defer alloc.free(config_path);
69
+
if (try environment.getHomeDir()) |home_dir| {
70
+
defer {
71
+
var dir = home_dir;
72
+
dir.close();
73
+
}
74
+
75
+
if (!environment.dirExists(home_dir, HOME_DIR_NAME)) {
76
+
try home_dir.makeDir(HOME_DIR_NAME);
77
+
}
16
78
17
-
var config_home: std.fs.Dir = undefined;
18
-
defer config_home.close();
19
-
if (try environment.get_xdg_config_home_dir()) |path| {
20
-
config_home = path;
21
-
config_path = try std.fs.path.join(alloc, &.{ "zfe", "config.json" });
22
-
} else {
23
-
if (try environment.get_home_dir()) |path| {
24
-
config_home = path;
25
-
config_path = try std.fs.path.join(alloc, &.{ ".config", "zfe", "config.json" });
26
-
} else {
27
-
return error.MissingConfigHomeEnvironmentVariable;
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;
28
89
}
29
-
}
30
90
31
-
if (!environment.file_exists(config_home, config_path)) {
32
-
return error.ConfigNotFound;
33
-
}
91
+
return;
92
+
};
34
93
35
-
const config_file = try config_home.openFile(config_path, .{});
94
+
const config_file = try dir.openFile(CONFIG_NAME, .{});
36
95
defer config_file.close();
37
96
38
97
const config_str = try config_file.readToEndAlloc(alloc, 1024 * 1024 * 1024);
39
98
defer alloc.free(config_str);
40
99
41
-
const c = try std.json.parseFromSlice(Config, alloc, config_str, .{});
42
-
defer c.deinit();
100
+
const parsed_config = try std.json.parseFromSlice(Config, alloc, config_str, .{});
101
+
defer parsed_config.deinit();
102
+
103
+
self.* = parsed_config.value;
104
+
self.config_dir = dir;
43
105
44
-
self.* = c.value;
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;
45
147
}
46
148
};
47
149
150
+
const Colours = struct {
151
+
const RGB = [3]u8;
152
+
const red: RGB = .{ 227, 23, 10 };
153
+
const orange: RGB = .{ 251, 139, 36 };
154
+
const blue: RGB = .{ 82, 209, 220 };
155
+
const grey: RGB = .{ 39, 39, 39 };
156
+
const black: RGB = .{ 0, 0, 0 };
157
+
const snow_white: RGB = .{ 254, 252, 253 };
158
+
};
159
+
160
+
const NotificationStyles = struct {
161
+
box: vaxis.Style = vaxis.Style{
162
+
.fg = .{ .rgb = Colours.snow_white },
163
+
.bg = .{ .rgb = Colours.grey },
164
+
},
165
+
err: vaxis.Style = vaxis.Style{
166
+
.fg = .{ .rgb = Colours.red },
167
+
.bg = .{ .rgb = Colours.grey },
168
+
},
169
+
warn: vaxis.Style = vaxis.Style{
170
+
.fg = .{ .rgb = Colours.orange },
171
+
.bg = .{ .rgb = Colours.grey },
172
+
},
173
+
info: vaxis.Style = vaxis.Style{
174
+
.fg = .{ .rgb = Colours.blue },
175
+
.bg = .{ .rgb = Colours.grey },
176
+
},
177
+
};
178
+
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
+
48
216
const Styles = struct {
49
217
selected_list_item: vaxis.Style = vaxis.Style{
50
-
.bg = .{ .rgb = .{ 45, 45, 45 } },
218
+
.bg = .{ .rgb = Colours.grey },
51
219
.bold = true,
52
220
},
221
+
notification: NotificationStyles = NotificationStyles{},
222
+
text_input: vaxis.Style = vaxis.Style{},
223
+
text_input_err: vaxis.Style = vaxis.Style{ .bg = .{ .rgb = Colours.red } },
53
224
list_item: vaxis.Style = vaxis.Style{},
54
225
file_name: vaxis.Style = vaxis.Style{},
55
226
file_information: vaxis.Style = vaxis.Style{
56
-
.fg = .{ .rgb = .{ 0, 0, 0 } },
57
-
.bg = .{ .rgb = .{ 255, 255, 255 } },
58
-
},
59
-
error_bar: vaxis.Style = vaxis.Style{
60
-
.bg = .{ .rgb = .{ 216, 74, 74 } },
227
+
.fg = .{ .rgb = Colours.black },
228
+
.bg = .{ .rgb = Colours.snow_white },
61
229
},
62
-
info_bar: vaxis.Style = vaxis.Style{
63
-
.bg = .{ .rgb = .{ 0, 140, 200 } },
230
+
git_branch: vaxis.Style = vaxis.Style{
231
+
.fg = .{ .rgb = Colours.blue },
64
232
},
65
233
};
66
234
67
-
pub var config: Config = Config{ .styles = Styles{} };
235
+
pub var config: Config = Config{};
+71
-100
src/directories.zig
+71
-100
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),
24
-
sub_entries: List([]const u8),
18
+
history: CircStack(usize, history_len),
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(),
33
-
.sub_entries = List([]const u8).init(alloc),
41
+
.history = CircStack(usize, history_len).init(),
42
+
.child_entries = List([]const u8).init(alloc),
34
43
.searcher = try fuzzig.Ascii.init(
35
44
alloc,
36
45
std.fs.max_path_bytes,
···
41
50
}
42
51
43
52
pub fn deinit(self: *Self) void {
44
-
self.cleanup();
45
-
self.cleanup_sub();
53
+
self.clearEntries();
54
+
self.clearChildEntries();
46
55
47
56
self.entries.deinit();
48
-
self.sub_entries.deinit();
49
-
50
-
if (self.pdf_contents) |pdf_contents| {
51
-
self.alloc.free(pdf_contents);
52
-
}
57
+
self.child_entries.deinit();
53
58
54
59
self.dir.close();
55
60
self.searcher.deinit();
61
+
62
+
if (self.pdf_contents) |contents| self.alloc.free(contents);
56
63
}
57
64
58
-
pub fn get_selected(self: *Self) !std.fs.Dir.Entry {
59
-
return self.entries.get_selected();
65
+
pub fn getSelected(self: *Self) !?std.fs.Dir.Entry {
66
+
return self.entries.getSelected();
60
67
}
61
68
62
69
/// Asserts there is a selected item.
63
-
pub fn remove_selected(self: *Self) void {
64
-
const entry = self.get_selected() catch return;
70
+
pub fn removeSelected(self: *Self) void {
71
+
const entry = lbl: {
72
+
const entry = self.getSelected() catch return std.debug.assert(false);
73
+
if (entry) |e| break :lbl e else return std.debug.assert(false);
74
+
};
65
75
self.alloc.free(entry.name);
66
76
_ = self.entries.items.orderedRemove(self.entries.selected);
67
77
}
68
78
69
-
pub fn full_path(self: *Self, relative_path: []const u8) ![]const u8 {
79
+
pub fn fullPath(self: *Self, relative_path: []const u8) ![]const u8 {
70
80
return try self.dir.realpath(relative_path, &self.path_buf);
71
81
}
72
82
73
-
pub fn populate_sub_entries(
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
+
102
+
pub fn populateChildEntries(
74
103
self: *Self,
75
104
relative_path: []const u8,
76
105
) !void {
···
79
108
80
109
var it = dir.iterate();
81
110
while (try it.next()) |entry| {
82
-
try self.sub_entries.append(try self.alloc.dupe(u8, entry.name));
83
-
}
84
-
85
-
if (config.sort_dirs == true) {
86
-
std.mem.sort([]const u8, self.sub_entries.all(), {}, sort_sub_entry);
87
-
}
88
-
}
89
-
90
-
pub fn write_sub_entries(
91
-
self: *Self,
92
-
window: vaxis.Window,
93
-
style: vaxis.Style,
94
-
) !void {
95
-
for (self.sub_entries.all(), 0..) |item, i| {
96
-
if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) {
97
-
continue;
98
-
}
99
-
100
-
if (i > window.height) {
111
+
if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) {
101
112
continue;
102
113
}
103
114
104
-
const w = window.child(.{
105
-
.y_off = i,
106
-
.height = .{ .limit = 1 },
107
-
});
108
-
w.fill(vaxis.Cell{
109
-
.style = style,
110
-
});
115
+
try self.child_entries.append(try self.alloc.dupe(u8, entry.name));
116
+
}
111
117
112
-
_ = try w.print(&.{
113
-
.{
114
-
.text = item,
115
-
.style = style,
116
-
},
117
-
}, .{});
118
+
if (config.sort_dirs == true) {
119
+
std.mem.sort([]const u8, self.child_entries.all(), {}, sortChildEntry);
118
120
}
119
121
}
120
122
121
-
pub fn populate_entries(self: *Self, fuzzy_search: []const u8) !void {
123
+
pub fn populateEntries(self: *Self, fuzzy_search: []const u8) !void {
122
124
var it = self.dir.iterate();
123
125
while (try it.next()) |entry| {
124
126
const score = self.searcher.score(entry.name, fuzzy_search) orelse 0;
···
126
128
continue;
127
129
}
128
130
131
+
if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) {
132
+
continue;
133
+
}
134
+
129
135
try self.entries.append(.{
130
136
.kind = entry.kind,
131
137
.name = try self.alloc.dupe(u8, entry.name),
···
133
139
}
134
140
135
141
if (config.sort_dirs == true) {
136
-
std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sort_entry);
142
+
std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sortEntry);
137
143
}
138
144
}
139
145
140
-
pub fn write_entries(
141
-
self: *Self,
142
-
window: vaxis.Window,
143
-
selected_list_item_style: vaxis.Style,
144
-
list_item_style: vaxis.Style,
145
-
) !void {
146
-
for (self.entries.all()[self.entries.offset..], 0..) |item, i| {
147
-
const selected = self.entries.selected - self.entries.offset;
148
-
const is_selected = selected == i;
149
-
150
-
if (std.mem.startsWith(u8, item.name, ".") and config.show_hidden == false) {
151
-
continue;
152
-
}
153
-
154
-
if (i > window.height) {
155
-
continue;
156
-
}
157
-
158
-
const w = window.child(.{
159
-
.y_off = i,
160
-
.height = .{ .limit = 1 },
161
-
});
162
-
w.fill(vaxis.Cell{
163
-
.style = if (is_selected) selected_list_item_style else list_item_style,
164
-
});
165
-
166
-
_ = try w.print(&.{
167
-
.{
168
-
.text = item.name,
169
-
.style = if (is_selected) selected_list_item_style else list_item_style,
170
-
},
171
-
}, .{});
172
-
}
173
-
}
174
-
175
-
fn sort_entry(_: void, lhs: std.fs.Dir.Entry, rhs: std.fs.Dir.Entry) bool {
146
+
fn sortEntry(_: void, lhs: std.fs.Dir.Entry, rhs: std.fs.Dir.Entry) bool {
176
147
return std.mem.lessThan(u8, lhs.name, rhs.name);
177
148
}
178
149
179
-
fn sort_sub_entry(_: void, lhs: []const u8, rhs: []const u8) bool {
150
+
fn sortChildEntry(_: void, lhs: []const u8, rhs: []const u8) bool {
180
151
return std.mem.lessThan(u8, lhs, rhs);
181
152
}
182
153
183
-
pub fn cleanup(self: *Self) void {
154
+
pub fn clearEntries(self: *Self) void {
184
155
for (self.entries.all()) |entry| {
185
156
self.entries.alloc.free(entry.name);
186
157
}
187
158
self.entries.clear();
188
159
}
189
160
190
-
pub fn cleanup_sub(self: *Self) void {
191
-
for (self.sub_entries.all()) |entry| {
192
-
self.sub_entries.alloc.free(entry);
161
+
pub fn clearChildEntries(self: *Self) void {
162
+
for (self.child_entries.all()) |entry| {
163
+
self.child_entries.alloc.free(entry);
193
164
}
194
-
self.sub_entries.clear();
165
+
self.child_entries.clear();
195
166
}
+716
src/drawer.zig
+716
src/drawer.zig
···
1
+
const std = @import("std");
2
+
const App = @import("./app.zig");
3
+
const FileLogger = @import("./file_logger.zig");
4
+
const Notification = @import("./notification.zig");
5
+
const Directories = @import("./directories.zig");
6
+
const config = &@import("./config.zig").config;
7
+
const vaxis = @import("vaxis");
8
+
const Git = @import("./git.zig");
9
+
const List = @import("./list.zig").List;
10
+
const zeit = @import("zeit");
11
+
12
+
const Drawer = @This();
13
+
14
+
const top_div: u16 = 1;
15
+
const info_div: u16 = 1;
16
+
17
+
// Used to detect whether to re-render an image.
18
+
current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
19
+
current_item_path: []u8 = "",
20
+
last_item_path_buf: [std.fs.max_path_bytes]u8 = undefined,
21
+
last_item_path: []u8 = "",
22
+
file_info_buf: [std.fs.max_path_bytes]u8 = undefined,
23
+
file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [<file_name>]
24
+
git_branch: [1024]u8 = undefined,
25
+
verbose: bool = false,
26
+
27
+
pub fn draw(self: *Drawer, app: *App) error{ OutOfMemory, NoSpaceLeft }!void {
28
+
const win = app.vx.window();
29
+
win.clear();
30
+
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(
54
+
win,
55
+
app.directories.entries,
56
+
abs_file_path_bar,
57
+
file_info_bar,
58
+
);
59
+
60
+
if (config.preview_file) {
61
+
const file_name_bar = try self.drawFileName(&app.directories, win);
62
+
try self.drawFilePreview(app, win, file_name_bar);
63
+
}
64
+
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);
70
+
}
71
+
72
+
fn drawFileName(
73
+
self: *Drawer,
74
+
directories: *Directories,
75
+
win: vaxis.Window,
76
+
) error{NoSpaceLeft}!vaxis.Window {
77
+
const file_name_bar = win.child(.{
78
+
.x_off = win.width / 2,
79
+
.y_off = 0,
80
+
.width = win.width,
81
+
.height = top_div,
82
+
});
83
+
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 }, .{});
91
+
92
+
return file_name_bar;
93
+
}
94
+
95
+
fn drawFilePreview(
96
+
self: *Drawer,
97
+
app: *App,
98
+
win: vaxis.Window,
99
+
file_name_win: vaxis.Window,
100
+
) error{ OutOfMemory, NoSpaceLeft }!void {
101
+
const bottom_div: u16 = 1;
102
+
103
+
const preview_win = win.child(.{
104
+
.x_off = win.width / 2,
105
+
.y_off = top_div + 1,
106
+
.width = win.width / 2,
107
+
.height = win.height - (file_name_win.height + top_div + bottom_div),
108
+
});
109
+
110
+
if (app.directories.entries.len() == 0 or !config.preview_file) return;
111
+
112
+
const entry = lbl: {
113
+
const entry = app.directories.getSelected() catch return;
114
+
if (entry) |e| break :lbl e else return;
115
+
};
116
+
117
+
@memcpy(&self.last_item_path_buf, &self.current_item_path_buf);
118
+
self.last_item_path = self.last_item_path_buf[0..self.current_item_path.len];
119
+
self.current_item_path = try std.fmt.bufPrint(
120
+
&self.current_item_path_buf,
121
+
"{s}/{s}",
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 },
133
+
);
134
+
135
+
switch (entry.kind) {
136
+
.directory => {
137
+
app.directories.clearChildEntries();
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;
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 }}, .{});
159
+
}
160
+
},
161
+
.file => file: {
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 {};
170
+
171
+
_ = preview_win.print(&.{
172
+
.{ .text = "Failed to open file. No preview available." },
173
+
}, .{});
174
+
175
+
break :file;
176
+
};
177
+
defer file.close();
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
+
};
190
+
191
+
// Handle image.
192
+
if (config.show_images == true) unsupported: {
193
+
var match = false;
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;
197
+
}
198
+
if (!match) break :unsupported;
199
+
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
+
}
243
+
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 {};
250
+
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
+
}
265
+
}
266
+
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
+
};
278
+
}
279
+
280
+
break :file;
281
+
}
282
+
283
+
// Handle pdf.
284
+
if (std.mem.eql(u8, std.fs.path.extension(entry.name), ".pdf")) {
285
+
const output = std.process.Child.run(.{
286
+
.allocator = app.alloc,
287
+
.argv = &[_][]const u8{
288
+
"pdftotext",
289
+
"-f",
290
+
"0",
291
+
"-l",
292
+
"5",
293
+
self.current_item_path,
294
+
"-",
295
+
},
296
+
.cwd_dir = app.directories.dir,
297
+
}) catch {
298
+
_ = preview_win.print(&.{.{
299
+
.text = "No preview available. Install pdftotext to get PDF previews.",
300
+
}}, .{});
301
+
break :file;
302
+
};
303
+
defer app.alloc.free(output.stderr);
304
+
defer app.alloc.free(output.stdout);
305
+
306
+
if (output.term.Exited != 0) {
307
+
_ = preview_win.print(&.{.{
308
+
.text = "No preview available. Install pdftotext to get PDF previews.",
309
+
}}, .{});
310
+
break :file;
311
+
}
312
+
313
+
if (app.directories.pdf_contents) |contents| app.alloc.free(contents);
314
+
app.directories.pdf_contents = try app.alloc.dupe(u8, output.stdout);
315
+
316
+
_ = preview_win.print(&.{
317
+
.{ .text = app.directories.pdf_contents.? },
318
+
}, .{});
319
+
break :file;
320
+
}
321
+
322
+
// Handle utf-8.
323
+
if (std.unicode.utf8ValidateSlice(app.directories.file_contents[0..bytes])) {
324
+
_ = preview_win.print(&.{
325
+
.{ .text = app.directories.file_contents[0..bytes] },
326
+
}, .{});
327
+
break :file;
328
+
}
329
+
330
+
// Fallback to no preview.
331
+
_ = preview_win.print(&.{.{ .text = "No preview available." }}, .{});
332
+
},
333
+
else => {
334
+
_ = preview_win.print(&.{
335
+
vaxis.Segment{ .text = self.current_item_path },
336
+
}, .{});
337
+
},
338
+
}
339
+
}
340
+
341
+
fn drawFileInfo(
342
+
self: *Drawer,
343
+
alloc: std.mem.Allocator,
344
+
directories: *Directories,
345
+
win: vaxis.Window,
346
+
) error{NoSpaceLeft}!vaxis.Window {
347
+
const bottom_div: u16 = if (self.verbose) 6 else 1;
348
+
349
+
const file_info_win = win.child(.{
350
+
.x_off = 0,
351
+
.y_off = win.height - bottom_div,
352
+
.width = if (config.preview_file) win.width / 2 else win.width,
353
+
.height = bottom_div,
354
+
});
355
+
file_info_win.fill(.{ .style = config.styles.file_information });
356
+
357
+
const entry = lbl: {
358
+
const entry = directories.getSelected() catch return file_info_win;
359
+
if (entry) |e| break :lbl e else return file_info_win;
360
+
};
361
+
362
+
var fbs = std.io.fixedBufferStream(&self.file_info_buf);
363
+
364
+
// Selected entry.
365
+
try fbs.writer().print(
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
+
},
373
+
);
374
+
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;
404
+
}
405
+
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
+
}
445
+
};
446
+
try fbs.writer().writeAll(file_perm_buf[0..file_perms]);
447
+
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
+
});
472
+
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
+
}
486
+
487
+
_ = file_info_win.printSegment(.{
488
+
.text = fbs.getWritten(),
489
+
.style = config.styles.file_information,
490
+
}, .{});
491
+
492
+
return file_info_win;
493
+
}
494
+
495
+
fn drawDirList(
496
+
win: vaxis.Window,
497
+
list: List(std.fs.Dir.Entry),
498
+
abs_file_path: vaxis.Window,
499
+
file_information: vaxis.Window,
500
+
) u16 {
501
+
const bottom_div: u16 = 1;
502
+
503
+
const current_dir_list_win = win.child(.{
504
+
.x_off = 0,
505
+
.y_off = top_div + 1,
506
+
.width = if (config.preview_file) win.width / 2 else win.width,
507
+
.height = win.height - (abs_file_path.height + file_information.height + top_div + bottom_div),
508
+
});
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
+
}
537
+
538
+
return win_height;
539
+
}
540
+
541
+
fn drawAbsFilePath(
542
+
self: *Drawer,
543
+
app: *App,
544
+
win: vaxis.Window,
545
+
) error{ OutOfMemory, NoSpaceLeft }!vaxis.Window {
546
+
const abs_file_path_bar = win.child(.{
547
+
.x_off = 0,
548
+
.y_off = 0,
549
+
.width = win.width,
550
+
.height = top_div,
551
+
});
552
+
553
+
const branch_alloc = Git.getGitBranch(app.alloc, app.directories.dir) catch null;
554
+
defer if (branch_alloc) |b| app.alloc.free(b);
555
+
const branch = if (branch_alloc) |b|
556
+
try std.fmt.bufPrint(
557
+
&self.git_branch,
558
+
"{s}",
559
+
.{std.mem.trim(u8, b, " \n\r")},
560
+
)
561
+
else
562
+
"";
563
+
564
+
_ = abs_file_path_bar.print(&.{
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
+
} },
572
+
vaxis.Segment{ .text = if (branch_alloc != null) " on " else "" },
573
+
vaxis.Segment{ .text = branch, .style = config.styles.git_branch },
574
+
}, .{});
575
+
576
+
return abs_file_path_bar;
577
+
}
578
+
579
+
fn drawUserInput(
580
+
current_state: App.State,
581
+
text_input: *vaxis.widgets.TextInput,
582
+
input: []const u8,
583
+
win: vaxis.Window,
584
+
) void {
585
+
const user_input_win = win.child(.{
586
+
.x_off = 0,
587
+
.y_off = top_div,
588
+
.width = win.width / 2,
589
+
.height = info_div,
590
+
});
591
+
user_input_win.fill(.{ .style = config.styles.text_input });
592
+
593
+
switch (current_state) {
594
+
.fuzzy, .new_file, .new_dir, .rename, .change_dir, .command => {
595
+
text_input.drawWithStyle(user_input_win, config.styles.text_input);
596
+
},
597
+
.normal => {
598
+
if (text_input.buf.realLength() > 0) {
599
+
text_input.drawWithStyle(
600
+
user_input_win,
601
+
if (std.mem.eql(u8, input, ":UnsupportedCommand"))
602
+
config.styles.text_input_err
603
+
else
604
+
config.styles.text_input,
605
+
);
606
+
}
607
+
608
+
win.hideCursor();
609
+
},
610
+
.help_menu => {
611
+
win.hideCursor();
612
+
},
613
+
}
614
+
}
615
+
616
+
fn drawNotification(
617
+
notification: *Notification,
618
+
file_logger: *?FileLogger,
619
+
win: vaxis.Window,
620
+
) void {
621
+
if (notification.len() == 0) return;
622
+
if (notification.clearIfEnded()) return;
623
+
624
+
const width_padding = 4;
625
+
const height_padding = 3;
626
+
const screen_pos_padding = 10;
627
+
628
+
const max_width = win.width / 4;
629
+
const width = notification.len() + width_padding;
630
+
const calculated_width = if (width > max_width) max_width else width;
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;
635
+
636
+
const notification_win = win.child(.{
637
+
.x_off = @intCast(win.width - (calculated_width + screen_pos_padding)),
638
+
.y_off = top_div,
639
+
.width = @intCast(calculated_width),
640
+
.height = @intCast(height),
641
+
.border = .{ .where = .all, .style = switch (notification.style) {
642
+
.info => config.styles.notification.info,
643
+
.err => config.styles.notification.err,
644
+
.warn => config.styles.notification.warn,
645
+
} },
646
+
});
647
+
648
+
notification_win.fill(.{ .style = config.styles.notification.box });
649
+
_ = notification_win.printSegment(.{
650
+
.text = notification.slice(),
651
+
.style = config.styles.notification.box,
652
+
}, .{ .wrap = .word });
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
+
}
+51
-10
src/environment.zig
+51
-10
src/environment.zig
···
1
1
const std = @import("std");
2
+
const zuid = @import("zuid");
2
3
const builtin = @import("builtin");
3
4
4
-
const log = &@import("./log.zig").log;
5
-
6
-
pub fn get_home_dir() !?std.fs.Dir {
5
+
pub fn getHomeDir() !?std.fs.Dir {
7
6
return try std.fs.openDirAbsolute(std.posix.getenv("HOME") orelse {
8
7
return null;
9
8
}, .{ .iterate = true });
10
9
}
11
10
12
-
pub fn get_xdg_config_home_dir() !?std.fs.Dir {
11
+
pub fn getXdgConfigHomeDir() !?std.fs.Dir {
13
12
return try std.fs.openDirAbsolute(std.posix.getenv("XDG_CONFIG_HOME") orelse {
14
13
return null;
15
14
}, .{ .iterate = true });
16
15
}
17
16
18
-
pub fn get_editor() ?[]const u8 {
17
+
pub fn getEditor() ?[]const u8 {
19
18
const editor = std.posix.getenv("EDITOR");
20
19
if (editor) |e| {
21
20
if (std.mem.trim(u8, e, " ").len > 0) {
···
25
24
return null;
26
25
}
27
26
28
-
pub fn open_file(alloc: std.mem.Allocator, dir: std.fs.Dir, file: []const u8, editor: []const u8) !void {
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
+
51
+
pub fn openFile(
52
+
alloc: std.mem.Allocator,
53
+
dir: std.fs.Dir,
54
+
file: []const u8,
55
+
editor: []const u8,
56
+
) !void {
29
57
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
30
58
const path = try dir.realpath(file, &path_buf);
31
59
···
33
61
_ = try child.spawnAndWait();
34
62
}
35
63
36
-
pub fn file_exists(dir: std.fs.Dir, path: []const u8) bool {
64
+
pub fn fileExists(dir: std.fs.Dir, path: []const u8) bool {
37
65
const result = blk: {
38
66
_ = dir.openFile(path, .{}) catch |err| {
39
67
switch (err) {
40
68
error.FileNotFound => break :blk false,
41
69
else => {
42
-
log.info("{}", .{err});
70
+
std.log.info("{}", .{err});
43
71
break :blk true;
44
72
},
45
73
}
···
49
77
return result;
50
78
}
51
79
52
-
pub fn dir_exists(dir: std.fs.Dir, path: []const u8) bool {
80
+
pub fn dirExists(dir: std.fs.Dir, path: []const u8) bool {
53
81
const result = blk: {
54
82
_ = dir.openDir(path, .{}) catch |err| {
55
83
switch (err) {
56
84
error.FileNotFound => break :blk false,
57
85
else => {
58
-
log.info("{}", .{err});
86
+
std.log.info("{}", .{err});
59
87
break :blk true;
60
88
},
61
89
}
···
64
92
};
65
93
return result;
66
94
}
95
+
96
+
///Deletes the contents of a directory but not the directory itself.
97
+
///Returns the amount of files failed to be delete.
98
+
pub fn deleteContents(dir: std.fs.Dir) !usize {
99
+
var failed: usize = 0;
100
+
var it = dir.iterate();
101
+
while (try it.next()) |entry| {
102
+
dir.deleteTree(entry.name) catch {
103
+
failed += 1;
104
+
};
105
+
}
106
+
return failed;
107
+
}
+306
src/event_handlers.zig
+306
src/event_handlers.zig
···
1
+
const std = @import("std");
2
+
const App = @import("./app.zig");
3
+
const environment = @import("./environment.zig");
4
+
const zuid = @import("zuid");
5
+
const vaxis = @import("vaxis");
6
+
const Key = vaxis.Key;
7
+
const config = &@import("./config.zig").config;
8
+
const commands = @import("./commands.zig");
9
+
const Keybinds = @import("./config.zig").Keybinds;
10
+
const events = @import("./events.zig");
11
+
12
+
pub fn handleGlobalEvent(
13
+
app: *App,
14
+
event: App.Event,
15
+
) error{OutOfMemory}!void {
16
+
switch (event) {
17
+
.key_press => |key| {
18
+
if ((key.codepoint == 'c' and key.mods.ctrl)) {
19
+
app.should_quit = true;
20
+
return;
21
+
}
22
+
23
+
if ((key.codepoint == 'r' and key.mods.ctrl)) {
24
+
if (config.parse(app.alloc, app)) {
25
+
app.notification.write("Reloaded configuration file.", .info) catch {};
26
+
} else |err| switch (err) {
27
+
error.SyntaxError => {
28
+
app.notification.write("Encountered a syntax error while parsing the config file.", .err) catch {
29
+
std.log.err("Encountered a syntax error while parsing the config file.", .{});
30
+
};
31
+
},
32
+
error.InvalidCharacter => {
33
+
app.notification.write("One or more overriden keybinds are invalid.", .err) catch {
34
+
std.log.err("One or more overriden keybinds are invalid.", .{});
35
+
};
36
+
},
37
+
error.DuplicateKeybind => {
38
+
// Error logged in function
39
+
},
40
+
else => {
41
+
const message = try std.fmt.allocPrint(app.alloc, "Encountend an unknown error while parsing the config file - {}", .{err});
42
+
defer app.alloc.free(message);
43
+
44
+
app.notification.write(message, .err) catch {
45
+
std.log.err("Encountend an unknown error while parsing the config file - {}", .{err});
46
+
};
47
+
},
48
+
}
49
+
}
50
+
},
51
+
else => {},
52
+
}
53
+
}
54
+
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
+
);
64
+
65
+
const maybe_remap: ?std.meta.FieldEnum(Keybinds) = lbl: {
66
+
inline for (std.meta.fields(Keybinds)) |field| {
67
+
if (@field(config.keybinds, field.name)) |field_value| {
68
+
if (key.codepoint == @intFromEnum(field_value)) {
69
+
break :lbl comptime std.meta.stringToEnum(std.meta.FieldEnum(Keybinds), field.name) orelse unreachable;
70
+
}
71
+
}
72
+
}
73
+
break :lbl null;
74
+
};
75
+
76
+
if (maybe_remap) |action| {
77
+
switch (action) {
78
+
.toggle_hidden_files => try events.toggleHiddenFiles(app),
79
+
.delete => try events.delete(app),
80
+
.rename => {
81
+
const entry = (app.directories.getSelected() catch {
82
+
app.notification.write("Can not rename item - no item selected.", .warn) catch {};
83
+
return;
84
+
}) orelse return;
85
+
86
+
app.text_input.clearAndFree();
87
+
88
+
// Try insert entry name into text input for a nicer experience.
89
+
// This failing shouldn't stop the user from entering a new name.
90
+
app.text_input.insertSliceAtCursor(entry.name) catch {};
91
+
app.state = .rename;
92
+
},
93
+
94
+
.create_dir => {
95
+
try app.repopulateDirectory("");
96
+
app.text_input.clearAndFree();
97
+
app.state = .new_dir;
98
+
},
99
+
.create_file => {
100
+
try app.repopulateDirectory("");
101
+
app.text_input.clearAndFree();
102
+
app.state = .new_file;
103
+
},
104
+
.fuzzy_find => {
105
+
app.text_input.clearAndFree();
106
+
app.state = .fuzzy;
107
+
},
108
+
.change_dir => {
109
+
app.text_input.clearAndFree();
110
+
app.state = .change_dir;
111
+
},
112
+
.enter_command_mode => {
113
+
app.text_input.clearAndFree();
114
+
app.text_input.insertSliceAtCursor(":") catch {};
115
+
app.state = .command;
116
+
},
117
+
.jump_bottom => app.directories.entries.selectLast(),
118
+
.jump_top => app.directories.entries.selectFirst(),
119
+
.toggle_verbose_file_information => app.drawer.verbose = !app.drawer.verbose,
120
+
.force_delete => try events.forceDelete(app),
121
+
.yank => try events.yank(app),
122
+
.paste => try events.paste(app),
123
+
}
124
+
} else {
125
+
switch (key.codepoint) {
126
+
'-', 'h', Key.left => try events.traverseLeft(app),
127
+
Key.enter, 'l', Key.right => try events.traverseRight(app),
128
+
'j', Key.down => app.directories.entries.next(),
129
+
'k', Key.up => app.directories.entries.previous(),
130
+
'u' => try events.undo(app),
131
+
else => {},
132
+
}
133
+
}
134
+
},
135
+
.image_ready => {},
136
+
.notification => {},
137
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
138
+
}
139
+
}
140
+
141
+
pub fn handleInputEvent(app: *App, event: App.Event) !void {
142
+
switch (event) {
143
+
.key_press => |key| {
144
+
switch (key.codepoint) {
145
+
Key.escape => {
146
+
switch (app.state) {
147
+
.fuzzy => {
148
+
try app.repopulateDirectory("");
149
+
app.text_input.clearAndFree();
150
+
},
151
+
.command => app.command_history.cursor = null,
152
+
else => {},
153
+
}
154
+
155
+
app.text_input.clearAndFree();
156
+
app.state = .normal;
157
+
},
158
+
Key.enter => {
159
+
const selected = app.directories.entries.selected;
160
+
switch (app.state) {
161
+
.new_dir => try events.createNewDir(app),
162
+
.new_file => try events.createNewFile(app),
163
+
.rename => try events.rename(app),
164
+
.change_dir => {
165
+
const path = app.inputToSlice();
166
+
try commands.cd(app, path);
167
+
app.text_input.clearAndFree();
168
+
},
169
+
.command => {
170
+
const command = app.inputToSlice();
171
+
172
+
// Push command to history if it's not empty.
173
+
if (!std.mem.eql(u8, std.mem.trim(u8, command, " "), ":")) {
174
+
app.command_history.add(command, app.alloc) catch |err| {
175
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to add command to history - {}.", .{err});
176
+
defer app.alloc.free(message);
177
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
178
+
};
179
+
}
180
+
181
+
supported: {
182
+
if (std.mem.eql(u8, command, ":q")) {
183
+
app.should_quit = true;
184
+
return;
185
+
}
186
+
187
+
if (std.mem.eql(u8, command, ":config")) {
188
+
try commands.config(app);
189
+
break :supported;
190
+
}
191
+
192
+
if (std.mem.eql(u8, command, ":trash")) {
193
+
try commands.trash(app);
194
+
break :supported;
195
+
}
196
+
197
+
if (std.mem.startsWith(u8, command, ":cd ")) {
198
+
try commands.cd(app, command[":cd ".len..]);
199
+
break :supported;
200
+
}
201
+
202
+
if (std.mem.eql(u8, command, ":empty_trash")) {
203
+
try commands.emptyTrash(app);
204
+
break :supported;
205
+
}
206
+
207
+
if (std.mem.eql(u8, command, ":h")) {
208
+
app.state = .help_menu;
209
+
break :supported;
210
+
}
211
+
212
+
app.text_input.clearAndFree();
213
+
try app.text_input.insertSliceAtCursor(":UnsupportedCommand");
214
+
}
215
+
216
+
app.command_history.cursor = null;
217
+
},
218
+
else => {},
219
+
}
220
+
221
+
if (app.state != .help_menu) app.state = .normal;
222
+
app.directories.entries.selected = selected;
223
+
},
224
+
Key.left => app.text_input.cursorLeft(),
225
+
Key.right => app.text_input.cursorRight(),
226
+
Key.up => {
227
+
if (app.state == .command) {
228
+
if (app.command_history.previous()) |command| {
229
+
app.text_input.clearAndFree();
230
+
app.text_input.insertSliceAtCursor(command) catch |err| {
231
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to get previous command history - {}.", .{err});
232
+
defer app.alloc.free(message);
233
+
app.notification.write(message, .err) catch {};
234
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
235
+
};
236
+
}
237
+
}
238
+
},
239
+
Key.down => {
240
+
if (app.state == .command) {
241
+
app.text_input.clearAndFree();
242
+
if (app.command_history.next()) |command| {
243
+
app.text_input.insertSliceAtCursor(command) catch |err| {
244
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err});
245
+
defer app.alloc.free(message);
246
+
app.notification.write(message, .err) catch {};
247
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
248
+
};
249
+
} else {
250
+
app.text_input.insertSliceAtCursor(":") catch |err| {
251
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to get next command history - {}.", .{err});
252
+
defer app.alloc.free(message);
253
+
app.notification.write(message, .err) catch {};
254
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
255
+
};
256
+
}
257
+
}
258
+
},
259
+
else => {
260
+
try app.text_input.update(.{ .key_press = key });
261
+
262
+
switch (app.state) {
263
+
.fuzzy => {
264
+
const fuzzy = app.inputToSlice();
265
+
try app.repopulateDirectory(fuzzy);
266
+
},
267
+
.command => {
268
+
const command = app.inputToSlice();
269
+
if (!std.mem.startsWith(u8, command, ":")) {
270
+
app.text_input.clearAndFree();
271
+
app.text_input.insertSliceAtCursor(":") catch |err| {
272
+
app.state = .normal;
273
+
274
+
const message = try std.fmt.allocPrint(app.alloc, "An input error occurred while attempting to enter a command - {}.", .{err});
275
+
defer app.alloc.free(message);
276
+
app.notification.write(message, .err) catch {};
277
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
278
+
};
279
+
}
280
+
},
281
+
else => {},
282
+
}
283
+
},
284
+
}
285
+
},
286
+
.image_ready => {},
287
+
.notification => {},
288
+
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.writer(), ws),
289
+
}
290
+
}
291
+
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),
305
+
}
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
+
}
+15
src/git.zig
+15
src/git.zig
···
1
+
const std = @import("std");
2
+
3
+
/// Callers owns memory returned.
4
+
pub fn getGitBranch(alloc: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 {
5
+
var file = try dir.openFile(".git/HEAD", .{});
6
+
defer file.close();
7
+
8
+
var buf: [1024]u8 = undefined;
9
+
const bytes = try file.readAll(&buf);
10
+
if (bytes == 0) return null;
11
+
12
+
const preamble = "ref: refs/heads/";
13
+
14
+
return try alloc.dupe(u8, buf[preamble.len..]);
15
+
}
+19
-28
src/list.zig
+19
-28
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;
34
31
}
35
32
36
-
pub fn get(self: *Self, index: usize) !T {
33
+
pub fn fromArray(self: *Self, array: []const T) !void {
34
+
for (array) |item| {
35
+
try self.append(item);
36
+
}
37
+
}
38
+
39
+
pub fn get(self: Self, index: usize) !T {
37
40
if (index + 1 > self.len()) {
38
41
return error.OutOfBounds;
39
42
}
···
41
44
return self.all()[index];
42
45
}
43
46
44
-
pub fn get_selected(self: *Self) !T {
47
+
pub fn getSelected(self: *Self) !?T {
45
48
if (self.len() > 0) {
46
49
if (self.selected >= self.len()) {
47
50
self.selected = self.len() - 1;
···
50
53
return try self.get(self.selected);
51
54
}
52
55
53
-
return error.EmptyList;
56
+
return null;
54
57
}
55
58
56
-
pub fn all(self: *Self) []T {
59
+
pub fn all(self: Self) []T {
57
60
return self.items.items;
58
61
}
59
62
60
-
pub fn len(self: *Self) usize {
63
+
pub fn len(self: Self) usize {
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 select_last(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
-
pub fn select_first(self: *Self) void {
83
+
pub fn selectFirst(self: *Self) void {
92
84
self.selected = 0;
93
-
self.offset = 0;
94
85
}
95
86
};
96
87
}
-31
src/log.zig
-31
src/log.zig
···
1
-
const std = @import("std");
2
-
3
-
pub const Logger = struct {
4
-
const Self = @This();
5
-
const BufferedFileWriter = std.io.BufferedWriter(4096, std.fs.File.Writer);
6
-
7
-
stdout: BufferedFileWriter = undefined,
8
-
stderr: BufferedFileWriter = undefined,
9
-
10
-
pub fn init(self: *Self) void {
11
-
self.stdout = BufferedFileWriter{ .unbuffered_writer = std.io.getStdOut().writer() };
12
-
self.stderr = BufferedFileWriter{ .unbuffered_writer = std.io.getStdErr().writer() };
13
-
}
14
-
15
-
pub fn info(self: *Self, comptime format: []const u8, args: anytype) void {
16
-
self.stdout.writer().print(format ++ "\n", args) catch return;
17
-
self.stdout.flush() catch return;
18
-
}
19
-
20
-
pub fn debug(self: *Self, comptime format: []const u8, args: anytype) void {
21
-
self.stdout.writer().print("[DEBUG] " ++ format ++ "\n", args) catch return;
22
-
self.stdout.flush() catch return;
23
-
}
24
-
25
-
pub fn err(self: *Self, comptime format: []const u8, args: anytype) void {
26
-
self.stderr.writer().print(format ++ "\n", args) catch return;
27
-
self.stderr.flush() catch return;
28
-
}
29
-
};
30
-
31
-
pub var log: Logger = Logger{};
+152
-27
src/main.zig
+152
-27
src/main.zig
···
1
1
const std = @import("std");
2
2
const builtin = @import("builtin");
3
+
const options = @import("options");
4
+
const App = @import("app.zig");
5
+
const FileLogger = @import("file_logger.zig");
6
+
const vaxis = @import("vaxis");
7
+
const config = &@import("./config.zig").config;
8
+
const resolvePath = @import("./commands.zig").resolvePath;
3
9
4
-
const App = @import("app.zig");
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
+
;
25
+
26
+
pub const std_options: std.Options = .{
27
+
.log_scope_levels = &.{
28
+
.{ .scope = .vaxis, .level = .warn },
29
+
.{ .scope = .vaxis_parser, .level = .warn },
30
+
},
31
+
};
5
32
6
-
const config = &@import("./config.zig").config;
7
-
const vaxis = @import("vaxis");
33
+
const Options = struct {
34
+
help: bool = false,
35
+
version: bool = false,
36
+
@"choose-dir": bool = false,
37
+
@"entry-path": []const u8 = ".",
8
38
9
-
var app: App = undefined;
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
+
};
10
45
11
46
pub fn main() !void {
12
47
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
13
-
defer _ = gpa.deinit();
48
+
defer {
49
+
const deinit_status = gpa.deinit();
50
+
if (deinit_status == .leak) {
51
+
std.log.err("memory leak", .{});
52
+
}
53
+
}
14
54
const alloc = gpa.allocator();
15
55
16
-
config.parse(alloc) catch |err| switch (err) {
17
-
error.ConfigNotFound => {},
18
-
error.MissingConfigHomeEnvironmentVariable => {
19
-
std.log.err("Could not read config due to $HOME or $XDG_CONFIG_HOME not being set.", .{});
20
-
return;
21
-
},
22
-
error.SyntaxError => {
23
-
std.log.err("Could not read config due to a syntax error.", .{});
24
-
return;
25
-
},
26
-
else => {
27
-
std.log.err("Could not read config due to an unknown error.", .{});
28
-
return;
29
-
},
30
-
};
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
+
}
100
+
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) {
119
+
error.SyntaxError => {
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
131
+
},
132
+
else => {
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
+
};
139
+
},
140
+
};
31
141
32
-
app = try App.init(alloc);
33
-
defer app.deinit();
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;
34
147
35
-
try app.run();
36
-
}
148
+
try app.run();
149
+
150
+
if (opts.@"choose-dir") {
151
+
last_dir = alloc.dupe(u8, try app.directories.fullPath(".")) catch null;
152
+
}
153
+
}
37
154
38
-
pub fn panic(msg: []const u8, trace: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
39
-
app.vx.deinit(app.alloc, app.tty.anyWriter());
40
-
std.builtin.default_panic(msg, trace, ret_addr);
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 {};
163
+
164
+
alloc.free(path);
165
+
}
41
166
}
+32
-35
src/notification.zig
+32
-35
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
9
+
/// Seconds.
10
+
pub const notification_timeout = 3;
11
+
5
12
const Style = enum {
6
13
err,
7
14
info,
15
+
warn,
8
16
};
9
17
10
-
const Error = enum {
11
-
PermissionDenied,
12
-
UnknownError,
13
-
UnableToUndo,
14
-
UnableToOpenFile,
15
-
UnableToDeleteItem,
16
-
EditorNotSet,
17
-
ItemAlreadyExists,
18
-
UnableToRename,
19
-
IncorrectPath,
20
-
};
18
+
var buf: [1024]u8 = undefined;
21
19
22
-
len: usize = 0,
23
-
buf: [1024]u8 = undefined,
24
20
style: Style = Style.info,
25
-
fbs: std.io.FixedBufferStream([]u8) = undefined,
26
-
27
-
pub fn init(self: *Self) void {
28
-
self.fbs = std.io.fixedBufferStream(&self.buf);
29
-
}
21
+
fbs: std.io.FixedBufferStream([]u8) = std.io.fixedBufferStream(&buf),
22
+
/// How long until the notification disappears in seconds.
23
+
timer: i64 = 0,
24
+
loop: ?*vaxis.Loop(Event) = null,
30
25
31
26
pub fn write(self: *Self, text: []const u8, style: Style) !void {
32
27
self.fbs.reset();
33
-
self.len = try self.fbs.write(text);
34
-
28
+
_ = try self.fbs.write(text);
29
+
self.timer = std.time.timestamp();
35
30
self.style = style;
36
-
}
37
31
38
-
pub fn write_err(self: *Self, err: Error) !void {
39
-
try switch (err) {
40
-
.PermissionDenied => self.write("Permission denied.", .err),
41
-
.UnknownError => self.write("An unknown error occurred.", .err),
42
-
.UnableToOpenFile => self.write("Unable to open file.", .err),
43
-
.UnableToDeleteItem => self.write("Unable to delete item.", .err),
44
-
.UnableToUndo => self.write("Unable to undo previous action.", .err),
45
-
.ItemAlreadyExists => self.write("Item already exists.", .err),
46
-
.UnableToRename => self.write("Unable to rename item.", .err),
47
-
.IncorrectPath => self.write("Unable to find path.", .err),
48
-
.EditorNotSet => self.write("$EDITOR is not set.", .err),
49
-
};
32
+
if (self.loop) |loop| {
33
+
loop.postEvent(.notification);
34
+
}
50
35
}
51
36
52
37
pub fn reset(self: *Self) void {
53
38
self.fbs.reset();
54
-
self.len = 0;
55
39
self.style = Style.info;
56
40
}
57
41
58
42
pub fn slice(self: *Self) []const u8 {
59
-
return self.buf[0..self.len];
43
+
return self.fbs.getWritten();
44
+
}
45
+
46
+
pub fn clearIfEnded(self: *Self) bool {
47
+
if (std.time.timestamp() - self.timer > notification_timeout) {
48
+
self.reset();
49
+
return true;
50
+
}
51
+
52
+
return false;
53
+
}
54
+
55
+
pub fn len(self: Self) usize {
56
+
return self.fbs.pos;
60
57
}