package choose_files import ( "encoding/json" "fmt" "io/fs" "os" "path/filepath" "strconv" "sync" "sync/atomic" "time" "github.com/hako/durafmt" "github.com/kovidgoyal/go-parallel" "github.com/kovidgoyal/kitty/tools/disk_cache" "github.com/kovidgoyal/kitty/tools/icons" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/humanize" "github.com/kovidgoyal/kitty/tools/utils/images" ) const IMAGE_METADATA_KEY = "image-metadata.json" const IMAGE_DATA_PREFIX = "image-data-" var dc_size atomic.Int64 var _ = fmt.Print var preview_cache = sync.OnceValues(func() (*disk_cache.DiskCache, error) { cdir := utils.CacheDir() cdir = filepath.Join(cdir, "choose-files") return disk_cache.NewDiskCache(cdir, dc_size.Load()) }) type ShowData struct { abspath string metadata fs.FileInfo x, y, width, height int cached_data map[string]string custom_metadata metadata } type PreviewRenderer interface { Unmarshall(map[string]string) (any, error) Render(string) (map[string][]byte, metadata, *images.ImageData, error) ShowMetadata(h *Handler, s ShowData) int String() string } type render_data struct { cached_data map[string]string img *images.ImageData metadata metadata err error } type metadata struct { image *images.SerializableImageMetadata custom any } type ImagePreview struct { abspath string metadata fs.FileInfo disk_cache *disk_cache.DiskCache cached_data map[string]string render_err Preview render_channel chan render_data ready atomic.Bool source_img *images.ImageData custom_metadata metadata renderer PreviewRenderer file_metadata_preview Preview WakeupMainThread func() bool } func (p *ImagePreview) String() string { return fmt.Sprintf("ImagePreview{%#v, ready: %v, renderer: %s}", p.abspath, p.ready.Load(), p.renderer.String()) } func (p *ImagePreview) IsValidForColorScheme(bool) bool { return true } func (p *ImagePreview) IsReady() bool { return p.ready.Load() || p.render_channel == nil } func (p *ImagePreview) Unload() { p.source_img = nil } func load_image(cached_data map[string]string) (img *images.ImageData, err error) { fp := cached_data[IMAGE_METADATA_KEY] if fp == "" { return nil, fmt.Errorf("missing cached image metadata") } b, err := os.ReadFile(fp) if err != nil { return nil, fmt.Errorf("failed to read cached image metadata: %w", err) } var m images.SerializableImageMetadata if err = json.Unmarshal(b, &m); err != nil { return nil, fmt.Errorf("failed to decode cached image metadata: %w", err) } frames := make([][]byte, len(m.Frames)) for i := range m.Frames { path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(i)] if path == "" { return nil, fmt.Errorf("missing cached data for frame: %d", i) } d, e := os.ReadFile(path) if e != nil { return nil, fmt.Errorf("failed to read cached image frame %d data: %w", i, e) } m.Frames[i].Size = len(d) frames[i] = d } return images.ImageFromSerialized(m, frames) } func (p *ImagePreview) ensure_source_image() (err error) { if p.source_img != nil { return } defer func() { if err != nil { p.render_err = NewErrorPreview(err) } }() p.source_img, err = load_image(p.cached_data) return } func (p *ImagePreview) render_image(h *Handler, x, y, width, height int) { defer func() { if r := recover(); r != nil { h.err_chan <- parallel.Format_stacktrace_on_panic(r, 1) p.WakeupMainThread() } }() offset := p.renderer.ShowMetadata(h, ShowData{ abspath: p.abspath, metadata: p.metadata, x: x, y: y, width: width, height: height, cached_data: p.cached_data, custom_metadata: p.custom_metadata, }) if p.custom_metadata.image != nil { h.graphics_handler.RenderImagePreview(h, p, x, y+offset, width, height-offset) } } func (p *ImagePreview) Render(h *Handler, x, y, width, height int) { if p.render_channel == nil { if p.render_err == nil { p.render_image(h, x, y, width, height) } else { p.render_err.Render(h, x, y, width, height) } return } select { case hd := <-p.render_channel: p.render_channel = nil p.cached_data = hd.cached_data p.source_img = hd.img p.custom_metadata = hd.metadata if hd.err != nil { p.render_err = NewFileMetadataPreviewWithError(p.abspath, p.metadata, fmt.Errorf("Failed to render the preview with error: %w", hd.err)) } p.Render(h, x, y, width, height) return default: } if p.file_metadata_preview == nil { p.file_metadata_preview = NewFileMetadataPreview(p.abspath, p.metadata) m := p.file_metadata_preview.(*MessagePreview) m.trailers = append(m.trailers, "", "Rendering image preview, please wait…") } p.file_metadata_preview.Render(h, x, y, width, height) } func (p *ImagePreview) start_rendering() { defer func() { if r := recover(); r != nil { p.render_channel <- render_data{err: parallel.Format_stacktrace_on_panic(r, 1)} } close(p.render_channel) p.ready.Store(true) p.WakeupMainThread() }() key, ans, err := p.disk_cache.GetPath(p.abspath) if err != nil { p.render_channel <- render_data{err: err} return } if len(ans) > 0 { mi := metadata{} if mi.custom, err = p.renderer.Unmarshall(ans); err == nil { if d := ans[IMAGE_METADATA_KEY]; d != "" { if b, err := os.ReadFile(d); err == nil { var m images.SerializableImageMetadata if err = json.Unmarshal(b, &m); err == nil { mi.image = &m } } } } if mi.custom != nil || mi.image != nil { p.render_channel <- render_data{cached_data: ans, metadata: mi} return } } rdata, metadata, img, err := p.renderer.Render(p.abspath) if err != nil { p.render_channel <- render_data{err: err} } else { ans, err = p.disk_cache.AddPath(p.abspath, key, rdata) if err == nil { p.render_channel <- render_data{cached_data: ans, metadata: metadata, img: img} } else { p.render_channel <- render_data{err: err} } } } type ImagePreviewRenderer uint func (p ImagePreviewRenderer) String() string { return "ImagePreviewRenderer" } func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, m metadata, img *images.ImageData, err error) { if img, err = images.OpenImageFromPath(abspath); err != nil { return nil, metadata{}, nil, err } im, data := img.Serialize() ans = make(map[string][]byte, len(data)+1) sm, err := json.Marshal(im) if err != nil { return nil, metadata{}, nil, err } ans[IMAGE_METADATA_KEY] = sm m = metadata{image: &im} for i, d := range data { key := IMAGE_DATA_PREFIX + strconv.Itoa(i) ans[key] = d } return } func (p ImagePreviewRenderer) Unmarshall(map[string]string) (any, error) { return nil, nil } func (p ImagePreviewRenderer) ShowMetadata(h *Handler, s ShowData) int { offset := 0 if m := s.custom_metadata.image; m != nil { text := fmt.Sprintf("%s: %dx%d %s", m.Format_uppercase, m.Width, m.Height, humanize.Bytes(uint64(s.metadata.Size()))) icon := icons.IconForPath("/a.gif") text = icon + " " + text offset += h.render_wrapped_text_in_region(text, s.x, s.y, s.width, s.height, true) } st := humanize.Time(s.metadata.ModTime()) if s.custom_metadata.image != nil && len(s.custom_metadata.image.Frames) > 0 { var d time.Duration for _, f := range s.custom_metadata.image.Frames { if f.Delay_ms > 0 { d += time.Duration(time.Duration(f.Delay_ms) * time.Millisecond) } } if d > 0 { st += fmt.Sprintf(", %s", durafmt.Parse(d).LimitFirstN(1).String()) } } offset += h.render_wrapped_text_in_region(st, s.x, s.y+offset, s.width, s.height-offset, true) return offset } func NewImagePreview( abspath string, metadata fs.FileInfo, opts Settings, WakeupMainThread func() bool, r PreviewRenderer, ) (Preview, error) { dc_size.Store(opts.DiskCacheSize()) ans := &ImagePreview{ abspath: abspath, metadata: metadata, render_channel: make(chan render_data, 1), WakeupMainThread: WakeupMainThread, renderer: r, } if dc, err := preview_cache(); err != nil { return nil, err } else { ans.disk_cache = dc } go ans.start_rendering() return ans, nil }