Update 2026-03-13

This commit is contained in:
2026-03-13 14:57:50 +02:00
parent b97f7aaf4a
commit 75f8df8582
35 changed files with 2158 additions and 1134 deletions

View File

@@ -10,8 +10,8 @@ hash = "e02a788e5b8ae0fb47fd0193dda589cc"
[[plugin.deps]]
use = "hankertrix/augment-command"
rev = "7c12bdf"
hash = "f7e6d377e4efee567ec6d1c2355f2ca3"
rev = "681158d"
hash = "63ed5325016895306781d13690b246bf"
[[plugin.deps]]
use = "kirasok/torrent-preview"
@@ -20,8 +20,8 @@ hash = "d849ad596b8a77902e62a42403aeba40"
[[plugin.deps]]
use = "ndtoan96/ouch"
rev = "594b8a2"
hash = "c9e628fc0312d198db22ae2fa74883b"
rev = "406ce6c"
hash = "f5afc904d5106ee368c8aa2ded43bd74"
[[plugin.deps]]
use = "pirafrank/what-size"
@@ -30,38 +30,38 @@ hash = "57056b9728006881d580ccabe8154a9c"
[[plugin.deps]]
use = "yazi-rs/plugins:git"
rev = "e07bf41"
hash = "270915fa8282a19908449530ff66f7e2"
rev = "1962818"
hash = "26db011a778f261d730d4f5f8bf24b3f"
[[plugin.deps]]
use = "yazi-rs/plugins:chmod"
rev = "e07bf41"
hash = "8da0b15a97b5dfd13941d1ecc617ac7c"
rev = "1962818"
hash = "f0c8c378184d5f8abd1b095a443d336d"
[[plugin.deps]]
use = "yazi-rs/plugins:full-border"
rev = "e07bf41"
hash = "3996fc74044bc44144b323686f887e1"
rev = "1962818"
hash = "6fa6a05a81c98dd000fbca3cca6e9682"
[[plugin.deps]]
use = "yazi-rs/plugins:mount"
rev = "e07bf41"
hash = "563e4068979d1466d3dfc2e70a296947"
rev = "1962818"
hash = "91937a4a9b779eabc6983e258befdfe9"
[[plugin.deps]]
use = "yazi-rs/plugins:smart-filter"
rev = "e07bf41"
hash = "407d19bc4fb46eff5fa8ff8337644847"
rev = "1962818"
hash = "c887903a63a2ff520081b6d90a4b3392"
[[plugin.deps]]
use = "yazi-rs/plugins:diff"
rev = "e07bf41"
hash = "2f08b8249b57737e7257298a3b2a2edc"
rev = "1962818"
hash = "8b1af6b5a69797ee951f2a80ce570818"
[[plugin.deps]]
use = "AnirudhG07/rich-preview"
rev = "573b275"
hash = "c3e2871c9ef244fd181f203791f9b0d2"
rev = "7d616ad"
hash = "d64feec9761392cbc250d199ab4b8a3a"
[[plugin.deps]]
use = "macydnah/office"
@@ -70,8 +70,8 @@ hash = "5805affd3ae8adcb3c72b6997d21c0a6"
[[plugin.deps]]
use = "boydaihungst/mediainfo"
rev = "dc61636"
hash = "2fa34959353b6f1a1c33659f50e098fd"
rev = "6fbed8d"
hash = "57ae6b43c477e117802f176683b78e74"
[[plugin.deps]]
use = "iynaix/time-travel"

View File

