mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
More work on image support for diff
This commit is contained in:
parent
9eedcc1d2a
commit
cece795b16
4 changed files with 187 additions and 33 deletions
|
|
@ -3,13 +3,17 @@
|
|||
package diff
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"kitty/tools/tui/graphics"
|
||||
"kitty/tools/tui/sgr"
|
||||
"kitty/tools/utils"
|
||||
"kitty/tools/utils/style"
|
||||
"kitty/tools/wcswidth"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
|
@ -31,10 +35,15 @@ type Reference struct {
|
|||
}
|
||||
|
||||
type LogicalLine struct {
|
||||
src Reference
|
||||
line_type LineType
|
||||
screen_lines []string
|
||||
is_change_start bool
|
||||
src Reference
|
||||
line_type LineType
|
||||
screen_lines []string
|
||||
is_change_start bool
|
||||
left_image, right_image struct {
|
||||
key string
|
||||
count int
|
||||
}
|
||||
image_lines_offset int
|
||||
}
|
||||
|
||||
func (self *LogicalLine) IncrementScrollPosBy(pos *ScrollPos, amt int) (delta int) {
|
||||
|
|
@ -248,7 +257,8 @@ func render_diff_line(number, text, ltype string, margin_size int, available_col
|
|||
return margin + content
|
||||
}
|
||||
|
||||
func image_lines(left_path, right_path string, columns, margin_size int, ans []*LogicalLine) ([]*LogicalLine, error) {
|
||||
func image_lines(left_path, right_path string, screen_size screen_size, margin_size int, image_size graphics.Size, ans []*LogicalLine) ([]*LogicalLine, error) {
|
||||
columns := screen_size.columns
|
||||
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)
|
||||
|
|
@ -267,6 +277,44 @@ func image_lines(left_path, right_path string, columns, margin_size int, ans []*
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ll.image_lines_offset = len(ll.screen_lines)
|
||||
|
||||
do_side := func(path string, filler string) []string {
|
||||
if path == "" {
|
||||
return nil
|
||||
}
|
||||
sz, err := image_collection.GetSizeIfAvailable(path, image_size)
|
||||
if err == nil {
|
||||
count := int(math.Ceil(float64(sz.Height) / float64(screen_size.cell_height)))
|
||||
return utils.Repeat(filler, count)
|
||||
}
|
||||
if errors.Is(err, graphics.ErrNotFound) {
|
||||
return style.WrapTextAsLines("Loading image...", "", available_cols)
|
||||
}
|
||||
return style.WrapTextAsLines(fmt.Sprintf("Failed to load image: %s", err), "", available_cols)
|
||||
}
|
||||
left_lines := do_side(left_path, removed_format(strings.Repeat(` `, available_cols)))
|
||||
if ll.left_image.count = len(left_lines); ll.left_image.count > 0 {
|
||||
ll.left_image.key = left_path
|
||||
}
|
||||
right_lines := do_side(right_path, added_format(strings.Repeat(` `, available_cols)))
|
||||
if ll.right_image.count = len(right_lines); ll.right_image.count > 0 {
|
||||
ll.right_image.key = right_path
|
||||
}
|
||||
filler := filler_format(strings.Repeat(` `, available_cols))
|
||||
m := strings.Repeat(` `, margin_size)
|
||||
get_line := func(i int, which []string, margin_fmt func(...any) string) string {
|
||||
if i < len(which) {
|
||||
return margin_fmt(m) + which[i]
|
||||
}
|
||||
return margin_filler_format(m) + filler
|
||||
}
|
||||
for i := 0; i < utils.Min(len(left_lines), len(right_lines)); i++ {
|
||||
left, right := get_line(i, left_lines, removed_margin_format), get_line(i, right_lines, added_margin_format)
|
||||
ll.screen_lines = append(ll.screen_lines, left+right)
|
||||
}
|
||||
|
||||
ll.line_type = IMAGE_LINE
|
||||
return append(ans, ll), nil
|
||||
}
|
||||
|
||||
|
|
@ -298,7 +346,11 @@ func first_binary_line(left_path, right_path string, columns, margin_size int, r
|
|||
}
|
||||
line = l + r
|
||||
}
|
||||
ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: left_path, linenum: 0}, screen_lines: []string{line}}
|
||||
ref := left_path
|
||||
if ref == "" {
|
||||
ref = right_path
|
||||
}
|
||||
ll := LogicalLine{is_change_start: true, line_type: CHANGE_LINE, src: Reference{path: ref, linenum: 0}, screen_lines: []string{line}}
|
||||
if left_path == "" {
|
||||
ll.src.path = right_path
|
||||
}
|
||||
|
|
@ -519,20 +571,11 @@ func rename_lines(path, other_path string, columns, margin_size int, ans []*Logi
|
|||
return append(ans, &ll), nil
|
||||
}
|
||||
|
||||
func render(collection *Collection, diff_map map[string]*Patch, columns int) (result *LogicalLines, err error) {
|
||||
largest_line_number := 0
|
||||
collection.Apply(func(path, typ, changed_path string) error {
|
||||
if typ == "diff" {
|
||||
patch := diff_map[path]
|
||||
if patch != nil {
|
||||
largest_line_number = utils.Max(largest_line_number, patch.largest_line_number)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
func render(collection *Collection, diff_map map[string]*Patch, screen_size screen_size, largest_line_number int, image_size graphics.Size) (result *LogicalLines, err error) {
|
||||
margin_size := utils.Max(3, len(strconv.Itoa(largest_line_number))+1)
|
||||
ans := make([]*LogicalLine, 0, 1024)
|
||||
empty_line := LogicalLine{line_type: EMPTY_LINE}
|
||||
columns := screen_size.columns
|
||||
err = collection.Apply(func(path, item_type, changed_path string) error {
|
||||
ans = title_lines(path, changed_path, columns, margin_size, ans)
|
||||
defer func() {
|
||||
|
|
@ -550,7 +593,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
|
|||
case "diff":
|
||||
if is_binary {
|
||||
if is_img {
|
||||
ans, err = image_lines(path, changed_path, columns, margin_size, ans)
|
||||
ans, err = image_lines(path, changed_path, screen_size, margin_size, image_size, ans)
|
||||
} else {
|
||||
ans, err = binary_lines(path, changed_path, columns, margin_size, ans)
|
||||
}
|
||||
|
|
@ -563,7 +606,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
|
|||
case "add":
|
||||
if is_binary {
|
||||
if is_img {
|
||||
ans, err = image_lines("", path, columns, margin_size, ans)
|
||||
ans, err = image_lines("", path, screen_size, margin_size, image_size, ans)
|
||||
} else {
|
||||
ans, err = binary_lines("", path, columns, margin_size, ans)
|
||||
}
|
||||
|
|
@ -576,7 +619,7 @@ func render(collection *Collection, diff_map map[string]*Patch, columns int) (re
|
|||
case "removal":
|
||||
if is_binary {
|
||||
if is_img {
|
||||
ans, err = image_lines(path, "", columns, margin_size, ans)
|
||||
ans, err = image_lines(path, "", screen_size, margin_size, image_size, ans)
|
||||
} else {
|
||||
ans, err = binary_lines(path, "", columns, margin_size, ans)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ const (
|
|||
DIFF
|
||||
HIGHLIGHT
|
||||
IMAGE_LOAD
|
||||
IMAGE_RESIZE
|
||||
)
|
||||
|
||||
type ScrollPos struct {
|
||||
|
|
@ -45,10 +46,12 @@ type AsyncResult struct {
|
|||
rtype ResultType
|
||||
collection *Collection
|
||||
diff_map map[string]*Patch
|
||||
page_size graphics.Size
|
||||
}
|
||||
|
||||
var image_collection *graphics.ImageCollection
|
||||
|
||||
type screen_size struct{ rows, columns, num_lines, cell_width, cell_height int }
|
||||
type Handler struct {
|
||||
async_results chan AsyncResult
|
||||
shortcut_tracker config.ShortcutTracker
|
||||
|
|
@ -59,7 +62,7 @@ type Handler struct {
|
|||
lp *loop.Loop
|
||||
current_context_count, original_context_count int
|
||||
added_count, removed_count int
|
||||
screen_size struct{ rows, columns, num_lines int }
|
||||
screen_size screen_size
|
||||
scroll_pos, max_scroll_pos ScrollPos
|
||||
restore_position *ScrollPos
|
||||
inputting_command bool
|
||||
|
|
@ -67,18 +70,30 @@ type Handler struct {
|
|||
rl *readline.Readline
|
||||
current_search *Search
|
||||
current_search_is_regex, current_search_is_backward bool
|
||||
largest_line_number int
|
||||
images_resized_to graphics.Size
|
||||
}
|
||||
|
||||
func (self *Handler) calculate_statistics() {
|
||||
self.added_count, self.removed_count = self.collection.added_count, self.collection.removed_count
|
||||
self.largest_line_number = 0
|
||||
for _, patch := range self.diff_map {
|
||||
self.added_count += patch.added_count
|
||||
self.removed_count += patch.removed_count
|
||||
self.largest_line_number = utils.Max(patch.largest_line_number, self.largest_line_number)
|
||||
}
|
||||
}
|
||||
|
||||
var DebugPrintln = tty.DebugPrintln
|
||||
|
||||
func (self *Handler) update_screen_size(sz loop.ScreenSize) {
|
||||
self.screen_size.rows = int(sz.HeightCells)
|
||||
self.screen_size.columns = int(sz.WidthCells)
|
||||
self.screen_size.num_lines = self.screen_size.rows - 1
|
||||
self.screen_size.cell_height = int(sz.CellHeight)
|
||||
self.screen_size.cell_width = int(sz.CellWidth)
|
||||
}
|
||||
|
||||
func (self *Handler) initialize() {
|
||||
self.rl = readline.New(self.lp, readline.RlInit{DontMarkPrompts: true, Prompt: "/"})
|
||||
image_collection = graphics.NewImageCollection()
|
||||
|
|
@ -87,9 +102,7 @@ func (self *Handler) initialize() {
|
|||
self.current_context_count = int(conf.Num_context_lines)
|
||||
}
|
||||
sz, _ := self.lp.ScreenSize()
|
||||
self.screen_size.rows = int(sz.HeightCells)
|
||||
self.screen_size.columns = int(sz.WidthCells)
|
||||
self.screen_size.num_lines = self.screen_size.rows - 1
|
||||
self.update_screen_size(sz)
|
||||
self.original_context_count = self.current_context_count
|
||||
self.lp.SetDefaultColor(loop.FOREGROUND, conf.Foreground)
|
||||
self.lp.SetDefaultColor(loop.CURSOR, conf.Foreground)
|
||||
|
|
@ -174,6 +187,27 @@ func (self *Handler) load_all_images() {
|
|||
}()
|
||||
}
|
||||
|
||||
func (self *Handler) resize_all_images_if_needed() {
|
||||
if self.logical_lines == nil {
|
||||
return
|
||||
}
|
||||
margin_size := self.logical_lines.margin_size
|
||||
columns := self.logical_lines.columns
|
||||
available_cols := columns/2 - margin_size
|
||||
sz := graphics.Size{
|
||||
Width: available_cols * self.screen_size.cell_width,
|
||||
Height: self.screen_size.num_lines * 2 * self.screen_size.cell_height,
|
||||
}
|
||||
if sz != self.images_resized_to {
|
||||
go func() {
|
||||
image_collection.ResizeForPageSize(sz.Width, sz.Height)
|
||||
r := AsyncResult{rtype: IMAGE_RESIZE, page_size: sz}
|
||||
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()
|
||||
|
|
@ -208,16 +242,17 @@ func (self *Handler) handle_async_result(r AsyncResult) error {
|
|||
self.restore_position = nil
|
||||
}
|
||||
self.draw_screen()
|
||||
case HIGHLIGHT, IMAGE_LOAD:
|
||||
case IMAGE_RESIZE:
|
||||
self.images_resized_to = r.page_size
|
||||
return self.rerender_diff()
|
||||
case IMAGE_LOAD, HIGHLIGHT:
|
||||
return self.rerender_diff()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *Handler) on_resize(old_size, new_size loop.ScreenSize) error {
|
||||
self.screen_size.rows = int(new_size.HeightCells)
|
||||
self.screen_size.num_lines = self.screen_size.rows - 1
|
||||
self.screen_size.columns = int(new_size.WidthCells)
|
||||
self.update_screen_size(new_size)
|
||||
if self.diff_map != nil && self.collection != nil {
|
||||
err := self.render_diff()
|
||||
if err != nil {
|
||||
|
|
@ -238,7 +273,7 @@ func (self *Handler) render_diff() (err error) {
|
|||
if self.screen_size.rows < 2 {
|
||||
return fmt.Errorf("Screen too short, need at least 2 rows")
|
||||
}
|
||||
self.logical_lines, err = render(self.collection, self.diff_map, self.screen_size.columns)
|
||||
self.logical_lines, err = render(self.collection, self.diff_map, self.screen_size, self.largest_line_number, self.images_resized_to)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -259,8 +294,8 @@ func (self *Handler) render_diff() (err error) {
|
|||
func (self *Handler) draw_screen() {
|
||||
self.lp.StartAtomicUpdate()
|
||||
defer self.lp.EndAtomicUpdate()
|
||||
g := (&graphics.GraphicsCommand{}).SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_delete_visible)
|
||||
g.WriteWithPayloadToLoop(self.lp, nil)
|
||||
self.resize_all_images_if_needed()
|
||||
image_collection.DeleteAllPlacements(self.lp)
|
||||
lp.MoveCursorTo(1, 1)
|
||||
lp.ClearToEndOfScreen()
|
||||
if self.logical_lines == nil || self.diff_map == nil || self.collection == nil {
|
||||
|
|
|
|||
|
|
@ -3,10 +3,12 @@
|
|||
package graphics
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"kitty/tools/tui/loop"
|
||||
"kitty/tools/utils/images"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
|
|
@ -34,6 +36,24 @@ type ImageCollection struct {
|
|||
images map[string]*Image
|
||||
}
|
||||
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
func (self *ImageCollection) GetSizeIfAvailable(key string, page_size Size) (Size, error) {
|
||||
if !self.mutex.TryLock() {
|
||||
return Size{}, ErrNotFound
|
||||
}
|
||||
defer self.mutex.Unlock()
|
||||
img := self.images[key]
|
||||
if img == nil {
|
||||
return Size{}, ErrNotFound
|
||||
}
|
||||
ans := img.renderings[page_size]
|
||||
if ans == nil {
|
||||
return Size{}, ErrNotFound
|
||||
}
|
||||
return Size{ans.Width, ans.Height}, img.err
|
||||
}
|
||||
|
||||
func (self *ImageCollection) ResolutionOf(key string) Size {
|
||||
if !self.mutex.TryLock() {
|
||||
return Size{-1, -1}
|
||||
|
|
@ -58,6 +78,40 @@ func (self *ImageCollection) AddPaths(paths ...string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (self *Image) ResizeForPageSize(width, height int) {
|
||||
sz := Size{width, height}
|
||||
if self.renderings[sz] != nil {
|
||||
return
|
||||
}
|
||||
final_width, final_height := images.FitImage(self.src.size.Width, self.src.size.Height, width, height)
|
||||
if final_width == self.src.size.Width && final_height == self.src.data.Height {
|
||||
self.renderings[sz] = self.src.data
|
||||
return
|
||||
}
|
||||
x_frac, y_frac := float64(final_width)/float64(self.src.size.Width), float64(final_height)/float64(self.src.size.Height)
|
||||
self.renderings[sz] = self.src.data.Resize(x_frac, y_frac)
|
||||
}
|
||||
|
||||
func (self *ImageCollection) ResizeForPageSize(width, height int) {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
||||
ctx := images.Context{}
|
||||
keys := maps.Keys(self.images)
|
||||
ctx.Parallel(0, len(keys), func(nums <-chan int) {
|
||||
for i := range nums {
|
||||
img := self.images[keys[i]]
|
||||
img.ResizeForPageSize(width, height)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (self *ImageCollection) DeleteAllPlacements(lp *loop.Loop) {
|
||||
g := &GraphicsCommand{}
|
||||
g.SetAction(GRT_action_delete).SetDelete(GRT_delete_visible)
|
||||
g.WriteWithPayloadToLoop(lp, nil)
|
||||
}
|
||||
|
||||
func (self *ImageCollection) LoadAll() {
|
||||
self.mutex.Lock()
|
||||
defer self.mutex.Unlock()
|
||||
|
|
|
|||
|
|
@ -57,6 +57,28 @@ type ImageData struct {
|
|||
Frames []*ImageFrame
|
||||
}
|
||||
|
||||
func (self *ImageFrame) Resize(x_frac, y_frac float64) *ImageFrame {
|
||||
b := self.Img.Bounds()
|
||||
left, top, width, height := b.Min.X, b.Min.Y, b.Dx(), b.Dy()
|
||||
ans := *self
|
||||
ans.Width = int(x_frac * float64(width))
|
||||
ans.Height = int(y_frac * float64(height))
|
||||
ans.Img = imaging.Resize(self.Img, ans.Width, ans.Height, imaging.Lanczos)
|
||||
ans.Left = int(x_frac * float64(left))
|
||||
ans.Top = int(y_frac * float64(top))
|
||||
return &ans
|
||||
|
||||
}
|
||||
|
||||
func (self *ImageData) Resize(x_frac, y_frac float64) *ImageData {
|
||||
ans := *self
|
||||
ans.Frames = utils.Map(func(f *ImageFrame) *ImageFrame { return f.Resize(x_frac, y_frac) }, self.Frames)
|
||||
if len(ans.Frames) > 0 {
|
||||
ans.Width, ans.Height = ans.Frames[0].Width, ans.Frames[0].Height
|
||||
}
|
||||
return &ans
|
||||
}
|
||||
|
||||
func CalcMinimumGIFGap(gaps []int) int {
|
||||
// Some broken GIF images have all zero gaps, browsers with their usual
|
||||
// idiot ideas render these with a default 100ms gap https://bugzilla.mozilla.org/show_bug.cgi?id=125137
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue