diff --git a/tools/cmd/diff/collect.go b/tools/cmd/diff/collect.go index 740aa9e20..9e656f0e5 100644 --- a/tools/cmd/diff/collect.go +++ b/tools/cmd/diff/collect.go @@ -18,6 +18,7 @@ var _ = fmt.Print var path_name_map, remote_dirs map[string]string var mimetypes_cache, data_cache, hash_cache *utils.LRUCache[string, string] +var size_cache *utils.LRUCache[string, int64] var lines_cache *utils.LRUCache[string, []string] var highlighted_lines_cache *utils.LRUCache[string, []string] var is_text_cache *utils.LRUCache[string, bool] @@ -26,6 +27,7 @@ func init_caches() { path_name_map = make(map[string]string, 32) remote_dirs = make(map[string]string, 32) const sz = 4096 + size_cache = utils.NewLRUCache[string, int64](sz) mimetypes_cache = utils.NewLRUCache[string, string](sz) data_cache = utils.NewLRUCache[string, string](sz) is_text_cache = utils.NewLRUCache[string, bool](sz) @@ -67,6 +69,16 @@ func data_for_path(path string) (string, error) { }) } +func size_for_path(path string) (int64, error) { + return size_cache.GetOrCreate(path, func(path string) (int64, error) { + s, err := os.Stat(path) + if err != nil { + return 0, err + } + return s.Size(), nil + }) +} + func is_image(path string) bool { return strings.HasPrefix(mimetype_for_path(path), "image/") } diff --git a/tools/cmd/diff/render.go b/tools/cmd/diff/render.go index 31ff8f7a9..438789da2 100644 --- a/tools/cmd/diff/render.go +++ b/tools/cmd/diff/render.go @@ -210,12 +210,7 @@ func (self *LogicalLines) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta i return } -func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) { - // TODO: Implement this - return ans, nil -} - -func human_readable(size int) string { +func human_readable(size int64) string { divisor, suffix := 1, "B" for i, candidate := range []string{"B", "KB", "MB", "GB", "TB", "PB", "EB"} { if size < (1 << ((i + 1) * 10)) { @@ -253,33 +248,79 @@ func render_diff_line(number, text, ltype string, margin_size int, available_col return margin + content } -func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) { +func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) { available_cols := columns/2 - margin_size - fl := func(path string, formatter func(...any) string) string { - if err == nil { - var data string - data, err = data_for_path(path) - text := fmt.Sprintf("Binary file: %s", human_readable(len(data))) - text = place_in(text, available_cols) - return margin_format(strings.Repeat(` `, margin_size)) + formatter(text) + ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string, formatter func(...any) string) (string, error) { + sz, err := size_for_path(path) + if err != nil { + return "", err } - return "" + text := fmt.Sprintf("Size: %s", human_readable(sz)) + res := image_collection.ResolutionOf(path) + if res.X > -1 { + text = fmt.Sprintf("Dimensions: %dx%d %s", res.X, res.Y, text) + } + text = place_in(text, available_cols) + return formatter(strings.Repeat(` `, margin_size) + text), err + }) + + if err != nil { + return nil, err } + return append(ans, ll), nil +} + +func first_binary_line(left_path, right_path string, columns, margin_size int, renderer func(path string, formatter func(...any) string) (string, error)) (*LogicalLine, error) { + available_cols := columns/2 - margin_size line := "" if left_path == "" { filler := render_diff_line(``, ``, `filler`, margin_size, available_cols) - line = filler + fl(right_path, added_format) + r, err := renderer(right_path, added_format) + if err != nil { + return nil, err + } + line = filler + r } else if right_path == "" { filler := render_diff_line(``, ``, `filler`, margin_size, available_cols) - line = fl(left_path, removed_format) + filler + l, err := renderer(left_path, removed_format) + if err != nil { + return nil, err + } + line = l + filler } else { - line = fl(left_path, removed_format) + fl(right_path, added_format) + l, err := renderer(left_path, removed_format) + if err != nil { + return nil, err + } + r, err := renderer(right_path, added_format) + if err != nil { + return nil, err + } + line = l + r } ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: left_path, linenum: 0}, screen_lines: []string{line}} if left_path == "" { ll.src.path = right_path } - return append(ans, &ll), err + return &ll, nil +} + +func binary_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) (ans2 []*LogicalLine, err error) { + available_cols := columns/2 - margin_size + ll, err := first_binary_line(left_path, right_path, columns, margin_size, func(path string, formatter func(...any) string) (string, error) { + sz, err := size_for_path(path) + if err != nil { + return "", err + } + text := fmt.Sprintf("Binary file: %s", human_readable(sz)) + text = place_in(text, available_cols) + return formatter(strings.Repeat(` `, margin_size) + text), err + }) + + if err != nil { + return nil, err + } + return append(ans, ll), nil } type DiffData struct { diff --git a/tools/cmd/diff/ui.go b/tools/cmd/diff/ui.go index 30c2207cf..a84b4c18f 100644 --- a/tools/cmd/diff/ui.go +++ b/tools/cmd/diff/ui.go @@ -25,6 +25,7 @@ const ( COLLECTION ResultType = iota DIFF HIGHLIGHT + IMAGE_LOAD ) type ScrollPos struct { @@ -46,10 +47,11 @@ type AsyncResult struct { diff_map map[string]*Patch } +var image_collection *graphics.ImageCollection + type Handler struct { async_results chan AsyncResult shortcut_tracker config.ShortcutTracker - pending_keys []string left, right string collection *Collection diff_map map[string]*Patch @@ -78,8 +80,8 @@ func (self *Handler) calculate_statistics() { var DebugPrintln = tty.DebugPrintln func (self *Handler) initialize() { - self.pending_keys = make([]string, 0, 4) self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"}) + image_collection = graphics.NewImageCollection() self.current_context_count = opts.Context if self.current_context_count < 0 { self.current_context_count = int(conf.Num_context_lines) @@ -154,12 +156,42 @@ func (self *Handler) highlight_all() { } +func (self *Handler) load_all_images() { + self.collection.Apply(func(path, item_type, changed_path string) error { + if path != "" && is_image(path) { + image_collection.AddPaths(path) + } + if changed_path != "" && is_image(changed_path) { + image_collection.AddPaths(changed_path) + } + return nil + }) + go func() { + r := AsyncResult{rtype: IMAGE_LOAD} + image_collection.LoadAll() + self.async_results <- r + self.lp.WakeupMainThread() + }() +} + +func (self *Handler) rerender_diff() error { + if self.diff_map != nil && self.collection != nil { + err := self.render_diff() + if err != nil { + return err + } + self.draw_screen() + } + return nil +} + func (self *Handler) handle_async_result(r AsyncResult) error { switch r.rtype { case COLLECTION: self.collection = r.collection self.generate_diff() self.highlight_all() + self.load_all_images() case DIFF: self.diff_map = r.diff_map self.calculate_statistics() @@ -176,14 +208,8 @@ func (self *Handler) handle_async_result(r AsyncResult) error { self.restore_position = nil } self.draw_screen() - case HIGHLIGHT: - if self.diff_map != nil && self.collection != nil { - err := self.render_diff() - if err != nil { - return err - } - self.draw_screen() - } + case HIGHLIGHT, IMAGE_LOAD: + return self.rerender_diff() } return nil } diff --git a/tools/cmd/icat/detect.go b/tools/cmd/icat/detect.go index 66d419e0e..c4a1c1656 100644 --- a/tools/cmd/icat/detect.go +++ b/tools/cmd/icat/detect.go @@ -11,6 +11,7 @@ import ( "kitty/tools/tui/graphics" "kitty/tools/tui/loop" "kitty/tools/utils" + "kitty/tools/utils/images" "kitty/tools/utils/shm" ) @@ -58,7 +59,7 @@ func DetectSupport(timeout time.Duration) (memory, files, direct bool, err error } direct_query_id = g(graphics.GRT_transmission_direct, "123") - tf, err := graphics.CreateTempInRAM() + tf, err := images.CreateTempInRAM() if err == nil { file_query_id = g(graphics.GRT_transmission_tempfile, tf.Name()) temp_files_to_delete = append(temp_files_to_delete, tf.Name()) diff --git a/tools/cmd/icat/process_images.go b/tools/cmd/icat/process_images.go index 553ae470f..287269c16 100644 --- a/tools/cmd/icat/process_images.go +++ b/tools/cmd/icat/process_images.go @@ -18,6 +18,7 @@ import ( "kitty/tools/tty" "kitty/tools/tui/graphics" "kitty/tools/utils" + "kitty/tools/utils/images" "kitty/tools/utils/shm" ) @@ -142,7 +143,7 @@ func (self *opened_input) PutOnFilesystem() (err error) { if self.name_to_unlink != "" { return } - f, err := graphics.CreateTempInRAM() + f, err := images.CreateTempInRAM() if err != nil { return fmt.Errorf("Failed to create a temporary file to store input data with error: %w", err) } diff --git a/tools/cmd/icat/transmit.go b/tools/cmd/icat/transmit.go index 8282021d7..0decaaf43 100644 --- a/tools/cmd/icat/transmit.go +++ b/tools/cmd/icat/transmit.go @@ -152,7 +152,7 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err frame.shm.Close() frame.shm = nil } else { - f, err := graphics.CreateTempInRAM() + f, err := images.CreateTempInRAM() if err != nil { return fmt.Errorf("Failed to create a temp file for image data transmission: %w", err) } diff --git a/tools/tui/graphics/collection.go b/tools/tui/graphics/collection.go new file mode 100644 index 000000000..0e341c17a --- /dev/null +++ b/tools/tui/graphics/collection.go @@ -0,0 +1,86 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package graphics + +import ( + "fmt" + "image" + "sync" + "sync/atomic" + + "kitty/tools/utils/images" + + "golang.org/x/exp/maps" +) + +var _ = fmt.Print + +type Image struct { + src struct { + path string + data *images.ImageData + size image.Point + loaded bool + } + renderings map[image.Point]*images.ImageData + err error +} + +type ImageCollection struct { + Shm_supported, Files_supported atomic.Bool + mutex sync.Mutex + + images map[string]*Image +} + +func (self *ImageCollection) ResolutionOf(key string) image.Point { + if !self.mutex.TryLock() { + return image.Point{-1, -1} + } + defer self.mutex.Unlock() + i := self.images[key] + if i == nil { + return image.Point{-2, -2} + } + return i.src.size +} + +func (self *ImageCollection) AddPaths(paths ...string) { + self.mutex.Lock() + defer self.mutex.Unlock() + for _, path := range paths { + if self.images[path] == nil { + i := &Image{} + i.src.path = path + self.images[path] = i + } + } +} + +func (self *ImageCollection) LoadAll() { + self.mutex.Lock() + defer self.mutex.Unlock() + ctx := images.Context{} + all := maps.Values(self.images) + ctx.Parallel(0, len(self.images), func(nums <-chan int) { + for i := range nums { + img := all[i] + if !img.src.loaded { + img.src.data, img.err = images.OpenImageFromPath(img.src.path) + if img.err == nil { + img.src.size.X, img.src.size.Y = img.src.data.Width, img.src.data.Height + } + } + } + }) +} + +func NewImageCollection(paths ...string) *ImageCollection { + items := make(map[string]*Image, len(paths)) + for _, path := range paths { + i := &Image{} + i.src.path = path + items[path] = i + } + return &ImageCollection{images: items} +} diff --git a/tools/tui/graphics/command.go b/tools/tui/graphics/command.go index 8c4473402..e4a6dd5b7 100644 --- a/tools/tui/graphics/command.go +++ b/tools/tui/graphics/command.go @@ -8,33 +8,15 @@ import ( "encoding/base64" "fmt" "io" - "os" "strconv" "strings" "kitty/tools/tui/loop" "kitty/tools/utils" - "kitty/tools/utils/shm" ) var _ = fmt.Print -const TempTemplate = "kitty-tty-graphics-protocol-*" - -func CreateTemp() (*os.File, error) { - return os.CreateTemp("", TempTemplate) -} - -func CreateTempInRAM() (*os.File, error) { - if shm.SHM_DIR != "" { - f, err := os.CreateTemp(shm.SHM_DIR, TempTemplate) - if err == nil { - return f, err - } - } - return CreateTemp() -} - // Enums {{{ type GRT_a int diff --git a/tools/utils/images/loading.go b/tools/utils/images/loading.go index a0b56c122..95cae4f18 100644 --- a/tools/utils/images/loading.go +++ b/tools/utils/images/loading.go @@ -17,7 +17,6 @@ import ( "strconv" "strings" - "kitty/tools/tui/graphics" "kitty/tools/utils" "kitty/tools/utils/shm" @@ -27,6 +26,22 @@ import ( var _ = fmt.Print +const TempTemplate = "kitty-tty-graphics-protocol-*" + +func CreateTemp() (*os.File, error) { + return os.CreateTemp("", TempTemplate) +} + +func CreateTempInRAM() (*os.File, error) { + if shm.SHM_DIR != "" { + f, err := os.CreateTemp(shm.SHM_DIR, TempTemplate) + if err == nil { + return f, err + } + } + return CreateTemp() +} + type ImageFrame struct { Width, Height, Left, Top int Number int // 1-based number @@ -189,7 +204,7 @@ type IdentifyRecord struct { Width, Height int Dpi struct{ X, Y float64 } Index int - Mode graphics.GRT_f + Is_opaque bool Needs_blend bool Disposal int Dimensions_swapped bool @@ -258,9 +273,9 @@ func parse_identify_record(ans *IdentifyRecord, raw *IdentifyOutput) (err error) } q := strings.ToLower(raw.Transparency) if q == "blend" || q == "true" { - ans.Mode = graphics.GRT_format_rgba + ans.Is_opaque = false } else { - ans.Mode = graphics.GRT_format_rgb + ans.Is_opaque = true } ans.Needs_blend = q == "blend" switch strings.ToLower(raw.Dispose) { @@ -376,7 +391,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) ( } defer os.RemoveAll(tdir) mode := "rgba" - if frames[0].Mode == graphics.GRT_format_rgb { + if frames[0].Is_opaque { mode = "rgb" } cmd = append(cmd, filepath.Join(tdir, "im-%[filename:f]."+mode)) @@ -431,7 +446,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) ( continue } identify_data := frames[index] - df, cerr := os.CreateTemp(base_dir, graphics.TempTemplate+"."+mode) + df, cerr := os.CreateTemp(base_dir, TempTemplate+"."+mode) if cerr != nil { err = fmt.Errorf("Failed to create a temporary file in %s with error: %w", base_dir, cerr) return @@ -444,7 +459,7 @@ func RenderWithMagick(path string, ro *RenderOptions, frames []IdentifyRecord) ( df.Close() fmap[index+1] = df.Name() frame := ImageFrame{ - Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Mode == graphics.GRT_format_rgb, + Number: index + 1, Width: width, Height: height, Left: x, Top: y, Is_opaque: identify_data.Is_opaque, } frame.set_delay(min_gap, identify_data.Gap) err = check_resize(&frame, df.Name())