Update 2026-01-31

Update 2026-01-27
This commit is contained in:
2026-01-31 17:45:16 +02:00
parent 8c606045e1
commit 5324f54618
73 changed files with 6508 additions and 5373 deletions

View File

@@ -47,7 +47,7 @@ plugin.
## Requirements
- [Yazi][yazi-link] v25.5.31+
- [Yazi][yazi-link] v25.12.29+
- [`7z` or `7zz` command][7z-link]
- [`file` command][file-command-link]
@@ -191,7 +191,6 @@ then it will operate on the selected items.
[this section above][augment-section].
Videos:
- When `prompt` is set to `true`:
[open-prompt-video]
@@ -258,7 +257,6 @@ then it will operate on the selected items.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[extract-must-have-hovered-item-video]
@@ -304,27 +302,19 @@ then it will operate on the selected items.
[opener]
extract = [
{ run = 'ya pub augmented-extract --list "$@"', desc = "Extract here", for = "unix" },
{ run = 'ya pub augmented-extract --list %*', desc = "Extract here", for = "windows" },
{ run = "ya pub augmented-extract --list %s", desc = "Extract here" },
]
```
If that exceeds your editor's line length limit,
another way to do it is:
Alternatively, another way to do it is:
```toml
# ~/.config/yazi/yazi.toml for Linux and macOS
# %AppData%\yazi\config\yazi.toml for Windows
[[opener.extract]]
run = 'ya pub augmented-extract --list "$@"'
run = "ya pub augmented-extract --list %s"
desc = "Extract here"
for = "unix"
[[opener.extract]]
run = 'ya pub augmented-extract --list %*'
desc = "Extract here"
for = "windows"
```
- The `extract` command supports recursively extracting archives,
@@ -467,7 +457,6 @@ then it will operate on the selected items.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[rename-must-have-hovered-item-video]
@@ -490,7 +479,6 @@ then it will operate on the selected items.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[remove-must-have-hovered-item-video]
@@ -513,7 +501,6 @@ then it will operate on the selected items.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[copy-must-have-hovered-item-video]
@@ -604,7 +591,6 @@ then it will operate on the selected items.
use the default `shell` command provided by Yazi.
Videos:
- When `must_have_hovered_item` is `true`:
[shell-must-have-hovered-item-video]
@@ -623,8 +609,8 @@ then it will operate on the selected items.
- To use this command, the syntax is exactly the same as the default
`shell` command provided by Yazi. You just provide
the command you want and provide any Yazi shell variable,
which is documented [here][yazi-shell-variables].
the command you want and provide any Yazi shell variable that
**provides the file path**, which is [documented here][yazi-shell-variables].
The plugin will automatically replace the shell variable you give
with the file paths for the item group before executing the command.
@@ -650,7 +636,7 @@ then it will operate on the selected items.
[[mgr.prepend_keymap]]
on = "i"
run = "plugin augment-command -- shell '$PAGER $@' --block --exit-if-dir"
run = "plugin augment-command -- shell '$PAGER %s' --block --exit-if-dir"
desc = "Open the pager"
```
@@ -667,7 +653,7 @@ then it will operate on the selected items.
[[mgr.prepend_keymap]]
on = "o"
run = "plugin augment-command -- shell '$EDITOR $@' --block --exit-if-dir"
run = "plugin augment-command -- shell '$EDITOR %s' --block --exit-if-dir"
desc = "Open the editor"
```
@@ -692,18 +678,10 @@ the shell command arguments, so here are a few ways to do it:
# %AppData%\yazi\config\keymap.toml on Windows
[[mgr.prepend_keymap]]
on = "i"
run = "plugin augment-command -- shell --block 'bat -p --pager less $@'"
run = "plugin augment-command -- shell --block 'bat -p --pager less %s'"
desc = "Open with bat"
```
Even though the `$@` argument above is considered
a shell variable in Linux and macOS,
the plugin automatically replaces it with the full path
of the items in the item group,
so it does not need to be quoted with
double quotes `"`, as it is expanded by the plugin,
and not meant to be expanded by the shell.
2. If the arguments to the `shell` command have special
shell variables on Linux and macOS, like `$SHELL`,
or special shell characters like `>`, `|`, or spaces,
@@ -765,16 +743,12 @@ the shell command arguments, so here are a few ways to do it:
on = "<C-e>"
run = '''plugin augment-command --
shell --
paths=$(for p in $@; do echo "$p"; done | paste -s -d,)
paths=$(for p in %s; do echo "$p"; done | paste -s -d,)
thunderbird -compose "attachment='$paths'"
'''
desc = "Email files using Mozilla Thunderbird"
```
Once again, the `$@` variable above does not need to be quoted
in double quotes `"` as it is expanded by the plugin
instead of the shell.
If the above few methods to avoid using backslashes
within your shell command to escape the quotes are
still insufficient for your use case,
@@ -1043,7 +1017,6 @@ in your `keymap.toml` file.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[archive-must-have-hovered-item-video]
@@ -1149,7 +1122,6 @@ in your `keymap.toml` file.
[this section above][augment-section].
Videos:
- When `must_have_hovered_item` is `true`:
[editor-must-have-hovered-item-video]
@@ -1181,7 +1153,6 @@ in your `keymap.toml` file.
causing a flash and causing Yazi to send a notification.
Videos:
- When `must_have_hovered_item` is `true`:
[pager-must-have-hovered-item-video]

View File

