diff options
Diffstat (limited to 'config/yazi/plugins')
19 files changed, 3650 insertions, 0 deletions
diff --git a/config/yazi/plugins/compress.yazi/LICENSE b/config/yazi/plugins/compress.yazi/LICENSE new file mode 100644 index 00000000..7ce7a2f1 --- /dev/null +++ b/config/yazi/plugins/compress.yazi/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Ciarán O'Brien + +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/compress.yazi/README.md b/config/yazi/plugins/compress.yazi/README.md new file mode 100644 index 00000000..7241cf7d --- /dev/null +++ b/config/yazi/plugins/compress.yazi/README.md @@ -0,0 +1,152 @@ +<h1 align="center">🗜️ compress.yazi</h1> +<p align="center"> + <b>A blazing fast, flexible archive plugin for <a href="https://github.com/sxyazi/yazi">Yazi</a></b><br> + <i>Effortlessly compress your files and folders with style!</i> +</p> + +## 📖 Table of Contents + +- [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) + +## 🚀 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 + +## 📦 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 +# Unix +git clone https://github.com/KKV9/compress.yazi.git ~/.config/yazi/plugins/compress.yazi + +# 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 pkg add KKV9/compress +``` +### 🔧 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 +[[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 + +1. **Select files/folders** in Yazi. +2. Press <kbd>c</kbd> <kbd>a</kbd> to open the archive dialog. +3. Choose: + - <kbd>a</kbd> for a standard archive + - <kbd>p</kbd> for password protection (zip/7z/rar) + - <kbd>h</kbd> to encrypt header (7z/rar) + - <kbd>l</kbd> to set compression level (all compression algorithims) + - <kbd>u</kbd> 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) +- `-s` Silence success message +- `<extention>` 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. +- If you leave the filename blank, the plugin will use the selected filename or parent directory, when multiple files are selected, to name the archive. + +## 📣 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/config/yazi/plugins/compress.yazi/main.lua b/config/yazi/plugins/compress.yazi/main.lua new file mode 100644 index 00000000..440f6e22 --- /dev/null +++ b/config/yazi/plugins/compress.yazi/main.lua @@ -0,0 +1,577 @@ +--- @since 25.12.29 + +-- Check for windows +local is_windows = ya.target_family() == "windows" + +-- Define default flags and strings +local is_password, is_encrypted, is_level, is_silent_success = false, false, false, false +local default_extension = "zip" + +-- Allow dots when matching file extension arguments +local function extension_pattern(ext) + return "%." .. ext:gsub("%.", "%%.") .. "$" +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 + +-- Function to send notifications +local function notify(message, level) + ya.notify({ + title = "Archive", + content = message, + level = level, + 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 + return os.execute(stat_cmd) +end + +-- Function to find first available command from list +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 + +-- Function to append filename to its parent directory url +local function combine_url(path, file) + local path_url = Url(path) + local file_url = Url(file) + return tostring(path_url:join(file_url)) +end + +-- Function to make a table of selected or hovered files: path = filenames +local selected_or_hovered = ya.sync(function() + local tab = cx.active + local paths = {} + local names = {} + local path_fnames = {} + + 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 + local path = paths[idx] + if not path_fnames[path] then + path_fnames[path] = {} + end + table.insert(path_fnames[path], name) + end + + return path_fnames, names, tostring(tab.current.cwd) +end) + +-- Function to cleanup temporary directory +local function cleanup_temp_dir(temp_dir) + local status, err = fs.remove("dir_all", Url(temp_dir)) + if not status then + notify( + string.format("Failed to clean up temporary directory %s, error: %s", ya.quote(temp_dir), tostring(err)), + "error" + ) + return false + end + return true +end + +-- Function for compression level +local function add_compression_level(target_args, level_arg, level_value) + if type(level_arg) == "table" then + -- Insert each element except last + for i = 1, #level_arg - 1 do + table.insert(target_args, i, level_arg[i]) + end + -- Add the level value with the last element + table.insert(target_args, #level_arg, level_arg[#level_arg] .. level_value) + else + -- Single string argument + table.insert(target_args, 1, level_arg .. level_value) + end +end + +-- Function for password handling +local function get_password_args(archive_cmd, encrypted, header_arg) + local output_password, event = ya.input({ + title = "Enter password:", + obscure = true, + pos = { "top-center", y = 3, w = 40 }, + }) + if event ~= 1 or output_password == "" then + return nil + end + -- Handling for RAR with encryption + if archive_cmd == "rar" and encrypted then + return { header_arg .. output_password } + end + return { "-P" .. output_password } +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" } }, + }, +} + +-- Function for command matching +local function find_archive_command(output_name) + for pattern, cmd_list in pairs(archive_commands) do + if output_name:match(pattern) then + for _, cmd in ipairs(cmd_list) do + -- Check if archive command is available + local cmd_name = type(cmd.command) == "table" and find_command_name(cmd.command) or cmd.command + if is_command_available(cmd_name) then + -- Check if compress command (if listed) is available + if not cmd.compress or is_command_available(cmd.compress) then + return { + cmd = cmd_name, + args = cmd.args, + compress = cmd.compress or "", + level_arg = cmd.level_arg or "", + level_min = cmd.level_min, + level_max = cmd.level_max, + header_arg = cmd.header_arg or "", + passwordable = cmd.passwordable or false, + compress_args = cmd.compress_args or {}, + } + end + end + end + -- Pattern matched but no suitable command found + return nil + end + end + -- No pattern matched - unsupported extension + return false +end + +return { + entry = function(_, job) + -- Parse flags and default extension + if job.args 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 + elseif flag == "s" then + is_silent_success = true + end + end + elseif arg:match("^[%w%.]+$") then + -- Handle default extension (e.g., 7z, zip) + if archive_commands[extension_pattern(arg)] then + default_extension = arg + else + notify(string.format("Unsupported extension: %s", arg), "warn") + end + else + notify(string.format("Unknown argument: %s", arg), "warn") + end + end + end + + -- 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:", + pos = { "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 + + -- 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 + + -- Validate the final archive filename + if not is_valid_filename(output_name) then + notify("Invalid archive filename", "error") + return + end + + -- Command matching + local archive_config = find_archive_command(output_name) + if archive_config == false then + notify("Unsupported file extension", "error") + return + elseif not archive_config then + notify("Could not find a suitable archive program for the selected file extension", "error") + return + end + + -- Extract configuration + local archive_cmd = archive_config.cmd + local archive_args = archive_config.args + local archive_compress = archive_config.compress + local archive_level_arg = is_level and archive_config.level_arg or "" + local archive_level_min = archive_config.level_min + local archive_level_max = archive_config.level_max + local archive_header_arg = is_encrypted and archive_config.header_arg or "" + local archive_passwordable = archive_config.passwordable + local archive_compress_args = archive_config.compress_args + + -- Password handling + if archive_passwordable and is_password then + local password_args = get_password_args(archive_cmd, is_encrypted, archive_header_arg) + if password_args then + for _, arg in ipairs(password_args) do + table.insert(archive_args, arg) + 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 + + -- Use extracted compression level + if archive_level_arg ~= "" and is_level then + local output_level, level_event = ya.input({ + title = string.format("Enter compression level (%s - %s)", archive_level_min, archive_level_max), + pos = { "top-center", y = 3, w = 40 }, + }) + if level_event ~= 1 then + return + end + -- Validate user input for compression level + local level_num = tonumber(output_level) + if level_num and level_num >= archive_level_min and level_num <= archive_level_max then + local target_args = archive_compress == "" and archive_args or archive_compress_args + add_compression_level(target_args, archive_level_arg, output_level) + else + notify("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) + 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(string.format("Failed to create temp directory, error code: %s", tostring(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( + string.format( + "Failed to create archive %s with '%s', error: %s", + ya.quote(output_name), + archive_cmd, + tostring(archive_err) + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- If compression is required, execute the compression command + if archive_compress ~= "" then + local compress_status, compress_err + + -- Check if using 7z for compression (requires output file argument) + if archive_compress:match("^7z") then + local compressed_output = combine_url(temp_dir, original_name) + compress_status, compress_err = Command(archive_compress) + :arg(archive_compress_args) + :arg(compressed_output) + :arg(temp_output_url) + :spawn() + :wait() + else + -- Native compression tools (gzip, xz, bzip2, etc.) compress in-place + compress_status, compress_err = + Command(archive_compress):arg(archive_compress_args):arg(temp_output_url):spawn():wait() + end + + 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( + string.format( + "Failed to compress archive %s with '%s', error: %s", + ya.quote(output_name), + archive_compress, + tostring(compress_err) + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- Move the final file from the temporary directory to the output directory + local final_output_url = combine_url(output_dir, original_name) + local temp_url_processed = combine_url(temp_dir, original_name) + final_output_url = tostring(fs.unique_name(Url(final_output_url))) + local from, to = Url(temp_url_processed), Url(final_output_url) + local move_status, move_err = fs.rename(from, to) + if not move_status then + if move_err and move_err.kind == "CrossesDevices" then + local copy_status, copy_err = fs.copy(from, to) + if not copy_status then + notify( + string.format( + "Failed to copy across devices %s to %s, error: %s", + ya.quote(from.name), + ya.quote(to.name), + copy_err and tostring(copy_err.kind) or "unknown" + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + else + notify( + string.format( + "Failed to move %s to %s, error: %s", + ya.quote(from.name), + ya.quote(to.name), + move_err and tostring(move_err.kind) or "unknown" + ), + "error" + ) + cleanup_temp_dir(temp_dir) + return + end + end + + -- Cleanup the temporary directory after successful operation + cleanup_temp_dir(temp_dir) + + -- Notify user of success + if not is_silent_success then + notify(string.format("Successfully created archive: %s", ya.quote(to.name)), "info") + end + end, +} diff --git a/config/yazi/plugins/full-border.yazi/LICENSE b/config/yazi/plugins/full-border.yazi/LICENSE new file mode 100644 index 00000000..fb5b1d62 --- /dev/null +++ b/config/yazi/plugins/full-border.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/full-border.yazi/README.md b/config/yazi/plugins/full-border.yazi/README.md new file mode 100644 index 00000000..269ca8ec --- /dev/null +++ b/config/yazi/plugins/full-border.yazi/README.md @@ -0,0 +1,32 @@ +# full-border.yazi + +Add a full border to Yazi to make it look fancier. + + + +## Installation + +```sh +ya pkg add yazi-rs/plugins:full-border +``` + +## Usage + +Add this to your `init.lua` to enable the plugin: + +```lua +require("full-border"):setup() +``` + +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/main.lua b/config/yazi/plugins/full-border.yazi/main.lua new file mode 100644 index 00000000..501156d7 --- /dev/null +++ b/config/yazi/plugins/full-border.yazi/main.lua @@ -0,0 +1,53 @@ +--- @since 25.2.26 + +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 or th.mgr.border_symbol ~= "│" then + return ui.Bar(ui.Edge.TOP) + end + + return ui.Bar(ui.Edge.TOP) + :area( + 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) } + ) + :symbol(c) + end + + local c = self._chunks + self._chunks = { + c[1]:pad(ui.Pad.y(1)), + -- TODO: remove this compatibility hack + fs.unique and c[2]:pad(ui.Pad.y(1)) or 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 = th.mgr.border_style + if rt.opener then -- TODO: remove this compatibility hack + self._base = ya.list_merge(self._base or {}, { + ui.Border(ui.Edge.ALL):area(self._area):type(type):style(style), + + bar("┬", c[2].x, c[1].y), + bar("┴", c[2].x, c[1].bottom - 1), + bar("┬", c[2].right - 1, c[2].y), + bar("┴", c[2].right - 1, c[2].bottom - 1), + }) + else + self._base = ya.list_merge(self._base or {}, { + ui.Border(ui.Edge.ALL):area(self._area):type(type):style(style), + + 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[2].bottom - 1), + }) + end + + old_build(self, ...) + end +end + +return { setup = setup } 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..042bac6c --- /dev/null +++ b/config/yazi/plugins/git.yazi/README.md @@ -0,0 +1,89 @@ +# git.yazi + +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 pkg add yazi-rs/plugins:git +``` + +## Setup + +Add the following to your `~/.config/yazi/init.lua`: + +```lua +require("git"):setup { + -- Order of status signs showing in the linemode + order = 1500, +} +``` + +And register it as fetchers in your `~/.config/yazi/yazi.toml`: + +```toml +[[plugin.prepend_fetchers]] +id = "git" # Remove if Yazi > v26.1.22 +url = "*" +run = "git" +group = "git" + +[[plugin.prepend_fetchers]] +id = "git" # Remove if Yazi > v26.1.22 +url = "*/" +run = "git" +group = "git" +``` + +## Advanced + +> [!NOTE] +> The following configuration must be put before `require("git"):setup()` + +You can customize the [Style](https://yazi-rs.github.io/docs/plugins/layout#style) of the status sign with: + +- `[git].unknown` - status cannot/not yet determined +- `[git].modified` - modified file +- `[git].added` - added file +- `[git].untracked` - untracked file +- `[git].ignored` - ignored file +- `[git].deleted` - deleted file +- `[git].updated` - updated file +- `[git].clean` - clean file + +For example: + +```toml +# theme.toml / flavor.toml +[git] +modified = { fg = "blue" } +deleted = { fg = "red", bold = true } +``` + +You can also customize the text of the status sign with: + +- `[git].unknown_sign` - status cannot/not yet determined +- `[git].modified_sign` - modified file +- `[git].added_sign` - added file +- `[git].untracked_sign` - untracked file +- `[git].ignored_sign` - ignored file +- `[git].deleted_sign` - deleted file +- `[git].updated_sign` - updated file +- `[git].clean_sign` - clean file + +For example: + +```toml +# theme.toml / flavor.toml +[git] +unknown_sign = " " +modified_sign = "M" +deleted_sign = "D" +clean_sign = "✔" +``` + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/git.yazi/main.lua b/config/yazi/plugins/git.yazi/main.lua new file mode 100644 index 00000000..19f5c445 --- /dev/null +++ b/config/yazi/plugins/git.yazi/main.lua @@ -0,0 +1,259 @@ +--- @since 26.5.6 + +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` +---@enum CODES +local CODES = { + unknown = 100, -- status cannot/not yet determined + excluded = 99, -- ignored directory + ignored = 6, -- ignored file + untracked = 5, + modified = 4, + added = 3, + deleted = 2, + updated = 1, + clean = 0, +} + +local PATTERNS = { + { "!$", CODES.ignored }, + { "?$", CODES.untracked }, + { "[MT]", CODES.modified }, + { "[AC]", CODES.added }, + { "D", CODES.deleted }, + { "U", CODES.updated }, + { "[AD][AD]", CODES.updated }, +} + +---@param line string +---@return CODES, string +local function match(line) + local signs = line:sub(1, 2) + 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 = WINDOWS and path:gsub("/", "\\") or path + end + if not path then + elseif path:find("[/\\]$") then + -- 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 code, path + end + ---@diagnostic disable-next-line: missing-return + end +end + +---@param cwd Url +---@return string? +local function root(cwd) + local is_worktree = function(url) + local file, head = io.open(tostring(url)), nil + if file then + head = file:read(8) + file:close() + end + return head == "gitdir: " + end + + repeat + local next = cwd:join(".git") + local cha = fs.cha(next) + if cha and (cha.is_dir or is_worktree(next)) then + return tostring(cwd) + end + cwd = cwd.parent + until not cwd +end + +---@param changed Changes +---@return Changes +local function bubble_up(changed) + local new, empty = {}, Url("") + 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 CODES.clean) > code and new[s] or code + url = url.parent + end + end + end + return new +end + +---@param excluded string[] +---@param cwd Url +---@param repo Url +---@return Changes +local function propagate_down(excluded, cwd, repo) + local new, rel = {}, cwd:strip_prefix(repo) + 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 +end + +---@param cwd string +---@param repo string +---@param changed Changes +local add = ya.sync(function(st, cwd, repo, changed) + ---@cast st State + + st.dirs[cwd] = repo + st.repos[repo] = st.repos[repo] or {} + for path, code in pairs(changed) do + if code == CODES.clean 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][path] = code + end + end + ui.render() +end) + +---@param cwd string +local remove = ya.sync(function(st, cwd) + ---@cast st State + + local repo = st.dirs[cwd] + if not repo then + return + end + + ui.render() + st.dirs[cwd] = nil + if not st.repos[repo] then + return + end + + for _, r in pairs(st.dirs) do + if r == repo then + return + end + end + st.repos[repo] = nil +end) + +---@param st State +---@param opts Options +local function setup(st, opts) + st.dirs = {} + st.repos = {} + + opts = opts or {} + opts.order = opts.order or 1500 + + local t = th.git or {} + local styles = { + [CODES.unknown] = t.unknown or ui.Style(), + [CODES.ignored] = t.ignored or ui.Style():fg("darkgray"), + [CODES.untracked] = t.untracked or ui.Style():fg("magenta"), + [CODES.modified] = t.modified or ui.Style():fg("yellow"), + [CODES.added] = t.added or ui.Style():fg("green"), + [CODES.deleted] = t.deleted or ui.Style():fg("red"), + [CODES.updated] = t.updated or ui.Style():fg("yellow"), + [CODES.clean] = t.clean or ui.Style(), + } + local signs = { + [CODES.unknown] = t.unknown_sign or "", + [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 " ", + [CODES.clean] = t.clean_sign or "", + } + + Linemode:children_add(function(self) + if not self._file.in_current then + return "" + end + + local url = self._file.url + local repo = st.dirs[tostring(url.base or url.parent)] + local code = CODES.unknown + if repo then + code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)] or CODES.clean + end + + if signs[code] == "" then + return "" + elseif self._file.is_hovered then + return ui.Line { " ", signs[code] } + else + return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) } + end + end, opts.order) +end + +---@type UnstableFetcher +local function fetch(_, job) + local cwd = job.files[1].url.base or job.files[1].url.parent + local repo = root(cwd) + if not repo then + remove(tostring(cwd)) + return true + end + + local paths = {} + for _, file in ipairs(job.files) do + paths[#paths + 1] = tostring(file.url) + end + + -- stylua: ignore + local output, err = Command("git") + :cwd(tostring(cwd)) + :arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" }) + :arg(paths) + :output() + if not output then + return true, Err("Cannot spawn `git` command, error: %s", err) + end + + local changed, excluded = {}, {} + for line in output.stdout:gmatch("[^\r\n]+") do + local code, path = match(line) + if code == CODES.excluded then + excluded[#excluded + 1] = path + else + changed[path] = code + end + end + + if job.files[1].cha.is_dir then + ya.dict_merge(changed, bubble_up(changed)) + 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 `clean`, + -- 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.clean + end + + add(tostring(cwd), repo, changed) + + return false +end + +return { setup = setup, fetch = fetch } diff --git a/config/yazi/plugins/git.yazi/types.lua b/config/yazi/plugins/git.yazi/types.lua new file mode 100644 index 00000000..99368495 --- /dev/null +++ b/config/yazi/plugins/git.yazi/types.lua @@ -0,0 +1,12 @@ +---@class State +---@field dirs table<string, string|CODES> Mapping between a directory and its corresponding repository +---@field repos table<string, Changes> Mapping between a repository and the status of each of its files + +---@class Options +---@field order number The order in which the status is displayed +---@field renamed boolean Whether to include renamed files in the status (or treat them as modified) + +-- TODO: move this to `types.yazi` once it's get stable +---@alias UnstableFetcher fun(self: unknown, job: { files: File[] }): boolean, Error? + +---@alias Changes table<string, CODES> diff --git a/config/yazi/plugins/smart-filter.yazi/LICENSE b/config/yazi/plugins/smart-filter.yazi/LICENSE new file mode 100644 index 00000000..fb5b1d62 --- /dev/null +++ b/config/yazi/plugins/smart-filter.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/smart-filter.yazi/README.md b/config/yazi/plugins/smart-filter.yazi/README.md new file mode 100644 index 00000000..29653394 --- /dev/null +++ b/config/yazi/plugins/smart-filter.yazi/README.md @@ -0,0 +1,28 @@ +# smart-filter.yazi + +A Yazi plugin that makes filters smarter: continuous filtering, automatically enter unique directory, open file on submitting. + +https://github.com/yazi-rs/plugins/assets/17523360/72aaf117-1378-4f7e-93ba-d425a79deac5 + +## Installation + +```sh +ya pkg add yazi-rs/plugins:smart-filter +``` + +## Usage + +Add this to your `~/.config/yazi/keymap.toml`: + +```toml +[[mgr.prepend_keymap]] +on = "F" +run = "plugin smart-filter" +desc = "Smart filter" +``` + +Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins. + +## License + +This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file. diff --git a/config/yazi/plugins/smart-filter.yazi/main.lua b/config/yazi/plugins/smart-filter.yazi/main.lua new file mode 100644 index 00000000..2d1c3cde --- /dev/null +++ b/config/yazi/plugins/smart-filter.yazi/main.lua @@ -0,0 +1,50 @@ +--- @since 25.12.29 + +local hovered = ya.sync(function() + local h = cx.active.current.hovered + if not h then + return {} + end + + return { + url = h.url, + is_dir = h.cha.is_dir, + unique = #cx.active.current.files == 1, + } +end) + +local function prompt() + return ya.input { + title = "Smart filter:", + pos = { "center", w = 50 }, + realtime = true, + debounce = 0.1, + } +end + +local function entry() + local input = prompt() + + while true do + local value, event = input:recv() + if event ~= 1 and event ~= 3 then + ya.emit("escape", { filter = true }) + break + end + + ya.emit("filter_do", { value, smart = true }) + + local h = hovered() + if h.unique and h.is_dir then + ya.emit("escape", { filter = true }) + ya.emit("enter", {}) + input = prompt() + elseif event == 1 then + ya.emit("escape", { filter = true }) + ya.emit(h.is_dir and "enter" or "open", { h.url }) + break + end + end +end + +return { entry = entry } diff --git a/config/yazi/plugins/yatline-githead.yazi/LICENSE b/config/yazi/plugins/yatline-githead.yazi/LICENSE new file mode 100644 index 00000000..0c37e850 --- /dev/null +++ b/config/yazi/plugins/yatline-githead.yazi/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2024 imsi32 +Copyright (c) 2024 llanosrocas + +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/yatline-githead.yazi/README.md b/config/yazi/plugins/yatline-githead.yazi/README.md new file mode 100644 index 00000000..e2af22e2 --- /dev/null +++ b/config/yazi/plugins/yatline-githead.yazi/README.md @@ -0,0 +1,213 @@ +# yatline-githead.yazi + +Manual clone of [githead.yazi](https://github.com/llanosrocas/githead.yazi) for supporting [yatline.yazi](https://github.com/imsi32/yatline.yazi) + +> [!IMPORTANT] +> This repository will not add new features other than coming from original repository. +> The latest commit in there is 317d09f728928943f0af72ff6ce31ea335351202. +> Which can be found in [here](https://github.com/llanosrocas/githead.yazi/commit/317d09f728928943f0af72ff6ce31ea335351202) + +All supported features are listed [here](#features) + +## Requirements + +- yazi version >= 25.5.28. +- Font with symbol support. For example [Nerd Fonts](https://www.nerdfonts.com/). +- [yatline.yazi](https://github.com/imsi32/yatline.yazi) (Mandatory) + +> [!IMPORTANT] +> Recommended to use [githead.yazi](https://github.com/llanosrocas/githead.yazi), if you are not using yatline.yazi + +## Installation + +```sh +ya pkg add imsi32/yatline-githead +``` + +## Usage + +Add this to your `~/.config/yazi/init.lua`, after yatline.yazi configuration: + +```lua +require("yatline-githead"):setup() +``` + +### Default Configuration + +```lua +require("yatline-githead"):setup({ + order = { + "branch", + "remote", + "tag", + "commit", + "behind_ahead_remote", + "stashes", + "state", + "staged", + "unstaged", + "untracked", + }, + + show_numbers = true, -- shows staged, unstaged, untracked, stashes count + + show_branch = true, + branch_prefix = "", + branch_color = "blue", + branch_symbol = "", + branch_borders = "", + + show_remote_branch = true, -- only shown if different from local branch + always_show_remote_branch = false, -- always show remote branch even if it the same as local branch + always_show_remote_repo = false, -- Adds `origin/` if `always_show_remote_branch` is enabled + remote_branch_prefix = ":", + remote_branch_color = "bright magenta", + + show_tag = true, -- only shown if branch is not available + always_show_tag = false, + tag_color = "magenta", + tag_symbol = "#", + + show_commit = true, -- only shown if branch AND tag are not available + always_show_commit = false, + commit_color = "bright magenta", + commit_symbol = "@", + + show_behind_ahead_remote = true, + behind_remote_color = "bright magenta", + behind_remote_symbol = "⇣", + ahead_remote_color = "bright magenta", + ahead_remote_symbol = "⇡", + + show_stashes = true, + stashes_color = "bright magenta", + stashes_symbol = "$", + + show_state = true, + show_state_prefix = true, + state_color = "red", + state_symbol = "~", + + show_staged = true, + staged_color = "bright yellow", + staged_symbol = "+", + + show_unstaged = true, + unstaged_color = "bright yellow", + unstaged_symbol = "!", + + show_untracked = true, + untracked_color = "blue", + untracked_symbol = "?", +}) +``` + +### Theme + +You can also use a [theme](https://github.com/imsi32/yatline-themes): + +```lua +local your_theme = { + branch_color = "blue", + remote_branch_color = "bright magenta", + tag_color = "magenta", + commit_color = "bright magenta", + behind_remote_color = "bright magenta", + ahead_remote_color = "bright magenta", + stashes_color = "bright magenta", + state_color = "red", + staged_color = "bright yellow", + unstaged_color = "bright yellow", + untracked_color = "blue", +} + +require("yatline-githead"):setup({ +-- === + + theme = your_theme, + +-- === +}) +``` + +### Adding as a Component + +If you are using yatline.yazi, you can use this component: + +``` lua +-- === + + {type = "coloreds", custom = false, name = "githead"}, + +-- === +``` + +``` text +/cwd on ( feature):origin/main #v1.0.0 #1234567 ⇣2⇡3 $1 rebase 1/2 ~2 +4 !1 ?5 +| | | | ││ | | | | | | | | | | | | +| | | | ││ | | | | | | | | | | | └─── untracked_symbol +| | | | ││ | | | | | | | | | | └────── unstaged_symbol +| | | | ││ | | | | | | | | | └───────── staged_symbol +| | | | ││ | | | | | | | | └──────────── state_symbol +| | | | ││ | | | | | | | └─────────────────────── state_prefix +| | | | ││ | | | | | | └────────────────────────── stashes_symbol +| | | | ││ | | | | | └───────────────────────────── ahead_symbol +| | | | ││ | | | | └─────────────────────────────── behind_symbol +| | | | ││ | | | └──────────────────────────────────────── commit_symbol +| | | | ││ | | └──────────────────────────────────────────────── tag_symbol +| | | | ││ | └───────────────────────────────────────────────────── remote_branch +| | | | ││ └─────────────────────────────────────────────────────────── remote_repo +| | | | |└───────────────────────────────────────────────────────────── remote_branch_prefix +| | | | └────────────────────────────────────────────────────────────── branch_borders +| | | └──────────────────────────────────────────────────────────────────── branch +| | └─────────────────────────────────────────────────────────────────────── branch_symbol +| └─────────────────────────────────────────────────────────────────────────── branch_prefix +└──────────────────────────────────────────────────────────────────────────────── cwd +``` + +## Features + +- [x] Current branch +- [x] Remote and remote branch +- [x] Latest tag +- [x] Latest commit +- [x] Behind/Ahead of the remote +- [x] Stashes +- [x] States + - [x] merge + - [x] cherry + - [x] rebase (+ done counter) + - [x] revert + - [x] bisect +- [x] Staged +- [x] Unstaged +- [x] Untracked + +### Under the hood + +The goal is to use minimum amount of shell commands. + +- Branch, stashes, staged files, unstaged files, untracked files. + +```shell +git status --ignore-submodules=dirty --branch --show-stash --ahead-behind +``` + +- Remote branch and repo: + +```shell +git rev-parse --abbrev-ref --symbolic-full-name @{upstream} +``` + +- Latest tag and commit: + +```shell +git log --format="commit %h%d" -n 1 +``` + +## Credits + +- [githead.yazi](https://github.com/llanosrocas/githead.yazi) +- [yazi source code](https://github.com/sxyazi/yazi) +- [powerlevel10k](https://github.com/romkatv/powerlevel10k) +- [twio142](https://github.com/twio142/githead.yazi) diff --git a/config/yazi/plugins/yatline-githead.yazi/main.lua b/config/yazi/plugins/yatline-githead.yazi/main.lua new file mode 100644 index 00000000..ab49987f --- /dev/null +++ b/config/yazi/plugins/yatline-githead.yazi/main.lua @@ -0,0 +1,569 @@ +---@diagnostic disable: undefined-global + +local save = ya.sync(function(this, cwd, output) + if cwd:match("%.git[/\\]") or cwd:match("%.git$") then + this.output = nil + return + end + if cx.active.current.cwd == Url(cwd) then + ---@type GitStatusData + this.output = output + ui.render() + end +end) + +return { + setup = function(this, options) + options = options or {} + + local config = { + order = options.order or { + "branch", + "remote_branch", + "tag", + "commit", + "behind_ahead_remote", + "stashes", + "state", + "staged", + "unstaged", + "untracked", + }, + + show_numbers = options.show_numbers == nil and true or options.show_numbers, + + show_branch = options.show_branch == nil and true or options.show_branch, + branch_prefix = options.branch_prefix or "", + branch_symbol = options.branch_symbol or "", + branch_borders = options.branch_borders or "", + + show_remote_branch = options.show_remote_branch == nil and true or options.show_remote_branch, + always_show_remote_branch = options.always_show_remote_branch == nil and false + or options.always_show_remote_branch, + always_show_remote_repo = options.always_show_remote_repo == nil and false + or options.always_show_remote_repo, + remote_branch_prefix = options.remote_branch_prefix or ":", + + show_tag = options.show_tag == nil and true or options.show_tag, + always_show_tag = options.always_show_tag == nil and false or options.always_show_tag, + tag_symbol = options.tag_symbol == nil and "#" or options.tag_symbol, + + show_commit = options.show_commit == nil and true or options.show_commit, + always_show_commit = options.always_show_commit == nil and false or options.always_show_commit, + commit_symbol = options.commit_symbol == nil and "@" or options.commit_symbol, + + show_behind_ahead_remote = options.show_behind_ahead_remote == nil and true + or options.show_behind_ahead_remote, + behind_remote_symbol = options.behind_remote_symbol or "⇣", + ahead_remote_symbol = options.ahead_remote_symbol or "⇡", + + show_stashes = options.show_stashes == nil and true or options.show_stashes, + stashes_symbol = options.stashes_symbol or "$", + + show_state = options.show_state == nil and true or options.show_state, + show_state_prefix = options.show_state_prefix == nil and true or options.show_state_prefix, + state_symbol = options.state_symbol or "~", + + show_staged = options.show_staged == nil and true or options.show_staged, + staged_symbol = options.staged_symbol or "+", + + show_unstaged = options.show_unstaged == nil and true or options.show_unstaged, + unstaged_symbol = options.unstaged_symbol or "!", + + show_untracked = options.show_untracked == nil and true or options.show_untracked, + untracked_symbol = options.untracked_symbol or "?", + } + + if options.theme then + options = options.theme + end + + local theme = { + branch_color = options.branch_color or "blue", + remote_branch_color = options.remote_branch_color or "bright magenta", + tag_color = options.tag_color or "magenta", + commit_color = options.commit_color or "bright magenta", + behind_remote_color = options.behind_remote_color or "bright magenta", + ahead_remote_color = options.ahead_remote_color or "bright magenta", + stashes_color = options.stashes_color or "bright magenta", + state_color = options.state_color or "red", + staged_color = options.staged_color or "bright yellow", + unstaged_color = options.unstaged_color or "bright yellow", + untracked_color = options.untracked_color or "bright blue", + } + + local function render_branch() + local branch = this.output.branch + + if not branch then + return nil + end + + local left_border = config.branch_borders:sub(1, 1) + local right_border = config.branch_borders:sub(2, 2) + + local branch_string = "" + + if config.branch_symbol == "" then + branch_string = left_border .. branch .. right_border + else + branch_string = left_border .. config.branch_symbol .. branch .. right_border + end + + local branch_prefix = config.branch_prefix == "" and "" or config.branch_prefix .. " " + + return { + { branch_prefix, nil }, + { branch_string, theme.branch_color }, + } + end + + local function render_remote_branch() + local branch = this.output.branch + local remote_branch = this.output.remote_branch + local remote_repo = this.output.remote_repo + + if not remote_branch then + return nil + end + + local show_remote = config.always_show_remote_branch or branch ~= remote_branch + if not show_remote then + return nil + end + + local remote_branch_label = config.always_show_remote_repo and (remote_repo .. "/" .. remote_branch) + or remote_branch + + return { + { config.remote_branch_prefix, nil }, + { remote_branch_label, theme.remote_branch_color }, + } + end + + local function render_tag() + local branch = this.output.branch + local tag = this.output.tag + + if not tag then + return nil + end + + if not branch or config.always_show_tag then + return { + { config.tag_symbol, nil }, + { tag, theme.tag_color }, + } + end + end + + local function render_commit() + local branch = this.output.branch + local tag = this.output.tag + local commit = this.output.commit + + if not commit then + return nil + end + + if (not branch and not tag) or config.always_show_commit then + return { + { config.commit_symbol, nil }, + { commit, theme.commit_color }, + } + end + end + + local function render_behind_ahead_remote() + local behind = this.output.behind_remote + local ahead = this.output.ahead_remote + + local behind_label = behind + and behind > 0 + and { + config.behind_remote_symbol .. (config.show_numbers and behind or ""), + theme.behind_remote_color, + } + + local ahead_label = ahead + and ahead > 0 + and { + config.ahead_remote_symbol .. (config.show_numbers and ahead or ""), + theme.ahead_remote_color, + } + + if ahead_label and behind_label then + return { + behind_label, + ahead_label, + } + elseif ahead_label then + return ahead_label + elseif behind_label then + return behind_label + else + return nil + end + end + + local function render_stashes() + local stashes_count = this.output.stashes + + if not stashes_count then + return nil + end + + local stashes_label = config.stashes_symbol + + if config.show_numbers then + stashes_label = stashes_label .. stashes_count + end + + return { stashes_label, theme.stashes_color } + end + + local function render_state() + local state_prefix = this.output.state_prefix + local unmerged_count = this.output.unmerged_count + local rebase_done = this.output.rebase_done + local rebase_total = this.output.rebase_total + + if state_prefix then + local state_label = "" + if config.show_state_prefix then + state_label = state_prefix + if state_prefix == "rebase-i" and config.show_numbers and rebase_done and rebase_total then + state_label = state_label .. " " .. rebase_done .. "/" .. rebase_total + end + end + + local unmerged_label = "" + local symbol = "" + + if unmerged_count and unmerged_count > 0 then + symbol = " " .. config.state_symbol + if config.show_numbers then + unmerged_label = tostring(unmerged_count) + end + end + + return { state_label .. symbol .. unmerged_label, theme.state_color } + end + return nil + end + + local function render_staged() + local staged = this.output.staged + + if staged then + local staged_label = config.staged_symbol + + if config.show_numbers then + local filtered_staged = staged:gsub("^[%s]*%b()[%s]*", "") + + local staged_count = 0 + for line in filtered_staged:gmatch("[^\r\n]+") do + if line:match("%S") then + staged_count = staged_count + 1 + end + end + + staged_label = staged_label .. staged_count + end + + return { staged_label, theme.staged_color } + end + + return nil + end + + local function render_unstaged() + local unstaged = this.output.unstaged + + if unstaged then + local unstaged_label = config.unstaged_symbol + + if config.show_numbers then + local filtered_unstaged = unstaged:gsub("^[%s]*%b()[\r\n]*", ""):gsub("^[%s]*%b()[\r\n]*", "") + + local unstaged_count = 0 + for line in filtered_unstaged:gmatch("[^\r\n]+") do + if line:match("%S") then + unstaged_count = unstaged_count + 1 + end + end + + unstaged_label = unstaged_label .. unstaged_count + end + + return { unstaged_label, theme.unstaged_color } + end + + return nil + end + + local function render_untracked() + local untracked = this.output.untracked + + if untracked then + local untracked_label = config.untracked_symbol + + if config.show_numbers then + local filtered_untracked = untracked:gsub("^[%s]*%b()[\r\n]*", "") + local untracked_count = 0 + + for line in filtered_untracked:gmatch("[^\r\n]+") do + if line:match("%S") then + untracked_count = untracked_count + 1 + end + end + + untracked_label = untracked_label .. untracked_count + end + + return { untracked_label, theme.untracked_color } + end + + return nil + end + + function Yatline.coloreds.get:githead() + if not this.output then + return nil + end + + local fn_table = { + branch = render_branch, + remote_branch = render_remote_branch, + tag = render_tag, + commit = render_commit, + behind_ahead_remote = render_behind_ahead_remote, + stashes = render_stashes, + state = render_state, + staged = render_staged, + unstaged = render_unstaged, + untracked = render_untracked, + } + + local head = {} + + for _, key in ipairs(config.order) do + local fn = fn_table[key] + local is_shown = config["show_" .. key] + + if fn and is_shown then + local value = fn() + if value then + if #head ~= 0 and head[#head][1] ~= " " and head[#head][2] ~= "black" then + table.insert(head, { " ", "black" }) + end + if type(value[1]) == "table" then + table.move(value, 1, #value, #head + 1, head) + else + table.insert(head, value) + end + end + end + end + + if #head ~= 0 then + return head + else + return nil + end + end + + local callback = function() + local cwd = cx.active.current.cwd + ya.emit("plugin", { + this._id, + ya.quote(tostring(cwd), true), + }) + end + + ps.sub("cd", callback) + ps.sub("rename", callback) + ps.sub("bulk", callback) + ps.sub("move", callback) + ps.sub("trash", callback) + ps.sub("delete", callback) + ps.sub("tab", callback) + end, + + entry = function(_, job) + local args = job.args or job + + ---@class GitStatusData + ---@field ahead_remote? integer + ---@field behind_remote? integer + ---@field branch? string + ---@field commit? string + ---@field rebase_done? integer + ---@field remote_branch? string + ---@field remote_repo? string + ---@field staged? string + ---@field stashes? integer + ---@field state_prefix? "bisect"|"cherry"|"merge"|"rebase-i"|"revert" + ---@field tag? string + ---@field unmerged_count? integer + ---@field unstaged? string + ---@field untracked? string + local data = {} + + --- @param status string + local get_behind_ahead_remote = function(status) + local diverged_ahead, diverged_behind = status:match("have (%d+) and (%d+) different") + if diverged_ahead and diverged_behind then + data.behind_remote = tonumber(diverged_behind) + data.ahead_remote = tonumber(diverged_ahead) + else + local behind_remote = status:match("behind %S+ by (%d+) commits?") + local ahead_remote = status:match("ahead of %S+ by (%d+) commits?") + + data.behind_remote = tonumber(behind_remote) + data.ahead_remote = tonumber(ahead_remote) + end + end + + --- @param status string + local get_branch = function(status) + data.branch = status:match("On branch (%S+)") + end + + --- @param status string + local get_stashes = function(status) + data.stashes = tonumber(status:match("Your stash currently has (%d+)")) + end + + --- @param status string + local get_state = function(status) + local unmerged = status:match("Unmerged paths:%s*(.-)%s*\n\n") + if unmerged then + local filtered_unmerged = unmerged:gsub("^[%s]*%b()[%s]*", ""):gsub("^[%s]*%b()[%s]*", "") + local count = 0 + for line in filtered_unmerged:gmatch("[^\r\n]+") do + if line:match("%S") then + count = count + 1 + end + end + data.unmerged_count = count + end + + if status:find("git merge") then + data.state_prefix = "merge" + elseif status:find("git cherry%-pick") then + data.state_prefix = "cherry" + elseif status:find("git rebase") then + data.state_prefix = "rebase-i" + local commands_done = status:match("%((%d+) commands? done%)") + if commands_done then + data.rebase_done = commands_done + if not data.unmerged_count then + data.rebase_total = commands_done + else + data.rebase_total = data.unmerged_count + end + end + elseif status:find("git revert") then + data.state_prefix = "revert" + elseif status:find("git bisect") then + data.state_prefix = "bisect" + end + end + + --- @param status string + local get_staged = function(status) + data.staged = status:match("Changes to be committed:%s*(.-)%s*\n\n") + end + + --- @param status string + local get_unstaged = function(status) + data.unstaged = status:match("Changes not staged for commit:%s*(.-)%s*\n\n") + end + + --- @param status string + local get_untracked = function(status) + data.untracked = status:match("Untracked files:%s*(.-)%s*\n\n") + end + + local get_status = function() + local cmd = Command("git") + :arg({ + "status", + "--ignore-submodules=dirty", + "--branch", + "--show-stash", + "--ahead-behind", + }) + :cwd(args[1]) + :env("LANGUAGE", "en_US.UTF-8") + :stdout(Command.PIPED) + local cmd_output = cmd:output() + + if cmd_output then + local status = cmd_output.stdout + + get_branch(status) + get_behind_ahead_remote(status) + get_stashes(status) + get_state(status) + get_staged(status) + get_unstaged(status) + get_untracked(status) + end + end + + local get_remote_branch = function() + local cmd = Command("git") + :arg({ + "rev-parse", + "--abbrev-ref", + "--symbolic-full-name", + "@{upstream}", + }) + :cwd(args[1]) + :env("LANGUAGE", "en_US.UTF-8") + :stdout(Command.PIPED) + local cmd_output = cmd:output() + + if cmd_output then + local remote_branch = cmd_output.stdout + + data.remote_branch = remote_branch:gsub("[\r\n]", ""):match("^[^/]+/(.+)") + data.remote_repo = remote_branch:gsub("[\r\n]", ""):match("^([^/]+)/") + end + end + + --- @param log string + local get_tag = function(log) + data.tag = log:match("tag: ([^, )]+)") + end + + --- @param log string + local get_commit = function(log) + data.commit = log:match("^commit%s+([a-f0-9]+)") + end + + local get_git_log = function() + local cmd = Command("git") + :arg({ "log", "--format=commit %h%d", "-n", "1" }) + :cwd(args[1]) + :env("LANGUAGE", "en_US.UTF-8") + :stdout(Command.PIPED) + local cmd_output = cmd:output() + + if cmd_output then + local log = cmd_output.stdout + + get_tag(log) + get_commit(log) + end + end + + get_status() + get_remote_branch() + get_git_log() + + save(args[1], data) + end, +} diff --git a/config/yazi/plugins/yatline.yazi/LICENSE b/config/yazi/plugins/yatline.yazi/LICENSE new file mode 100644 index 00000000..6374a660 --- /dev/null +++ b/config/yazi/plugins/yatline.yazi/LICENSE @@ -0,0 +1,23 @@ +MIT License + +Copyright (c) 2024 imsi32 +Copyright (c) 2023 - sxyazi +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/yatline.yazi/README.md b/config/yazi/plugins/yatline.yazi/README.md new file mode 100644 index 00000000..1ee9c1eb --- /dev/null +++ b/config/yazi/plugins/yatline.yazi/README.md @@ -0,0 +1,23 @@ +# yatline.yazi + +The first Yazi plugin for customizing both header-line and status-line. + + + +> [!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 +- Support for Yazi Plugins +- Themes (See: [yatline-themes](https://github.com/imsi32/yatline-themes)) +- Add-ons (See: [yatline-addons](https://github.com/imsi32/yatline-addons)) + +## Credits + +- [Lualine](https://github.com/nvim-lualine/lualine.nvim) +- [Yazi](https://github.com/sxyazi/yazi) diff --git a/config/yazi/plugins/yatline.yazi/main.lua b/config/yazi/plugins/yatline.yazi/main.lua new file mode 100644 index 00000000..eda7213b --- /dev/null +++ b/config/yazi/plugins/yatline.yazi/main.lua @@ -0,0 +1,1464 @@ +--- @since 25.12.29 +--- @diagnostic disable: undefined-global, undefined-field +--- @alias Mode Mode Comes from Yazi. +--- @alias Rect Rect Comes from Yazi. +--- @alias Paragraph Paragraph Comes from Yazi. +--- @alias Line Line Comes from Yazi. +--- @alias Span Span Comes from Yazi. +--- @alias Color Color Comes from Yazi. + +--==================-- +-- Type Declaration -- +--==================-- + +--- @enum Side +local Side = { + LEFT = 0, -- The left side of either the header-line or status-line. [ LEFT ... ] + RIGHT = 1, -- The right side of either the header-line or status-line. [ ... RIGHT] +} + +--- @enum SeparatorType +local SeparatorType = { + OUTER = 0, -- Separators on the outer side of sections. [ c o | c o | c o ... ] or [ ... o c | o c | o c ] + INNER = 1, -- Separators on the inner side of sections. [ c i c | c i c | c i c ... ] or [ ... c i c | c i c | c i c ] +} + +--- @enum ComponentType +local ComponentType = { + A = 0, -- Components on the first section. [ A | | ... ] or [ ... | | A ] + B = 1, -- Components on the second section. [ | B | ... ] or [ ... | B | ] + C = 2, -- Components on the third section. [ | | C ... ] or [ ... C | | ] +} + +--- @alias Colored [string, Color] Stores text and its foreground color. +--- @alias Coloreds Colored[] The array of Coloreds. + +--- @generic T +--- @alias T T Type of the component. + +--- @class Yatline +--- @field config YatlineConfig Configuration of Yatline. +--- @field string? {} Table that stores string components. +--- @field line? {} Table that stores Line components. +--- @field coloreds? {} Table that stores Coloreds components. +Yatline = {} + +--- @class (exact) ComponentConfig +--- @field type string Defines the type of the component (T). +--- @field custom? boolean Toggles the usage of a function defined or name field. +--- @field name string | T Either defined function name or variable of defined type T. +--- @field params? {} Contains the parameters that can be used by the function called. + +--- @class (exact) SideConfig +--- @field section_a ComponentConfig[] Array of configuration of components in the first section. +--- @field section_b ComponentConfig[] Array of configuration of components in the second section. +--- @field section_c ComponentConfig[] Array of configuration of components in the third section. + +--- @class (exact) LineConfig +--- @field left SideConfig Configuration of the left side of the line. +--- @field right SideConfig Configuration of the right side of the line. + +--- @class (exact) YatlineConfig +--- @field section_separator {open: string, close: string} Separators that are between sections. +--- @field part_separator {open: string, close: string} Separators that are between components. +--- @field inverse_separator {open: string, close: string} Separators that are used when foreground color of separator is reset. +--- @field padding {inner: integer, outer: integer} Number of space padding surronding the component. +--- @field style_a {bg: Color, fg: Color, bg_mode: {normal: Color, select: Color, un_set: Color}} Style of the first section. +--- @field style_b {bg: Color, fg: Color} Style of the second section. +--- @field style_c {bg: Color, fg: Color} Style of the third section. +--- @field permissions_t_fg Color Foreground color of the type of permission. +--- @field permissions_r_fg Color Foreground color of the read permission. +--- @field permissions_w_fg Color Foreground color of the write permission. +--- @field permissions_x_fg Color Foreground color of the execute permission. +--- @field permissions_s_fg Color Foreground color of the separators between permission. +--- @field tab_width integer Maximum tab width of the tabs component. +--- @field selected {icon: string, fg: Color} Configuration for the count of files that selected. +--- @field copied {icon: string, fg: Color} Configuration for the count of files that copied. +--- @field cut {icon: string, fg: Color} Configuration for the count of files that cut. +--- @field files {icon: string, fg: Color} Configuration for the count of files in the active tab. +--- @field filtereds {icon: string, fg: Color} Configuration for the count of files in the active tab that are filtered. +--- @field total {icon: string, fg: Color} Configuration for the count of progress tasks that finished. +--- @field success {icon: string, fg: Color} Configuration for the count of progress tasks that successed. +--- @field failed {icon: string, fg: Color} Configuration for the count of progress tasks that failed. +--- @field show_background boolean Toggle the visibility of the background where no component exists. +--- @field display_header_line boolean Toggle the visibility of the header-line. +--- @field display_status_line boolean Toggle the visibility of the status-line. +--- @field component_positions string[] Arrange positions of the Yazi sections. +--- @field header_line LineConfig Configuration of header-line components. +--- @field status_line LineConfig Configuration of status-line components. +Yatline.config = { + section_separator = { open = "", close = "" }, + part_separator = { open = "", close = "" }, + inverse_separator = { open = "", close = "" }, + + padding = { inner = 1, outer = 1 }, + + style_a = { + bg = "white", + fg = "black", + bg_mode = { + normal = "white", + select = "brightyellow", + un_set = "brightred", + }, + }, + style_b = { bg = "brightblack", fg = "brightwhite" }, + style_c = { bg = "black", fg = "brightwhite" }, + + permissions_t_fg = "green", + permissions_r_fg = "yellow", + permissions_w_fg = "red", + permissions_x_fg = "cyan", + permissions_s_fg = "white", + + tab_width = 20, + + selected = { icon = "", fg = "yellow" }, + copied = { icon = "", fg = "green" }, + cut = { icon = "", fg = "red" }, + + files = { icon = "", fg = "blue" }, + filtereds = { icon = "", fg = "magenta" }, + + total = { icon = "", fg = "yellow" }, + success = { icon = "", fg = "green" }, + failed = { icon = "", fg = "red" }, + + show_background = true, + + display_header_line = true, + display_status_line = true, + + component_positions = { "header", "tab", "status" }, + + header_line = { + left = { + section_a = { + { type = "line", name = "tabs" }, + }, + section_b = {}, + section_c = {}, + }, + right = { + section_a = { + { type = "string", name = "date", params = { "%A, %d %B %Y" } }, + }, + section_b = { + { type = "string", name = "date", params = { "%X" } }, + }, + section_c = {}, + }, + }, + + status_line = { + left = { + section_a = { + { type = "string", name = "tab_mode" }, + }, + section_b = { + { type = "string", name = "hovered_size" }, + }, + section_c = { + { type = "string", name = "hovered_path" }, + { type = "coloreds", name = "count" }, + }, + }, + right = { + section_a = { + { type = "string", name = "cursor_position" }, + }, + section_b = { + { type = "string", name = "cursor_percentage" }, + }, + section_c = { + { type = "string", name = "hovered_file_extension", params = { true } }, + { type = "coloreds", name = "permissions" }, + }, + }, + }, +} + +--=================-- +-- Component Setup -- +--=================-- + +--- Sets the background of style_a according to the tab's mode. +--- @param mode Mode The mode of the active tab. +--- @see cx.active.mode To get the active tab's mode. +local function set_mode_style(mode) + if mode.is_select then + Yatline.config.style_a.bg = Yatline.config.style_a.bg_mode.select + elseif mode.is_unset then + Yatline.config.style_a.bg = Yatline.config.style_a.bg_mode.un_set + else + Yatline.config.style_a.bg = Yatline.config.style_a.bg_mode.normal + end +end + +--- Helper function to apply style table to a component +--- @param component Span The component to style +--- @param style table The style table with fg and/or bg fields +local function apply_style_table(component, style) + if not style then + return component + end + -- Apply manually + if style.fg then + component:fg(style.fg) + end + if style.bg then + component:bg(style.bg) + end + if style.bold then + component:bold() + end + if style.dim then + component:dim() + end + if style.italic then + component:italic() + end + if style.underline then + component:underline() + end + if style.blink then + component:blink() + end + if style.blink_rapid then + component:blink_rapid() + end + if style.reverse then + component:reverse() + end + if style.hidden then + component:hidden() + end + if style.crossed then + component:crossed() + end + + return component +end + +--- Helper function to apply style table to a component +--- @param component Span The component to style +--- @param style table The style table with fg and/or bg fields +local function apply_style_table(component, style) + if not style then + return component + end + -- Apply manually + if style.fg then component:fg(style.fg) end + if style.bg then component:bg(style.bg) end + if style.bold then component:bold() end + if style.dim then component:dim() end + if style.italic then component:italic() end + if style.underline then component:underline() end + if style.blink then component:blink() end + if style.blink_rapid then component:blink_rapid() end + if style.reverse then component:reverse() end + if style.hidden then component:hidden() end + if style.crossed then component:crossed() end + return component +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 ]. +--- @see Style To see how to style, in Yazi's documentation. +local function set_component_style(component, component_type) + if component_type == ComponentType.A then + apply_style_table(component, Yatline.config.style_a):bold() + elseif component_type == ComponentType.B then + apply_style_table(component, Yatline.config.style_b) + else + apply_style_table(component, Yatline.config.style_c) + end +end + +--- Surronds component with paddings. +--- @param component string | Span | Line Component that will be connected to paddings. +--- @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. +--- @return Line line A Line which is a component that has padding. +local function connect_padding(component, component_type, in_side) + local inner = ui.Span(string.rep(" ", Yatline.config.padding.inner)) + local outer = ui.Span(string.rep(" ", Yatline.config.padding.outer)) + + set_mode_style(cx.active.mode) + set_component_style(inner, component_type) + set_component_style(outer, component_type) + + if in_side == Side.LEFT then + return ui.Line({ outer, component, inner }) + else + return ui.Line({ inner, component, outer }) + end +end + +--- Connects component to a separator. +--- @param component Span Component that will be connected to separator. +--- @param in_side Side Left or right side of the either header-line or status-line. +--- @param separator_type SeparatorType Where will there be a separator in the section. +--- @param separator_style {bg: string?, fg: string?} Holds the style of the separator. +--- @return Line line A Line which may have either both component and separator, or component. +local function connect_separator(component, in_side, separator_type, separator_style) + local open, close + if + separator_type == SeparatorType.OUTER and not (separator_style.bg == "reset" and separator_style.fg == "reset") + then + open = ui.Span(Yatline.config.section_separator.open) + close = ui.Span(Yatline.config.section_separator.close) + + if separator_style.fg == "reset" then + if separator_style.bg ~= "" then + open = ui.Span(Yatline.config.inverse_separator.open) + close = ui.Span(Yatline.config.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(Yatline.config.part_separator.open) + close = ui.Span(Yatline.config.part_separator.close) + end + + apply_style_table(open, separator_style) + apply_style_table(close, separator_style) + + if in_side == Side.LEFT then + return ui.Line({ component, close }) + else + return ui.Line({ open, component }) + end +end + +--==================-- +-- Helper Functions -- +--==================-- + +--- Gets the file name from given file extension. +--- @param file_name string The name of a file whose extension will be taken. +--- @return string file_extension Extension of a file. +local function get_file_extension(file_name) + local extension = file_name:match("^.+%.(.+)$") + + if extension == nil or extension == "" then + return "null" + else + return extension + end +end + +--- Reverse the order of given array +--- @param array Line Array which wants to be reversed. +--- @return table reversed Reversed ordered given array. +local function reverse_order(array) + local reversed = {} + for i = #array, 1, -1 do + table.insert(reversed, array[i]) + end + + return reversed +end + +--- the number of characters in a UTF-8 string +--- @param s string The string to process. +--- @return integer The number of characters in the string. +local function utf8len(s) + -- count the number of non-continuing bytes + return select(2, s:gsub("[^\128-\193]", "")) +end + +--- like string.sub() but i, j are utf8 strings +--- a utf8-safe string.sub() +--- @param s string The string to process. +--- @param i integer The start position. +--- @param j integer The end position. +--- @return string The substring. +local function utf8sub(s, i, j) + -- pattern for matching UTF-8 characters + local pattern = "[%z\1-\127\194-\244][\128-\191]*" + + -- helper function for position calculation + --- @param pos integer The position of the character. + --- @param len integer The length of the string. + --- @return integer The relative position of the character. + local function posrelat(pos, len) + if pos < 0 then + pos = len + pos + 1 + end + return pos + end + + -- helper function to iterate over UTF-8 chars + local function chars(_s, no_subs) + local function map(f) + local _i = 0 + if no_subs then + for b, e in _s:gmatch("()" .. pattern .. "()") do + _i = _i + 1 + local c = e - b + f(_i, c, b) + end + else + for b, c in _s:gmatch("()(" .. pattern .. ")") do + _i = _i + 1 + f(_i, c, b) + end + end + end + return coroutine.wrap(function() + return map(coroutine.yield) + end) + end + + local l = utf8len(s) + + i = posrelat(i, l) + j = j and posrelat(j, l) or l + + if i < 1 then + i = 1 + end + if j > l then + j = l + end + + if i > j then + return "" + end + + local diff = j - i + local iter = chars(s, true) + + -- advance up to i + for _ = 1, i - 1 do + iter() + end + + local c, b = select(2, iter()) + + -- becareful with the edge case of empty string + if not b then + return "" + end + + -- i and j are the same, single-character sub + if diff == 0 then + return string.sub(s, b, b + c - 1) + end + + i = b + + -- advance up to j + for _ = 1, diff - 1 do + iter() + end + + c, b = select(2, iter()) + + return string.sub(s, i, b + c - 1) +end + +--- Trims the filename if it is longer than the max_length. +--- @param filename string The name of a file which will be trimmed. +--- @param max_length integer Maximum length of the filename. +--- @param trim_length integer Length of the trimmed filename. +--- @return string trimmed_filename Trimmed filename. +local function trim_filename(filename, max_length, trim_length) + if not max_length or not trim_length then + return filename + end + + -- Count UTF-8 characters + local len = utf8len(filename) + + if len <= max_length then + return filename + end + + if len <= trim_length * 2 then + return filename + end + + return utf8sub(filename, 1, trim_length) .. "..." .. utf8sub(filename, len - trim_length + 1, len) +end + +--========================-- +-- Component String Group -- +--========================-- + +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. +--- @param trimmed? boolean Whether to trim the filename if it's too long (default: false) +--- @param max_length? integer Maximum length of the filename (default: 24) +--- @param trim_length? integer Length of each end when trimming (default: 10) +--- @param show_symlink? boolean Whether to show symlink target (default: false) +--- @return string name Current active tab's hovered file's name +function Yatline.string.get:hovered_name(trimmed, max_length, trim_length, show_symlink) + trimmed = trimmed or false + max_length = max_length or 24 + trim_length = trim_length or 10 + show_symlink = show_symlink or false + + local hovered = cx.active.current.hovered + if not hovered then + return "" + end + + local link_delimiter = " -> " + local linked = (show_symlink and hovered.link_to ~= nil) and (link_delimiter .. tostring(hovered.link_to)) or "" + + if trimmed then + local trimmed_name = trim_filename(hovered.name, max_length, trim_length) + local trimmed_linked = #linked ~= 0 + and link_delimiter .. trim_filename( + string.sub(linked, #link_delimiter + 1, -1), + max_length, + trim_length + ) + or "" + return trimmed_name .. trimmed_linked + else + return hovered.name .. linked + end +end + +--- Gets the hovered file's path of the current active tab. +--- @param trimmed? boolean Whether to trim the file path if it's too long (default: false) +--- @param max_length? integer Maximum length of the file path (default: 24) +--- @param trim_length? integer Length of each end when trimming (default: 10) +--- @return string path Current active tab's hovered file's path. +function Yatline.string.get:hovered_path(trimmed, max_length, trim_length) + trimmed = trimmed or false + max_length = max_length or 24 + trim_length = trim_length or 10 + + local hovered = cx.active.current.hovered + if not hovered then + return "" + end + + if trimmed then + return trim_filename(ya.readable_path(tostring(hovered.url)), max_length, trim_length) + else + return ya.readable_path(tostring(hovered.url)) + end +end + +--- Gets the hovered file's size of the current active tab. +--- @return string size Current active tab's hovered file's 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.len) + else + return "" + end +end + +--- Gets the hovered file's path of the current active tab. +--- @return string mime Current active tab's hovered file's mime. +function Yatline.string.get:hovered_mime() + local hovered = cx.active.current.hovered + if hovered then + return hovered:mime() + else + return "" + end +end + +--- Gets the hovered file's user and group ownership of the current active tab. +--- Unix-like systems only. +--- @return string ownership Current active tab's hovered file's user and group ownership. +function Yatline.string.get:hovered_ownership() + local hovered = cx.active.current.hovered + + if hovered then + if not hovered.cha.uid or not hovered.cha.gid then + return "" + end + + local username = ya.user_name(hovered.cha.uid) or tostring(hovered.cha.uid) + local groupname = ya.group_name(hovered.cha.gid) or tostring(hovered.cha.gid) + + return username .. ":" .. groupname + else + return "" + end +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 Yatline.string.get:hovered_file_extension(show_icon) + local hovered = cx.active.current.hovered + + if hovered then + local cha = hovered.cha + + local name + if cha.is_dir then + name = "dir" + else + name = get_file_extension(hovered.url.name) + end + + if show_icon then + local icon = hovered:icon().text + return icon .. " " .. name + else + return name + end + else + return "" + end +end + +--- Gets the path of the current active tab. +--- @param trimmed? boolean Whether to trim the current active tab's path if it's too long (default: false) +--- @param max_length? integer Maximum length of the current active tab's path (default: 24) +--- @param trim_length? integer Length of each end when trimming (default: 10) +--- @return string path Current active tab's path. +function Yatline.string.get:tab_path(trimmed, max_length, trim_length) + trimmed = trimmed or false + max_length = max_length or 24 + trim_length = trim_length or 10 + + local cwd = cx.active.current.cwd + local filter = cx.active.current.files.filter + local finder = cx.active.finder + + local t = {} + if cwd.is_search then + t[#t + 1] = string.format("search: %s", cwd.domain) + end + if filter then + t[#t + 1] = string.format("filter: %s", filter) + end + if finder then + t[#t + 1] = string.format("find: %s", finder) + end + + local suffix + if #t ~= 0 then + suffix = " (" .. table.concat(t, ", ") .. ")" + else + suffix = "" + end + + if trimmed then + return trim_filename(ya.readable_path(tostring(cwd)), max_length, trim_length) .. suffix + else + return ya.readable_path(tostring(cwd)) .. suffix + end +end + +--- Gets the filtered query. +--- @param key? string Key value that indicates filtered query (default: "filter:") +--- @return string query Filtered query. +function Yatline.string.get:filter_query(key) + key = key or "filter:" + + local filter = cx.active.current.files.filter + + if filter then + return string.format("%s %s", key, tostring(filter)) + else + return "" + end +end + +--- Gets the searched query. +--- @param key? string Key value that indicates searched query (default: "search:") +--- @return string query Searched query. +function Yatline.string.get:search_query(key) + key = key or "search:" + + local cwd = cx.active.current.cwd + + if cwd.is_search then + return string.format("%s %s", key, cwd.domain) + else + return "" + end +end + +--- Gets the finded query. +--- @param key? string Key value that indicates finded query (default: "find:") +--- @return string query Finded query. +function Yatline.string.get:finder_query(key) + key = key or "find:" + + local finder = cx.active.finder + + if finder then + return string.format("%s %s", key, tostring(finder)) + else + return "" + end +end + +--- Gets the mode of active tab. +--- @return string mode Active tab's mode. +function Yatline.string.get:tab_mode() + local mode = tostring(cx.active.mode):upper() + if mode == "UNSET" then + mode = "UN-SET" + end + + return mode +end + +--- Gets the number of files in the current active tab. +--- @return string num_files Number of files in the current active tab. +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 Yatline.string.get:cursor_position() + local cursor = cx.active.current.cursor + local length = #cx.active.current.files + + if length ~= 0 then + return string.format("%d/%d", cursor + 1, length) + else + return "0" + end +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 Yatline.string.get:cursor_percentage() + local percentage = 0 + local cursor = cx.active.current.cursor + local length = #cx.active.current.files + if cursor ~= 0 and length ~= 0 then + percentage = math.floor((cursor + 1) * 100 / length) + end + + if percentage == 0 then + return "Top" + elseif percentage == 100 then + return "Bot" + else + return string.format("%d%%", percentage) + end +end + +--- Gets the local date or time values. +--- @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 Yatline.string.get:date(format) + return tostring(os.date(format)) +end + +--======================-- +-- Component Line Group -- +--======================-- + +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? string Left or right side of the either header-line or status-line. +--- @return Line line Customized Line which contains tabs. +--- @see set_mode_style To know how mode style selected. +--- @see set_component_style To know how component style applied. +--- @see connect_padding To know how components have paddings. +--- @see connect_separator To know how component and separator connected. +function Yatline.line.get:tabs(side) + side = side or "left" + + local tabs = #cx.tabs + local lines = {} + + local in_side + if side == "left" then + in_side = Side.LEFT + else + in_side = Side.RIGHT + end + + for i = 1, tabs do + local text = tostring(i) + if Yatline.config.tab_width > 2 then + text = ui.truncate(text .. " " .. cx.tabs[i].name, { max = Yatline.config.tab_width }) + end + + local separator_style = { bg = nil, fg = nil } + if i == cx.tabs.idx then + local tab = connect_padding(text, ComponentType.A, in_side) + set_mode_style(cx.tabs[i].mode) + set_component_style(tab, ComponentType.A) + + if Yatline.config.style_a.bg ~= "reset" or Yatline.config.show_background then + separator_style.fg = Yatline.config.style_a.bg + if Yatline.config.show_background then + separator_style.bg = Yatline.config.style_c.bg + end + + lines[#lines + 1] = connect_separator(tab, in_side, SeparatorType.OUTER, separator_style) + else + separator_style.fg = Yatline.config.style_a.fg + + lines[#lines + 1] = connect_separator(tab, in_side, SeparatorType.INNER, separator_style) + end + else + local tab = ui.Span(text) + local inner = ui.Span(string.rep(" ", Yatline.config.padding.inner)) + local outer = ui.Span(string.rep(" ", Yatline.config.padding.outer)) + + if Yatline.config.show_background then + set_component_style(inner, ComponentType.C) + set_component_style(outer, ComponentType.C) + set_component_style(tab, ComponentType.C) + else + apply_style_table(tab, { fg = Yatline.config.style_c.fg }) + end + + if in_side == Side.LEFT then + tab = ui.Line({ outer, tab, inner }) + else + tab = ui.Line({ inner, tab, outer }) + end + + if i == cx.tabs.idx - 1 then + set_mode_style(cx.tabs[i + 1].mode) + + local open, close + if + Yatline.config.style_a.bg ~= "reset" + or (Yatline.config.show_background and Yatline.config.style_c.bg ~= "reset") + then + if + not Yatline.config.show_background + or (Yatline.config.show_background and Yatline.config.style_c.bg == "reset") + then + separator_style.fg = Yatline.config.style_a.bg + if Yatline.config.show_background then + separator_style.bg = Yatline.config.style_c.bg + end + + open = ui.Span(Yatline.config.inverse_separator.open) + close = ui.Span(Yatline.config.inverse_separator.close) + else + separator_style.bg = Yatline.config.style_a.bg + if Yatline.config.show_background then + separator_style.fg = Yatline.config.style_c.bg + end + + open = ui.Span(Yatline.config.section_separator.open) + close = ui.Span(Yatline.config.section_separator.close) + end + else + separator_style.fg = Yatline.config.style_c.fg + + open = ui.Span(Yatline.config.part_separator.open) + close = ui.Span(Yatline.config.part_separator.close) + end + + apply_style_table(open, separator_style) + apply_style_table(close, separator_style) + + if in_side == Side.LEFT then + lines[#lines + 1] = ui.Line({ tab, close }) + else + lines[#lines + 1] = ui.Line({ open, tab }) + end + else + separator_style.fg = Yatline.config.style_c.fg + if Yatline.config.show_background then + separator_style.bg = Yatline.config.style_c.bg + end + + lines[#lines + 1] = connect_separator(tab, in_side, SeparatorType.INNER, separator_style) + end + end + end + + if in_side == Side.RIGHT then + return ui.Line(reverse_order(lines)) + else + return ui.Line(lines) + end +end + +--==========================-- +-- Component Coloreds Group -- +--==========================-- + +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. +--- Unix-like systems only. +--- @return Coloreds? coloreds Current active tab's hovered file's permissions +function Yatline.coloreds.get:permissions() + local hovered = cx.active.current.hovered + + if hovered then + local perm = hovered.cha:perm() + + if perm then + local coloreds = {} + + for i = 1, #perm do + local c = perm:sub(i, i) + + local fg = Yatline.config.permissions_t_fg + if c == "-" then + fg = Yatline.config.permissions_s_fg + elseif c == "r" then + fg = Yatline.config.permissions_r_fg + elseif c == "w" then + fg = Yatline.config.permissions_w_fg + elseif c == "x" or c == "s" or c == "S" or c == "t" or c == "T" then + fg = Yatline.config.permissions_x_fg + end + + table.insert(coloreds, { c, fg }) + end + + return coloreds + else + return nil + end + else + return nil + end +end + +--- Gets the number of selected and yanked files and also number of files or filtered files of the active tab. +--- @param filter? boolean Whether or not number of files (or filtered files) will be shown. +--- @param zero_check? boolean Whether or not counts will be shown if count is zero. +--- @return Coloreds? coloreds Active tab's number of selected and yanked files and also number of files or filtered files +function Yatline.coloreds.get:count(filter, zero_check) + filter = filter or false + zero_check = zero_check or false + + local num_yanked = #cx.yanked + local num_selected = #cx.active.selected + local num_files = #cx.active.current.files + + local coloreds = {} + + if filter then + local files_count_fg, files_count_icon + if cx.active.current.files.filter or cx.active.current.cwd.is_search then + files_count_fg = Yatline.config.filtereds.fg + files_count_icon = Yatline.config.filtereds.icon + else + files_count_fg = Yatline.config.files.fg + files_count_icon = Yatline.config.files.icon + end + + if (zero_check and num_files > 0) or not zero_check then + table.insert(coloreds, { string.format("%s %d", files_count_icon, num_files), files_count_fg }) + end + end + + if (zero_check and num_selected > 0) or not zero_check then + if #coloreds > 0 then + table.insert(coloreds, { " ", Yatline.config.selected.fg }) + end + + table.insert( + coloreds, + { string.format("%s %d", Yatline.config.selected.icon, num_selected), Yatline.config.selected.fg } + ) + end + + if (zero_check and num_yanked > 0) or not zero_check then + local yanked_fg, yanked_icon + if cx.yanked.is_cut then + yanked_fg = Yatline.config.cut.fg + yanked_icon = Yatline.config.cut.icon + else + yanked_fg = Yatline.config.copied.fg + yanked_icon = Yatline.config.copied.icon + end + + if #coloreds > 0 then + table.insert(coloreds, { " ", yanked_fg }) + end + + table.insert(coloreds, { string.format("%s %d", yanked_icon, num_yanked), yanked_fg }) + end + + if #coloreds > 0 then + return coloreds + else + return nil + end +end + +--- Gets the number of task states. +--- @param zero_check? boolean Whether or not counts will be shown if count is zero. +--- @return Coloreds? coloreds Number of task states. +function Yatline.coloreds.get:task_states(zero_check) + zero_check = zero_check or false + + local summary = cx.tasks.summary + local coloreds = {} + + if (zero_check and summary.total > 0) or not zero_check then + table.insert( + coloreds, + { string.format("%s %d", Yatline.config.total.icon, summary.total), Yatline.config.total.fg } + ) + end + + if (zero_check and summary.success > 0) or not zero_check then + if #coloreds > 0 then + table.insert(coloreds, { " ", Yatline.config.success.fg }) + end + + table.insert( + coloreds, + { string.format("%s %d", Yatline.config.success.icon, summary.success), Yatline.config.success.fg } + ) + end + + if (zero_check and summary.failed > 0) or not zero_check then + if #coloreds > 0 then + table.insert(coloreds, { " ", Yatline.config.failed.fg }) + end + + table.insert( + coloreds, + { string.format("%s %d", Yatline.config.failed.icon, summary.failed), Yatline.config.failed.fg } + ) + end + + if #coloreds > 0 then + return coloreds + else + return nil + end +end + +--- Gets colored which contains string based component's string and desired foreground color. +--- @param component_name string String based component's name. +--- @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 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(Yatline.string.get, table.unpack(params)) + else + output = getter() + end + + if output ~= nil and output ~= "" then + return { { output, fg } } + end + end + + return nil +end + +--===============-- +-- Configuration -- +--===============-- + +--- Configure separators if it is need to be added to the components. +--- Connects them with each component. +--- @param section_components [Line, boolean][] 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_padding To know how components have paddings. +--- @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 -- Does component have separator? + local separator_style = { bg = nil, fg = nil } + + local separator_type + if i ~= num_section_components then -- Does component is not at the end of the section? + separator_type = SeparatorType.INNER + + if component_type == ComponentType.A then + separator_style = Yatline.config.style_a + elseif component_type == ComponentType.B then + separator_style = Yatline.config.style_b + else + separator_style = Yatline.config.style_c + end + else -- Does component is at the end of the section? + separator_type = SeparatorType.OUTER + + if component_type == ComponentType.A then + separator_style.fg = Yatline.config.style_a.bg + elseif component_type == ComponentType.B then + separator_style.fg = Yatline.config.style_b.bg + else + separator_style.fg = Yatline.config.style_c.bg + end + + if component_type == ComponentType.A and num_section_b_components ~= 0 then + separator_style.bg = Yatline.config.style_b.bg + elseif num_section_c_components == 0 or component_type == ComponentType.C then + if Yatline.config.show_background then + separator_style.bg = Yatline.config.style_c.bg + end + else + separator_style.bg = Yatline.config.style_c.bg + end + end + + component[1] = connect_padding(component[1], component_type, in_side) + section_line_components[i] = connect_separator(component[1], in_side, separator_type, separator_style) + else + section_line_components[i] = component[1] + end + end + + return section_line_components +end + +--- Creates configured section according to its components' config. +--- @param section ComponentConfig[] Array of components' config in a section. +--- @param component_type ComponentType Which section that components will be. +--- @return [Line, boolean][] section_components Configured components array whose components are in section. +local function config_section(section, component_type) + --- @type [Line, boolean][] + local section_components = {} + + for _, component in ipairs(section) do + local component_group = Yatline[component.type] + + if component_group then -- Does component group exist? + if component.custom then -- Does component is custom? + if component.name ~= nil and component.name ~= "" and #component.name ~= 0 then -- Does component name is valid? + section_components[#section_components + 1] = -- Insert component to the table. + { component_group.create(component.name, component_type), component_group.has_separator } + end + else + local getter = component_group.get[component.name] -- Get component function that will be called. + + if getter then -- Does function exist? + local output -- Output of the function. + if component.params then -- Does component function has parameters? + output = getter(component_group.get, table.unpack(component.params)) + else + output = getter() + end + + if output ~= nil and output ~= "" then -- Does component is not empty? + section_components[#section_components + 1] = -- Insert component to the table. + { component_group.create(output, component_type), component_group.has_separator } + end + end + end + end + end + + return section_components +end + +--- Automatically creates and configures either header-line or status-line. +--- @param side SideConfig Configuration of either left or right side. +--- @param in_side Side Which side components will be. +--- @return Line left_line Consist of components that are in left side of the line. +--- @return Line right_line Consist of components that are in right side of the line. +--- @see config_section To know how components are gotten from sections' config. +--- @see config_components_separators To know how components are connected with separators. +local function config_line(side, in_side) + -- Configures components of sections. + local section_a_components = config_section(side.section_a, ComponentType.A) + local section_b_components = config_section(side.section_b, ComponentType.B) + local section_c_components = config_section(side.section_c, ComponentType.C) + + local num_section_b_components = #section_b_components + local num_section_c_components = #section_c_components + + -- Connects components of section by separators. + 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 + ) + + if in_side == Side.RIGHT then -- Reverse the order of the components if it is in the right side. + section_a_line_components = reverse_order(section_a_line_components) + section_b_line_components = reverse_order(section_b_line_components) + section_c_line_components = reverse_order(section_c_line_components) + end + + -- Combines components of section into single components. + local section_a_line = ui.Line(section_a_line_components) + local section_b_line = ui.Line(section_b_line_components) + local section_c_line = ui.Line(section_c_line_components) + + if in_side == Side.LEFT then + return ui.Line({ section_a_line, section_b_line, section_c_line }) + else + return ui.Line({ section_c_line, section_b_line, section_a_line }) + end +end + +--- Checks if either header-line or status-line contains components. +--- @param line LineConfig Configuration of either header-line or status-line. +--- @return boolean show_line Returns yes if it contains components, otherwise returns no. +local function show_line(line) + for _, side in pairs(line) do + for _, section in pairs(side) do + if #section ~= 0 then + return true + end + end + end + + return false +end + +--- Creates and configures paragraph which is used as left or right of either +--- header-line or status-line. +--- @param area Rect The area where paragraph will be placed in. +--- @param line? Line The line which used in paragraph. It is optional. +--- @return Paragraph paragraph Configured parapgraph. +local function config_paragraph(area, line) + local line_array = { line } or {} + if Yatline.config.show_background then + return apply_style_table(ui.Text(line_array):area(area), Yatline.config.style_c) + else + return ui.Text(line_array):area(area) + end +end + +return { + setup = function(_, config, pre_theme) + if config then + -- Fills the sections that are not given if the line exists. + for _, line in ipairs({ "header_line", "status_line" }) do + if config[line] then + for _, side in ipairs({ "left", "right" }) do + if config[line][side] then + for _, section in ipairs({ "section_a", "section_b", "section_c" }) do + config[line][side][section] = config[line][side][section] or {} + end + else + config[line][side] = {} + for _, section in ipairs({ "section_a", "section_b", "section_c" }) do + config[line][side][section] = {} + end + end + end + end + end + + -- Get the current theme according to the light/dark mode. (default: config.theme) + config.theme = (not rt.term.light and config.theme_dark) + or (rt.term.light and config.theme_light) + or config.theme + + -- Extracts theme fields to the config unless that fields does not exists. + if config.theme then + for key, value in pairs(config.theme) do + if not config[key] then + config[key] = value + end + end + end + + -- Extracts config fields to the YatlineConfig if that fields exists. + for key, value in pairs(config) do + if Yatline.config[key] then + Yatline.config[key] = value + end + end + end + + -- Extracts pre_theme fields to the YatlineConfig if that fields exists. + if pre_theme then + for key, value in pairs(pre_theme) do + if Yatline.config[key] then + Yatline.config[key] = value + end + end + end + + if Yatline.config.display_header_line then -- Controls displaying header-line. + if show_line(Yatline.config.header_line) then -- Controls recoding of header-line. + -- Empties default Yazi header-line. + Header._left = {} + Header._right = {} + + Header.redraw = function(self) + -- Gets Yazi components. + local right = self:children_redraw(self.RIGHT) + self._right_width = right:width() + local left = self:children_redraw(self.LEFT) + + -- Gets Yatline components. + local left_line = config_line(Yatline.config.header_line.left, Side.LEFT) + local right_line = config_line(Yatline.config.header_line.right, Side.RIGHT) + + return { + config_paragraph(self._area, ui.Line({ left_line, left })), -- Styles left_line if show_background set. + ui.Line({ right, right_line }):area(self._area):align(ui.Align.RIGHT), + } + end + end + else + Header.redraw = function() + return {} + end + end + + if Yatline.config.display_status_line then -- Controls displaying status-line. + if show_line(Yatline.config.status_line) then -- Controls recoding of status-line. + -- Empties default Yazi status-line. + Status._left = {} + Status._right = {} + + Status.redraw = function(self) + -- Gets Yazi components. + local left = self:children_redraw(self.LEFT) + local right = self:children_redraw(self.RIGHT) + + -- Gets Yatline components. + local left_line = config_line(Yatline.config.status_line.left, Side.LEFT) + local right_line = config_line(Yatline.config.status_line.right, Side.RIGHT) + + local sum_right = ui.Line({ right, right_line }) -- Needed for error prevention. + return { + config_paragraph(self._area, ui.Line({ left_line, left })), -- Styles left_line if show_background set. + sum_right:area(self._area):align(ui.Align.RIGHT), + table.unpack(ui.redraw(Progress:new(self._area, sum_right:width()))), -- Inserts Progress bar. + } + end + end + else + Status.redraw = function() + return {} + end + end + + Root.layout = function(self) + local constraints = {} + -- Sets Yazi layout according to the given positions. + for _, component in ipairs(Yatline.config.component_positions) do + if + (component == "header" and Yatline.config.display_header_line) + or (component == "status" and Yatline.config.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 = {} + -- Fills the layout according to the given positions. + local i = 1 + for _, component in ipairs(Yatline.config.component_positions) do + if component == "header" and Yatline.config.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 Yatline.config.display_status_line then + table.insert(childrens, Status:new(self._chunks[i], cx.active)) + i = i + 1 + end + end + + table.insert(childrens, Modal:new(self._area)) + + self._children = childrens + end + end, +} |
