refactoring

This commit is contained in:
Matt Nish-Lapidus 2025-03-21 16:50:54 -04:00
parent 74ae1150a3
commit 69400c1aa3
142 changed files with 11 additions and 70 deletions

View file

@ -0,0 +1,19 @@
Copyright (c) 2024 boydaihungst
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,102 @@
# file-extra-metadata
<!--toc:start-->
- [file-extra-metadata](#file-extra-metadata)
- [Preview](#preview)
- [Before:](#before)
- [After:](#after)
- [Requirements](#requirements)
- [Installation](#installation)
- [For developer](#for-developer)
<!--toc:end-->
This is a Yazi plugin that replaces the default file previewer and spotter with extra information.
## Preview
### Before:
- Previewer
![Before preview](statics/2024-11-17-12-06-24.png)
- Spotter (yazi >= v0.4 after 21/11/2024)
![Before spot](statics/2024-11-21-04-19-01.png)
### After:
- Previewer
![After previewer](statics/2024-11-21-05-27-48.png)
- Spotter (yazi >= v0.4 after 21/11/2024)
![After spotter](statics/2024-11-21-05-29-50.png)
## Requirements
- [yazi >=0.4](https://github.com/sxyazi/yazi)
- Tested on Linux. For MacOS, Windows: some fields will shows empty values.
## Installation
Install the plugin:
```sh
ya pack -a boydaihungst/file-extra-metadata
```
Add spotter keybind, makes sure not conflict with other `<Tab>` keybind in
`manager` section:
```toml
[manager]
keymap = [
# ...
# Spotting
{ on = "<Tab>", run = "spot", desc = "Spot hovered file" },
]
```
Create `~/.config/yazi/yazi.toml` and add:
```toml
[plugin]
append_previewers = [
{ name = "*", run = "file-extra-metadata" },
]
# yazi v0.4 after 21/11/2024
# Setup keybind for spotter: https://github.com/sxyazi/yazi/pull/1802
append_spotters = [
{ name = "*", run = "file-extra-metadata" },
]
```
or
```toml
[plugin]
previewers = [
# ... the rest
# disable default file plugin { name = "*", run = "file" },
{ name = "*", run = "file-extra-metadata" },
]
# yazi v0.4 after 21/11/2024
# Setup keybind for spotter: https://github.com/sxyazi/yazi/pull/1802
spotters = [
# ... the rest
# Fallback
# { name = "*", run = "file" },
{ name = "*", run = "file-extra-metadata" },
]
```
## For developer
If you want to compile this with other spotter/previewer:
```lua
require("file-extra-metadata"):render_table(job, { show_plugins_section = true })
```

View file

@ -0,0 +1,489 @@
local M = {}
local function permission(file)
local h = file
if not h then
return ""
end
local perm = h.cha:perm()
if not perm then
return ""
end
local spans = ""
for i = 1, #perm do
local c = perm:sub(i, i)
spans = spans .. c
end
return spans
end
local function link_count(file)
local h = file
if h == nil or ya.target_family() ~= "unix" then
return ""
end
return h.cha.nlink
end
local function owner_group(file)
local h = file
if h == nil or ya.target_family() ~= "unix" then
return ""
end
return (ya.user_name(h.cha.uid) or tostring(h.cha.uid)) .. "/" .. (ya.group_name(h.cha.gid) or tostring(h.cha.gid))
end
local file_size_and_folder_childs = function(file)
local h = file
if not h or h.cha.is_link then
return ""
end
return h.cha.len and ya.readable_size(h.cha.len) or ""
end
--- get file timestamp
---@param file any
---@param type "mtime" | "atime" | "btime"
---@return any
local function fileTimestamp(file, type)
local h = file
if not h or h.cha.is_link then
return ""
end
local time = math.floor(h.cha[type] or 0)
if time == 0 then
return ""
else
return os.date("%Y-%m-%d %H:%M", time)
end
end
-- Function to split a string by spaces (considering multiple spaces as one delimiter)
local function split_by_whitespace(input)
local result = {}
for word in string.gmatch(input, "%S+") do
table.insert(result, word)
end
return result
end
local function get_filesystem_extra(file)
local result = {
filesystem = "",
device = "",
type = "",
used_space = "",
avail_space = "",
total_space = "",
used_space_percent = "",
avail_space_percent = "",
error = nil,
}
local h = file
local file_url = tostring(h.url)
if not h or ya.target_family() ~= "unix" then
return result
end
local output, _ = Command("tail")
:args({ "-n", "-1" })
:stdin(Command("df"):args({ "-P", "-T", "-h", file_url }):stdout(Command.PIPED):spawn():take_stdout())
:stdout(Command.PIPED)
:output()
if output then
-- Splitting the data
local parts = split_by_whitespace(output.stdout)
-- Display the result
for i, part in ipairs(parts) do
if i == 1 then
result.filesystem = part
elseif i == 2 then
result.device = part
elseif i == 3 then
result.total_space = part
elseif i == 4 then
result.used_space = part
elseif i == 5 then
result.avail_space = part
elseif i == 6 then
result.used_space_percent = part
result.avail_space_percent = 100 - tonumber((string.match(part, "%d+") or "0"))
elseif i == 7 then
result.type = part
end
end
else
result.error = "tail, df are installed?"
end
return result
end
local function attributes(file)
local h = file
local file_url = tostring(h.url)
if not h or ya.target_family() ~= "unix" then
return ""
end
local output, _ = Command("lsattr"):args({ "-d", file_url }):stdout(Command.PIPED):output()
if output then
-- Splitting the data
local parts = split_by_whitespace(output.stdout)
-- Display the result
for i, part in ipairs(parts) do
if i == 1 then
return part
end
end
return ""
else
return "lsattr is installed?"
end
end
---shorten string
---@param _s string string
---@param _t string tail
---@param _w number max characters
---@return string
local shorten = function(_s, _t, _w)
local s = _s or utf8.len(_s)
local t = _t or ""
local ellipsis = "" .. t
local w = _w < utf8.len(ellipsis) and utf8.len(ellipsis) or _w
local n_ellipsis = utf8.len(ellipsis) or 0
if utf8.len(s) > w then
return s:sub(1, (utf8.offset(s, w - n_ellipsis + 1) or 2) - 1) .. ellipsis
end
return s
end
local is_supported_table = type(ui.Table) ~= "nil" and type(ui.Row) ~= "nil"
local styles = {
header = ui.Style():fg("green"),
row_label = ui.Style():fg("reset"),
row_value = ui.Style():fg("blue"),
row_value_spot_hovered = ui.Style():fg("blue"):reverse(),
}
function M:render_table(job, opts)
local filesystem_extra = get_filesystem_extra(job.file)
local prefix = " "
local label_lines, value_lines, rows = {}, {}, {}
local label_max_length = 15
local file_name_extension = job.file.cha.is_dir and "" or ("." .. (job.file.url.ext(job.file.url) or ""))
local row = function(key, value)
local h = type(value) == "table" and #value or 1
rows[#rows + 1] = ui.Row({ ui.Line(key):style(styles.row_label), ui.Line(value):style(styles.row_value) })
:height(h)
end
local file_name = shorten(
job.file.name,
file_name_extension,
math.floor(job.area.w - label_max_length - utf8.len(file_name_extension))
)
local location =
shorten(tostring(job.file.url:parent()), "", math.floor(job.area.w - label_max_length - utf8.len(prefix)))
local filesystem_error = filesystem_extra.error
and shorten(filesystem_extra.error, "", math.floor(job.area.w - label_max_length - utf8.len(prefix)))
or nil
local filesystem =
shorten(filesystem_extra.filesystem, "", math.floor(job.area.w - label_max_length - utf8.len(prefix)))
if not is_supported_table then
table.insert(
label_lines,
ui.Line({
ui.Span("Metadata:"),
}):style(styles.header)
)
table.insert(
value_lines,
ui.Line({
ui.Span(""),
})
)
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("File:"),
}):style(styles.row_label)
)
table.insert(
value_lines,
ui.Line({
ui.Span(file_name),
}):style(styles.row_value)
)
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Mimetype: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(job._mime or job.mime)):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Location: "),
}):style(styles.row_label)
)
table.insert(
value_lines,
ui.Line({
ui.Span(location),
}):style(styles.row_value)
)
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Mode: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(permission(job.file)):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Attributes: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(attributes(job.file))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Links: "),
}):style(styles.row_label)
)
table.insert(
value_lines,
ui.Line({
ui.Span(tostring(link_count(job.file))),
}):style(styles.row_value)
)
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Owner: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(owner_group(job.file))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Size: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(file_size_and_folder_childs(job.file))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Created: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(fileTimestamp(job.file, "btime"))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Modified: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(fileTimestamp(job.file, "mtime"))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Accessed: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(fileTimestamp(job.file, "atime"))):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Filesystem: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(filesystem_error or filesystem)):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Device: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(filesystem_error or filesystem_extra.device)):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Type: "),
}):style(styles.row_label)
)
table.insert(value_lines, ui.Line(ui.Span(filesystem_error or filesystem_extra.type)):style(styles.row_value))
table.insert(
label_lines,
ui.Line({
ui.Span(prefix),
ui.Span("Free space: "),
}):style(styles.row_label)
)
table.insert(
value_lines,
ui.Line(
ui.Span(
filesystem_extra.error
or (
filesystem_extra.avail_space
.. " / "
.. filesystem_extra.total_space
.. " ("
.. filesystem_extra.avail_space_percent
.. "%)"
)
)
):style(styles.row_value)
)
else
rows[#rows + 1] = ui.Row({ "Metadata", "" }):style(styles.header)
row(prefix .. "File:", file_name)
row(prefix .. "Mimetype:", job.mime)
row(prefix .. "Location:", location)
row(prefix .. "Mode:", permission(job.file))
row(prefix .. "Attributes:", attributes(job.file))
row(prefix .. "Links:", tostring(link_count(job.file)))
row(prefix .. "Owner:", owner_group(job.file))
row(prefix .. "Size:", file_size_and_folder_childs(job.file))
row(prefix .. "Created:", fileTimestamp(job.file, "btime"))
row(prefix .. "Modified:", fileTimestamp(job.file, "mtime"))
row(prefix .. "Accessed:", fileTimestamp(job.file, "atime"))
row(prefix .. "Filesystem:", filesystem_error or filesystem)
row(prefix .. "Device:", filesystem_error or filesystem_extra.device)
row(prefix .. "Type:", filesystem_error or filesystem_extra.type)
row(
prefix .. "Free space:",
filesystem_error
or (
(
filesystem_extra.avail_space
and filesystem_extra.total_space
and filesystem_extra.avail_space_percent
)
and (filesystem_extra.avail_space .. " / " .. filesystem_extra.total_space .. " (" .. filesystem_extra.avail_space_percent .. "%)")
or ""
)
)
if opts and opts.show_plugins_section and PLUGIN then
local spotter = PLUGIN.spotter(job.file.url, job.mime)
local previewer = PLUGIN.previewer(job.file.url, job.mime)
local fetchers = PLUGIN.fetchers(job.file, job.mime)
local preloaders = PLUGIN.preloaders(job.file.url, job.mime)
for i, v in ipairs(fetchers) do
fetchers[i] = v.cmd
end
for i, v in ipairs(preloaders) do
preloaders[i] = v.cmd
end
rows[#rows + 1] = ui.Row({ { "", "Plugins" }, "" }):height(2):style(styles.header)
row(prefix .. "Spotter:", spotter and spotter.cmd or "")
row(prefix .. "Previewer:", previewer and previewer.cmd or "")
row(prefix .. "Fetchers:", #fetchers ~= 0 and fetchers or "")
row(prefix .. "Preloaders:", #preloaders ~= 0 and preloaders or "")
end
end
if not is_supported_table then
local areas = ui.Layout()
:direction(ui.Layout.HORIZONTAL)
:constraints({ ui.Constraint.Length(label_max_length), ui.Constraint.Fill(1) })
:split(job.area)
local label_area = areas[1]
local value_area = areas[2]
return {
ui.Text(label_lines):area(label_area):align(ui.Text.LEFT):wrap(ui.Text.WRAP_NO),
ui.Text(value_lines):area(value_area):align(ui.Text.LEFT):wrap(ui.Text.WRAP_NO),
}
else
return {
ui.Table(rows):area(job.area):row(1):col(1):col_style(styles.row_value):widths({
ui.Constraint.Length(label_max_length),
ui.Constraint.Fill(1),
}),
}
end
end
function M:peek(job)
local start, cache = os.clock(), ya.file_cache(job)
if not cache or self:preload(job) ~= 1 then
return 1
end
ya.sleep(math.max(0, PREVIEW.image_delay / 1000 + start - os.clock()))
ya.preview_widgets(job, self:render_table(job))
end
function M:seek(job)
local h = cx.active.current.hovered
if h and h.url == job.file.url then
local step = math.floor(job.units * job.area.h / 10)
ya.manager_emit("peek", {
tostring(math.max(0, cx.active.preview.skip + step)),
only_if = tostring(job.file.url),
})
end
end
function M:preload(job)
local cache = ya.file_cache(job)
if not cache or fs.cha(cache) then
return 1
end
return 1
end
function M:spot(job)
job.area = ui.Pos({ "center", w = 80, h = 25 })
ya.spot_table(
job,
self:render_table(job, { show_plugins_section = true })[1]:cell_style(styles.row_value_spot_hovered)
)
end
return M