@@ -1,4 +1,4 @@
--- @since 25.5.31
--- @since 25.12.29
-- Plugin to make some Yazi commands smarter
-- Written in Lua 5.4
@@ -24,29 +24,50 @@
-- The type for the archiver list items command
---@alias Archiver.ListItemsCommand fun(
--- self: Archiver,
---): output: CommandOutput|nil, error: Error|nil
---): output: Output?, error: Error?
-- The type for the archiver get items function
---@alias Archiver.GetItems fun(
--- self: Archiver,
---): files: string[], directories: string[], error: string|nil
---): files: string[], dirs: string[], result: Archiver.Result
-- The type for the archiver extract function
---@alias Archiver.Extract fun(
--- self: Archiver,
--- has_only_one_file: boolean|nil,
--- has_only_one_file: boolean?,
---): Archiver.Result
-- The type for the archiver archive function
---@alias Archiver.Archive fun(
--- self: Archiver,
--- item_paths: string[],
--- password: string|nil,
--- encrypt_headers: boolean|nil,
--- password: string?,
--- encrypt_headers: boolean?,
---): Archiver.Result
-- The type for the archiver command function
---@alias Archiver.Command fun(): output: CommandOutput|nil, error: Error|nil
---@alias Archiver.Command fun(): output: Output?, error: Error?
-- The type for the Yazi input options
---@alias YaziInputOptions {
--- title: string,
--- value: string?,
--- obscure: boolean?,
--- pos: AsPos,
--- realtime: boolean?,
--- debounce: number?,
---}
-- The type for the Yazi notification options
---@alias YaziNotificationOptions {
--- title: string,
--- content: string,
--- timeout: number,
--- level: "info"|"warn"|"error"|nil,
---}
-- The type for the Yazi confirm options
---@alias YaziConfirmOptions { pos: AsPos, title: AsLine, body: AsText }
-- The type of the function to get the password options
---@alias GetPasswordOptions fun(is_confirm_password: boolean): YaziInputOptions
@@ -93,13 +114,13 @@
-- The type for the archiver function result
---@class (exact) Archiver.Result
---@field successful boolean Whether the archiver function was successful
---@field output string|nil The output of the archiver function
---@field cancelled boolean|nil boolean Whether the archiver was cancelled
---@field error string|nil The error message
---@field archive_path string|nil The path to the archive
---@field destination_path string|nil The path to the destination
---@field extracted_items_path string|nil The path to the extracted items
---@field archiver_name string|nil The name of the archiver
---@field output string? The output of the archiver function
---@field cancelled boolean? boolean Whether the archiver was cancelled
---@field error string? The error message
---@field archive_path string? The path to the archive
---@field destination_path string? The path to the destination
---@field extracted_items_path string? The path to the extracted items
---@field archiver_name string? The name of the archiver
-- The module table
---@class AugmentCommandPlugin
@@ -168,7 +189,7 @@ local INPUT_AND_CONFIRM_OPTIONS = {
"title",
"origin",
"offset",
"content",
"body",
}
-- The default configuration for the plugin
@@ -221,7 +242,7 @@ local DEFAULT_NOTIFICATION_OPTIONS = {
-- The values are just dummy values
-- so that I don't have to maintain two
-- different types for the same thing.
---@type tab.Preference
---@type tab__Pref
local TAB_PREFERENCE_KEYS = {
sort_by = "alphabetical",
sort_sensitive = false,
@@ -300,7 +321,7 @@ local BASE_ARCHIVER_ERROR = table.concat({
-- The base archiver that all archivers inherit from
---@class Archiver
---@field name string The name of the archiver
---@field command string|nil The shell command for the archiver
---@field command string? The shell command for the archiver
---@field commands string[] The possible archiver commands
---
--- Whether the archiver supports preserving file permissions
@@ -334,7 +355,12 @@ end
-- The method to get the archive items
---@type Archiver.GetItems
function Archiver:get_items() return {}, {}, BASE_ARCHIVER_ERROR end
function Archiver:get_items()
return {}, {}, {
successful = false,
error = BASE_ARCHIVER_ERROR,
}
end
-- The method to extract the archive
---@type Archiver.Extract
@@ -424,7 +450,7 @@ local get_mime_type_without_prefix_template_pattern =
-- The pattern to get the shell variables in a command
---@type string
local shell_variable_pattern = "[%$%%][%*@0]"
local shell_variable_pattern = "%%[hs]%d?"
-- The pattern to match the bat command
---@type string
@@ -446,8 +472,8 @@ local bat_command_pattern = "%f[%a]bat%f[%A]"
-- Pass true as the first parameter to get the function
-- to merge the tables recursively.
---@param deep_or_target table<any, any>|boolean|nil Recursively merge or not
---@param target table<any, any> The target table to merge
---@param ... table<any, any>[] The tables to merge
---@param target table<any, any>? The target table to merge
---@param ... table<any, any>[]? The tables to merge
---@return table<any, any> merged_table The merged table
local function merge_tables(deep_or_target, target, ...)
--
@@ -489,6 +515,9 @@ local function merge_tables(deep_or_target, target, ...)
args = { target, ... }
end
-- The target table will not be nil after the checks above
---@cast target_table table<any, any>
-- Initialise the index variable
local index = #target_table + 1
@@ -549,7 +578,7 @@ end
-- Function to split a string into a list
---@param given_string string The string to split
---@param separator string|nil The character to split the string by
---@param separator string? The character to split the string by
---@return string[] splitted_strings The list of strings split by the character
local function string_split(given_string, separator)
--
@@ -705,7 +734,7 @@ end
-- Function to show a warning
---@param warning_message any The warning message
---@param options YaziNotificationOptions|nil Options for the notification
---@param options YaziNotificationOptions? Options for the notification
---@return nil
local function show_warning(warning_message, options)
return ya.notify(
@@ -718,7 +747,7 @@ end
-- Function to show an error
---@param error_message any The error message
---@param options YaziNotificationOptions|nil Options for the notification
---@param options YaziNotificationOptions? Options for the notification
---@return nil
local function show_error(error_message, options)
return ya.notify(
@@ -737,7 +766,7 @@ local function throw_error(error_message, ...)
end
-- Function to get the theme from an async function
---@type fun(): Th The theme object
---@type fun(): th The theme object
local get_theme = ya.sync(function(state) return state.theme end)
-- Function to get the component option string
@@ -752,13 +781,13 @@ end
---@param component BuiltInComponents|PluginComponents The name of the component
---@param defaults {
--- prompts: string|string[], -- The default prompts
--- content: string|ui.Line|ui.Text|nil, -- The default contents
--- origin: string|nil, -- The default origin
--- offset: Position|nil, -- The default offset
--- body: string|ui.Line|ui.Text|nil, -- The default body
--- origin: string?, -- The default origin
--- offset: ui.Pos?, -- The default offset
---}
---@param is_plugin_options boolean|nil Whether the options are plugin specific
---@param is_confirm boolean|nil Whether the component is the confirm component
---@param title_index integer|nil The index to get the title
---@param is_plugin_options boolean? Whether the options are plugin specific
---@param is_confirm boolean? Whether the component is the confirm component
---@param title_index integer? The index to get the title
---@return YaziInputOptions|YaziConfirmOptions options The resolved options
local function get_user_input_or_confirm_options(
component,
@@ -808,7 +837,7 @@ local function get_user_input_or_confirm_options(
end
-- Unpack the options
local title_option, origin_option, offset_option, content_option =
local title_option, origin_option, offset_option, body_option =
table.unpack(option_list)
-- Get the value of all the options
@@ -818,7 +847,7 @@ local function get_user_input_or_confirm_options(
or defaults.origin
or default_options[1]
local offset = theme_config[offset_option or ""] or {}
local content = theme_config[content_option or ""] or defaults.content
local body = theme_config[body_option or ""] or defaults.body
-- Get the title
local title = type(raw_title) == "string" and raw_title
@@ -837,16 +866,16 @@ local function get_user_input_or_confirm_options(
-- Return the options
return {
title = title,
[is_confirm and "pos" or "position"] = position,
content = content,
pos = position,
body = body,
}
end
-- Function to get a password from the user
---@param get_password_options GetPasswordOptions Get password options function
---@param want_confirmation boolean|nil Whether to get a confirmation password
---@return string|nil password The password or nil if the user cancelled
---@return InputEvent|nil event The event for the input function
---@param want_confirmation boolean? Whether to get a confirmation password
---@return string? password The password or nil if the user cancelled
---@return number? event The event for the input function
local function get_password(get_password_options, want_confirmation)
--
@@ -920,40 +949,38 @@ local function show_overwrite_prompt(file_path_to_overwrite)
ConfigurableComponents.BuiltIn.Overwrite,
{
prompts = "Overwrite file?",
content = ui.Line("Will overwrite the following file:"),
body = ui.Line("Will overwrite the following file:"),
},
false,
true
)
-- Get the type of the overwrite content
local overwrite_content_type = type(overwrite_confirm_options.content)
-- Get the type of the overwrite body
---@cast overwrite_confirm_options YaziConfirmOptions
local overwrite_body_type = type(overwrite_confirm_options.body)
-- Initialise the first line of the content
-- Initialise the first line of the body
local first_line = nil
-- If the content section is a string
if
overwrite_content_type == "string"
or overwrite_content_type == "table"
then
-- If the body section is a string
if overwrite_body_type == "string" or overwrite_body_type == "table" then
--
-- Wrap the string in a line and align it to the center.
first_line = ui.Line(overwrite_confirm_options.content)
first_line = ui.Line(overwrite_confirm_options.body)
:align(ui.Align.CENTER)
-- Otherwise, just set the first line to the content given
-- Otherwise, just set the first line to the body given
else
first_line = overwrite_confirm_options.content
first_line = overwrite_confirm_options.body
end
-- Create the content for the overwrite prompt
-- Create the body for the overwrite prompt
---@cast first_line ui.Line|ui.Span
overwrite_confirm_options.content = ui.Text({
overwrite_confirm_options.body = ui.Text({
first_line,
ui.Line(string.rep("", overwrite_confirm_options.pos.w - 2))
:style(ui.Style(th.confirm.border))
:style(th.confirm.border)
:align(ui.Align.LEFT),
ui.Line(tostring(file_path_to_overwrite)):align(ui.Align.LEFT),
}):wrap(ui.Wrap.TRIM)
@@ -967,7 +994,7 @@ local function show_overwrite_prompt(file_path_to_overwrite)
end
-- Function to merge the given configuration table with the default one
---@param config UserConfiguration|nil The configuration table to merge
---@param config UserConfiguration? The configuration table to merge
---@return UserConfiguration merged_config The merged configuration table
local function merge_configuration(config)
--
@@ -1069,7 +1096,7 @@ end
-- Function to initialise the configuration
---@type fun(
--- user_config: UserConfiguration|nil, -- The configuration object
--- user_config: UserConfiguration?, -- The configuration object
---): Configuration The initialised configuration object
local initialise_config = ya.sync(function(state, user_config)
--
@@ -1090,7 +1117,7 @@ local initialise_config = ya.sync(function(state, user_config)
end)
-- Function to initialise the theme configuration
---@type fun(): Th
---@type fun(): th
local initialise_theme = ya.sync(function(state)
--
@@ -1130,9 +1157,9 @@ end)
-- Function to try if a shell command exists
---@param shell_command string The shell command to check
---@param args string[]|nil The arguments to the shell command
---@param args string[]? The arguments to the shell command
---@return boolean shell_command_exists Whether the shell command exists
---@return CommandOutput|nil output The output of the shell command
---@return Output? output The output of the shell command
local function async_shell_command_exists(shell_command, args)
--
@@ -1194,9 +1221,9 @@ local subscribe_to_augmented_extract_event = ya.sync(function(_)
end)
-- Function to initialise the plugin
---@param opts UserConfiguration|nil The options given to the plugin
---@param opts UserConfiguration? The options given to the plugin
---@return Configuration config The initialised configuration object
---@return Th theme The saved theme object
---@return th theme The saved theme object
local function initialise_plugin(opts)
--
@@ -1246,7 +1273,7 @@ local function standardise_mime_type(mime_type)
end
-- Function to check if a given mime type is an archive
---@param mime_type string|nil The mime type of the file
---@param mime_type string? The mime type of the file
---@return boolean is_archive Whether the mime type is an archive
local function is_archive_mime_type(mime_type)
--
@@ -1266,7 +1293,7 @@ end
-- Function to check if a given file extension
-- is an archive file extension
---@param file_extension string|nil The file extension of the file
---@param file_extension string? The file extension of the file
---@return boolean is_archive Whether the file extension is an archive
local function is_archive_file_extension(file_extension)
--
@@ -1335,8 +1362,8 @@ end
-- Function to get a temporary directory url
-- for the given file path
---@param path string The path to the item to create a temporary directory
---@param destination_given boolean|nil Whether the destination was given
---@return Url|nil url The url of the temporary directory
---@param destination_given boolean? Whether the destination was given
---@return Url? url The url of the temporary directory
local function get_temporary_directory_url(path, destination_given)
--
@@ -1377,8 +1404,8 @@ local get_current_directory = ya.sync(
-- Function to get the path of the hovered item
---@type fun(
--- quote: boolean|nil, -- Whether to escape the characters in the path
---): string|nil The path of the hovered item
--- quote: boolean?, -- Whether to escape the characters in the path
---): string? The path of the hovered item
local get_path_of_hovered_item = ya.sync(function(_, quote)
--
@@ -1425,8 +1452,8 @@ end)
-- Function to get the paths of the selected items
---@type fun(
--- quote: boolean|nil, -- Whether to escape the characters in the path
---): string[]|nil The list of paths of the selected items
--- quote: boolean?, -- Whether to escape the characters in the path
---): string[]? The list of paths of the selected items
local get_paths_of_selected_items = ya.sync(function(_, quote)
--
@@ -1463,7 +1490,7 @@ end)
local get_number_of_tabs = ya.sync(function() return #cx.tabs end)
-- Function to get the tab preferences
---@type fun(): tab.Preference
---@type fun(): tab__Pref
local get_tab_preferences = ya.sync(function(_)
--
@@ -1488,7 +1515,7 @@ end)
-- ItemGroup.Selected for the selected items,
-- and ItemGroup.Prompt to tell the calling function
-- to prompt the user.
---@type fun(): ItemGroup|nil The desired item group
---@type fun(): ItemGroup? The desired item group
local get_item_group_from_state = ya.sync(function(state)
--
@@ -1539,7 +1566,7 @@ local get_item_group_from_state = ya.sync(function(state)
end)
-- Function to prompt the user for their desired item group
---@return ItemGroup|nil item_group The item group selected by the user
---@return ItemGroup? item_group The item group selected by the user
local function prompt_for_desired_item_group()
--
@@ -1547,7 +1574,7 @@ local function prompt_for_desired_item_group()
local config = get_config()
-- Get the default item group
---@type ItemGroup|nil
---@type ItemGroup?
local default_item_group = config.default_item_group_for_prompt
-- Get the input options, which the (h/s) options
@@ -1598,7 +1625,7 @@ local function prompt_for_desired_item_group()
end
-- Function to get the item group
---@return ItemGroup|nil item_group The desired item group
---@return ItemGroup? item_group The desired item group
local function get_item_group()
--
@@ -1619,7 +1646,7 @@ end
-- Function to get all the items in the given directory
---@param directory_path string The path to the directory
---@param get_hidden_items boolean Whether to get hidden items
---@param directories_only boolean|nil Whether to only get directories
---@param directories_only boolean? Whether to only get directories
---@return string[] directory_items The list of urls to the directory items
local function get_directory_items(
directory_path,
@@ -1716,8 +1743,8 @@ end
-- The function to create a new instance of the archiver
---@param archive_path string The path to the archive
---@param config Configuration The configuration object
---@param destination_path string|nil The path to extract to
---@return Archiver|nil instance An instance of the archiver if available
---@param destination_path string? The path to extract to
---@return Archiver? instance An instance of the archiver if available
function Archiver:new(archive_path, config, destination_path)
--
@@ -1774,7 +1801,7 @@ end
-- Function to retry the archiver
---@private
---@param archiver_function Archiver.Command Archiver command to retry
---@param clean_up_wanted boolean|nil Whether to clean up the destination path
---@param clean_up_wanted boolean? Whether to clean up the destination path
---@return Archiver.Result result Result of the archiver function
function SevenZip:retry_archiver(archiver_function, clean_up_wanted)
--
@@ -1898,7 +1925,7 @@ function SevenZip:retry_archiver(archiver_function, clean_up_wanted)
-- Set the width of the component to the input width
---@cast password_input_options YaziInputOptions
password_input_options.position.w = input_width
password_input_options.pos.w = input_width
-- Return the password input options
return password_input_options
@@ -1992,15 +2019,11 @@ function SevenZip:get_items()
-- Get the output
local output = archiver_result.output
-- Get the error
local error = archiver_result.error
-- If the archiver command was not successful,
-- or the output was nil,
-- then return nil the error message,
-- and nil as the correct password
-- then return the result
if not archiver_result.successful or not output then
return files, directories, error
return files, directories, archiver_result
end
-- Otherwise, split the output at the newline character
@@ -2042,16 +2065,15 @@ function SevenZip:get_items()
::continue::
end
-- Return the list of files, the list of directories,
-- the error message, and the password
return files, directories, error
-- Return the list of files, the list of directories and the result
return files, directories, archiver_result
end
-- Function to extract an archive using the command
---@param extract_files_only boolean|nil Extract the files only or not
---@param extract_behaviour ExtractBehaviour|nil The extraction behaviour
---@return CommandOutput|nil output The output of the command
---@return Error|nil error The error if any
---@param extract_files_only boolean? Extract the files only or not
---@param extract_behaviour ExtractBehaviour? The extraction behaviour
---@return Output? output The output of the command
---@return Error? error The error if any
function SevenZip:extract_command(extract_files_only, extract_behaviour)
--
@@ -2128,10 +2150,10 @@ end
-- Function to call the command to add items to an archive
---@param item_paths string[] The path to the items being added to the archive
---@param password string|nil The password to encrypt the archive with
---@param encrypt_headers boolean|nil Whether to encrypt the archive headers
---@return CommandOutput|nil output The output of the command
---@return Error|nil error The error if any
---@param password string? The password to encrypt the archive with
---@param encrypt_headers boolean? Whether to encrypt the archive headers
---@return Output? output The output of the command
---@return Error? error The error if any
function SevenZip:archive_command(item_paths, password, encrypt_headers)
--
@@ -2241,8 +2263,15 @@ function Tar:get_items()
---@type string[]
local directories = {}
-- If there is no output, return the empty lists and the error
if not output then return files, directories, tostring(error) end
-- If there is no output, return the empty lists and the result
if not output then
return files,
directories,
{
successful = false,
error = tostring(error),
}
end
-- Otherwise, split the output into lines and iterate over it
for _, line in ipairs(string_split(output.stdout, "\n")) do
@@ -2268,11 +2297,16 @@ function Tar:get_items()
end
-- Return the list of files and directories and the error
return files, directories, output.stderr
return files,
directories,
{
successful = true,
error = output.stderr,
}
end
-- Function to extract an archive using the command
---@param extract_behaviour ExtractBehaviour|nil The extract behaviour to use
---@param extract_behaviour ExtractBehaviour? The extract behaviour to use
function Tar:extract_command(extract_behaviour)
--
@@ -2437,8 +2471,8 @@ end
---@param archive_path string The path to the archive file
---@param command SupportedCommands The command the archiver is used for
---@param config Configuration The configuration for the plugin
---@param destination_path string|nil The path to the destination directory
---@return Archiver|nil archiver The archiver for the file type
---@param destination_path string? The path to the destination directory
---@return Archiver? archiver The archiver for the file type
---@return Archiver.Result result The results of getting the archiver
local function get_archiver(archive_path, command, config, destination_path)
--
@@ -2536,7 +2570,7 @@ local function move_extracted_items(archive_url, destination_url)
-- The function to clean up the destination directory
-- and return the archiver result in the event of an error
---@param error string The error message to return
---@param empty_dir_only boolean|nil Whether to remove the empty dir only
---@param empty_dir_only boolean? Whether to remove the empty dir only
---@return Archiver.Result
local function fail(error, empty_dir_only)
--
@@ -2675,7 +2709,7 @@ end
---@param archive_path string The path to the archive
---@param args Arguments The arguments passed to the plugin
---@param config Configuration The configuration object
---@param destination_path string|nil The destination path to extract to
---@param destination_path string? The destination path to extract to
---@return Archiver.Result extraction_result The extraction results
local function recursively_extract_archive(
archive_path,
@@ -2739,21 +2773,13 @@ local function recursively_extract_archive(
-- Get the list of archive files and directories,
-- the error message and the password
local archive_files, archive_directories, error = archiver:get_items()
local archive_files, archive_directories, archiver_result =
archiver:get_items()
-- If there are no are no archive files and directories
-- If there are no are no archive files and directories,
-- return the extraction result
if #archive_files == 0 and #archive_directories == 0 then
--
-- The extraction result
---@type Archiver.Result
local extraction_result = {
successful = false,
error = error or "Archive is empty",
}
-- Return the extraction result
return add_additional_info(extraction_result)
return add_additional_info(archiver_result)
end
-- Get if the archive has only one file
@@ -3274,7 +3300,7 @@ end
-- Function to enter or open the created file
---@param item_url Url The url of the item to create
---@param is_directory boolean|nil Whether the item to create is a directory
---@param is_directory boolean? Whether the item to create is a directory
---@param args Arguments The arguments passed to the plugin
---@param config Configuration The configuration object
---@return nil
@@ -3890,18 +3916,18 @@ local function handle_quit(args, config)
local quit_confirm_options =
get_user_input_or_confirm_options(ConfigurableComponents.Plugin.Quit, {
prompts = "Quit?",
content = ui.Text({
body = ui.Text({
"There are multiple tabs open.",
"Are you sure you want to quit?",
}):wrap(ui.Wrap.TRIM),
}, true, true)
-- Get the type of the quit content
local quit_content_type = type(quit_confirm_options.content)
-- Get the type of the quit body
local quit_body_type = type(quit_confirm_options.body)
-- If the type of the quit content is a string or a list of strings
if quit_content_type == "string" or quit_content_type == "table" then
quit_confirm_options.content = ui.Text(quit_confirm_options.content)
-- If the type of the quit body is a string or a list of strings
if quit_body_type == "string" or quit_body_type == "table" then
quit_confirm_options.body = ui.Text(quit_confirm_options.body)
:wrap(ui.Wrap.TRIM)
end
@@ -4641,7 +4667,7 @@ local function handle_editor(args, config)
if not editor then return end
-- Initialise the shell command
local shell_command = string.format(editor .. " $@")
local shell_command = editor .. " %s"
-- Get the cha object of the hovered file
local hovered_item_cha = fs.cha(
@@ -4653,7 +4679,7 @@ local function handle_editor(args, config)
-- and sudo edit is supported,
-- set the shell command to "sudo -e"
if config.sudo_edit_supported and hovered_item_cha.uid == 0 then
shell_command = "sudo -e $@"
shell_command = "sudo -e %s"
end
-- Call the handle shell function
@@ -4683,7 +4709,7 @@ local function handle_pager(args, config)
-- with the pager command
handle_shell(
merge_tables({
pager .. " $@",
pager .. " %s",
block = true,
exit_if_dir = true,
}, args),
@@ -4725,7 +4751,7 @@ local function run_command_func(command, args, config)
}
-- Get the function for the command
---@type CommandFunction|nil
---@type CommandFunction?
local command_func = command_table[command]
-- If the function isn't found, notify the user and exit the function
@@ -4738,7 +4764,7 @@ local function run_command_func(command, args, config)
end
-- The setup function to setup the plugin
---@param opts UserConfiguration|nil The options given to the plugin
---@param opts UserConfiguration? The options given to the plugin
---@return nil
function M:setup(opts)
--

View File

@@ -1,4 +1,4 @@
--- @since 25.5.31
--- @since 25.12.29
local selected_or_hovered = ya.sync(function()
local tab, paths = cx.active, {}
@@ -11,6 +11,15 @@ local selected_or_hovered = ya.sync(function()
return paths
end)
local function fail(s, ...)
ya.notify {
title = "Chmod",
content = string.format(s, ...),
level = "error",
timeout = 5,
}
end
return {
entry = function()
ya.emit("escape", { visual = true })
@@ -23,20 +32,16 @@ return {
local value, event = ya.input {
title = "Chmod:",
pos = { "top-center", y = 3, w = 40 },
position = { "top-center", y = 3, w = 40 }, -- TODO: remove
}
if event ~= 1 then
return
end
local status, err = Command("chmod"):arg(value):arg(urls):spawn():wait()
if not status or not status.success then
ya.notify {
title = "Chmod",
content = string.format("Chmod on selected files failed, error: %s", status and status.code or err),
level = "error",
timeout = 5,
}
local output, err = Command("chmod"):arg(value):arg(urls):stderr(Command.PIPED):output()
if not output then
fail("Failed to run chmod: %s", err)
elseif not output.status.success then
fail("Chmod failed with stderr:\n%s", output.stderr:gsub("^chmod:%s*", ""))
end
end,
}

View File

@@ -1,4 +1,4 @@
--- @since 25.2.7
--- @since 26.1.22
local function info(content)
return ya.notify {
@@ -8,20 +8,20 @@ local function info(content)
}
end
local selected_url = ya.sync(function()
local selected_path = ya.sync(function()
for _, u in pairs(cx.active.selected) do
return u
return u.cache or u
end
end)
local hovered_url = ya.sync(function()
local hovered_path = ya.sync(function()
local h = cx.active.current.hovered
return h and h.url
return h and h.path
end)
return {
entry = function()
local a, b = selected_url(), hovered_url()
local a, b = selected_path(), hovered_path()
if not a then
return info("No file selected")
elseif not b then

View File

@@ -15,36 +15,41 @@ ya pkg add yazi-rs/plugins:git
Add the following to your `~/.config/yazi/init.lua`:
```lua
require("git"):setup()
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"
name = "*"
run = "git"
id = "git"
url = "*"
run = "git"
[[plugin.prepend_fetchers]]
id = "git"
name = "*/"
run = "git"
id = "git"
url = "*/"
run = "git"
```
## Advanced
> [!NOTE]
> [!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`
- `th.git.unknown` - status cannot/not yet determined
- `th.git.modified` - modified file
- `th.git.added` - added file
- `th.git.untracked` - untracked file
- `th.git.ignored` - ignored file
- `th.git.deleted` - deleted file
- `th.git.updated` - updated file
- `th.git.clean` - clean file
For example:
@@ -57,20 +62,24 @@ 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`
- `th.git.unknown_sign` - status cannot/not yet determined
- `th.git.modified_sign` - modified file
- `th.git.added_sign` - added file
- `th.git.untracked_sign` - untracked file
- `th.git.ignored_sign` - ignored file
- `th.git.deleted_sign` - deleted file
- `th.git.updated_sign` - updated file
- `th.git.clean_sign` - clean file
For example:
```lua
-- ~/.config/yazi/init.lua
th.git = th.git or {}
th.git.unknown_sign = " "
th.git.modified_sign = "M"
th.git.deleted_sign = "D"
th.git.clean_sign = ""
```
## License

View File

@@ -1,4 +1,4 @@
--- @since 25.5.31
--- @since 25.12.29
local WINDOWS = ya.target_family() == "windows"
@@ -7,14 +7,15 @@ local WINDOWS = ya.target_family() == "windows"
-- see `bubble_up`
---@enum CODES
local CODES = {
excluded = 100, -- ignored directory
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,
unknown = 0,
clean = 0,
}
local PATTERNS = {
@@ -79,7 +80,7 @@ local function bubble_up(changed)
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
new[s] = (new[s] or CODES.clean) > code and new[s] or code
url = url.parent
end
end
@@ -116,7 +117,7 @@ local add = ya.sync(function(st, cwd, repo, changed)
st.dirs[cwd] = repo
st.repos[repo] = st.repos[repo] or {}
for path, code in pairs(changed) do
if code == CODES.unknown then
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
@@ -125,12 +126,7 @@ local add = ya.sync(function(st, cwd, repo, changed)
st.repos[repo][path] = code
end
end
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
ui.render()
end)
---@param cwd string
@@ -142,12 +138,7 @@ local remove = ya.sync(function(st, cwd)
return
end
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
ui.render()
st.dirs[cwd] = nil
if not st.repos[repo] then
return
@@ -172,31 +163,39 @@ local function setup(st, opts)
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"),
[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.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.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)
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)]
if not self._file.in_current then
return ""
end
if not code or signs[code] == "" then
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] }
@@ -208,7 +207,7 @@ end
---@type UnstableFetcher
local function fetch(_, job)
local cwd = job.files[1].url.base
local cwd = job.files[1].url.base or job.files[1].url.parent
local repo = root(cwd)
if not repo then
remove(tostring(cwd))
@@ -246,11 +245,11 @@ local function fetch(_, job)
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`,
-- 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.unknown
changed[s] = changed[s] or CODES.clean
end
add(tostring(cwd), repo, changed)

View File

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

View File

@@ -40,7 +40,6 @@ using `ffmpeg` if available and media metadata using `mediainfo`.
## Installation
- Install mediainfo CLI:
- [https://mediaarea.net/en/MediaInfo/Download](https://mediaarea.net/en/MediaInfo/Download)
- Run this command in terminal to check if it's installed correctly:
@@ -52,7 +51,6 @@ using `ffmpeg` if available and media metadata using `mediainfo`.
- Install ImageMagick (for linux, you can use your distro package manager to install):
https://imagemagick.org/script/download.php
- Install this plugin:
```bash
@@ -74,7 +72,7 @@ Create `.../yazi/yazi.toml` and add:
> [!IMPORTANT]
>
> For yazi nightly replace `name` with `url`
> For yazi (>=v25.12.29) replace `name` with `url`
```toml
[plugin]
@@ -120,3 +118,18 @@ title = { fg = "green" }
# Example: `Format: FLAC` with blue color in preview images above
tbl_col = { fg = "blue" }
```
## (Optional) Keymaps to hide metadata and to preview images in full screen
> [!IMPORTANT]
> Use any key you want, but make sure there is no conflicts with [default Keybindings](https://github.com/sxyazi/yazi/blob/main/yazi-config/preset/keymap-default.toml).
Since Yazi prioritizes the first matching key, `prepend_keymap` takes precedence over defaults.
Or you can use `keymap` to replace all other keys
```toml
[mgr]
prepend_keymap = [
{ on = "<F9>", run = "plugin mediainfo -- toggle-metadata", desc = "Toggle media preview metadata" },
]
```

View File

@@ -10,6 +10,16 @@ local skip_labels = {
["MD5 of the unencoded content"] = true,
}
local ENTRY_ACTION = {
toggle_metadata = "toggle-metadata",
}
local STATE_KEY = {
units = "units",
hide_metadata = "hide_metadata",
prev_metadata_area = "prev_metadata_area",
}
local magick_image_mimes = {
avif = true,
hei = true,
@@ -18,8 +28,10 @@ local magick_image_mimes = {
["heif-sequence"] = true,
["heic-sequence"] = true,
jxl = true,
tiff = true,
xml = true,
["svg+xml"] = true,
["canon-cr2"] = true,
}
local seekable_mimes = {
@@ -76,7 +88,9 @@ local function image_layer_count(job)
if layer_count then
return layer_count
end
local output, err = Command("identify"):arg({ tostring(job.file.url) }):output()
local output, err = Command("identify")
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
if err then
return 0
end
@@ -110,73 +124,85 @@ function M:peek(job)
return
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. suffix
local output = read_mediainfo_cached_file(cache_mediainfo_path)
local hide_metadata = get_state(STATE_KEY.hide_metadata)
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local is_wrap = rt.preview.wrap == "yes"
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
output = read_mediainfo_cached_file(cache_mediainfo_path)
end
for str in output:gsub("\n+$", ""):gmatch("[^\n]*") do
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not skip_labels[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. suffix
local output = read_mediainfo_cached_file(cache_mediainfo_path)
if output then
local max_width = math.max(1, job.area.w)
if output:match("^Error:") then
job.args.force_reload_mediainfo = true
preload_status, preload_err = self:preload(job)
if not preload_status or preload_err then
return
end
elseif str ~= "General" then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
output = read_mediainfo_cached_file(cache_mediainfo_path)
end
if line then
local line_height = math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)
if (last_line + line_height) > job.skip then
table.insert(lines, line)
for str in output:gsub("\n+$", ""):gmatch("[^\n]*") do
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not skip_labels[label] then
line = ui.Line({
ui.Span(label .. ": "):style(ui.Style():fg("reset"):bold()),
ui.Span(value):style(th.spot.tbl_col or ui.Style():fg("blue")),
})
end
elseif str ~= "General" then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
break
if line then
local line_height = math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1)
if (last_line + line_height) > job.skip then
table.insert(lines, line)
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
break
end
last_line = last_line + line_height
end
last_line = last_line + line_height
end
end
mediainfo_height = math.min(limit, last_line)
end
local mediainfo_height = math.min(limit, last_line)
if
(job.skip > 0 and #lines == 0)
(job.skip > 0 and #lines == 0 and not hide_metadata)
and (
not is_seekable
or (is_video and job.skip >= 90)
or (
(job.mime == "image/adobe.photoshop" or job.mime == "application/postscript")
and image_layer_count(job)
< (1 + math.floor(math.max(0, get_state("units") and (job.skip / get_state("units")) or 0)))
< (1 + math.floor(
math.max(0, get_state(STATE_KEY.units) and (job.skip / get_state(STATE_KEY.units)) or 0)
))
)
)
then
ya.emit(
"peek",
{ math.max(0, job.skip - (get_state("units") or limit)), only_if = job.file.url, upper_bound = true }
)
ya.emit("peek", {
math.max(0, job.skip - (get_state(STATE_KEY.units) or limit)),
only_if = job.file.url,
upper_bound = true,
})
return
end
force_render()
-- NOTE: Hacky way to prevent image overlap with old metadata area
if hide_metadata and get_state(STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(get_state(STATE_KEY.prev_metadata_area))),
})
ya.sleep(0.1)
end
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
@@ -189,7 +215,6 @@ function M:peek(job)
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
@@ -220,12 +245,19 @@ function M:peek(job)
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
set_state(STATE_KEY.prev_metadata_area, not hide_metadata and {
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
} or nil)
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
set_state("units", job.units)
set_state(STATE_KEY.units, job.units)
ya.emit("peek", {
math.max(0, cx.active.preview.skip + job.units),
only_if = job.file.url,
@@ -242,7 +274,7 @@ function M:preload(job)
cache_img_url = seekable_mimes[job.mime] and ya.file_cache(job) or cache_img_url
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
local err_msg = ""
local is_valid_utf8_path = is_valid_utf8(tostring(job.file.url))
local is_valid_utf8_path = is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- video mimetype
if job.mime then
if string.find(job.mime, "^video/") then
@@ -261,15 +293,11 @@ function M:preload(job)
"error",
"-threads",
1,
"-hwaccel",
"auto",
"-skip_frame",
"nokey",
"-an",
"-sn",
"-dn",
"-i",
tostring(job.file.url),
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-vframes",
1,
"-q:v",
@@ -283,13 +311,17 @@ function M:preload(job)
}):output()
-- NOTE: Some audio types doesn't have cover image -> error ""
if
(audio_preload_output and audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
(
audio_preload_output
and audio_preload_output.stderr ~= nil
and audio_preload_output.stderr ~= ""
and not audio_preload_output.stderr:find("Output file does not contain any stream")
) or audio_preload_err
then
err_msg = err_msg
.. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
else
cache_img_url_cha = fs.cha(cache_img_url)
cache_img_url_cha, _ = fs.cha(cache_img_url)
if not cache_img_url_cha then
-- NOTE: Workaround case audio has no cover image. Prevent regenerate preview image
audio_preload_output, audio_preload_err = require("magick")
@@ -317,18 +349,18 @@ function M:preload(job)
-- image
elseif string.find(job.mime, "^image/") or job.mime == "application/postscript" then
local svg_plugin_ok, svg_plugin = pcall(require, "svg")
local _, magick_plugin = pcall(require, "magick")
local magick_plugin_ok, magick_plugin = pcall(require, "magick")
local mime = job.mime:match(".*/(.*)$")
local image_plugin = magick_image_mimes[mime]
and ((mime == "svg+xml" and svg_plugin_ok) and svg_plugin or magick_plugin)
and ((mime == "svg+xml" and svg_plugin_ok) and svg_plugin or (magick_plugin_ok and magick_plugin))
or require("image")
local cache_img_status, image_preload_err
-- psd, ai, eps
if mime == "adobe.photoshop" or job.mime == "application/postscript" then
local layer_index = 0
local units = get_state("units")
local units = get_state(STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, job.skip / units))
@@ -346,7 +378,10 @@ function M:preload(job)
:arg({
"-background",
"none",
tostring(job.file.url) .. "[" .. tostring(layer_index) .. "]",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url)
.. "["
.. tostring(layer_index)
.. "]",
"-auto-orient",
"-strip",
"-resize",
@@ -371,7 +406,7 @@ function M:preload(job)
:arg({
"-background",
"none",
tostring(job.file.url),
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
"-flatten",
@@ -403,15 +438,20 @@ function M:preload(job)
local cmd = "mediainfo"
local output, err
if is_valid_utf8_path then
output, err = Command(cmd):arg({ tostring(job.file.url) }):output()
output, err = Command(cmd)
:arg({ tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) })
:output()
else
cmd = "cd "
.. path_quote(job.file.url.parent)
.. path_quote(job.file.path or job.file.cache or (job.file.url.path or job.file.url).parent)
.. " && "
.. cmd
.. " "
.. path_quote(tostring(job.file.url.name))
output, err = Command(SHELL):arg({ "-c", cmd }):arg({ tostring(job.file.url) }):output()
.. path_quote(tostring(job.file.path or job.file.cache or job.file.url.name))
output, err = Command(SHELL)
:arg({ "-c", cmd })
:arg({ tostring(job.file.path or job.file.cache or (job.file.url.path or job.file.url)) })
:output()
end
if err then
err_msg = err_msg .. string.format("Failed to start `%s`, Do you have `%s` installed?\n", cmd, cmd)
@@ -422,4 +462,15 @@ function M:preload(job)
)
end
function M:entry(job)
local action = job.args[1]
if action == ENTRY_ACTION.toggle_metadata then
set_state(STATE_KEY.hide_metadata, not get_state(STATE_KEY.hide_metadata))
ya.emit("peek", {
force = true,
})
end
end
return M

View File

@@ -1,4 +1,4 @@
--- @since 25.5.31
--- @since 25.12.29
local toggle_ui = ya.sync(function(self)
if self.children then
@@ -7,12 +7,7 @@ local toggle_ui = ya.sync(function(self)
else
self.children = Modal:children_add(self, 10)
end
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
ui.render()
end)
local subscribe = ya.sync(function(self)
@@ -23,12 +18,7 @@ end)
local update_partitions = ya.sync(function(self, partitions)
self.partitions = partitions
self.cursor = math.max(0, math.min(self.cursor or 0, #self.partitions - 1))
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
ui.render()
end)
local active_partition = ya.sync(function(self) return self.partitions[self.cursor + 1] end)
@@ -39,12 +29,7 @@ local update_cursor = ya.sync(function(self, cursor)
else
self.cursor = ya.clamp(0, self.cursor + cursor, #self.partitions - 1)
end
-- TODO: remove this
if ui.render then
ui.render()
else
ya.render()
end
ui.render()
end)
local M = {
@@ -272,24 +257,33 @@ function M.operate(type)
return -- TODO: mount/unmount main disk
end
local output, err
local cmd
if ya.target_os() == "macos" then
output, err = Command("diskutil"):arg({ type, active.src }):output()
cmd = Command("diskutil"):arg { type, active.src }
end
if ya.target_os() == "linux" then
if type == "eject" and active.src:match("^/dev/sr%d+") then
Command("udisksctl"):arg({ "unmount", "-b", active.src }):status()
output, err = Command("eject"):arg({ "--traytoggle", active.src }):output()
cmd = Command("eject"):arg { "--traytoggle", active.src }
elseif type == "eject" then
Command("udisksctl"):arg({ "unmount", "-b", active.src }):status()
output, err = Command("udisksctl"):arg({ "power-off", "-b", active.src }):output()
cmd = Command("udisksctl"):arg { "power-off", "-b", active.src }
else
output, err = Command("udisksctl"):arg({ type, "-b", active.src }):output()
cmd = Command("udisksctl"):arg { type, "-b", active.src }
end
end
if not cmd then
return M.fail("mount.yazi is not currently supported on your platform")
end
local output, err = cmd:output()
if not output then
M.fail("Failed to %s `%s`: %s", type, active.src, err)
if cmd.program then
M.fail("Failed to spawn `%s`: %s", cmd.program, err)
else
M.fail("Failed to spawn `udisksctl`: %s", err) -- TODO: remove
end
elseif not output.status.success then
M.fail("Failed to %s `%s`: %s", type, active.src, output.stderr)
end

View File

@@ -35,29 +35,38 @@ Make sure you have [ouch](https://github.com/ouch-org/ouch) installed and in you
For archive preview, add this to your `yazi.toml`:
```toml
[plugin]
prepend_previewers = [
# Archive previewer
{ mime = "application/*zip", run = "ouch" },
{ mime = "application/x-tar", run = "ouch" },
{ mime = "application/x-bzip2", run = "ouch" },
{ mime = "application/x-7z-compressed", run = "ouch" },
{ mime = "application/x-rar", run = "ouch" },
{ mime = "application/vnd.rar", run = "ouch" },
{ mime = "application/x-xz", run = "ouch" },
{ mime = "application/xz", run = "ouch" },
{ mime = "application/x-zstd", run = "ouch" },
{ mime = "application/zstd", run = "ouch" },
{ mime = "application/java-archive", run = "ouch" },
]
[[plugin.prepend_previewers]]
mime = "application/{*zip,tar,bzip2,7z*,rar,xz,zstd,java-archive}"
run = "ouch"
```
Now go to an archive on Yazi, you should see the archive's content in the preview pane. You can use `J` and `K` to roll up and down the preview.
If you want to change the icon or the style of text, you can modify the `peek` function in `init.lua` file (all of them are stored in the `lines` variable).
#### Customization
Previews can be customized by adding extra arguments in the `run` string:
```toml
[plugin]
prepend_previewers = [
# Change the top-level archive icon
{ ..., run = "ouch --archive-icon='🗄️ '" },
# Or remove it by setting it to ''
{ ..., run = "ouch --archive-icon=''" },
# Enable file icons
{ ..., run = "ouch --show-file-icons" },
# Disable tree view
{ ..., run = "ouch --list-view" },
# These can be combined
{ ..., run = "ouch --archive-icon='🗄️ ' --show-file-icons --list-view" },
]
```
### Compression
For compession, add this to your `keymap.toml`:
For compression, add this to your `keymap.toml`:
```toml
[[mgr.prepend_keymap]]

View File

@@ -1,15 +1,50 @@
local M = {}
-- Extract the tree prefix (if any) from a line
local function get_tree_prefix(line)
local _, prefix_len = line:find("", 1, true)
if prefix_len then
return line:sub(1, prefix_len)
else
return ""
end
end
-- Add a filetype icon to a line
local function line_with_icon(line)
line = line:gsub("[\r\n]+$", "") -- Trailing newlines mess with filetype detection
local tree_prefix = get_tree_prefix(line)
local url = line:sub(#tree_prefix + 1)
local icon = File({
url = Url(url),
cha = Cha {
mode = tonumber(url:sub(-1) == "/" and "40700" or "100644", 8),
kind = url:sub(-1) == "/" and 1 or 0, -- For Yazi <25.9.x compatibility
}
}):icon()
if icon then
line = ui.Line { tree_prefix, ui.Span(icon.text .. " "):style(icon.style), url }
end
return line
end
function M:peek(job)
local child = Command("ouch")
:arg({ "l", "-t", "-y", tostring(job.file.url) })
local cmd = Command("ouch"):arg("l")
if not job.args.list_view then
cmd:arg("-t")
end
cmd:arg({ "-y", tostring(job.file.url) })
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:spawn()
local child = cmd:spawn()
local limit = job.area.h
local archive_icon = job.args.archive_icon or "\u{1f4c1} "
local file_name = string.match(tostring(job.file.url), ".*[/\\](.*)")
local lines = string.format("\u{1f4c1} %s\n", file_name)
local num_lines = 1
local lines = { string.format(" %s%s", archive_icon, file_name) }
local num_skip = 0
repeat
local line, event = child:read_line()
@@ -21,19 +56,29 @@ function M:peek(job)
if line:find('Archive', 1, true) ~= 1 and line:find('[INFO]', 1, true) ~= 1 then
if num_skip >= job.skip then
lines = lines .. line
num_lines = num_lines + 1
if job.args.show_file_icons then
if line:find ('[ERROR]', 1, true) == 1 then
-- On error, disable file icons for the rest of the output
job.args.show_file_icons = false
elseif line:find ('[WARNING]', 1, true) ~= 1 then
-- Show icons for non-warning lines only
line = line_with_icon(line)
end
end
line = ui.Line { " ", line } -- One space padding
table.insert(lines, line)
else
num_skip = num_skip + 1
end
end
until num_lines >= limit
until #lines >= limit
child:start_kill()
if job.skip > 0 and num_lines < limit then
if job.skip > 0 and #lines < limit then
ya.emit(
"peek",
{ tostring(math.max(0, job.skip - (limit - num_lines))), only_if = tostring(job.file.url), upper_bound = "" }
{ tostring(math.max(0, job.skip - (limit - #lines))), only_if = tostring(job.file.url), upper_bound = "" }
)
else
ya.preview_widget(job, { ui.Text(lines):area(job.area) })
@@ -125,7 +170,6 @@ function M:entry(job)
local output_name, name_event = ya.input({
title = "Create archive:",
value = default_name .. "." .. default_fmt,
position = { "top-center", y = 3, w = 40 },
pos = { "top-center", y = 3, w = 40 },
})
if name_event ~= 1 then
@@ -136,7 +180,6 @@ function M:entry(job)
if file_exists(output_name) then
local confirm, confirm_event = ya.input({
title = "Overwrite " .. output_name .. "? (y/N)",
position = { "top-center", y = 3, w = 40 },
pos = { "top-center", y = 3, w = 40 },
})
if not (confirm_event == 1 and confirm:lower() == "y") then

View File

@@ -40,12 +40,12 @@ Add the below to your `yazi.toml` file to allow the respective file to previewed
[plugin]
prepend_previewers = [
{ name = "*.csv", run = "rich-preview"}, # for csv files
{ name = "*.md", run = "rich-preview" }, # for markdown (.md) files
{ name = "*.rst", run = "rich-preview"}, # for restructured text (.rst) files
{ name = "*.ipynb", run = "rich-preview"}, # for jupyter notebooks (.ipynb)
{ name = "*.json", run = "rich-preview"}, # for json (.json) files
# { name = "*.lang_type", run = "rich-preview"} # for particular language files eg. .py, .go., .lua, etc.
{ url = "*.csv", run = "rich-preview"}, # for csv files
{ url = "*.md", run = "rich-preview" }, # for markdown (.md) files
{ url = "*.rst", run = "rich-preview"}, # for restructured text (.rst) files
{ url = "*.ipynb", run = "rich-preview"}, # for jupyter notebooks (.ipynb)
{ url = "*.json", run = "rich-preview"}, # for json (.json) files
# { url = "*.lang_type", run = "rich-preview"} # for particular language files eg. .py, .go., .lua, etc.
]
```
@@ -78,7 +78,7 @@ To use `rich` with piper, you can add this in your `yazi.toml` file:
```toml
[[plugin.prepend_previewers]]
name = "*.md"
url = "*.md"
run = 'piper -- rich -j --left --panel=rounded --guides --line-numbers --force-terminal "$1"'
```

View File

@@ -1,4 +1,4 @@
--- @since 25.5.31
--- @since 25.12.29
local hovered = ya.sync(function()
local h = cx.active.current.hovered
@@ -17,7 +17,6 @@ local function prompt()
return ya.input {
title = "Smart filter:",
pos = { "center", w = 50 },
position = { "center", w = 50 }, -- TODO: remove
realtime = true,
debounce = 0.1,
}

View File

@@ -8,11 +8,11 @@ https://github.com/user-attachments/assets/6d2fc9e7-f86e-4444-aab6-4e11e51e8b34
## Installation
```sh
ya pack -a iynaix/time-travel
ya pkg add iynaix/time-travel
```
> [!NOTE]
> The minimum required yazi version is 25.2.7.
> The minimum required yazi version is 25.12.29.
## Usage
@@ -21,17 +21,17 @@ Add keymaps similar to the following to your `~/.config/yazi/keymap.toml`:
```toml
[[manager.prepend_keymap]]
on = ["z", "h"]
run = "plugin time-travel --args=prev"
run = "plugin time-travel prev"
desc = "Go to previous snapshot"
[[manager.prepend_keymap]]
on = ["z", "l"]
run = "plugin time-travel --args=next"
run = "plugin time-travel next"
desc = "Go to next snapshot"
[[manager.prepend_keymap]]
on = ["z", "e"]
run = "plugin time-travel --args=exit"
run = "plugin time-travel exit"
desc = "Exit browsing snapshots"
```

View File

@@ -23,7 +23,7 @@ end
--- Verify if `sudo` is already authenticated
--- @return boolean
local function sudo_already()
local status = Command("sudo"):args({ "--validate", "--non-interactive" }):status()
local status = Command("sudo"):arg({ "--validate", "--non-interactive" }):status()
assert(status, "Failed to run `sudo --validate --non-interactive`")
return status.success
end
@@ -36,12 +36,12 @@ end
--- nil: no error
--- 1: sudo failed
local function run_with_sudo(program, args)
local cmd = Command("sudo"):args({ program, table.unpack(args) }):stdout(Command.PIPED):stderr(Command.PIPED)
local cmd = Command("sudo"):arg({ program, table.unpack(args) }):stdout(Command.PIPED):stderr(Command.PIPED)
if sudo_already() then
return cmd:output()
end
local permit = ya.hide()
local permit = ui.hide()
print(string.format("Sudo password required to run: `%s %s`", program, table.concat(args, " ")))
local output = cmd:output()
permit:drop()
@@ -67,7 +67,7 @@ end
---@param cwd string
---@return string|nil
local get_filesystem_type = function(cwd)
local stat, _ = Command("stat"):args({ "-f", "-c", "%T", cwd }):output()
local stat, _ = Command("stat"):arg({ "-f", "-c", "%T", cwd }):output()
if not stat.status.success then
return nil
end
@@ -77,7 +77,7 @@ end
---@param cwd string
---@return string|nil
local zfs_dataset = function(cwd)
local df, _ = Command("df"):args({ "--output=source", cwd }):output()
local df, _ = Command("df"):arg({ "--output=source", cwd }):output()
local dataset = nil
for line in df.stdout:gmatch("[^\r\n]+") do
-- dataset is last line in output
@@ -89,7 +89,7 @@ end
---@param dataset string
---@return string|nil
local zfs_mountpoint = function(dataset)
local zfs, _ = Command("zfs"):args({ "get", "-H", "-o", "value", "mountpoint", dataset }):output()
local zfs, _ = Command("zfs"):arg({ "get", "-H", "-o", "value", "mountpoint", dataset }):output()
-- not a dataset!
if not zfs.status.success then
@@ -157,7 +157,7 @@ end
---@return Snapshot[]
local zfs_snapshots = function(dataset, mountpoint, relative)
-- -S is for reverse order
local zfs_snapshots, _ = Command("zfs"):args({ "list", "-H", "-t", "snapshot", "-o", "name", "-S", "creation",
local zfs_snapshots, _ = Command("zfs"):arg({ "list", "-H", "-t", "snapshot", "-o", "name", "-S", "creation",
dataset })
:output()
@@ -183,7 +183,7 @@ end
---@param cwd string
---@return string|nil
local function btrfs_mountpoint(cwd)
local cmd, _ = Command("findmnt"):args({ "-no", "TARGET", "-T", cwd }):output()
local cmd, _ = Command("findmnt"):arg({ "-no", "TARGET", "-T", cwd }):output()
if not cmd.status.success then
return nil
end
@@ -329,7 +329,7 @@ return {
end
if action == "exit" then
ya.manager_emit("cd", { latest_path })
ya.emit("cd", { latest_path })
return
end
@@ -343,7 +343,7 @@ return {
local find_and_goto_snapshot = function(start_idx, end_idx, step)
if start_idx == 0 then
-- going from newest snapshot to current state
return ya.manager_emit("cd", { latest_path })
return ya.emit("cd", { latest_path })
elseif start_idx < 0 then
return notify_warn("No earlier snapshots found.")
elseif start_idx > #snapshots then
@@ -353,7 +353,7 @@ return {
for i = start_idx, end_idx, step do
local snapshot_dir = snapshots[i].path
if io.open(snapshot_dir, "r") then
return ya.manager_emit("cd", { snapshot_dir })
return ya.emit("cd", { snapshot_dir })
end
end

View File

@@ -0,0 +1,43 @@
local M = {}
function M:peek(job)
local child = Command("transmission-show")
:arg(tostring(job.file.url))
:stdout(Command.PIPED)
:stderr(Command.PIPED)
:spawn()
if not child then
return require("code"):peek(job)
end
local limit = job.area.h
local i, lines = 0, ""
repeat
local next, event = child:read_line()
if event == 1 then
return require("code"):peek(job)
elseif event ~= 0 then
break
end
i = i + 1
if i > job.skip then
lines = lines .. next
end
until i >= job.skip + limit
child:start_kill()
if job.skip > 0 and i < job.skip + limit then
ya.manager_emit("peek", { math.max(0, i - limit), only_if = job.file.url, upper_bound = true })
else
lines = lines:gsub("\t", string.rep(" ", rt.preview.tab_size))
ya.preview_widget(job, { ui.Text.parse(lines):area(job.area) })
end
end
function M:seek(job)
require("code"):seek(job)
end
return M

View File

@@ -14,31 +14,46 @@ what-size supports Yazi on Linux, macOS, and Windows.
### Yazi
- yazi `25.5.28` and onwards since commit `c5c939b` ([link](https://github.com/pirafrank/what-size.yazi/commit/c5c939bb37ec1d132c942cf5724d4e847acc2977))
- yazi `25.x`-`25.4.8` since commit `fce1778` ([link](https://github.com/pirafrank/what-size.yazi/commit/fce1778d911621dc57796cdfdf11dcda3c2e28de))
- yazi `0.4.x` since commit `2780de5` ([link](https://github.com/pirafrank/what-size.yazi/commit/2780de5aeef1ed16d1973dd6e0cd4d630c900d56))
- yazi `0.3.x` up to commit `f08f7f2` ([link](https://github.com/pirafrank/what-size.yazi/commit/f08f7f2d5c94958ac4cb66c51a7c24b4319c6c93))
In an effort to make things easy, I keep `compatibility/yazi-x.y.z` branches with each pointing to the most up-to-date commit compatible with yazi release `x.y.z`. Full table below.
|Yazi releases|what-size branch name|
|---|---|
|*[latest stable](https://github.com/sxyazi/yazi/releases/latest)*|`main`|
|`25.5.28`|`compatibility/yazi-25.5.28`|
|`25.x`-`25.4.8`|`compatibility/yazi-25.4.8`|
|`0.4.x`|`compatibility/yazi-0.4.x`|
|`0.3.x`|`compatibility/yazi-0.3.x`|
Please notice that `nightly` releses may work but are not explicitly supported.
## Requirements
### Before Yazi's version 25.5.28
- Use this commit: [Old version](https://github.com/pirafrank/what-size.yazi/commit/d8966568f2a80394bf1f9a1ace6708ddd4cc8154)
- `du` on Linux and macOS
- PowerShell on Windows
### On Yazi's version 25.5.28 or newer
- No requirement
## Installation
```sh
ya pkg add pirafrank/what-size
```
or
or (**DEPRECATED** - use only for yazi `25.4.8` and older):
**DEPRECATED**
```sh
ya pack -a 'pirafrank/what-size'
```
## Usage
### Keymap
Add this to your `~/.config/yazi/keymap.toml`:
```toml
@@ -54,18 +69,30 @@ If you want to copy the result to clipboard, you can add `--clipboard` or `-c` a
[[mgr.prepend_keymap]]
on = [ ".", "s" ]
run = "plugin what-size -- '--clipboard'"
desc = "Calc size of selection or cwd"
desc = "Calc size of sel/cwd + paste to clipboard"
```
```toml
[[mgr.prepend_keymap]]
on = [ ".", "s" ]
run = "plugin what-size -- '-c'"
desc = "Calc size of selection or cwd"
desc = "Calc size of sel/cwd + paste to clipboard"
```
Change to whatever keybinding you like.
### User interface (optional)
If you want to place the size value exactly where you want, modify the priority value. Also changing two strings `LEFT` and `RIGHT` will add them to the left and right side of the value. Remember to add to and change these lines inside your `init.lua` file if you want to customize, or the plugin will use this configuration by default:
```lua
require("what-size"):setup({
priority = 400,
LEFT = "",
RIGHT = " ",
})
```
## Feedback
If you have any feedback, suggestions, or ideas please let me know by opening an issue.
@@ -82,6 +109,16 @@ YAZI_LOG=debug yazi
Logs will be saved to `~.local/state/yazi/yazi.log` file.
### Plugin definition
The repo already has a `.luarc.json` file. You only need to run the following to add the `types` plugin dependency:
```sh
ya pkg add yazi-rs/plugins:types
```
as per the [docs](https://github.com/yazi-rs/plugins/tree/main/types.yazi).
## Contributing
Contributions are welcome. Please fork the repository and submit a PR.

View File

@@ -1,102 +1,238 @@
--- @since 25.5.28
-- This plugin is now only supporting Yazi's version 25.5.28 or newer
-- since commit https://github.com/sxyazi/yazi/pull/2695
-- function to get paths of selected elements or current directory
-- if no elements are selected
local get_paths = ya.sync(function()
local paths = {}
-- get selected files
for _, u in pairs(cx.active.selected) do
paths[#paths + 1] = tostring(u)
end
-- if no files are selected, get current directory
if #paths == 0 then
if cx.active.current.cwd then
paths[1] = tostring(cx.active.current.cwd)
else
ya.err("what-size would return nil paths")
-- TODO: Asynchronous calculating and dynamic displaying in statusline,
-- perhaps by using this:
-- https://yazi-rs.github.io/docs/plugins/utils/#ps.sub
-- and by using ui.render() method
-- See also:
-- https://github.com/sxyazi/yazi/pull/1903
-- https://yazi-rs.github.io/docs/dds/#kinds
-- https://github.com/sxyazi/yazi/pull/2210
-- https://github.com/imsi32/yatline.yazi
-- TODO: Add options to choose displaying in popup box or in statusline
-- TODO: Add spotter and previewer widget to support simpler displaying
-- TODO: Remove note [1] and [2] after add them to the setup
-- configuration
-- Get selected paths {{{1
local get_selected_paths = ya.sync(function(state)
local result = {}
if cx and cx.active and cx.active.selected then
for _, url in pairs(cx.active.selected) do
result[#result + 1] = url
end
end
end
return paths
return result
end)
-- Function to get total size from output
-- Unix use `du`, Windows use PowerShell
-- }}}1
-- Get current working directory in sync context {{{1
local get_cwd = ya.sync(function(state)
if cx and cx.active and cx.active.current and cx.active.current.cwd then
return cx.active.current.cwd
end
return nil
end)
-- }}}1
-- Function to get paths of selected files or current directory {{{1
--- @param selected table Table of selected urls
--- @return paths table Table of selected urls
local function get_paths(selected)
-- If no files are selected, get current directory
if #selected == 0 then
local paths = {}
-- Try fs.cwd() first (async, optimized for slow devices)
local cwd, err = fs.cwd()
if cwd then
paths[1] = cwd
else
-- Fallback to cx.active.current.cwd (via sync block)
local sync_cwd = get_cwd()
if sync_cwd then
paths[1] = sync_cwd
else
ya.notify {
title = "What size",
content = "Cannot get current working directory: " .. (err or "unknown error"),
timeout = 5,
level = "error",
}
end
end
return paths
else
-- This variable is a table of urls already
return selected
end
end
-- }}}1
-- Function to get total size using Yazi's fs.calc_size API {{{1
-- See: https://github.com/sxyazi/yazi/pull/2695
-- See: https://github.com/sxyazi/yazi/blob/main/yazi-plugin/preset/plugins/folder.lua
local function get_total_size(items)
local is_windows = package.config:sub(1,1) == '\\'
if is_windows then
local total = 0
for _, path in ipairs(items) do
path = path:gsub('"', '\\"')
local ps_cmd = string.format(
[[powershell -Command "& { $p = '%s'; if (Test-Path $p) { if ((Get-ChildItem -Path $p -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum) { (Get-ChildItem -Path $p -Recurse -Force -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum } else { (Get-Item $p).Length } } }"]],
path
)
local pipe = io.popen(ps_cmd)
local result = pipe:read("*a")
-- Debug
-- ya.notify {
-- title = "Debug Output",
-- content = result,
-- timeout = 5,
-- }
pipe:close()
local num = tonumber(result)
if num then total = total + num end
for _, url in ipairs(items) do
local it = fs.calc_size(url)
while true do
local next = it:recv()
if next then
total = total + next
else
break
end
end
end
return total
else
local arg = ya.target_os() == "macos" and "-scA" or "-scb"
-- pass args as string
local cmd = Command("du"):arg(arg)
for _, path in ipairs(items) do
cmd = cmd:arg(path)
end
local output, err = cmd:output()
if not output then
ya.err("Failed to run du: " .. err)
end
local lines = {}
for line in output.stdout:gmatch("[^\n]+") do
lines[#lines + 1] = line
end
local last_line = lines[#lines]
local size = tonumber(last_line:match("^(%d+)"))
return ya.target_os() == "macos" and size * 512 or size
end
end
-- Function to format file size
-- }}}1
-- Function to format files/folders size {{{1
local function format_size(size)
local units = { "B", "KB", "MB", "GB", "TB" }
local unit_index = 1
while size > 1024 and unit_index < #units do
size = size / 1024
unit_index = unit_index + 1
end
return string.format("%.2f %s", size, units[unit_index])
end
return {
-- as per doc: https://yazi-rs.github.io/docs/plugins/overview#functional-plugin
entry = function(_, job)
-- defaults not to use clipboard, use it only if required by the user
local clipboard = job.args.clipboard == true or job.args[1] == "--clipboard" or job.args[1] == "-c"
local items = get_paths()
local total_size = get_total_size(items)
local formatted_size = format_size(total_size)
local notification_content = "Total size: " .. formatted_size
if clipboard then
ya.clipboard(formatted_size)
notification_content = notification_content .. "\nCopied to clipboard."
local units = { "B", "KB", "MB", "GB", "TB" }
local unit_index = 1
while size > 1024 and unit_index < #units do
size = size / 1024
unit_index = unit_index + 1
end
return string.format("%.2f %s", size, units[unit_index])
end
-- }}}1
-- Generic setter for any state field {{{1
local set_state = ya.sync(function(state, field, value)
state[field] = value
end)
-- }}}1
-- Generic getter for any state field {{{1
local get_state = ya.sync(function(state, field)
return state[field] or nil
end)
-- }}}1
-- Get selecting state {{{1
local get_selected = ya.sync(function()
return (not cx.active.mode.is_visual) and (#cx.active.selected ~= 0)
end)
-- }}}1
-- Set separators {{{1
local set_separator = ya.sync(function(state, table)
if table and table.LEFT and table.RIGHT then
state.LEFT = table.LEFT
state.RIGHT = table.RIGHT
else
state.LEFT = " "
state.RIGHT = " "
end
end)
-- }}}1
-- Get separators {{{1
local get_separator = ya.sync(function(state)
return {state.LEFT, state.RIGHT}
end)
-- }}}1
-- Redraw statusline {{{1
local redraw_statusline = ya.sync(function(state)
ui.render()
end)
-- }}}1
-- Set ui line in statusline for size, clean up when no selection exists {{{1
-- @return of get_state("renewed_state") number or nil Returning -1
-- means never show the size - suitable for setup function;
-- returning 0 means the size will be shown after triggering the
-- calculation, but without unselect the selections, or it will be
-- hidden after nothing is selected; returning 1 means hidden when
-- nothing is selected as said.
local set_ui_line = function(state)
local sep_left, sep_right = table.unpack(get_separator())
ya.notify {
title = "What size",
content = notification_content,
timeout = 4,
}
end,
}
if get_state("renewed_state") == -1 then
return ""
else
if not get_selected() then
if not get_state("is_held") then
set_state("renewed_state", 1)
return ""
end
-- NOTE [1]: Set this line if DON'T want to clear the value
-- in the statusline when move the cursor, after calculating
-- with NO selection(s). Or just return ""
return ui.Span(sep_left .. get_state("size") .. sep_right)
end
if get_state("renewed_state") == 0 then
return ui.Span(sep_left .. get_state("size") .. sep_right)
else
-- NOTE [2]: Set this line if want to clear the value in the
-- statusline when move the cursor, after calculating WITH
-- selection: return ui.Span(sep_left .. get_state("size") .. sep_right)
-- or just remove after the unselection like below
return ""
end
end
end
-- }}}1
--- @since 25.12.29
return {
entry = function(self, job)
local clipboard = job.args.clipboard or job.args[1] == '-c'
local selected = get_selected_paths()
local prepend_msg
-- Keep showing the size after CWD calculation (no selections)
if #selected == 0 then
set_state("is_held", true)
prepend_msg = "Current Dir: "
else
set_state("is_held", false)
prepend_msg = "Selected: "
end
local items = get_paths(selected)
if not items or #items == 0 then
ya.notify {
title = "What size",
content = "Failed to get paths",
timeout = 5,
}
return
end
local total_size = get_total_size(items)
if not total_size then
ya.notify {
title = "What size",
content = "Failed to calculate size",
timeout = 5,
}
return
end
local formatted_size = format_size(total_size)
local notification_content = prepend_msg .. formatted_size
if clipboard then
ya.clipboard(formatted_size)
notification_content = notification_content .. "\nCopied to clipboard."
end
ya.notify {
title = "What size",
content = notification_content,
timeout = 4,
}
set_state("size", formatted_size)
set_state("renewed_state", 0)
redraw_statusline()
end,
setup = function(state, opts)
opts = opts or {}
local priority = opts.priority or 400
set_separator(opts)
set_state("renewed_state", -1)
if Status and type(Status.children_add) == "function" then
Status:children_add(set_ui_line, priority, Status.RIGHT)
else
ya.err("Failed to initialize status bar: Status or children_add not available")
end
end,
}

View File

@@ -1,4 +1,5 @@
# yatline.yazi
The first Yazi plugin for customizing both header-line and status-line.
![yatline](https://github.com/user-attachments/assets/61013ec8-7fd9-42df-a9f4-f254663871fe)
@@ -7,13 +8,16 @@ The first Yazi plugin for customizing both header-line and status-line.
> 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)

File diff suppressed because it is too large Load Diff