mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
327 lines
11 KiB
Go
327 lines
11 KiB
Go
package choose_files
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
|
|
"github.com/kovidgoyal/kitty/tools/tui"
|
|
"github.com/kovidgoyal/kitty/tools/tui/graphics"
|
|
"github.com/kovidgoyal/kitty/tools/tui/loop"
|
|
"github.com/kovidgoyal/kitty/tools/utils"
|
|
"github.com/kovidgoyal/kitty/tools/utils/images"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
type placement struct {
|
|
gc *graphics.GraphicsCommand
|
|
x, y, x_offset int
|
|
}
|
|
|
|
func (p placement) equal(o placement) bool {
|
|
return p.x == o.x && p.x_offset == o.x_offset && p.y == o.y
|
|
}
|
|
|
|
type GraphicsHandler struct {
|
|
running_in_tmux bool
|
|
image_id_counter, detection_file_id uint32
|
|
files_to_delete []string
|
|
files_supported atomic.Bool
|
|
last_rendered_image struct {
|
|
p *ImagePreview
|
|
width, height int
|
|
image_width, image_height int
|
|
}
|
|
image_transmitted uint32
|
|
current_placement, last_transmitted_placement placement
|
|
}
|
|
|
|
func (self *GraphicsHandler) Cleanup() {
|
|
for _, f := range self.files_to_delete {
|
|
_ = os.Remove(f)
|
|
}
|
|
}
|
|
|
|
func (self *GraphicsHandler) new_graphics_command() *graphics.GraphicsCommand {
|
|
gc := graphics.GraphicsCommand{}
|
|
if self.running_in_tmux {
|
|
gc.WrapPrefix = "\033Ptmux;"
|
|
gc.WrapSuffix = "\033\\"
|
|
gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
|
|
}
|
|
return &gc
|
|
}
|
|
|
|
func (self *GraphicsHandler) Initialize(lp *loop.Loop) error {
|
|
tmux := tui.TmuxSocketAddress()
|
|
if tmux != "" && tui.TmuxAllowPassthrough() == nil {
|
|
self.running_in_tmux = true
|
|
}
|
|
if !self.running_in_tmux {
|
|
g := func(t graphics.GRT_t, payload string) uint32 {
|
|
self.image_id_counter++
|
|
g1 := self.new_graphics_command()
|
|
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(self.image_id_counter).SetDataWidth(1).SetDataHeight(1).SetFormat(
|
|
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
|
|
_ = g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
|
|
return self.image_id_counter
|
|
}
|
|
tf, err := images.CreateTempInRAM()
|
|
if err == nil {
|
|
if _, err = tf.Write([]byte{1, 2, 3}); err == nil {
|
|
self.detection_file_id = g(graphics.GRT_transmission_tempfile, tf.Name())
|
|
self.files_to_delete = append(self.files_to_delete, tf.Name())
|
|
}
|
|
tf.Close()
|
|
}
|
|
|
|
}
|
|
self.image_id_counter++
|
|
return nil
|
|
}
|
|
|
|
func (self *GraphicsHandler) free_image_from_terminal(lp *loop.Loop) {
|
|
if self.image_transmitted > 0 {
|
|
self.new_graphics_command().SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_by_id).SetImageId(self.image_transmitted).WriteWithPayloadToLoop(lp, nil)
|
|
self.image_transmitted = 0
|
|
}
|
|
}
|
|
|
|
func (self *GraphicsHandler) Finalize(lp *loop.Loop) {
|
|
self.free_image_from_terminal(lp)
|
|
}
|
|
|
|
func (self *GraphicsHandler) ClearPlacements(lp *loop.Loop) {
|
|
self.current_placement.gc = nil
|
|
}
|
|
|
|
func (self *GraphicsHandler) ApplyPlacements(lp *loop.Loop) {
|
|
if self.current_placement.gc == nil {
|
|
g := self.new_graphics_command()
|
|
g.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_by_id).SetImageId(self.image_transmitted)
|
|
_ = g.WriteWithPayloadToLoop(lp, nil)
|
|
self.last_transmitted_placement.gc = nil
|
|
} else {
|
|
if self.last_transmitted_placement.gc == nil || !self.current_placement.equal(self.last_transmitted_placement) {
|
|
lp.MoveCursorTo(self.current_placement.x, self.current_placement.y)
|
|
_ = self.current_placement.gc.WriteWithPayloadToLoop(lp, nil)
|
|
self.last_transmitted_placement = self.current_placement
|
|
}
|
|
}
|
|
}
|
|
|
|
func (self *GraphicsHandler) HandleGraphicsCommand(gc *graphics.GraphicsCommand) error {
|
|
switch gc.ImageId() {
|
|
case self.detection_file_id:
|
|
if gc.ResponseMessage() == "OK" {
|
|
self.files_supported.Store(true)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (self *GraphicsHandler) cache_resized_image(cdir, cache_key string, img *images.ImageData) (m *images.SerializableImageMetadata, cached_data map[string]string, err error) {
|
|
s, frames := img.Serialize()
|
|
sd, err := json.Marshal(s)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
|
|
if err = os.WriteFile(path, sd, 0o600); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to write resized frame metadata to cache: %w", err)
|
|
}
|
|
cached_data = make(map[string]string, len(frames)+1)
|
|
cached_data[IMAGE_METADATA_KEY] = path
|
|
for i, f := range frames {
|
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
|
|
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
|
if err = os.WriteFile(path, f, 0o600); err != nil {
|
|
return nil, nil, fmt.Errorf("failed to write resized frame %d data to cache: %w", i, err)
|
|
}
|
|
cached_data[key] = path
|
|
}
|
|
m = &s
|
|
return
|
|
}
|
|
|
|
func (self *GraphicsHandler) cached_resized_image(cdir, cache_key string) (m *images.SerializableImageMetadata, cached_data map[string]string) {
|
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-metadata.json", cache_key))
|
|
b, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var s images.SerializableImageMetadata
|
|
if err = json.Unmarshal(b, &s); err != nil {
|
|
return
|
|
}
|
|
m = &s
|
|
cached_data = make(map[string]string, len(s.Frames)+1)
|
|
cached_data[IMAGE_METADATA_KEY] = path
|
|
for i := range len(s.Frames) {
|
|
path := filepath.Join(cdir, fmt.Sprintf("rsz-%s-%d", cache_key, i))
|
|
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
|
cached_data[key] = path
|
|
}
|
|
return
|
|
}
|
|
|
|
func transmit_by_escape_code(lp *loop.Loop, frame []byte, gc *graphics.GraphicsCommand) {
|
|
atomic := lp.IsAtomicUpdateActive()
|
|
lp.EndAtomicUpdate()
|
|
gc.SetTransmission(graphics.GRT_transmission_direct)
|
|
_ = gc.WriteWithPayloadToLoop(lp, frame)
|
|
if atomic {
|
|
lp.StartAtomicUpdate()
|
|
}
|
|
}
|
|
|
|
func transmit_by_file(lp *loop.Loop, frame_path []byte, gc *graphics.GraphicsCommand) {
|
|
gc.SetTransmission(graphics.GRT_transmission_file)
|
|
_ = gc.WriteWithPayloadToLoop(lp, frame_path)
|
|
}
|
|
|
|
func (self *GraphicsHandler) transmit(lp *loop.Loop, img *images.ImageData, m *images.SerializableImageMetadata, cached_data map[string]string) {
|
|
if m == nil {
|
|
s := img.SerializeOnlyMetadata()
|
|
m = &s
|
|
}
|
|
self.image_transmitted = self.image_id_counter
|
|
self.last_transmitted_placement.gc = nil
|
|
self.last_rendered_image.image_width = m.Width
|
|
self.last_rendered_image.image_height = m.Height
|
|
is_animated := len(m.Frames) > 0
|
|
frame_control_cmd := self.new_graphics_command()
|
|
frame_control_cmd.SetAction(graphics.GRT_action_animate).SetImageId(self.image_transmitted)
|
|
for frame_num, frame := range m.Frames {
|
|
gc := self.new_graphics_command()
|
|
gc.SetImageId(self.image_transmitted)
|
|
gc.SetDataWidth(uint64(frame.Width)).SetDataHeight(uint64(frame.Height))
|
|
gc.SetFormat(utils.IfElse(frame.Is_opaque, graphics.GRT_format_rgb, graphics.GRT_format_rgba))
|
|
switch frame_num {
|
|
case 0:
|
|
gc.SetAction(graphics.GRT_action_transmit)
|
|
gc.SetCursorMovement(graphics.GRT_cursor_static)
|
|
default:
|
|
gc.SetAction(graphics.GRT_action_frame)
|
|
gc.SetGap(int32(frame.Delay_ms))
|
|
if frame.Replace {
|
|
gc.SetCompositionMode(graphics.Overwrite)
|
|
}
|
|
if frame.Compose_onto > 0 {
|
|
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
|
|
}
|
|
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
|
|
}
|
|
if cached_data == nil {
|
|
_, _, _, data := img.Frames[frame_num].Data()
|
|
transmit_by_escape_code(lp, data, gc)
|
|
} else {
|
|
path := cached_data[IMAGE_DATA_PREFIX+strconv.Itoa(frame_num)]
|
|
transmit_by_file(lp, utils.UnsafeStringToBytes(path), gc)
|
|
}
|
|
if is_animated {
|
|
switch frame_num {
|
|
case 0:
|
|
// set gap for the first frame and number of loops for the animation
|
|
c := frame_control_cmd
|
|
c.SetTargetFrame(uint64(frame.Number))
|
|
c.SetGap(int32(frame.Delay_ms))
|
|
c.SetNumberOfLoops(1)
|
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
|
case 1:
|
|
c := frame_control_cmd
|
|
c.SetAnimationControl(2) // set animation to loading mode
|
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
|
}
|
|
}
|
|
}
|
|
if is_animated {
|
|
c := frame_control_cmd
|
|
c.SetAnimationControl(3) // set animation to normal mode
|
|
_ = c.WriteWithPayloadToLoop(lp, nil)
|
|
}
|
|
|
|
}
|
|
|
|
func (self *GraphicsHandler) place_image(x, y, px_width, y_offset int, sz ScreenSize) {
|
|
gc := self.new_graphics_command()
|
|
gc.SetAction(graphics.GRT_action_display).SetImageId(self.image_transmitted).SetPlacementId(1).SetCursorMovement(graphics.GRT_cursor_static)
|
|
if extra := px_width - self.last_rendered_image.image_width; extra > 1 {
|
|
extra /= 2
|
|
x += extra / sz.cell_width
|
|
self.current_placement.x_offset = extra % sz.cell_width
|
|
gc.SetXOffset(uint64(self.current_placement.x_offset))
|
|
}
|
|
gc.SetYOffset(uint64(y_offset))
|
|
self.current_placement.x, self.current_placement.y = x, y
|
|
self.current_placement.gc = gc
|
|
}
|
|
|
|
func (self *GraphicsHandler) RenderImagePreview(h *Handler, p *ImagePreview, x, y, width, height int) {
|
|
sz := h.screen_size
|
|
px_width, px_height := width*sz.cell_width, height*sz.cell_height
|
|
y_offset := sz.cell_height / 2
|
|
px_height -= y_offset
|
|
var err error
|
|
defer func() {
|
|
self.last_rendered_image.p = p
|
|
self.last_rendered_image.width, self.last_rendered_image.height = width, height
|
|
if err != nil {
|
|
NewErrorPreview(fmt.Errorf("Failed to render image: %w", err)).Render(h, x, y, width, height)
|
|
} else if self.image_transmitted > 0 {
|
|
self.place_image(x, y, px_width, y_offset, sz)
|
|
}
|
|
}()
|
|
if self.last_rendered_image.p == p && self.last_rendered_image.width == width && self.last_rendered_image.height == height {
|
|
return
|
|
}
|
|
files_supported := self.files_supported.Load()
|
|
|
|
if p.custom_metadata.image.Width <= px_width && p.custom_metadata.image.Height <= px_height {
|
|
if files_supported {
|
|
self.transmit(h.lp, nil, p.custom_metadata.image, p.cached_data)
|
|
} else {
|
|
if err = p.ensure_source_image(); err != nil {
|
|
return
|
|
}
|
|
self.transmit(h.lp, p.source_img, p.custom_metadata.image, nil)
|
|
}
|
|
return
|
|
}
|
|
cache_key := fmt.Sprintf("%d-%d-%p", width, height, p)
|
|
img_metadata, cached_data := self.cached_resized_image(p.disk_cache.ResultsDir(), cache_key)
|
|
var img *images.ImageData
|
|
if len(cached_data) == 0 {
|
|
if err = p.ensure_source_image(); err != nil {
|
|
return
|
|
}
|
|
img = p.source_img
|
|
final_width, final_height := images.FitImage(img.Width, img.Height, px_width, px_height)
|
|
if final_width != img.Width || final_height != img.Height {
|
|
x_frac, y_frac := float64(final_width)/float64(img.Width), float64(final_height)/float64(img.Height)
|
|
img = img.Resize(x_frac, y_frac)
|
|
}
|
|
if img_metadata, cached_data, err = self.cache_resized_image(p.disk_cache.ResultsDir(), cache_key, img); err != nil {
|
|
err = fmt.Errorf("failed to cache resized image: %w", err)
|
|
return
|
|
}
|
|
}
|
|
if files_supported {
|
|
self.transmit(h.lp, img, img_metadata, cached_data)
|
|
} else {
|
|
if img == nil {
|
|
if img, err = load_image(cached_data); err != nil {
|
|
err = fmt.Errorf("failed to load resized image from cache: %w", err)
|
|
return
|
|
}
|
|
}
|
|
self.transmit(h.lp, img, nil, nil)
|
|
}
|
|
}
|