diff --git a/config/HybridBar/scripts/change-active-workspace b/config/HybridBar/scripts/change-active-workspace old mode 100755 new mode 100644 diff --git a/config/HybridBar/scripts/get-active-workspace b/config/HybridBar/scripts/get-active-workspace old mode 100755 new mode 100644 diff --git a/config/HybridBar/scripts/get-window-title b/config/HybridBar/scripts/get-window-title old mode 100755 new mode 100644 diff --git a/config/HybridBar/scripts/get-workspaces b/config/HybridBar/scripts/get-workspaces old mode 100755 new mode 100644 diff --git a/config/eww/scripts/change-active-workspace b/config/eww/scripts/change-active-workspace old mode 100755 new mode 100644 diff --git a/config/eww/scripts/get-active-workspace b/config/eww/scripts/get-active-workspace old mode 100755 new mode 100644 diff --git a/config/eww/scripts/get-music b/config/eww/scripts/get-music old mode 100755 new mode 100644 diff --git a/config/eww/scripts/get-network b/config/eww/scripts/get-network old mode 100755 new mode 100644 diff --git a/config/eww/scripts/get-window-title b/config/eww/scripts/get-window-title old mode 100755 new mode 100644 diff --git a/config/eww/scripts/get-workspaces b/config/eww/scripts/get-workspaces old mode 100755 new mode 100644 diff --git a/config/eww/scripts/getvol b/config/eww/scripts/getvol old mode 100755 new mode 100644 diff --git a/config/eww/scripts/github b/config/eww/scripts/github old mode 100755 new mode 100644 diff --git a/config/lf/cleaner b/config/lf/cleaner old mode 100755 new mode 100644 diff --git a/config/lf/lfrc b/config/lf/lfrc old mode 100755 new mode 100644 diff --git a/config/nsxiv/exec/image-info b/config/nsxiv/exec/image-info old mode 100755 new mode 100644 diff --git a/config/nsxiv/exec/key-handler b/config/nsxiv/exec/key-handler old mode 100755 new mode 100644 diff --git a/config/nsxiv/exec/nsxiv-url b/config/nsxiv/exec/nsxiv-url old mode 100755 new mode 100644 diff --git a/config/nsxiv/exec/thumb-info b/config/nsxiv/exec/thumb-info old mode 100755 new mode 100644 diff --git a/config/nsxiv/exec/win-title b/config/nsxiv/exec/win-title old mode 100755 new mode 100644 diff --git a/config/x11/opt-apps b/config/x11/opt-apps old mode 100755 new mode 100644 diff --git a/config/yazi/init.lua b/config/yazi/init.lua index 15eb213d..bdf6db08 100644 --- a/config/yazi/init.lua +++ b/config/yazi/init.lua @@ -1,4 +1,4 @@ --- require("relative-motions"):setup({ show_numbers = "relative", show_motion = true }) +require("relative-motions"):setup({ show_numbers = "relative", show_motion = true }) require("full-border"):setup() require("starship"):setup() require("augment-command"):setup({ @@ -14,3 +14,4 @@ require("augment-command"):setup({ ignore_hidden_items = false, wraparound_file_navigation = false, }) +require("git"):setup() diff --git a/config/yazi/keymap.toml b/config/yazi/keymap.toml index 0472c405..5e18ac6b 100644 --- a/config/yazi/keymap.toml +++ b/config/yazi/keymap.toml @@ -45,11 +45,11 @@ keymap = [ {on = [ "G" ], run = "arrow 99999999", desc = "Move cursor to the bottom"}, # Selection - {on = [ "" ], run = [ "select --state=none", "arrow 1" ], desc = "Toggle the current selection state"}, + {on = [ "" ], run = [ "toggle --state=none", "arrow 1" ], desc = "Toggle the current selection state"}, {on = [ "v" ], run = "visual_mode", desc = "Enter visual mode (selection mode)"}, {on = [ "V" ], run = "visual_mode --unset", desc = "Enter visual mode (unset mode)"}, - {on = [ "" ], run = "select_all --state=true", desc = "Select all files"}, - {on = [ "" ], run = "select_all --state=none", desc = "Inverse selection of all files"}, + {on = [ "" ], run = "toggle_all --state=true", desc = "Select all files"}, + {on = [ "" ], run = "toggle_all --state=none", desc = "Inverse selection of all files"}, # Operation {on = [ "e" ], run = "open", desc = "Open the selected files"}, @@ -159,11 +159,12 @@ prepend_keymap = [ # chmod {on = [ "c", "m" ], run = "plugin chmod", desc = "Chmod on selected files"}, # Archive - {on = [ "c", "a" ], run = "plugin archive", desc = "Archive selected files"}, + {on = [ "c", "a" ], run = "plugin ouch --args=zip", desc = "Compress with ouch"}, # Augment Command {on = [ "l" ], run = "plugin augment-command --args='enter'", desc = "Enter a directory and skip directories with only a single subdirectory"}, {on = [ "k" ], run = "plugin augment-command --args='arrow -1'", desc = "Move cursor up"}, {on = [ "r" ], run = "plugin augment-command --args='rename --cursor=before_ext'", desc = "Rename a file or directory"}, + {on = [ "R" ], run = "plugin augment-command --args='rename'", desc = "Rename a file or directory"}, {on = [ "D" ], run = "plugin augment-command --args='remove --permanently'", desc = "Permanently delete the files"}, # Dragon {on = [ "m", "a" ], run = ''' shell 'ripdrag -atk "$@"' --confirm ''', desc = "Drag and drop all"}, @@ -175,15 +176,16 @@ prepend_keymap = [ # Hide Preview {on = "", run = "plugin --sync hide-preview", desc = "Hide or show preview"}, # Relative motions - # {on = [ "1" ], run = "plugin relative-motions --args=1", desc = "Move in relative steps"}, - # {on = [ "2" ], run = "plugin relative-motions --args=2", desc = "Move in relative steps"}, - # {on = [ "3" ], run = "plugin relative-motions --args=3", desc = "Move in relative steps"}, - # {on = [ "4" ], run = "plugin relative-motions --args=4", desc = "Move in relative steps"}, - # {on = [ "5" ], run = "plugin relative-motions --args=5", desc = "Move in relative steps"}, - # {on = [ "6" ], run = "plugin relative-motions --args=6", desc = "Move in relative steps"}, - # {on = [ "7" ], run = "plugin relative-motions --args=7", desc = "Move in relative steps"}, - # {on = [ "8" ], run = "plugin relative-motions --args=8", desc = "Move in relative steps"}, - # {on = [ "9" ], run = "plugin relative-motions --args=9", desc = "Move in relative steps"}, + {on = [ "1" ], run = "plugin relative-motions --args=1", desc = "Move in relative steps"}, + {on = [ "2" ], run = "plugin relative-motions --args=2", desc = "Move in relative steps"}, + {on = [ "3" ], run = "plugin relative-motions --args=3", desc = "Move in relative steps"}, + {on = [ "4" ], run = "plugin relative-motions --args=4", desc = "Move in relative steps"}, + {on = [ "5" ], run = "plugin relative-motions --args=5", desc = "Move in relative steps"}, + {on = [ "6" ], run = "plugin relative-motions --args=6", desc = "Move in relative steps"}, + {on = [ "7" ], run = "plugin relative-motions --args=7", desc = "Move in relative steps"}, + {on = [ "8" ], run = "plugin relative-motions --args=8", desc = "Move in relative steps"}, + {on = [ "9" ], run = "plugin relative-motions --args=9", desc = "Move in relative steps"}, + {on = [ "c", "s" ], run = "plugin what-size", desc = "Calc size of selection or cwd"}, ] [tasks] diff --git a/config/yazi/package.toml b/config/yazi/package.toml index 989b092b..cf3d3b8b 100644 --- a/config/yazi/package.toml +++ b/config/yazi/package.toml @@ -1,20 +1,22 @@ [plugin] deps = [ - {use = "AnirudhG07/nbpreview", commit = "f0149a4"}, - {use = "kirasok/torrent-preview", commit = "76970b6"}, - {use = "Sonico98/exifaudio", commit = "92366cf"}, - {use = "Reledia/miller", commit = "75f0002"}, - {use = "Reledia/glow", commit = "536185a"}, - {use = "yazi-rs/plugins#chmod", commit = "3783ea0"}, - {use = "yazi-rs/plugins#full-border", commit = "3783ea0"}, - {use = "KKV9/archive", commit = "9f3b049"}, - {use = "Rolv-Apneseth/starship", commit = "6197e4c"}, - {use = "Reledia/hexyl", commit = "64daf93"}, - {use = "hankertrix/augment-command", commit = "d81ddb8"}, - {use = "imsi32/yatline", commit = "d26ffbc"}, - {use = "dedukun/relative-motions", commit = "a5465c0"}, - {use = "yazi-rs/plugins#max-preview", commit = "3783ea0"}, - {use = "yazi-rs/plugins#hide-preview", commit = "3783ea0"}, + {use = "AnirudhG07/nbpreview", rev = "52a14b3"}, + {use = "Reledia/glow", rev = "d8b36ff"}, + {use = "Reledia/hexyl", rev = "ccc0a4a"}, + {use = "Reledia/miller", rev = "40e0265"}, + {use = "Rolv-Apneseth/starship", rev = "77a65f5"}, + {use = "Sonico98/exifaudio", rev = "d75db46"}, + {use = "dedukun/relative-motions", rev = "6aecfcd"}, + {use = "hankertrix/augment-command", rev = "b0f113d"}, + {use = "imsi32/yatline", rev = "7b56434"}, + {use = "kirasok/torrent-preview", rev = "76970b6"}, + {use = "ndtoan96/ouch", rev = "251da69"}, + {use = "pirafrank/what-size", rev = "f08f7f2"}, + {use = "yazi-rs/plugins:chmod", rev = "4f1d0ae"}, + {use = "yazi-rs/plugins:full-border", rev = "4f1d0ae"}, + {use = "yazi-rs/plugins:git", rev = "4f1d0ae"}, + {use = "yazi-rs/plugins:hide-preview", rev = "4f1d0ae"}, + {use = "yazi-rs/plugins:max-preview", rev = "4f1d0ae"}, ] [flavor] diff --git a/config/yazi/plugins/archive.yazi/README.md b/config/yazi/plugins/archive.yazi/README.md index 542b7889..385fe38b 100644 --- a/config/yazi/plugins/archive.yazi/README.md +++ b/config/yazi/plugins/archive.yazi/README.md @@ -1,6 +1,6 @@ -# archive.yazi +# ~~archive.yazi~~ compress.yazi -A Yazi plugin that compresses selected files to an archive. +A Yazi plugin that compresses selected files to an archive. Supporting yazi versions 0.2.5 and up. ## Supported file types @@ -10,8 +10,9 @@ A Yazi plugin that compresses selected files to an archive. | .7z | 7z a | 7z a | | .tar | tar rpf | tar rpf | | .tar.gz | gzip | 7z a -tgzip | -| .tar.bz2 | bzip2 | 7z a -tbzip2 | | .tar.xz | xz | 7z a -txz | +| .tar.bz2 | bzip2 | 7z a -tbzip2 | +| .tar.zst | zstd | zstd | **NOTE:** Windows users are required to install 7-Zip and add 7z.exe to the `path` environment variable, only tar archives will be available otherwise. @@ -21,13 +22,13 @@ A Yazi plugin that compresses selected files to an archive. ```bash # For Unix platforms -git clone https://github.com/KKV9/archive.yazi.git ~/.config/yazi/plugins/archive.yazi +git clone https://github.com/KKV9/compress.yazi.git ~/.config/yazi/plugins/compress.yazi ## For Windows -git clone https://github.com/KKV9/archive.yazi.git %AppData%\yazi\config\plugins\archive.yazi +git clone https://github.com/KKV9/compress.yazi.git %AppData%\yazi\config\plugins\compress.yazi # Or with yazi plugin manager -ya pack -a KKV9/archive +ya pack -a KKV9/compress ``` - Add this to your `keymap.toml`: @@ -35,7 +36,7 @@ ya pack -a KKV9/archive ```toml [[manager.prepend_keymap]] on = [ "c", "a" ] -run = "plugin archive" +run = "plugin compress" desc = "Archive selected files" ``` diff --git a/config/yazi/plugins/archive.yazi/init.lua b/config/yazi/plugins/archive.yazi/init.lua index 77bea578..333587fe 100644 --- a/config/yazi/plugins/archive.yazi/init.lua +++ b/config/yazi/plugins/archive.yazi/init.lua @@ -9,7 +9,7 @@ local function notify_error(message, urgency) end -- Check for windows -local is_windows = package.config:sub(1, 1) == "\\" +local is_windows = ya.target_family() == "windows" -- Make table of selected or hovered: path = filenames local selected_or_hovered = ya.sync(function() @@ -45,11 +45,20 @@ local function is_command_available(cmd) if cmd_exists then return true else - notify_error(string.format("%s not available", cmd), "error") return false end end +-- Archive command list --> string +local function find_binary(cmd_list) + for _, cmd in ipairs(cmd_list) do + if is_command_available(cmd) then + return cmd + end + end + return cmd_list[1] -- Return first command as fallback +end + -- Check if file exists local function file_exists(name) local f = io.open(name, "r") @@ -87,10 +96,11 @@ return { -- Use appropriate archive command local archive_commands = { ["%.zip$"] = { command = "zip", args = { "-r" } }, - ["%.7z$"] = { command = "7z", args = { "a" } }, + ["%.7z$"] = { command = { "7z", "7zz" }, args = { "a" } }, ["%.tar.gz$"] = { command = "tar", args = { "rpf" }, compress = "gzip" }, ["%.tar.xz$"] = { command = "tar", args = { "rpf" }, compress = "xz" }, ["%.tar.bz2$"] = { command = "tar", args = { "rpf" }, compress = "bzip2" }, + ["%.tar.zst$"] = { command = "tar", args = { "rpf" }, compress = "zstd", compress_args = { "--rm" } }, ["%.tar$"] = { command = "tar", args = { "rpf" } }, } @@ -116,6 +126,7 @@ return { compress = "7z", compress_args = { "a", "-tbzip2", "-sdel", output_name }, }, + ["%.tar.zst$"] = { command = "tar", args = { "rpf" }, compress = "zstd", compress_args = { "--rm" } }, ["%.tar$"] = { command = "tar", args = { "rpf" } }, } end @@ -131,6 +142,11 @@ return { end end + -- Check if archive command has multiple names + if type(archive_cmd) == "table" then + archive_cmd = find_binary(archive_cmd) + end + -- Check if no archive command is available for the extention if not archive_cmd then notify_error("Unsupported file extention", "error") @@ -139,11 +155,13 @@ return { -- Exit if archive command is not available if not is_command_available(archive_cmd) then + notify_error(string.format("%s not available", archive_cmd), "error") return end -- Exit if compress command is not available if archive_compress and not is_command_available(archive_compress) then + notify_error(string.format("%s compression not available", archive_compress), "error") return end diff --git a/config/yazi/plugins/augment-command.yazi/README.md b/config/yazi/plugins/augment-command.yazi/README.md index d13fd1c5..6cdd16e7 100644 --- a/config/yazi/plugins/augment-command.yazi/README.md +++ b/config/yazi/plugins/augment-command.yazi/README.md @@ -16,26 +16,16 @@ and the [fast-enter.yazi](https://github.com/ourongxing/fast-enter.yazi) plugin. - [Augmented commands](#augmented-commands) - [New commands](#new-commands) - [Usage](#usage) -- [Windows support?](#windows-support) - [Licence](#licence) ## Requirements -- [Yazi](https://github.com/sxyazi/yazi) v0.2.4+ -- [Unarchiver (unar)](https://theunarchiver.com/command-line) -- [ls](https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html#ls-invocation) -- Linux or macOS +- [Yazi](https://github.com/sxyazi/yazi) v0.3.0+ +- [`7z` or `7zz` command](https://github.com/p7zip-project/p7zip) +- [`file` command](https://www.darwinsys.com/file/) ## Installation -### Yazi v0.2.5 and before (manual installation) - -```sh -git clone https://github.com/hankertrix/augment-command.yazi ~/.config/yazi/plugins/augment-command.yazi -``` - -### Yazi nightly (latest Git commit) (package manager) - ```sh # Add the plugin ya pack -a hankertrix/augment-command @@ -49,24 +39,30 @@ ya pack -u ## Configuration -| Configuration | Values | Default | Description | -| ----------------------------------- | -------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `prompt` | `true` or `false` | `false` | Create a prompt to choose between hovered and selected items when both exist. If this option is disabled, selected items will only be operated on when the hovered item is selected, otherwise the hovered item will be the default item that is operated on. | -| `default_item_group_for_prompt` | `hovered`, `selected` or `none` | `hovered` | The default item group to operate on when the prompt is submitted without any value. `hovered` means the hovered item is operated on, `selected` means the selected items are operated on, and `none` just cancels the operation. | -| `smart_enter` | `true` or `false` | `true` | Use one command to open files or enter a directory. With this option set, the `enter` and `open` commands will both call the `enter` command when a directory is hovered and call the `open` command when a regular file is hovered. | -| `smart_paste` | `true` or `false` | `false` | Paste items into a directory without entering it. The behaviour is exactly the same as the [smart-paste tip on Yazi's documentation](https://yazi-rs.github.io/docs/tips#smart-paste). Setting this option to `false` will use the default `paste` behaviour. You can also enable smart pasting by passing the `--smart` flag to the paste command. | -| `enter_archives` | `true` or `false` | `true` | Automatically extract and enter archive files. This option requires [Unarchiver (unar)](https://theunarchiver.com/command-line) to be installed. | -| `extract_behaviour` | `overwrite`, `rename`, or `skip` | `skip` | Determines how unar deals with existing files when extracting an archive. `overwrite` results in unar overwriting existing files when extracting. `rename` results in unar renaming the new files with the same name as existing files. `skip` results in unar skipping files that have the same name as existing files. Use the `man unar` command for more information. | -| `must_have_hovered_item` | `true` or `false` | `true` | This option stops the plugin from executing any commands when there is no hovered item. | -| `skip_single_subdirectory_on_enter` | `true` or `false` | `true` | Skip directories when there is only one subdirectory and no other files when entering directories. This behaviour can be turned off by passing the `--no-skip` flag to the `enter` or `open` commands. | -| `skip_single_subdirectory_on_leave` | `true` or `false` | `true` | Skip directories when there is only one subdirectory and no other files when leaving directories. This behaviour can be turned off by passing the `--no-skip` flag to the `leave` command. | -| `ignore_hidden_items` | `true` or `false` | `false` | Ignore hidden items when determining whether a directory only has one subdirectory and no other items. Setting this option to `false` will mean that hidden items in a directory will stop the plugin from skipping the single subdirectory. | -| `wraparound_file_navigation` | `true` or `false` | `false` | Wrap around from the bottom to the top or from the top to the bottom when using the `arrow` command to navigate. | +| Configuration | Values | Default | Description | +| ----------------------------------- | ------------------------------------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `prompt` | `true` or `false` | `false` | Create a prompt to choose between hovered and selected items when both exist. If this option is disabled, selected items will only be operated on when the hovered item is selected, otherwise the hovered item will be the default item that is operated on. | +| `default_item_group_for_prompt` | `hovered`, `selected` or `none` | `hovered` | The default item group to operate on when the prompt is submitted without any value. This only takes effect if `prompt` is set to `true`, otherwise this option doesn't do anything. `hovered` means the hovered item is operated on, `selected` means the selected items are operated on, and `none` just cancels the operation. | +| `smart_enter` | `true` or `false` | `true` | Use one command to open files or enter a directory. With this option set, the `enter` and `open` commands will both call the `enter` command when a directory is hovered and call the `open` command when a regular file is hovered. | +| `smart_paste` | `true` or `false` | `false` | Paste items into a directory without entering it. The behaviour is exactly the same as the [smart-paste tip on Yazi's documentation](https://yazi-rs.github.io/docs/tips#smart-paste). Setting this option to `false` will use the default `paste` behaviour. You can also enable smart pasting by passing the `--smart` flag to the paste command. | +| `enter_archives` | `true` or `false` | `true` | Automatically extract and enter archive files. This option requires the [7z or 7zz command](https://github.com/p7zip-project/p7zip) to be present. | +| `extract_retries` | An integer, like `1`, `3`, `10`, etc. | `3` | This option determines how many times the plugin will retry opening an encrypted or password-protected archive when a wrong password is given. This value plus 1 is the total number of times the plugin will try opening an encrypted or password-protected archive. | +| `must_have_hovered_item` | `true` or `false` | `true` | This option stops the plugin from executing any commands when there is no hovered item. | +| `skip_single_subdirectory_on_enter` | `true` or `false` | `true` | Skip directories when there is only one subdirectory and no other files when entering directories. This behaviour can be turned off by passing the `--no-skip` flag to the `enter` or `open` commands. | +| `skip_single_subdirectory_on_leave` | `true` or `false` | `true` | Skip directories when there is only one subdirectory and no other files when leaving directories. This behaviour can be turned off by passing the `--no-skip` flag to the `leave` command. | +| `ignore_hidden_items` | `true` or `false` | `false` | Ignore hidden items when determining whether a directory only has one subdirectory and no other items. Setting this option to `false` will mean that hidden items in a directory will stop the plugin from skipping the single subdirectory. | +| `wraparound_file_navigation` | `true` or `false` | `false` | Wrap around from the bottom to the top or from the top to the bottom when using the `arrow` or `parent-arrow` command to navigate. | +| `sort_directories_first` | `true` or `false` | `true` | This option tells the plugin if you have sorted directories first in your [`yazi.toml` file](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first), located at `~/.config/yazi/yazi.toml` on Linux and macOS or `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` on Windows, where `USERNAME` is your Windows username. If you have set [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) to `true` in your [`yazi.toml` file](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first), set this option to `true` as well. If you have set [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) to `false` instead, set this option to `false` as well. This option only affects the `parent-arrow` command with `wraparound_file_navigation` set to `true`. If the [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) setting doesn't match the plugin's `sort_directories_first` setting, i.e. Yazi's `sort_dir_first` is `true` but the plugin's `sort_directories_first` is `false`, or Yazi's `sort_dir_first` is `false` but the plugin's `sort_directories_first` is `true`, the wraparound functionality of the `parent-arrow` command will not work properly and may act erratically. The default value of `sort_directories_first` follows Yazi's [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) default value, which is `true`. | -To configure this plugin, add the code below to your `~/.config/yazi/init.lua` file: +If you would like to use the default configuration, which is shown below, +you don't need to add anything to your `~/.config/yazi/init.lua` +file on Linux and macOS, or your +`C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua` +file on Windows, where `USERNAME` is your Windows username. ```lua --- ~/.config/yazi/init.lua +-- ~/.config/yazi/init.lua for Linux and macOS +-- C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua for Windows -- Using the default configuration require("augment-command"):setup({ @@ -75,36 +71,42 @@ require("augment-command"):setup({ smart_enter = true, smart_paste = false, enter_archives = true, - extract_behaviour = "skip", + extract_retries = 3, must_have_hovered_item = true, skip_single_subdirectory_on_enter = true, skip_single_subdirectory_on_leave = true, ignore_hidden_items = false, wraparound_file_navigation = false, + sort_directories_first = true, }) ``` -Note that you don't have to do this if you want to use the default configuration. -You also can leave out configuration options that you would like to be left as default, -for example: +However, if you would like to configure the plugin, you can add +your desired configuration options to your `~/.config/yazi/init.lua` file +on Linux and macOS, or your `C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua` +file on Windows, where `USERNAME` is your Windows username. +You can leave out configuration options that you would like to be left as default. +An example configuration is shown below: ```lua --- ~/.config/yazi/init.lua +-- ~/.config/yazi/init.lua for Linux and macOS +-- C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua for Windows -- Custom configuration require("augment-command"):setup({ prompt = true, default_item_group_for_prompt = "none", - extract_behaviour = "overwrite", + extract_retries = 5, ignore_hidden_items = true, wraparound_file_navigation = true, + sort_directories_first = false, }) ``` ## What about the commands are augmented? All commands that can operate on multiple files and directories, -like `open`, `rename` and `remove`, +like `open`, `rename`, `remove` and `shell`, as well as the new commands `editor` and `pager`, now determine an item group to operate on. By default, the command will operate on the hovered item, @@ -125,7 +127,7 @@ then it will operate on the selected items. ### Open (`open`) - When `smart_enter` is set to `true`, - calls the `enter` command when the hovered item is a directory. + it calls the `enter` command when the hovered item is a directory. - `--no-skip` flag, which only applies when `smart_enter` is used as it is passed to the `enter` command. More details about this flag can be found at the documentation @@ -134,21 +136,33 @@ then it will operate on the selected items. with support for skipping directories that contain only one subdirectory in the extracted archive. This can be disabled by setting `enter_archives` to `false` in the configuration. - This feature requires - [unarchiver (unar)](https://theunarchiver.com/command-line) - to be installed as well as the - [ls](https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html#ls-invocation) command. + This feature requires the + [`7z` or `7zz` command](https://github.com/p7zip-project/p7zip) + to be present to extract the archives. +- If the archive file contains only a single file, + the command will automatically extract it to the + current directory instead of creating a folder + for the contents of the archive. + If this extracted file is also an archive file, + the command will automatically + extract its contents before deleting it. + This feature requires the + [`file` command](https://www.darwinsys.com/file/) + to detect the mime type of the extracted file, + and to check whether it is an archive file or not. + This makes extracting binaries from + compressed tarballs much easier, as there's no need + to press a key twice to decompress and extract + the compressed tarballs. ### Enter (`enter`) - When `smart_enter` is set to `true`, - calls the `open` command when the hovered item is a file. + it calls the `open` command when the hovered item is a file. - Automatically skips directories that contain only one subdirectory when entering directories. This can be turned off by setting `skip_single_subdirectory_on_enter` to `false` in the configuration. - This feature requires the - [ls](https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html#ls-invocation) command. - `--no-skip` flag. It stops the plugin from skipping directories that contain only one subdirectory when entering directories, even when `skip_single_subdirectory_on_enter` is set to `true`. @@ -162,33 +176,21 @@ then it will operate on the selected items. This can be turned off by setting `skip_single_subdirectory_on_leave` to `false` in the configuration. - This feature requires the - [ls](https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html#ls-invocation) command. - `--no-skip` flag. It stops the plugin from skipping directories that contain only one subdirectory, even when `skip_single_subdirectory_on_leave` is set to `true`. - This allows you to set a key to navigate into directories + This allows you to set a key to navigate out of directories without skipping the directories that contain only one subdirectory. ### Rename (`rename`) -- Unfortunately, to use the augmented `rename` command, - you need to use the latest Git version of Yazi as - [this commit](https://github.com/sxyazi/yazi/commit/9961251248c74202d8310085102d5809c279757c) - adds the necessary `--hovered` flag. -- If you don't use the latest Git version of Yazi, - it just behaves like the provided `rename` command - and the prompts don't do anything. +- The `rename` command is augmented as stated in + [this section above](#what-about-the-commands-are-augmented). ### Remove (`remove`) -- Unfortunately, to use the augmented `remove` command, - you need to use the latest Git version of Yazi as - [this commit](https://github.com/sxyazi/yazi/commit/9961251248c74202d8310085102d5809c279757c) - adds the necessary `--hovered` flag. -- If you don't use the latest Git version of Yazi, - it just behaves like the provided `remove` command - and the prompts don't do anything. +- The `remove` command is augmented as stated in + [this section above](#what-about-the-commands-are-augmented). ### Paste (`paste`) @@ -197,6 +199,9 @@ then it will operate on the selected items. into a hovered directory without entering it. If the hovered item is not a directory, the command pastes in the current directory instead. + Otherwise, when `smart_paste` is set to `false`, + the `paste` command will behave like the default + `paste` command. - `--smart` flag to enable pasting in a hovered directory without entering the directory. This flag will cause the `paste` command to paste items @@ -204,6 +209,74 @@ then it will operate on the selected items. This allows you to set a key to use smart paste instead of using smart paste for every paste command. +### Shell (`shell`) + +- This command runs the shell command given with the augment stated in + [this section above](#what-about-the-commands-are-augmented). You should + only use this command if you need the plugin to determine a suitable + item group for the command to operate on. Otherwise, you should just + use the default `shell` command provided by Yazi. +- To use this command, the syntax is exactly the same as the default + `shell` command provided by Yazi. You just provide the command you want and + provide any Yazi shell variable, which is documented + [here](https://yazi-rs.github.io/docs/configuration/keymap/#manager.shell). + The plugin will automatically replace the shell variable you give + with the file paths for the item group before executing the command. +- You will also need to escape the quotes when giving the shell command + if you use the same quotes to quote the given arguments to the plugin. + For example, if you pass the arguments to the plugin with double quotes, + i.e. `--args="shell"`, you will have to escape the double quotes with a + backslash character, like shown below: + + ```toml + # ~/.config/yazi/keymap.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + + [[manager.prepend_keymap]] + on = [ "o" ] + run = 'plugin augment-command --args="shell \"$EDITOR $@\" --block --confirm"' + desc = "Open the editor" + ``` + +- Alternatively, you can use the triple single quote `'''` delimiter + for the run string and avoid the escaping the shell command altogether, + like the two examples below: + + ```toml + # ~/.config/yazi/keymap.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + + [[manager.prepend_keymap]] + on = [ "o" ] + run = '''plugin augment-command --args='shell "$EDITOR $@" --block --confirm'''' + desc = "Open the editor" + + [[manager.prepend_keymap]] + on = [ "i" ] + run = '''plugin augment-command --args="shell '$PAGER $@' --block --confirm"''' + desc = "Open the pager" + ``` + +- `--exit-if-directory` flag to stop the shell command given + from executing if the item group consists only of directories. + For example, if the item group is the hovered item, then + the shell command will not be executed if the hovered item + is a directory. If the item group is the selected items group, + then the shell command will not be executed if **all** + the selected items are directories. This behaviour comes + from it being used in the `pager` command. + The `pager` command is essentially: + + ```toml + # ~/.config/yazi/keymap.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + + [[manager.prepend_keymap]] + on = [ "i" ] + run = '''plugin augment-command --args="shell '$PAGER $@' --block --confirm --exit-if-directory"''' + desc = "Open the pager" + ``` + ### Arrow (`arrow`) - When `wraparound_file_navigation` is set to `true`, @@ -217,60 +290,129 @@ then it will operate on the selected items. - This command behaves like the `arrow` command, but in the parent directory. - It allows you to navigate the parent directory + It allows you to navigate in the parent directory without leaving the current directory. - When `wraparound_file_navigation` is set to `true`, this command will also wrap around from the bottom to the top or from top to the bottom when navigating in the parent directory. - For this feature to work, you will need the - [ls](https://www.gnu.org/software/coreutils/manual/html_node/ls-invocation.html#ls-invocation) command. - You will also need to have your directories - sorted first for this feature to work, - i.e. in your `~/.config/yazi/yazi.toml` file: + For this feature to work properly, the + [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) + option in your `~/.config/yazi/yazi.toml` file on Linux and macOS, + or your `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` + file on Windows, where `USERNAME` is your Windows username, + has to match the plugin's `sort_directories_first` option, + i.e. if you have set the + [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) + to `true` in your `~/.config/yazi/yazi.toml` file on Linux and macOS, + or your `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` on Windows, + like so: -```toml -# ~/.config/yazi/yazi.toml -[manager] -sort_dir_first = true -``` + ```toml + # ~/.config/yazi/yazi.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml on Windows + + [manager] + sort_dir_first = true + ``` + + Then `sort_directories_first` should be set to `true` + as well in your `~/.config/yazi/init.lua` file on Linux and macOS, + or your `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` + file on Windows, where `USERNAME` is your Windows username, like so: + + ```lua + -- ~/.config/yazi/init.lua on Linux and macOS + -- C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua on Windows + + require("augment-command"):setup({ + sort_directories_first = true + }) + ``` + + If your `~/.config/yazi/yazi.toml` file on Linux and macOS, or your + `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` file on Windows, + where `USERNAME` is your Windows username, has + [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) + set to `false`, like so: + + ```toml + # ~/.config/yazi/yazi.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml on Windows + + [manager] + sort_dir_first = false + ``` + + Then `sort_directories_first` should be set to `false` + as well in your `~/.config/yazi/init.lua` file on Linux and macOS, + or your `C:\Users\USERNAME\AppData\Roaming\yazi\config\yazi.toml` + file on Windows, where `USERNAME` is your Windows username, like so: + + ```lua + -- ~/.config/yazi/init.lua on Linux and macOS + -- C:\Users\USERNAME\AppData\Roaming\yazi\config\init.lua on Windows + + require("augment-command"):setup({ + sort_directories_first = false + }) + ``` + + The default value of `sort_directories_first` follows Yazi's + [`sort_dir_first`](https://yazi-rs.github.io/docs/configuration/yazi#manager.sort_dir_first) + default value, which is `true`. - You can also replicate this using this series of commands below, but it doesn't work as well, and doesn't support wraparound navigation: -```toml -# ~/.config/yazi/keymap.toml + ```toml + # ~/.config/yazi/keymap.toml on Linux and macOS + # C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows -# Use K to move up in the parent directory -[[manager.prepend_keymap]] -on = [ "K" ] -run = [ "leave", "arrow -1", "enter" ] -desc = "Move up in the parent directory" + # Use K to move up in the parent directory + [[manager.prepend_keymap]] + on = [ "K" ] + run = [ "leave", "arrow -1", "enter" ] + desc = "Move up in the parent directory" -# Use J to move down in the parent directory -[[manager.prepend_keymap]] -on = [ "J" ] -run = [ "leave", "arrow 1", "enter" ] -desc = "Move down in the parent directory" -``` + # Use J to move down in the parent directory + [[manager.prepend_keymap]] + on = [ "J" ] + run = [ "leave", "arrow 1", "enter" ] + desc = "Move down in the parent directory" + ``` ### Editor (`editor`) -- This command opens the default editor set by the `$EDITOR` environment variable. +- The `editor` command opens the default editor set by the `$EDITOR` environment variable. +- The command is also augmented as stated in + [this section above](#what-about-the-commands-are-augmented). ### Pager (`pager`) -- This command opens the default pager set by the `$PAGER` environment variable. +- The `pager` command opens the default pager set by the `$PAGER` environment variable. +- The command is also augmented as stated in + [this section above](#what-about-the-commands-are-augmented). +- The `pager` command will also skip opening directories, as the pager + cannot open directories and will error out. + Hence, the command will not do anything when the hovered item + is a directory, or if **all** the selected items are directories. + This makes the pager command less annoying as it will not + try to open a directory and then immediately fail with an error, + causing a flash and Yazi to send a notification. ## Usage Add the commands that you would like to use to your `keymap.toml` file, -located at `~/.config/yazi/keymap.toml`, -in this format: +located at `~/.config/yazi/keymap.toml` on Linux and macOS +and at `C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml` +on Windows, in this format: ```toml -# ~/.config/yazi/keymap.toml +# ~/.config/yazi/keymap.toml on Linux and macOS +# C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + [[manager.prepend_keymap]] on = [ "key" ] run = "plugin augment-command --args='command arguments --flags --options=42'" @@ -280,7 +422,9 @@ desc = "Description" For example, to use the augmented `enter` command: ```toml -# ~/.config/yazi/keymap.toml +# ~/.config/yazi/keymap.toml on Linux and macOS +# C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + [[manager.prepend_keymap]] on = [ "l" ] run = "plugin augment-command --args='enter'" @@ -290,7 +434,9 @@ desc = "Enter a directory and skip directories with only a single subdirectory" All the default arguments, flags and options provided by Yazi are also supported, for example: ```toml -# ~/.config/yazi/keymap.toml +# ~/.config/yazi/keymap.toml on Linux and macOS +# C:\Users\USERNAME\AppData\Roaming\yazi\config\keymap.toml on Windows + [[manager.prepend_keymap]] on = [ "k" ] run = "plugin augment-command --args='arrow -1'" @@ -324,10 +470,6 @@ For a full configuration example, you can take a look at [my `keymap.toml` file](https://github.com/hankertrix/Dotfiles/blob/master/.config/yazi/keymap.toml). -## Windows support? - -Pull requests for Windows support are welcome! - ## Licence This plugin is licenced under the GNU GPL v3 licence. diff --git a/config/yazi/plugins/augment-command.yazi/init.lua b/config/yazi/plugins/augment-command.yazi/init.lua index 2a22d823..3c38d6fe 100644 --- a/config/yazi/plugins/augment-command.yazi/init.lua +++ b/config/yazi/plugins/augment-command.yazi/init.lua @@ -1,7 +1,33 @@ -- Plugin to make some Yazi commands smarter -- Written in Lua 5.4 +-- The type for the arguments +---@alias Arguments table + +-- The type for the Command output +---@class (exact) CommandOutput +---@field stdout string +---@field stderr string +---@field status table { success: boolean, code: number } + +--- The type for the Url object +---@class (exact) Url +---@field frag string +---@field is_regular boolean +---@field is_search boolean +---@field is_archive boolean +---@field is_absolute boolean +---@field has_root boolean +---@field name function(): string|nil +---@field stem function(): string|nil +---@field join function(url: Url|string): Url +---@field parent function(): Url|nil +---@field starts_with function(url: Url|string): boolean +---@field ends_with function(url: Url|string): boolean +---@field strip_prefix function(url: Url|string): boolean + -- The enum for which group of items to operate on +---@enum ItemGroup local ItemGroup = { Hovered = "hovered", Selected = "selected", @@ -9,21 +35,8 @@ local ItemGroup = { Prompt = "prompt", } --- The enum for the archive extraction behaviour -local ExtractBehaviour = { - Overwrite = "overwrite", - Rename = "rename", - Skip = "skip", -} - --- The enum for the flags for the archive extraction behaviour -local ExtractBehaviourFlags = { - [ExtractBehaviour.Overwrite] = "-f", - [ExtractBehaviour.Rename] = "-r", - [ExtractBehaviour.Skip] = "-s", -} - -- The enum for the supported commands +---@enum SupportedCommands local Commands = { Open = "open", Enter = "enter", @@ -31,6 +44,7 @@ local Commands = { Rename = "rename", Remove = "remove", Paste = "paste", + Shell = "shell", Arrow = "arrow", ParentArrow = "parent-arrow", Editor = "editor", @@ -38,32 +52,56 @@ local Commands = { } -- The default configuration for the plugin +---@class (exact) Configuration +---@field prompt boolean +---@field default_item_group_for_prompt ItemGroup +---@field smart_enter boolean +---@field smart_paste boolean +---@field enter_archives boolean +---@field extract_retries number +---@field must_have_hovered_item boolean +---@field skip_single_subdirectory_on_enter boolean +---@field skip_single_subdirectory_on_leave boolean +---@field ignore_hidden_items boolean +---@field wraparound_file_navigation boolean +---@field sort_directories_first boolean +---@field extractor_command string local DEFAULT_CONFIG = { prompt = false, default_item_group_for_prompt = ItemGroup.Hovered, smart_enter = true, smart_paste = false, enter_archives = true, - extract_behaviour = ExtractBehaviour.Skip, + extract_retries = 3, must_have_hovered_item = true, skip_single_subdirectory_on_enter = true, skip_single_subdirectory_on_leave = true, ignore_hidden_items = false, wraparound_file_navigation = false, + sort_directories_first = true, } -- The default notification options for this plugin +---@class (exact) NotificationOptions +---@field title string +---@field timeout number +---@field content string +---@field level "info" | "warn" | "error" local DEFAULT_NOTIFICATION_OPTIONS = { title = "Augment Command Plugin", - timeout = 5.0 + timeout = 5.0, } -- The default input options for this plugin +---@class (exact) InputOptions +---@field position { x: number, y: number, w: number } +---@field title string local DEFAULT_INPUT_OPTIONS = { - position = { "top-center", y = 2, w = 50 } + position = { "top-center", y = 2, w = 50 }, } -- The table of input options for the prompt +---@enum InputOptionsTable local INPUT_OPTIONS_TABLE = { [ItemGroup.Hovered] = "(H/s)", [ItemGroup.Selected] = "(h/S)", @@ -71,46 +109,86 @@ local INPUT_OPTIONS_TABLE = { } -- The list of archive mime types +---@type string[] local ARCHIVE_MIME_TYPES = { "application/zip", "application/gzip", - "application/x-tar", - "application/x-bzip", - "application/x-bzip2", - "application/x-7z-compressed", - "application/x-rar", - "application/x-xz", + "application/tar", + "application/bzip", + "application/bzip2", + "application/7z-compressed", + "application/rar", + "application/xz", } -- The pattern to get the double dash from the front of the argument +---@type string local double_dash_pattern = "^%-%-" --- The pattern to get the parent directory of the current directory -local get_parent_directory_pattern = "(.*)/.*" +-- The pattern to get the mime type without the "x-" prefix +---@type string +local get_mime_type_without_x_prefix_pattern = "^(%a-)/x%-([%-%d%a]-)$" --- The pattern to get if a file path is a directory -local is_directory_pattern = "(.*)/$" +-- The pattern to get the information from an archive item +---@type string +local archive_item_info_pattern = "%s+([%.%a]+)%s+(%d+)%s+(%d+)%s+(.+)$" --- The pattern to get the filename of a file -local get_filename_pattern = "(.*)%.[^%.]+$" +-- The pattern to get the shell variables in a command +---@type string +local shell_variable_pattern = "[%$%%][%*@0]" +-- The pattern to match the bat command with the pager option passed +---@type string +local bat_command_with_pager_pattern = "%f[%a]bat%f[%A].*%-%-pager%s+" -- Function to merge tables. --- The tables given later in the argument list WILL OVERRIDE +-- +-- The key-value pairs of the tables given later +-- in the argument list WILL OVERRIDE -- the tables given earlier in the argument list. +-- +-- The list items in the table will be added in order, +-- with the items in the first table being added first, +-- and the items in the second table being added second, +-- and so on. +---@param ... table[] +---@return table local function merge_tables(...) + -- -- Initialise a new table local new_table = {} + -- Initialise the index variable + local index = 1 + -- Iterates over the tables given - for _, table in ipairs({...}) do + for _, table in ipairs({ ... }) do + -- -- Iterate over all of the keys and values for key, value in pairs(table) do + -- - -- Set the key in the new table to the value given - new_table[key] = value + -- If the key is a number, then add using the index + -- instead of the key. + -- This is to allow lists to be merged. + if type(key) == "number" then + -- + + -- Set the value mapped to the index + new_table[index] = value + + -- Increment the index + index = index + 1 + + -- Otherwise, the key isn't a number + else + -- + + -- Set the key in the new table to the value given + new_table[key] = value + end end end @@ -118,12 +196,16 @@ local function merge_tables(...) return new_table end - -- Function to check if a list contains a given value +---@param list any[] +---@param value any +---@return boolean local function list_contains(list, value) + -- -- Iterate over all of the items in the list for _, item in ipairs(list) do + -- -- If the item is equal to the given value, -- then return true @@ -134,20 +216,22 @@ local function list_contains(list, value) return false end - -- Function to split a string into a list +---@param given_string string +---@param separator string +---@return string[] local function string_split(given_string, separator) + -- -- If the separator isn't given, set it to the whitespace character - if separator == nil then - separator = "%s" - end + if separator == nil then separator = "%s" end -- Initialise the list of splitted strings local splitted_strings = {} -- Iterate over all of the strings found by pattern for string in string.gmatch(given_string, "([^" .. separator .. "]+)") do + -- -- Add the string to the list of splitted strings table.insert(splitted_strings, string) @@ -157,30 +241,47 @@ local function string_split(given_string, separator) return splitted_strings end +-- The function to trim a string +---@param string string +---@return string +local function string_trim(string) + -- + + -- Return the string with the whitespace characters + -- removed from the start and end + return string:match("^%s*(.-)%s*$") +end -- Function to parse the arguments given. -- This function takes the arguments passed to the entry function +---@param args string[] +---@return Arguments local function parse_args(args) + -- - -- The table of options to pass to ya.manager_emit - local options = {} + -- The table of arguments to pass to ya.manager_emit + ---@type table<(string|number), (string|number|boolean)> + local parsed_arguments = {} -- Iterates over the arguments given for index, argument in ipairs(args) do + -- -- If the index isn't 1, -- which means it is the arguments to the command given if index ~= 1 then + -- -- If the argument doesn't start with a double dash - if not argument:match(double_dash_pattern) then + if not argument:find(double_dash_pattern) then + -- -- Try to convert the argument to a number local number_argument = tonumber(argument) -- Add the argument to the list of options table.insert( - options, + parsed_arguments, number_argument and number_argument or argument ) @@ -189,19 +290,18 @@ local function parse_args(args) end -- Otherwise, remove the double dash from the front of the argument - local cleaned_argument = - argument:gsub(double_dash_pattern, "") + local cleaned_argument = argument:gsub(double_dash_pattern, "") -- Replace all of the dashes with underscores cleaned_argument = cleaned_argument:gsub("%-", "_") -- Split the arguments at the = character - local arg_name, arg_value = table.unpack( - string_split(cleaned_argument, "=") - ) + local arg_name, arg_value = + table.unpack(string_split(cleaned_argument, "=")) -- If the argument value is nil if arg_value == nil then + -- -- Set the argument name to the cleaned argument arg_name = cleaned_argument @@ -211,6 +311,7 @@ local function parse_args(args) -- Otherwise else + -- -- Try to convert the argument value to a number local number_arg_value = tonumber(arg_value) @@ -221,20 +322,23 @@ local function parse_args(args) end -- Add the argument name and value to the options - options[arg_name] = arg_value + parsed_arguments[arg_name] = arg_value end -- The label to continue the loop ::continue:: end - -- Return the table of options - return options + -- Return the table of arguments + return parsed_arguments end - -- Function to initialise the configuration +---@param state any +---@param opts Configuration +---@return Configuration local initialise_config = ya.sync(function(state, opts) + -- -- Merge the default configuration with the given one -- and set it to the state. @@ -244,55 +348,148 @@ local initialise_config = ya.sync(function(state, opts) return state.config end) +-- The function to try if a shell command exists +---@param shell_command string +---@return boolean +local function shell_command_exists(shell_command) + -- + + -- Initialise the null output + local null_output = "/dev/null" + + -- If the OS is Windows + if ya.target_family() == "windows" then + -- + + -- Set the null output to the NUL device + null_output = "NUL" + end + + -- Get whether the shell command is successfully executed + -- + -- "1> /dev/null" redirects the standard output + -- of the shell command to /dev/null, which accepts + -- and discards all input and produces no output. + -- + -- "2>&1" redirects the standard error to the file + -- descriptor of the standard output, which is the + -- /dev/null file or the NUL device on Windows, + -- which accepts and discards + -- all input and produces no output. + -- + -- The full thing, "1> /dev/null 2>&1" just makes sure + -- the shell command doesn't produce any output when executed. + -- + -- The equivalent command on Windows is "1> NUL 2>&1". + -- + -- https://stackoverflow.com/questions/10508843/what-is-dev-null-21 + -- https://stackoverflow.com/questions/818255/what-does-21-mean + -- https://www.gnu.org/software/bash/manual/html_node/Redirections.html + local successfully_executed = + os.execute(shell_command .. " 1> " .. null_output .. " 2>&1") + + -- If the command was not successfully executed, + -- set the successfully executed variable to false + if not successfully_executed then successfully_executed = false end + + -- Return the result of the os.execute command + return successfully_executed +end + +-- The function to initialise the plugin +---@param opts Configuration|nil +---@return Configuration +local function initialise_plugin(opts) + -- + + -- Initialise the extractor command + local extractor_command = "7z" + + -- If the 7zz command exists + if shell_command_exists("7zz") then + -- + + -- Set the 7z command to the 7zz command + extractor_command = "7zz" + end + + -- Initialise the configuration object + local config = initialise_config(merge_tables({ + extractor_command = extractor_command, + }, opts)) + + -- Return the configuration object + return config +end + +-- Function to check if a given mime type is an archive +---@param mime_type string|nil The mime type of the file +---@return boolean is_archive Whether the mime type is an archive +local function is_archive_mime_type(mime_type) + -- + + -- If the mime type is nil, return false + if not mime_type then return false end + + -- Trim the whitespace from the mime type + mime_type = string_trim(mime_type) + + -- Remove the "x-" prefix from the mime type + mime_type = mime_type:gsub(get_mime_type_without_x_prefix_pattern, "%1/%2") + + -- Get if the mime type is an archive + local is_archive = list_contains(ARCHIVE_MIME_TYPES, mime_type) + + -- Return if the mime type is an archive + return is_archive +end -- Function to get the configuration from an async function +---@param state any +---@return Configuration local get_config = ya.sync(function(state) + -- -- Returns the configuration object return state.config end) - -- Function to get the current working directory +---@param _ any +---@return string local get_current_directory = ya.sync(function(_) return tostring(cx.active.current.cwd) end) - --- Function to get the parent working directory -local get_parent_directory = ya.sync(function(_) - - -- Get the parent directory - local parent_directory = cx.active.parent - - -- If the parent directory doesn't exist, - -- return nil - if not parent_directory then return nil end - - -- Otherwise, return the path of the parent directory - return tostring(parent_directory.cwd) -end) - - --- Function to get the hovered item path -local get_hovered_item_path = ya.sync(function(_) +-- Function to get the path of the hovered item +---@param _ any +---@param quote boolean +---@return string|nil +local get_path_of_hovered_item = ya.sync(function(_, quote) + -- -- Get the hovered item local hovered_item = cx.active.current.hovered - -- If the hovered item exists - if hovered_item then + -- If there is no hovered item, exit the function + if not hovered_item then return end - -- Return the path of the hovered item - return tostring(cx.active.current.hovered.url) + -- Convert the url of the hovered item to a string + local hovered_item_path = tostring(cx.active.current.hovered.url) - -- Otherwise, return nil - else return nil end + -- If the quote flag is passed, + -- then quote the path of the hovered item + if quote then hovered_item_path = ya.quote(hovered_item_path) end + + -- Return the path of the hovered item + return hovered_item_path end) - -- Function to get if the hovered item is a directory +---@param _ any +---@return boolean local hovered_item_is_dir = ya.sync(function(_) + -- -- Get the hovered item local hovered_item = cx.active.current.hovered @@ -301,26 +498,64 @@ local hovered_item_is_dir = ya.sync(function(_) return hovered_item and hovered_item.cha.is_dir end) - -- Function to get if the hovered item is an archive +---@param _ any +---@return boolean local hovered_item_is_archive = ya.sync(function(_) + -- -- Get the hovered item local hovered_item = cx.active.current.hovered -- Return if the hovered item exists and is an archive - return hovered_item and list_contains( - ARCHIVE_MIME_TYPES, hovered_item:mime() - ) + return hovered_item + and is_archive_mime_type(hovered_item:mime()) end) +-- Function to get the paths of the selected items +---@param _ any +---@param quote boolean +---@return string[]|nil +local get_paths_of_selected_items = ya.sync(function(_, quote) + -- + + -- Get the selected items + local selected_items = cx.active.selected + + -- If there are no selected items, exit the function + if #selected_items == 0 then return end + + -- Initialise the list of paths of the selected items + local paths_of_selected_items = {} + + -- Iterate over the selected items + for _, item in pairs(selected_items) do + -- + + -- Convert the url of the item to a string + local item_path = tostring(item) + + -- If the quote flag is passed, + -- then quote the path of the item + if quote then item_path = ya.quote(item_path) end + + -- Add the path of the item to the list of paths + table.insert(paths_of_selected_items, item_path) + end + + -- Return the list of paths of the selected items + return paths_of_selected_items +end) -- Function to choose which group of items to operate on. -- It returns ItemGroup.Hovered for the hovered item, -- ItemGroup.Selected for the selected items, -- and ItemGroup.Prompt to tell the calling function -- to prompt the user. +---@param state any +---@return ItemGroup|nil local get_item_group_from_state = ya.sync(function(state) + -- -- Get the hovered item local hovered_item = cx.active.current.hovered @@ -330,21 +565,27 @@ local get_item_group_from_state = ya.sync(function(state) -- If there is no hovered item if not hovered_item then + -- -- If there are no selected items, exit the function - if no_selected_items then return + if no_selected_items then + return -- Otherwise, if the configuration is set to have a hovered item, -- exit the function - elseif state.config.must_have_hovered_item then return + elseif state.config.must_have_hovered_item then + return -- Otherwise, return the enum for the selected items - else return ItemGroup.Selected end + else + return ItemGroup.Selected + end -- Otherwise, there is a hovered item -- and if there are no selected items, -- return the enum for the hovered item. - elseif no_selected_items then return ItemGroup.Hovered + elseif no_selected_items then + return ItemGroup.Hovered -- Otherwise if there are selected items and the user wants a prompt, -- then tells the calling function to prompt them @@ -353,15 +594,19 @@ local get_item_group_from_state = ya.sync(function(state) -- Otherwise, if the hovered item is selected, -- then return the enum for the selected items - elseif hovered_item:is_selected() then return ItemGroup.Selected + elseif hovered_item:is_selected() then + return ItemGroup.Selected -- Otherwise, return the enum for the hovered item - else return ItemGroup.Hovered end + else + return ItemGroup.Hovered + end end) - -- Function to prompt the user for their desired item group +---@return ItemGroup|nil local function prompt_for_desired_item_group() + -- -- Get the configuration local config = get_config() @@ -373,65 +618,109 @@ local function prompt_for_desired_item_group() local input_options = INPUT_OPTIONS_TABLE[default_item_group] -- If the default item group is None, then set it to nil - if default_item_group == ItemGroup.None then - default_item_group = nil - end + if default_item_group == ItemGroup.None then default_item_group = nil end -- Prompt the user for their input local user_input, event = ya.input(merge_tables(DEFAULT_INPUT_OPTIONS, { - title = "Operate on hovered or selected items? " .. input_options + title = "Operate on hovered or selected items? " .. input_options, })) -- Lowercase the user's input user_input = user_input:lower() -- If the user did not confirm the input, exit the function - if event ~= 1 then return + if event ~= 1 then + return -- Otherwise, if the user's input starts with "h", -- return the item group representing the hovered item - elseif user_input:find("^h") then return ItemGroup.Hovered + elseif user_input:find("^h") then + return ItemGroup.Hovered -- Otherwise, if the user's input starts with "s", -- return the item group representing the selected items - elseif user_input:find("^s") then return ItemGroup.Selected + elseif user_input:find("^s") then + return ItemGroup.Selected -- Otherwise, return the default item group - else return default_item_group end + else + return default_item_group + end end - -- Function to get the item group +---@return ItemGroup|nil local function get_item_group() + -- -- Get the item group from the state local item_group = get_item_group_from_state() -- If the item group isn't the prompt one, -- then return the item group immediately - if item_group ~= ItemGroup.Prompt then return item_group + if item_group ~= ItemGroup.Prompt then + return item_group -- Otherwise, prompt the user for the desired item group - else return prompt_for_desired_item_group() end + else + return prompt_for_desired_item_group() + end end +-- The function to get all the items in the given directory +---@param directory string +---@param ignore_hidden_items boolean +---@param directories_only boolean|nil +---@return string[] +local function get_directory_items( + directory, + ignore_hidden_items, + directories_only +) + -- --- The ls command to get the items in the directory -local function ls_command(directory, ignore_hidden_items) - return Command("ls") - :args({ - directory, - ignore_hidden_items and "-1p" or "-1pA", - "--group-directories-first", - }) - :stdout(Command.PIPED) - :stderr(Command.PIPED) - :output() + -- Initialise the list of directory items + local directory_items = {} + + -- Read the contents of the directory + local directory_contents, _ = fs.read_dir(Url(directory), {}) + + -- If there are no directory contents, + -- then return the empty list of directory items + if not directory_contents then return directory_items end + + -- Iterate over the directory contents + for _, item in ipairs(directory_contents) do + -- + + -- If the ignore hidden items flag is passed + -- and the item is a hidden item, + -- then continue the loop + if ignore_hidden_items and item.cha.is_hidden then goto continue end + + -- If the directories only flag is passed + -- and the item is not a directory, + -- then continue the loop + if directories_only and not item.cha.is_dir then goto continue end + + -- Otherwise, add the item path to the list of directory items + table.insert(directory_items, tostring(item.url)) + + -- The continue label to continue the loop + ::continue:: + end + + -- Return the list of directory items + return directory_items end - -- Function to skip child directories with only one directory +---@param args Arguments +---@param config Configuration +---@param initial_directory string +---@return nil local function skip_single_child_directories(args, config, initial_directory) + -- -- If the user doesn't want to skip single subdirectories on enter, -- or one of the arguments passed is no skip, @@ -445,41 +734,858 @@ local function skip_single_child_directories(args, config, initial_directory) -- Start an infinite loop while true do + -- - -- Run the ls command to get the items in the directory - local output, _ = ls_command(directory, config.ignore_hidden_items) - - -- If there is no output, then break out of the loop - if not output then break end - - -- Get the list of items in the directory - local directory_items = string_split(output.stdout, "\n") + -- Get all the items in the current directory + local directory_items = + get_directory_items(directory, config.ignore_hidden_items) -- If the number of directory items is not 1, - -- then break out of the loop + -- then break out of the loop. if #directory_items ~= 1 then break end - -- Otherwise, get the item in the directory + -- Otherwise, get the directory item local directory_item = table.unpack(directory_items) - -- Match the directory item against the pattern to - -- check if it is a directory - directory_item = directory_item:match(is_directory_pattern) + -- Get the cha object of the directory item + -- and don't follow symbolic links + local directory_item_cha = fs.cha(Url(directory_item), false) - -- If the directory item isn't a directory, break the loop - if directory_item == nil then break end + -- If the directory item is not a directory, + -- break the loop + if not directory_item_cha.is_dir then break end -- Otherwise, set the directory to the inner directory - directory = directory .. "/" .. directory_item + directory = directory_item end -- Emit the change directory command to change to the directory variable ya.manager_emit("cd", { directory }) end +-- The function to check if an archive is password protected +---@param command_error_string string +---@return boolean +local function archive_is_encrypted(command_error_string) + -- + + -- Return true if the string contains the word "wrong password", + -- and false otherwise + if command_error_string:lower():find("wrong password", 1, true) then + return true + else + return false + end +end + +-- The function to test the password on the archive +-- without actually extracting the archive +---@param archive_path string +---@param config Configuration +---@param password string +---@return CommandOutput, integer +local function test_archive_password(archive_path, config, password) + -- + + -- Return the command to test the password on the archive + return Command(config.extractor_command) + :args({ + + -- Test the archive + "t", + + -- Pass the password to the command + "-p" .. password, + + -- The archive file to test + archive_path, + }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() +end + +-- The function to handle retrying the extractor command +-- +-- The extractor command is a function that takes +-- two arguments, first being the password to the archive, +-- and the second being the configuration object. +-- It returns the output and the err +-- from the Command:output() function. +-- +-- The initial password is the password given to the extractor command +-- and the test encryption is to test the archive password without +-- actually executing the given extractor command. +---@param extractor_command function +---@param config Configuration +---@param initial_password string|nil +---@param test_encryption boolean|nil +---@param archive_path string|nil +---@return boolean successful Whether the extraction was successful +---@return string|nil error_message An error message for unsuccessful extracts +---@return string|nil stdout The standard output of the extractor command +---@return string|nil correct_password The correct password to the archive +local function retry_extractor( + extractor_command, + config, + initial_password, + test_encryption, + archive_path +) + -- + + -- Initialise the password to the initial password + -- or an empty string if it's not given + local password = initial_password or "" + + -- Initialise the test encryption flag to false if it is not given + test_encryption = test_encryption or false + + -- Initialise the archive path to the given archive path + -- or an empty string if it's not given + archive_path = archive_path or "" + + -- If the archive path is empty, + -- set the test encryption flag to false + if string.len(string_trim(archive_path)) < 1 then + test_encryption = false + end + + -- Initialise the error message from the archive extractor + local error_message = "" + + -- Initialise the number of tries + -- to the number of retries plus 1 + local total_number_of_tries = config.extract_retries + 1 + + -- Iterate over the number of times to try the extraction + for tries = 0, total_number_of_tries do + -- + + -- Initialise the output and error to nil + local output, err = nil, nil + + -- If the test encryption flag is true + if test_encryption then + -- + + -- Call the function to test the encryption of the archive + output, err = test_archive_password(archive_path, config, password) + + -- Otherwise, execute the extractor command + else + -- + + -- Execute the extractor command + output, err = extractor_command(password, config) + end + + -- If there is no output + -- then return false, the error code as a string, + -- nil for the output, and nil for the password + if not output then return false, tostring(err), nil, nil end + + -- If the test encryption flag is true and the output status code is 0 + if test_encryption and output.status.code == 0 then + -- + + -- Actually execute the extractor command + output, err = extractor_command(password, config) + end + + -- If the output was 0, which means the extractor command was successful + if output.status.code == 0 then + -- + + -- Initialise the correct password to nil + local correct_password = nil + + -- If the password is not empty, + -- then set the correct password to the password + if string.len(string_trim(password)) > 0 then + correct_password = password + end + + -- Return true, nil for the error message, + -- the standard output of the output, + -- and the correct password + return true, nil, output.stdout, correct_password + end + + -- Set the error message to the standard error + -- from the archive extractor + error_message = output.stderr + + -- If the command failed for some other reason other + -- than the archive being encrypted, then return false, + -- the error message, the standard output of the output, + -- and nil for the password to the archive + if + not ( + output.status.code == 2 and archive_is_encrypted(output.stderr) + ) + then + return false, error_message, output.stdout, nil + end + + -- If it is the last try, then return false + -- and the error message, the standard output of the output, + -- and nil for the password to the archive. + if tries == total_number_of_tries then + return false, error_message, output.stdout, nil + end + + -- Initialise the prompt for the password + local password_prompt = "Wrong password, please enter another password:" + + -- If this is the first time opening the archive, + -- which means the number of tries is 0, + -- then ask the user for the password + -- instead of giving the wrong password message. + if tries == 0 then + password_prompt = "Archive is encrypted, please enter the password:" + end + + -- Ask the user for the password + local user_input, event = ya.input(merge_tables(DEFAULT_INPUT_OPTIONS, { + title = password_prompt, + })) + + -- If the user has confirmed the input, + -- set the password to the user's input + if event == 1 then + password = user_input + + -- Otherwise, return false, the error message, + -- the standard output of the output, + -- and nil for the password to the archive + -- as the user has cancelled the prompt, + -- or an unknown error has occurred + else + return false, error_message, output.stdout, nil + end + end + + -- If all the tries have been exhausted, + -- then return false, the error message + -- and nil + return false, error_message, nil, nil +end + +-- The command to list the items in an archive +---@param archive_path string +---@param config Configuration +---@param password string|nil +---@param remove_headers boolean|nil +---@param show_details boolean|nil +---@return CommandOutput, integer +local function list_archive_items_command( + archive_path, + config, + password, + remove_headers, + show_details +) + -- + + -- Initialise the password to an empty string if it's not given + password = password or "" + + -- Initialise the remove headers flag to false if it's not given + remove_headers = remove_headers or false + + -- Initialise the show details flag to false if it's not given + show_details = show_details or false + + -- Initialise the arguments for the command + local arguments = { + + -- List the items in the archive + "l", + + -- Pass the password to the command + "-p" .. password, + } + + -- If the remove headers flag is passed + if remove_headers then + -- + + -- Add the switch to remove the headers (undocumented switch) + table.insert(arguments, "-ba") + end + + -- If the show details flag is passed + if show_details then + -- + + -- Add the switch to show the details + table.insert(arguments, "-slt") + end + + -- Add the archive path to the arguments + table.insert(arguments, archive_path) + + -- Return the result of the command to list the items in the archive + return Command(config.extractor_command) + :args(arguments) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() +end + +-- The function to get the items in the archive. +---@param archive_path string The path to the archive file +---@param config Configuration The configuration object +---@param files_only boolean|nil Whether to only get the files in the archive +---@return string[] archive_items The list of archive items +---@return string[] directories The list of directories in the archive +---@return string|nil error_message The error message for an incorrect password +---@return string|nil correct_password The correct password to the archive +local function get_archive_items(archive_path, config, files_only) + -- + + -- Initialise the files only flag to false if it's not given + files_only = files_only or false + + -- The function to list the items in the archive + local function list_items_in_archive(password, configuration, _) + return list_archive_items_command( + archive_path, + configuration, + password, + true + ) + end + + -- Initialise the list of archive items + ---@type string[] + local archive_items = {} + + -- Initialise the list of directories + ---@type string[] + local directories = {} + + -- Call the function to retry the extractor command + -- with the list items in the archive function + local successful, error_message, output, password = + retry_extractor(list_items_in_archive, config) + + -- If the extractor command was not successful, + -- or the output was nil, + -- then return the empty list of archive items, + -- the empty list of directories in the archive, + -- the error message, and nil as the correct password + if not successful or not output then + return archive_items, directories, error_message, nil + end + + -- Otherwise, split the output at the newline character + local output_lines = string_split(output, "\n") + + -- Iterate over the lines of the output + for _, line in ipairs(output_lines) do + -- + + -- Get the information about the archive item from the line. + -- The information is in the format: + -- Attributes, Size, Compressed Size, File Path + local attributes, _, _, file_path = + line:match(archive_item_info_pattern) + + -- If the file path doesn't exist, then continue the loop + if not file_path then goto continue end + + -- If the attributes of the item starts with a "D", + -- which means the item is a directory + if attributes and attributes:find("^D") then + -- + + -- Add the directory to the list of directories + table.insert(directories, file_path) + + -- Continue the loop if only files are wanted + if files_only then goto continue end + end + + -- Otherwise, add the file path to the list of archive items + table.insert(archive_items, file_path) + + -- The continue label to continue the loop + ::continue:: + end + + -- Return the list of archive items, the list of directories, + -- nil for the error message and the correct password + return archive_items, directories, nil, password +end + +-- Function to get a temporary name. +-- The code is taken from Yazi's source code. +---@param file_path string +---@return string +local function get_temporary_name(file_path) + return ".tmp_" + .. ya.md5(string.format("extract//%s//%.10f", file_path, ya.time())) +end + +-- Function to get a temporary directory url +-- for the given file path +---@param file_path string +---@return Url|nil +local function get_temporary_directory_url(file_path) + -- + + -- Get the parent directory of the file path + local parent_directory = Url(file_path):parent() + + -- If the parent directory doesn't exist, then return nil + if not parent_directory then return nil end + + -- Otherwise, create the temporary directory path + local temporary_directory_url = + fs.unique_name(parent_directory:join(get_temporary_name(file_path))) + + -- Return the temporary directory path + return temporary_directory_url +end + +-- The extract command to extract an archive +---@param archive_path string +---@param destination_directory_path string +---@param config Configuration +---@param password string|nil +---@param extract_files_only boolean|nil +---@return CommandOutput, integer +local function extract_command( + archive_path, + destination_directory_path, + config, + password, + extract_files_only +) + -- + + -- Initialise the password to an empty string if it's not given + password = password or "" + + -- Initialise the extract files only flag to false if it's not given + extract_files_only = extract_files_only or false + + -- Initialise the extraction mode to use. + -- By default, it extracts the archive with + -- full paths, which keeps the archive structure. + local extraction_mode = "x" + + -- If the extract files only flag is passed + if extract_files_only then + -- + + -- Use the regular extract, + -- without the full paths, which will move + -- all files in the archive into the current directory + -- and ignore the archive folder structure. + extraction_mode = "e" + end + + -- Initialise the arguments for the command + local arguments = { + + -- The extraction mode + extraction_mode, + + -- Assume yes to all prompts + "-y", + + -- Configure the extraction behaviour to rename + "-aou", + + -- Pass the password to the command + "-p" .. password, + + -- The archive file to extract + archive_path, + + -- The destination directory path + "-o" .. destination_directory_path, + } + + -- Return the command to extract the archive + return Command(config.extractor_command) + :args(arguments) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() +end + +-- The function to get the mime type of a file +---@param file_path string +---@return string +local function get_mime_type(file_path) + -- + + -- Get the output of the file command + local output, _ = Command("file") + :args({ + + -- Don't prepend file names to the output + "-b", + + -- Print the mime type of the file + "--mime-type", + + -- The file path to get the mime type of + file_path, + }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :output() + + -- If there is no output, then return an empty string + if not output then return "" end + + -- Otherwise, get the mime type from the standard output + local mime_type = string_trim(output.stdout) + + -- Return the mime type + return mime_type +end + +-- The function to check if a file is an archive +---@param file_path string +---@return boolean +local function is_archive_file(file_path) + -- + + -- Initialise the is archive variable to false + local is_archive = false + + -- Call the function to get the mime type of the file + local mime_type = get_mime_type(file_path) + + -- Set the is archive variable + is_archive = is_archive_mime_type(mime_type) + + -- Return the is archive variable + return is_archive +end + +-- The function to clean up the temporary directory +-- after extracting an archive. +---@param temporary_directory_url Url +---@param removal_mode "dir" | "dir_all" | "dir_clean" +---@param ... any +---@return ... any +local function clean_up_temporary_directory( + temporary_directory_url, + removal_mode, + ... +) + -- + + -- Remove the temporary directory + fs.remove(removal_mode, temporary_directory_url) + + -- Return the given return values + return ... +end + +-- The function to move extracted items out of the temporary directory +---@param archive_url Url +---@param temporary_directory_url Url +---@return boolean move_successful A boolean if the move was successful +---@return string|nil error_message An error message for unsuccessful extracts +---@return string|nil extracted_items_path The path of the extracted item +local function move_extracted_items_to_archive_parent_directory( + archive_url, + temporary_directory_url +) + -- + + -- Initialise whether or not the move is successful to false + local move_successful = false + + -- Initialise the path of the extracted items + local extracted_items_path = nil + + -- Get the extracted items in the directory + -- containing the extracted items. + -- There is a limit of 2 as there should only be + -- a single item in the directory. + local extracted_items = fs.read_dir(temporary_directory_url, { limit = 2 }) + + -- If the extracted items doesn't exist, + -- clean up the temporary directory and + -- return that the move successful variable + -- the error message, and the extracted item path + if not extracted_items then + return clean_up_temporary_directory( + temporary_directory_url, + "dir_all", + move_successful, + "Failed to read the temporary directory", + extracted_items_path + ) + end + + -- If there are no extracted items, + -- clean up the temporary directory and + -- return that the move successful variable + -- the error message, and the extracted item path + if #extracted_items == 0 then + return clean_up_temporary_directory( + temporary_directory_url, + "dir", + move_successful, + "No files extracted from the archive", + extracted_items_path + ) + end + + -- Get the parent directory url of the archive + local parent_directory_url = archive_url:parent() + + -- If the parent directory url is nil, + -- then return the move successful variable, + -- the error message, and the extracted item path + if not parent_directory_url then + return clean_up_temporary_directory( + temporary_directory_url, + "dir_all", + move_successful, + "Parent directory doesn't exist", + extracted_items_path + ) + end + + -- Get the first extracted item + local first_extracted_item = table.unpack(extracted_items) + + -- Get the url of the first extracted item + local first_extracted_item_url = first_extracted_item.url + + -- Initialise the variable to + -- store whether there is only + -- a single file in the archive + local only_one_item_in_archive = false + + -- Initialise the target directory url to move the extracted items to, + -- which is the parent directory of the archive + -- joined with the file name of the archive without the extension + local target_url = parent_directory_url:join(archive_url:stem()) + + -- If there is only one item in the archive + if #extracted_items == 1 then + -- + + -- Set the only one item in archive variable to true + only_one_item_in_archive = true + + -- Set the target url to the parent directory of the archive + -- joined with the file name of the extracted item + target_url = parent_directory_url:join(first_extracted_item_url:name()) + end + + -- Get a unique name for the target url + target_url = fs.unique_name(target_url) + + -- If the target url is nil somehow, + -- clean up the temporary directory and + -- return the move successful variable, + -- the error message and the extracted item path + if not target_url then + return clean_up_temporary_directory( + temporary_directory_url, + "dir_all", + move_successful, + "Failed to get a unique name for to move the extracted items to", + extracted_items_path + ) + end + + -- Set the extracted items path to the target path + extracted_items_path = tostring(target_url) + + -- Initialise the error message to nil + local error_message = nil + + -- If there is only one item in the archive + if only_one_item_in_archive then + -- + + -- Move the item to the target path + move_successful, error_message = + os.rename(tostring(first_extracted_item_url), extracted_items_path) + + -- Otherwise + else + -- + + -- Rename the temporary directory itself to the target path + move_successful, error_message = + os.rename(tostring(temporary_directory_url), extracted_items_path) + end + + -- Clean up the temporary directory + -- and return if the move was successful + -- the error message and the extracted item path + return clean_up_temporary_directory( + temporary_directory_url, + move_successful and "dir" or "dir_all", + move_successful, + error_message, + extracted_items_path + ) +end + +--- The function to extract an archive. +--- This function returns 2 values: +--- 1. A boolean to indicate if the extraction of the archive was successful +--- 2. An error message if the extraction was unsuccessful +--- 3. The file path indicating the directory to change to, which can be nil +---@param archive_path string +---@param config Configuration +---@return boolean successful A boolean indicating extraction success +---@return string|nil error_message An error message if extraction failed +---@return string|nil extracted_items_path The path of the extracted items +local function extract_archive(archive_path, config) + -- + + -- Initialise the extract files only flag to false + local extract_files_only = false + + -- Initialise the successful variable to false + local successful = false + + -- Initialise the error message to nil + local error_message = nil + + -- Get the list of archive items, the error message and the password + local archive_items, archive_directories, archive_error, correct_password = + get_archive_items(archive_path, config, true) + + -- Initialise the extracted items path to nil + local extracted_items_path = nil + + -- If there are no files in the archive, + -- then return the successful variable, + -- the error message, and the extracted items path + if #archive_items == 0 then + return successful, archive_error, extracted_items_path + end + + -- Otherwise, if the number of archive items is 1, + -- and the number of directories in the archive is 0, + -- then set the files only flag to true + if #archive_items == 1 and #archive_directories == 0 then + extract_files_only = true + end + + -- Get the url of the temporary directory + local temporary_directory_url = get_temporary_directory_url(archive_path) + + -- If the temporary directory url is nil, + -- then return the successful variable, an error message + -- saying a path for the temporary directory + -- cannot be determined, and the extracted items path + if not temporary_directory_url then + return successful, + "Failed to determine a path for the temporary directory", + extracted_items_path + end + + -- Get the url of the archive + local archive_url = Url(archive_path) + + -- Get the name of the archive + local archive_name = archive_url:stem() + + -- If the archive name is nil, + -- then return the successful variable, + -- an error message saying + -- that the archive file name is somehow empty, + -- and the extracted items path + if not archive_name then + return successful, "Archive file name is empty", extracted_items_path + end + + -- Create the extractor command + local function extractor_command(password, configuration) + return extract_command( + archive_path, + tostring(temporary_directory_url), + configuration, + password, + extract_files_only + ) + end + + -- Call the function to retry the extractor command + successful, error_message, _, _ = retry_extractor( + extractor_command, + config, + correct_password, + true, + archive_path + ) + + -- If the extraction was not successful, + -- then return whether the extraction was successful, + -- the error message and the extracted items path + if not successful then + return successful, error_message, extracted_items_path + end + + -- Otherwise, move the extracted items + -- to the parent directory of the archive + successful, error_message, extracted_items_path = + move_extracted_items_to_archive_parent_directory( + archive_url, + temporary_directory_url + ) + + -- If the extract files only flag is false, + -- then return whether the extraction was successful, + -- the error message and the extracted items path + if not extract_files_only or not extracted_items_path then + return successful, error_message, extracted_items_path + end + + -- If the item is not an archive + -- then return whether the extraction was successful, + -- the error message and the extract directory + if not is_archive_file(extracted_items_path) then + return successful, error_message, extracted_items_path + end + + -- Save the extracted archive path + local extracted_archive_path = extracted_items_path + + -- Extract the archive item + successful, error_message, extracted_items_path = + extract_archive(extracted_archive_path, config) + + -- If the extraction was not successful, + -- then return whether the extraction was successful, + -- the error message and the extracted items path + if not successful then + return successful, error_message, extracted_items_path + end + + -- Remove the archive after extracting it successfully + fs.remove("file", Url(extracted_archive_path)) + + -- Return the result of the extraction + return successful, error_message, extracted_items_path +end -- Function to handle the open command +---@param args Arguments +---@param config Configuration +---@param command_table CommandTable +---@return nil local function handle_open(args, config, command_table) + -- -- Call the function to get the item group local item_group = get_item_group() @@ -490,6 +1596,7 @@ local function handle_open(args, config, command_table) -- If the item group is the selected items, -- then execute the command and exit the function if item_group == ItemGroup.Selected then + -- -- Emit the command and exit the function return ya.manager_emit("open", args) @@ -501,6 +1608,7 @@ local function handle_open(args, config, command_table) -- If the hovered item is a directory if hovered_item_is_dir() then + -- -- If smart enter is wanted, -- calls the function to enter the directory @@ -509,67 +1617,89 @@ local function handle_open(args, config, command_table) return enter_command(args, config, command_table) -- Otherwise, just exit the function - else return end + else + return + end end -- Otherwise, if the hovered item is not an archive, - -- or entering archives isn't wanted - if not hovered_item_is_archive() or not config.enter_archives then + -- or entering archives isn't wanted, + -- or the interactive flag is passed + if + not hovered_item_is_archive() + or not config.enter_archives + or args.interactive + then + -- - -- Simply emit the open command and exit the function - return ya.manager_emit("open", args) + -- Simply emit the open command, + -- opening only the hovered item + -- as the item group is the hovered item, + -- and exit the function + return ya.manager_emit("open", merge_tables(args, { hovered = true })) end -- Otherwise, the hovered item is an archive -- and entering archives is wanted, -- so get the path of the hovered item - local archive_path = get_hovered_item_path() + local archive_path = get_path_of_hovered_item() -- If the archive path somehow doesn't exist, then exit the function if not archive_path then return end - -- Run the command to extract the archive - local output, err = Command("unar") - :args({ - "-d", - ExtractBehaviourFlags[config.extract_behaviour], - archive_path - }) - :stdout(Command.PIPED) - :stderr(Command.PIPED) - :output() + -- Run the function to extract the archive + local extract_successful, err, extracted_items_path = + extract_archive(archive_path, config) - -- If the command isn't successful, notify the user - if not output then + -- If the extraction of the archive isn't successful, + -- notify the user and exit the function + if not extract_successful then return ya.notify(merge_tables(DEFAULT_NOTIFICATION_OPTIONS, { content = "Failed to extract archive at: " .. archive_path - .. "\nError code: " - .. tostring(err), - level = "error" + .. "\nError: " + .. err, + level = "error", })) end - -- Get the filename of the archive - local archive_filename = archive_path:match(get_filename_pattern) + -- If the extracted items path is nil, + -- then exit the function + if not extracted_items_path then return end + + -- Get the cha object of the extracted items path + local extracted_items_cha = fs.cha(Url(extracted_items_path), false) + + -- If the cha object of the extracted items path is nil + -- then exit the function + if not extracted_items_cha then return end + + -- If the extracted items path is not a directory, + -- then exit the function + if not extracted_items_cha.is_dir then return end -- Enter the archive directory - ya.manager_emit("cd", { archive_filename }) + ya.manager_emit("cd", { extracted_items_path }) -- Calls the function to skip child directories -- with only a single directory inside - skip_single_child_directories(args, config, archive_filename) + skip_single_child_directories(args, config, extracted_items_path) end - -- Function to handle the enter command +---@param args Arguments +---@param config Configuration +---@param command_table CommandTable +---@return nil local function handle_enter(args, config, command_table) + -- -- Get the function for the open command local open_command = command_table[Commands.Open] -- If the hovered item is not a directory if not hovered_item_is_dir() and config.smart_enter then + -- -- If smart enter is wanted, -- call the function for the open command @@ -578,7 +1708,9 @@ local function handle_enter(args, config, command_table) return open_command(args, config, command_table) -- Otherwise, just exit the function - else return end + else + return + end end -- Otherwise, always emit the enter command, @@ -589,9 +1721,12 @@ local function handle_enter(args, config, command_table) skip_single_child_directories(args, config, get_current_directory()) end - -- Function to handle the leave command +---@param args Arguments +---@param config Configuration +---@return nil local function handle_leave(args, config) + -- -- Always emit the leave command ya.manager_emit("leave", args) @@ -606,34 +1741,39 @@ local function handle_leave(args, config) -- Otherwise, initialise the directory to the current directory local directory = get_current_directory() - -- Otherwise, start an infinite loop + -- Start an infinite loop while true do + -- - -- Run the ls command to get the items in the directory - local output, _ = ls_command(directory, config.ignore_hidden_items) - - -- If there is no output, then break out of the loop - if not output then break end - - -- Get the list of items in the directory - local directory_items = string_split(output.stdout, "\n") + -- Get all the items in the current directory + local directory_items = + get_directory_items(directory, config.ignore_hidden_items) -- If the number of directory items is not 1, - -- then break out of the loop + -- then break out of the loop. if #directory_items ~= 1 then break end - -- Otherwise, set the new directory - -- to the parent of the current directory - directory = directory:match(get_parent_directory_pattern) + -- Get the parent directory of the current directory + local parent_directory = Url(directory):parent() + + -- If the parent directory is nil, + -- break the loop + if not parent_directory then break end + + -- Otherwise, set the new directory to the parent directory + directory = tostring(parent_directory) end -- Emit the change directory command to change to the directory variable ya.manager_emit("cd", { directory }) end - --- Function to handle the a command -local function handle_command(command, args) +-- Function to handle a Yazi command +---@param command string A Yazi command +---@param args Arguments +---@return nil +local function handle_yazi_command(command, args) + -- -- Call the function to get the item group local item_group = get_item_group() @@ -643,66 +1783,34 @@ local function handle_command(command, args) -- If the item group is the selected items if item_group == ItemGroup.Selected then + -- -- Emit the command to operate on the selected items ya.manager_emit(command, args) -- If the item group is the hovered item elseif item_group == ItemGroup.Hovered then + -- -- Emit the command with the hovered option ya.manager_emit(command, merge_tables(args, { hovered = true })) -- Otherwise, exit the function - else return end + else + return + end end - --- Function to handle a shell command -local function handle_shell_command(command, args) - - -- Call the function to get the item group - local item_group = get_item_group() - - -- If no item group is returned, exit the function - if not item_group then return end - - -- If the item group is the selected items - if item_group == ItemGroup.Selected then - - -- Merge the arguments for the shell command with additional ones - args = merge_tables({ - command .. " $@", - confirm = true, - block = true, - }, args) - - -- Emit the command to operate the selected items - ya.manager_emit("shell", args) - - -- If the item group is the hovered item - elseif item_group == ItemGroup.Hovered then - - -- Merge the arguments for the shell command with additional ones - args = merge_tables({ - command .. " $0", - confirm = true, - block = true, - }, args) - - -- Emit the command to operate on the hovered item - ya.manager_emit("shell", args) - - -- Otherwise, exit the function - else return end -end - - -- Function to handle the paste command +---@param args Arguments +---@param config Configuration +---@return nil local function handle_paste(args, config) + -- -- If the hovered item is a directory and smart paste is wanted if hovered_item_is_dir() and (config.smart_paste or args.smart) then + -- -- Enter the directory ya.manager_emit("enter", {}) @@ -713,15 +1821,259 @@ local function handle_paste(args, config) -- Leave the directory ya.manager_emit("leave", {}) - -- Otherwise, just paste the items inside the current directory - else - ya.manager_emit("paste", args) + -- Exit the function + return end + + -- Otherwise, just paste the items inside the current directory + ya.manager_emit("paste", args) end +-- Function to remove the F flag from the less command +---@param command string +---@return string command The command with the F flag removed +---@return boolean f_flag_found Whether the F flag was found +local function remove_f_flag_from_less_command(command) + -- + + -- Initialise the variable to store if the F flag is found + local f_flag_found = false + + -- Initialise the variable to store the replacement count + local replacement_count = 0 + + -- Remove the F flag when it is passed at the start + -- of the flags given to the less command + command, replacement_count = command:gsub("(%f[%a]less%f[%A].*)%-F", "%1") + + -- If the replacement count is not 0, + -- set the f_flag_found variable to true + if replacement_count ~= 0 then f_flag_found = true end + + -- Remove the F flag when it is passed in the middle + -- or end of the flags given to the less command command + command, replacement_count = + command:gsub("(%f[%a]less%f[%A].*%-)(%a*)F(%a*)", "%1%2%3") + + -- If the replacement count is not 0, + -- set the f_flag_found variable to true + if replacement_count ~= 0 then f_flag_found = true end + + -- Return the command and whether or not the F flag was found + return command, f_flag_found +end + +-- Function to fix a command containing less. +-- All this function does is remove +-- the F flag from a command containing less. +---@param command string +---@return string command The fixed shell command +local function fix_shell_command_containing_less(command) + -- + + -- Remove the F flag from the given command + local fixed_command = remove_f_flag_from_less_command(command) + + -- Get the LESS environment variable + local less_environment_variable = os.getenv("LESS") + + -- If the LESS environment variable is not set, + -- then return the given command with the F flag removed + if not less_environment_variable then return fixed_command end + + -- Otherwise, remove the F flag from the LESS environment variable + -- and check if the F flag was found + local less_command_with_modified_env_variables, f_flag_found = + remove_f_flag_from_less_command("less " .. less_environment_variable) + + -- If the F flag isn't found, + -- then return the given command with the F flag removed + if not f_flag_found then return fixed_command end + + -- Add the less environment variable flags to the less command + fixed_command = fixed_command:gsub( + "%f[%a]less%f[%A]", + less_command_with_modified_env_variables + ) + + -- Unset the LESS environment variable before calling the command + fixed_command = "unset LESS; " .. fixed_command + + -- Return the fixed command + return fixed_command +end + +-- Function to fix the bat default pager command +---@param command string +---@return string command The fixed bat command +local function fix_bat_default_pager_shell_command(command) + -- + + -- Initialise the default pager command for bat without the F flag + local bat_default_pager_command_without_f_flag = "less -RX" + + -- Get the modified command and the replacement count + -- when replacing the less command when it is quoted + local modified_command, replacement_count = command:gsub( + "(" + .. bat_command_with_pager_pattern + .. "['\"]+%s*" + .. ")" + .. "less" + .. "(%s*['\"]+)", + "%1" .. bat_default_pager_command_without_f_flag .. "%2" + ) + + -- If the replacement count is not 0, + -- then return the modified command + if replacement_count ~= 0 then return modified_command end + + -- Otherwise, get the modified command and the replacement count + -- when replacing the less command when it is unquoted + modified_command, replacement_count = command:gsub( + "(" .. bat_command_with_pager_pattern .. ")" .. "less", + '%1"' .. bat_default_pager_command_without_f_flag .. '"' + ) + + -- If the replacement count is not 0, + -- then return the modified command + if replacement_count ~= 0 then return modified_command end + + -- Otherwise, return the given command + return command +end + +-- Function to fix the shell commands given to work properly with Yazi +---@param command string A shell command +---@return string command The fixed shell command +local function fix_shell_command(command) + -- + + -- If the given command includes the less command + if command:find("%f[%a]less%f[%A]") ~= nil then + -- + + -- Fix the command containing less + command = fix_shell_command_containing_less(command) + end + + -- If the given command contains the bat command with the pager + -- option passed + if command:find(bat_command_with_pager_pattern) ~= nil then + -- + + -- Calls the command to fix the bat command with the default pager + command = fix_bat_default_pager_shell_command(command) + end + + -- Return the modified command + return command +end + +-- Function to handle a shell command +---@param args Arguments +---@param _ nil +---@param exit_if_directory boolean|nil +---@return nil +local function handle_shell(args, _, _, exit_if_directory) + -- + + -- Get the first item of the arguments given + -- and set it to the command variable + local command = table.remove(args, 1) + + -- If the command isn't a string, exit the function + if type(command) ~= "string" then return end + + -- Fix the given command + command = fix_shell_command(command) + + -- Call the function to get the item group + local item_group = get_item_group() + + -- If no item group is returned, exit the function + if not item_group then return end + + -- If the exit if directory flag is not given, + -- and the arguments contain the + -- exit if directory flag + if not exit_if_directory and args.exit_if_directory then + -- + + -- Set the exit if directory flag to true + exit_if_directory = true + end + + -- If the item group is the selected items + if item_group == ItemGroup.Selected then + -- + + -- If the exit if directory flag is passed + if exit_if_directory then + -- + + -- Initialise the number of files + local number_of_files = 0 + + -- Iterate over all of the selected items + for _, item in pairs(get_paths_of_selected_items()) do + -- + + -- Get the cha object of the item + local item_cha = fs.cha(Url(item), false) + + -- If the item isn't a directory + if not item_cha.is_dir then + -- + + -- Increment the number of files + number_of_files = number_of_files + 1 + end + end + + -- If the number of files is 0, then exit the function + if number_of_files == 0 then return end + end + + -- Replace the shell variable in the command + -- with the quoted paths of the selected items + command = command:gsub( + shell_variable_pattern, + table.concat(get_paths_of_selected_items(true), " ") + ) + + -- If the item group is the hovered item + elseif item_group == ItemGroup.Hovered then + -- + + -- If the exit if directory flag is passed, + -- and the hovered item is a directory, + -- then exit the function + if exit_if_directory and hovered_item_is_dir() then return end + + -- Replace the shell variable in the command + -- with the quoted path of the hovered item + command = + command:gsub(shell_variable_pattern, get_path_of_hovered_item(true)) + + -- Otherwise, exit the function + else + return + end + + -- Merge the command back into the arguments given + args = merge_tables({ command }, args) + + -- Emit the command to operate on the hovered item + ya.manager_emit("shell", args) +end -- Function to do the wraparound for the arrow command +---@param _ any +---@param args Arguments +---@return nil local wraparound_arrow = ya.sync(function(_, args) + -- -- Get the current tab local current_tab = cx.active.current @@ -743,15 +2095,18 @@ local wraparound_arrow = ya.sync(function(_, args) -- Emit the arrow function with the new cursor index minus -- the current cursor index to determine how to move the cursor - ya.manager_emit("arrow", merge_tables( - args, - { new_cursor_index - current_tab.cursor } - )) + ya.manager_emit( + "arrow", + merge_tables(args, { new_cursor_index - current_tab.cursor }) + ) end) - -- Function to handle the arrow command +---@param args Arguments +---@param config Configuration +---@return nil local function handle_arrow(args, config) + -- -- If wraparound file navigation isn't wanted, -- then execute the arrow command @@ -759,118 +2114,175 @@ local function handle_arrow(args, config) ya.manager_emit("arrow", args) -- Otherwise, call the wraparound arrow function - else wraparound_arrow(args) end + else + wraparound_arrow(args) + end end +-- Function to get the directory items in the parent directory +---@param _ any +---@param directories_only boolean +---@return string[] +local get_parent_directory_items = ya.sync(function(_, directories_only) + -- + + -- Initialise the list of directory items + local directory_items = {} + + -- Get the parent directory + local parent_directory = cx.active.parent + + -- If the parent directory doesn't exist, + -- return the empty list of directory items + if not parent_directory then return directory_items end + + -- Otherwise, iterate over the items in the parent directory + for _, item in ipairs(parent_directory.files) do + -- + + -- If the directories only flag is passed, + -- and the item is not a directory, + -- then skip the item + if directories_only and not item.cha.is_dir then goto continue end + + -- Otherwise, add the item to the list of directory items + table.insert(directory_items, item) + + -- The continue label to skip the item + ::continue:: + end + + -- Return the list of directory items + return directory_items +end) -- Function to execute the parent arrow command -local execute_parent_arrow_command = ya.sync( - function(state, args, number_of_directories) +---@param state any +---@param args Arguments +---@return nil +local execute_parent_arrow_command = ya.sync(function(state, args) + -- - -- Gets the parent directory - local parent_directory = cx.active.parent + -- Gets the parent directory + local parent_directory = cx.active.parent - -- If the parent directory doesn't exist, - -- then exit the function - if not parent_directory then return end + -- If the parent directory doesn't exist, + -- then exit the function + if not parent_directory then return end - -- Get the step from the arguments given - local step = table.remove(args, 1) + -- Get the offset from the arguments given + local offset = table.remove(args, 1) - -- Initialise the new cursor index - -- to the current parent cursor index - local new_cursor_index = parent_directory.cursor + -- If the offset is not a number, then exit the function + if type(offset) ~= "number" then return end - -- Otherwise, if wraparound file navigation is wanted - -- and the number of directories is given and isn't 0 - if - state.config.wraparound_file_navigation - and number_of_directories - and number_of_directories ~= 0 - then + -- Get the number of items in the parent directory + local number_of_items = #parent_directory.files - -- Get the new cursor index by adding the step, - -- and modding the whole thing by the number of - -- directories given. - new_cursor_index = (parent_directory.cursor + step) + -- Initialise the new cursor index + -- to the current cursor index + local new_cursor_index = parent_directory.cursor + + -- If wraparound file navigation is wanted + if state.config.wraparound_file_navigation then + -- + + -- If the user sorts their directories first + if state.config.sort_directories_first then + -- + + -- Get the directories in the parent directory + local directories = get_parent_directory_items(true) + + -- Get the number of directories in the parent directory + local number_of_directories = #directories + + -- If the number of directories is 0, then exit the function + if number_of_directories == 0 then return end + + -- Get the new cursor index by adding the offset, + -- and modding the whole thing by the number of directories + new_cursor_index = (parent_directory.cursor + offset) % number_of_directories - else - -- Otherwise, get the new cursor index normally. - new_cursor_index = parent_directory.cursor + step + -- Otherwise, if the user doesn't sort their directories first + else + -- + + -- Get the new cursor index by adding the offset, + -- and modding the whole thing by the number of + -- items in the parent directory + new_cursor_index = (parent_directory.cursor + offset) + % number_of_items end - -- Increment the cursor index by 1. - -- The cursor index needs to be increased by 1 - -- as the cursor index is 0-based, while Lua - -- tables are 1-based. - new_cursor_index = new_cursor_index + 1 + -- Otherwise, get the new cursor index normally + -- by adding the offset to the cursor index + else + new_cursor_index = parent_directory.cursor + offset + end - -- Get the target directory - local target_directory = parent_directory.files[new_cursor_index] + -- Increment the cursor index by 1. + -- The cursor index needs to be increased by 1 + -- as the cursor index is 0-based, while Lua + -- tables are 1-based. + new_cursor_index = new_cursor_index + 1 - -- If the target directory exists and is a directory - if target_directory and target_directory.cha.is_dir then + -- Get the starting index of the loop + local start_index = new_cursor_index - -- Emit the command to change directory - -- to the target directory - ya.manager_emit("cd", { target_directory.url }) + -- Get the ending index of the loop. + -- + -- If the offset given is negative, set the end index to 1, + -- as the loop will iterate backwards. + -- Otherwise, if the step given is positive, + -- set the end index to the number of items in the + -- parent directory. + local end_index = offset < 0 and 1 or number_of_items + + -- Get the step for the loop. + -- + -- If the offset given is negative, set the step to -1, + -- as the loop will iterate backwards. + -- Otherwise, if the step given is positive, set + -- the step to 1 to iterate forwards. + local step = offset < 0 and -1 or 1 + + -- Iterate over the parent directory items + for i = start_index, end_index, step do + -- + + -- Get the directory item + local directory_item = parent_directory.files[i] + + -- If the directory item exists and is a directory + if directory_item and directory_item.cha.is_dir then + -- + + -- Emit the command to change directory to + -- the directory item and exit the function + return ya.manager_emit("cd", { directory_item.url }) end end -) - +end) -- Function to handle the parent arrow command -local function handle_parent_arrow(args, config) - - -- If wraparound file navigation isn't wanted, - -- then execute the parent arrow command and exit the function - if not config.wraparound_file_navigation then - return execute_parent_arrow_command(args) - end - - -- Otherwise, get the path of the parent directory - local parent_directory_path = get_parent_directory() - - -- If there is no parent directory, exit the function - if not parent_directory_path then return end - - -- Call the ls command to get the number of directories - local output, _ = ls_command( - parent_directory_path, - config.ignore_hidden_items - ) - - -- If there is no output, exit the function - if not output then return end - - -- Get the item in the parent directory - local directory_items = string_split(output.stdout, "\n") - - -- Initialise the number of directories - local number_of_directories = 0 - - -- Iterate over the directory items - for _, item in ipairs(directory_items) do - - -- If the item is a directory - if item:match(is_directory_pattern) then - - -- Increment the number of directories by 1 - number_of_directories = number_of_directories + 1 - - -- Otherwise, break out of the loop, - -- as the directories are grouped together - else break end - end +---@param args Arguments +---@return nil +local function handle_parent_arrow(args) + -- -- Call the function to execute the parent arrow command - execute_parent_arrow_command(args, number_of_directories) + -- with the arguments given + execute_parent_arrow_command(args) end - --- Function to handle the pager command -local function handle_pager(args) +-- Function to handle the editor command +---@param args Arguments +---@param config Configuration +---@return nil +local function handle_editor(args, config) + -- -- Call the function to get the item group local item_group = get_item_group() @@ -878,61 +2290,85 @@ local function handle_pager(args) -- If no item group is returned, exit the function if not item_group then return end - -- If the item group is the selected items, - -- then execute the command and exit the function - if item_group == ItemGroup.Selected then + -- Get the editor environment variable + local editor = os.getenv("EDITOR") - -- Combine the arguments with additional ones - args = merge_tables({ - "$PAGER $@", + -- If the editor not set, exit the function + if not editor then return end + + -- Call the handle shell function + -- with the editor command + handle_shell( + merge_tables({ + editor .. " $@", confirm = true, - block = true - }, args) - - -- Emit the command and exit the function - return ya.manager_emit("shell", args) - end - - -- Otherwise, the item group is the hovered item, - -- and if the hovered item is a directory, exit the function - if hovered_item_is_dir() then return - - -- Otherwise - else - - -- Combine the arguments with additional ones - args = merge_tables({ - "$PAGER $0", - confirm = true, - block = true - }, args) - - -- Emit the command and exit the function - return ya.manager_emit("shell", args) - end + block = true, + }, args), + config + ) end +-- Function to handle the pager command +---@param args Arguments +---@param config Configuration +---@param command_table CommandTable +---@return nil +local function handle_pager(args, config, command_table) + -- + + -- Get the pager environment variable + local pager = os.getenv("PAGER") + + -- If the pager is not set, exit the function + if not pager then return end + + -- If the pager is the less command + if pager:find("^less") ~= nil then + -- + + -- Remove the F flag from the command + pager = pager:gsub("%-F", ""):gsub("(%a*)F(%a*)", "%1%2") + end + + -- Call the handle shell function + -- with the pager command + handle_shell( + merge_tables({ + pager .. " $@", + confirm = true, + block = true, + }, args), + config, + command_table, + true + ) +end -- Function to run the commands given +---@param command string +---@param args Arguments +---@param config Configuration +---@return nil local function run_command_func(command, args, config) + -- -- The command table + ---@enum CommandTable local command_table = { [Commands.Open] = handle_open, [Commands.Enter] = handle_enter, [Commands.Leave] = handle_leave, [Commands.Rename] = function(_) - handle_command("rename", args) + handle_yazi_command("rename", args) end, [Commands.Remove] = function(_) - handle_command("remove", args) + handle_yazi_command("remove", args) end, [Commands.Paste] = handle_paste, + [Commands.Shell] = handle_shell, [Commands.Arrow] = handle_arrow, [Commands.ParentArrow] = handle_parent_arrow, - [Commands.Editor] = function(_) - handle_shell_command("$EDITOR", args) - end, + [Commands.Editor] = handle_editor, [Commands.Pager] = handle_pager, } @@ -941,12 +2377,10 @@ local function run_command_func(command, args, config) -- If the function isn't found, notify the user and exit the function if not command_func then - return ya.notify( - merge_tables(DEFAULT_NOTIFICATION_OPTIONS, { - content = "Unknown command: " .. command, - level = "error" - }) - ) + return ya.notify(merge_tables(DEFAULT_NOTIFICATION_OPTIONS, { + content = "Unknown command: " .. command, + level = "error", + })) end -- Parse the arguments and set it to the args variable @@ -957,15 +2391,22 @@ local function run_command_func(command, args, config) end -- The setup function to setup the plugin +---@param _ any +---@param opts Configuration|nil +---@return nil local function setup(_, opts) + -- - -- Initialise the configuration with the default configuration - initialise_config(opts) + -- Initialise the plugin + initialise_plugin(opts) end - -- The function to be called to use the plugin +---@param _ any +---@param args string[] +---@return nil local function entry(_, args) + -- -- Gets the command passed to the plugin local command = args[1] @@ -976,17 +2417,17 @@ local function entry(_, args) -- Gets the configuration object local config = get_config() - -- If the configuration hasn't been initialised, - -- then initialise the configuration - if not config then - config = initialise_config() - end + -- If the configuration hasn't been initialised yet, + -- then initialise the plugin with the default configuration, + -- as it hasn't been initialised either + if not config then config = initialise_plugin() end -- Call the function to handle the commands run_command_func(command, args, config) end -- Returns the table required for Yazi to run the plugin +---@return table { setup: function, entry: function } return { setup = setup, entry = entry, diff --git a/config/yazi/plugins/chmod.yazi/README.md b/config/yazi/plugins/chmod.yazi/README.md index 95fab171..e8a66142 100644 --- a/config/yazi/plugins/chmod.yazi/README.md +++ b/config/yazi/plugins/chmod.yazi/README.md @@ -7,7 +7,7 @@ https://github.com/yazi-rs/plugins/assets/17523360/7aa3abc2-d057-498c-8473-a6282 ## Installation ```sh -ya pack -a yazi-rs/plugins#chmod +ya pack -a yazi-rs/plugins:chmod ``` ## Usage @@ -22,3 +22,7 @@ desc = "Chmod on selected files" ``` Make sure the c => m key is not used elsewhere. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/exifaudio.yazi/README.md b/config/yazi/plugins/exifaudio.yazi/README.md index 69a1597a..e646d0c0 100644 --- a/config/yazi/plugins/exifaudio.yazi/README.md +++ b/config/yazi/plugins/exifaudio.yazi/README.md @@ -7,6 +7,10 @@ Preview audio metadata and cover on [Yazi](https://github.com/sxyazi/yazi). ## Installation ```sh +# Automatically with yazi 0.3.0 +ya pack -a "Sonico98/exifaudio" + +# Or manually under: # Linux/macOS git clone https://github.com/Sonico98/exifaudio.yazi.git ~/.config/yazi/plugins/exifaudio.yazi @@ -27,6 +31,8 @@ prepend_previewers = [ Make sure you have [exiftool](https://exiftool.org/) installed and in your `PATH`. +Optional: if you have [mediainfo](https://mediaarea.net/en/MediaInfo) installed and in your `PATH`, it will be used instead for more accurate metadata. Exiftool is still required to display the cover. + ## Thanks Thanks to [sxyazi](https://github.com/sxyazi) for the PDF previewer code, on which this previewer is based on. diff --git a/config/yazi/plugins/exifaudio.yazi/init.lua b/config/yazi/plugins/exifaudio.yazi/init.lua index 521ff33b..fe6e23bb 100644 --- a/config/yazi/plugins/exifaudio.yazi/init.lua +++ b/config/yazi/plugins/exifaudio.yazi/init.lua @@ -1,11 +1,14 @@ local M = {} -function M:peek() - local cache = ya.file_cache(self) - if not cache then - return +function GetPath(str) + local sep = '/' + if ya.target_family() == "windows" then + sep = '\\' end + return str:match("(.*"..sep..")") +end +function Exiftool(...) local child = Command("exiftool") :args({ "-q", "-q", "-S", "-Title", "-SortName", @@ -16,11 +19,58 @@ function M:peek() "-AlbumArtistSortOrder", "-Genre", "-TrackNumber", "-Year", "-Duration", "-SampleRate", "-AudioSampleRate", "-AudioBitrate", "-AvgBitrate", - "-Channels", "-AudioChannels", tostring(self.file.url), + "-Channels", "-AudioChannels", tostring(...), }) :stdout(Command.PIPED) :stderr(Command.NULL) - :spawn() + :spawn() + return child +end + +function Mediainfo(...) + local file, cache_dir = ... + local template = cache_dir.."mediainfo.txt" + local child = Command("mediainfo") + :args({ + "--Output=file://"..template, tostring(file) + }) + :stdout(Command.PIPED) + :stderr(Command.NULL) + :spawn() + return child +end + +function M:peek() + local cache = ya.file_cache(self) + if not cache then + return + end + + -- Get cache dir to find the mediainfo template file + local cache_dir = GetPath(tostring(cache)) + + -- Try mediainfo, otherwise use exiftool + pcall(display_metadata_legacy) + pcall(display_metadata) + local status, child = pcall(Mediainfo, self.file.url, cache_dir) + if not status or child == nil then + status, child = pcall(Exiftool, self.file.url) + if not status or child == nil then + local error = ui.Line { ui.Span("Make sure exiftool is installed and in your PATH") } + -- TODO)) Remove legacy method when v0.4 gets released + local function display_error_legacy() + local p = ui.Paragraph(self.area, { error }):wrap(ui.Paragraph.WRAP) + ya.preview_widgets(self, { p }) + end + local function display_error() + local p = ui.Text(error):area(self.area):wrap(ui.Text.WRAP) + ya.preview_widgets(self, { p }) + end + pcall(display_error_legacy) + pcall(display_error) + return + end + end local limit = self.area.h local i, metadata = 0, {} @@ -34,16 +84,27 @@ function M:peek() i = i + 1 if i > self.skip then - local m_title, m_tag = prettify(next) - local ti = ui.Span(m_title):bold() - local ta = ui.Span(m_tag) - table.insert(metadata, ui.Line{ti, ta}) - table.insert(metadata, ui.Line{}) + local m_title, m_tag = Prettify(next) + if m_title ~= "" and m_tag ~= "" then + local ti = ui.Span(m_title):bold() + local ta = ui.Span(m_tag) + table.insert(metadata, ui.Line{ti, ta}) + table.insert(metadata, ui.Line{}) + end end until i >= self.skip + limit - - local p = ui.Paragraph(self.area, metadata):wrap(ui.Paragraph.WRAP) - ya.preview_widgets(self, { p }) + + -- TODO)) Remove legacy method when v0.4 gets released + local function display_metadata_legacy() + local p = ui.Paragraph(self.area, metadata):wrap(ui.Paragraph.WRAP) + ya.preview_widgets(self, { p }) + end + local function display_metadata() + local p = ui.Text(metadata):area(self.area):wrap(ui.Text.WRAP) + ya.preview_widgets(self, { p }) + end + pcall(display_metadata_legacy) + pcall(display_metadata) local cover_width = self.area.w / 2 - 5 local cover_height = (self.area.h / 4) + 3 @@ -60,7 +121,7 @@ function M:peek() end end -function prettify(metadata) +function Prettify(metadata) local substitutions = { Sortname = "Sort Title:", SortName = "Sort Title:", @@ -99,11 +160,19 @@ function prettify(metadata) -- Separate the tag title from the tag data local t={} for str in string.gmatch(metadata , "([^"..":".."]+)") do - table.insert(t, str) + if str ~= "\n" then + table.insert(t, str) + else + table.insert(t, nil) + end end -- Add back semicolon to title, rejoin tag data if it happened to contain a semicolon - return t[1]..":", table.concat(t, ":", 2) + local title, tag_data = "", "" + if t[1] ~= nil then + title, tag_data = t[1]..":", table.concat(t, ":", 2) + end + return title, tag_data end @@ -123,6 +192,33 @@ function M:preload() return 1 end + local mediainfo_template = 'General;"\ +$if(%Track%,Title: %Track%,)\ +$if(%Track/Sort%,Sort Title: %Track/Sort%,)\ +$if(%Title/Sort%,Sort Title: %Title/Sort%,)\ +$if(%TITLESORT%,Sort Title: %TITLESORT%,)\ +$if(%Performer%,Artist: %Performer%,)\ +$if(%Performer/Sort%,Sort Artist: %Performer/Sort%,)\ +$if(%ARTISTSORT%,Sort Artist: %ARTISTSORT%,)\ +$if(%Album%,Album: %Album%,)\ +$if(%Album/Sort%,Sort Album: %Album/Sort%)\ +$if(%ALBUMSORT%,Sort Album: %ALBUMSORT%)\ +$if(%Album/Performer%,Album Artist: %Album/Performer%)\ +$if(%Album/Performer/Sort%,Sort Album Artist: %Album/Performer/Sort%)\ +$if(%Genre%,Genre: %Genre%)\ +$if(%Track/Position%,Track Number: %Track/Position%)\ +$if(%Recorded_Date%,Year: %Recorded_Date%)\ +$if(%Duration/String%,Duration: %Duration/String%)\ +$if(%BitRate/String%,Bitrate: %BitRate/String%)\ +"\ +Audio;"Sample Rate: %SamplingRate%\ +Channels: %Channel(s)%"\ +' + + -- Write the mediainfo template file into yazi's cache dir + local cache_dir = GetPath(tostring(cache)) + fs.write(Url(cache_dir.."mediainfo.txt"), mediainfo_template) + local output = Command("exiftool") :args({ "-b", "-CoverArt", "-Picture", tostring(self.file.url) }) :stdout(Command.PIPED) diff --git a/config/yazi/plugins/full-border.yazi/README.md b/config/yazi/plugins/full-border.yazi/README.md index 05e8386f..6e78bd4d 100644 --- a/config/yazi/plugins/full-border.yazi/README.md +++ b/config/yazi/plugins/full-border.yazi/README.md @@ -7,7 +7,7 @@ Add a full border to Yazi to make it look fancier. ## Installation ```sh -ya pack -a yazi-rs/plugins#full-border +ya pack -a yazi-rs/plugins:full-border ``` ## Usage @@ -18,5 +18,15 @@ Add this to your `init.lua` to enable the plugin: require("full-border"):setup() ``` -This plugin overrides the [`Manager.render`](https://github.com/sxyazi/yazi/blob/latest/yazi-plugin/preset/components/manager.lua) method, -you might need to check if any other plugins that also need to override it are enabled. +Or you can customize the border type: + +```lua +require("full-border"):setup { + -- Available values: ui.Border.PLAIN, ui.Border.ROUNDED + type = ui.Border.ROUNDED, +} +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/full-border.yazi/init.lua b/config/yazi/plugins/full-border.yazi/init.lua index 9c6cc5fd..42f88657 100644 --- a/config/yazi/plugins/full-border.yazi/init.lua +++ b/config/yazi/plugins/full-border.yazi/init.lua @@ -1,13 +1,29 @@ -local function setup() +-- TODO: remove this once v0.4 is released +local v4 = function(typ, area, ...) + if typ == "bar" then + return ui.Table and ui.Bar(...):area(area) or ui.Bar(area, ...) + else + return ui.Table and ui.Border(...):area(area) or ui.Border(area, ...) + end +end + +local function setup(_, opts) + local type = opts and opts.type or ui.Border.ROUNDED local old_build = Tab.build + Tab.build = function(self, ...) local bar = function(c, x, y) if x <= 0 or x == self._area.w - 1 then - return ui.Bar(ui.Rect.default, ui.Bar.TOP) + return v4("bar", ui.Rect.default, ui.Bar.TOP) end return ui.Bar( - ui.Rect { x = x, y = math.max(0, y), w = ya.clamp(0, self._area.w - x, 1), h = math.min(1, self._area.h) }, + ui.Rect({ + x = x, + y = math.max(0, y), + w = ya.clamp(0, self._area.w - x, 1), + h = math.min(1, self._area.h), + }), ui.Bar.TOP ):symbol(c) end @@ -15,13 +31,13 @@ local function setup() local c = self._chunks self._chunks = { c[1]:padding(ui.Padding.y(1)), - c[2]:padding(c[1].w > 0 and ui.Padding.y(1) or ui.Padding(1, 0, 1, 1)), + c[2]:padding(ui.Padding(c[1].w > 0 and 0 or 1, c[3].w > 0 and 0 or 1, 1, 1)), c[3]:padding(ui.Padding.y(1)), } local style = THEME.manager.border_style self._base = ya.list_merge(self._base or {}, { - ui.Border(self._area, ui.Border.ALL):type(ui.Border.ROUNDED):style(style), + ui.Border(self._area, ui.Border.ALL):type(type):style(style), ui.Bar(self._chunks[1], ui.Bar.RIGHT):style(style), ui.Bar(self._chunks[3], ui.Bar.LEFT):style(style), diff --git a/config/yazi/plugins/git.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY b/config/yazi/plugins/git.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY new file mode 100644 index 00000000..e69de29b diff --git a/config/yazi/plugins/git.yazi/LICENSE b/config/yazi/plugins/git.yazi/LICENSE new file mode 100644 index 00000000..fb5b1d62 --- /dev/null +++ b/config/yazi/plugins/git.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 yazi-rs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/yazi/plugins/git.yazi/README.md b/config/yazi/plugins/git.yazi/README.md new file mode 100644 index 00000000..fad9ce44 --- /dev/null +++ b/config/yazi/plugins/git.yazi/README.md @@ -0,0 +1,81 @@ +# git.yazi + +> [!NOTE] +> Yazi v0.3.3 or later is required for this plugin to work. + +Show the status of Git file changes as linemode in the file list. + +https://github.com/user-attachments/assets/34976be9-a871-4ffe-9d5a-c4cdd0bf4576 + +## Installation + +```sh +ya pack -a yazi-rs/plugins:git +``` + +## Setup + +Add the following to your `~/.config/yazi/init.lua`: + +```lua +require("git"):setup() +``` + +And register it as fetchers in your `~/.config/yazi/yazi.toml`: + +```toml +[[plugin.prepend_fetchers]] +id = "git" +name = "*" +run = "git" + +[[plugin.prepend_fetchers]] +id = "git" +name = "*/" +run = "git" +``` + +## Advanced + +> [!NOTE] +> This section currently requires Yazi nightly that includes https://github.com/sxyazi/yazi/pull/1637 + +You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with: + +- `THEME.git.modified` +- `THEME.git.added` +- `THEME.git.untracked` +- `THEME.git.ignored` +- `THEME.git.deleted` +- `THEME.git.updated` + +For example: + +```lua +-- ~/.config/yazi/init.lua +THEME.git = THEME.git or {} +THEME.git.modified = ui.Style():fg("blue") +THEME.git.deleted = ui.Style():fg("red"):bold() +``` + +You can also customize the text of the status sign with: + +- `THEME.git.modified_sign` +- `THEME.git.added_sign` +- `THEME.git.untracked_sign` +- `THEME.git.ignored_sign` +- `THEME.git.deleted_sign` +- `THEME.git.updated_sign` + +For example: + +```lua +-- ~/.config/yazi/init.lua +THEME.git = THEME.git or {} +THEME.git.modified_sign = "M" +THEME.git.deleted_sign = "D" +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/git.yazi/init.lua b/config/yazi/plugins/git.yazi/init.lua new file mode 100644 index 00000000..965628a7 --- /dev/null +++ b/config/yazi/plugins/git.yazi/init.lua @@ -0,0 +1,198 @@ +local WIN = ya.target_family() == "windows" +local PATS = { + { "[MT]", 6 }, -- Modified + { "[AC]", 5 }, -- Added + { "?$", 4 }, -- Untracked + { "!$", 3 }, -- Ignored + { "D", 2 }, -- Deleted + { "U", 1 }, -- Updated + { "[AD][AD]", 1 }, -- Updated +} + +local function match(line) + local signs = line:sub(1, 2) + for _, p in ipairs(PATS) do + local path + if signs:find(p[1]) then + path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) + path = WIN and path:gsub("/", "\\") or path + end + if not path then + elseif path:find("[/\\]$") then + return p[2] == 3 and 30 or p[2], path:sub(1, -2) + else + return p[2], path + end + end +end + +local function root(cwd) + repeat + local cha = fs.cha(cwd:join(".git")) + if cha and cha.is_dir then + return tostring(cwd) + end + cwd = cwd:parent() + until not cwd +end + +local function bubble_up(changed) + local new, empty = {}, Url("") + for k, v in pairs(changed) do + if v ~= 3 and v ~= 30 then + local url = Url(k):parent() + while url and url ~= empty do + local s = tostring(url) + new[s] = (new[s] or 0) > v and new[s] or v + url = url:parent() + end + end + end + return new +end + +local function propagate_down(ignored, cwd, repo) + local new, rel = {}, cwd:strip_prefix(repo) + for k, v in pairs(ignored) do + if v == 30 then + if rel:starts_with(k) then + new[tostring(repo:join(rel))] = 30 + elseif cwd == repo:join(k):parent() then + new[k] = 3 + end + end + end + return new +end + +local add = ya.sync(function(st, cwd, repo, changed) + st.dirs[cwd] = repo + st.repos[repo] = st.repos[repo] or {} + for k, v in pairs(changed) do + if v == 0 then + st.repos[repo][k] = nil + elseif v == 30 then + st.dirs[k] = "" + else + st.repos[repo][k] = v + end + end + ya.render() +end) + +local remove = ya.sync(function(st, cwd) + local dir = st.dirs[cwd] + if not dir then + return + end + + ya.render() + st.dirs[cwd] = nil + if not st.repos[dir] then + return + end + + for _, r in pairs(st.dirs) do + if r == dir then + return + end + end + st.repos[dir] = nil +end) + +local function setup(st, opts) + st.dirs = {} + st.repos = {} + + opts = opts or {} + opts.order = opts.order or 1500 + + -- Chosen by ChatGPT fairly, PRs are welcome to adjust them + local t = THEME.git or {} + local styles = { + [6] = t.modified and ui.Style(t.modified) or ui.Style():fg("#ffa500"), + [5] = t.added and ui.Style(t.added) or ui.Style():fg("#32cd32"), + [4] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("#a9a9a9"), + [3] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("#696969"), + [2] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("#ff4500"), + [1] = t.updated and ui.Style(t.updated) or ui.Style():fg("#1e90ff"), + } + local signs = { + [6] = t.modified_sign and t.modified_sign or "", + [5] = t.added_sign and t.added_sign or "", + [4] = t.untracked_sign and t.untracked_sign or "", + [3] = t.ignored_sign and t.ignored_sign or "", + [2] = t.deleted_sign and t.deleted_sign or "", + [1] = t.updated_sign and t.updated_sign or "U", + } + + Linemode:children_add(function(self) + local url = self._file.url + local dir = st.dirs[tostring(url:parent())] + local change + if dir then + change = dir == "" and 3 or st.repos[dir][tostring(url):sub(#dir + 2)] + end + + if not change or signs[change] == "" then + return ui.Line("") + elseif self._file:is_hovered() then + return ui.Line { ui.Span(" "), ui.Span(signs[change]) } + else + return ui.Line { ui.Span(" "), ui.Span(signs[change]):style(styles[change]) } + end + end, opts.order) +end + +local function fetch(self) + local cwd = self.files[1].url:parent() + local repo = root(cwd) + if not repo then + remove(tostring(cwd)) + return 1 + end + + local paths = {} + for _, f in ipairs(self.files) do + paths[#paths + 1] = tostring(f.url) + end + + -- stylua: ignore + local output, err = Command("git") + :cwd(tostring(cwd)) + :args({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) + :args(paths) + :stdout(Command.PIPED) + :output() + if not output then + ya.err("Cannot spawn git command, error code " .. tostring(err)) + return 0 + end + + local changed, ignored = {}, {} + for line in output.stdout:gmatch("[^\r\n]+") do + local sign, path = match(line) + if sign == 30 then + ignored[path] = sign + else + changed[path] = sign + end + end + + if self.files[1].cha.is_dir then + ya.dict_merge(changed, bubble_up(changed)) + ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) + else + ya.dict_merge(changed, propagate_down(ignored, cwd, Url(repo))) + end + + for _, p in ipairs(paths) do + local s = p:sub(#repo + 2) + changed[s] = changed[s] or 0 + end + add(tostring(cwd), repo, changed) + + return 3 +end + +return { setup = setup, fetch = fetch } diff --git a/config/yazi/plugins/glow.yazi/README.md b/config/yazi/plugins/glow.yazi/README.md index fdfefa2a..abcacac6 100644 --- a/config/yazi/plugins/glow.yazi/README.md +++ b/config/yazi/plugins/glow.yazi/README.md @@ -1,9 +1,9 @@ # glow.yazi -Plugin for [Yazi](https://github.com/sxyazi/yazi) to preview markdown files with [glow](https://github.com/charmbracelet/glow). To install, clone the repo inside your `~/.config/yazi/plugins/`: +Plugin for [Yazi](https://github.com/sxyazi/yazi) to preview markdown files with [glow](https://github.com/charmbracelet/glow). To install, run the below mentioned command: ```bash -git clone https://github.com/Reledia/glow.yazi.git +ya pack -a Reledia/glow ``` then include it in your `yazi.toml` to use: diff --git a/config/yazi/plugins/glow.yazi/init.lua b/config/yazi/plugins/glow.yazi/init.lua index bad70d28..bb383042 100644 --- a/config/yazi/plugins/glow.yazi/init.lua +++ b/config/yazi/plugins/glow.yazi/init.lua @@ -9,6 +9,7 @@ function M:peek() tostring(self.area.w), tostring(self.file.url), }) + :env("CLICOLOR_FORCE", "1") :stdout(Command.PIPED) :stderr(Command.PIPED) :spawn() @@ -57,9 +58,13 @@ function M:seek(units) end function M:fallback_to_builtin() - local _, bound = ya.preview_code(self) + local err, bound = ya.preview_code(self) if bound then - ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) + ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) + elseif err and not err:find("cancelled", 1, true) then + ya.preview_widgets(self, { + ui.Paragraph(self.area, { ui.Line(err):reverse() }), + }) end end diff --git a/config/yazi/plugins/hexyl.yazi/README.md b/config/yazi/plugins/hexyl.yazi/README.md index 7848a0e1..029dde08 100644 --- a/config/yazi/plugins/hexyl.yazi/README.md +++ b/config/yazi/plugins/hexyl.yazi/README.md @@ -1,9 +1,9 @@ # hexyl.yazi -Preview any file on [Yazi](https://github.com/sxyazi/yazi) using [hexyl](https://github.com/sharkdp/hexyl). To install, clone the repo inside your `~/.config/yazi/plugins/`: +Preview any file on [Yazi](https://github.com/sxyazi/yazi) using [hexyl](https://github.com/sharkdp/hexyl). To install, use the `ya pack` cli utility: ```bash -git clone https://github.com/Reledia/hexyl.yazi.git +ya pack -a Reledia/hexyl ``` then include it in your `yazi.toml` to use: @@ -15,7 +15,7 @@ append_previewers = [ ] ``` -Make sure you have [hexyl](https://github.com/sharkdp/hexyl) installed, and can be found in `PATH`. +Make sure you have [hexyl](https://github.com/sharkdp/hexyl) installed, and that can be found in `PATH`. ## Preview diff --git a/config/yazi/plugins/hexyl.yazi/init.lua b/config/yazi/plugins/hexyl.yazi/init.lua index 63d044f4..91f05f2c 100644 --- a/config/yazi/plugins/hexyl.yazi/init.lua +++ b/config/yazi/plugins/hexyl.yazi/init.lua @@ -2,7 +2,7 @@ local M = {} function M:peek() local child - local l = self.file.cha.length + local l = self.file.cha.len if l == 0 then child = Command("hexyl") :args({ @@ -43,13 +43,10 @@ function M:peek() child:start_kill() if self.skip > 0 and i < self.skip + limit then - ya.manager_emit( - "peek", - { tostring(math.max(0, i - limit)), only_if = tostring(self.file.url), upper_bound = "" } - ) + ya.manager_emit("peek", { math.max(0, i - limit), only_if = self.file.url, upper_bound = true }) else lines = lines:gsub("\t", string.rep(" ", PREVIEW.tab_size)) - ya.preview_widgets(self, { ui.Paragraph.parse(self.area, lines) }) + ya.preview_widgets(self, { ui.Text.parse(lines):area(self.area) }) end end diff --git a/config/yazi/plugins/hide-preview.yazi/README.md b/config/yazi/plugins/hide-preview.yazi/README.md index 999db112..67fc777b 100644 --- a/config/yazi/plugins/hide-preview.yazi/README.md +++ b/config/yazi/plugins/hide-preview.yazi/README.md @@ -7,7 +7,7 @@ https://github.com/yazi-rs/plugins/assets/17523360/c4f0b5c4-ff9f-4be8-ba73-4d8e7 ## Installation ```sh -ya pack -a yazi-rs/plugins#hide-preview +ya pack -a yazi-rs/plugins:hide-preview ``` ## Usage @@ -22,3 +22,20 @@ desc = "Hide or show preview" ``` Make sure the T key is not used elsewhere. + +## Advanced + +In addition to triggering the plugin with a keypress, you can also trigger it in your `init.lua` file: + +```lua +if os.getenv("NVIM") then + require("hide-preview"):entry() +end +``` + +In the example above, when it detects that you're [using Yazi in nvim](https://yazi-rs.github.io/docs/resources#vim), the +preview is hidden by default - you can always press `T` (or any key you've bound) to display it again. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/hide-preview.yazi/init.lua b/config/yazi/plugins/hide-preview.yazi/init.lua index 50403216..ad4df0b5 100644 --- a/config/yazi/plugins/hide-preview.yazi/init.lua +++ b/config/yazi/plugins/hide-preview.yazi/init.lua @@ -18,4 +18,6 @@ local function entry(st) ya.app_emit("resize", {}) end -return { entry = entry } +local function enabled(st) return st.old ~= nil end + +return { entry = entry, enabled = enabled } diff --git a/config/yazi/plugins/max-preview.yazi/README.md b/config/yazi/plugins/max-preview.yazi/README.md index ba11df18..439309ad 100644 --- a/config/yazi/plugins/max-preview.yazi/README.md +++ b/config/yazi/plugins/max-preview.yazi/README.md @@ -7,7 +7,7 @@ https://github.com/yazi-rs/plugins/assets/17523360/8976308e-ebfe-4e9e-babe-153eb ## Installation ```sh -ya pack -a yazi-rs/plugins#max-preview +ya pack -a yazi-rs/plugins:max-preview ``` ## Usage @@ -22,3 +22,26 @@ desc = "Maximize or restore preview" ``` Make sure the T key is not used elsewhere. + +## Tips + +This plugin only maximizes the "available preview area", without actually changing the content size. + +This means that the appearance of your preview largely depends on the previewer you are using. +However, most previewers tend to make the most of the available space, so this usually isn't an issue. + +For image previews, you may want to tune up the [`max_width`][max-width] and [`max_height`][max-height] options in your `yazi.toml`: + +```toml +[preview] +# Change them to your desired values +max_width = 1000 +max_height = 1000 +``` + +[max-width]: https://yazi-rs.github.io/docs/configuration/yazi/#preview.max_width +[max-height]: https://yazi-rs.github.io/docs/configuration/yazi/#preview.max_height + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/max-preview.yazi/init.lua b/config/yazi/plugins/max-preview.yazi/init.lua index d3d6083c..ae4efe66 100644 --- a/config/yazi/plugins/max-preview.yazi/init.lua +++ b/config/yazi/plugins/max-preview.yazi/init.lua @@ -17,4 +17,6 @@ local function entry(st) ya.app_emit("resize", {}) end -return { entry = entry } +local function enabled(st) return st.old ~= nil end + +return { entry = entry, enabled = enabled } diff --git a/config/yazi/plugins/miller.yazi/README.md b/config/yazi/plugins/miller.yazi/README.md index 172674e9..0cd410c0 100644 --- a/config/yazi/plugins/miller.yazi/README.md +++ b/config/yazi/plugins/miller.yazi/README.md @@ -1,6 +1,6 @@ # miller.yazi -[Miller](https://github.com/johnkerl/miller) now in [yazi](https://github.com/sxyazi/yazi). To install, clone the repo inside `~/.config/yazi/plugins/`: +[Miller](https://github.com/johnkerl/miller) now in [yazi](https://github.com/sxyazi/yazi). To install, use the command `ya pack -a Reledia/miller` and add to your `yazi.toml`: ```toml [plugin] @@ -20,7 +20,8 @@ To change colors of keys and values, edit the `init.lua` file after the `--key-c ## Other types of file To adapt this plugin to the other format supported from miller (like json): + - copy the plugin folder -- change the name of the copied folder into miller_(fmt) +- change the name of the copied folder into miller\_(fmt) - change the `--icsv` flag inside `init.lua` to `--i(fmt)` - add the correct mime/name rule into `prepend_previewers` and the exec as `miller_(fmt)` diff --git a/config/yazi/plugins/nbpreview.yazi/README.md b/config/yazi/plugins/nbpreview.yazi/README.md index cb8dfcd4..73a15242 100644 --- a/config/yazi/plugins/nbpreview.yazi/README.md +++ b/config/yazi/plugins/nbpreview.yazi/README.md @@ -8,11 +8,14 @@ View your Jupyter notebooks beautifully in the preview in Yazi. - [nbpreview](https://github.com/paw-lu/nbpreview) ## Previews + image ## Installation ```bash +ya pack -a AnirudhG07/nbpreview + ## For linux and MacOS git clone https://github.com/AnirudhG07/nbpreview.yazi.git ~/.config/yazi/plugins/nbpreview.yazi @@ -69,4 +72,8 @@ You can change the default give color scheme and theme to any you like. > [!Note] > -> The loading of `ipynb` might appear slow. This is due to the lag created by the command itself and not because of the plugin or yazi +> The loading of `ipynb` might appear slow. This is due to the lag created by the command itself and not because of the plugin or yazi + +## Explore Yazi + +Yazi is an amazing, blazing fast terminal file manager, with a variety of plugins, flavors and themes. Check them out at [awesome-yazi](https://github.com/AnirudhG07/awesome-yazi) and the official [yazi webpage](https://yazi-rs.github.io/). diff --git a/config/yazi/plugins/nbpreview.yazi/init.lua b/config/yazi/plugins/nbpreview.yazi/init.lua index b62bc0ca..4d3fff84 100644 --- a/config/yazi/plugins/nbpreview.yazi/init.lua +++ b/config/yazi/plugins/nbpreview.yazi/init.lua @@ -68,9 +68,13 @@ function M:seek(units) end function M:fallback_to_builtin() - local _, bound = ya.preview_code(self) + local err, bound = ya.preview_code(self) if bound then - ya.manager_emit("peek", { tostring(bound), only_if = tostring(self.file.url), upper_bound = "" }) + ya.manager_emit("peek", { bound, only_if = self.file.url, upper_bound = true }) + elseif err and not err:find("cancelled", 1, true) then + ya.preview_widgets(self, { + ui.Paragraph(self.area, { ui.Line(err):reverse() }), + }) end end diff --git a/config/yazi/plugins/ouch.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY b/config/yazi/plugins/ouch.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY new file mode 100644 index 00000000..e69de29b diff --git a/config/yazi/plugins/ouch.yazi/LICENSE b/config/yazi/plugins/ouch.yazi/LICENSE new file mode 100644 index 00000000..3f9d766f --- /dev/null +++ b/config/yazi/plugins/ouch.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 ndtoan96 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/yazi/plugins/ouch.yazi/README.md b/config/yazi/plugins/ouch.yazi/README.md new file mode 100644 index 00000000..2b49af2c --- /dev/null +++ b/config/yazi/plugins/ouch.yazi/README.md @@ -0,0 +1,67 @@ +# ouch.yazi + +[ouch](https://github.com/ouch-org/ouch) plugin for [Yazi](https://github.com/sxyazi/yazi). + +![ouch.yazi](https://github.com/ndtoan96/ouch.yazi/assets/33489972/946397ec-b37b-4bf4-93f1-c676fc8e59f2) + +## Features +- Archive preview +- Compression + +## Installation + +```bash +# Linux/macOS +git clone https://github.com/ndtoan96/ouch.yazi.git ~/.config/yazi/plugins/ouch.yazi + +# Windows +git clone https://github.com/ndtoan96/ouch.yazi.git %AppData%\yazi\config\plugins\ouch.yazi +``` + +Make sure you have [ouch](https://github.com/ouch-org/ouch) installed and in your `PATH`. + +## Usage + +### Preview +For archive preview, add this to your `yazi.toml`: + +```toml +[plugin] +prepend_previewers = [ + # Archive previewer + { mime = "application/*zip", run = "ouch" }, + { mime = "application/x-tar", run = "ouch" }, + { mime = "application/x-bzip2", run = "ouch" }, + { mime = "application/x-7z-compressed", run = "ouch" }, + { mime = "application/x-rar", run = "ouch" }, + { mime = "application/x-xz", run = "ouch" }, +] +``` + +Now go to an archive on Yazi, you should see the archive's content in the preview pane. You can use `Alt-j` and `Alt-k` to roll up and down the preview. + +If you want to change the icon or the style of text, you can modify the `peek` function in `init.lua` file (all of them are stored in the `lines` variable). + +### Compression +For compession, add this to your `keymap.toml`: + +```toml +[[manager.prepend_keymap]] +on = ["C"] +run = "plugin ouch --args=zip" +desc = "Compress with ouch" +``` + +The `--args=zip` part tells the plugin that default format is `zip`. You can change that to whatever format you want. + +### Decompression +This plugin does not provide a decompression feature because it already is supported by Yazi. +To decompress with `ouch`, configure the opener in `yazi.toml`. + +```toml +[opener] +extract = [ + { run = 'ouch d -y "%*"', desc = "Extract here with ouch", for = "windows" }, + { run = 'ouch d -y "$@"', desc = "Extract here with ouch", for = "unix" }, +] +``` diff --git a/config/yazi/plugins/ouch.yazi/init.lua b/config/yazi/plugins/ouch.yazi/init.lua new file mode 100644 index 00000000..c6f2e699 --- /dev/null +++ b/config/yazi/plugins/ouch.yazi/init.lua @@ -0,0 +1,143 @@ +local M = {} + +function M:peek() + local child = Command("ouch") + :args({ "l", "-t", "-y", tostring(self.file.url) }) + :stdout(Command.PIPED) + :stderr(Command.PIPED) + :spawn() + local limit = self.area.h + local file_name = string.match(tostring(self.file.url), ".*[/\\](.*)") + local lines = string.format("\x1b[2m📁 %s\x1b[0m\n", file_name) + local num_lines = 1 + local num_skip = 0 + repeat + local line, event = child:read_line() + if event == 1 then + ya.err(tostring(event)) + elseif event ~= 0 then + break + end + + if line:find('Archive', 1, true) ~= 1 and line:find('[INFO]', 1, true) ~= 1 then + if num_skip >= self.skip then + lines = lines .. line + num_lines = num_lines + 1 + else + num_skip = num_skip + 1 + end + end + until num_lines >= limit + + child:start_kill() + if self.skip > 0 and num_lines < limit then + ya.manager_emit( + "peek", + { tostring(math.max(0, self.skip - (limit - num_lines))), only_if = tostring(self.file.url), upper_bound = "" } + ) + else + ya.preview_widgets(self, { ui.Paragraph.parse(self.area, lines) }) + end +end + +function M:seek(units) + local h = cx.active.current.hovered + if h and h.url == self.file.url then + local step = math.floor(units * self.area.h / 10) + ya.manager_emit("peek", { + math.max(0, cx.active.preview.skip + step), + only_if = tostring(self.file.url), + }) + end +end + +-- Check if file exists +local function file_exists(name) + local f = io.open(name, "r") + if f ~= nil then + io.close(f) + return true + else + return false + end +end + +-- Get the files that need to be compressed and infer a default archive name +local get_compression_target = ya.sync(function() + local tab = cx.active + local default_name + local paths = {} + if #tab.selected == 0 then + if tab.current.hovered then + local name = tab.current.hovered.name + default_name = name + table.insert(paths, name) + else + return + end + else + default_name = tab.current.cwd:name() + for _, url in pairs(tab.selected) do + table.insert(paths, tostring(url)) + end + -- The compression targets are aquired, now unselect them + ya.manager_emit("escape", {}) + end + return paths, default_name +end) + +local function invoke_compress_command(paths, name) + local cmd_output, err_code = Command("ouch") + :args({ "c", "-y" }) + :args(paths) + :arg(name) + :stderr(Command.PIPED) + :output() + if err_code ~= nil then + ya.notify({ + title = "Failed to run ouch command", + content = "Status: " .. err_code, + timeout = 5.0, + level = "error", + }) + elseif not cmd_output.status.success then + ya.notify({ + title = "Compression failed: status code " .. cmd_output.status.code, + content = cmd_output.stderr, + timeout = 5.0, + level = "error", + }) + end +end + +function M:entry(args) + local default_fmt = args[1] + + -- Get the files that need to be compressed and infer a default archive name + local paths, default_name = get_compression_target() + + -- Get archive name from user + local output_name, name_event = ya.input({ + title = "Create archive:", + value = default_name .. "." .. default_fmt, + position = { "top-center", y = 3, w = 40 }, + }) + if name_event ~= 1 then + return + end + + -- Get confirmation if file exists + if file_exists(output_name) then + local confirm, confirm_event = ya.input({ + title = "Overwrite " .. output_name .. "? (y/N)", + position = { "top-center", y = 3, w = 40 }, + }) + if not (confirm_event == 1 and confirm:lower() == "y") then + return + end + end + + invoke_compress_command(paths, output_name) +end + +return M diff --git a/config/yazi/plugins/relative-motions.yazi/README.md b/config/yazi/plugins/relative-motions.yazi/README.md index b3b50afa..dcf6ea4e 100644 --- a/config/yazi/plugins/relative-motions.yazi/README.md +++ b/config/yazi/plugins/relative-motions.yazi/README.md @@ -6,39 +6,14 @@ https://github.com/dedukun/relative-motions.yazi/assets/25795432/04fb186a-5efe-4 ## Requirements -- [Yazi](https://github.com/sxyazi/yazi) v0.2.4+ +- [Yazi](https://github.com/sxyazi/yazi) v0.3.0+ ## Installation -
-Yazi v0.2.5 and before (manual installation) - ```sh -# Linux/macOS -git clone https://github.com/dedukun/relative-motions.yazi.git ~/.config/yazi/plugins/relative-motions.yazi - -# Windows -git clone https://github.com/dedukun/relative-motions.yazi.git %AppData%\yazi\config\plugins\relative-motions.yazi -``` - -
- -
-Latest commit in Yazi (package manager) - -```sh -# Add the plugin ya pack -a dedukun/relative-motions - -# Install plugin -ya pack -i - -# Update plugin -ya pack -u ``` -
- ## Configuration If you want to use the numbers directly to start a motion add this to your `keymap.toml`: @@ -122,10 +97,10 @@ require("relative-motions"):setup({ show_numbers="relative", show_motion = true ``` > [!NOTE] -> The `show_numbers` and `show_motion` functionalities overwrite [`Current:render`](https://github.com/sxyazi/yazi/blob/e51e8ad789914b2ab4a9485da7aa7fbc7b3bb450/yazi-plugin/preset/components/current.lua#L5) -> and [`Status:render`](https://github.com/sxyazi/yazi/blob/e51e8ad789914b2ab4a9485da7aa7fbc7b3bb450/yazi-plugin/preset/components/status.lua#L111) respectively. +> The `show_numbers` and `show_motion` functionalities overwrite [`Current:render`](https://github.com/sxyazi/yazi/blob/43b5ae0e6cc5c8ee96462651f01d78a0d98077fc/yazi-plugin/preset/components/current.lua#L26) +> and [`Status:children_render`](https://github.com/sxyazi/yazi/blob/43b5ae0e6cc5c8ee96462651f01d78a0d98077fc/yazi-plugin/preset/components/status.lua#L172) respectively. > If you have custom implementations for any of this functions -> you can add the provided `File:number` and `Status:motion` to your implementations, just check [here](https://github.com/dedukun/relative-motions.yazi/blob/main/init.lua#L39) how we are doing things. +> you can add the provided `Entity:number` and `Status:motion` to your implementations, just check [here](https://github.com/dedukun/relative-motions.yazi/blob/main/init.lua#L126) how we are doing things. ## Usage diff --git a/config/yazi/plugins/relative-motions.yazi/init.lua b/config/yazi/plugins/relative-motions.yazi/init.lua index eed035f7..0ab659e5 100644 --- a/config/yazi/plugins/relative-motions.yazi/init.lua +++ b/config/yazi/plugins/relative-motions.yazi/init.lua @@ -34,22 +34,24 @@ local SHOW_NUMBERS_RELATIVE_ABSOLUTE = 2 ----------------- R E N D E R ----------------- ----------------------------------------------- -local render_motion_setup = ya.sync(function() +local render_motion_setup = ya.sync(function(_) ya.render() Status.motion = function() return ui.Span("") end - Status.render = function(self, area) - self.area = area - - local left = ui.Line { self:mode(), self:size(), self:name() } - local right = ui.Line { self:motion(), self:permissions(), self:percentage(), self:position() } - return { - ui.Paragraph(area, { left }), - ui.Paragraph(area, { right }):align(ui.Paragraph.RIGHT), - table.unpack(Progress:render(area, right:width())), - } + Status.children_render = function(self, side) + local lines = {} + if side == self.RIGHT then + lines[1] = self:motion(self) + end + for _, c in ipairs(side == self.RIGHT and self._right or self._left) do + lines[#lines + 1] = (type(c[1]) == "string" and self[c[1]] or c[1])(self) + end + return ui.Line(lines) end + + -- TODO: check why it doesn't work line this + -- Status:children_add(Status.motion, 100, Status.RIGHT) end) local render_motion = ya.sync(function(_, motion_num, motion_cmd) @@ -60,7 +62,7 @@ local render_motion = ya.sync(function(_, motion_num, motion_cmd) return ui.Span("") end - local style = self.style() + local style = self:style() local motion_span if not motion_cmd then @@ -81,7 +83,7 @@ end) local render_numbers = ya.sync(function(_, mode) ya.render() - File.number = function(_, index, file, hovered) + Entity.number = function(_, index, file, hovered) local idx if mode == SHOW_NUMBERS_RELATIVE then idx = math.abs(hovered - index) @@ -105,12 +107,10 @@ local render_numbers = ya.sync(function(_, mode) end end - Current.render = function(self, area) - self.area = area - - local files = Folder:by_kind(Folder.CURRENT).window + Current.render = function(self) + local files = self._folder.window if #files == 0 then - return self:empty(area) + return self:empty() end local hovered_index @@ -121,22 +121,17 @@ local render_numbers = ya.sync(function(_, mode) end end - local items, markers = {}, {} + local entities, linemodes = {}, {} for i, f in ipairs(files) do - items[#items + 1] = ui.ListItem(ui.Line(ya.flat { File:number(i, f, hovered_index), File:full(f) })) - :style(File:style(f)) + linemodes[#linemodes + 1] = Linemode:new(f):render() - -- Yanked/marked/selected files - local marker = File:marker(f) - if marker ~= 0 then - markers[#markers + 1] = { i, marker } - end + local entity = Entity:new(f) + entities[#entities + 1] = ui.Line({ Entity:number(i, f, hovered_index), entity:render() }):style(entity:style()) end - return ya.flat { - ui.List(area, items), - Folder:linemode(area, files), - Folder:markers(area, markers), + return { + ui.List(entities):area(self._area), + ui.Text(linemodes):area(self._area):align(ui.Text.RIGHT), } end end) diff --git a/config/yazi/plugins/starship.yazi/README.md b/config/yazi/plugins/starship.yazi/README.md index 274c5c55..a6fcab43 100644 --- a/config/yazi/plugins/starship.yazi/README.md +++ b/config/yazi/plugins/starship.yazi/README.md @@ -6,19 +6,21 @@ Starship prompt plugin for [Yazi](https://github.com/sxyazi/yazi) ## Requirements -- [Yazi](https://github.com/sxyazi/yazi) v0.2.4+ +- [Yazi](https://github.com/sxyazi/yazi) +- [starship](https://github.com/starship/starship) ## Installation -### Linux / MacOS - -```sh -git clone https://github.com/Rolv-Apneseth/starship.yazi.git ~/.config/yazi/plugins/starship.yazi +```bash +ya pack -a Rolv-Apneseth/starship ``` -### Windows +### Manual ```sh +# Linux / MacOS +git clone https://github.com/Rolv-Apneseth/starship.yazi.git ~/.config/yazi/plugins/starship.yazi +# Windows git clone https://github.com/Rolv-Apneseth/starship.yazi.git %AppData%\yazi\config\plugins\starship.yazi ``` @@ -30,60 +32,70 @@ Add this to `~/.config/yazi/init.lua`: require("starship"):setup() ``` +If you wish to define a custom config file for `starship` to use, you can pass in a path +to the setup function like this: + +```lua +starship:setup({ config_file = "/home/rolv/.config/starship_secondary.toml" }) +``` + Make sure you have [starship](https://github.com/starship/starship) installed and in your `PATH`. ## Extra -If you use a `starship` theme with a background colour, it might look a bit to cramped on just the one line `Yazi` gives the header by default. You can add some space for the header by either using the [full border tip](https://yazi-rs.github.io/docs/tips/#full-border) from the `Yazi` docs, or add this slightly modified version (which won't add in the borders) to your `init.lua`: +If you use a `starship` theme with a background colour, it might look a bit to cramped on just the one line `Yazi` gives the header by default. To fix this, you can add this to your `init.lua`:
Click to expand ```lua -function Manager:render(area) - local chunks = self:layout(area) - +local old_build = Tab.build +Tab.build = function(self, ...) local bar = function(c, x, y) - x, y = math.max(0, x), math.max(0, y) + if x <= 0 or x == self._area.w - 1 then + return ui.Bar(ui.Rect.default, ui.Bar.TOP) + end + return ui.Bar( ui.Rect({ x = x, - y = y, - w = ya.clamp(0, area.w - x, 1), - h = math.min(1, area.h), + y = math.max(0, y), + w = ya.clamp(0, self._area.w - x, 1), + h = math.min(1, self._area.h), }), ui.Bar.TOP ):symbol(c) end - return ya.flat({ - ui.Bar(chunks[1], ui.Bar.RIGHT) - :symbol(THEME.manager.border_symbol) - :style(THEME.manager.border_style), - ui.Bar(chunks[3], ui.Bar.LEFT) - :symbol(THEME.manager.border_symbol) - :style(THEME.manager.border_style), + local c = self._chunks + self._chunks = { + c[1]:padding(ui.Padding.y(1)), + c[2]:padding(ui.Padding(c[1].w > 0 and 0 or 1, c[3].w > 0 and 0 or 1, 1, 1)), + c[3]:padding(ui.Padding.y(1)), + } - bar("┬", chunks[1].right - 1, chunks[1].y), - bar("┴", chunks[1].right - 1, chunks[1].bottom - 1), - bar("┬", chunks[2].right, chunks[2].y), - bar("┴", chunks[2].right, chunks[1].bottom - 1), + local style = THEME.manager.border_style + self._base = ya.list_merge(self._base or {}, { + -- Enable for full border + --[[ ui.Border(self._area, ui.Border.ALL):type(ui.Border.ROUNDED):style(style), ]] + ui.Bar(self._chunks[1], ui.Bar.RIGHT):style(style), + ui.Bar(self._chunks[3], ui.Bar.LEFT):style(style), - -- Parent - Parent:render(chunks[1]:padding(ui.Padding.xy(1))), - -- Current - Current:render(chunks[2]:padding(ui.Padding.y(1))), - -- Preview - Preview:render(chunks[3]:padding(ui.Padding.xy(1))), + bar("┬", c[1].right - 1, c[1].y), + bar("┴", c[1].right - 1, c[1].bottom - 1), + bar("┬", c[2].right, c[2].y), + bar("┴", c[2].right, c[1].bottom - 1), }) + + old_build(self, ...) end ```
> [!NOTE] -> This works by overriding your `Manager:render` function so make sure this is the only place you're doing that in your config +> This works by overriding your `Tab.build` function so make sure this is the only place you're doing that in your config. For example, this would be incompatible with the [full-border plugin](https://github.com/yazi-rs/plugins/tree/main/full-border.yazi) -## Acknowledgements +## Thanks - [sxyazi](https://github.com/sxyazi) for providing the code for this plugin and the demo video [in this comment](https://github.com/sxyazi/yazi/issues/767#issuecomment-1977082834) diff --git a/config/yazi/plugins/starship.yazi/init.lua b/config/yazi/plugins/starship.yazi/init.lua index 2b70b799..b8672c54 100644 --- a/config/yazi/plugins/starship.yazi/init.lua +++ b/config/yazi/plugins/starship.yazi/init.lua @@ -1,27 +1,78 @@ local save = ya.sync(function(st, cwd, output) - if cx.active.current.cwd == Url(cwd) then - st.output = output - ya.render() - end + if cx.active.current.cwd == Url(cwd) then + st.output = output + ya.render() + end +end) + +-- Helper function for accessing the `config_file` state variable +---@return string +local get_config_file = ya.sync(function(st) + return st.config_file end) return { - setup = function(st) - Header.cwd = function() - local cwd = cx.active.current.cwd - if st.cwd ~= cwd then - st.cwd = cwd - ya.manager_emit("plugin", { st._name, args = ya.quote(tostring(cwd)) }) - end + ---User arguments for setup method + ---@class SetupArgs + ---@field config_file string Absolute path to a starship config file - return ui.Line.parse(st.output or "") - end - end, + --- Setup plugin + --- @param st table State + --- @param args SetupArgs|nil + setup = function(st, args) + -- Replace default header widget + Header:children_remove(1, Header.LEFT) + Header:children_add(function() + return ui.Line.parse(st.output or "") + end, 1000, Header.LEFT) - entry = function(_, args) - local output = Command("starship"):arg("prompt"):cwd(args[1]):env("STARSHIP_SHELL", ""):output() - if output then - save(args[1], output.stdout:gsub("^%s+", "")) - end - end, + -- Check for custom starship config file + if args ~= nil and args.config_file ~= nil then + local url = Url(args.config_file) + if url.is_regular then + local config_file = args.config_file + + -- Manually replace '~' and '$HOME' at the start of the path with the OS environment variable + local home = os.getenv("HOME") + if home then + home = tostring(home) + config_file = config_file:gsub("^~", home):gsub("^$HOME", home) + end + + st.config_file = config_file + end + end + + -- Pass current working directory and custom config path (if specified) to the plugin's entry point + ---Callback for subscribers to update the prompt + local callback = function() + local cwd = cx.active.current.cwd + if st.cwd ~= cwd then + st.cwd = cwd + ya.manager_emit("plugin", { + st._id, + args = ya.quote(tostring(cwd), true), + }) + end + end + + -- Subscribe to events + ps.sub("cd", callback) + ps.sub("tab", callback) + end, + + entry = function(_, args) + local command = Command("starship"):arg("prompt"):cwd(args[1]):env("STARSHIP_SHELL", "") + + -- Point to custom starship config + local config_file = get_config_file() + if config_file then + command = command:env("STARSHIP_CONFIG", config_file) + end + + local output = command:output() + if output then + save(args[1], output.stdout:gsub("^%s+", "")) + end + end, } diff --git a/config/yazi/plugins/what-size.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY b/config/yazi/plugins/what-size.yazi/DO_NOT_MODIFY_ANYTHING_IN_THIS_DIRECTORY new file mode 100644 index 00000000..e69de29b diff --git a/config/yazi/plugins/what-size.yazi/LICENSE b/config/yazi/plugins/what-size.yazi/LICENSE new file mode 100644 index 00000000..dc368715 --- /dev/null +++ b/config/yazi/plugins/what-size.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Francesco Pira + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/config/yazi/plugins/what-size.yazi/README.md b/config/yazi/plugins/what-size.yazi/README.md new file mode 100644 index 00000000..f2be2cd7 --- /dev/null +++ b/config/yazi/plugins/what-size.yazi/README.md @@ -0,0 +1,39 @@ +# what-size.yazi + +A plugin for [yazi](https://github.com/sxyazi/yazi) to calculate the size of the current selection or the current working directory (if no selection is made). + +## Requirements + +- `du` + +## Installation + +```sh +ya pack -a 'pirafrank/what-size' +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[manager] +prepend_keymap = [ + { on = [ ".", "s" ], run = "plugin what-size", desc = "Calc size of selection or cwd" }, +] +``` + +If you want to copy the result to clipboard, you can add `--clipboard` or `-c` as first argument: + +```toml +[manager] +prepend_keymap = [ + { on = [ ".", "s" ], run = "plugin what-size --args='--clipboard'", desc = "Calc size of selection or cwd" }, +] +``` + +Change to whatever keybinding you like. + +## License + +MIT diff --git a/config/yazi/plugins/what-size.yazi/init.lua b/config/yazi/plugins/what-size.yazi/init.lua new file mode 100644 index 00000000..f4eea8b1 --- /dev/null +++ b/config/yazi/plugins/what-size.yazi/init.lua @@ -0,0 +1,72 @@ + +-- function to get paths of selected elements or current directory +-- of no elements are selected +local get_paths = ya.sync(function() + local paths = {} + -- get selected files + for _, u in pairs(cx.active.selected) do + paths[#paths + 1] = tostring(u) + end + -- if no files are selected, get current directory + if #paths == 0 then + if cx.active.current.cwd then + paths[1] = tostring(cx.active.current.cwd) + else + ya.err("what-size would return nil paths") + end + end + return paths +end) + +-- Function to get total size from du output +local get_total_size = function(s) + local lines = {} + for line in s:gmatch("[^\n]+") do lines[#lines + 1] = line end + local last_line = lines[#lines] + local last_line_parts = {} + for part in last_line:gmatch("%S+") do last_line_parts[#last_line_parts + 1] = part end + local total_size = last_line_parts[1] + return total_size +end + +-- Function to format file size +local function format_size(size) + local units = { "B", "KB", "MB", "GB", "TB" } + local unit_index = 1 + + while size > 1024 and unit_index < #units do + size = size / 1024 + unit_index = unit_index + 1 + end + + return string.format("%.2f %s", size, units[unit_index]) +end + +return { + entry = function(self, args) + -- defaults not to use clipboard, use it only if required by the user + local clipboard = args[1] == '--clipboard' or args[1] == '-c' + local items = get_paths() + + local cmd = "du" + local output, err = Command(cmd):arg("-scb"):args(items):output() + if not output then + ya.err("Failed to run diff, error: " .. err) + else + local total_size = get_total_size(output.stdout) + local formatted_size = format_size(tonumber(total_size)) + + local notification_content = "Total size: " .. formatted_size + if clipboard then + ya.clipboard(formatted_size) + notification_content = notification_content .. "\nCopied to clipboard." + end + + ya.notify { + title = "What size", + content = notification_content, + timeout = 5, + } + end + end, +} diff --git a/config/yazi/plugins/yatline.yazi/README.md b/config/yazi/plugins/yatline.yazi/README.md index d8d7dcf6..6e381349 100644 --- a/config/yazi/plugins/yatline.yazi/README.md +++ b/config/yazi/plugins/yatline.yazi/README.md @@ -1,14 +1,19 @@ # yatline.yazi The first Yazi plugin for customizing both header-line and status-line. -![yatline](https://github.com/imsi32/yatline.yazi/assets/81227251/dade37fb-e258-478d-8c8e-f6224f0f31c5) -![select_mode](https://github.com/imsi32/yatline.yazi/assets/81227251/2a624fb4-7154-45ae-bbb3-81c8f6836972) +![yatline](https://github.com/user-attachments/assets/61013ec8-7fd9-42df-a9f4-f254663871fe) + +> [!NOTE] +> Check out [wiki](https://github.com/imsi32/yatline.yazi/wiki) for installation steps, configuration and further information. ## Features - Lualine-like Design - Flexible - Simple - Automatic Configuration +- Themes (See: [yatline-themes](https://github.com/imsi32/yatline-themes)) +- Add-ons (See: [yatline-addons](https://github.com/imsi32/yatline-addons)) -> [!NOTE] -> Check out [wiki](https://github.com/imsi32/yatline.yazi/wiki) for installation steps, configuration and further information. +## Credits +- [Lualine](https://github.com/nvim-lualine/lualine.nvim) +- [Yazi](https://github.com/sxyazi/yazi) diff --git a/config/yazi/plugins/yatline.yazi/init.lua b/config/yazi/plugins/yatline.yazi/init.lua index 67e88165..d7089e27 100644 --- a/config/yazi/plugins/yatline.yazi/init.lua +++ b/config/yazi/plugins/yatline.yazi/init.lua @@ -22,12 +22,12 @@ -- Type Declaration -- --==================-- +Yatline = {} + local Side = { LEFT = 0, RIGHT = 1 } local SeparatorType = { OUTER = 0, INNER = 1 } local ComponentType = { A = 0, B = 1, C = 2 } -os.setlocale("") - --=========================-- -- Variable Initialization -- --=========================-- @@ -58,7 +58,6 @@ local permissions_x_fg local permissions_s_fg local tab_width -local tab_use_inverse local selected_icon local copied_icon @@ -82,9 +81,6 @@ local task_processed_fg local show_background -local display_header_line -local display_status_line - local section_order = { "section_a", "section_b", "section_c" } --=================-- @@ -104,42 +100,6 @@ local function set_mode_style(mode) end end ---- Sets the style of the separator according to the parameters. ---- While selecting component type of both previous and following components, ---- always think separator is in middle of two components ---- and previous component is in left side and following component is in right side. ---- Thus, side of component does not important when choosing these two components. ---- @param separator_type SeparatorType Where will there be a separator in the section. ---- @param component_type ComponentType Which section component will be in [ a | b | c ]. -local function set_separator_style(separator_type, component_type) - separator_style = { bg = nil, fg = nil } - if separator_type == SeparatorType.OUTER then - if component_type == ComponentType.A then - separator_style.bg = style_b.bg - separator_style.fg = style_a.bg - elseif component_type == ComponentType.B then - separator_style.bg = style_c.bg - separator_style.fg = style_b.bg - else - separator_style.fg = style_c.bg - if show_background then - separator_style.bg = style_c.bg - end - end - else - if component_type == ComponentType.A then - separator_style.bg = style_a.bg - separator_style.fg = style_a.fg - elseif component_type == ComponentType.B then - separator_style.bg = style_b.bg - separator_style.fg = style_b.fg - else - separator_style.bg = style_c.bg - separator_style.fg = style_c.fg - end - end -end - --- Sets the style of the component according to the its type. --- @param component Span Component that will be styled. --- @param component_type ComponentType Which section component will be in [ a | b | c ]. @@ -161,9 +121,20 @@ end --- @return Line line A Line which has component and separator. local function connect_separator(component, side, separator_type) local open, close - if separator_type == SeparatorType.OUTER then + if separator_type == SeparatorType.OUTER and not (separator_style.bg == "reset" and separator_style.fg == "reset") then open = ui.Span(section_separator_open) close = ui.Span(section_separator_close) + + if separator_style.fg == "reset" then + if separator_style.bg ~= "reset" and separator_style.bg ~= nil then + open = ui.Span(inverse_separator_open) + close = ui.Span(inverse_separator_close) + + separator_style.fg, separator_style.bg = separator_style.bg, separator_style.fg + else + return ui.Line { component } + end + end else open = ui.Span(part_separator_open) close = ui.Span(part_separator_close) @@ -179,46 +150,6 @@ local function connect_separator(component, side, separator_type) end end ---- Creates a component from given string according to other parameters. ---- @param string string The text which will be shown inside of the component. ---- @param component_type ComponentType Which section component will be in [ a | b | c ]. ---- @return Line line Customized Line which follows desired style of the parameters. ---- @see set_mode_style To know how mode style selected. ---- @see set_separator_style To know how separator style applied. ---- @see set_component_style To know how component style applied. ---- @see connect_separator To know how component and separator connected. -local function create_component_from_str(string, component_type) - local span = ui.Span(" " .. string .. " ") - set_mode_style(cx.active.mode) - set_component_style(span, component_type) - - return ui.Line{span} -end - ---- Creates a component from given Coloreds according to other parameters. ---- The component it created, can contain multiple strings with different foreground color. ---- @param coloreds Coloreds The array which contains an array which contains text which will be shown inside of the component and its foreground color. ---- @param component_type ComponentType Which section component will be in [ a | b | c ]. ---- @return Line line Customized Line which follows desired style of the parameters. ---- @see set_mode_style To know how mode style selected. ---- @see set_separator_style To know how separator style applied. ---- @see set_component_style To know how component style applied. ---- @see connect_separator To know how component and separator connected. -local function create_component_from_coloreds(coloreds, component_type) - set_mode_style(cx.active.mode) - - local spans = {} - for i, colored in ipairs(coloreds) do - local span = ui.Span(colored[1]) - set_component_style(span, component_type) - span:fg(colored[2]) - - spans[i] = span - end - - return ui.Line(spans) -end - --==================-- -- Helper Functions -- --==================-- @@ -248,15 +179,31 @@ local function reverse_order(array) return reversed end ---==================-- --- Getter Functions -- ---==================-- +--========================-- +-- Component String Group -- +--========================-- -local get = {} +Yatline.string = {} +Yatline.string.get = {} +Yatline.string.has_separator = true + +--- Creates a component from given string according to other parameters. +--- @param string string The text which will be shown inside of the component. +--- @param component_type ComponentType Which section component will be in [ a | b | c ]. +--- @return Line line Customized Line which follows desired style of the parameters. +--- @see set_mode_style To know how mode style selected. +--- @see set_component_style To know how component style applied. +function Yatline.string.create(string, component_type) + local span = ui.Span(" " .. string .. " ") + set_mode_style(cx.active.mode) + set_component_style(span, component_type) + + return ui.Line{span} +end --- Gets the hovered file's name of the current active tab. --- @return string name Current active tab's hovered file's name. -function get:hovered_name() +function Yatline.string.get:hovered_name() local hovered = cx.active.current.hovered if hovered then return hovered.name @@ -267,10 +214,10 @@ end --- Gets the hovered file's path of the current active tab. --- @return string path Current active tab's hovered file's path. -function get:hovered_path() +function Yatline.string.get:hovered_path() local hovered = cx.active.current.hovered if hovered then - return tostring(hovered.url) + return ya.readable_path(tostring(hovered.url)) else return "" end @@ -278,7 +225,7 @@ end --- Gets the hovered file's size of the current active tab. --- @return string size Current active tab's hovered file's size. -function get:hovered_size() +function Yatline.string.get:hovered_size() local hovered = cx.active.current.hovered if hovered then return ya.readable_size(hovered:size() or hovered.cha.length) @@ -289,7 +236,7 @@ end --- Gets the hovered file's path of the current active tab. --- @return string mime Current active tab's hovered file's path. -function get:hovered_mime() +function Yatline.string.get:hovered_mime() local hovered = cx.active.current.hovered if hovered then return hovered:mime() @@ -301,7 +248,7 @@ end --- Gets the hovered file's extension of the current active tab. --- @param show_icon boolean Whether or not an icon will be shown. --- @return string file_extension Current active tab's hovered file's extension. -function get:hovered_file_extension(show_icon) +function Yatline.string.get:hovered_file_extension(show_icon) local hovered = cx.active.current.hovered if hovered then @@ -327,13 +274,27 @@ end --- Gets the path of the current active tab. --- @return string path Current active tab's path. -function get:tab_path() - return cx.active.current.cwd +function Yatline.string.get:tab_path() + local cwd = cx.active.current.cwd + local filter = cx.active.current.files.filter + + local search = cwd.is_search and string.format(" (search: %s", cwd:frag()) or "" + + local suffix + if not filter then + suffix = search == "" and search or search .. ")" + elseif search == "" then + suffix = string.format(" (filter: %s)", tostring(filter)) + else + suffix = string.format("%s, filter: %s)", search, tostring(filter)) + end + + return ya.readable_path(tostring(cx.active.current.cwd)) .. suffix end --- Gets the mode of active tab. --- @return string mode Active tab's mode. -function get:tab_mode() +function Yatline.string.get:tab_mode() local mode = tostring(cx.active.mode):upper() if mode == "UNSET" then mode = "UN-SET" @@ -344,13 +305,13 @@ end --- Gets the number of files in the current active tab. --- @return string num_files Number of files in the current active tab. -function get:tab_num_files() +function Yatline.string.get:tab_num_files() return tostring(#cx.active.current.files) end --- Gets the cursor position in the current active tab. --- @return string cursor_position Current active tab's cursor position. -function get:cursor_position() +function Yatline.string.get:cursor_position() local cursor = cx.active.current.cursor local length = #cx.active.current.files @@ -363,7 +324,7 @@ end --- Gets the cursor position as percentage which is according to the number of files inside of current active tab. --- @return string percentage Percentage of current active tab's cursor position and number of percentages. -function get:cursor_percentage() +function Yatline.string.get:cursor_percentage() local percentage = 0 local cursor = cx.active.current.cursor local length = #cx.active.current.files @@ -384,15 +345,25 @@ end --- @param format string Format for giving desired date or time values. --- @return string date Date or time values. --- @see os.date To see how format works. -function get:date(format) +function Yatline.string.get:date(format) return tostring(os.date(format)) end ---=====================-- --- Component Functions -- ---=====================-- +--======================-- +-- Component Line Group -- +--======================-- -local create = {} +Yatline.line = {} +Yatline.line.get = {} +Yatline.line.has_separator = false + +--- To follow component group naming and functions, returns the given line without any changes. +--- @param line Line The line already pre-defined. +--- @param component_type ComponentType Which section component will be in [ a | b | c ]. Will not be used. +--- @return Line line The given line as an input. +function Yatline.line.create(line, component_type) + return line +end --- Creates and returns line component for tabs. --- @param side Side Left or right side of the either header-line or status-line. @@ -400,7 +371,7 @@ local create = {} --- @see set_mode_style To know how mode style selected. --- @see set_component_style To know how component style applied. --- @see connect_separator To know how component and separator connected. -function create:tabs(side) +function Yatline.line.get:tabs(side) local tabs = #cx.tabs local lines = {} @@ -423,12 +394,18 @@ function create:tabs(side) set_mode_style(cx.tabs[i].mode) set_component_style(span, ComponentType.A) - separator_style.fg = style_a.bg - if show_background then - separator_style.bg = style_c.bg - end + if style_a.bg ~= "reset" or show_background then + separator_style.fg = style_a.bg + if show_background then + separator_style.bg = style_c.bg + end - lines[#lines + 1] = connect_separator(span, in_side, SeparatorType.OUTER) + lines[#lines + 1] = connect_separator(span, in_side, SeparatorType.OUTER) + else + separator_style.fg = style_a.fg + + lines[#lines + 1] = connect_separator(span, in_side, SeparatorType.INNER) + end else local span = ui.Span(" " .. text .. " ") if show_background then @@ -441,22 +418,29 @@ function create:tabs(side) set_mode_style(cx.tabs[i + 1].mode) local open, close - if tab_use_inverse then - separator_style.fg = style_a.bg - if show_background then - separator_style.bg = style_c.bg - end + if style_a.bg ~= "reset" or ( show_background and style_c.bg ~= "reset" ) then + if not show_background or ( show_background and style_c.bg == "reset" ) then + separator_style.fg = style_a.bg + if show_background then + separator_style.bg = style_c.bg + end - open = ui.Span(inverse_separator_open) - close = ui.Span(inverse_separator_close) + open = ui.Span(inverse_separator_open) + close = ui.Span(inverse_separator_close) + else + separator_style.bg = style_a.bg + if show_background then + separator_style.fg = style_c.bg + end + + open = ui.Span(section_separator_open) + close = ui.Span(section_separator_close) + end else - separator_style.bg = style_a.bg - if show_background then - separator_style.fg = style_c.bg - end + separator_style.fg = style_c.fg - open = ui.Span(section_separator_open) - close = ui.Span(section_separator_close) + open = ui.Span(part_separator_open) + close = ui.Span(part_separator_close) end open:style(separator_style) @@ -490,15 +474,39 @@ function create:tabs(side) end end ---====================-- --- Coloreds Functions -- ---====================-- +--==========================-- +-- Component Coloreds Group -- +--==========================-- -local colorize = {} +Yatline.coloreds = {} +Yatline.coloreds.get = {} +Yatline.coloreds.has_separator = true + +--- Creates a component from given Coloreds according to other parameters. +--- The component it created, can contain multiple strings with different foreground color. +--- @param coloreds Coloreds The array which contains an array which contains text which will be shown inside of the component and its foreground color. +--- @param component_type ComponentType Which section component will be in [ a | b | c ]. +--- @return Line line Customized Line which follows desired style of the parameters. +--- @see set_mode_style To know how mode style selected. +--- @see set_component_style To know how component style applied. +function Yatline.coloreds.create(coloreds, component_type) + set_mode_style(cx.active.mode) + + local spans = {} + for i, colored in ipairs(coloreds) do + local span = ui.Span(colored[1]) + set_component_style(span, component_type) + span:fg(colored[2]) + + spans[i] = span + end + + return ui.Line(spans) +end --- Gets the hovered file's permissions of the current active tab. --- @return Coloreds coloreds Current active tab's hovered file's permissions -function colorize:permissions() +function Yatline.coloreds.get:permissions() local hovered = cx.active.current.hovered if hovered then @@ -534,7 +542,7 @@ end --- Gets the number of selected and yanked files of the active tab. --- @return Coloreds coloreds Active tab's number of selected and yanked files. -function colorize:count() +function Yatline.coloreds.get:count() local num_yanked = #cx.yanked local num_selected = #cx.active.selected @@ -557,7 +565,7 @@ end --- Gets the number of task states. --- @return Coloreds coloreds Number of task states. -function colorize:task_states() +function Yatline.coloreds.get:task_states() local tasks = cx.tasks.progress local coloreds = { @@ -571,7 +579,7 @@ end --- Gets the number of task workloads. --- @return Coloreds coloreds Number of task workloads. -function colorize:task_workload() +function Yatline.coloreds.get:task_workload() local tasks = cx.tasks.progress local coloreds = { @@ -587,20 +595,20 @@ end --- @param fg Color Desired foreground color. --- @param params? table Array of parameters of string based component. It is optional. --- @return Coloreds coloreds Array of solely array of string based component's string and desired foreground color. -function colorize:string_based_component(component_name, fg, params) - local getter = get[component_name] +function Yatline.coloreds.get:string_based_component(component_name, fg, params) + local getter = Yatline.string.get[component_name] if getter then local output if params then - output = getter(get, table.unpack(params)) + output = getter(Yatline.string.get, table.unpack(params)) else output = getter() end if output ~= nil and output ~= "" then - return { { output, fg } } + return { { " " .. output .. " ", fg } } else return "" end @@ -613,149 +621,86 @@ end -- Configuration -- --===============-- ---- Connects given components with configured separator +--- Configure separators if it is need to be added to the components. +--- Connects them with each component. +--- @param section_components table Array of components in one of the sections. +--- @param component_type ComponentType Which section component will be in [ a | b | c ]. +--- @param in_side Side Left or right side of the either header-line or status-line. +--- @param num_section_b_components integer Number of components in section-b. +--- @param num_section_c_components integer Number of components in section-c. +--- @return table section_line_components Array of line components whether or not connected with separators. +--- @see connect_separator To know how component and separator connected. +local function config_components_separators(section_components, component_type, in_side, num_section_b_components, num_section_c_components) + local num_section_components = #section_components + local section_line_components = {} + for i, component in ipairs(section_components) do + if component[2] == true then + separator_style = { bg = nil, fg = nil } + + local separator_type + if i ~= num_section_components then + if component_type == ComponentType.A then + separator_style = style_a + elseif component_type == ComponentType.B then + separator_style = style_b + else + separator_style = style_c + end + + separator_type = SeparatorType.INNER + else + if component_type == ComponentType.A then + separator_style.fg = style_a.bg + elseif component_type == ComponentType.B then + separator_style.fg = style_b.bg + else + separator_style.fg = style_c.bg + end + + if component_type == ComponentType.A and num_section_b_components ~= 0 then + separator_style.bg = style_b.bg + else + if num_section_c_components == 0 or component_type == ComponentType.C then + if show_background then + separator_style.bg = style_c.bg + end + else + separator_style.bg = style_c.bg + end + end + + separator_type = SeparatorType.OUTER + end + + section_line_components[i] = connect_separator(component[1], in_side, separator_type) + else + if in_side == Side.LEFT then + section_line_components[i] = component[1] + else + section_line_components[i] = component[1] + end + end + end + + return section_line_components +end + +--- Leads the given parameters to the other functions. --- @param section_a_components table Components array whose components are in section-a of either side. --- @param section_b_components table Components array whose components are in section-b of either side. --- @param section_c_components table Components array whose components are in section-c of either side. ---- @param side Side Left or right side of the either header-line or status-line. +--- @param in_side Side Left or right side of the either header-line or status-line. --- @return table section_a_line_components Array of components whose components are connected to separator and are in section-a of either side. --- @return table section_b_line_components Array of components whose components are connected to separator and are in section-b of either side. --- @return table section_c_line_components Array of components whose components are connected to separator and are in section-c of either side. -local function config_separator(section_a_components, section_b_components, section_c_components, side) - local num_section_a_components = #section_a_components +--- @see config_components_separators To know how separators are configured. +local function config_components(section_a_components, section_b_components, section_c_components, in_side) local num_section_b_components = #section_b_components local num_section_c_components = #section_c_components - local section_a_line_components = {} - for i, component in ipairs(section_a_components) do - if component[2] == true then - separator_style = { bg = nil, fg = nil } - - local open, close - if i ~= num_section_a_components then - separator_style.bg = style_a.bg - separator_style.fg = style_a.fg - - open = ui.Span(part_separator_open) - close = ui.Span(part_separator_close) - else - separator_style.fg = style_a.bg - - if num_section_b_components == 0 and num_section_c_components == 0 then - if show_background then - separator_style.bg = style_c.bg - end - elseif num_section_b_components == 0 then - separator_style.bg = style_c.bg - else - separator_style.bg = style_b.bg - end - - open = ui.Span(section_separator_open) - close = ui.Span(section_separator_close) - end - - open:style(separator_style) - close:style(separator_style) - - if side == Side.LEFT then - section_a_line_components[i] = ui.Line { component[1], close } - else - section_a_line_components[i] = ui.Line { open, component[1] } - end - else - if side == Side.LEFT then - section_a_line_components[i] = component[1] - else - section_a_line_components[i] = component[1] - end - end - end - - local section_b_line_components = {} - for i, component in ipairs(section_b_components) do - if component[2] == true then - separator_style = { bg = nil, fg = nil } - - local open, close - if i ~= num_section_b_components then - separator_style.bg = style_b.bg - separator_style.fg = style_b.fg - - open = ui.Span(part_separator_open) - close = ui.Span(part_separator_close) - else - separator_style.fg = style_b.bg - - if num_section_c_components == 0 then - if show_background then - separator_style.bg = style_c.bg - end - else - separator_style.bg = style_c.bg - end - - open = ui.Span(section_separator_open) - close = ui.Span(section_separator_close) - end - - open:style(separator_style) - close:style(separator_style) - - if side == Side.LEFT then - section_b_line_components[i] = ui.Line { component[1], close } - else - section_b_line_components[i] = ui.Line { open, component[1] } - end - else - if side == Side.LEFT then - section_b_line_components[i] = component[1] - else - section_b_line_components[i] = component[1] - end - - end - end - - local section_c_line_components = {} - for i, component in ipairs(section_c_components) do - if component[2] == true then - separator_style = { bg = nil, fg = nil } - - local open, close - if i ~= num_section_c_components then - separator_style.bg = style_c.bg - separator_style.fg = style_c.fg - - open = ui.Span(part_separator_open) - close = ui.Span(part_separator_close) - else - separator_style.fg = style_c.bg - - if show_background then - separator_style.bg = style_c.bg - end - - open = ui.Span(section_separator_open) - close = ui.Span(section_separator_close) - end - - open:style(separator_style) - close:style(separator_style) - - if side == Side.LEFT then - section_c_line_components[i] = ui.Line { component[1], close } - else - section_c_line_components[i] = ui.Line { open, component[1] } - end - else - if side == Side.LEFT then - section_c_line_components[i] = component[1] - else - section_c_line_components[i] = component[1] - end - end - end + local section_a_line_components = config_components_separators(section_a_components, ComponentType.A, in_side, num_section_b_components, num_section_c_components) + local section_b_line_components = config_components_separators(section_b_components, ComponentType.B, in_side, num_section_b_components, num_section_c_components) + local section_c_line_components = config_components_separators(section_c_components, ComponentType.C, in_side, num_section_b_components, num_section_c_components) return section_a_line_components, section_b_line_components, section_c_line_components end @@ -786,60 +731,24 @@ local function config_side(side) end for _, component in ipairs(components) do - if component.type == "string" then + local component_group = Yatline[component.type] + + if component_group then if component.custom then - section_components[#section_components + 1] = { create_component_from_str(component.name, in_section), true } + section_components[#section_components + 1] = { component_group.create(component.name, in_section), component_group.has_separator } else - local getter = get[component.name] + local getter = component_group.get[component.name] if getter then local output if component.params then - output = getter(get, table.unpack(component.params)) + output = getter(component_group.get, table.unpack(component.params)) else output = getter() end if output ~= nil and output ~= "" then - section_components[#section_components + 1] = { create_component_from_str(output, in_section), true } - end - end - end - elseif component.type == "coloreds" then - if component.custom then - section_components[#section_components + 1] = { create_component_from_coloreds(component.name, in_section), true } - else - local colorizer = colorize[component.name] - - if colorizer then - local output - if component.params then - output = colorizer(colorize, table.unpack(component.params)) - else - output = colorizer() - end - - if output ~= nil and output ~= "" then - section_components[#section_components + 1] = { create_component_from_coloreds(output, in_section), true } - end - end - end - elseif component.type == "line" then - if component.custom then - section_components[#section_components + 1] = component.name - else - local creator = create[component.name] - - if creator then - local output - if component.params then - output = creator(create, table.unpack(component.params)) - else - output = creator() - end - - if output then - section_components[#section_components + 1] = { output, false } + section_components[#section_components + 1] = { component_group.create(output, in_section), component_group.has_separator } end end end @@ -854,10 +763,12 @@ end --- @param side Config Configuration of either left or right side. --- @return table left_components Components array whose components are in left side of the line. --- @return table right_components Components array whose components are in right side of the line. +--- @see config_side To know how components are gotten from side's config. +--- @see config_components To know how components are configured. local function config_line(side, in_side) local section_a_components, section_b_components, section_c_components = config_side(side) - local section_a_line_components, section_b_line_components, section_c_line_components = config_separator(section_a_components, section_b_components, section_c_components, in_side) + local section_a_line_components, section_b_line_components, section_c_line_components = config_components(section_a_components, section_b_components, section_c_components, in_side) if in_side == Side.RIGHT then section_a_line_components = reverse_order(section_a_line_components) @@ -907,64 +818,149 @@ end return { setup = function(_, config) - section_separator_open = config.section_separator.open - section_separator_close = config.section_separator.close + config = config or {} - inverse_separator_open = config.inverse_separator.open - inverse_separator_close = config.inverse_separator.close + tab_width = config.tab_width or 20 - part_separator_open = config.part_separator.open - part_separator_close = config.part_separator.close + local component_positions = config.component_positions or { "header", "tab", "status" } - style_a = { bg = config.style_a.bg_mode.normal, fg = config.style_a.fg } - style_b = config.style_b - style_c = config.style_c + show_background = config.show_background or false - style_a_normal_bg = config.style_a.bg_mode.normal - style_a_select_bg = config.style_a.bg_mode.select - style_a_un_set_bg = config.style_a.bg_mode.un_set + local display_header_line = config.display_header_line + if display_header_line == nil then + display_header_line = true + end - permissions_t_fg = config.permissions_t_fg - permissions_r_fg = config.permissions_r_fg - permissions_w_fg = config.permissions_w_fg - permissions_x_fg = config.permissions_x_fg - permissions_s_fg = config.permissions_s_fg + local display_status_line = config.display_status_line + if display_status_line == nil then + display_status_line = true + end - tab_width = config.tab_width - tab_use_inverse = config.tab_use_inverse + local header_line = config.header_line or { left = { section_a = {}, section_b = {}, section_c = {} }, right = { section_a = {}, section_b = {}, section_c = {} } } + local status_line = config.status_line or { left = { section_a = {}, section_b = {}, section_c = {} }, right = { section_a = {}, section_b = {}, section_c = {} } } - selected_icon = config.selected.icon - copied_icon = config.copied.icon - cut_icon = config.cut.icon + if config.theme then + config = config.theme + end - selected_fg = config.selected.fg - copied_fg = config.copied.fg - cut_fg = config.cut.fg + if config.section_separator then + section_separator_open = config.section_separator.open + section_separator_close = config.section_separator.close + else + section_separator_open = "" + section_separator_close = "" + end - task_total_icon = config.total.icon - task_succ_icon = config.succ.icon - task_fail_icon = config.fail.icon - task_found_icon = config.found.icon - task_processed_icon = config.processed.icon + if config.inverse_separator then + inverse_separator_open = config.inverse_separator.open + inverse_separator_close = config.inverse_separator.close + else + inverse_separator_open = "" + inverse_separator_close = "" + end - task_total_fg = config.total.fg - task_succ_fg = config.succ.fg - task_fail_fg = config.fail.fg - task_found_fg = config.found.fg - task_processed_fg = config.processed.fg + if config.part_separator then + part_separator_open = config.part_separator.open + part_separator_close = config.part_separator.close + else + part_separator_open = "" + part_separator_close = "" + end - show_background = config.show_background + if config.style_a then + style_a = { bg = config.style_a.bg_mode.normal, fg = config.style_a.fg } - display_header_line = config.display_header_line - display_status_line = config.display_status_line + style_a_normal_bg = config.style_a.bg_mode.normal + style_a_select_bg = config.style_a.bg_mode.select + style_a_un_set_bg = config.style_a.bg_mode.un_set + else + style_a = { bg = "white", fg = "black" } + + style_a_normal_bg = "white" + style_a_select_bg = "brightyellow" + style_a_un_set_bg = "brightred" + end + + style_b = config.style_b or { bg = "brightblack", fg = "brightwhite" } + style_c = config.style_c or { bg = "black", fg = "brightwhite" } + + permissions_t_fg = config.permissions_t_fg or "green" + permissions_r_fg = config.permissions_r_fg or "yellow" + permissions_w_fg = config.permissions_w_fg or "red" + permissions_x_fg = config.permissions_x_fg or "cyan" + permissions_s_fg = config.permissions_s_fg or "white" + + if config.selected then + selected_fg = config.selected.fg + selected_icon = config.selected.icon + else + selected_fg = "yellow" + selected_icon = "󰻭" + end + + if config.copied then + copied_fg = config.copied.fg + copied_icon = config.copied.icon + else + copied_fg = "green" + copied_icon = "" + end + + if config.cut then + cut_icon = config.cut.icon + cut_fg = config.cut.fg + else + cut_icon = "" + cut_fg = "red" + end + + if config.total then + task_total_icon = config.total.icon + task_total_fg = config.total.fg + else + task_total_icon = "󰮍" + task_total_fg = "yellow" + end + + if config.succ then + task_succ_icon = config.succ.icon + task_succ_fg = config.succ.fg + else + task_succ_icon = "" + task_succ_fg = "green" + end + + if config.fail then + task_fail_icon = config.fail.icon + task_fail_fg = config.fail.fg + else + task_fail_icon = "" + task_fail_fg = "red" + end + + if config.found then + task_found_icon = config.found.icon + task_found_fg = config.found.fg + else + task_found_icon = "󰮕" + task_found_fg = "blue" + end + + if config.processed then + task_processed_icon = config.processed.icon + task_processed_fg = config.processed.fg + else + task_processed_icon = "󰐍" + task_processed_fg = "green" + end Progress.partial_render = function(self) local progress = cx.tasks.progress if progress.total == 0 then - return { config_paragraph(self.area) } + return { config_paragraph(self._area) } end - local gauge = ui.Gauge(self.area) + local gauge = ui.Gauge(self._area) if progress.fail == 0 then gauge = gauge:gauge_style(THEME.status.progress_normal) else @@ -984,55 +980,76 @@ return { } end - local header_number = 0 - local status_number = 0 - if display_header_line then - if show_line(config.header_line) then - Header.render = function(self, area) - self.area = area - - local left_line = config_line(config.header_line.left, Side.LEFT) - local right_line = config_line(config.header_line.right, Side.RIGHT) + if show_line(header_line) then + Header.render = function(self) + local left_line = config_line(header_line.left, Side.LEFT) + local right_line = config_line(header_line.right, Side.RIGHT) return { - config_paragraph(area, left_line), - ui.Paragraph(area, { right_line }):align(ui.Paragraph.RIGHT) + config_paragraph(self._area, left_line), + ui.Paragraph(self._area, { right_line }):align(ui.Paragraph.RIGHT) } end + + Header.children_add = function() return {} end + Header.children_remove = function() return {} end end else - header_number = 1 - function Header:render() return {} end - + Header.render = function() return {} end end if display_status_line then - if show_line(config.status_line) then - Status.render = function(self, area) - self.area = area - - local left_line = config_line(config.status_line.left, Side.LEFT) - local right_line = config_line(config.status_line.right, Side.RIGHT) + if show_line(status_line) then + Status.render = function(self) + local left_line = config_line(status_line.left, Side.LEFT) + local right_line = config_line(status_line.right, Side.RIGHT) return { - config_paragraph(area, left_line), - ui.Paragraph(area, { right_line }):align(ui.Paragraph.RIGHT), - table.unpack(Progress:render(area, right_line:width())), + config_paragraph(self._area, left_line), + ui.Paragraph(self._area, { right_line }):align(ui.Paragraph.RIGHT), + table.unpack(Progress:render(self._area, right_line:width())), } end + + Status.children_add = function() return {} end + Status.children_remove = function() return {} end end else - status_number = 1 - function Status:render() return {} end - + Status.render = function() return {} end end - if header_number + status_number ~= 0 then - local old_manager_render = Manager.render - function Manager:render(area) - return old_manager_render(self, ui.Rect { x = area.x, y = area.y - header_number, w = area.w, h = area.h + header_number + status_number }) + Root.layout = function(self) + local constraints = {} + for _, component in ipairs(component_positions) do + if (component == "header" and display_header_line) or (component == "status" and display_status_line) then + table.insert(constraints, ui.Constraint.Length(1)) + elseif component == "tab" then + table.insert(constraints, ui.Constraint.Fill(1)) + end end + + self._chunks = ui.Layout():direction(ui.Layout.VERTICAL):constraints(constraints):split(self._area) + end + + Root.build = function(self) + local childrens = {} + + local i = 1 + for _, component in ipairs(component_positions) do + if component == "header" and display_header_line then + table.insert(childrens, Header:new(self._chunks[i], cx.active)) + i = i + 1 + elseif component == "tab" then + table.insert(childrens, Tab:new(self._chunks[i], cx.active)) + i = i + 1 + elseif component == "status" and display_status_line then + table.insert(childrens, Status:new(self._chunks[i], cx.active)) + i = i + 1 + end + end + + self._children = childrens end end, } diff --git a/config/yazi/yazi.toml b/config/yazi/yazi.toml index c7a077f2..5719bb64 100644 --- a/config/yazi/yazi.toml +++ b/config/yazi/yazi.toml @@ -42,7 +42,10 @@ reveal = [ {run = 'explorer /select, "%1"', orphan = true, desc = "Reveal", for = "windows"}, {run = '''exiftool "$1"; echo "Press enter to exit"; read _''', block = true, desc = "Show EXIF", for = "unix"}, ] -extract = [ {run = 'unar "$1"', desc = "Extract here", for = "unix"}, {run = 'unar "%1"', desc = "Extract here", for = "windows"} ] +extract = [ + {run = 'ouch d -y "%*"', desc = "Extract here with ouch", for = "windows"}, + {run = 'ouch d -y "$@"', desc = "Extract here with ouch", for = "unix"}, +] play = [ {run = 'mpv "$@"', orphan = true, for = "unix"}, {run = 'mpv "%1"', orphan = true, for = "windows"}, @@ -87,7 +90,7 @@ previewers = [ {mime = "text/*", run = "code"}, {mime = "*/{xml,javascript,x-wine-extension-ini}", run = "code"}, # JSON - {mime = "application/json", run = "json"}, + {mime = "application/json", run = "code"}, # Image {mime = "image/vnd.djvu", run = "noop"}, {mime = "image/*", run = "image"}, @@ -95,20 +98,27 @@ previewers = [ {mime = "video/*", run = "video"}, # PDF {mime = "application/pdf", run = "pdf"}, - # Archive - {mime = "application/*zip", run = "archive"}, - {mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", run = "archive"}, # Fallback {name = "*", run = "file"}, ] -prepend_previewers = [ +prepend_previewers = [ ] + +append_previewers = [ {name = "*.ipynb", run = "nbpreview"}, {mime = "application/x-bittorrent", run = "torrent-preview"}, {mime = "audio/*", run = "exifaudio"}, {mime = "text/csv", run = "miller"}, {name = "*.{md,mdx,markdown}", run = "glow"}, + # Archive previewer + {mime = "application/*zip", run = "ouch"}, + {mime = "application/x-{tar,bzip*,7z-compressed,xz,rar}", run = "ouch"}, + {name = "*", run = "hexyl"}, +] + +prepend_fetchers = [ + {id = "git", name = "*", run = "git"}, + {id = "git", name = "*/", run = "git"}, ] -append_previewers = [ {name = "*", run = "hexyl"} ] [input] # cd diff --git a/config/zathura/zathurarc b/config/zathura/zathurarc index 98aa2205..05d6654a 120000 --- a/config/zathura/zathurarc +++ b/config/zathura/zathurarc @@ -1 +1 @@ -themes/rose-pine \ No newline at end of file +themes/zathura-gruvbox-light \ No newline at end of file diff --git a/config/zsh/.zshrc b/config/zsh/.zshrc index 08c44713..e577990d 100644 --- a/config/zsh/.zshrc +++ b/config/zsh/.zshrc @@ -91,16 +91,14 @@ lfcd() { fi } -yazicd() { - tmp="$(mktemp -uq)" - trap 'rm -f $tmp >/dev/null 2>&1 && trap - HUP INT QUIT TERM PWR EXIT' HUP INT QUIT TERM PWR EXIT - yazi "$@" --cwd-file="$tmp" - if [ -f "$tmp" ]; then - dir="$(cat "$tmp")" - [ -d "$dir" ] && [ "$dir" != "$(pwd)" ] && cd "$dir" || return - fi +function yazicd() { + local tmp="$(mktemp -t "yazi-cwd.XXXXXX")" + yazi "$@" --cwd-file="$tmp" + if cwd="$(command cat -- "$tmp")" && [ -n "$cwd" ] && [ "$cwd" != "$PWD" ]; then + builtin cd -- "$cwd" + fi + rm -f -- "$tmp" } - tmux-window-name() { ($TMUX_PLUGIN_MANAGER_PATH/tmux-window-name/scripts/rename_session_windows.py &) } diff --git a/dotter b/dotter old mode 100755 new mode 100644 diff --git a/dotter.arm b/dotter.arm old mode 100755 new mode 100644 diff --git a/dotter.exe b/dotter.exe old mode 100755 new mode 100644 diff --git a/local/share/applications/pdf.desktop b/local/share/applications/pdf.desktop index a0f17378..8cb66fdc 100644 --- a/local/share/applications/pdf.desktop +++ b/local/share/applications/pdf.desktop @@ -1,4 +1,4 @@ [Desktop Entry] Type=Application Name=PDF reader -Exec=/usr/bin/zathura -e %u +Exec=/usr/bin/zathura --fork %u