Choose files: Preview for common archive types

This commit is contained in:
Kovid Goyal 2025-11-24 21:13:41 +05:30
parent 158ebee92c
commit 22f0292e35
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
4 changed files with 162 additions and 0 deletions

2
go.mod
View file

@ -12,12 +12,14 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b
github.com/klauspost/compress v1.18.1
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1
github.com/kovidgoyal/go-parallel v1.1.1
github.com/kovidgoyal/go-shm v1.0.0
github.com/kovidgoyal/imaging v1.8.17
github.com/seancfoley/ipaddress-go v1.7.1
github.com/shirou/gopsutil/v4 v4.25.10
github.com/ulikunitz/xz v0.5.15
github.com/zeebo/xxh3 v1.0.2
golang.org/x/exp v0.0.0-20230801115018-d63ba01acd4b
golang.org/x/image v0.33.0

4
go.sum
View file

@ -26,6 +26,8 @@ github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1p
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kovidgoyal/dbus v0.0.0-20250519011319-e811c41c0bc1 h1:rMY/hWfcVzBm6BLX6YLA+gLJEpuXBed/VP6YEkXt8R4=
@ -56,6 +58,8 @@ github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8O
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=

View file

@ -0,0 +1,153 @@
package choose_files
import (
"archive/tar"
"compress/bzip2"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/klauspost/compress/gzip"
"github.com/klauspost/compress/zip"
"github.com/klauspost/compress/zstd"
"github.com/ulikunitz/xz"
"github.com/kovidgoyal/kitty/tools/utils"
)
var _ = fmt.Print
func IsSupportedArchiveFile(abspath string) bool {
name := strings.ToLower(filepath.Base(abspath))
ext := filepath.Ext(name)
switch ext {
case ".zip", ".tgz", ".tbz2", ".tzst", ".txz":
return true
case ".gz", ".bz2", ".zst", ".xz":
name = name[:len(name)-len(ext)]
ext = filepath.Ext(name)
return ext == ".tar"
default:
return false
}
}
type archive_preview struct {
path string
metadata fs.FileInfo
WakeupMainThread func() bool
ch chan *MessagePreview
mp *MessagePreview
}
func displayFilename(s string) string {
if isPrintableUTF8(s) {
return s
}
return fmt.Sprintf("%X", utils.UnsafeStringToBytes(s))
}
func isPrintableUTF8(s string) bool {
for _, r := range s {
if r == '\uFFFD' { // replacement char
return false
}
}
return true
}
func (p *archive_preview) render() {
name := strings.ToLower(filepath.Base(p.path))
ext := filepath.Ext(name)
names := []string{""}
populate_tar := func(r io.Reader) {
tr := tar.NewReader(r)
for len(names) < 500 {
hdr, err := tr.Next()
if err != nil {
break
}
names = append(names, displayFilename(hdr.Name))
}
}
switch ext {
case ".zip":
r, err := zip.OpenReader(p.path)
if err == nil || errors.Is(err, zip.ErrInsecurePath) {
defer r.Close()
for _, f := range r.File {
if f.NonUTF8 {
names = append(names, displayFilename(f.Name))
} else {
names = append(names, f.Name)
}
}
}
case ".gz", ".tgz":
if f, err := os.Open(p.path); err == nil {
defer f.Close()
if gz, err := gzip.NewReader(f); err == nil {
defer gz.Close()
populate_tar(gz)
}
}
case ".xz", ".txz":
if f, err := os.Open(p.path); err == nil {
defer f.Close()
if gz, err := xz.NewReader(f); err == nil {
populate_tar(gz)
}
}
case ".bz2", ".tbz2":
if f, err := os.Open(p.path); err == nil {
defer f.Close()
populate_tar(bzip2.NewReader(f))
}
case ".zst", ".tzst":
if f, err := os.Open(p.path); err == nil {
defer f.Close()
if gz, err := zstd.NewReader(f); err == nil {
defer gz.Close()
populate_tar(gz)
}
}
}
mp := *p.mp
mp.trailers = append(mp.trailers, names...)
p.ch <- &mp
p.WakeupMainThread()
}
func (p *archive_preview) IsReady() bool { return true }
func (p *archive_preview) Unload() {}
func (p *archive_preview) IsValidForColorScheme(light bool) bool { return true }
func (p *archive_preview) String() string { return fmt.Sprintf("ArchivePreview{%s}", p.path) }
func (p *archive_preview) Render(h *Handler, x, y, width, height int) {
if p.ch != nil {
select {
case mp := <-p.ch:
p.mp = mp
close(p.ch)
p.ch = nil
default:
}
}
p.mp.Render(h, x, y, width, height)
}
func NewArchivePeview(
abspath string, metadata fs.FileInfo, opts Settings, WakeupMainThread func() bool,
) Preview {
mp := NewFileMetadataPreview(abspath, metadata)
ans := &archive_preview{
path: abspath, metadata: metadata, WakeupMainThread: WakeupMainThread, ch: make(chan *MessagePreview, 1), mp: mp,
}
go ans.render()
return ans
}

View file

@ -354,6 +354,9 @@ func (pm *PreviewManager) preview_for(abspath string, ftype fs.FileMode) (ans Pr
case strings.HasPrefix(mt, "video/"):
return NewFFMpegPreview(abspath, s, pm.settings, pm.WakeupMainThread)
case IsSupportedArchiveFile(abspath):
return NewArchivePeview(abspath, s, pm.settings, pm.WakeupMainThread)
case IsSupportedByCalibre(abspath):
return NewCalibrePreview(abspath, s, pm.settings, pm.WakeupMainThread)
}