better structure

This commit is contained in:
2025-10-19 00:14:19 +02:00
parent 057afc086e
commit 8733656ed9
630 changed files with 81 additions and 137 deletions

View File

@@ -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.

View File

@@ -0,0 +1,173 @@
<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)
- `<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.
---
## 📣 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.
---

View File

@@ -0,0 +1,496 @@
-- Check for windows
local is_windows = ya.target_family() == "windows"
-- Define flags and strings
local is_password, is_encrypted, is_level, cmd_password, cmd_level, default_extension = false, false, false, "", "", "zip"
-- 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_error(message, urgency)
ya.notify(
{
title = "Archive",
content = message,
level = urgency,
timeout = 5
}
)
end
-- Function to check if command is available
local function is_command_available(cmd)
local stat_cmd
if is_windows then
stat_cmd = string.format("where %s > nul 2>&1", cmd)
else
stat_cmd = string.format("command -v %s >/dev/null 2>&1", cmd)
end
local cmd_exists = os.execute(stat_cmd)
if cmd_exists then
return true
else
return false
end
end
-- Function to change command arrays --> string -- Use first command available or first command
local function find_command_name(cmd_list)
for _, cmd in ipairs(cmd_list) do
if is_command_available(cmd) then
return cmd
end
end
return cmd_list[1] -- Return first command as fallback
end
-- Function to append filename to it's parent directory url
local function combine_url(path, file)
path, file = Url(path), Url(file)
return tostring(path:join(file))
end
-- Function to make a table of selected or hovered files: path = filenames
local selected_or_hovered =
ya.sync(
function()
local tab, paths, names, path_fnames = cx.active, {}, {}, {}
for _, u in pairs(tab.selected) do
paths[#paths + 1] = tostring(u.parent)
names[#names + 1] = tostring(u.name)
end
if #paths == 0 and tab.current.hovered then
paths[1] = tostring(tab.current.hovered.url.parent)
names[1] = tostring(tab.current.hovered.name)
end
for idx, name in ipairs(names) do
if not path_fnames[paths[idx]] then
path_fnames[paths[idx]] = {}
end
table.insert(path_fnames[paths[idx]], name)
end
return path_fnames, names, tostring(tab.current.cwd)
end
)
-- Table of archive commands
local archive_commands = {
["%.zip$"] = {
{command = "zip", args = {"-r"}, level_arg = "-", level_min = 0, level_max = 9, passwordable = true},
{
command = {"7z", "7zz", "7za"},
args = {"a", "-tzip"},
level_arg = "-mx=",
level_min = 0,
level_max = 9,
passwordable = true
},
{
command = {"tar", "bsdtar"},
args = {"-caf"},
level_arg = {"--option", "compression-level="},
level_min = 1,
level_max = 9
}
},
["%.7z$"] = {
{
command = {"7z", "7zz", "7za"},
args = {"a"},
level_arg = "-mx=",
level_min = 0,
level_max = 9,
header_arg = "-mhe=on",
passwordable = true
}
},
["%.rar$"] = {
{
command = "rar",
args = {"a"},
level_arg = "-m",
level_min = 0,
level_max = 5,
header_arg = "-hp",
passwordable = true
}
},
["%.tar.gz$"] = {
{command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "gzip"},
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-mx=",
level_min = 1,
level_max = 9,
compress = "7z",
compress_args = {"a", "-tgzip"}
},
{
command = {"tar", "bsdtar"},
args = {"-czf"},
level_arg = {"--option", "gzip:compression-level="},
level_min = 1,
level_max = 9
}
},
["%.tar.xz$"] = {
{command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "xz"},
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-mx=",
level_min = 1,
level_max = 9,
compress = "7z",
compress_args = {"a", "-txz"}
},
{
command = {"tar", "bsdtar"},
args = {"-cJf"},
level_arg = {"--option", "xz:compression-level="},
level_min = 1,
level_max = 9
}
},
["%.tar.bz2$"] = {
{command = {"tar", "bsdtar"}, args = {"rpf"}, level_arg = "-", level_min = 1, level_max = 9, compress = "bzip2"},
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-mx=",
level_min = 1,
level_max = 9,
compress = "7z",
compress_args = {"a", "-tbzip2"}
},
{
command = {"tar", "bsdtar"},
args = {"-cjf"},
level_arg = {"--option", "bzip2:compression-level="},
level_min = 1,
level_max = 9
}
},
["%.tar.zst$"] = {
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-",
level_min = 1,
level_max = 22,
compress = "zstd",
compress_args = {"--ultra"}
}
},
["%.tar.lz4$"] = {
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-",
level_min = 1,
level_max = 12,
compress = "lz4"
}
},
["%.tar.lha$"] = {
{
command = {"tar", "bsdtar"},
args = {"rpf"},
level_arg = "-o",
level_min = 5,
level_max = 7,
compress = "lha",
compress_args = {"-a"}
}
},
["%.tar$"] = {
{command = {"tar", "bsdtar"}, args = {"rpf"}}
}
}
return {
entry = function(_, job)
-- Parse flags and default extension
if job.args ~= nil then
for _, arg in ipairs(job.args) do
if arg:match("^%-(%w+)$") then
-- Handle combined flags (e.g., -phl)
for flag in arg:sub(2):gmatch(".") do
if flag == "p" then
is_password = true
elseif flag == "h" then
is_encrypted = true
elseif flag == "l" then
is_level = true
end
end
elseif arg:match("^%w[%w\\.]*$") then
-- Handle default extension (e.g., 7z, zip)
if archive_commands["%." .. arg .. "$"] then
default_extension = arg
else
notify_error(string.format("Unsupported extension: %s", arg), "warn")
end
else
notify_error(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:",
position = {"top-center", y = 3, w = 40}
}
)
if event ~= 1 then
return
end
-- Determine the default name for the archive
local default_name = #fnames == 1 and fnames[1] or Url(output_dir).name
output_name = output_name == "" and string.format("%s.%s", default_name, default_extension) or output_name
-- 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_error("Invalid archive filename", "error")
return
end
-- Match user input to archive command
local archive_cmd,
archive_args,
archive_compress,
archive_level_arg,
archive_level_min,
archive_level_max,
archive_header_arg,
archive_passwordable,
archive_compress_args
local matched_pattern = false
for pattern, cmd_list in pairs(archive_commands) do
if output_name:match(pattern) then
matched_pattern = true -- Mark that file extension is correct
for _, cmd in ipairs(cmd_list) do
-- Check if archive_cmd is available
local find_command = type(cmd.command) == "table" and find_command_name(cmd.command) or cmd.command
if is_command_available(find_command) then
-- Check if compress_cmd (if listed) is available
if cmd.compress == nil or is_command_available(cmd.compress) then
archive_cmd = find_command
archive_args = cmd.args
archive_compress = cmd.compress or ""
archive_level_arg = is_level and cmd.level_arg or ""
archive_level_min = cmd.level_min
archive_level_max = cmd.level_max
archive_header_arg = is_encrypted and cmd.header_arg or ""
archive_passwordable = cmd.passwordable or false
archive_compress_args = cmd.compress_args or {}
break
end
end
end
if archive_cmd then
break
end
end
end
-- Check if no archive command is available for the extension
if not matched_pattern then
notify_error("Unsupported file extension", "error")
return
end
-- Check if no suitable archive program was found
if not archive_cmd then
notify_error("Could not find a suitable archive program for the selected file extension", "error")
return
end
-- Check if archive command has multiple names
if type(archive_cmd) == "table" then
archive_cmd = find_command_name(archive_cmd)
end
-- Exit if archive command is not available
if not is_command_available(archive_cmd) then
notify_error(string.format("%s not available", archive_cmd), "error")
return
end
-- Exit if compress command is not available
if archive_compress ~= "" and not is_command_available(archive_compress) then
notify_error(string.format("%s compression not available", archive_compress), "error")
return
end
-- Add password arg if selected
if archive_passwordable and is_password then
local output_password, event =
ya.input(
{
title = "Enter password:",
obscure = true,
position = {"top-center", y = 3, w = 40}
}
)
if event ~= 1 then
return
end
if output_password ~= "" then
cmd_password = "-P" .. output_password
if archive_cmd == "rar" and is_encrypted then
cmd_password = archive_header_arg .. output_password -- Add archive arg for rar
end
table.insert(archive_args, cmd_password)
end
end
-- Add header arg if selected for 7z
if is_encrypted and archive_header_arg ~= "" and archive_cmd ~= "rar" then
table.insert(archive_args, archive_header_arg)
end
-- Add level arg if selected
if archive_level_arg ~= "" and is_level then
local output_level, event =
ya.input(
{
title = string.format("Enter compression level (%s - %s)", archive_level_min, archive_level_max),
position = {"top-center", y = 3, w = 40}
}
)
if event ~= 1 then
return
end
-- Validate user input for compression level
if
output_level ~= "" and tonumber(output_level) ~= nil and tonumber(output_level) >= archive_level_min and
tonumber(output_level) <= archive_level_max
then
cmd_level =
type(archive_level_arg) == "table" and archive_level_arg[#archive_level_arg] .. output_level or
archive_level_arg .. output_level
local target_args = archive_compress == "" and archive_args or archive_compress_args
if type(archive_level_arg) == "table" then
-- Insert each element of archive_level_arg (except last) into target_args at the correct position
for i = 1, #archive_level_arg - 1 do
table.insert(target_args, i, archive_level_arg[i])
end
table.insert(target_args, #archive_level_arg, cmd_level) -- Add level at the end
else
-- Insert the compression level argument at the start if not a table
table.insert(target_args, 1, cmd_level)
end
else
notify_error("Invalid level specified. Using defaults.", "warn")
end
end
-- Store the original output name for later use
local original_name = output_name
-- If compression is needed, adjust the output name to exclude extensions like ".tar"
if archive_compress ~= "" then
output_name = output_name:match("(.*%.tar)") or output_name
end
-- Create a temporary directory for intermediate files
local temp_dir_name = ".tmp_compress"
local temp_dir = combine_url(output_dir, temp_dir_name)
local temp_dir, _ = tostring(fs.unique_name(Url(temp_dir)))
-- Attempt to create the temporary directory
local temp_dir_status, temp_dir_err = fs.create("dir_all", Url(temp_dir))
if not temp_dir_status then
-- Notify the user if the temporary directory creation fails
notify_error(string.format("Failed to create temp directory, error code: %s", temp_dir_err), "error")
return
end
-- Define the temporary output file path within the temporary directory
local temp_output_url = combine_url(temp_dir, output_name)
-- Add files to the output archive
for filepath, filenames in pairs(path_fnames) do
-- Execute the archive command for each path and its respective files
local archive_status, archive_err =
Command(archive_cmd):arg(archive_args):arg(temp_output_url):arg(filenames):cwd(filepath):spawn():wait()
if not archive_status or not archive_status.success then
-- Notify the user if the archiving process fails and clean up the temporary directory
notify_error(string.format("Failed to create archive %s with '%s', error: %s", output_name, archive_cmd, archive_err), "error")
local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
if not cleanup_status then
notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
end
return
end
end
-- If compression is required, execute the compression command
if archive_compress ~= "" then
local compress_status, compress_err =
Command(archive_compress):arg(archive_compress_args):arg(temp_output_url):spawn():wait()
if not compress_status or not compress_status.success then
-- Notify the user if the compression process fails and clean up the temporary directory
notify_error(string.format("Failed to compress archive %s with '%s', error: %s", output_name, archive_compress, compress_err), "error")
local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
if not cleanup_status then
notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
end
return
end
end
-- Move the final file from the temporary directory to the output directory
local final_output_url, temp_url_processed = combine_url(output_dir, original_name), combine_url(temp_dir, original_name)
final_output_url, _ = tostring(fs.unique_name(Url(final_output_url)))
local move_status, move_err = os.rename(temp_url_processed, final_output_url)
if not move_status then
-- Notify the user if the move operation fails and clean up the temporary directory
notify_error(string.format("Failed to move %s to %s, error: %s", temp_url_processed, final_output_url, move_err), "error")
local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
if not cleanup_status then
notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
end
return
end
-- Cleanup the temporary directory after successful operation
local cleanup_status, cleanup_err = fs.remove("dir_all", Url(temp_dir))
if not cleanup_status then
notify_error(string.format("Failed to clean up temporary directory %s, error: %s", temp_dir, cleanup_err), "error")
end
end
}

View File

@@ -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.

View File

@@ -0,0 +1,78 @@
# 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()
```
And register it as fetchers in your `~/.config/yazi/yazi.toml`:
```toml
[[plugin.prepend_fetchers]]
id = "git"
name = "*"
run = "git"
[[plugin.prepend_fetchers]]
id = "git"
name = "*/"
run = "git"
```
## Advanced
> [!NOTE]
> 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:
- `th.git.modified`
- `th.git.added`
- `th.git.untracked`
- `th.git.ignored`
- `th.git.deleted`
- `th.git.updated`
For example:
```lua
-- ~/.config/yazi/init.lua
th.git = th.git or {}
th.git.modified = ui.Style():fg("blue")
th.git.deleted = ui.Style():fg("red"):bold()
```
You can also customize the text of the status sign with:
- `th.git.modified_sign`
- `th.git.added_sign`
- `th.git.untracked_sign`
- `th.git.ignored_sign`
- `th.git.deleted_sign`
- `th.git.updated_sign`
For example:
```lua
-- ~/.config/yazi/init.lua
th.git = th.git or {}
th.git.modified_sign = "M"
th.git.deleted_sign = "D"
```
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.

