mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-06 01:05:48 +02:00
More work on image preview rendering
This commit is contained in:
294
kittens/choose_files/graphics.go
Normal file
294
kittens/choose_files/graphics.go
Normal file
@@ -0,0 +1,294 @@
|
||||
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 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
|
||||
has_placements bool
|
||||
}
|
||||
|
||||
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) Finalize(lp *loop.Loop) {
|
||||
if self.image_transmitted > 0 {
|
||||
g := self.new_graphics_command()
|
||||
g.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_by_id).SetImageId(self.image_transmitted)
|
||||
_ = g.WriteWithPayloadToLoop(lp, nil)
|
||||
self.image_transmitted = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (self *GraphicsHandler) ClearPlacements(lp *loop.Loop) {
|
||||
if self.image_transmitted > 0 && self.has_placements {
|
||||
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.has_placements = false
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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.last_rendered_image.image_width = m.Width
|
||||
self.last_rendered_image.image_height = m.Height
|
||||
is_animated := len(m.Frames) > 0
|
||||
self.image_transmitted = self.image_id_counter
|
||||
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))
|
||||
if frame.Is_opaque {
|
||||
gc.SetFormat(graphics.GRT_format_rgb)
|
||||
}
|
||||
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.Compose_onto > 0 {
|
||||
gc.SetOverlaidFrame(uint64(frame.Compose_onto))
|
||||
}
|
||||
gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
|
||||
}
|
||||
if cached_data == nil {
|
||||
transmit_by_escape_code(lp, img.Frames[frame_num].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(lp *loop.Loop, x, y, px_width int, sz ScreenSize) {
|
||||
self.has_placements = true
|
||||
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 > 0 {
|
||||
x += extra / sz.cell_width
|
||||
gc.SetXOffset(uint64(extra % sz.cell_width))
|
||||
}
|
||||
lp.MoveCursorTo(x, y)
|
||||
_ = gc.WriteWithPayloadToLoop(lp, nil)
|
||||
}
|
||||
|
||||
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
|
||||
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(h.lp, x, y, px_width, 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.img_metadata.Width <= px_width && p.img_metadata.Height <= px_height {
|
||||
if files_supported {
|
||||
self.transmit(h.lp, nil, p.img_metadata, p.cached_data)
|
||||
} else {
|
||||
if err = p.ensure_source_image(); err != nil {
|
||||
return
|
||||
}
|
||||
self.transmit(h.lp, p.source_img, p.img_metadata, 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 {
|
||||
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, p.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)
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
package choose_files
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/kovidgoyal/kitty/tools/disk_cache"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/humanize"
|
||||
"github.com/kovidgoyal/kitty/tools/utils/images"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
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()
|
||||
@@ -21,14 +28,24 @@ var preview_cache = sync.OnceValues(func() (*disk_cache.DiskCache, error) {
|
||||
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
|
||||
img_metadata *images.SerializableImageMetadata
|
||||
}
|
||||
|
||||
type PreviewRenderer interface {
|
||||
Render(string) (map[string][]byte, error)
|
||||
ShowMetadata(h *Handler, abspath string, metadata fs.FileInfo, x, y, width, height int, cached_data map[string]string) int
|
||||
Render(string) (map[string][]byte, *images.ImageData, error)
|
||||
ShowMetadata(h *Handler, s ShowData) int
|
||||
}
|
||||
|
||||
type render_data struct {
|
||||
cached_data map[string]string
|
||||
err error
|
||||
cached_data map[string]string
|
||||
img *images.ImageData
|
||||
img_metadata *images.SerializableImageMetadata
|
||||
err error
|
||||
}
|
||||
|
||||
type ImagePreview struct {
|
||||
@@ -38,17 +55,72 @@ type ImagePreview struct {
|
||||
cached_data map[string]string
|
||||
render_err Preview
|
||||
render_channel chan render_data
|
||||
source_img *images.ImageData
|
||||
img_metadata *images.SerializableImageMetadata
|
||||
renderer PreviewRenderer
|
||||
file_metadata_preview Preview
|
||||
WakeupMainThread func() bool
|
||||
}
|
||||
|
||||
func (p ImagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||
func (p *ImagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||
|
||||
func (p ImagePreview) Render(h *Handler, x, y, width, height int) {
|
||||
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(h *Handler, x, y, width, height int) {
|
||||
if p.render_channel == nil {
|
||||
if p.render_err == nil {
|
||||
y += p.renderer.ShowMetadata(h, p.abspath, p.metadata, x, y, width, height, p.cached_data)
|
||||
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,
|
||||
img_metadata: p.img_metadata,
|
||||
})
|
||||
y += offset
|
||||
height -= offset
|
||||
h.graphics_handler.RenderImagePreview(h, p, x, y, width, height)
|
||||
|
||||
} else {
|
||||
p.render_err.Render(h, x, y, width, height)
|
||||
}
|
||||
@@ -58,6 +130,8 @@ func (p ImagePreview) Render(h *Handler, x, y, width, height int) {
|
||||
case hd := <-p.render_channel:
|
||||
p.render_channel = nil
|
||||
p.cached_data = hd.cached_data
|
||||
p.source_img = hd.img
|
||||
p.img_metadata = hd.img_metadata
|
||||
p.render_err = NewErrorPreview(fmt.Errorf("Failed to render the preview with error: %w", hd.err))
|
||||
p.Render(h, x, y, width, height)
|
||||
return
|
||||
@@ -75,28 +149,60 @@ func (p *ImagePreview) start_rendering() {
|
||||
}()
|
||||
key, ans, err := p.disk_cache.GetPath(p.abspath)
|
||||
if err != nil {
|
||||
p.render_channel <- render_data{nil, err}
|
||||
}
|
||||
if len(ans) > 0 {
|
||||
p.render_channel <- render_data{ans, nil}
|
||||
p.render_channel <- render_data{err: err}
|
||||
return
|
||||
}
|
||||
rdata, err := p.renderer.Render(p.abspath)
|
||||
if len(ans) > 0 {
|
||||
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 {
|
||||
p.render_channel <- render_data{cached_data: ans, img_metadata: &m}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
rdata, img, err := p.renderer.Render(p.abspath)
|
||||
if err != nil {
|
||||
p.render_channel <- render_data{nil, err}
|
||||
p.render_channel <- render_data{err: err}
|
||||
} else {
|
||||
ans, err = p.disk_cache.AddPath(p.abspath, key, rdata)
|
||||
p.render_channel <- render_data{utils.IfElse(err == nil, ans, nil), err}
|
||||
if err == nil {
|
||||
m := img.SerializeOnlyMetadata()
|
||||
p.render_channel <- render_data{cached_data: ans, img_metadata: &m, img: img}
|
||||
} else {
|
||||
p.render_channel <- render_data{err: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ImagePreviewRenderer uint
|
||||
|
||||
func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, err error) {
|
||||
func (p ImagePreviewRenderer) Render(abspath string) (ans map[string][]byte, img *images.ImageData, err error) {
|
||||
if img, err = images.OpenImageFromPath(abspath); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
m, data := img.Serialize()
|
||||
ans = make(map[string][]byte, len(data)+1)
|
||||
metadata, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ans[IMAGE_METADATA_KEY] = metadata
|
||||
for i, d := range data {
|
||||
key := IMAGE_DATA_PREFIX + strconv.Itoa(i)
|
||||
ans[key] = d
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p ImagePreviewRenderer) ShowMetadata(h *Handler, abspath string, metadata fs.FileInfo, x, y, width, height int, cached_data map[string]string) int {
|
||||
func (p ImagePreviewRenderer) ShowMetadata(h *Handler, s ShowData) int {
|
||||
text := ""
|
||||
if s.img_metadata != nil {
|
||||
text = fmt.Sprintf("%s: %dx%d %s", s.img_metadata.Format_uppercase, s.img_metadata.Width, s.img_metadata.Height, humanize.Bytes(uint64(s.metadata.Size())))
|
||||
}
|
||||
h.render_wrapped_text_in_region(text, s.x, s.y, s.width, s.height, false)
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/kovidgoyal/kitty/tools/ignorefiles"
|
||||
"github.com/kovidgoyal/kitty/tools/tty"
|
||||
"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/tui/readline"
|
||||
"github.com/kovidgoyal/kitty/tools/utils"
|
||||
@@ -206,16 +207,29 @@ type ScreenSize struct {
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
state State
|
||||
screen_size ScreenSize
|
||||
result_manager *ResultManager
|
||||
lp *loop.Loop
|
||||
rl *readline.Readline
|
||||
err_chan chan error
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
msg_printer *message.Printer
|
||||
spinner *tui.Spinner
|
||||
preview_manager *PreviewManager
|
||||
state State
|
||||
screen_size ScreenSize
|
||||
result_manager *ResultManager
|
||||
lp *loop.Loop
|
||||
rl *readline.Readline
|
||||
err_chan chan error
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
msg_printer *message.Printer
|
||||
spinner *tui.Spinner
|
||||
preview_manager *PreviewManager
|
||||
last_rendered_preview Preview
|
||||
graphics_handler GraphicsHandler
|
||||
}
|
||||
|
||||
func (self *Handler) on_escape_code(etype loop.EscapeCodeType, payload []byte) error {
|
||||
switch etype {
|
||||
case loop.APC:
|
||||
gc := graphics.GraphicsCommandFromAPC(payload)
|
||||
if gc != nil {
|
||||
return self.graphics_handler.HandleGraphicsCommand(gc)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *Handler) draw_screen() (err error) {
|
||||
@@ -226,7 +240,8 @@ func (h *Handler) draw_screen() (err error) {
|
||||
h.state.mouse_state.ApplyHoverStyles(h.lp)
|
||||
h.lp.EndAtomicUpdate()
|
||||
}()
|
||||
h.lp.ClearScreen()
|
||||
h.lp.ClearScreenButNotGraphics()
|
||||
h.graphics_handler.ClearPlacements(h.lp)
|
||||
h.state.mouse_state.ClearCellRegions()
|
||||
switch h.state.screen {
|
||||
case NORMAL:
|
||||
@@ -294,6 +309,7 @@ func (h *Handler) OnInitialize() (ans string, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
err = h.graphics_handler.Initialize(h.lp)
|
||||
h.result_manager.set_root_dir()
|
||||
h.draw_screen()
|
||||
return
|
||||
@@ -778,6 +794,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
lp.MouseTrackingMode(loop.FULL_MOUSE_TRACKING)
|
||||
lp.ColorSchemeChangeNotifications()
|
||||
handler := Handler{lp: lp, err_chan: make(chan error, 8), msg_printer: message.NewPrinter(utils.LanguageTag()), spinner: tui.NewSpinner("dots")}
|
||||
defer handler.graphics_handler.Cleanup()
|
||||
handler.rl = readline.New(lp, readline.RlInit{
|
||||
Prompt: "> ", ContinuationPrompt: ". ", Completer: FilePromptCompleter(handler.state.CurrentDir),
|
||||
})
|
||||
@@ -813,6 +830,10 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
lp.RequestCurrentColorScheme()
|
||||
return handler.OnInitialize()
|
||||
}
|
||||
lp.OnFinalize = func() string {
|
||||
handler.graphics_handler.Finalize(lp)
|
||||
return ""
|
||||
}
|
||||
lp.OnResize = func(old, new_size loop.ScreenSize) (err error) {
|
||||
handler.init_sizes(new_size)
|
||||
return handler.draw_screen()
|
||||
@@ -829,6 +850,7 @@ func main(_ *cli.Command, opts *Options, args []string) (rc int, err error) {
|
||||
lp.OnKeyEvent = handler.OnKeyEvent
|
||||
lp.OnText = handler.OnText
|
||||
lp.OnMouseEvent = handler.OnMouseEvent
|
||||
lp.OnEscapeCode = handler.on_escape_code
|
||||
lp.OnWakeup = func() (err error) {
|
||||
select {
|
||||
case err = <-handler.err_chan:
|
||||
|
||||
@@ -25,6 +25,7 @@ var _ = fmt.Print
|
||||
type Preview interface {
|
||||
Render(h *Handler, x, y, width, height int)
|
||||
IsValidForColorScheme(light bool) bool
|
||||
Unload()
|
||||
}
|
||||
|
||||
type PreviewManager struct {
|
||||
@@ -80,6 +81,7 @@ type MessagePreview struct {
|
||||
|
||||
func (p MessagePreview) IsValidForColorScheme(bool) bool { return true }
|
||||
|
||||
func (p MessagePreview) Unload() {}
|
||||
func (p MessagePreview) Render(h *Handler, x, y, width, height int) {
|
||||
offset := 0
|
||||
if p.title != "" {
|
||||
@@ -189,6 +191,8 @@ type TextFilePreview struct {
|
||||
|
||||
func (p TextFilePreview) IsValidForColorScheme(light bool) bool { return p.light == light }
|
||||
|
||||
func (p *TextFilePreview) Unload() {}
|
||||
|
||||
func (p *TextFilePreview) Render(h *Handler, x, y, width, height int) {
|
||||
if p.highlighted_chan != nil {
|
||||
select {
|
||||
@@ -316,9 +320,14 @@ func (h *Handler) draw_preview_content(x, y, width, height int) {
|
||||
return
|
||||
}
|
||||
abspath := filepath.Join(h.state.CurrentDir(), r.text)
|
||||
if h.last_rendered_preview != nil {
|
||||
h.last_rendered_preview.Unload()
|
||||
h.last_rendered_preview = nil
|
||||
}
|
||||
if p := h.preview_manager.preview_for(abspath, r.ftype); p == nil {
|
||||
h.render_wrapped_text_in_region("No preview available", x, y, width, height, false)
|
||||
} else {
|
||||
h.last_rendered_preview = p
|
||||
p.Render(h, x, y, width, height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ func NewDiskCache(path string, max_size int64) (dc *DiskCache, err error) {
|
||||
return new_disk_cache(path, max_size)
|
||||
}
|
||||
|
||||
func (dc *DiskCache) ResultsDir() string {
|
||||
return dc.get_dir
|
||||
}
|
||||
|
||||
func (dc *DiskCache) Get(key string, items ...string) (map[string]string, error) {
|
||||
dc.lock()
|
||||
defer dc.unlock()
|
||||
|
||||
@@ -157,11 +157,10 @@ func (dc *DiskCache) write_entries_if_dirty() (err error) {
|
||||
removed = true
|
||||
}
|
||||
}()
|
||||
if err := os.WriteFile(temp, d, 0o600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.Rename(temp, path); err == nil {
|
||||
removed = true
|
||||
if err = os.WriteFile(temp, d, 0o600); err == nil {
|
||||
if err = os.Rename(temp, path); err == nil {
|
||||
removed = true
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -175,13 +175,20 @@ type SerializableImageMetadata struct {
|
||||
|
||||
const SERIALIZE_VERSION = 1
|
||||
|
||||
func (self *ImageData) SerializeOnlyMetadata() SerializableImageMetadata {
|
||||
f := make([]SerializableImageFrame, len(self.Frames))
|
||||
for i, s := range self.Frames {
|
||||
f[i] = s.Serialize()
|
||||
}
|
||||
return SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase, Frames: f}
|
||||
}
|
||||
|
||||
func (self *ImageData) Serialize() (SerializableImageMetadata, [][]byte) {
|
||||
m := SerializableImageMetadata{Version: SERIALIZE_VERSION, Width: self.Width, Height: self.Height, Format_uppercase: self.Format_uppercase}
|
||||
m := self.SerializeOnlyMetadata()
|
||||
data := make([][]byte, len(self.Frames))
|
||||
for i, f := range self.Frames {
|
||||
m.Frames = append(m.Frames, f.Serialize())
|
||||
data[i] = f.Data()
|
||||
m.Frames[len(m.Frames)-1].Size = len(data[i])
|
||||
m.Frames[i].Size = len(data[i])
|
||||
}
|
||||
return m, data
|
||||
}
|
||||
@@ -272,29 +279,7 @@ func MakeTempDir(template string) (ans string, err error) {
|
||||
return os.MkdirTemp("", template)
|
||||
}
|
||||
|
||||
func check_resize(frame *ImageFrame, filename string) error {
|
||||
// ImageMagick sometimes generates RGBA images smaller than the specified
|
||||
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
||||
s, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sz := int(s.Size())
|
||||
bytes_per_pixel := 4
|
||||
if frame.Is_opaque {
|
||||
bytes_per_pixel = 3
|
||||
}
|
||||
expected_size := bytes_per_pixel * frame.Width * frame.Height
|
||||
if sz < expected_size {
|
||||
missing := expected_size - sz
|
||||
if missing%(bytes_per_pixel*frame.Width) != 0 {
|
||||
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
|
||||
}
|
||||
frame.Height -= missing / (bytes_per_pixel * frame.Width)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Native {{{
|
||||
func (frame *ImageFrame) set_delay(min_gap, delay int) {
|
||||
frame.Delay_ms = int32(max(min_gap, delay) * 10)
|
||||
if frame.Delay_ms == 0 {
|
||||
@@ -344,11 +329,36 @@ func OpenNativeImageFromReader(f io.ReadSeeker) (ans *ImageData, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// }}}
|
||||
|
||||
// ImageMagick {{{
|
||||
var MagickExe = sync.OnceValue(func() string {
|
||||
return utils.FindExe("magick")
|
||||
})
|
||||
|
||||
func check_resize(frame *ImageFrame, filename string) error {
|
||||
// ImageMagick sometimes generates RGBA images smaller than the specified
|
||||
// size. See https://github.com/kovidgoyal/kitty/issues/276 for examples
|
||||
s, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sz := int(s.Size())
|
||||
bytes_per_pixel := 4
|
||||
if frame.Is_opaque {
|
||||
bytes_per_pixel = 3
|
||||
}
|
||||
expected_size := bytes_per_pixel * frame.Width * frame.Height
|
||||
if sz < expected_size {
|
||||
missing := expected_size - sz
|
||||
if missing%(bytes_per_pixel*frame.Width) != 0 {
|
||||
return fmt.Errorf("ImageMagick failed to resize correctly. It generated %d < %d of data (w=%d h=%d bpp=%d)", sz, expected_size, frame.Width, frame.Height, bytes_per_pixel)
|
||||
}
|
||||
frame.Height -= missing / (bytes_per_pixel * frame.Width)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunMagick(path string, cmd []string) ([]byte, error) {
|
||||
if MagickExe() != "magick" {
|
||||
cmd = append([]string{MagickExe()}, cmd...)
|
||||
|
||||
Reference in New Issue
Block a user