地圖 (Jido) is a lightweight Unix TUI file explorer designed for speed and simplicity.

Compare changes

Choose any two refs to compare.

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

This is a binary file and will not be displayed.

+30 -11
build.zig
··· 1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 4 const targets: []const std.Target.Query = &.{ 5 .{ .cpu_arch = .aarch64, .os_tag = .macos }, 6 .{ .cpu_arch = .aarch64, .os_tag = .linux }, ··· 8 .{ .cpu_arch = .x86_64, .os_tag = .macos }, 9 }; 10 11 - fn createExe(b: *std.Build, exe_name: []const u8, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) !*std.Build.Step.Compile { 12 - const libvaxis = b.dependency("vaxis", .{ .target = target }).module("vaxis"); 13 - const fuzzig = b.dependency("fuzzig", .{ .target = target }).module("fuzzig"); 14 - const zuid = b.dependency("zuid", .{ .target = target }).module("zuid"); 15 16 const exe = b.addExecutable(.{ 17 .name = exe_name, 18 - .root_source_file = b.path("src/main.zig"), 19 - .target = target, 20 - .optimize = optimize, 21 }); 22 23 exe.root_module.addImport("vaxis", libvaxis); 24 exe.root_module.addImport("fuzzig", fuzzig); 25 exe.root_module.addImport("zuid", zuid); 26 27 return exe; ··· 31 const target = b.standardTargetOptions(.{}); 32 const optimize = b.standardOptimizeOption(.{}); 33 34 // Building targets for release. 35 const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false; 36 if (build_all) { 37 - try buildTargets(b); 38 return; 39 } 40 41 - const exe = try createExe(b, "jido", target, optimize); 42 b.installArtifact(exe); 43 44 const run_cmd = b.addRunArtifact(exe); ··· 50 run_step.dependOn(&run_cmd.step); 51 } 52 53 - fn buildTargets(b: *std.Build) !void { 54 for (targets) |t| { 55 const target = b.resolveTargetQuery(t); 56 57 - const exe = try createExe(b, "jido", target, .ReleaseSafe); 58 b.installArtifact(exe); 59 60 const target_output = b.addInstallArtifact(exe, .{
··· 1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 4 + ///Must match the `version` in `build.zig.zon`. 5 + const version = std.SemanticVersion{ .major = 1, .minor = 4, .patch = 0 }; 6 + 7 const targets: []const std.Target.Query = &.{ 8 .{ .cpu_arch = .aarch64, .os_tag = .macos }, 9 .{ .cpu_arch = .aarch64, .os_tag = .linux }, ··· 11 .{ .cpu_arch = .x86_64, .os_tag = .macos }, 12 }; 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; ··· 45 const target = b.standardTargetOptions(.{}); 46 const optimize = b.standardOptimizeOption(.{}); 47 48 + const build_options = b.addOptions(); 49 + build_options.step.name = "build options"; 50 + build_options.addOption(std.SemanticVersion, "version", version); 51 + const build_options_module = build_options.createModule(); 52 + 53 // Building targets for release. 54 const build_all = b.option(bool, "all-targets", "Build all targets in ReleaseSafe mode.") orelse false; 55 if (build_all) { 56 + try buildTargets(b, build_options_module); 57 return; 58 } 59 60 + const exe = try createExe(b, "jido", target, optimize, build_options_module); 61 b.installArtifact(exe); 62 63 const run_cmd = b.addRunArtifact(exe); ··· 69 run_step.dependOn(&run_cmd.step); 70 } 71 72 + fn buildTargets(b: *std.Build, build_options: *std.Build.Module) !void { 73 for (targets) |t| { 74 const target = b.resolveTargetQuery(t); 75 76 + const exe = try createExe(b, "jido", target, .ReleaseSafe, build_options); 77 b.installArtifact(exe); 78 79 const target_output = b.addInstallArtifact(exe, .{
+25 -10
build.zig.zon
··· 1 .{ 2 - .name = "jido", 3 - .version = "0.8.0", 4 - .minimum_zig_version = "0.13.0", 5 6 .dependencies = .{ 7 .vaxis = .{ 8 - .url = "git+https://github.com/rockorager/libvaxis#77f5795892b08cd64ad6a103f0c53a7d1db50b18", 9 - .hash = "1220d587525255e734670ae74f38cb09d75df936c7889b07a6eab739c066dc736f85", 10 }, 11 .fuzzig = .{ 12 - .url = "git+https://github.com/fjebaker/fuzzig#0fd156d5097365151e85a85eef9d8cf0eebe7b00", 13 - .hash = "122019f077d09686b1ec47928ca2b4bf264422f3a27afc5b49dafb0129a4ceca0d01", 14 }, 15 .zuid = .{ 16 - .url = "git+https://github.com/KeithBrown39423/zuid#49e5980ba83f7d9ae967fa7ce4d54384c1c0f82b", 17 - .hash = "1220e05a3f459c0adbf2b09b4764838833e3e716a712852aec6ef1636f4d8e9f646e", 18 }, 19 }, 20 21 .paths = .{ 22 - "./src/", 23 }, 24 }
··· 1 .{ 2 + .name = .jido, 3 + .fingerprint = 0xee45eabe36cafb57, 4 + .version = "1.3.0", 5 + .minimum_zig_version = "0.15.2", 6 7 .dependencies = .{ 8 + // Replace with rockorager/libvaxis once https://github.com/rockorager/libvaxis/pull/293 is merged 9 .vaxis = .{ 10 + .url = "git+https://github.com/rob9315/libvaxis.git#8d04cffd9137b4a8c56b356de98b32023ae752f3", 11 + .hash = "vaxis-0.5.1-BWNV_OA-CQDeFBHIx9ryyASogr2GE3FsAm-l5Ii5-HZT", 12 }, 13 .fuzzig = .{ 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", 20 }, 21 + // Replace with KeithBrown39423/zuid once https://github.com/KeithBrown39423/zuid/pull/4 is merged 22 .zuid = .{ 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", 30 }, 31 }, 32 33 .paths = .{ 34 + "LICENSE", 35 + "build.zig", 36 + "build.zig.zon", 37 + "src", 38 }, 39 }
+4
example-config.json
··· 3 "sort_dirs": false, 4 "show_images": true, 5 "preview_file": true, 6 "styles": { 7 "selected_list_item": { 8 "bg": {
··· 3 "sort_dirs": false, 4 "show_images": true, 5 "preview_file": true, 6 + "keybinds": { 7 + "toggle_hidden_files": "h", 8 + "force_delete": null 9 + }, 10 "styles": { 11 "selected_list_item": { 12 "bg": {
+186 -45
src/app.zig
··· 6 const config = &@import("./config.zig").config; 7 const List = @import("./list.zig").List; 8 const Directories = @import("./directories.zig"); 9 const CircStack = @import("./circ_stack.zig").CircularStack; 10 const zuid = @import("zuid"); 11 const vaxis = @import("vaxis"); 12 const Key = vaxis.Key; 13 const EventHandlers = @import("./event_handlers.zig"); 14 15 pub const State = enum { 16 normal, ··· 20 change_dir, 21 rename, 22 command, 23 - }; 24 - 25 - const ActionPaths = struct { 26 - /// Allocated. 27 - old: []const u8, 28 - /// Allocated. 29 - new: []const u8, 30 }; 31 32 pub const Action = union(enum) { 33 - delete: ActionPaths, 34 - rename: ActionPaths, 35 }; 36 37 pub const Event = union(enum) { 38 key_press: Key, 39 winsize: vaxis.Winsize, 40 }; 41 42 const actions_len = 100; 43 44 const App = @This(); 45 46 alloc: std.mem.Allocator, 47 should_quit: bool, 48 vx: vaxis.Vaxis = undefined, 49 tty: vaxis.Tty = undefined, 50 state: State = .normal, 51 actions: CircStack(Action, actions_len), 52 53 directories: Directories, 54 - notification: Notification, 55 56 text_input: vaxis.widgets.TextInput, 57 text_input_buf: [std.fs.max_path_bytes]u8 = undefined, 58 59 - image: ?vaxis.Image = null, 60 last_known_height: usize, 61 62 - pub fn init(alloc: std.mem.Allocator) !App { 63 var vx = try vaxis.init(alloc, .{ 64 .kitty_keyboard_flags = .{ 65 .report_text = false, ··· 70 }, 71 }); 72 73 - var notification = Notification{}; 74 - notification.init(); 75 76 - return App{ 77 .alloc = alloc, 78 .should_quit = false, 79 .vx = vx, 80 - .tty = try vaxis.Tty.init(), 81 - .directories = try Directories.init(alloc), 82 - .text_input = vaxis.widgets.TextInput.init(alloc, &vx.unicode), 83 - .notification = notification, 84 .actions = CircStack(Action, actions_len).init(), 85 .last_known_height = vx.window().height, 86 }; 87 } 88 89 pub fn deinit(self: *App) void { 90 - for (self.actions.buf[0..self.actions.count]) |action| { 91 switch (action) { 92 - .delete, .rename => |a| { 93 - self.alloc.free(a.new); 94 - self.alloc.free(a.old); 95 }, 96 } 97 } 98 99 self.directories.deinit(); 100 self.text_input.deinit(); 101 - self.vx.deinit(self.alloc, self.tty.anyWriter()); 102 self.tty.deinit(); 103 } 104 105 - pub fn run(self: *App) !void { 106 - var drawer = Drawer{}; 107 - try self.directories.populateEntries(""); 108 109 - var loop: vaxis.Loop(Event) = .{ 110 - .vaxis = &self.vx, 111 - .tty = &self.tty, 112 }; 113 - try loop.start(); 114 - defer loop.stop(); 115 116 - try self.vx.enterAltScreen(self.tty.anyWriter()); 117 - try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s); 118 119 while (!self.should_quit) { 120 - loop.pollEvent(); 121 - while (loop.tryEvent()) |event| { 122 switch (self.state) { 123 .normal => { 124 - try EventHandlers.handleNormalEvent(self, event, &loop); 125 }, 126 else => { 127 try EventHandlers.handleInputEvent(self, event); ··· 129 } 130 } 131 132 - try drawer.draw(self); 133 134 - var buffered = self.tty.bufferedWriter(); 135 - try self.vx.render(buffered.writer().any()); 136 - try buffered.flush(); 137 } 138 139 if (config.empty_trash_on_exit) { 140 - if (try config.trashDir()) |dir| { 141 - var trash_dir = dir; 142 - defer trash_dir.close(); 143 - _ = try environment.deleteContents(trash_dir); 144 } 145 } 146 }
··· 6 const config = &@import("./config.zig").config; 7 const List = @import("./list.zig").List; 8 const Directories = @import("./directories.zig"); 9 + const FileLogger = @import("./file_logger.zig"); 10 const CircStack = @import("./circ_stack.zig").CircularStack; 11 const zuid = @import("zuid"); 12 const vaxis = @import("vaxis"); 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 + }; 57 58 pub const State = enum { 59 normal, ··· 63 change_dir, 64 rename, 65 command, 66 + help_menu, 67 }; 68 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, 73 }; 74 75 pub const Event = union(enum) { 76 + image_ready, 77 + notification, 78 key_press: Key, 79 winsize: vaxis.Winsize, 80 }; 81 82 + pub const Image = struct { 83 + const Status = enum { 84 + ready, 85 + processing, 86 + failed, 87 + }; 88 + 89 + ///Only use on first transmission. Subsequent draws should use 90 + ///`Image.image`. 91 + data: ?vaxis.zigimg.Image = null, 92 + image: ?vaxis.Image = null, 93 + path: ?[]const u8 = null, 94 + status: Status = .processing, 95 + 96 + pub fn deinit(self: @This(), alloc: std.mem.Allocator, vx: vaxis.Vaxis, tty: *vaxis.Tty) void { 97 + if (self.image) |image| { 98 + vx.freeImage(tty.writer(), image.id); 99 + } 100 + if (self.data) |data| { 101 + var d = data; 102 + d.deinit(alloc); 103 + } 104 + if (self.path) |path| alloc.free(path); 105 + } 106 + }; 107 + 108 const actions_len = 100; 109 + const image_cache_cap = 100; 110 111 const App = @This(); 112 113 alloc: std.mem.Allocator, 114 should_quit: bool, 115 vx: vaxis.Vaxis = undefined, 116 + tty_buffer: [1024]u8 = undefined, 117 tty: vaxis.Tty = undefined, 118 + loop: vaxis.Loop(Event) = undefined, 119 state: State = .normal, 120 actions: CircStack(Action, actions_len), 121 + command_history: CommandHistory = CommandHistory{}, 122 + drawer: Drawer = Drawer{}, 123 124 + help_menu: List([]const u8), 125 directories: Directories, 126 + notification: Notification = Notification{}, 127 + file_logger: ?FileLogger = null, 128 129 text_input: vaxis.widgets.TextInput, 130 text_input_buf: [std.fs.max_path_bytes]u8 = undefined, 131 132 + yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null, 133 last_known_height: usize, 134 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 { 141 var vx = try vaxis.init(alloc, .{ 142 .kitty_keyboard_flags = .{ 143 .report_text = false, ··· 148 }, 149 }); 150 151 + var help_menu = List([]const u8).init(alloc); 152 + try help_menu.fromArray(&help_menu_items); 153 154 + var app: App = .{ 155 .alloc = alloc, 156 .should_quit = false, 157 .vx = vx, 158 + .directories = try Directories.init(alloc, entry_dir), 159 + .help_menu = help_menu, 160 + .text_input = vaxis.widgets.TextInput.init(alloc), 161 .actions = CircStack(Action, actions_len).init(), 162 .last_known_height = vx.window().height, 163 + .images = .{ .cache = .init(alloc) }, 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; 172 } 173 174 pub fn deinit(self: *App) void { 175 + while (self.actions.pop()) |action| { 176 switch (action) { 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); 184 }, 185 + .paste => |a| self.alloc.free(a), 186 } 187 } 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(); 197 self.directories.deinit(); 198 self.text_input.deinit(); 199 + self.vx.deinit(self.alloc, self.tty.writer()); 200 self.tty.deinit(); 201 + if (self.file_logger) |file_logger| file_logger.deinit(); 202 + 203 + var image_iter = self.images.cache.iterator(); 204 + while (image_iter.next()) |img| { 205 + img.value_ptr.deinit(self.alloc, self.vx, &self.tty); 206 + } 207 + self.images.cache.deinit(); 208 } 209 210 + pub fn inputToSlice(self: *App) []const u8 { 211 + self.text_input.buf.cursor = self.text_input.buf.realLength(); 212 + return self.text_input.sliceToCursor(&self.text_input_buf); 213 + } 214 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 }; 223 + } 224 + 225 + pub fn run(self: *App) !void { 226 + try self.repopulateDirectory(""); 227 + try self.loop.start(); 228 + defer self.loop.stop(); 229 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; 233 234 while (!self.should_quit) { 235 + self.loop.pollEvent(); 236 + while (self.loop.tryEvent()) |event| { 237 + // Global keybinds. 238 + try EventHandlers.handleGlobalEvent(self, event); 239 + 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); 247 }, 248 else => { 249 try EventHandlers.handleInputEvent(self, event); ··· 251 } 252 } 253 254 + try self.drawer.draw(self); 255 256 + try self.vx.render(self.tty.writer()); 257 } 258 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; 263 + } 264 + if (self.file_logger) |file_logger| file_logger.write("Failed to open trash directory.", .err) catch { 265 + std.log.err("Failed to open trash directory.", .{}); 266 + }; 267 + return; 268 + }; 269 + defer trash_dir.close(); 270 + 271 + const failed = environment.deleteContents(trash_dir) catch |err| { 272 + const message = try std.fmt.allocPrint(self.alloc, "Failed to empty trash - {}.", .{err}); 273 + defer self.alloc.free(message); 274 + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 275 + std.log.err("Failed to empty trash - {}.", .{err}); 276 + }; 277 + return; 278 + }; 279 + if (failed > 0) { 280 + const message = try std.fmt.allocPrint(self.alloc, "Failed to empty {d} items from the trash.", .{failed}); 281 + defer self.alloc.free(message); 282 + if (self.file_logger) |file_logger| file_logger.write(message, .err) catch { 283 + std.log.err("Failed to empty {d} items from the trash.", .{failed}); 284 + }; 285 } 286 } 287 }
+142 -35
src/commands.zig
··· 1 const App = @import("app.zig"); 2 const environment = @import("environment.zig"); 3 - const _config = &@import("./config.zig").config; 4 5 ///Navigate the user to the config dir. 6 - pub fn config(app: *App) !void { 7 const dir = dir: { 8 notfound: { 9 - break :dir (_config.configDir() catch break :notfound) orelse break :notfound; 10 } 11 - try app.notification.writeErr(.ConfigPathNotFound); 12 return; 13 }; 14 - app.directories.clearEntries(); 15 app.directories.dir.close(); 16 app.directories.dir = dir; 17 - app.directories.populateEntries("") catch |err| { 18 - switch (err) { 19 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 20 - else => try app.notification.writeErr(.UnknownError), 21 - } 22 - }; 23 } 24 25 ///Navigate the user to the trash dir. 26 - pub fn trash(app: *App) !void { 27 const dir = dir: { 28 notfound: { 29 - break :dir (_config.trashDir() catch break :notfound) orelse break :notfound; 30 } 31 - try app.notification.writeErr(.ConfigPathNotFound); 32 return; 33 }; 34 - app.directories.clearEntries(); 35 app.directories.dir.close(); 36 app.directories.dir = dir; 37 - app.directories.populateEntries("") catch |err| { 38 - switch (err) { 39 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 40 - else => try app.notification.writeErr(.UnknownError), 41 - } 42 - }; 43 } 44 45 ///Empty the trash. 46 - pub fn emptyTrash(app: *App) !void { 47 - const dir = dir: { 48 notfound: { 49 - break :dir (_config.trashDir() catch break :notfound) orelse break :notfound; 50 } 51 - try app.notification.writeErr(.ConfigPathNotFound); 52 return; 53 }; 54 55 - var trash_dir = dir; 56 - defer trash_dir.close(); 57 - const failed = try environment.deleteContents(trash_dir); 58 - if (failed > 0) try app.notification.writeErr(.FailedToDeleteSomeItems); 59 60 - app.directories.clearEntries(); 61 - app.directories.populateEntries("") catch |err| { 62 - switch (err) { 63 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 64 - else => try app.notification.writeErr(.UnknownError), 65 - } 66 }; 67 }
··· 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 }
+134 -47
src/config.zig
··· 2 const builtin = @import("builtin"); 3 const environment = @import("./environment.zig"); 4 const vaxis = @import("vaxis"); 5 const Notification = @import("./notification.zig"); 6 7 - pub const ParseRes = struct { deprecated: bool }; 8 9 const Config = struct { 10 show_hidden: bool = true, ··· 12 show_images: bool = true, 13 preview_file: bool = true, 14 empty_trash_on_exit: bool = false, 15 - styles: Styles, 16 17 - config_path_buf: [std.fs.max_path_bytes]u8 = undefined, 18 - config_path: ?[]u8 = null, 19 20 ///Returned dir needs to be closed by user. 21 pub fn configDir(self: Config) !?std.fs.Dir { 22 - if (self.config_path) |path| { 23 - return try std.fs.openDirAbsolute(std.mem.trimRight(u8, path, "config.json"), .{ .iterate = true }); 24 } else return null; 25 } 26 ··· 28 pub fn trashDir(self: Config) !?std.fs.Dir { 29 var parent = try self.configDir() orelse return null; 30 defer parent.close(); 31 - if (!environment.dirExists(parent, "trash")) { 32 - try parent.makeDir("trash"); 33 } 34 35 - return try parent.openDir("trash", .{ .iterate = true }); 36 } 37 38 - pub fn parse(self: *Config, alloc: std.mem.Allocator) !ParseRes { 39 - var deprecated = false; 40 - var config_location: struct { 41 - home_dir: std.fs.Dir, 42 - path: []const u8, 43 - } = lbl: { 44 if (try environment.getXdgConfigHomeDir()) |home_dir| { 45 - const path = "jido" ++ std.fs.path.sep_str ++ "config.json"; 46 - if (environment.fileExists(home_dir, path)) { 47 - break :lbl .{ 48 - .home_dir = home_dir, 49 - .path = path, 50 - }; 51 } 52 53 - var dir = home_dir; 54 - dir.close(); 55 } 56 57 if (try environment.getHomeDir()) |home_dir| { 58 - const path = ".jido" ++ std.fs.path.sep_str ++ "config.json"; 59 - if (environment.fileExists(home_dir, path)) { 60 - break :lbl .{ 61 - .home_dir = home_dir, 62 - .path = path, 63 - }; 64 } 65 66 - const deprecated_path = ".config" ++ std.fs.path.sep_str ++ "jido" ++ std.fs.path.sep_str ++ "config.json"; 67 - if (environment.fileExists(home_dir, deprecated_path)) { 68 - deprecated = true; 69 - break :lbl .{ 70 - .home_dir = home_dir, 71 - .path = deprecated_path, 72 - }; 73 } 74 75 - var dir = home_dir; 76 - dir.close(); 77 } 78 79 - return .{ .deprecated = deprecated }; 80 }; 81 - defer config_location.home_dir.close(); 82 83 - const config_file = try config_location.home_dir.openFile(config_location.path, .{}); 84 defer config_file.close(); 85 86 const config_str = try config_file.readToEndAlloc(alloc, 1024 * 1024 * 1024); ··· 90 defer parsed_config.deinit(); 91 92 self.* = parsed_config.value; 93 - self.config_path = config_location.home_dir.realpath( 94 - config_location.path, 95 - &self.config_path_buf, 96 - ) catch null; 97 - return .{ .deprecated = deprecated }; 98 } 99 }; 100 ··· 110 111 const NotificationStyles = struct { 112 box: vaxis.Style = vaxis.Style{ 113 .bg = .{ .rgb = Colours.grey }, 114 }, 115 err: vaxis.Style = vaxis.Style{ ··· 126 }, 127 }; 128 129 const Styles = struct { 130 selected_list_item: vaxis.Style = vaxis.Style{ 131 .bg = .{ .rgb = Colours.grey }, ··· 145 }, 146 }; 147 148 - pub var config: Config = Config{ .styles = Styles{} };
··· 2 const builtin = @import("builtin"); 3 const environment = @import("./environment.zig"); 4 const vaxis = @import("vaxis"); 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"; 13 14 const Config = struct { 15 show_hidden: bool = true, ··· 17 show_images: bool = true, 18 preview_file: bool = true, 19 empty_trash_on_exit: bool = false, 20 + true_dir_size: bool = false, 21 + entry_dir: ?[]const u8 = null, 22 + styles: Styles = .{}, 23 + keybinds: Keybinds = .{}, 24 25 + config_dir: ?std.fs.Dir = null, 26 27 ///Returned dir needs to be closed by user. 28 pub fn configDir(self: Config) !?std.fs.Dir { 29 + if (self.config_dir) |dir| { 30 + return try dir.openDir(".", .{ .iterate = true }); 31 } else return null; 32 } 33 ··· 35 pub fn trashDir(self: Config) !?std.fs.Dir { 36 var parent = try self.configDir() orelse return null; 37 defer parent.close(); 38 + if (!environment.dirExists(parent, TRASH_DIR_NAME)) { 39 + try parent.makeDir(TRASH_DIR_NAME); 40 } 41 42 + return try parent.openDir(TRASH_DIR_NAME, .{ .iterate = true }); 43 } 44 45 + pub fn parse(self: *Config, alloc: std.mem.Allocator, app: *App) !void { 46 + var dir = lbl: { 47 if (try environment.getXdgConfigHomeDir()) |home_dir| { 48 + defer { 49 + var dir = home_dir; 50 + dir.close(); 51 + } 52 + 53 + if (!environment.dirExists(home_dir, XDG_CONFIG_HOME_DIR_NAME)) { 54 + try home_dir.makeDir(XDG_CONFIG_HOME_DIR_NAME); 55 } 56 57 + const jido_dir = try home_dir.openDir( 58 + XDG_CONFIG_HOME_DIR_NAME, 59 + .{ .iterate = true }, 60 + ); 61 + self.config_dir = jido_dir; 62 + 63 + if (environment.fileExists(jido_dir, CONFIG_NAME)) { 64 + break :lbl jido_dir; 65 + } 66 + return; 67 } 68 69 if (try environment.getHomeDir()) |home_dir| { 70 + defer { 71 + var dir = home_dir; 72 + dir.close(); 73 } 74 75 + if (!environment.dirExists(home_dir, HOME_DIR_NAME)) { 76 + try home_dir.makeDir(HOME_DIR_NAME); 77 } 78 79 + const jido_dir = try home_dir.openDir( 80 + HOME_DIR_NAME, 81 + .{ .iterate = true }, 82 + ); 83 + self.config_dir = jido_dir; 84 + 85 + if (environment.fileExists(jido_dir, CONFIG_NAME)) { 86 + break :lbl jido_dir; 87 + } 88 + return; 89 } 90 91 + return; 92 }; 93 94 + const config_file = try dir.openFile(CONFIG_NAME, .{}); 95 defer config_file.close(); 96 97 const config_str = try config_file.readToEndAlloc(alloc, 1024 * 1024 * 1024); ··· 101 defer parsed_config.deinit(); 102 103 self.* = parsed_config.value; 104 + self.config_dir = dir; 105 + 106 + // Check duplicate keybinds 107 + { 108 + var file_logger = FileLogger.init(dir); 109 + defer file_logger.deinit(); 110 + 111 + var key_map = std.AutoHashMap(u21, []const u8).init(alloc); 112 + defer { 113 + var it = key_map.iterator(); 114 + while (it.next()) |entry| { 115 + alloc.free(entry.value_ptr.*); 116 + } 117 + key_map.deinit(); 118 + } 119 + 120 + inline for (std.meta.fields(Keybinds)) |field| { 121 + if (@field(self.keybinds, field.name)) |field_value| { 122 + const codepoint = @intFromEnum(field_value); 123 + 124 + const res = try key_map.getOrPut(codepoint); 125 + if (res.found_existing) { 126 + var keybind_str: [1024]u8 = undefined; 127 + const keybind_str_bytes = try std.unicode.utf8Encode(codepoint, &keybind_str); 128 + 129 + const message = try std.fmt.allocPrint( 130 + alloc, 131 + "'{s}' and '{s}' have the same keybind: '{s}'. This can cause undefined behaviour.", 132 + .{ res.value_ptr.*, field.name, keybind_str[0..keybind_str_bytes] }, 133 + ); 134 + defer alloc.free(message); 135 + 136 + app.notification.write(message, .err) catch {}; 137 + file_logger.write(message, .err) catch {}; 138 + 139 + return error.DuplicateKeybind; 140 + } 141 + res.value_ptr.* = try alloc.dupe(u8, field.name); 142 + } 143 + } 144 + } 145 + 146 + return; 147 } 148 }; 149 ··· 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{ ··· 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 + 216 const Styles = struct { 217 selected_list_item: vaxis.Style = vaxis.Style{ 218 .bg = .{ .rgb = Colours.grey }, ··· 232 }, 233 }; 234 235 + pub var config: Config = Config{};
+45 -58
src/directories.zig
··· 5 const vaxis = @import("vaxis"); 6 const fuzzig = @import("fuzzig"); 7 8 - const History = struct { 9 - selected: usize, 10 - offset: usize, 11 - }; 12 - 13 const history_len: usize = 100; 14 15 const Self = @This(); ··· 20 file_contents: [4096]u8 = undefined, 21 pdf_contents: ?[]u8 = null, 22 entries: List(std.fs.Dir.Entry), 23 - history: CircStack(History, history_len), 24 child_entries: List([]const u8), 25 searcher: fuzzig.Ascii, 26 27 - pub fn init(alloc: std.mem.Allocator) !Self { 28 return Self{ 29 .alloc = alloc, 30 - .dir = try std.fs.cwd().openDir(".", .{ .iterate = true }), 31 .entries = List(std.fs.Dir.Entry).init(alloc), 32 - .history = CircStack(History, history_len).init(), 33 .child_entries = List([]const u8).init(alloc), 34 .searcher = try fuzzig.Ascii.init( 35 alloc, ··· 71 return try self.dir.realpath(relative_path, &self.path_buf); 72 } 73 74 pub fn populateChildEntries( 75 self: *Self, 76 relative_path: []const u8, ··· 80 81 var it = dir.iterate(); 82 while (try it.next()) |entry| { 83 try self.child_entries.append(try self.alloc.dupe(u8, entry.name)); 84 } 85 ··· 88 } 89 } 90 91 - pub fn writeChildEntries( 92 - self: *Self, 93 - window: vaxis.Window, 94 - style: vaxis.Style, 95 - ) !void { 96 - for (self.child_entries.all(), 0..) |item, i| { 97 - if (std.mem.startsWith(u8, item, ".") and config.show_hidden == false) { 98 - continue; 99 - } 100 - 101 - if (i > window.height) continue; 102 - 103 - const w = window.child(.{ .y_off = @intCast(i), .height = 1 }); 104 - w.fill(vaxis.Cell{ .style = style }); 105 - 106 - _ = w.print(&.{.{ .text = item, .style = style }}, .{}); 107 - } 108 - } 109 - 110 pub fn populateEntries(self: *Self, fuzzy_search: []const u8) !void { 111 var it = self.dir.iterate(); 112 while (try it.next()) |entry| { ··· 115 continue; 116 } 117 118 try self.entries.append(.{ 119 .kind = entry.kind, 120 .name = try self.alloc.dupe(u8, entry.name), ··· 123 124 if (config.sort_dirs == true) { 125 std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sortEntry); 126 - } 127 - } 128 - 129 - pub fn writeEntries( 130 - self: *Self, 131 - window: vaxis.Window, 132 - selected_list_item_style: vaxis.Style, 133 - list_item_style: vaxis.Style, 134 - ) !void { 135 - for (self.entries.all()[self.entries.offset..], 0..) |item, i| { 136 - const selected = self.entries.selected - self.entries.offset; 137 - const is_selected = selected == i; 138 - 139 - if (std.mem.startsWith(u8, item.name, ".") and config.show_hidden == false) { 140 - continue; 141 - } 142 - 143 - if (i > window.height) continue; 144 - 145 - const w = window.child(.{ .y_off = @intCast(i), .height = 1 }); 146 - w.fill(vaxis.Cell{ 147 - .style = if (is_selected) selected_list_item_style else list_item_style, 148 - }); 149 - 150 - _ = w.print(&.{ 151 - .{ 152 - .text = item.name, 153 - .style = if (is_selected) selected_list_item_style else list_item_style, 154 - }, 155 - }, .{}); 156 } 157 } 158
··· 5 const vaxis = @import("vaxis"); 6 const fuzzig = @import("fuzzig"); 7 8 const history_len: usize = 100; 9 10 const Self = @This(); ··· 15 file_contents: [4096]u8 = undefined, 16 pdf_contents: ?[]u8 = null, 17 entries: List(std.fs.Dir.Entry), 18 + history: CircStack(usize, history_len), 19 child_entries: List([]const u8), 20 searcher: fuzzig.Ascii, 21 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 + 37 return Self{ 38 .alloc = alloc, 39 + .dir = dir, 40 .entries = List(std.fs.Dir.Entry).init(alloc), 41 + .history = CircStack(usize, history_len).init(), 42 .child_entries = List([]const u8).init(alloc), 43 .searcher = try fuzzig.Ascii.init( 44 alloc, ··· 80 return try self.dir.realpath(relative_path, &self.path_buf); 81 } 82 83 + pub fn getDirSize(self: Self, dir: std.fs.Dir) !usize { 84 + var total_size: usize = 0; 85 + 86 + var walker = try dir.walk(self.alloc); 87 + defer walker.deinit(); 88 + 89 + while (try walker.next()) |entry| { 90 + switch (entry.kind) { 91 + .file => { 92 + const stat = try entry.dir.statFile(entry.basename); 93 + total_size += stat.size; 94 + }, 95 + else => {}, 96 + } 97 + } 98 + 99 + return total_size; 100 + } 101 + 102 pub fn populateChildEntries( 103 self: *Self, 104 relative_path: []const u8, ··· 108 109 var it = dir.iterate(); 110 while (try it.next()) |entry| { 111 + if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) { 112 + continue; 113 + } 114 + 115 try self.child_entries.append(try self.alloc.dupe(u8, entry.name)); 116 } 117 ··· 120 } 121 } 122 123 pub fn populateEntries(self: *Self, fuzzy_search: []const u8) !void { 124 var it = self.dir.iterate(); 125 while (try it.next()) |entry| { ··· 128 continue; 129 } 130 131 + if (std.mem.startsWith(u8, entry.name, ".") and config.show_hidden == false) { 132 + continue; 133 + } 134 + 135 try self.entries.append(.{ 136 .kind = entry.kind, 137 .name = try self.alloc.dupe(u8, entry.name), ··· 140 141 if (config.sort_dirs == true) { 142 std.mem.sort(std.fs.Dir.Entry, self.entries.all(), {}, sortEntry); 143 } 144 } 145
+416 -102
src/drawer.zig
··· 1 const std = @import("std"); 2 const App = @import("./app.zig"); 3 const Notification = @import("./notification.zig"); 4 const Directories = @import("./directories.zig"); 5 const config = &@import("./config.zig").config; 6 const vaxis = @import("vaxis"); 7 const Git = @import("./git.zig"); 8 - const inputToSlice = @import("./event_handlers.zig").inputToSlice; 9 10 const Drawer = @This(); 11 12 const top_div: u16 = 1; 13 const info_div: u16 = 1; 14 - const bottom_div: u16 = 1; 15 16 // Used to detect whether to re-render an image. 17 current_item_path_buf: [std.fs.max_path_bytes]u8 = undefined, ··· 21 file_info_buf: [std.fs.max_path_bytes]u8 = undefined, 22 file_name_buf: [std.fs.max_path_bytes + 2]u8 = undefined, // +2 to accomodate for [<file_name>] 23 git_branch: [1024]u8 = undefined, 24 25 - pub fn draw(self: *Drawer, app: *App) !void { 26 const win = app.vx.window(); 27 win.clear(); 28 29 - const abs_file_path_bar = try self.drawAbsFilePath(app.alloc, &app.directories, win); 30 - const file_info_bar = try self.drawFileInfo(&app.directories, win); 31 - app.last_known_height = try drawDirList( 32 - &app.directories, 33 win, 34 abs_file_path_bar, 35 file_info_bar, 36 ); 37 38 - if (config.preview_file == true) { 39 const file_name_bar = try self.drawFileName(&app.directories, win); 40 try self.drawFilePreview(app, win, file_name_bar); 41 } 42 43 - const input = inputToSlice(app); 44 - try drawUserInput(app.state, &app.text_input, input, win); 45 - try drawNotification(&app.notification, win); 46 } 47 48 fn drawFileName( 49 self: *Drawer, 50 directories: *Directories, 51 win: vaxis.Window, 52 - ) !vaxis.Window { 53 const file_name_bar = win.child(.{ 54 .x_off = win.width / 2, 55 .y_off = 0, ··· 57 .height = top_div, 58 }); 59 60 - lbl: { 61 - const entry = directories.getSelected() catch break :lbl; 62 - if (entry) |e| { 63 - const file_name = try std.fmt.bufPrint( 64 - &self.file_name_buf, 65 - "[{s}]", 66 - .{e.name}, 67 - ); 68 - _ = file_name_bar.print(&.{vaxis.Segment{ 69 - .text = file_name, 70 - .style = config.styles.file_name, 71 - }}, .{}); 72 - } 73 - } 74 75 return file_name_bar; 76 } ··· 80 app: *App, 81 win: vaxis.Window, 82 file_name_win: vaxis.Window, 83 - ) !void { 84 const preview_win = win.child(.{ 85 .x_off = win.width / 2, 86 .y_off = top_div + 1, ··· 100 self.current_item_path = try std.fmt.bufPrint( 101 &self.current_item_path_buf, 102 "{s}/{s}", 103 - .{ try app.directories.fullPath("."), entry.name }, 104 ); 105 106 switch (entry.kind) { 107 .directory => { 108 app.directories.clearChildEntries(); 109 - if (app.directories.populateChildEntries(entry.name)) { 110 - try app.directories.writeChildEntries(preview_win, config.styles.list_item); 111 - } else |err| { 112 - switch (err) { 113 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 114 - else => try app.notification.writeErr(.UnknownError), 115 } 116 } 117 }, 118 .file => file: { 119 - var file = app.directories.dir.openFile(entry.name, .{ .mode = .read_only }) catch |err| { 120 - switch (err) { 121 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 122 - else => try app.notification.writeErr(.UnknownError), 123 - } 124 125 _ = preview_win.print(&.{ 126 - .{ .text = "No preview available." }, 127 }, .{}); 128 129 break :file; 130 }; 131 defer file.close(); 132 - const bytes = try file.readAll(&app.directories.file_contents); 133 134 // Handle image. 135 if (config.show_images == true) unsupported: { 136 var match = false; 137 - inline for (@typeInfo(vaxis.zigimg.Image.Format).Enum.fields) |field| { 138 - const entry_ext = std.mem.trimLeft( 139 - u8, 140 - std.fs.path.extension(entry.name), 141 - ".", 142 - ); 143 - if (std.mem.eql(u8, entry_ext, field.name)) { 144 - match = true; 145 - } 146 } 147 if (!match) break :unsupported; 148 149 - if (std.mem.eql(u8, self.last_item_path, self.current_item_path)) break :unsupported; 150 151 - var image = vaxis.zigimg.Image.fromFilePath( 152 - app.alloc, 153 - self.current_item_path, 154 - ) catch { 155 - break :unsupported; 156 - }; 157 - defer image.deinit(); 158 159 - if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &image, .rgba)) |img| { 160 - app.image = img; 161 - } else |_| { 162 - if (app.image) |img| { 163 - app.vx.freeImage(app.tty.anyWriter(), img.id); 164 } 165 - app.image = null; 166 - break :unsupported; 167 - } 168 169 - if (app.image) |img| { 170 - try img.draw(preview_win, .{ .scale = .contain }); 171 } 172 173 break :file; ··· 233 234 fn drawFileInfo( 235 self: *Drawer, 236 directories: *Directories, 237 win: vaxis.Window, 238 - ) !vaxis.Window { 239 const file_info_win = win.child(.{ 240 .x_off = 0, 241 .y_off = win.height - bottom_div, ··· 250 }; 251 252 var fbs = std.io.fixedBufferStream(&self.file_info_buf); 253 try fbs.writer().print( 254 - "{d}/{d} ", 255 - .{ directories.entries.selected + 1, directories.entries.len() }, 256 ); 257 258 - if (entry.kind == .directory) { 259 - _ = file_info_win.printSegment(.{ 260 - .text = fbs.getWritten(), 261 - .style = config.styles.file_information, 262 - }, .{}); 263 - return file_info_win; 264 } 265 266 - const file_size: u64 = lbl: { 267 - const formatted_size = directories.dir.statFile(entry.name) catch break :lbl 0; 268 - break :lbl formatted_size.size; 269 }; 270 271 - const extension = std.fs.path.extension(entry.name); 272 - if (extension.len > 0) try fbs.writer().print("{s} ", .{extension}); 273 274 - try fbs.writer().print("{:.2}", .{std.fmt.fmtIntSizeDec(file_size)}); 275 276 _ = file_info_win.printSegment(.{ 277 .text = fbs.getWritten(), ··· 282 } 283 284 fn drawDirList( 285 - directories: *Directories, 286 win: vaxis.Window, 287 abs_file_path: vaxis.Window, 288 file_information: vaxis.Window, 289 - ) !u16 { 290 const current_dir_list_win = win.child(.{ 291 .x_off = 0, 292 .y_off = top_div + 1, 293 .width = if (config.preview_file) win.width / 2 else win.width, 294 .height = win.height - (abs_file_path.height + file_information.height + top_div + bottom_div), 295 }); 296 - try directories.writeEntries( 297 - current_dir_list_win, 298 - config.styles.selected_list_item, 299 - config.styles.list_item, 300 - ); 301 302 - return current_dir_list_win.height; 303 } 304 305 fn drawAbsFilePath( 306 self: *Drawer, 307 - alloc: std.mem.Allocator, 308 - directories: *Directories, 309 win: vaxis.Window, 310 - ) !vaxis.Window { 311 const abs_file_path_bar = win.child(.{ 312 .x_off = 0, 313 .y_off = 0, ··· 315 .height = top_div, 316 }); 317 318 - const branch_alloc = try Git.getGitBranch(alloc, directories.dir); 319 - defer if (branch_alloc) |b| alloc.free(b); 320 const branch = if (branch_alloc) |b| 321 try std.fmt.bufPrint( 322 &self.git_branch, ··· 327 ""; 328 329 _ = abs_file_path_bar.print(&.{ 330 - vaxis.Segment{ .text = try directories.fullPath(".") }, 331 vaxis.Segment{ .text = if (branch_alloc != null) " on " else "" }, 332 vaxis.Segment{ .text = branch, .style = config.styles.git_branch }, 333 }, .{}); ··· 340 text_input: *vaxis.widgets.TextInput, 341 input: []const u8, 342 win: vaxis.Window, 343 - ) !void { 344 const user_input_win = win.child(.{ 345 .x_off = 0, 346 .y_off = top_div, ··· 351 352 switch (current_state) { 353 .fuzzy, .new_file, .new_dir, .rename, .change_dir, .command => { 354 - text_input.draw(user_input_win); 355 }, 356 .normal => { 357 if (text_input.buf.realLength() > 0) { 358 text_input.drawWithStyle( 359 user_input_win, 360 - if (std.mem.eql(u8, input, ":UnsupportedCommand")) config.styles.text_input_err else .{}, 361 ); 362 } 363 364 win.hideCursor(); 365 }, 366 } 367 } 368 369 fn drawNotification( 370 notification: *Notification, 371 win: vaxis.Window, 372 - ) !void { 373 if (notification.len() == 0) return; 374 if (notification.clearIfEnded()) return; 375 ··· 380 const max_width = win.width / 4; 381 const width = notification.len() + width_padding; 382 const calculated_width = if (width > max_width) max_width else width; 383 - const height = try std.math.divCeil(usize, notification.len(), calculated_width) + height_padding; 384 385 const notification_win = win.child(.{ 386 .x_off = @intCast(win.width - (calculated_width + screen_pos_padding)), ··· 400 .style = config.styles.notification.box, 401 }, .{ .wrap = .word }); 402 }
··· 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, ··· 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, ··· 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 } ··· 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, ··· 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; ··· 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, ··· 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(), ··· 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, ··· 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, ··· 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 }, .{}); ··· 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, ··· 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 ··· 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)), ··· 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 + }
+26
src/environment.zig
··· 1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 4 pub fn getHomeDir() !?std.fs.Dir { ··· 23 return null; 24 } 25 26 pub fn openFile( 27 alloc: std.mem.Allocator, 28 dir: std.fs.Dir, ··· 68 return result; 69 } 70 71 ///Returns the amount of files failed to be delete. 72 pub fn deleteContents(dir: std.fs.Dir) !usize { 73 var failed: usize = 0;
··· 1 const std = @import("std"); 2 + const zuid = @import("zuid"); 3 const builtin = @import("builtin"); 4 5 pub fn getHomeDir() !?std.fs.Dir { ··· 24 return null; 25 } 26 27 + pub fn checkDuplicatePath( 28 + buf: []u8, 29 + dir: std.fs.Dir, 30 + relative_path: []const u8, 31 + ) error{NoSpaceLeft}!struct { 32 + path: []const u8, 33 + had_duplicate: bool, 34 + } { 35 + var had_duplicate = false; 36 + const new_path = if (fileExists(dir, relative_path)) lbl: { 37 + had_duplicate = true; 38 + const extension = std.fs.path.extension(relative_path); 39 + break :lbl try std.fmt.bufPrint( 40 + buf, 41 + "{s}-{f}{s}", 42 + .{ relative_path[0 .. relative_path.len - extension.len], zuid.new.v4(), extension }, 43 + ); 44 + } else lbl: { 45 + break :lbl try std.fmt.bufPrint(buf, "{s}", .{relative_path}); 46 + }; 47 + 48 + return .{ .path = new_path, .had_duplicate = had_duplicate }; 49 + } 50 + 51 pub fn openFile( 52 alloc: std.mem.Allocator, 53 dir: std.fs.Dir, ··· 93 return result; 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;
+202 -390
src/event_handlers.zig
··· 6 const Key = vaxis.Key; 7 const config = &@import("./config.zig").config; 8 const commands = @import("./commands.zig"); 9 - 10 - pub fn inputToSlice(self: *App) []const u8 { 11 - self.text_input.buf.cursor = self.text_input.buf.realLength(); 12 - return self.text_input.sliceToCursor(&self.text_input_buf); 13 - } 14 15 - pub fn handleNormalEvent( 16 app: *App, 17 event: App.Event, 18 - loop: *vaxis.Loop(App.Event), 19 - ) !void { 20 switch (event) { 21 .key_press => |key| { 22 if ((key.codepoint == 'c' and key.mods.ctrl)) { ··· 24 return; 25 } 26 27 - switch (key.codepoint) { 28 - '-', 'h', Key.left => { 29 - app.text_input.clearAndFree(); 30 - 31 - if (app.directories.dir.openDir("../", .{ .iterate = true })) |dir| { 32 - app.directories.dir.close(); 33 - app.directories.dir = dir; 34 - 35 - app.directories.clearEntries(); 36 - const fuzzy = inputToSlice(app); 37 - app.directories.populateEntries(fuzzy) catch |err| { 38 - switch (err) { 39 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 40 - else => try app.notification.writeErr(.UnknownError), 41 - } 42 }; 43 - 44 - if (app.directories.history.pop()) |history| { 45 - app.directories.entries.selected = history.selected; 46 - app.directories.entries.offset = history.offset; 47 - } 48 - } else |err| { 49 - switch (err) { 50 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 51 - else => try app.notification.writeErr(.UnknownError), 52 - } 53 - } 54 - }, 55 - Key.enter, 'l', Key.right => { 56 - const entry = lbl: { 57 - const entry = app.directories.getSelected() catch return; 58 - if (entry) |e| break :lbl e else return; 59 - }; 60 - 61 - switch (entry.kind) { 62 - .directory => { 63 - app.text_input.clearAndFree(); 64 - 65 - if (app.directories.dir.openDir(entry.name, .{ .iterate = true })) |dir| { 66 - app.directories.dir.close(); 67 - app.directories.dir = dir; 68 - 69 - _ = app.directories.history.push(.{ 70 - .selected = app.directories.entries.selected, 71 - .offset = app.directories.entries.offset, 72 - }); 73 - 74 - app.directories.clearEntries(); 75 - const fuzzy = inputToSlice(app); 76 - app.directories.populateEntries(fuzzy) catch |err| { 77 - switch (err) { 78 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 79 - else => try app.notification.writeErr(.UnknownError), 80 - } 81 - }; 82 - } else |err| { 83 - switch (err) { 84 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 85 - else => try app.notification.writeErr(.UnknownError), 86 - } 87 - } 88 - }, 89 - .file => { 90 - if (environment.getEditor()) |editor| { 91 - try app.vx.exitAltScreen(app.tty.anyWriter()); 92 - try app.vx.resetState(app.tty.anyWriter()); 93 - loop.stop(); 94 - 95 - environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch { 96 - try app.notification.writeErr(.UnableToOpenFile); 97 - }; 98 - 99 - try loop.start(); 100 - try app.vx.enterAltScreen(app.tty.anyWriter()); 101 - try app.vx.enableDetectedFeatures(app.tty.anyWriter()); 102 - app.vx.queueRefresh(); 103 - } else { 104 - try app.notification.writeErr(.EditorNotSet); 105 - } 106 - }, 107 - else => {}, 108 - } 109 - }, 110 - 'j', Key.down => { 111 - app.directories.entries.next(app.last_known_height); 112 - }, 113 - 'k', Key.up => { 114 - app.directories.entries.previous(app.last_known_height); 115 - }, 116 - 'G' => { 117 - app.directories.entries.selectLast(app.last_known_height); 118 - }, 119 - 'g' => app.directories.entries.selectFirst(), 120 - 'D' => { 121 - const entry = lbl: { 122 - const entry = app.directories.getSelected() catch { 123 - try app.notification.writeErr(.UnableToDelete); 124 - return; 125 }; 126 - if (entry) |e| break :lbl e else return; 127 - }; 128 - 129 - var old_path_buf: [std.fs.max_path_bytes]u8 = undefined; 130 - const old_path = try app.alloc.dupe(u8, try app.directories.dir.realpath(entry.name, &old_path_buf)); 131 - 132 - var trash_dir = dir: { 133 - notfound: { 134 - break :dir (config.trashDir() catch break :notfound) orelse break :notfound; 135 - } 136 - app.alloc.free(old_path); 137 - try app.notification.writeErr(.ConfigPathNotFound); 138 - return; 139 - }; 140 - defer trash_dir.close(); 141 - var trash_dir_path_buf: [std.fs.max_path_bytes]u8 = undefined; 142 - const trash_dir_path = try trash_dir.realpath(".", &trash_dir_path_buf); 143 - 144 - if (std.mem.eql(u8, old_path, trash_dir_path)) { 145 - try app.notification.writeErr(.CannotDeleteTrashDir); 146 - app.alloc.free(old_path); 147 - return; 148 - } 149 150 - var tmp_path_buf: [std.fs.max_path_bytes]u8 = undefined; 151 - const tmp_path = try app.alloc.dupe(u8, try std.fmt.bufPrint(&tmp_path_buf, "{s}/{s}-{s}", .{ trash_dir_path, entry.name, zuid.new.v4().toString() })); 152 153 - if (app.directories.dir.rename(entry.name, tmp_path)) { 154 - if (app.actions.push(.{ 155 - .delete = .{ .old = old_path, .new = tmp_path }, 156 - })) |prev_elem| { 157 - app.alloc.free(prev_elem.delete.old); 158 - app.alloc.free(prev_elem.delete.new); 159 - } 160 161 - try app.notification.writeInfo(.Deleted); 162 - app.directories.removeSelected(); 163 - } else |err| { 164 - switch (err) { 165 - error.RenameAcrossMountPoints => try app.notification.writeErr(.UnableToDeleteAcrossMountPoints), 166 - else => try app.notification.writeErr(.UnableToDelete), 167 } 168 - app.alloc.free(old_path); 169 - app.alloc.free(tmp_path); 170 } 171 - }, 172 - 'd' => { 173 - app.text_input.clearAndFree(); 174 - app.directories.clearEntries(); 175 - app.directories.populateEntries("") catch |err| { 176 - switch (err) { 177 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 178 - else => try app.notification.writeErr(.UnknownError), 179 - } 180 - }; 181 - app.state = .new_dir; 182 - }, 183 - '%' => { 184 - app.text_input.clearAndFree(); 185 - app.directories.clearEntries(); 186 - app.directories.populateEntries("") catch |err| { 187 - switch (err) { 188 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 189 - else => try app.notification.writeErr(.UnknownError), 190 - } 191 - }; 192 - app.state = .new_file; 193 - }, 194 - 'u' => { 195 - if (app.actions.pop()) |action| { 196 - const selected = app.directories.entries.selected; 197 - 198 - switch (action) { 199 - .delete => |a| { 200 - defer app.alloc.free(a.new); 201 - defer app.alloc.free(a.old); 202 203 - // TODO: Will overwrite an item if it has the same name. 204 - if (app.directories.dir.rename(a.new, a.old)) { 205 - app.directories.clearEntries(); 206 - const fuzzy = inputToSlice(app); 207 - app.directories.populateEntries(fuzzy) catch |err| { 208 - switch (err) { 209 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 210 - else => try app.notification.writeErr(.UnknownError), 211 - } 212 - }; 213 - try app.notification.writeInfo(.RestoredDelete); 214 - } else |_| { 215 - try app.notification.writeErr(.UnableToUndo); 216 - } 217 - }, 218 - .rename => |a| { 219 - defer app.alloc.free(a.new); 220 - defer app.alloc.free(a.old); 221 - 222 - // TODO: Will overwrite an item if it has the same name. 223 - if (app.directories.dir.rename(a.new, a.old)) { 224 - app.directories.clearEntries(); 225 - const fuzzy = inputToSlice(app); 226 - app.directories.populateEntries(fuzzy) catch |err| { 227 - switch (err) { 228 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 229 - else => try app.notification.writeErr(.UnknownError), 230 - } 231 - }; 232 - try app.notification.writeInfo(.RestoredRename); 233 - } else |_| { 234 - try app.notification.writeErr(.UnableToUndo); 235 - } 236 - }, 237 - } 238 239 - app.directories.entries.selected = selected; 240 - } else { 241 - try app.notification.writeInfo(.EmptyUndo); 242 - } 243 - }, 244 - '/' => { 245 - app.text_input.clearAndFree(); 246 - app.state = .fuzzy; 247 - }, 248 - 'R' => { 249 - app.text_input.clearAndFree(); 250 - app.state = .rename; 251 252 - const entry = lbl: { 253 - const entry = app.directories.getSelected() catch { 254 - app.state = .normal; 255 - try app.notification.writeErr(.UnableToRename); 256 - return; 257 - }; 258 - if (entry) |e| break :lbl e else { 259 - app.state = .normal; 260 - return; 261 - } 262 - }; 263 264 - app.text_input.insertSliceAtCursor(entry.name) catch { 265 - app.state = .normal; 266 - try app.notification.writeErr(.UnableToRename); 267 - return; 268 - }; 269 - }, 270 - 'c' => { 271 - app.text_input.clearAndFree(); 272 - app.state = .change_dir; 273 - }, 274 - ':' => { 275 - app.text_input.clearAndFree(); 276 - app.text_input.insertSliceAtCursor(":") catch {}; 277 - app.state = .command; 278 - }, 279 - else => {}, 280 } 281 }, 282 - .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws), 283 } 284 } 285 286 pub fn handleInputEvent(app: *App, event: App.Event) !void { 287 switch (event) { 288 .key_press => |key| { 289 - if ((key.codepoint == 'c' and key.mods.ctrl)) { 290 - app.should_quit = true; 291 - return; 292 - } 293 - 294 switch (key.codepoint) { 295 Key.escape => { 296 switch (app.state) { 297 .fuzzy => { 298 - app.directories.clearEntries(); 299 - app.directories.populateEntries("") catch |err| { 300 - switch (err) { 301 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 302 - else => try app.notification.writeErr(.UnknownError), 303 - } 304 - }; 305 }, 306 else => {}, 307 } 308 ··· 312 Key.enter => { 313 const selected = app.directories.entries.selected; 314 switch (app.state) { 315 - .new_dir => { 316 - const dir = inputToSlice(app); 317 - if (app.directories.dir.makeDir(dir)) { 318 - try app.notification.writeInfo(.CreatedFolder); 319 - 320 - app.directories.clearEntries(); 321 - app.directories.populateEntries("") catch |err| { 322 - switch (err) { 323 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 324 - else => try app.notification.writeErr(.UnknownError), 325 - } 326 - }; 327 - } else |err| { 328 - switch (err) { 329 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 330 - error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists), 331 - else => try app.notification.writeErr(.UnknownError), 332 - } 333 - } 334 - app.text_input.clearAndFree(); 335 - }, 336 - .new_file => { 337 - const file = inputToSlice(app); 338 - if (environment.fileExists(app.directories.dir, file)) { 339 - try app.notification.writeErr(.ItemAlreadyExists); 340 - } else { 341 - if (app.directories.dir.createFile(file, .{})) |f| { 342 - f.close(); 343 - 344 - try app.notification.writeInfo(.CreatedFile); 345 - 346 - app.directories.clearEntries(); 347 - app.directories.populateEntries("") catch |err| { 348 - switch (err) { 349 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 350 - else => try app.notification.writeErr(.UnknownError), 351 - } 352 - }; 353 - } else |err| { 354 - switch (err) { 355 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 356 - else => try app.notification.writeErr(.UnknownError), 357 - } 358 - } 359 - } 360 - app.text_input.clearAndFree(); 361 - }, 362 - .rename => { 363 - var dir_prefix_buf: [std.fs.max_path_bytes]u8 = undefined; 364 - const dir_prefix = try app.directories.dir.realpath(".", &dir_prefix_buf); 365 - 366 - const old = lbl: { 367 - const entry = app.directories.getSelected() catch { 368 - try app.notification.writeErr(.UnableToRename); 369 - return; 370 - }; 371 - if (entry) |e| break :lbl e else return; 372 - }; 373 - const new = inputToSlice(app); 374 - 375 - if (environment.fileExists(app.directories.dir, new)) { 376 - try app.notification.writeErr(.ItemAlreadyExists); 377 - } else { 378 - app.directories.dir.rename(old.name, new) catch |err| switch (err) { 379 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 380 - error.PathAlreadyExists => try app.notification.writeErr(.ItemAlreadyExists), 381 - else => try app.notification.writeErr(.UnknownError), 382 - }; 383 - if (app.actions.push(.{ 384 - .rename = .{ 385 - .old = try std.fs.path.join(app.alloc, &.{ dir_prefix, old.name }), 386 - .new = try std.fs.path.join(app.alloc, &.{ dir_prefix, new }), 387 - }, 388 - })) |prev_elem| { 389 - app.alloc.free(prev_elem.rename.old); 390 - app.alloc.free(prev_elem.rename.new); 391 - } 392 - 393 - try app.notification.writeInfo(.Renamed); 394 - 395 - app.directories.clearEntries(); 396 - app.directories.populateEntries("") catch |err| { 397 - switch (err) { 398 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 399 - else => try app.notification.writeErr(.UnknownError), 400 - } 401 - }; 402 - } 403 app.text_input.clearAndFree(); 404 }, 405 - .change_dir => { 406 - const path = inputToSlice(app); 407 - if (app.directories.dir.openDir(path, .{ .iterate = true })) |dir| { 408 - app.directories.dir.close(); 409 - app.directories.dir = dir; 410 - 411 - try app.notification.writeInfo(.ChangedDir); 412 413 - app.directories.clearEntries(); 414 - app.directories.populateEntries("") catch |err| { 415 - switch (err) { 416 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 417 - else => try app.notification.writeErr(.UnknownError), 418 - } 419 }; 420 - app.directories.history.reset(); 421 - } else |err| { 422 - switch (err) { 423 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 424 - error.FileNotFound => try app.notification.writeErr(.IncorrectPath), 425 - error.NotDir => try app.notification.writeErr(.IncorrectPath), 426 - else => try app.notification.writeErr(.UnknownError), 427 - } 428 } 429 - 430 - app.text_input.clearAndFree(); 431 - }, 432 - .command => { 433 - const command = inputToSlice(app); 434 435 supported: { 436 if (std.mem.eql(u8, command, ":q")) { ··· 448 break :supported; 449 } 450 451 - // TODO(06-01-25): Add a confirmation for this. 452 if (std.mem.eql(u8, command, ":empty_trash")) { 453 try commands.emptyTrash(app); 454 break :supported; 455 } 456 457 app.text_input.clearAndFree(); 458 try app.text_input.insertSliceAtCursor(":UnsupportedCommand"); 459 } 460 }, 461 else => {}, 462 } 463 - app.state = .normal; 464 app.directories.entries.selected = selected; 465 }, 466 else => { 467 try app.text_input.update(.{ .key_press = key }); 468 469 switch (app.state) { 470 .fuzzy => { 471 - app.directories.clearEntries(); 472 - const fuzzy = inputToSlice(app); 473 - app.directories.populateEntries(fuzzy) catch |err| { 474 - switch (err) { 475 - error.AccessDenied => try app.notification.writeErr(.PermissionDenied), 476 - else => try app.notification.writeErr(.UnknownError), 477 - } 478 - }; 479 }, 480 .command => { 481 - const command = inputToSlice(app); 482 if (!std.mem.startsWith(u8, command, ":")) { 483 app.text_input.clearAndFree(); 484 - try app.text_input.insertSliceAtCursor(":"); 485 } 486 }, 487 else => {}, ··· 489 }, 490 } 491 }, 492 - .winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws), 493 } 494 }
··· 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)) { ··· 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 ··· 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")) { ··· 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 => {}, ··· 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
···
··· 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
···
··· 1 + const std = @import("std"); 2 + const environment = @import("environment.zig"); 3 + const config = &@import("./config.zig").config; 4 + 5 + pub const LOG_PATH = "log.txt"; 6 + 7 + const LogLevel = enum { 8 + err, 9 + info, 10 + warn, 11 + 12 + pub fn toString(level: LogLevel) []const u8 { 13 + return switch (level) { 14 + .err => "ERROR", 15 + .info => "INFO", 16 + .warn => "WARN", 17 + }; 18 + } 19 + }; 20 + 21 + const FileLogger = @This(); 22 + 23 + dir: std.fs.Dir, 24 + file: ?std.fs.File, 25 + 26 + pub fn init(dir: std.fs.Dir) FileLogger { 27 + const file = dir.createFile(LOG_PATH, .{ .truncate = false, .read = true }) catch |err| { 28 + std.log.err("Failed to create/open log file: {s}", .{@errorName(err)}); 29 + return .{ .dir = dir, .file = null }; 30 + }; 31 + 32 + return .{ .dir = dir, .file = file }; 33 + } 34 + 35 + pub fn deinit(self: FileLogger) void { 36 + if (self.file) |file| { 37 + var f = file; 38 + f.close(); 39 + } 40 + } 41 + 42 + pub fn write(self: FileLogger, msg: []const u8, level: LogLevel) !void { 43 + const file = if (self.file) |file| file else return error.NoLogFile; 44 + 45 + if (try file.tryLock(.exclusive)) { 46 + defer file.unlock(); 47 + 48 + var buffer: [1024]u8 = undefined; 49 + var file_writer_impl = file.writer(&buffer); 50 + const file_writer = &file_writer_impl.interface; 51 + try file_writer_impl.seekTo(file.getEndPos() catch 0); 52 + 53 + try file_writer.print( 54 + "({d}) {s}: {s}\n", 55 + .{ std.time.timestamp(), LogLevel.toString(level), msg }, 56 + ); 57 + try file_writer.flush(); 58 + } 59 + }
+5 -6
src/git.zig
··· 2 3 /// Callers owns memory returned. 4 pub fn getGitBranch(alloc: std.mem.Allocator, dir: std.fs.Dir) !?[]const u8 { 5 - var file = dir.openFile(".git/HEAD", .{}) catch return null; 6 defer file.close(); 7 8 var buf: [1024]u8 = undefined; 9 - const bytes = file.readAll(&buf) catch return null; 10 11 - var it = std.mem.splitBackwardsSequence(u8, buf[0..bytes], "/"); 12 - const branch = it.next() orelse return null; 13 - if (std.mem.eql(u8, branch, "")) return null; 14 15 - return try alloc.dupe(u8, branch); 16 }
··· 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 }
+13 -22
src/list.zig
··· 8 alloc: std.mem.Allocator, 9 items: std.ArrayList(T), 10 selected: usize, 11 - offset: usize, 12 13 pub fn init(alloc: std.mem.Allocator) Self { 14 return Self{ 15 .alloc = alloc, 16 - .items = std.ArrayList(T).init(alloc), 17 .selected = 0, 18 - .offset = 0, 19 }; 20 } 21 22 pub fn deinit(self: *Self) void { 23 - self.items.deinit(); 24 } 25 26 pub fn append(self: *Self, item: T) !void { 27 - try self.items.append(item); 28 } 29 30 pub fn clear(self: *Self) void { 31 - self.items.clearAndFree(); 32 self.selected = 0; 33 - self.offset = 0; 34 } 35 36 pub fn get(self: Self, index: usize) !T { ··· 61 return self.items.items.len; 62 } 63 64 - pub fn next(self: *Self, win_height: usize) void { 65 if (self.selected + 1 < self.len()) { 66 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 } 72 } 73 74 - pub fn previous(self: *Self, win_height: usize) void { 75 if (self.selected > 0) { 76 self.selected -= 1; 77 - 78 - if (self.offset > 0 and self.selected < self.offset + (win_height / 2)) { 79 - self.offset -= 1; 80 - } 81 } 82 } 83 84 - pub fn selectLast(self: *Self, win_height: usize) void { 85 self.selected = self.len() - 1; 86 - if (self.selected >= win_height) { 87 - self.offset = self.selected - (win_height - 1); 88 - } 89 } 90 91 pub fn selectFirst(self: *Self) void { 92 self.selected = 0; 93 - self.offset = 0; 94 } 95 }; 96 }
··· 8 alloc: std.mem.Allocator, 9 items: std.ArrayList(T), 10 selected: usize, 11 12 pub fn init(alloc: std.mem.Allocator) Self { 13 return Self{ 14 .alloc = alloc, 15 + .items = .empty, 16 .selected = 0, 17 }; 18 } 19 20 pub fn deinit(self: *Self) void { 21 + self.items.deinit(self.alloc); 22 } 23 24 pub fn append(self: *Self, item: T) !void { 25 + try self.items.append(self.alloc, item); 26 } 27 28 pub fn clear(self: *Self) void { 29 + self.items.clearAndFree(self.alloc); 30 self.selected = 0; 31 + } 32 + 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 { ··· 64 return self.items.items.len; 65 } 66 67 + pub fn next(self: *Self) void { 68 if (self.selected + 1 < self.len()) { 69 self.selected += 1; 70 } 71 } 72 73 + pub fn previous(self: *Self) void { 74 if (self.selected > 0) { 75 self.selected -= 1; 76 } 77 } 78 79 + pub fn selectLast(self: *Self) void { 80 self.selected = self.len() - 1; 81 } 82 83 pub fn selectFirst(self: *Self) void { 84 self.selected = 0; 85 } 86 }; 87 }
+132 -11
src/main.zig
··· 1 const std = @import("std"); 2 const builtin = @import("builtin"); 3 const App = @import("app.zig"); 4 const vaxis = @import("vaxis"); 5 - const ConfigParseRes = @import("./config.zig").ParseRes; 6 const config = &@import("./config.zig").config; 7 8 pub const panic = vaxis.panic_handler; 9 10 pub const std_options: std.Options = .{ 11 .log_scope_levels = &.{ ··· 14 }, 15 }; 16 17 pub fn main() !void { 18 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 19 defer { ··· 24 } 25 const alloc = gpa.allocator(); 26 27 - var app = try App.init(alloc); 28 - defer app.deinit(); 29 30 - const config_parse_res = config.parse(alloc) catch |err| lbl: { 31 - switch (err) { 32 error.SyntaxError => { 33 - try app.notification.writeErr(.ConfigSyntaxError); 34 }, 35 else => { 36 - try app.notification.writeErr(.ConfigUnknownError); 37 }, 38 } 39 40 - break :lbl ConfigParseRes{ .deprecated = false }; 41 - }; 42 - if (config_parse_res.deprecated) try app.notification.writeWarn(.DeprecatedConfigPath); 43 44 - try app.run(); 45 }
··· 1 const std = @import("std"); 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; 9 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 = &.{ ··· 30 }, 31 }; 32 33 + const Options = struct { 34 + help: bool = false, 35 + version: bool = false, 36 + @"choose-dir": bool = false, 37 + @"entry-path": []const u8 = ".", 38 + 39 + fn optKind(a: []const u8) enum { short, long, positional } { 40 + if (std.mem.startsWith(u8, a, "--")) return .long; 41 + if (std.mem.startsWith(u8, a, "-")) return .short; 42 + return .positional; 43 + } 44 + }; 45 + 46 pub fn main() !void { 47 var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 48 defer { ··· 53 } 54 const alloc = gpa.allocator(); 55 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 + }; 141 + 142 + app.file_logger = if (config.config_dir) |dir| FileLogger.init(dir) else logger: { 143 + std.log.err("Failed to initialise file logger - no config directory found", .{}); 144 + break :logger null; 145 + }; 146 + app.notification.loop = &app.loop; 147 + 148 + try app.run(); 149 + 150 + if (opts.@"choose-dir") { 151 + last_dir = alloc.dupe(u8, try app.directories.fullPath(".")) catch null; 152 } 153 + } 154 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 + } 166 }
+10 -77
src/notification.zig
··· 1 const std = @import("std"); 2 3 const Self = @This(); 4 ··· 11 warn, 12 }; 13 14 - const Error = enum { 15 - PermissionDenied, 16 - UnknownError, 17 - UnableToUndo, 18 - UnableToOpenFile, 19 - UnableToDelete, 20 - FailedToDeleteSomeItems, 21 - UnableToDeleteAcrossMountPoints, 22 - UnsupportedImageFormat, 23 - EditorNotSet, 24 - ItemAlreadyExists, 25 - UnableToRename, 26 - IncorrectPath, 27 - ConfigSyntaxError, 28 - ConfigUnknownError, 29 - ConfigPathNotFound, 30 - CannotDeleteTrashDir, 31 - }; 32 - 33 - const Info = enum { 34 - CreatedFile, 35 - CreatedFolder, 36 - Deleted, 37 - Renamed, 38 - RestoredDelete, 39 - RestoredRename, 40 - EmptyUndo, 41 - ChangedDir, 42 - }; 43 - 44 - const Warn = enum { DeprecatedConfigPath }; 45 46 - buf: [1024]u8 = undefined, 47 style: Style = Style.info, 48 - fbs: std.io.FixedBufferStream([]u8) = undefined, 49 /// How long until the notification disappears in seconds. 50 timer: i64 = 0, 51 - 52 - pub fn init(self: *Self) void { 53 - self.fbs = std.io.fixedBufferStream(&self.buf); 54 - self.timer = std.time.timestamp(); 55 - } 56 57 pub fn write(self: *Self, text: []const u8, style: Style) !void { 58 self.fbs.reset(); 59 _ = try self.fbs.write(text); 60 self.timer = std.time.timestamp(); 61 self.style = style; 62 - } 63 64 - pub fn writeErr(self: *Self, err: Error) !void { 65 - try switch (err) { 66 - .PermissionDenied => self.write("Permission denied.", .err), 67 - .UnknownError => self.write("An unknown error occurred.", .err), 68 - .UnableToOpenFile => self.write("Unable to open file.", .err), 69 - .UnableToDelete => self.write("Unable to delete item.", .err), 70 - .FailedToDeleteSomeItems => self.write("Failed to delete some items..", .err), 71 - .UnableToDeleteAcrossMountPoints => self.write("Unable to move item to /tmp. Failed to delete.", .err), 72 - .UnableToUndo => self.write("Unable to undo previous action.", .err), 73 - .ItemAlreadyExists => self.write("Item already exists.", .err), 74 - .UnableToRename => self.write("Unable to rename item.", .err), 75 - .IncorrectPath => self.write("Unable to find path.", .err), 76 - .EditorNotSet => self.write("$EDITOR is not set.", .err), 77 - .UnsupportedImageFormat => self.write("Unsupported image format.", .err), 78 - .ConfigSyntaxError => self.write("Could not read config due to a syntax error.", .err), 79 - .ConfigUnknownError => self.write("Could not read config due to an unknown error.", .err), 80 - .ConfigPathNotFound => self.write("Could not read config due to unset env variables. Please set either $HOME or $XDG_CONFIG_HOME.", .err), 81 - .CannotDeleteTrashDir => self.write("Cannot delete trash directory.", .err), 82 - }; 83 - } 84 - 85 - pub fn writeInfo(self: *Self, info: Info) !void { 86 - try switch (info) { 87 - .CreatedFile => self.write("Successfully created file.", .info), 88 - .CreatedFolder => self.write("Successfully created folder.", .info), 89 - .Deleted => self.write("Successfully deleted item.", .info), 90 - .Renamed => self.write("Successfully renamed item.", .info), 91 - .RestoredDelete => self.write("Successfully restored deleted item.", .info), 92 - .RestoredRename => self.write("Successfully restored renamed item.", .info), 93 - .EmptyUndo => self.write("Nothing to undo.", .info), 94 - .ChangedDir => self.write("Successfully changed directory.", .info), 95 - }; 96 - } 97 - 98 - pub fn writeWarn(self: *Self, warning: Warn) !void { 99 - try switch (warning) { 100 - .DeprecatedConfigPath => self.write("You are using a deprecated config path. Please move your config to either `$XDG_CONFIG_HOME/jido` or `$HOME/.jido`", .warn), 101 - }; 102 } 103 104 pub fn reset(self: *Self) void {
··· 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"); 6 7 const Self = @This(); 8 ··· 15 warn, 16 }; 17 18 + var buf: [1024]u8 = undefined; 19 20 style: Style = Style.info, 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, 25 26 pub fn write(self: *Self, text: []const u8, style: Style) !void { 27 self.fbs.reset(); 28 _ = try self.fbs.write(text); 29 self.timer = std.time.timestamp(); 30 self.style = style; 31 32 + if (self.loop) |loop| { 33 + loop.postEvent(.notification); 34 + } 35 } 36 37 pub fn reset(self: *Self) void {