View File

@@ -0,0 +1,261 @@
--- @since 25.5.31
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 = {
excluded = 100, -- ignored directory
ignored = 6, -- ignored file
untracked = 5,
modified = 4,
added = 3,
deleted = 2,
updated = 1,
unknown = 0,
}
local PATTERNS = {
{ "!$", CODES.ignored },
{ "?$", CODES.untracked },
{ "[MT]", CODES.modified },
{ "[AC]", CODES.added },
{ "D", CODES.deleted },
{ "U", CODES.updated },
{ "[AD][AD]", CODES.updated },
}
---@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.unknown) > 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.unknown then
st.repos[repo][path] = nil
elseif code == CODES.excluded then
-- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering
st.dirs[path] = CODES.excluded
else
st.repos[repo][path] = code
end
end
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
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
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
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.ignored] = t.ignored and ui.Style(t.ignored) or ui.Style():fg("darkgray"),
[CODES.untracked] = t.untracked and ui.Style(t.untracked) or ui.Style():fg("magenta"),
[CODES.modified] = t.modified and ui.Style(t.modified) or ui.Style():fg("yellow"),
[CODES.added] = t.added and ui.Style(t.added) or ui.Style():fg("green"),
[CODES.deleted] = t.deleted and ui.Style(t.deleted) or ui.Style():fg("red"),
[CODES.updated] = t.updated and ui.Style(t.updated) or ui.Style():fg("yellow"),
}
local signs = {
[CODES.ignored] = t.ignored_sign or "",
[CODES.untracked] = t.untracked_sign or "?",
[CODES.modified] = t.modified_sign or "",
[CODES.added] = t.added_sign or "",
[CODES.deleted] = t.deleted_sign or "",
[CODES.updated] = t.updated_sign or "",
}
Linemode:children_add(function(self)
local url = self._file.url
local repo = st.dirs[tostring(url.base)]
local code
if repo then
code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)]
end
if not code or 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
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)
:stdout(Command.PIPED)
: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 `unknown`,
-- so that cleaning up outdated statuses from `st.repos`
for _, path in ipairs(paths) do
local s = path:sub(#repo + 2)
changed[s] = changed[s] or CODES.unknown
end
add(tostring(cwd), repo, changed)
return false
end
return { setup = setup, fetch = fetch }

View File

@@ -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.

View File

@@ -0,0 +1,40 @@
# smart-enter.yazi
[`Open`][open] files or [`enter`][enter] directories all in one key!
## Installation
```sh
ya pkg add yazi-rs/plugins:smart-enter
```
## Usage
Bind your <kbd>l</kbd> key to the plugin, in your `~/.config/yazi/keymap.toml`:
```toml
[[mgr.prepend_keymap]]
on = "l"
run = "plugin smart-enter"
desc = "Enter the child directory, or open the file"
```
## Advanced
By default, `--hovered` is passed to the [`open`][open] command, make the behavior consistent with [`enter`][enter] avoiding accidental triggers,
which means both will only target the currently hovered file.
If you still want `open` to target multiple selected files, add this to your `~/.config/yazi/init.lua`:
```lua
require("smart-enter"):setup {
open_multi = true,
}
```
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.
[open]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.open
[enter]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.enter

View File

@@ -0,0 +1,11 @@
--- @since 25.5.31
--- @sync entry
local function setup(self, opts) self.open_multi = opts.open_multi end
local function entry(self)
local h = cx.active.current.hovered
ya.emit(h and h.cha.is_dir and "enter" or "open", { hovered = not self.open_multi })
end
return { entry = entry, setup = setup }

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Hunter Hwang
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.

View File

@@ -0,0 +1,112 @@
# Yet another bookmarks
A [Yazi](https://github.com/sxyazi/yazi) plugin for bookmark management, supporting the following features
- Persistent bookmarks. No bookmarks are lost after you close yazi.
- Quickly jump, delete, and rename a bookmark by keymap.
- Support fuzzy search through [fzf](https://github.com/junegunn/fzf).
- Configure your bookmarks using Lua language.
## Installation
> [!NOTE]
> Yazi >= 0.25.
```sh
# Linux/macOS
git clone https://github.com/h-hg/yamb.yazi.git ~/.config/yazi/plugins/yamb.yazi
# Windows
git clone https://github.com/h-hg/yamb.yazi.git $env:APPDATA\yazi\config\plugins\yamb.yazi
# if you are using Yazi version >= 3.0
ya pack -a h-hg/yamb
```
## Usage
Add this to your `init.lua`
```lua
-- You can configure your bookmarks by lua language
local bookmarks = {}
local path_sep = package.config:sub(1, 1)
local home_path = ya.target_family() == "windows" and os.getenv("USERPROFILE") or os.getenv("HOME")
if ya.target_family() == "windows" then
table.insert(bookmarks, {
tag = "Scoop Local",
path = (os.getenv("SCOOP") or home_path .. "\\scoop") .. "\\",
key = "p"
})
table.insert(bookmarks, {
tag = "Scoop Global",
path = (os.getenv("SCOOP_GLOBAL") or "C:\\ProgramData\\scoop") .. "\\",
key = "P"
})
end
table.insert(bookmarks, {
tag = "Desktop",
path = home_path .. path_sep .. "Desktop" .. path_sep,
key = "d"
})
require("yamb"):setup {
-- Optional, the path ending with path seperator represents folder.
bookmarks = bookmarks,
-- Optional, recieve notification everytime you jump.
jump_notify = true,
-- Optional, the cli of fzf.
cli = "fzf",
-- Optional, a string used for randomly generating keys, where the preceding characters have higher priority.
keys = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
-- Optional, the path of bookmarks
path = (ya.target_family() == "windows" and os.getenv("APPDATA") .. "\\yazi\\config\\bookmark") or
(os.getenv("HOME") .. "/.config/yazi/bookmark"),
}
```
Add this to your `keymap.toml`:
```toml
[[manager.prepend_keymap]]
on = [ "u", "a" ]
run = "plugin yamb save"
desc = "Add bookmark"
[[manager.prepend_keymap]]
on = [ "u", "g" ]
run = "plugin yamb jump_by_key"
desc = "Jump bookmark by key"
[[manager.prepend_keymap]]
on = [ "u", "G" ]
run = "plugin yamb jump_by_fzf"
desc = "Jump bookmark by fzf"
[[manager.prepend_keymap]]
on = [ "u", "d" ]
run = "plugin yamb delete_by_key"
desc = "Delete bookmark by key"
[[manager.prepend_keymap]]
on = [ "u", "D" ]
run = "plugin yamb delete_by_fzf"
desc = "Delete bookmark by fzf"
[[manager.prepend_keymap]]
on = [ "u", "A" ]
run = "plugin yamb delete_all"
desc = "Delete all bookmarks"
[[manager.prepend_keymap]]
on = [ "u", "r" ]
run = "plugin yamb rename_by_key"
desc = "Rename bookmark by key"
[[manager.prepend_keymap]]
on = [ "u", "R" ]
run = "plugin yamb rename_by_fzf"
desc = "Rename bookmark by fzf"
```

View File

@@ -0,0 +1,355 @@
local path_sep = package.config:sub(1, 1)
local get_hovered_path = ya.sync(function(state)
local h = cx.active.current.hovered
if h then
local path = tostring(h.url)
if h.cha.is_dir then
return path .. path_sep
end
return path
else
return ''
end
end)
local get_state_attr = ya.sync(function(state, attr)
return state[attr]
end)
local set_state_attr = ya.sync(function(state, attr, value)
state[attr] = value
end)
local set_bookmarks = ya.sync(function(state, path, value)
state.bookmarks[path] = value
end)
local sort_bookmarks = function(bookmarks, key1, key2, reverse)
reverse = reverse or false
table.sort(bookmarks, function(x, y)
if x[key1] == nil and y[key1] == nil then
return x[key2] < y[key2]
elseif x[key1] == nil then
return false
elseif y[key1] == nil then
return true
else
return x[key1] < y[key1]
end
end)
if reverse then
local n = #bookmarks
for i = 1, math.floor(n / 2) do
bookmarks[i], bookmarks[n - i + 1] = bookmarks[n - i + 1], bookmarks[i]
end
end
return bookmarks
end
local save_to_file = function(mb_path, bookmarks)
local file = io.open(mb_path, "w")
if file == nil then
return
end
local array = {}
for _, item in pairs(bookmarks) do
table.insert(array, item)
end
sort_bookmarks(array, "tag", "key", true)
for _, item in ipairs(array) do
file:write(string.format("%s\t%s\t%s\n", item.tag, item.path, item.key))
end
file:close()
end
local fzf_find = function(cli, mb_path)
local permit = ya.hide()
local cmd = string.format("%s < \"%s\"", cli, mb_path)
local handle = io.popen(cmd, "r")
local result = ""
if handle then
-- strip
result = string.gsub(handle:read("*all") or "", "^%s*(.-)%s*$", "%1")
handle:close()
end
permit:drop()
local tag, path, key = string.match(result or "", "(.-)\t(.-)\t(.*)")
return path
end
local which_find = function(bookmarks)
local cands = {}
for path, item in pairs(bookmarks) do
if #item.tag ~= 0 then
table.insert(cands, { desc = item.tag, on = item.key, path = item.path })
end
end
sort_bookmarks(cands, "on", "desc", false)
if #cands == 0 then
ya.notify {
title = "Bookmarks",
content = "Empty bookmarks",
timeout = 2,
level = "info",
}
return nil
end
local idx = ya.which { cands = cands }
if idx == nil then
return nil
end
return cands[idx].path
end
local action_jump = function(bookmarks, path, jump_notify)
if path == nil then
return
end
local tag = bookmarks[path].tag
if string.sub(path, -1) == path_sep then
ya.manager_emit("cd", { path })
else
ya.manager_emit("reveal", { path })
end
if jump_notify then
ya.notify {
title = "Bookmarks",
content = 'Jump to "' .. tag .. '"',
timeout = 2,
level = "info",
}
end
end
local generate_key = function(bookmarks)
local keys = get_state_attr("keys")
local key2rank = get_state_attr("key2rank")
local mb = {}
for _, item in pairs(bookmarks) do
if #item.key == 1 then
table.insert(mb, item.key)
end
end
if #mb == 0 then
return keys[1]
end
table.sort(mb, function(a, b)
return key2rank[a] < key2rank[b]
end)
local idx = 1
for _, key in ipairs(keys) do
if idx > #mb or key2rank[key] < key2rank[mb[idx]] then
return key
end
idx = idx + 1
end
return nil
end
local action_save = function(mb_path, bookmarks, path)
if path == nil or #path == 0 then
return
end
local path_obj = bookmarks[path]
-- check tag
local tag = path_obj and path_obj.tag or path:match(".*[\\/]([^\\/]+)[\\/]?$")
while true do
local value, event = ya.input({
title = "Tag (alias name)",
value = tag,
position = { "top-center", y = 3, w = 40 },
})
if event ~= 1 then
return
end
tag = value or ''
if #tag == 0 then
ya.notify {
title = "Bookmarks",
content = "Empty tag",
timeout = 2,
level = "info",
}
else
-- check the tag
local tag_obj = nil
for _, item in pairs(bookmarks) do
if item.tag == tag then
tag_obj = item
break
end
end
if tag_obj == nil or tag_obj.path == path then
break
end
ya.notify {
title = "Bookmarks",
content = "Duplicated tag",
timeout = 2,
level = "info",
}
end
end
-- check key
local key = path_obj and path_obj.key or generate_key(bookmarks)
while true do
local value, event = ya.input({
title = "Key (1 character, optional)",
value = key,
position = { "top-center", y = 3, w = 40 },
})
if event ~= 1 then
return
end
key = value or ""
if key == "" then
key = ""
break
elseif #key == 1 then
-- check the key
local key_obj = nil
for _, item in pairs(bookmarks) do
if item.key == key then
key_obj = item
break
end
end
if key_obj == nil or key_obj.path == path then
break
else
ya.notify {
title = "Bookmarks",
content = "Duplicated key",
timeout = 2,
level = "info",
}
end
else
ya.notify {
title = "Bookmarks",
content = "The length of key shoule be 1",
timeout = 2,
level = "info",
}
end
end
-- save
set_bookmarks(path, { tag = tag, path = path, key = key })
bookmarks = get_state_attr("bookmarks")
save_to_file(mb_path, bookmarks)
ya.notify {
title = "Bookmarks",
content = '"' .. tag .. '" saved"',
timeout = 2,
level = "info",
}
end
local action_delete = function(mb_path, bookmarks, path)
if path == nil then
return
end
local tag = bookmarks[path].tag
set_bookmarks(path, nil)
bookmarks = get_state_attr("bookmarks")
save_to_file(mb_path, bookmarks)
ya.notify {
title = "Bookmarks",
content = '"' .. tag .. '" deleted',
timeout = 2,
level = "info",
}
end
local action_delete_all = function(mb_path)
local value, event = ya.input({
title = "Delete all bookmarks? (y/n)",
position = { "top-center", y = 3, w = 40 },
})
if event ~= 1 then
return
end
if string.lower(value) == "y" then
set_state_attr("bookmarks", {})
save_to_file(mb_path, {})
ya.notify {
title = "Bookmarks",
content = "All bookmarks deleted",
timeout = 2,
level = "info",
}
else
ya.notify {
title = "Bookmarks",
content = "Cancel delete",
timeout = 2,
level = "info",
}
end
end
return {
setup = function(state, options)
state.path = options.path or
(ya.target_family() == "windows" and os.getenv("APPDATA") .. "\\yazi\\config\\bookmark") or
(os.getenv("HOME") .. "/.config/yazi/bookmark")
state.cli = options.cli or "fzf"
state.jump_notify = options.jump_notify and true
-- init the keys
local keys = options.keys or "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
state.keys = {}
state.key2rank = {}
for i = 1, #keys do
local char = keys:sub(i, i)
table.insert(state.keys, char)
state.key2rank[char] = i
end
-- init the bookmarks
local bookmarks = {}
for _, item in pairs(options.bookmarks or {}) do
bookmarks[item.path] = { tag = item.tag, path = item.path, key = item.key }
end
-- load the config
local file = io.open(state.path, "r")
if file ~= nil then
for line in file:lines() do
local tag, path, key = string.match(line, "(.-)\t(.-)\t(.*)")
if tag and path then
key = key or ""
bookmarks[path] = { tag = tag, path = path, key = key }
end
end
file:close()
end
-- create bookmarks file to enable fzf
save_to_file(state.path, bookmarks)
state.bookmarks = bookmarks
end,
entry = function(self, jobs)
local action = jobs.args[1]
if not action then
return
end
local mb_path, cli, bookmarks, jump_notify = get_state_attr("path"), get_state_attr("cli"), get_state_attr("bookmarks"), get_state_attr("jump_notify")
if action == "save" then
action_save(mb_path, bookmarks, get_hovered_path())
elseif action == "delete_by_key" then
action_delete(mb_path, bookmarks, which_find(bookmarks))
elseif action == "delete_by_fzf" then
action_delete(mb_path, bookmarks, fzf_find(cli, mb_path))
elseif action == "delete_all" then
action_delete_all(mb_path)
elseif action == "jump_by_key" then
action_jump(bookmarks, which_find(bookmarks), jump_notify)
elseif action == "jump_by_fzf" then
action_jump(bookmarks, fzf_find(cli, mb_path), jump_notify)
elseif action == "rename_by_key" then
action_save(mb_path, bookmarks, which_find(bookmarks))
elseif action == "rename_by_fzf" then
action_save(mb_path, bookmarks, fzf_find(cli, mb_path))
end
end,
}

View File

@@ -0,0 +1,21 @@
MIT License
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.

View File

@@ -0,0 +1,166 @@
# yaziline.yazi
Simple lualine-like status line for yazi.
Read more about features and configuration [here](#features).
![preview](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/preview.png)
## Requirements
- yazi version >= [25.5.28](https://github.com/sxyazi/yazi/releases/tag/v25.5.28)
- Font with symbol support. For example [Nerd Fonts](https://www.nerdfonts.com/).
## Installation
```sh
ya pkg add llanosrocas/yaziline
```
Or manually copy `main.lua` to the `~/.config/yazi/plugins/yaziline.yazi/main.lua`
## Usage
Add this to your `~/.config/yazi/init.lua`:
```lua
require("yaziline"):setup()
```
Optionally, configure line:
```lua
require("yaziline"):setup({
color = "#98c379", -- main theme color
secondary_color = "#5A6078", -- secondary color
default_files_color = "darkgray", -- color of the file counter when it's inactive
selected_files_color = "white",
yanked_files_color = "green",
cut_files_color = "red",
separator_style = "angly", -- "angly" | "curvy" | "liney" | "empty"
separator_open = "",
separator_close = "",
separator_open_thin = "",
separator_close_thin = "",
separator_head = "",
separator_tail = "",
select_symbol = "",
yank_symbol = "󰆐",
filename_max_length = 24, -- truncate when filename > 24
filename_truncate_length = 6, -- leave 6 chars on both sides
filename_truncate_separator = "..." -- the separator of the truncated filename
})
```
```
 MODE  size  long_file...name.md  S 0 Y 0
| | | | | | | | |
| | | | | | | | └─── yank_symbol
| | | | | | | └─────── select_symbol
| | | | | | └───────── separator_close_thin
| | | | | └─────────────────── filename_truncate_separator
| | | | └─────────────────────────────── separator_close
| | | └────────────────────────────────── secondary_color
| | └────────────────────────────────────── separator_close
| └────────────────────────────────────────── color
└───────────────────────────────────────────── separator_head
```
## Features
### Preconfigured separators
Choose your style:
- `angly`
![angly](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/angly.png)
- `curvy`
![curvy](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/curvy.png)
- `liney`
![liney](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/liney.png)
- `empty`
![empty](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/empty.png)
### Separator customization
You can provide your own symbols for separators combined with preconfigured separators. For example:
```lua
require("yaziline"):setup({
-- Optinal config
separator_style = "angly", -- preconfigured style
separator_open = "", -- instead of 
separator_close = "", -- instead of 
separator_open_thin = "", -- change to anything
separator_close_thin = "", -- change to anything
separator_head = "", -- to match the style
separator_tail = "" -- to match the style
})
```
![empty](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/separator-combination.png)
_You can find more symbols [here](https://www.nerdfonts.com/cheat-sheet)_
### File actions icons
You can provide your own symbols for `select` and `yank`. For example:
```lua
require("yaziline"):setup({
-- Optinal config
select_symbol = "", -- "S" by default
yank_symbol = "󰆐" -- "Y" by default
})
```
![empty](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/file-actions.png)
_You can find more symbols [here](https://www.nerdfonts.com/cheat-sheet)_
### Colors and font weight
By default yaziline uses color values from your `theme.toml` (or flavor) but you can set custom colors in the `init.lua`:
```lua
require("yaziline"):setup({
color = "#98c379",
default_files_color = "darkgray",
selected_files_color = "white",
yanked_files_color = "green",
cut_files_color = "red",
})
```
For example, here is how my line looks like:
![preview-2](https://github.com/llanosrocas/yaziline.yazi/blob/master/.github/images/preview-2.png)
### Selected and Yanked Counter
Displays the number of selected ('S') and yanked ('Y') files on the left. If files are cut, the yank counter changes color, since its `yank --cut` under the hood.
### Truncated filename
Displays the truncated filename on the left, which is useful for smaller windows or long filenames. By default, it's 24 characters with trimming to 12 (6 + 6). Adjust in the `setup`.
```lua
require("yaziline"):setup({
filename_max_length = 24, -- truncate when filename > 24
filename_truncate_length = 6, -- leave 6 chars on both sides
filename_truncate_separator = "..." -- the separator of the truncated filename
})
```
### ISO Date for 'Modified'
On the right, you'll find the date and time the file was modified, formatted in an [ISO](https://en.wikipedia.org/wiki/ISO_8601)-like string for universal date representation. Adjust in the `Status:date` function.
## Credits
- [yazi source code](https://github.com/sxyazi/yazi)
- [yatline.yazi](https://github.com/imsi32/yatline.yazi/tree/main)
- [lualine.nvim](https://github.com/nvim-lualine/lualine.nvim)

View File

@@ -0,0 +1,212 @@
---@diagnostic disable: undefined-global
local function setup(_, options)
options = options or {}
local default_separators = {
angly = { "", "", "", "" },
curvy = { "", "", "", "" },
liney = { "", "", "|", "|" },
empty = { "", "", "", "" },
}
local separators = default_separators[options.separator_style or "angly"]
local config = {
separator_styles = {
separator_open = options.separator_open or separators[1],
separator_close = options.separator_close or separators[2],
separator_open_thin = options.separator_open_thin or separators[3],
separator_close_thin = options.separator_close_thin or separators[4],
separator_head = options.separator_head or "",
separator_tail = options.separator_tail or "",
},
select_symbol = options.select_symbol or "S",
yank_symbol = options.yank_symbol or "Y",
filename_max_length = options.filename_max_length or 24,
filename_truncate_length = options.filename_truncate_length or 6,
filename_truncate_separator = options.filename_truncate_separator or "...",
color = options.color or nil,
secondary_color = options.secondary_color or nil,
default_files_color = options.default_files_color
or th.which.separator_style.fg
or "darkgray",
selected_files_color = options.selected_files_color
or th.mgr.count_selected.bg
or "white",
yanked_files_color = options.selected_files_color
or th.mgr.count_copied.bg
or "green",
cut_files_color = options.cut_files_color
or th.mgr.count_cut.bg
or "red",
}
local current_separator_style = config.separator_styles
function Header:count()
return ui.Line({})
end
function Status:mode()
local mode = tostring(self._tab.mode):upper()
local style = self:style()
return ui.Line({
ui.Span(current_separator_style.separator_head)
:fg(config.color or style.main.bg),
ui.Span(" " .. mode .. " ")
:fg(th.which.mask.bg)
:bg(config.color or style.main.bg),
})
end
function Status:size()
local h = self._current.hovered
local size = h and (h:size() or h.cha.len) or 0
local style = self:style()
return ui.Span(current_separator_style.separator_close .. " " .. ya.readable_size(size) .. " ")
:fg(config.color or style.main.bg)
:bg(config.secondary_color or th.which.separator_style.fg)
end
function Status:utf8_sub(str, start_char, end_char)
local start_byte = utf8.offset(str, start_char)
local end_byte = end_char and (utf8.offset(str, end_char + 1) - 1) or #str
if not start_byte or not end_byte then
return ""
end
return string.sub(str, start_byte, end_byte)
end
function Status:truncate_name(filename, max_length)
local base_name, extension = filename:match("^(.+)(%.[^%.]+)$")
base_name = base_name or filename
extension = extension or ""
if utf8.len(base_name) > max_length then
base_name = self:utf8_sub(base_name, 1, config.filename_truncate_length)
.. config.filename_truncate_separator
.. self:utf8_sub(base_name, -config.filename_truncate_length)
end
return base_name .. extension
end
function Status:name()
local h = self._current.hovered
if not h then
return ui.Line({
ui.Span(current_separator_style.separator_close .. " ")
:fg(config.secondary_color or th.which.separator_style.fg),
ui.Span("Empty dir")
:fg(config.color or style.main.bg),
})
end
local truncated_name = self:truncate_name(h.name, config.filename_max_length)
local style = self:style()
return ui.Line({
ui.Span(current_separator_style.separator_close .. " ")
:fg(config.secondary_color or th.which.separator_style.fg),
ui.Span(truncated_name)
:fg(config.color or style.main.bg),
})
end
function Status:files()
local files_yanked = #cx.yanked
local files_selected = #cx.active.selected
local files_cut = cx.yanked.is_cut
local selected_fg = files_selected > 0
and config.selected_files_color
or config.default_files_color
local yanked_fg = files_yanked > 0
and
(files_cut
and config.cut_files_color
or config.yanked_files_color
)
or config.default_files_color
local yanked_text = files_yanked > 0
and config.yank_symbol .. " " .. files_yanked
or config.yank_symbol .. " 0"
return ui.Line({
ui.Span(" " .. current_separator_style.separator_close_thin .. " ")
:fg(th.which.separator_style.fg),
ui.Span(config.select_symbol .. " " .. files_selected .. " ")
:fg(selected_fg),
ui.Span(yanked_text .. " ")
:fg(yanked_fg),
})
end
function Status:modified()
local hovered = cx.active.current.hovered
if not hovered then
return ""
end
local cha = hovered.cha
local time = (cha.mtime or 0) // 1
return ui.Span(os.date("%Y-%m-%d %H:%M", time) .. " " .. current_separator_style.separator_open_thin .. " ")
:fg(th.which.separator_style.fg)
end
function Status:percent()
local percent = 0
local cursor = self._tab.current.cursor
local length = #self._tab.current.files
if cursor ~= 0 and length ~= 0 then
percent = math.floor((cursor + 1) * 100 / length)
end
if percent == 0 then
percent = " Top "
elseif percent == 100 then
percent = " Bot "
else
percent = string.format(" %2d%% ", percent)
end
local style = self:style()
return ui.Line({
ui.Span(" " .. current_separator_style.separator_open)
:fg(config.secondary_color or th.which.separator_style.fg),
ui.Span(percent)
:fg(config.color or style.main.bg)
:bg(config.secondary_color or th.which.separator_style.fg),
ui.Span(current_separator_style.separator_open)
:fg(config.color or style.main.bg)
:bg(config.secondary_color or th.which.separator_style.fg),
})
end
function Status:position()
local cursor = self._tab.current.cursor
local length = #self._tab.current.files
local style = self:style()
return ui.Line({
ui.Span(string.format(" %2d/%-2d ", math.min(cursor + 1, length), length))
:fg(th.which.mask.bg)
:bg(config.color or style.main.bg),
ui.Span(current_separator_style.separator_tail):fg(config.color or style.main.bg),
})
end
Status:children_add(Status.files, 4000, Status.LEFT)
Status:children_add(Status.modified, 0, Status.RIGHT)
end
return { setup = setup }