@@ -452,10 +452,6 @@ local get_mime_type_without_prefix_template_pattern =
---@type string
local shell_variable_pattern = "%%[hs]%d?"
-- The pattern to match the bat command
---@type string
local bat_command_pattern = "%f[%a]bat%f[%A]"
-- Utility functions
-- Function to merge tables.
@@ -1415,8 +1411,8 @@ local get_path_of_hovered_item = ya.sync(function(_, quote)
-- If there is no hovered item, exit the function
if not hovered_item then return end
-- Convert the url of the hovered item to a string
local hovered_item_path = tostring(cx.active.current.hovered.url)
-- Convert the path of the hovered item to a string
local hovered_item_path = tostring(cx.active.current.hovered.url.path)
-- If the quote flag is passed,
-- then quote the path of the hovered item
@@ -1470,8 +1466,8 @@ local get_paths_of_selected_items = ya.sync(function(_, quote)
for _, item in pairs(selected_items) do
--
-- Convert the url of the item to a string
local item_path = tostring(item)
-- Convert the path of the item to a string
local item_path = tostring(item.path)
-- If the quote flag is passed,
-- then quote the path of the item
@@ -3481,11 +3477,35 @@ local function handle_create(args, config)
return execute_create(full_url, is_directory, args, config)
end
-- Function to match a binary name against a search string
---@param binary_name string The name of the binary
---@param search_string string The string to search for the binary name
---@return string binary_pattern The pattern for the binary
---@return string? binary_path The path to the binary
local function match_binary_name(binary_name, search_string)
--
-- The binary pattern
local binary_pattern = "%f[%w_%-%.].*" .. binary_name .. "%f[%W%s]"
-- Get the binary path
local binary_path = search_string:match(binary_pattern)
-- Escape the binary path if it's not nil
if binary_path ~= nil then
binary_path = escape_replacement_string(binary_path)
end
-- Return the binary pattern and the path
return binary_pattern, binary_path
end
-- Function to remove the F flag from the less command
---@param command string The shell command containing the less command
---@param less_binary_pattern string The pattern to match the less binary
---@return string command The command with the F flag removed
---@return boolean f_flag_found Whether the F flag was found
local function remove_f_flag_from_less_command(command)
local function remove_f_flag_from_less_command(command, less_binary_pattern)
--
-- Initialise the variable to store if the F flag is found
@@ -3494,9 +3514,13 @@ local function remove_f_flag_from_less_command(command)
-- Initialise the variable to store the replacement count
local replacement_count = 0
-- Initialised the modified command
local modified_command = command
-- Remove the F flag when it is passed at the start
-- of the flags given to the less command
command, replacement_count = command:gsub("(%f[%a]less%f[%A].*)%-F", "%1")
modified_command, replacement_count =
modified_command:gsub("(" .. less_binary_pattern .. ".*)%-F", "%1")
-- If the replacement count is not 0,
-- set the f_flag_found variable to true
@@ -3504,27 +3528,36 @@ local function remove_f_flag_from_less_command(command)
-- Remove the F flag when it is passed in the middle
-- or end of the flags given to the less command command
command, replacement_count =
command:gsub("(%f[%a]less%f[%A].*%-)(%a*)F(%a*)", "%1%2%3")
modified_command, replacement_count = modified_command:gsub(
"(" .. less_binary_pattern .. ".*%-)(%a*)F(%a*)",
"%1%2%3"
)
-- If the replacement count is not 0,
-- set the f_flag_found variable to true
if replacement_count ~= 0 then f_flag_found = true end
-- Return the command and whether or not the F flag was found
return command, f_flag_found
return modified_command, f_flag_found
end
-- Function to fix a command containing less.
-- All this function does is remove
-- the F flag from a command containing less.
---@param command string The shell command containing the less command
---@param less_binary_pattern string The pattern to match the less binary
---@param less_binary_path string The path to the less binary
---@return string command The fixed shell command
local function fix_shell_command_containing_less(command)
local function fix_shell_command_containing_less(
command,
less_binary_pattern,
less_binary_path
)
--
-- Remove the F flag from the given command
local fixed_command = remove_f_flag_from_less_command(command)
local fixed_command =
remove_f_flag_from_less_command(command, less_binary_pattern)
-- Get the LESS environment variable
local less_environment_variable = os.getenv("LESS")
@@ -3536,7 +3569,10 @@ local function fix_shell_command_containing_less(command)
-- Otherwise, remove the F flag from the LESS environment variable
-- and check if the F flag was found
local less_command_with_modified_env_variables, f_flag_found =
remove_f_flag_from_less_command("less " .. less_environment_variable)
remove_f_flag_from_less_command(
string.format("%s %s", less_binary_path, less_environment_variable),
less_binary_pattern
)
-- If the F flag isn't found,
-- then return the given command with the F flag removed
@@ -3544,7 +3580,7 @@ local function fix_shell_command_containing_less(command)
-- Add the less environment variable flags to the less command
fixed_command = fixed_command:gsub(
"%f[%a]less%f[%A]",
less_binary_pattern,
escape_replacement_string(less_command_with_modified_env_variables)
)
@@ -3557,13 +3593,22 @@ end
-- Function to fix the bat default pager command
---@param command string The command containing the bat default pager command
---@param bat_binary_pattern string The pattern to match the bat binary
---@param bat_binary_path string The path to the bat binary
---@return string command The fixed bat command
local function fix_shell_command_containing_bat(command)
local function fix_shell_command_containing_bat(
command,
bat_binary_pattern,
bat_binary_path
)
--
-- The pattern to match the pager argument for the bat command
local bat_pager_pattern = "(%-%-pager)%s+(%S+)"
-- The default bat pager command without the -F flag
local bat_default_pager_command_without_f_flag = "less -RX"
-- Get the pager argument for the bat command
local _, pager_argument = command:match(bat_pager_pattern)
@@ -3606,16 +3651,13 @@ local function fix_shell_command_containing_bat(command)
return modified_command
end
-- If there is no pager argument,
-- initialise the default pager command for bat without the F flag
local bat_default_pager_command_without_f_flag = "less -RX"
-- Replace the bat command with the command to use the
-- bat default pager command without the F flag
-- Replace the bat command with the command to use
-- the bat default pager command without the F flag
local modified_command = command:gsub(
bat_command_pattern,
bat_binary_pattern,
string.format(
"bat --pager '%s'",
"%s --pager '%s'",
bat_binary_path,
bat_default_pager_command_without_f_flag
),
1
@@ -3631,24 +3673,43 @@ end
local function fix_shell_command(command)
--
-- If the given command contains the bat command
if command:find(bat_command_pattern) ~= nil then
-- Get the bat binary pattern and path from the command
local bat_binary_pattern, bat_binary_path =
match_binary_name("bat", command)
-- Initialise the fixed command
local fixed_command = command
-- If the bat binary is in the command
if bat_binary_path ~= nil then
--
-- Calls the command to fix the bat command
command = fix_shell_command_containing_bat(command)
fixed_command = fix_shell_command_containing_bat(
command,
bat_binary_pattern,
bat_binary_path
)
end
-- If the given command includes the less command
if command:find("%f[%a]less%f[%A]") ~= nil then
-- Get the less binary pattern and path from the fixed command
local less_binary_pattern, less_binary_path =
match_binary_name("less", fixed_command)
-- If the less binary is in the command
if less_binary_path ~= nil then
--
-- Fix the command containing less
command = fix_shell_command_containing_less(command)
fixed_command = fix_shell_command_containing_less(
fixed_command,
less_binary_pattern,
less_binary_path
)
end
-- Return the modified command
return command
-- Return the fixed command
return fixed_command
end
-- Function to handle a shell command

View File

@@ -21,7 +21,7 @@ run = "plugin chmod"
desc = "Chmod on selected files"
```
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other commands/plugins.
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins.
## License

View File

@@ -1,4 +1,4 @@
--- @since 25.12.29
--- @since 26.1.22
local selected_or_hovered = ya.sync(function()
local tab, paths = cx.active, {}
@@ -37,7 +37,7 @@ return {
return
end
local output, err = Command("chmod"):arg(value):arg(urls):stderr(Command.PIPED):output()
local output, err = Command("chmod"):arg(value):arg(urls):output()
if not output then
fail("Failed to run chmod: %s", err)
elseif not output.status.success then

View File

@@ -21,7 +21,7 @@ run = "plugin diff"
desc = "Diff the selected with the hovered file"
```
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other commands/plugins.
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins.
## License

View File

@@ -20,7 +20,8 @@ local function setup(_, opts)
local c = self._chunks
self._chunks = {
c[1]:pad(ui.Pad.y(1)),
c[2]:pad(ui.Pad(1, c[3].w > 0 and 0 or 1, 1, c[1].w > 0 and 0 or 1)),
-- TODO: remove this compatibility hack
fs.unique and c[2]:pad(ui.Pad.y(1)) or c[2]:pad(ui.Pad(1, c[3].w > 0 and 0 or 1, 1, c[1].w > 0 and 0 or 1)),
c[3]:pad(ui.Pad.y(1)),
}

View File

@@ -1,4 +1,4 @@
--- @since 25.12.29
--- @since 26.1.22
local WINDOWS = ya.target_family() == "windows"
@@ -224,7 +224,6 @@ local function fetch(_, job)
: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)

View File

@@ -1,4 +1,5 @@
Copyright (c) 2024 Lauri Niskanen
Copyright (c) 2025 Huy Hoang
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -13,7 +13,8 @@ This is a Yazi plugin for previewing media files. The preview shows thumbnail
using `ffmpeg` if available and media metadata using `mediainfo`.
> [!IMPORTANT]
> Minimum version: yazi v25.5.31.
> Minimum version: yazi v26.1.22.
> Check it via command `yazi --debug`
## Preview
@@ -70,29 +71,48 @@ Config folder for each OS: https://yazi-rs.github.io/docs/configuration/overview
Create `.../yazi/yazi.toml` and add:
> [!IMPORTANT]
>
> For yazi (>=v25.12.29) replace `name` with `url`
```toml
[plugin]
prepend_preloaders = [
# Replace magick, image, video with mediainfo
{ mime = "{audio,video,image}/*", run = "mediainfo" },
{ mime = "application/subrip", run = "mediainfo" },
# Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above
# Adobe Photoshop is image/adobe.photoshop, already handled above
# Adobe Illustrator
{ mime = "application/postscript", run = "mediainfo" },
{ mime = "application/illustrator", run = "mediainfo" },
{ mime = "application/dvb.ait", run = "mediainfo" },
{ mime = "application/vnd.adobe.illustrator", run = "mediainfo" },
{ mime = "image/x-eps", run = "mediainfo" },
{ mime = "application/eps", run = "mediainfo" },
# Sometimes AI file is recognized as "application/pdf". Lmao.
# In this case use file extension instead:
{ url = "*.{ai,eps,ait}", run = "mediainfo" },
]
prepend_previewers = [
# Replace magick, image, video with mediainfo
{ mime = "{audio,video,image}/*", run = "mediainfo"},
{ mime = "application/subrip", run = "mediainfo" },
# Adobe Illustrator, Adobe Photoshop is image/adobe.photoshop, already handled above
# Adobe Photoshop is image/adobe.photoshop, already handled above
# Adobe Illustrator
{ mime = "application/postscript", run = "mediainfo" },
{ mime = "application/illustrator", run = "mediainfo" },
{ mime = "application/dvb.ait", run = "mediainfo" },
{ mime = "application/vnd.adobe.illustrator", run = "mediainfo" },
{ mime = "image/x-eps", run = "mediainfo" },
{ mime = "application/eps", run = "mediainfo" },
# Sometimes AI file is recognized as "application/pdf". Lmao.
# In this case use file extension instead:
{ url = "*.{ai,eps,ait}", run = "mediainfo" },
]
# There are more extensions which are supported by mediainfo.
# There are more extensions, mime types which are supported by mediainfo.
# Just add file's MIME type to `previewers`, `preloaders` above.
# https://mediaarea.net/en/MediaInfo/Support/Formats
# If it's not working, file an issue at https://github.com/boydaihungst/mediainfo.yazi/issues
# For a large file like Adobe Illustrator, Adobe Photoshop, etc
# you may need to increase the memory limit if no image is rendered.
@@ -104,7 +124,7 @@ Create `.../yazi/yazi.toml` and add:
## Custom theme
Using the same style with spotter. [Read more](https://github.com/sxyazi/yazi/pull/2391)
Using the same style with spotter windows. [Read more](https://github.com/sxyazi/yazi/pull/2391)
Edit or add `yazi/theme.toml`:

View File

@@ -0,0 +1,283 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
local function image_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = utils.get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
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 or not output then
return 0
end
layer_count = 0
for line in output.stdout:gmatch("[^\r\n]+") do
if line:match("%S") then
layer_count = layer_count + 1
end
end
utils.set_state("f" .. tostring(cache), layer_count)
return layer_count
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata)
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.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
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not const.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 line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not hide_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if
image_layer_count(job)
< (
1
+ math.floor(
math.max(
0,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
)
then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))),
})
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.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:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
local layer_index = 0
local units = utils.get_state(const.STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, math.abs(job.skip / units)))
if layer_index + 1 > max_layer then
layer_index = math.max(0, max_layer - 1)
end
end
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url) .. "[" .. tostring(
layer_index
) .. "]",
"-auto-orient",
"-strip",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
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 "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M

View File

@@ -0,0 +1,314 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
local function cover_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = utils.get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
local output, err = Command("ffprobe"):arg({
"-v",
"error",
"-select_streams",
"v",
"-show_entries",
"stream=index:stream_disposition=attached_pic",
"-of",
"json",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
}):output()
if err or not output then
return 0
end
layer_count = 0
local data = ya.json_decode(output.stdout)
layer_count = #data.streams
utils.set_state("f" .. tostring(cache), layer_count)
return layer_count
end
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache(job)
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata)
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.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
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not const.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 line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not hide_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if
cover_layer_count(job)
< (
1
+ math.floor(
math.max(
0,
utils.get_state(const.STATE_KEY.units)
and (math.abs(job.skip / utils.get_state(const.STATE_KEY.units)))
or 0
)
)
)
then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))),
})
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.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:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local cache_img_url = ya.file_cache(job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cover_index = 0
local units = utils.get_state(const.STATE_KEY.units)
if units ~= nil then
local max_layer = cover_layer_count(job)
cover_index = math.floor(math.max(0, math.abs(job.skip / units)))
if cover_index + 1 > max_layer then
cover_index = math.max(0, max_layer - 1)
end
end
local qv = 31 - math.floor(rt.preview.image_quality * 0.3)
local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({
"-v",
"error",
"-threads",
1,
"-i",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-map",
string.format("0:v:%d?", cover_index),
"-an",
"-sn",
"-dn",
"-vframes",
1,
"-q:v",
qv,
"-vf",
string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2),
"-f",
"image2",
"-y",
tostring(cache_img_url),
}):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 ~= ""
and not audio_preload_output.stderr:find("Output file does not contain any stream")
) or audio_preload_err
then
ya.dbg("mediainfo", audio_preload_err)
ya.dbg("mediainfo", audio_preload_output.stderr)
err_msg = err_msg
.. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
else
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")
.with_limit()
:arg({
"-size",
"1x1",
"canvas:none",
string.format("PNG32:%s", cache_img_url),
})
:output()
if (audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "") or audio_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg
.. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "magick", "magick")
end
end
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
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 "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M

View File

@@ -0,0 +1,57 @@
--- @since 26.1.22
local M = {}
M.skip_labels = {
["Complete name"] = true,
["CompleteName_Last"] = true,
["Unique ID"] = true,
["File size"] = true,
["Format/Info"] = true,
["Codec ID/Info"] = true,
["MD5 of the unencoded content"] = true,
}
M.ENTRY_ACTION = {
toggle_metadata = "toggle-metadata",
}
M.STATE_KEY = {
units = "units",
hide_metadata = "hide_metadata",
prev_metadata_area = "prev_metadata_area",
prev_image_height = "prev_image_height",
last_valid_mediainfo_skip = "last_valid_mediainfo_skip",
}
M.magick_image_mimes = {
avif = true,
hei = true,
heic = true,
heif = true,
["heif-sequence"] = true,
["heic-sequence"] = true,
jxl = true,
tiff = true,
xml = true,
-- ["svg+xml"] = true,
["canon-cr2"] = true,
}
M.seekable_mimes = {
-- NOTE: Adobe illustrator photoshop mimetypes
["application/postscript"] = true,
["application/dvb.ait"] = true,
["application/illustrator"] = true,
["application/vnd.adobe.illustrator"] = true,
["image/x-eps"] = true,
["application/eps"] = true,
["application/pdf"] = true,
["image/adobe.photoshop"] = true,
}
M.suffix = "_mediainfo"
M.SHELL = os.getenv("SHELL") or ""
return M

View File

@@ -0,0 +1,234 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = 0,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata)
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.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
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not const.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 line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
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
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not hide_metadata and EOF_mediainfo and #lines == 0 and job.skip > 0 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
end
utils.force_render()
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))),
})
end
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.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:preload(job)
local cmd = "mediainfo"
local err_msg = ""
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
-- NOTE: Preload image
local mime = job.mime:match(".*/(.*)$")
local is_svg = mime == "svg+xml"
local is_magick = const.magick_image_mimes[mime]
local no_skip_job = { skip = 0, file = job.file, args = job.args, area = job.area }
local cache_img_url = ya.file_cache(no_skip_job)
local cache_img_url_cha = cache_img_url and fs.cha(cache_img_url)
-- NOTE: Only generate preview image when cache image is not exist
if not cache_img_url_cha or cache_img_url_cha.len <= 0 then
local cache_img_status, image_preload_err
if not is_valid_utf8_path then
-- NOTE: Case not valid utf8 path, use trick to generate preview image
if is_svg then
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = type(fs.unique) == "function" and fs.unique("file", cache_img_url_tmp)
or fs.unique_name(cache_img_url_tmp)
-- svg under invalid utf8 path
cache_img_status, image_preload_err = require("magick")
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
end
else
-- NOTE: Case valid utf8 path, use image, svg, or magick module
local image_module = is_svg and "svg" or (is_magick and "magick" or "image")
cache_img_status, image_preload_err = require(image_module):preload(no_skip_job)
end
if not cache_img_status and image_preload_err then
ya.dbg("mediainfo", image_preload_err)
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
if is_valid_utf8_path then
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 "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M

View File

@@ -1,472 +1,79 @@
--- @since 25.5.31
local skip_labels = {
["Complete name"] = true,
["CompleteName_Last"] = true,
["Unique ID"] = true,
["File size"] = true,
["Format/Info"] = true,
["Codec ID/Info"] = true,
["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,
heic = true,
heif = true,
["heif-sequence"] = true,
["heic-sequence"] = true,
jxl = true,
tiff = true,
xml = true,
["svg+xml"] = true,
["canon-cr2"] = true,
}
local seekable_mimes = {
["application/postscript"] = true,
["image/adobe.photoshop"] = true,
}
--- @since 26.1.22
local M = {}
local suffix = "_mediainfo"
local SHELL = os.getenv("SHELL") or ""
local function is_valid_utf8(str)
return utf8.len(str) ~= nil
end
local function path_quote(path)
if not path or tostring(path) == "" then
return path
end
local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'"
return result
end
local function read_mediainfo_cached_file(file_path)
-- Open the file in read mode
local file = io.open(file_path, "r")
if file then
-- Read the entire file content
local content = file:read("*all")
file:close()
return content
end
end
local set_state = ya.sync(function(state, key, value)
state[key] = value
end)
local get_state = ya.sync(function(state, key)
return state[key]
end)
local force_render = ya.sync(function(_, _)
(ui.render or ya.render)()
end)
local function image_layer_count(job)
local cache = ya.file_cache({ file = job.file, skip = 0 })
if not cache then
return 0
end
local layer_count = get_state("f" .. tostring(cache))
if layer_count then
return layer_count
end
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
layer_count = 0
for line in output.stdout:gmatch("[^\r\n]+") do
if line:match("%S") then
layer_count = layer_count + 1
end
end
set_state("f" .. tostring(cache), layer_count)
return layer_count
end
local const = require(".const")
local utils = require(".utils")
local adobe = require(".adobe")
local audio = require(".audio")
local image = require(".image")
local video = require(".video")
function M:peek(job)
-- debounce peek
local start = os.clock()
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
-- Need mime to decide which module to use
if not job.mime then
return
end
local is_video = string.find(job.mime, "^video/")
local is_audio = string.find(job.mime, "^audio/")
local is_image = string.find(job.mime, "^image/")
local is_seekable = seekable_mimes[job.mime] or is_video
local cache_img_url = (is_audio or is_image) and cache_img_url_no_skip
local is_adobe = const.seekable_mimes[job.mime]
if is_seekable then
cache_img_url = ya.file_cache(job)
if is_adobe then
return adobe:peek(job)
elseif is_image then
return image:peek(job)
elseif is_video then
return video:peek(job)
elseif is_audio then
return audio:peek(job)
end
local preload_status, preload_err = self:preload(job)
if not preload_status then
return
end
ya.sleep(math.max(0, rt.preview.image_delay / 1000 + start - os.clock()))
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 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
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")),
})
end
elseif str ~= "General" then
line = ui.Line({ ui.Span(str):style(th.spot.title or ui.Style():fg("green")) })
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)
end
if (last_line + line_height) >= job.skip + limit then
last_line = job.skip + limit
break
end
last_line = last_line + line_height
end
end
end
mediainfo_height = math.min(limit, last_line)
end
if
(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(STATE_KEY.units) and (job.skip / get_state(STATE_KEY.units)) or 0)
))
)
)
then
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(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
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
if is_audio and image_height == 1 then
local info = ya.image_info(cache_img_url)
if not info or (info.w == 1 and info.h == 1) then
image_height = 0
end
end
-- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration
if is_video and not rendered_img_rect then
image_height = math.max(job.area.h - mediainfo_height, 0)
end
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
: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(STATE_KEY.units, job.units)
utils.set_state(const.STATE_KEY.units, job.units)
ya.emit("peek", {
math.max(0, cx.active.preview.skip + job.units),
only_if = job.file.url,
})
end
end
function M:preload(job)
local cache_img_url = ya.file_cache({ file = job.file, skip = 0 })
if not cache_img_url then
ya.dbg("mediainfo", "Can't access yazi cache folder")
return true
end
local cache_mediainfo_url = Url(tostring(cache_img_url) .. suffix)
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.path or job.file.cache or job.file.url))
-- video mimetype
if job.mime then
if string.find(job.mime, "^video/") then
local cache_img_status, video_preload_err = require("video"):preload(job)
if not cache_img_status and video_preload_err then
err_msg = err_msg
.. string.format("Failed to start `%s`, Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
end
-- audo and image mimetype
elseif cache_img_url and (not cache_img_url_cha or cache_img_url_cha.len <= 0) then
-- audio
if string.find(job.mime, "^audio/") then
local qv = 31 - math.floor(rt.preview.image_quality * 0.3)
local audio_preload_output, audio_preload_err = Command("ffmpeg"):arg({
"-v",
"error",
"-threads",
1,
"-an",
"-sn",
"-dn",
"-i",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-vframes",
1,
"-q:v",
qv,
"-vf",
string.format("scale=-1:'min(%d,ih)':flags=fast_bilinear", rt.preview.max_height / 2),
"-f",
"image2",
"-y",
tostring(cache_img_url),
}):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 ~= ""
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)
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")
.with_limit()
:arg({
"-size",
"1x1",
"canvas:none",
string.format("PNG32:%s", cache_img_url),
})
:output()
if
(audio_preload_output.stderr ~= nil and audio_preload_output.stderr ~= "")
or audio_preload_err
then
err_msg = err_msg
.. string.format(
"Failed to start `%s`, Do you have `%s` installed?\n",
"magick",
"magick"
)
end
end
end
-- 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_ok, magick_plugin = pcall(require, "magick")
local mime = job.mime:match(".*/(.*)$")
if not job.mime then
return false
end
local is_video = string.find(job.mime, "^video/")
local is_audio = string.find(job.mime, "^audio/")
local is_image = string.find(job.mime, "^image/")
local is_adobe = const.seekable_mimes[job.mime]
local image_plugin = magick_image_mimes[mime]
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(STATE_KEY.units)
if units ~= nil then
local max_layer = image_layer_count(job)
layer_index = math.floor(math.max(0, job.skip / units))
if layer_index + 1 > max_layer then
layer_index = max_layer - 1
end
end
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp)
cache_img_status, image_preload_err = magick_plugin
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url)
.. "["
.. tostring(layer_index)
.. "]",
"-auto-orient",
"-strip",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
elseif mime == "svg+xml" and not is_valid_utf8_path then
local cache_img_url_tmp = Url(cache_img_url .. ".tmp")
if fs.cha(cache_img_url_tmp) then
fs.remove("file", cache_img_url_tmp)
end
local tmp_file_path, _ = fs.unique_name(cache_img_url_tmp)
-- svg under invalid utf8 path
cache_img_status, image_preload_err = magick_plugin
.with_limit()
:arg({
"-background",
"none",
tostring(job.file.path or job.file.cache or job.file.url.path or job.file.url),
"-auto-orient",
"-strip",
"-flatten",
"-resize",
string.format("%dx%d>", rt.preview.max_width, rt.preview.max_height),
"-quality",
rt.preview.image_quality,
string.format("PNG32:%s", tostring(tmp_file_path)),
})
:status()
if cache_img_status then
os.rename(tostring(tmp_file_path), tostring(cache_img_url))
end
else
-- other image
local no_skip_job = { skip = 0, file = job.file, args = {} }
cache_img_status, image_preload_err = image_plugin:preload(no_skip_job)
end
if not cache_img_status then
err_msg = err_msg .. (image_preload_err and (tostring(image_preload_err)) or "")
end
end
end
if is_adobe then
return adobe:preload(job)
elseif is_image then
return image:preload(job)
elseif is_video then
return video:preload(job)
elseif is_audio then
return audio:preload(job)
end
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local cmd = "mediainfo"
local output, err
if is_valid_utf8_path then
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.path or job.file.cache or (job.file.url.path or job.file.url).parent)
.. " && "
.. cmd
.. " "
.. 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)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
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))
if action == const.ENTRY_ACTION.toggle_metadata then
utils.set_state(const.STATE_KEY.hide_metadata, not utils.get_state(const.STATE_KEY.hide_metadata))
ya.emit("peek", {
force = true,
})

View File

@@ -0,0 +1,41 @@
--- @since 26.1.22
local M = {}
function M.is_valid_utf8(str)
return utf8.len(str) ~= nil
end
function M.path_quote(path)
if not path or tostring(path) == "" then
return path
end
local result = "'" .. string.gsub(tostring(path), "'", "'\\''") .. "'"
return result
end
function M.read_mediainfo_cached_file(file_path)
-- Open the file in read mode
local file = io.open(file_path, "r")
if file then
-- Read the entire file content
local content = file:read("*all")
file:close()
return content
end
end
M.force_render = ya.sync(function(_, _)
(ui.render or ya.render)()
end)
M.set_state = ya.sync(function(state, key, value)
state[key] = value
end)
M.get_state = ya.sync(function(state, key)
return state[key]
end)
return M

View File

@@ -0,0 +1,222 @@
--- @since 26.1.22
local M = {}
local const = require(".const")
local utils = require(".utils")
function M:peek(job)
local preload_status, preload_err = self:preload(job)
-- Stop if preload failed
if not preload_status then
return
end
local cache_img_url = ya.file_cache({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
local cache_img_url_no_skip = ya.file_cache({ file = job.file, skip = 0 })
local hide_metadata = utils.get_state(const.STATE_KEY.hide_metadata)
local mediainfo_job_skip = job.skip
::recalc_mediainfo_job_skip::
local mediainfo_height = 0
local lines = {}
local limit = job.area.h
local last_line = 0
local EOF_mediainfo = true
local is_wrap = rt.preview.wrap == "yes" or rt.preview.wrap == ui.Wrap.YES
if not hide_metadata then
local cache_mediainfo_path = tostring(cache_img_url_no_skip) .. const.suffix
local output = utils.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
output = utils.read_mediainfo_cached_file(cache_mediainfo_path)
end
output = output:gsub("\n+$", "")
local iter = output:gmatch("[^\n]*")
local str = iter()
while str ~= nil do
local next_str = iter()
local label, value = str:match("(.*[^ ]) +: (.*)")
local line
if label then
if not const.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 line then
local line_height = ui.height
and ui.height(str, { width = max_width, ansi = true, wrap = rt.preview.wrap })
or (math.max(1, is_wrap and math.ceil(ui.width(line) / max_width) or 1))
if next_str == nil and line_height == 1 then
EOF_mediainfo = true
end
if (last_line + line_height) > mediainfo_job_skip then
table.insert(lines, line)
end
if (last_line + line_height) >= mediainfo_job_skip + limit then
last_line = mediainfo_job_skip + limit
EOF_mediainfo = false
break
end
last_line = last_line + line_height
end
str = next_str
end
end
mediainfo_height = math.min(limit, last_line)
end
if not hide_metadata then
if EOF_mediainfo and #lines == 0 and mediainfo_job_skip > 0 then
if job.skip > 90 then
ya.emit("peek", {
math.max(0, (job.skip - (utils.get_state(const.STATE_KEY.units) or 0))),
only_if = job.file.url,
upper_bound = true,
})
return
else
-- NOTE: Recalculate mediainfo using cached latest valid skip value when reach the end of mediainfo output
local last_valid_mediainfo_skip = utils.get_state(const.STATE_KEY.last_valid_mediainfo_skip)
mediainfo_job_skip = last_valid_mediainfo_skip
and last_valid_mediainfo_skip[tostring(cache_img_url_no_skip)]
or math.max(0, mediainfo_job_skip - (utils.get_state(const.STATE_KEY.units) or 0))
goto recalc_mediainfo_job_skip
end
else
utils.set_state(
const.STATE_KEY.last_valid_mediainfo_skip,
{ [tostring(cache_img_url_no_skip)] = mediainfo_job_skip }
)
end
end
-- NOTE: Hacky way to prevent image overlap with old metadata area
if utils.get_state(const.STATE_KEY.prev_metadata_area) then
ya.preview_widget(job, {
ui.Clear(ui.Rect(utils.get_state(const.STATE_KEY.prev_metadata_area))),
})
end
utils.force_render()
local rendered_img_rect = cache_img_url
and fs.cha(cache_img_url)
and ya.image_show(
cache_img_url,
ui.Rect({
x = job.area.x,
y = job.area.y,
w = job.area.w,
h = mediainfo_height > 0 and math.max(job.area.h - mediainfo_height, job.area.h / 2) or job.area.h,
})
)
or nil
local image_height = rendered_img_rect and rendered_img_rect.h or 0
-- NOTE: Workaround case video.lua doesn't doesn't generate preview image because of `skip` overflow video duration
if not rendered_img_rect then
local prev_image_height = utils.get_state(const.STATE_KEY.prev_image_height)
image_height = prev_image_height and prev_image_height[tostring(cache_img_url_no_skip)] or 0
else
utils.set_state(const.STATE_KEY.prev_image_height, { [tostring(cache_img_url_no_skip)] = image_height })
end
-- Handle image preload error
if preload_err then
table.insert(lines, ui.Line(tostring(preload_err)):style(th.spot.title or ui.Style():fg("red")))
end
ya.preview_widget(job, {
ui.Text(lines)
:area(ui.Rect({
x = job.area.x,
y = job.area.y + image_height,
w = job.area.w,
h = job.area.h - image_height,
}))
:wrap(is_wrap and ui.Wrap.YES or ui.Wrap.NO),
})
-- NOTE: Hacky way to prevent image overlap with old metadata area
utils.set_state(const.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:preload(job)
local cmd = "mediainfo"
local err_msg = ""
-- NOTE: Preload image from video
local cache_img_status, video_preload_err = require("video"):preload({
skip = job.skip > 90 and 90 or job.skip,
args = job.args,
file = job.file,
area = job.area,
})
if not cache_img_status and video_preload_err then
ya.dbg("mediainfo", video_preload_err)
err_msg = err_msg .. string.format("Failed to start `%s`.\n Do you have `%s` installed?\n", "ffmpeg", "ffmpeg")
end
-- NOTE: Get mediainfo and save to cache folder
local cache_mediainfo_url = Url(tostring(ya.file_cache({ file = job.file, skip = 0 })) .. const.suffix)
local cache_mediainfo_cha = fs.cha(cache_mediainfo_url)
-- Case peek function called preload to refetch mediainfo
if cache_mediainfo_cha and not job.args.force_reload_mediainfo then
return true, err_msg ~= "" and ("Error: " .. err_msg) or nil
end
local output, err
local is_valid_utf8_path = utils.is_valid_utf8(tostring(job.file.path or job.file.cache or job.file.url))
if is_valid_utf8_path then
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 "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url.path or job.file.url).parent))
.. " && "
.. cmd
.. " "
.. utils.path_quote(tostring((job.file.path or job.file.cache or job.file.url).name))
output, err = Command(const.SHELL):arg({ "-c", cmd }):output()
end
if err then
ya.dbg("mediainfo", tostring(err))
err_msg = err_msg .. string.format("Failed to start `%s`. \n Do you have `%s` installed?\n", cmd, cmd)
end
return fs.write(
cache_mediainfo_url,
(err_msg ~= "" and ("Error: " .. err_msg) or "") .. (output and output.stdout or "")
)
end
return M

View File

@@ -25,7 +25,7 @@ on = "M"
run = "plugin mount"
```
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other commands/plugins.
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins.
## Actions

View File

@@ -0,0 +1,66 @@
local M = {}
--- @param type "mount"|"unmount"|"eject"
--- @param partition table
function M.operate(type, partition)
if not partition then
return
elseif not partition.sub then
return -- TODO: mount/unmount main disk
end
local cmd, output, err
if ya.target_os() == "macos" then
cmd, output, err = "diskutil", M.diskutil(type, partition.src)
elseif ya.target_os() == "linux" then
if type == "eject" and partition.src:match("^/dev/sr%d+") then
M.udisksctl("unmount", partition.src)
cmd, output, err = "eject", M.eject(partition.src)
elseif type == "eject" then
M.udisksctl("unmount", partition.src)
cmd, output, err = "udisksctl", M.udisksctl("power-off", partition.src)
else
cmd, output, err = "udisksctl", M.udisksctl(type, partition.src)
end
end
if not cmd then
M.fail("mount.yazi is not currently supported on your platform")
elseif not output then
M.fail("Failed to spawn `%s`: %s", cmd, err)
elseif not output.status.success then
M.fail("Failed to %s `%s`: %s", type, partition.src, output.stderr)
end
end
--- @param type "mount"|"unmount"|"eject"
--- @param src string
--- @return Output? output
--- @return Error? err
function M.diskutil(type, src) return Command("diskutil"):arg({ type, src }):output() end
--- @param type "mount"|"unmount"|"power-off"
--- @param src string
--- @return Output? output
--- @return Error? err
function M.udisksctl(type, src)
local args = { type, "-b", src, "--no-user-interaction" }
local output, err = Command("udisksctl"):arg(args):output()
if not output or err then
return nil, err
elseif output.stderr:find("org.freedesktop.UDisks2.Error.NotAuthorizedCanObtain", 1, true) then
return require(".sudo").run_with_sudo("udisksctl", args)
else
return output
end
end
--- @param src string
--- @return Output? output
--- @return Error? err
function M.eject(src) return Command("eject"):arg({ "--traytoggle", src }):output() end
function M.fail(...) ya.notify { title = "Mount", content = string.format(...), timeout = 10, level = "error" } end
return M

View File

@@ -129,11 +129,11 @@ function M:entry(job)
if run == "quit" then
break
elseif run == "mount" then
self.operate("mount")
require(".cross").operate("mount", active_partition())
elseif run == "unmount" then
self.operate("unmount")
require(".cross").operate("unmount", active_partition())
elseif run == "eject" then
self.operate("eject")
require(".cross").operate("eject", active_partition())
end
until not run
end
@@ -249,48 +249,6 @@ function M.fillin(tbl)
return tbl
end
function M.operate(type)
local active = active_partition()
if not active then
return
elseif not active.sub then
return -- TODO: mount/unmount main disk
end
local cmd
if ya.target_os() == "macos" then
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()
cmd = Command("eject"):arg { "--traytoggle", active.src }
elseif type == "eject" then
Command("udisksctl"):arg({ "unmount", "-b", active.src }):status()
cmd = Command("udisksctl"):arg { "power-off", "-b", active.src }
else
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
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
end
function M.fail(...) ya.notify { title = "Mount", content = string.format(...), timeout = 10, level = "error" } end
function M:click() end
function M:scroll() end

View File

@@ -0,0 +1,54 @@
local M = {}
--- Verify if `sudo` is already authenticated
--- @return boolean
--- @return Error?
function M.sudo_already()
local status, err = Command("sudo"):arg({ "--validate", "--non-interactive" }):status()
return status and status.success or false, err
end
--- Run a program with `sudo` privilege
--- @param program string
--- @param args table
--- @return Output? output
--- @return Error? err
function M.run_with_sudo(program, args)
local cmd = Command("sudo")
:arg({ "--stdin", "--user", "#" .. ya.uid(), "--", program })
:arg(args)
:stdin(Command.PIPED)
:stdout(Command.PIPED)
:stderr(Command.PIPED)
if M.sudo_already() then
return cmd:output()
end
local value, event = ya.input {
pos = { "top-center", y = 3, w = 40 },
title = string.format("Password for `sudo %s`:", program),
obscure = true,
}
if not value or event ~= 1 then
return nil, Err("Sudo password input cancelled")
end
local child, err = cmd:spawn()
if not child or err then
return nil, err
end
child:write_all(value .. "\n")
child:flush()
local output, err = child:wait_with_output()
if not output or err then
return nil, err
elseif output.status.success or M.sudo_already() then
return output
else
return nil, Err("Incorrect sudo password")
end
end
return M

View File

@@ -1,3 +1,35 @@
<div align="right">
<details>
<summary >🌐 Language</summary>
<div>
<div align="center">
<a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=en">English</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=zh-CN">简体中文</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=zh-TW">繁體中文</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ja">日本語</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ko">한국어</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=hi">हिन्दी</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=th">ไทย</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=fr">Français</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=de">Deutsch</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=es">Español</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=it">Italiano</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ru">Русский</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=pt">Português</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=nl">Nederlands</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=pl">Polski</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=ar">العربية</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=fa">فارسی</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=tr">Türkçe</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=vi">Tiếng Việt</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=id">Bahasa Indonesia</a>
| <a href="https://openaitx.github.io/view.html?user=ndtoan96&project=ouch.yazi&lang=as">অসমীয়া</
</div>
</div>
</details>
</div>
# ouch.yazi
[ouch](https://github.com/ouch-org/ouch) plugin for [Yazi](https://github.com/sxyazi/yazi).

View File

@@ -2,6 +2,7 @@ local M = {}
function M:peek(job)
local child = Command("rich")
:env("COLUMNS", tostring(job.area.w))
:arg({
"-j",
"--left",

View File

@@ -21,7 +21,7 @@ run = "plugin smart-filter"
desc = "Smart filter"
```
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other commands/plugins.
Note that, the keybindings above are just examples, please tune them up as needed to ensure they don't conflict with your other actions/plugins.
## License