From 1dd8680f693e6b511ef83a0bd2f3079d2f8684f6 Mon Sep 17 00:00:00 2001 From: David Chen Date: Wed, 25 Jun 2025 19:47:17 -0700 Subject: [PATCH] upgrade yazi plugins --- yazi/package.toml | 24 +- yazi/plugins/compress.yazi/README.md | 175 ++++++- yazi/plugins/compress.yazi/main.lua | 670 +++++++++++++++++------- yazi/plugins/git.yazi/README.md | 41 +- yazi/plugins/git.yazi/main.lua | 183 ++++--- yazi/plugins/smart-enter.yazi/README.md | 8 +- yazi/plugins/smart-enter.yazi/main.lua | 4 +- yazi/plugins/starship.yazi/README.md | 13 +- yazi/plugins/starship.yazi/main.lua | 30 +- yazi/plugins/yamb.yazi/README.md | 16 +- yazi/plugins/yaziline.yazi/README.md | 42 +- yazi/plugins/yaziline.yazi/main.lua | 313 ++++++----- 12 files changed, 990 insertions(+), 529 deletions(-) diff --git a/yazi/package.toml b/yazi/package.toml index 3a63f00..291677e 100644 --- a/yazi/package.toml +++ b/yazi/package.toml @@ -1,32 +1,32 @@ [[plugin.deps]] use = "llanosrocas/yaziline" -rev = "e06c47f" -hash = "7df84299e89ffefd4686d1e20a295c8a" +rev = "e79b067" +hash = "f590c5b7d0730e8d6023b1b34ddf7ead" [[plugin.deps]] use = "Rolv-Apneseth/starship" -rev = "6c639b4" -hash = "5614c46d5de76623c283d415bfac8025" +rev = "6a0f3f7" +hash = "d5a74d95c2e8194ac352c5e10e81e64f" [[plugin.deps]] use = "yazi-rs/plugins:git" -rev = "5186af7" -hash = "771f18427fb75fb19990ce602bb322f4" +rev = "5eea960" +hash = "e01006bfb38951ea89031bd918b20dbb" [[plugin.deps]] use = "yazi-rs/plugins:smart-enter" -rev = "5186af7" -hash = "aef2b1a805b80cce573bb766f1459d88" +rev = "5eea960" +hash = "56fdabc96fc1f4d53c96eb884b02a5be" [[plugin.deps]] use = "h-hg/yamb" -rev = "3f7c51f" -hash = "e11b980e5635f0fbabd80931b1a1347e" +rev = "22af003" +hash = "7cc42012a7c2099f80064d228feb8d44" [[plugin.deps]] use = "KKV9/compress" -rev = "60b24af" -hash = "ee025be766240cc98e671754ac836da3" +rev = "9fc8fe0" +hash = "2ad086899bf368fba211a33d8f68c39d" [flavor] deps = [] diff --git a/yazi/plugins/compress.yazi/README.md b/yazi/plugins/compress.yazi/README.md index 385fe38..ae1f329 100644 --- a/yazi/plugins/compress.yazi/README.md +++ b/yazi/plugins/compress.yazi/README.md @@ -1,48 +1,173 @@ -# ~~archive.yazi~~ compress.yazi +

🗜️ compress.yazi

+

+ A blazing fast, flexible archive plugin for Yazi
+ Effortlessly compress your files and folders with style! +

