mirror of
https://github.com/kristoferssolo/solorice.git
synced 2026-03-18 08:09:40 +00:00
Update 2026-03-13
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`:
|
||||
|
||||
|
||||
283
config/yazi/plugins/mediainfo.yazi/adobe.lua
Normal file
283
config/yazi/plugins/mediainfo.yazi/adobe.lua
Normal 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
|
||||
314
config/yazi/plugins/mediainfo.yazi/audio.lua
Normal file
314
config/yazi/plugins/mediainfo.yazi/audio.lua
Normal 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
|
||||
57
config/yazi/plugins/mediainfo.yazi/const.lua
Normal file
57
config/yazi/plugins/mediainfo.yazi/const.lua
Normal 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
|
||||
234
config/yazi/plugins/mediainfo.yazi/image.lua
Normal file
234
config/yazi/plugins/mediainfo.yazi/image.lua
Normal 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
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
41
config/yazi/plugins/mediainfo.yazi/utils.lua
Normal file
41
config/yazi/plugins/mediainfo.yazi/utils.lua
Normal 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
|
||||
222
config/yazi/plugins/mediainfo.yazi/video.lua
Normal file
222
config/yazi/plugins/mediainfo.yazi/video.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
|
||||
66
config/yazi/plugins/mount.yazi/cross.lua
Normal file
66
config/yazi/plugins/mount.yazi/cross.lua
Normal 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
|
||||
@@ -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
|
||||
|
||||
54
config/yazi/plugins/mount.yazi/sudo.lua
Normal file
54
config/yazi/plugins/mount.yazi/sudo.lua
Normal 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
|
||||
@@ -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).
|
||||
|
||||
@@ -2,6 +2,7 @@ local M = {}
|
||||
|
||||
function M:peek(job)
|
||||
local child = Command("rich")
|
||||
:env("COLUMNS", tostring(job.area.w))
|
||||
:arg({
|
||||
"-j",
|
||||
"--left",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user