feat: support compressed tarballs (.tar.gz, .tar.bz2, etc.) in the preset archive previewer (#3518)

Co-authored-by: sxyazi <sxyazi@gmail.com>
This commit is contained in:
itsvyle 2026-01-08 08:35:29 +01:00 committed by GitHub
parent c6e1702c61
commit 83674f19b4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 152 additions and 44 deletions

View file

@ -12,6 +12,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
## [Unreleased]
### Added
- Support compressed tarballs (`.tar.gz`, `.tar.bz2`, etc.) in the preset archive previewer ([#3518])
### Fixed
- Account for URL covariance in `Url:join()` ([#3514])
@ -367,7 +371,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
### Fixed
- Correctly handle CRLF on Windows in preset `archive` and `json` plugins ([#2017])
- Correctly handle CRLF on Windows in preset archive and JSON plugins ([#2017])
- Failed to parse certain image dimensions for Überzug++ backend ([#2020])
- Disable passthrough when the user launches Yazi in Neovim inside tmux ([#2014])
@ -1588,3 +1592,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
[#3482]: https://github.com/sxyazi/yazi/pull/3482
[#3494]: https://github.com/sxyazi/yazi/pull/3494
[#3514]: https://github.com/sxyazi/yazi/pull/3514
[#3518]: https://github.com/sxyazi/yazi/pull/3518

View file

@ -2,14 +2,19 @@ local M = {}
function M:peek(job)
local limit = job.area.h
local files, bound, err = self.list_files({ "-p", tostring(job.file.path) }, job.skip, limit)
local files, bound, err = self.list_archive({ "-p", tostring(job.file.path) }, job.skip, limit)
local first = (#files == 1 and files[1]) or (#files == 0 and M.list_if_only_one(job.file.path))
if first and M.should_decompress_tar(first) then
files, bound, err = self.list_compressed_tar({ "-p", tostring(job.file.path) }, job.skip, limit)
end
if err then
return ya.preview_widget(job, err)
elseif job.skip > 0 and bound < job.skip + limit then
return ya.emit("peek", { math.max(0, bound - limit), only_if = job.file.url, upper_bound = true })
elseif #files == 0 then
files = { { path = job.file.url.stem, size = 0, attr = "" } }
files = { { path = job.file.url.stem, size = 0, packed_size = 0, attr = "" } }
end
local left, right = {}, {}
@ -66,6 +71,34 @@ function M.spawn_7z(args)
return child, last_err
end
-- Spawn a 7z instance which pipes a "7z {src_args}" into a "7z {dst_args}"
-- Used for previewing compressed tarballs, by doing "7z x -so .. | 7z l -si .."
function M.spawn_7z_piped(src_args, dst_args)
local last_err = nil
local try = function(name)
local src, err = Command(name):arg(src_args):stdout(Command.PIPED):stderr(Command.PIPED):spawn()
if not src then
last_err = err
return src
end
local dst, err =
Command(name):arg(dst_args):stdin(src:take_stdout()):stdout(Command.PIPED):stderr(Command.PIPED):spawn()
if not dst then
last_err = err
end
return src, dst
end
local src, dst = try("7zz")
if not src then
src, dst = try("7z")
end
if not dst then
return ya.err("Failed to start either `7zz` or `7z`, error: " .. last_err)
end
return src, dst, last_err
end
---List files in an archive
---@param args table
---@param skip integer
@ -73,56 +106,66 @@ end
---@return table files
---@return integer bound
---@return Error? err
function M.list_files(args, skip, limit)
function M.list_archive(args, skip, limit)
local child = M.spawn_7z { "l", "-ba", "-slt", "-sccUTF-8", table.unpack(args) }
if not child then
return {}, 0, Err("Failed to start either `7zz` or `7z`. Do you have 7-zip installed?")
end
local i, files, err = 0, { { path = "", size = 0, attr = "" } }, nil
local key, value, stderr = "", "", {}
repeat
local next, event = child:read_line()
if event == 1 and M.is_encrypted(next) then
err = Err("File list in this archive is encrypted")
break
elseif event == 1 then
stderr[#stderr + 1] = next
goto continue
elseif event ~= 0 then
break
end
if next == "\n" or next == "\r\n" then
i = i + 1
if files[#files].path ~= "" then
files[#files + 1] = { path = "", size = 0, attr = "" }
end
goto continue
elseif i < skip then
goto continue
end
key, value = next:match("^(%u%l+) = (.-)[\r\n]+")
if key == "Path" then
files[#files].path = value
elseif key == "Size" then
files[#files].size = tonumber(value) or 0
elseif key == "Attributes" then
files[#files].attr = value
end
::continue::
until i >= skip + limit
local files, bound, err = M.parse_7z_slt(child, skip, limit)
child:start_kill()
if files[#files].path == "" then
files[#files] = nil
return files, bound, err
end
---List files in a compressed tarball
---@param args table
---@param skip integer
---@param limit integer
---@return table files
---@return integer bound
---@return Error? err
function M.list_compressed_tar(args, skip, limit)
local src, dst = M.spawn_7z_piped(
{ "x", "-so", table.unpack(args) },
{ "l", "-ba", "-slt", "-ttar", "-sccUTF-8", "-si" }
)
if not dst then
return {}, 0, Err("Failed to start either `7zz` or `7z`. Do you have 7-zip installed?")
end
if #stderr ~= 0 then
err = Err("7-zip errored out while listing files, stderr: %s", table.concat(stderr, "\n"))
local files, bound, err = M.parse_7z_slt(dst, skip, limit)
src:start_kill()
dst:start_kill()
return files, bound, err
end
function M.list_if_only_one(path)
-- For certain compressed tarballs (e.g. .tar.xz),
-- 7-zip doesn't print a .tar file if -slt is specified, so we are not doing that here
local child = M.spawn_7z { "l", "-ba", "-sccUTF-8", "-p", tostring(path) }
if not child then
return false
end
local files = {}
while #files < 2 do
local next, event = child:read_line()
if event == 0 then
local attr, size, packed_size, path = next:sub(20):match("([^ ]+) +(%d+) +(%d+) +([^\r\n]+)")
if path then
files[#files + 1] = { path = path, size = tonumber(size), packed_size = tonumber(packed_size), attr = attr }
end
elseif event ~= 1 then
break
end
end
child:start_kill()
if #files == 1 then
return files[1]
end
return files, i, err
end
---List metadata of an archive
@ -170,4 +213,64 @@ function M.is_encrypted(s) return s:find(" Wrong password", 1, true) end
function M.is_tar(path) return M.list_meta { "-p", tostring(path) } == "tar" end
function M.should_decompress_tar(file)
return file.packed_size <= 1024 * 1024 * 1024 and file.path:lower():find(".+%.tar$") ~= nil
end
-- Parse the output of a "7z l -slt" command.
-- The caller is responsible for killing the child process right after the execution of this function
---@param child Child
---@param skip integer
---@param limit integer
---@return table files
---@return integer bound
---@return Error? err
function M.parse_7z_slt(child, skip, limit)
local i, files, err = 0, { { path = "", size = 0, packed_size = 0, attr = "" } }, nil
local key, value, stderr = "", "", {}
repeat
local next, event = child:read_line()
if event == 1 and M.is_encrypted(next) then
err = Err("File list in this archive is encrypted")
break
elseif event == 1 then
stderr[#stderr + 1] = next
goto continue
elseif event ~= 0 then
break
end
if next == "\n" or next == "\r\n" then
i = i + 1
if files[#files].path ~= "" then
files[#files + 1] = { path = "", size = 0, packed_size = 0, attr = "" }
end
goto continue
elseif i < skip then
goto continue
end
key, value = next:match("^(%u[%a ]+) = (.-)[\r\n]+")
if key == "Path" then
files[#files].path = value
elseif key == "Size" then
files[#files].size = tonumber(value) or 0
elseif key == "Packed Size" then
files[#files].packed_size = tonumber(value) or 0
elseif key == "Attributes" then
files[#files].attr = value
end
::continue::
until i >= skip + limit
if files[#files].path == "" then
files[#files] = nil
end
if #stderr ~= 0 then
err = Err("7-zip errored out while listing files, stderr: %s", table.concat(stderr, "\n"))
end
return files, i, err
end
return M