-A Yazi plugin that compresses selected files to an archive. Supporting yazi versions 0.2.5 and up. +--- -## Supported file types +## 📖 Table of Contents -| Extention | Unix Command | Windows Command | -| ------------- | ------------- | --------------- | -| .zip | zip -r | 7z a -tzip | -| .7z | 7z a | 7z a | -| .tar | tar rpf | tar rpf | -| .tar.gz | gzip | 7z a -tgzip | -| .tar.xz | xz | 7z a -txz | -| .tar.bz2 | bzip2 | 7z a -tbzip2 | -| .tar.zst | zstd | zstd | +- [Features](#-features) +- [Supported File Types](#-supported-file-types) +- [Installation](#%EF%B8%8F-installation) +- [Keymap Example](#-keymap-example) +- [Usage](#%EF%B8%8F-usage) +- [Flags](#%EF%B8%8F-flags) +- [Tips](#-tips) +- [Credits](#-credits) +--- -**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. +## 🚀 Features +- 🗂️ **Multi-format support:** zip, 7z, rar, tar, tar.gz, tar.xz, tar.bz2, tar.zst, tar.lz4, tar.lha +- 🌍 **Cross-platform:** Works on Unix & Windows +- 🔒 **Password protection:** Secure your archives (zip/7z/rar) +- 🛡️ **Header encryption:** Hide file lists (7z/rar) +- ⚡ **Compression level:** Choose your balance of speed vs. size +- 🛑 **Overwrite safety:** Never lose files by accident +- 🎯 **Seamless Yazi integration:** Fast, native-like UX -## Install +--- + +## 📦 Supported File Types + +| Extension | Default Command | 7z Command | Bsdtar Command (Win10+ & Unix) | +| ------------- | ----------------- | -------------- | ------------------------------ | +| `.zip` | `zip -r` | `7z a -tzip` | `tar -caf` | +| `.7z` | `7z a` | `7z a` | | +| `.rar` | `rar a` | | | +| `.tar` | `tar rpf` | | `tar rpf` | +| `.tar.gz` | `tar rpf + gzip` | `7z a -tgzip` | `tar -czf` | +| `.tar.xz` | `tar rpf + xz` | `7z a -txz` | `tar -cJf` | +| `.tar.bz2` | `tar rpf + bzip2` | `7z a -tbzip2` | `tar -cjf` | +| `.tar.zst` | `tar rpf + zstd` | | `tar --zstd -cf` | +| `.tar.lz4` | `tar rpf + lz4` | | | +| `.tar.lha` | `tar rpf + lha` | | | + +--- + +## ⚡️ Installation ```bash -# For Unix platforms +# Unix git clone https://github.com/KKV9/compress.yazi.git ~/.config/yazi/plugins/compress.yazi -## For Windows +# Windows (CMD, not PowerShell!) git clone https://github.com/KKV9/compress.yazi.git %AppData%\yazi\config\plugins\compress.yazi # Or with yazi plugin manager -ya pack -a KKV9/compress +ya pkg add KKV9/compress ``` -- Add this to your `keymap.toml`: +--- + +### 🔧 Extras (Windows) + +To enable additional compression formats and features on Windows, follow these steps: + +1. **Install [7-Zip](https://www.7-zip.org/):** + Add `C:\Program Files\7-Zip` to your `PATH`. + This enables support for `.7z` archives and password-protected `.zip` files. + +2. **Alternative: Install [Nanazip](https://github.com/M2Team/NanaZip):** + A modern alternative to 7-Zip with similar functionality and extra features. + +3. **Install [WinRAR](https://www.win-rar.com/download.html):** + Add `C:\Program Files\WinRAR` to your `PATH`. + This enables support for `.rar` archives. + +4. **Install Additional Tools:** + To use formats like `lha`, `lz4`, `gzip`, etc., install their respective tools and ensure they are added to your `PATH`. + +--- + +## 🎹 Keymap Example + +Add this to your `keymap.toml`: + ```toml -[[manager.prepend_keymap]] -on = [ "c", "a" ] +[[mgr.prepend_keymap]] +on = [ "c", "a", "a" ] run = "plugin compress" desc = "Archive selected files" + +[[mgr.prepend_keymap]] +on = [ "c", "a", "p" ] +run = "plugin compress -p" +desc = "Archive selected files (password)" + +[[mgr.prepend_keymap]] +on = [ "c", "a", "h" ] +run = "plugin compress -ph" +desc = "Archive selected files (password+header)" + +[[mgr.prepend_keymap]] +on = [ "c", "a", "l" ] +run = "plugin compress -l" +desc = "Archive selected files (compression level)" + +[[mgr.prepend_keymap]] +on = [ "c", "a", "u" ] +run = "plugin compress -phl" +desc = "Archive selected files (password+header+level)" ``` -## Usage +--- - - Select files or folders to add, then press `c` `a` to create a new archive. - - Type a name for the new file. - - The file extention must match one of the supported filetype extentions. - - The desired archive/compression command must be installed on your system. +## 🛠️ Usage + +1. **Select files/folders** in Yazi. +2. Press c a to open the archive dialog. +3. Choose: + - a for a standard archive + - p for password protection (zip/7z/rar) + - h to encrypt header (7z/rar) + - l to set compression level (all compression algorithims) + - u for all options together +4. **Type a name** for your archive (or leave blank for suggested name). +5. **Enter password** and/or **compression level** if prompted. +6. **Overwrite protect** if a file already exists, the new file will be given a suffix _#. +7. Enjoy your shiny new archive! + +--- + +## 🏳️‍🌈 Flags + +- Combine flags for more power! +- when separating flags with spaces, make sure to single quote them (eg., `'-ph rar'`) +- `-p` Password protect (zip/7z/rar) +- `-h` Encrypt header (7z/rar) +- `-l` Set compression level (all compression algorithims) +- `` Specify a default extention (eg., `7z`, `tar.gz`) + +#### Combining multiple flags: +```toml +[[mgr.prepend_keymap]] +on = [ "c", "a", "7" ] +run = "plugin compress '-ph 7z'" +desc = "Archive selected files to 7z (password+header)" +[[mgr.prepend_keymap]] +on = [ "c", "a", "r" ] +run = "plugin compress '-p -l rar'" +desc = "Archive selected files to rar (password+level)" +``` + +--- + +## 💡 Tips + +- The file extension **must** match a supported type. +- The required compression tool **must** be installed and in your `PATH` (7zip/rar etc.). +- If no extention is provided, the default extention (zip) will be appended automatically. + +--- + +## 📣 Credits + +Made with ❤️ for [Yazi](https://github.com/sxyazi/yazi) by [KKV9](https://github.com/KKV9). +Contributions are welcome! Feel free to submit a pull request. + +--- diff --git a/yazi/plugins/compress.yazi/main.lua b/yazi/plugins/compress.yazi/main.lua index 333587f..e538c9c 100644 --- a/yazi/plugins/compress.yazi/main.lua +++ b/yazi/plugins/compress.yazi/main.lua @@ -1,228 +1,494 @@ --- Send error notification -local function notify_error(message, urgency) - ya.notify({ - title = "Archive", - content = message, - level = urgency, - timeout = 5, - }) -end - -- Check for windows local is_windows = ya.target_family() == "windows" +-- Define flags and strings +local is_password, is_encrypted, is_level, cmd_password, cmd_level, default_extension = false, false, false, "", "", "zip" --- Make table of selected or hovered: path = filenames -local selected_or_hovered = ya.sync(function() - local tab, paths, names, path_fnames = cx.active, {}, {}, {} - for _, u in pairs(tab.selected) do - paths[#paths + 1] = tostring(u:parent()) - names[#names + 1] = tostring(u:name()) - end - if #paths == 0 and tab.current.hovered then - paths[1] = tostring(tab.current.hovered.url:parent()) - names[1] = tostring(tab.current.hovered.name) - end - for idx, name in ipairs(names) do - if not path_fnames[paths[idx]] then - path_fnames[paths[idx]] = {} - end - table.insert(path_fnames[paths[idx]], name) - end - return path_fnames, tostring(tab.current.cwd) -end) +-- Function to check valid filename +local function is_valid_filename(name) + -- Trim whitespace from both ends + name = name:match("^%s*(.-)%s*$") + if name == "" then + return false + end + if is_windows then + -- Windows forbidden chars and reserved names + if name:find('[<>:"/\\|%?%*]') then + return false + end + else + -- Unix forbidden chars + if name:find("/") or name:find("%z") then + return false + end + end + return true +end --- Check if archive command is available +-- Function to send notifications +local function notify_error(message, urgency) + ya.notify( + { + title = "Archive", + content = message, + level = urgency, + timeout = 5 + } + ) +end + +-- Function to check if command is available local function is_command_available(cmd) - local stat_cmd - - if is_windows then - stat_cmd = string.format("where %s > nul 2>&1", cmd) - else - stat_cmd = string.format("command -v %s >/dev/null 2>&1", cmd) - end - - local cmd_exists = os.execute(stat_cmd) - if cmd_exists then - return true - else - return false - end + local stat_cmd + if is_windows then + stat_cmd = string.format("where %s > nul 2>&1", cmd) + else + stat_cmd = string.format("command -v %s >/dev/null 2>&1", cmd) + end + local cmd_exists = os.execute(stat_cmd) + if cmd_exists then + return true + else + 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 +-- Function to change command arrays --> string -- Use first command available or first command +local function find_command_name(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") - if f ~= nil then - io.close(f) - return true - else - return false - end -end - --- Append filename to it's parent directory +-- Function to append filename to it's parent directory url local function combine_url(path, file) - path, file = Url(path), Url(file) - return tostring(path:join(file)) + path, file = Url(path), Url(file) + return tostring(path:join(file)) end +-- Function to make a table of selected or hovered files: path = filenames +local selected_or_hovered = + ya.sync( + function() + local tab, paths, names, path_fnames = cx.active, {}, {}, {} + for _, u in pairs(tab.selected) do + paths[#paths + 1] = tostring(u.parent) + names[#names + 1] = tostring(u.name) + end + if #paths == 0 and tab.current.hovered then + paths[1] = tostring(tab.current.hovered.url.parent) + names[1] = tostring(tab.current.hovered.name) + end + for idx, name in ipairs(names) do + if not path_fnames[paths[idx]] then + path_fnames[paths[idx]] = {} + end + table.insert(path_fnames[paths[idx]], name) + end + return path_fnames, names, tostring(tab.current.cwd) + end +) + +-- Table of archive commands +local archive_commands = { + ["%.zip$"] = { + {command = "zip", args = {"-r"}, level_arg = "-", level_min = 0, level_max = 9, passwordable = true}, + { + command = {"7z", "7zz", "7za"}, + args = {"a", "-tzip"}, + level_arg = "-mx=", + level_min = 0, + level_max = 9, + passwordable = true + }, + { + command = {"tar", "bsdtar"}, + args = {"-caf"}, + level_arg = {"--option", "compression-level="}, + level_min = 1, + level_max = 9 + } + }, + ["%.7z$"] = { + { + command = {"7z", "7zz", "7za"}, + args = {"a"}, + level_arg = "-mx=", + level_min = 0, + level_max = 9, + header_arg = "-mhe=on", + passwordable = true + } + }, + ["%.rar$"] = { + { + command = "rar", + args = {"a"}, + level_arg = "-m", + level_min = 0, + level_max = 5, + header_arg = "-hp", + passwordable = true + } + }, + ["%.tar.gz$"] = { + {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "gzip"}, + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = {"a", "-tgzip"} + }, + { + command = {"tar", "bsdtar"}, + args = {"-czf"}, + level_arg = {"--option", "gzip:compression-level="}, + level_min = 1, + level_max = 9 + } + }, + ["%.tar.xz$"] = { + {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "xz"}, + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = {"a", "-txz"} + }, + { + command = {"tar", "bsdtar"}, + args = {"-cJf"}, + level_arg = {"--option", "xz:compression-level="}, + level_min = 1, + level_max = 9 + } + }, + ["%.tar.bz2$"] = { + {command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "bzip2"}, + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-mx=", + level_min = 1, + level_max = 9, + compress = "7z", + compress_args = {"a", "-tbzip2"} + }, + { + command = {"tar", "bsdtar"}, + args = {"-cjf"}, + level_arg = {"--option", "bzip2:compression-level="}, + level_min = 1, + level_max = 9 + } + }, + ["%.tar.zst$"] = { + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-", + level_min = 1, + level_max = 22, + compress = "zstd", + compress_args = {"--ultra"} + } + }, + ["%.tar.lz4$"] = { + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-", + level_min = 1, + level_max = 12, + compress = "lz4" + } + }, + ["%.tar.lha$"] = { + { + command = {"tar", "bsdtar"}, + args = {"rpf"}, + level_arg = "-o", + level_min = 5, + level_max = 7, + compress = "lha", + compress_args = {"-a"} + } + }, + ["%.tar$"] = { + {command = {"tar", "bsdtar"}, args = {"rpf"}} + } +} + return { - entry = function() - -- Exit visual mode - ya.manager_emit("escape", { visual = true }) + entry = function(_, job) + -- Parse flags and default extension + if job.args ~= nil then + for _, arg in ipairs(job.args) do + if arg:match("^%-(%w+)$") then + -- Handle combined flags (e.g., -phl) + for flag in arg:sub(2):gmatch(".") do + if flag == "p" then + is_password = true + elseif flag == "h" then + is_encrypted = true + elseif flag == "l" then + is_level = true + end + end + elseif arg:match("^%w+$") then + -- Handle default extension (e.g., 7z, zip) + if archive_commands["%." .. arg .. "$"] then + default_extension = arg + else + notify_error(string.format("Unsupported extension: %s", arg), "warn") + end + end + end + end - -- Define file table and output_dir (pwd) - local path_fnames, output_dir = selected_or_hovered() + -- Exit visual mode + ya.emit("escape", {visual = true}) + -- Define file table and output_dir (pwd) + local path_fnames, fnames, output_dir = selected_or_hovered() + -- Get archive filename + local output_name, event = + ya.input( + { + title = "Create archive:", + position = {"top-center", y = 3, w = 40} + } + ) + if event ~= 1 then + return + end - -- Get input - local output_name, event = ya.input({ - title = "Create archive:", - position = { "top-center", y = 3, w = 40 }, - }) - if event ~= 1 then - return - end + -- Determine the default name for the archive + local default_name = #fnames == 1 and fnames[1] or Url(output_dir).name + output_name = output_name == "" and string.format("%s.%s", default_name, default_extension) or output_name - -- Use appropriate archive command - local archive_commands = { - ["%.zip$"] = { command = "zip", args = { "-r" } }, - ["%.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" } }, - } + -- Add default extension if none is specified + if not output_name:match("%.%w+$") then + output_name = string.format("%s.%s", output_name, default_extension) + end - if is_windows then - archive_commands = { - ["%.zip$"] = { command = "7z", args = { "a", "-tzip" } }, - ["%.7z$"] = { command = "7z", args = { "a" } }, - ["%.tar.gz$"] = { - command = "tar", - args = { "rpf" }, - compress = "7z", - compress_args = { "a", "-tgzip", "-sdel", output_name }, - }, - ["%.tar.xz$"] = { - command = "tar", - args = { "rpf" }, - compress = "7z", - compress_args = { "a", "-txz", "-sdel", output_name }, - }, - ["%.tar.bz2$"] = { - command = "tar", - args = { "rpf" }, - 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 + -- Validate the final archive filename + if not is_valid_filename(output_name) then + notify_error("Invalid archive filename", "error") + return + end - -- Match user input to archive command - local archive_cmd, archive_args, archive_compress, archive_compress_args - for pattern, cmd_pair in pairs(archive_commands) do - if output_name:match(pattern) then - archive_cmd = cmd_pair.command - archive_args = cmd_pair.args - archive_compress = cmd_pair.compress - archive_compress_args = cmd_pair.compress_args or {} - end - end + -- Match user input to archive command + local archive_cmd, + archive_args, + archive_compress, + archive_level_arg, + archive_level_min, + archive_level_max, + archive_header_arg, + archive_passwordable, + archive_compress_args + local matched_pattern = false + for pattern, cmd_list in pairs(archive_commands) do + if output_name:match(pattern) then + matched_pattern = true -- Mark that file extension is correct + for _, cmd in ipairs(cmd_list) do + -- Check if archive_cmd is available + local find_command = type(cmd.command) == "table" and find_command_name(cmd.command) or cmd.command + if is_command_available(find_command) then + -- Check if compress_cmd (if listed) is available + if cmd.compress == nil or is_command_available(cmd.compress) then + archive_cmd = find_command + archive_args = cmd.args + archive_compress = cmd.compress or "" + archive_level_arg = is_level and cmd.level_arg or "" + archive_level_min = cmd.level_min + archive_level_max = cmd.level_max + archive_header_arg = is_encrypted and cmd.header_arg or "" + archive_passwordable = cmd.passwordable or false + archive_compress_args = cmd.compress_args or {} + break + end + end + end + if archive_cmd then + break + end + 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 extension + if not matched_pattern then + notify_error("Unsupported file extension", "error") + return + end - -- Check if no archive command is available for the extention - if not archive_cmd then - notify_error("Unsupported file extention", "error") - return - end + -- Check if no suitable archive program was found + if not archive_cmd then + notify_error("Could not find a suitable archive program for the selected file extension", "error") + return + end - -- 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 + -- Check if archive command has multiple names + if type(archive_cmd) == "table" then + archive_cmd = find_command_name(archive_cmd) + 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 + -- 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 - -- If file exists show overwrite prompt - local output_url = combine_url(output_dir, output_name) - while true do - if file_exists(output_url) then - local overwrite_answer = ya.input({ - title = "Overwrite " .. output_name .. "? y/N:", - position = { "top-center", y = 3, w = 40 }, - }) - if overwrite_answer:lower() ~= "y" then - notify_error("Operation canceled", "warn") - return -- If no overwrite selected, exit - else - local rm_status, rm_err = os.remove(output_url) - if not rm_status then - notify_error(string.format("Failed to remove %s, exit code %s", output_name, rm_err), "error") - return - end -- If overwrite fails, exit - end - end - if archive_compress and not output_name:match("%.tar$") then - output_name = output_name:match("(.*%.tar)") -- Test for .tar and .tar.* - output_url = combine_url(output_dir, output_name) -- Update output_url - else - break - end - 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 - -- Add to output archive in each path, their respective files - for path, names in pairs(path_fnames) do - local archive_status, archive_err = - Command(archive_cmd):args(archive_args):arg(output_url):args(names):cwd(path):spawn():wait() - if not archive_status or not archive_status.success then - notify_error( - string.format( - "%s with selected files failed, exit code %s", - archive_args, - archive_status and archive_status.code or archive_err - ), - "error" - ) - end - end + -- Add password arg if selected + if archive_passwordable and is_password then + local output_password, event = + ya.input( + { + title = "Enter password:", + obscure = true, + position = {"top-center", y = 3, w = 40} + } + ) + if event ~= 1 then + return + end + if output_password ~= "" then + cmd_password = "-P" .. output_password + if archive_cmd == "rar" and is_encrypted then + cmd_password = archive_header_arg .. output_password -- Add archive arg for rar + end + table.insert(archive_args, cmd_password) + end + end - -- Use compress command if needed - if archive_compress then - local compress_status, compress_err = - Command(archive_compress):args(archive_compress_args):arg(output_name):cwd(output_dir):spawn():wait() - if not compress_status or not compress_status.success then - notify_error( - string.format( - "%s with %s failed, exit code %s", - archive_compress, - output_name, - compress_status and compress_status.code or compress_err - ), - "error" - ) - end - end - end, + -- Add header arg if selected for 7z + if is_encrypted and archive_header_arg ~= "" and archive_cmd ~= "rar" then + table.insert(archive_args, archive_header_arg) + end + + -- Add level arg if selected + if archive_level_arg ~= "" and is_level then + local output_level, event = + ya.input( + { + title = string.format("Enter compression level (%s - %s)", archive_level_min, archive_level_max), + position = {"top-center", y = 3, w = 40} + } + ) + if event ~= 1 then + return + end + -- Validate user input for compression level + if + output_level ~= "" and tonumber(output_level) ~= nil and tonumber(output_level) >= archive_level_min and + tonumber(output_level) <= archive_level_max + then + cmd_level = + type(archive_level_arg) == "table" and archive_level_arg[#archive_level_arg] .. output_level or + archive_level_arg .. output_level + local target_args = archive_compress == "" and archive_args or archive_compress_args + if type(archive_level_arg) == "table" then + -- Insert each element of archive_level_arg (except last) into target_args at the correct position + for i = 1, #archive_level_arg - 1 do + table.insert(target_args, i, archive_level_arg[i]) + end + table.insert(target_args, #archive_level_arg, cmd_level) -- Add level at the end + else + -- Insert the compression level argument at the start if not a table + table.insert(target_args, 1, cmd_level) + end + else + notify_error("Invalid level specified. Using defaults.", "warn") + end + end + + -- Store the original output name for later use + local original_name = output_name + + -- If compression is needed, adjust the output name to exclude extensions like ".tar" + if archive_compress ~= "" then + output_name = output_name:match("(.*%.tar)") or output_name + end + + -- Create a temporary directory for intermediate files + local temp_dir_name = ".tmp_compress" + local temp_dir = combine_url(output_dir, temp_dir_name) + local temp_dir, _ = tostring(fs.unique_name(Url(temp_dir))) + + -- Attempt to create the temporary directory + local temp_dir_status, temp_dir_err = fs.create("dir_all", Url(temp_dir)) + if not temp_dir_status then + -- Notify the user if the temporary directory creation fails + notify_error(string.format("Failed to create temp directory, error code: %s", temp_dir_err), "error") + return + end + + -- Define the temporary output file path within the temporary directory + local temp_output_url = combine_url(temp_dir, output_name) + + -- Add files to the output archive + for filepath, filenames in pairs(path_fnames) do + -- Execute the archive command for each path and its respective files + local archive_status, archive_err = + Command(archive_cmd):arg(archive_args):arg(temp_output_url):arg(filenames):cwd(filepath):spawn():wait() + if not archive_status or not archive_status.success then + -- Notify the user if the archiving process fails and clean up the temporary directory + notify_error(string.format("Failed to create archive %s with '%s', error: %s", output_name, archive_cmd, archive_err), "error") + local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir)) + if not cleanup_status then + notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error") + end + return + end + end + + -- If compression is required, execute the compression command + if archive_compress ~= "" then + local compress_status, compress_err = + Command(archive_compress):arg(archive_compress_args):arg(temp_output_url):spawn():wait() + if not compress_status or not compress_status.success then + -- Notify the user if the compression process fails and clean up the temporary directory + notify_error(string.format("Failed to compress archive %s with '%s', error: %s", output_name, archive_compress, compress_err), "error") + local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir)) + if not cleanup_status then + notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error") + end + return + end + end + + -- Move the final file from the temporary directory to the output directory + local final_output_url, temp_url_processed = combine_url(output_dir, original_name), combine_url(temp_dir, original_name) + final_output_url, _ = tostring(fs.unique_name(Url(final_output_url))) + local move_status, move_err = os.rename(temp_url_processed, final_output_url) + if not move_status then + -- Notify the user if the move operation fails and clean up the temporary directory + notify_error(string.format("Failed to move %s to %s, error: %s", temp_url_processed, final_output_url, move_err), "error") + local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir)) + if not cleanup_status then + notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error") + end + return + end + + -- Cleanup the temporary directory after successful operation + local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir)) + if not cleanup_status then + notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error") + end + end } diff --git a/yazi/plugins/git.yazi/README.md b/yazi/plugins/git.yazi/README.md index 1054230..f1848e9 100644 --- a/yazi/plugins/git.yazi/README.md +++ b/yazi/plugins/git.yazi/README.md @@ -1,8 +1,5 @@ # git.yazi -> [!NOTE] -> Yazi v25.2.7 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 @@ -10,7 +7,7 @@ https://github.com/user-attachments/assets/34976be9-a871-4ffe-9d5a-c4cdd0bf4576 ## Installation ```sh -ya pack -a yazi-rs/plugins:git +ya pkg add yazi-rs/plugins:git ``` ## Setup @@ -39,38 +36,38 @@ run = "git" 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` +- `th.git.modified` +- `th.git.added` +- `th.git.untracked` +- `th.git.ignored` +- `th.git.deleted` +- `th.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() +th.git = th.git or {} +th.git.modified = ui.Style():fg("blue") +th.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` +- `th.git.modified_sign` +- `th.git.added_sign` +- `th.git.untracked_sign` +- `th.git.ignored_sign` +- `th.git.deleted_sign` +- `th.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" +th.git = th.git or {} +th.git.modified_sign = "M" +th.git.deleted_sign = "D" ``` ## License diff --git a/yazi/plugins/git.yazi/main.lua b/yazi/plugins/git.yazi/main.lua index edd54fc..140ad5a 100644 --- a/yazi/plugins/git.yazi/main.lua +++ b/yazi/plugins/git.yazi/main.lua @@ -1,29 +1,45 @@ ---- @since 25.2.7 +--- @since 25.5.31 -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 WINDOWS = ya.target_family() == "windows" + +-- The code of supported git status, +-- also used to determine which status to show for directories when they contain different statuses +-- see `bubble_up` +local CODES = { + excluded = 100, -- ignored directory + ignored = 6, -- ignored file + untracked = 5, + modified = 4, + added = 3, + deleted = 2, + updated = 1, + unknown = 0, +} + +local PATTERNS = { + { "!$", CODES.ignored }, + { "?$", CODES.untracked }, + { "[MT]", CODES.modified }, + { "[AC]", CODES.added }, + { "D", CODES.deleted }, + { "U", CODES.updated }, + { "[AD][AD]", CODES.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 + for _, p in ipairs(PATTERNS) do + local path, pattern, code = nil, p[1], p[2] + if signs:find(pattern) then path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4) - path = WIN and path:gsub("/", "\\") or path + path = WINDOWS 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) + -- Mark the ignored directory as `excluded`, so we can process it further within `propagate_down` + return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2) else - return p[2], path + return code, path end end end @@ -44,34 +60,36 @@ local function root(cwd) if cha and (cha.is_dir or is_worktree(next)) then return tostring(cwd) end - cwd = cwd:parent() + 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() + for path, code in pairs(changed) do + if code ~= CODES.ignored then + local url = Url(path).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() + new[s] = (new[s] or CODES.unknown) > code and new[s] or code + url = url.parent end end end return new end -local function propagate_down(ignored, cwd, repo) +local function propagate_down(excluded, 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 + for _, path in ipairs(excluded) do + if rel:starts_with(path) then + -- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded` + new[tostring(cwd)] = CODES.excluded + elseif cwd == repo:join(path).parent then + -- If `path` is a direct subdirectory of `cwd`, mark it as `ignored` + new[path] = CODES.ignored + else + -- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance end end return new @@ -80,84 +98,84 @@ 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] = "" + for path, code in pairs(changed) do + if code == CODES.unknown then + st.repos[repo][path] = nil + elseif code == CODES.excluded then + -- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering + st.dirs[path] = CODES.excluded else - st.repos[repo][k] = v + st.repos[repo][path] = code end end ya.render() end) local remove = ya.sync(function(st, cwd) - local dir = st.dirs[cwd] - if not dir then + local repo = st.dirs[cwd] + if not repo then return end ya.render() st.dirs[cwd] = nil - if not st.repos[dir] then + if not st.repos[repo] then return end for _, r in pairs(st.dirs) do - if r == dir then + if r == repo then return end end - st.repos[dir] = nil + st.repos[repo] = nil end) local function setup(st, opts) - st.dirs = {} - st.repos = {} + st.dirs = {} -- Mapping between a directory and its corresponding repository + st.repos = {} -- Mapping between a repository and the status of each of its files 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 t = th.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"), + [CODES.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"), + [CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"), + [CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"), + [CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"), + [CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"), + [CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"), } 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", + [CODES.ignored] = t.ignored_sign or "", + [CODES.untracked] = t.untracked_sign or "?", + [CODES.modified] = t.modified_sign or "", + [CODES.added] = t.added_sign or "", + [CODES.deleted] = t.deleted_sign or "", + [CODES.updated] = t.updated_sign or "", } 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)] + local repo = st.dirs[tostring(url.base)] + local code + if repo then + code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)] end - if not change or signs[change] == "" then + if not code or signs[code] == "" then return "" - elseif self._file:is_hovered() then - return ui.Line { " ", signs[change] } + elseif self._file.is_hovered then + return ui.Line { " ", signs[code] } else - return ui.Line { " ", ui.Span(signs[change]):style(styles[change]) } + return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) } end end, opts.order) end local function fetch(_, job) - local cwd = job.files[1].url:parent() + local cwd = job.files[1].url.base local repo = root(cwd) if not repo then remove(tostring(cwd)) @@ -165,42 +183,43 @@ local function fetch(_, job) end local paths = {} - for _, f in ipairs(job.files) do - paths[#paths + 1] = tostring(f.url) + for _, file in ipairs(job.files) do + paths[#paths + 1] = tostring(file.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) + :arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) + :arg(paths) :stdout(Command.PIPED) :output() if not output then return true, Err("Cannot spawn `git` command, error: %s", err) end - local changed, ignored = {}, {} + local changed, excluded = {}, {} for line in output.stdout:gmatch("[^\r\n]+") do - local sign, path = match(line) - if sign == 30 then - ignored[path] = sign + local code, path = match(line) + if code == CODES.excluded then + excluded[#excluded + 1] = path else - changed[path] = sign + changed[path] = code end end if job.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 + ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo))) + + -- Reset the status of any files that don't appear in the output of `git status` to `unknown`, + -- so that cleaning up outdated statuses from `st.repos` + for _, path in ipairs(paths) do + local s = path:sub(#repo + 2) + changed[s] = changed[s] or CODES.unknown 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 false diff --git a/yazi/plugins/smart-enter.yazi/README.md b/yazi/plugins/smart-enter.yazi/README.md index d4c6bbd..742f2e1 100644 --- a/yazi/plugins/smart-enter.yazi/README.md +++ b/yazi/plugins/smart-enter.yazi/README.md @@ -5,7 +5,7 @@ ## Installation ```sh -ya pack -a yazi-rs/plugins:smart-enter +ya pkg add yazi-rs/plugins:smart-enter ``` ## Usage @@ -13,7 +13,7 @@ ya pack -a yazi-rs/plugins:smart-enter Bind your l key to the plugin, in your `~/.config/yazi/keymap.toml`: ```toml -[[manager.prepend_keymap]] +[[mgr.prepend_keymap]] on = "l" run = "plugin smart-enter" desc = "Enter the child directory, or open the file" @@ -36,5 +36,5 @@ require("smart-enter"):setup { This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. -[open]: https://yazi-rs.github.io/docs/configuration/keymap/#manager.open -[enter]: https://yazi-rs.github.io/docs/configuration/keymap/#manager.enter +[open]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.open +[enter]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.enter diff --git a/yazi/plugins/smart-enter.yazi/main.lua b/yazi/plugins/smart-enter.yazi/main.lua index 32d1630..e9e2ec6 100644 --- a/yazi/plugins/smart-enter.yazi/main.lua +++ b/yazi/plugins/smart-enter.yazi/main.lua @@ -1,11 +1,11 @@ ---- @since 25.2.7 +--- @since 25.5.31 --- @sync entry local function setup(self, opts) self.open_multi = opts.open_multi end local function entry(self) local h = cx.active.current.hovered - ya.manager_emit(h and h.cha.is_dir and "enter" or "open", { hovered = not self.open_multi }) + ya.emit(h and h.cha.is_dir and "enter" or "open", { hovered = not self.open_multi }) end return { entry = entry, setup = setup } diff --git a/yazi/plugins/starship.yazi/README.md b/yazi/plugins/starship.yazi/README.md index e5d0097..a9858bc 100644 --- a/yazi/plugins/starship.yazi/README.md +++ b/yazi/plugins/starship.yazi/README.md @@ -6,13 +6,13 @@ Starship prompt plugin for [Yazi](https://github.com/sxyazi/yazi) ## Requirements -- [Yazi](https://github.com/sxyazi/yazi) +- [Yazi](https://github.com/sxyazi/yazi) (v25.4.8+) - [starship](https://github.com/starship/starship) ## Installation ```bash -ya pack -a Rolv-Apneseth/starship +ya pkg add Rolv-Apneseth/starship ``` ### Manual @@ -78,14 +78,13 @@ Tab.build = function(self, ...) 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)), + c[1]:pad(ui.Pad.y(1)), + c[2]:pad(ui.Pad(1, c[3].w > 0 and 0 or 1, 1, c[1].w > 0 and 0 or 1)), + c[3]:pad(ui.Pad.y(1)), } - local style = THEME.manager.border_style + local style = th.mgr.border_style self._base = ya.list_merge(self._base or {}, { - ui.Border(ui.Border.ALL):area(self._area):type(ui.Border.ROUNDED):style(style), ui.Bar(ui.Bar.RIGHT):area(self._chunks[1]):style(style), ui.Bar(ui.Bar.LEFT):area(self._chunks[1]):style(style), diff --git a/yazi/plugins/starship.yazi/main.lua b/yazi/plugins/starship.yazi/main.lua index 4048508..00220d4 100644 --- a/yazi/plugins/starship.yazi/main.lua +++ b/yazi/plugins/starship.yazi/main.lua @@ -1,4 +1,4 @@ ---- @since 25.2.7 +--- @since 25.4.8 -- For development --[[ local function notify(message) ]] @@ -89,19 +89,13 @@ return { if st.cwd ~= cwd then st.cwd = cwd - if ya.confirm then - -- >= yazi 25.2.7 - ya.manager_emit("plugin", { - st._id, - ya.quote(tostring(cwd), true), - }) - else - -- < yazi 25.2.7 - ya.manager_emit("plugin", { - st._id, - args = ya.quote(tostring(cwd), true), - }) - end + -- `ya.emit` as of 25.5.28 + local emit = ya.emit or ya.manager_emit + + emit("plugin", { + st._id, + ya.quote(tostring(cwd), true), + }) end end @@ -110,12 +104,8 @@ return { ps.sub("tab", callback) end, - entry = function(_, job_or_args) - -- yazi 2024-11-29 changed the way arguments are passed to the plugin - -- entry point. They were moved inside {args = {...}}. If the user is using - -- a version before this change, they can use the old implementation. - -- https://github.com/sxyazi/yazi/pull/1966 - local args = job_or_args.args or job_or_args + entry = function(_, job) + local args = job.args local command = Command("starship") :arg("prompt") :stdin(Command.INHERIT) diff --git a/yazi/plugins/yamb.yazi/README.md b/yazi/plugins/yamb.yazi/README.md index 3f08f48..5f322de 100644 --- a/yazi/plugins/yamb.yazi/README.md +++ b/yazi/plugins/yamb.yazi/README.md @@ -72,41 +72,41 @@ Add this to your `keymap.toml`: ```toml [[manager.prepend_keymap]] on = [ "u", "a" ] -run = "plugin yamb --args=save" +run = "plugin yamb save" desc = "Add bookmark" [[manager.prepend_keymap]] on = [ "u", "g" ] -run = "plugin yamb --args=jump_by_key" +run = "plugin yamb jump_by_key" desc = "Jump bookmark by key" [[manager.prepend_keymap]] on = [ "u", "G" ] -run = "plugin yamb --args=jump_by_fzf" +run = "plugin yamb jump_by_fzf" desc = "Jump bookmark by fzf" [[manager.prepend_keymap]] on = [ "u", "d" ] -run = "plugin yamb --args=delete_by_key" +run = "plugin yamb delete_by_key" desc = "Delete bookmark by key" [[manager.prepend_keymap]] on = [ "u", "D" ] -run = "plugin yamb --args=delete_by_fzf" +run = "plugin yamb delete_by_fzf" desc = "Delete bookmark by fzf" [[manager.prepend_keymap]] on = [ "u", "A" ] -run = "plugin yamb --args=delete_all" +run = "plugin yamb delete_all" desc = "Delete all bookmarks" [[manager.prepend_keymap]] on = [ "u", "r" ] -run = "plugin yamb --args=rename_by_key" +run = "plugin yamb rename_by_key" desc = "Rename bookmark by key" [[manager.prepend_keymap]] on = [ "u", "R" ] -run = "plugin yamb --args=rename_by_fzf" +run = "plugin yamb rename_by_fzf" desc = "Rename bookmark by fzf" ``` diff --git a/yazi/plugins/yaziline.yazi/README.md b/yazi/plugins/yaziline.yazi/README.md index 207abc1..eeb8d12 100644 --- a/yazi/plugins/yaziline.yazi/README.md +++ b/yazi/plugins/yaziline.yazi/README.md @@ -8,16 +8,16 @@ Read more about features and configuration [here](#features). ## Requirements -- yazi version >= 25.2.11 +- yazi version >= [25.5.28](https://github.com/sxyazi/yazi/releases/tag/v25.5.28) - Font with symbol support. For example [Nerd Fonts](https://www.nerdfonts.com/). ## Installation ```sh -ya pack -a llanosrocas/yaziline +ya pkg add llanosrocas/yaziline ``` -Or manually copy `init.lua` to the `~/.config/yazi/plugins/yaziline.yazi/init.lua` +Or manually copy `main.lua` to the `~/.config/yazi/plugins/yaziline.yazi/main.lua` ## Usage @@ -32,6 +32,12 @@ Optionally, configure line: ```lua require("yaziline"):setup({ color = "#98c379", -- main theme color + secondary_color = "#5A6078", -- secondary color + default_files_color = "darkgray", -- color of the file counter when it's inactive + selected_files_color = "white", + yanked_files_color = "green", + cut_files_color = "red", + separator_style = "angly", -- "angly" | "curvy" | "liney" | "empty" separator_open = "", separator_close = "", @@ -39,14 +45,30 @@ require("yaziline"):setup({ separator_close_thin = "", separator_head = "", separator_tail = "", + select_symbol = "", yank_symbol = "󰆐", + filename_max_length = 24, -- truncate when filename > 24 filename_truncate_length = 6, -- leave 6 chars on both sides filename_truncate_separator = "..." -- the separator of the truncated filename }) ``` +``` + MODE  size  long_file...name.md  S 0 Y 0 +| | | | | | | | | +| | | | | | | | └─── yank_symbol +| | | | | | | └─────── select_symbol +| | | | | | └───────── separator_close_thin +| | | | | └─────────────────── filename_truncate_separator +| | | | └─────────────────────────────── separator_close +| | | └────────────────────────────────── secondary_color +| | └────────────────────────────────────── separator_close +| └────────────────────────────────────────── color +└───────────────────────────────────────────── separator_head +``` + ## Features ### Preconfigured separators @@ -101,17 +123,15 @@ _You can find more symbols [here](https://www.nerdfonts.com/cheat-sheet)_ ### Colors and font weight -You can change font weight in your `yazi/flavors/flavor.toml`: - -```toml -mode_normal = { bold = false } -``` - -And set custom color in the `init.lua`: +By default yaziline uses color values from your `theme.toml` (or flavor) but you can set custom colors in the `init.lua`: ```lua require("yaziline"):setup({ - color = "#98c379" + color = "#98c379", + default_files_color = "darkgray", + selected_files_color = "white", + yanked_files_color = "green", + cut_files_color = "red", }) ``` diff --git a/yazi/plugins/yaziline.yazi/main.lua b/yazi/plugins/yaziline.yazi/main.lua index 3343481..3906079 100644 --- a/yazi/plugins/yaziline.yazi/main.lua +++ b/yazi/plugins/yaziline.yazi/main.lua @@ -1,167 +1,212 @@ +---@diagnostic disable: undefined-global + local function setup(_, options) - options = options or {} + options = options or {} - local default_separators = { - angly = { "", "", "", "" }, - curvy = { "", "", "", "" }, - liney = { "", "", "|", "|" }, - empty = { "", "", "", "" } - } - local separators = default_separators[options.separator_style or "angly"] + local default_separators = { + angly = { "", "", "", "" }, + curvy = { "", "", "", "" }, + liney = { "", "", "|", "|" }, + empty = { "", "", "", "" }, + } + local separators = default_separators[options.separator_style or "angly"] - local config = { - separator_styles = { - separator_open = options.separator_open or separators[1], - separator_close = options.separator_close or separators[2], - separator_open_thin = options.separator_open_thin or separators[3], - separator_close_thin = options.separator_close_thin or separators[4], - separator_head = options.separator_head or "", - separator_tail = options.separator_tail or "" - }, - select_symbol = options.select_symbol or "S", - yank_symbol = options.yank_symbol or "Y", - filename_max_length = options.filename_max_length or 24, - filename_truncate_length = options.filename_truncate_length or 6, - filename_truncate_separator = options.filename_truncate_separator or "...", - color = options.color or nil - } + local config = { + separator_styles = { + separator_open = options.separator_open or separators[1], + separator_close = options.separator_close or separators[2], + separator_open_thin = options.separator_open_thin or separators[3], + separator_close_thin = options.separator_close_thin or separators[4], + separator_head = options.separator_head or "", + separator_tail = options.separator_tail or "", + }, + select_symbol = options.select_symbol or "S", + yank_symbol = options.yank_symbol or "Y", - local current_separator_style = config.separator_styles + filename_max_length = options.filename_max_length or 24, + filename_truncate_length = options.filename_truncate_length or 6, + filename_truncate_separator = options.filename_truncate_separator or "...", - function Header:count() - return ui.Line {} - end + color = options.color or nil, + secondary_color = options.secondary_color or nil, + default_files_color = options.default_files_color + or th.which.separator_style.fg + or "darkgray", + selected_files_color = options.selected_files_color + or th.mgr.count_selected.bg + or "white", + yanked_files_color = options.selected_files_color + or th.mgr.count_copied.bg + or "green", + cut_files_color = options.cut_files_color + or th.mgr.count_cut.bg + or "red", + } - function Status:mode() - local mode = tostring(self._tab.mode):upper() - if mode == "UNSET" then - mode = "UN-SET" - end + local current_separator_style = config.separator_styles - local style = self:style() - return ui.Line({ - ui.Span(current_separator_style.separator_head):fg(config.color or style.main.bg), - ui.Span(" " .. mode .. " "):fg(THEME.which.mask.bg):bg(config.color or style.main.bg), - }) - end + function Header:count() + return ui.Line({}) + end - function Status:size() - local h = self._tab.current.hovered - if not h then - return ui.Line {} - end + function Status:mode() + local mode = tostring(self._tab.mode):upper() - local style = self:style() - return ui.Span(current_separator_style.separator_close .. " " .. ya.readable_size(h:size() or h.cha.len) .. " ") - :fg(config.color or style.main.bg):bg(THEME.which.separator_style.fg) - end + local style = self:style() + return ui.Line({ + ui.Span(current_separator_style.separator_head) + :fg(config.color or style.main.bg), + ui.Span(" " .. mode .. " ") + :fg(th.which.mask.bg) + :bg(config.color or style.main.bg), + }) + end - function Status:utf8_sub(str, start_char, end_char) - local start_byte = utf8.offset(str, start_char) - local end_byte = end_char and (utf8.offset(str, end_char + 1) - 1) or #str + function Status:size() + local h = self._current.hovered + local size = h and (h:size() or h.cha.len) or 0 - if not start_byte or not end_byte then - return "" - end + local style = self:style() + return ui.Span(current_separator_style.separator_close .. " " .. ya.readable_size(size) .. " ") + :fg(config.color or style.main.bg) + :bg(config.secondary_color or th.which.separator_style.fg) + end - return string.sub(str, start_byte, end_byte) - end + function Status:utf8_sub(str, start_char, end_char) + local start_byte = utf8.offset(str, start_char) + local end_byte = end_char and (utf8.offset(str, end_char + 1) - 1) or #str - function Status:truncate_name(filename, max_length) - local base_name, extension = filename:match("^(.-)(%.[^%.]+)$") - base_name = base_name or filename - extension = extension or "" + if not start_byte or not end_byte then + return "" + end - if utf8.len(base_name) > max_length then - base_name = self:utf8_sub(base_name, 1, config.filename_truncate_length) .. - config.filename_truncate_separator .. - self:utf8_sub(base_name, -config.filename_truncate_length) - end + return string.sub(str, start_byte, end_byte) + end - return base_name .. extension - end + function Status:truncate_name(filename, max_length) + local base_name, extension = filename:match("^(.+)(%.[^%.]+)$") + base_name = base_name or filename + extension = extension or "" - function Status:name() - local h = self._tab.current.hovered - if not h then - return ui.Line {} - end + if utf8.len(base_name) > max_length then + base_name = self:utf8_sub(base_name, 1, config.filename_truncate_length) + .. config.filename_truncate_separator + .. self:utf8_sub(base_name, -config.filename_truncate_length) + end - local truncated_name = self:truncate_name(h.name, config.filename_max_length) + return base_name .. extension + end - local style = self:style() - return ui.Line { - ui.Span(current_separator_style.separator_close .. " "):fg(THEME.which.separator_style.fg), - ui.Span(truncated_name):fg(config.color or style.main.bg), - } - end + function Status:name() + local h = self._current.hovered + if not h then + return ui.Line({ + ui.Span(current_separator_style.separator_close .. " ") + :fg(config.secondary_color or th.which.separator_style.fg), + ui.Span("Empty dir") + :fg(config.color or style.main.bg), + }) + end - function Status:files() - local files_yanked = #cx.yanked - local files_selected = #cx.active.selected - local files_is_cut = cx.yanked.is_cut + local truncated_name = self:truncate_name(h.name, config.filename_max_length) - local selected_fg = files_selected > 0 and THEME.manager.count_selected.bg or THEME.which.separator_style.fg - local yanked_fg = files_yanked > 0 and - (files_is_cut and THEME.manager.count_cut.bg or THEME.manager.count_copied.bg) or - THEME.which.separator_style.fg + local style = self:style() + return ui.Line({ + ui.Span(current_separator_style.separator_close .. " ") + :fg(config.secondary_color or th.which.separator_style.fg), + ui.Span(truncated_name) + :fg(config.color or style.main.bg), + }) + end - local yanked_text = files_yanked > 0 and config.yank_symbol .. " " .. files_yanked or config.yank_symbol .. " 0" + function Status:files() + local files_yanked = #cx.yanked + local files_selected = #cx.active.selected + local files_cut = cx.yanked.is_cut - return ui.Line { - ui.Span(" " .. current_separator_style.separator_close_thin .. " "):fg( - THEME.which.separator_style.fg), - ui.Span(config.select_symbol .. " " .. files_selected .. " "):fg(selected_fg), - ui.Span(yanked_text .. " "):fg(yanked_fg), - } - end + local selected_fg = files_selected > 0 + and config.selected_files_color + or config.default_files_color + local yanked_fg = files_yanked > 0 + and + (files_cut + and config.cut_files_color + or config.yanked_files_color + ) + or config.default_files_color - function Status:modified() - local hovered = cx.active.current.hovered - local cha = hovered.cha - local time = (cha.mtime or 0) // 1 + local yanked_text = files_yanked > 0 + and config.yank_symbol .. " " .. files_yanked + or config.yank_symbol .. " 0" - return ui.Span(os.date("%Y-%m-%d %H:%M", time) .. " " .. current_separator_style.separator_open_thin .. " "):fg( - THEME.which.separator_style.fg) - end + return ui.Line({ + ui.Span(" " .. current_separator_style.separator_close_thin .. " ") + :fg(th.which.separator_style.fg), + ui.Span(config.select_symbol .. " " .. files_selected .. " ") + :fg(selected_fg), + ui.Span(yanked_text .. " ") + :fg(yanked_fg), + }) + end - function Status:percent() - local percent = 0 - local cursor = self._tab.current.cursor - local length = #self._tab.current.files - if cursor ~= 0 and length ~= 0 then - percent = math.floor((cursor + 1) * 100 / length) - end + function Status:modified() + local hovered = cx.active.current.hovered - if percent == 0 then - percent = " Top " - elseif percent == 100 then - percent = " Bot " - else - percent = string.format(" %2d%% ", percent) - end + if not hovered then + return "" + end - local style = self:style() - return ui.Line { - ui.Span(" " .. current_separator_style.separator_open):fg(THEME.which.separator_style.fg), - ui.Span(percent):fg(config.color or style.main.bg):bg(THEME.which.separator_style.fg), - ui.Span(current_separator_style.separator_open):fg(config.color or style.main.bg):bg(THEME.which.separator_style.fg) - } - end + local cha = hovered.cha + local time = (cha.mtime or 0) // 1 - function Status:position() - local cursor = self._tab.current.cursor - local length = #self._tab.current.files + return ui.Span(os.date("%Y-%m-%d %H:%M", time) .. " " .. current_separator_style.separator_open_thin .. " ") + :fg(th.which.separator_style.fg) + end - local style = self:style() - return ui.Line({ - ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):fg(THEME.which.mask.bg):bg(config.color or style.main.bg), - ui.Span(current_separator_style.separator_tail):fg(config.color or style.main.bg), - }) - end + function Status:percent() + local percent = 0 + local cursor = self._tab.current.cursor + local length = #self._tab.current.files + if cursor ~= 0 and length ~= 0 then + percent = math.floor((cursor + 1) * 100 / length) + end - Status:children_add(Status.files, 4000, Status.LEFT) - Status:children_add(Status.modified, 0, Status.RIGHT) + if percent == 0 then + percent = " Top " + elseif percent == 100 then + percent = " Bot " + else + percent = string.format(" %2d%% ", percent) + end + + local style = self:style() + return ui.Line({ + ui.Span(" " .. current_separator_style.separator_open) + :fg(config.secondary_color or th.which.separator_style.fg), + ui.Span(percent) + :fg(config.color or style.main.bg) + :bg(config.secondary_color or th.which.separator_style.fg), + ui.Span(current_separator_style.separator_open) + :fg(config.color or style.main.bg) + :bg(config.secondary_color or th.which.separator_style.fg), + }) + end + + function Status:position() + local cursor = self._tab.current.cursor + local length = #self._tab.current.files + + local style = self:style() + return ui.Line({ + ui.Span(string.format(" %2d/%-2d ", math.min(cursor + 1, length), length)) + :fg(th.which.mask.bg) + :bg(config.color or style.main.bg), + ui.Span(current_separator_style.separator_tail):fg(config.color or style.main.bg), + }) + end + + Status:children_add(Status.files, 4000, Status.LEFT) + Status:children_add(Status.modified, 0, Status.RIGHT) end return { setup = setup } \ No newline at end of file