diff --git a/go.mod b/go.mod index 591493488..ca9d4e870 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a0b41fb52..18b9b20af 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/kittens/choose_files/archive.go b/kittens/choose_files/archive.go new file mode 100644 index 000000000..5ab1684e3 --- /dev/null +++ b/kittens/choose_files/archive.go @@ -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 +} diff --git a/kittens/choose_files/preview.go b/kittens/choose_files/preview.go index 0fb48f4d2..1b63702d1 100644 --- a/kittens/choose_files/preview.go +++ b/kittens/choose_files/preview.go @@ -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) }