diff --git a/docs/graphics-protocol.rst b/docs/graphics-protocol.rst index e1db0e6df..1577cb148 100644 --- a/docs/graphics-protocol.rst +++ b/docs/graphics-protocol.rst @@ -504,6 +504,10 @@ the desired number of lines and columns:: _Ga=p,U=1,i=,c=,r=\ +The creation of the placement need not be a separate escape code, it can be +combined with ``a=T`` to both transmit and create the virtual placement with a +single code. + The image will eventually be fit to the specified rectangle, its aspect ratio preserved. Finally, the image can be actually displayed by using the placeholder character, encoding the image ID in its foreground color. The row @@ -912,6 +916,8 @@ Key Value Default Description ``r`` Positive integer ``0`` The number of rows to display the image over ``C`` Positive integer ``0`` Cursor movement policy. ``0`` is the default, to move the cursor to after the image. ``1`` is to not move the cursor at all when placing the image. +``U`` Positive integer ``0`` Set to ``1`` to create a virtual placement for a Unicode placeholder. + ``1`` is to not move the cursor at all when placing the image. ``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image **Keys for animation frame loading** diff --git a/gen-go-code.py b/gen-go-code.py index 4c5097b20..6a5792617 100755 --- a/gen-go-code.py +++ b/gen-go-code.py @@ -5,6 +5,7 @@ import bz2 import io import json import os +import re import struct import subprocess import sys @@ -459,6 +460,10 @@ def generate_constants() -> str: from kitty.options.types import Options from kitty.options.utils import allowed_shell_integration_values ref_map = load_ref_map() + with open('kitty/data-types.h') as dt: + m = re.search(r'^#define IMAGE_PLACEHOLDER_CHAR (\S+)', dt.read(), flags=re.M) + assert m is not None + placeholder_char = int(m.group(1), 16) dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help)) return f'''\ package kitty @@ -468,6 +473,7 @@ type VersionType struct {{ }} const VersionString string = "{kc.str_version}" const WebsiteBaseURL string = "{kc.website_base_url}" +const ImagePlaceholderChar rune = {placeholder_char} const VCSRevision string = "" const SSHControlMasterTemplate = "{kc.ssh_control_master_template}" const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}" diff --git a/tools/cmd/icat/main.go b/tools/cmd/icat/main.go index ea0706533..3a4aa566f 100644 --- a/tools/cmd/icat/main.go +++ b/tools/cmd/icat/main.go @@ -220,13 +220,18 @@ func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) { } return 0, nil } + use_unicode_placeholder := opts.UnicodePlaceholder for num_of_items > 0 { imgd := <-output_channel + imgd.use_unicode_placeholder = use_unicode_placeholder num_of_items-- if imgd.err != nil { - print_error("Failed to process \x1b[31m%s\x1b[39m: %v\r\n", imgd.source_name, imgd.err) + print_error("Failed to process \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err) } else { transmit_image(imgd) + if imgd.err != nil { + print_error("Failed to transmit \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err) + } } } keep_going.Store(false) diff --git a/tools/cmd/icat/process_images.go b/tools/cmd/icat/process_images.go index 074951f6f..af7323fa0 100644 --- a/tools/cmd/icat/process_images.go +++ b/tools/cmd/icat/process_images.go @@ -181,9 +181,12 @@ type image_data struct { scaled_frac struct{ x, y float64 } frames []*image_frame image_number uint32 + image_id uint32 cell_x_offset int move_x_by int move_to struct{ x, y int } + width_cells, height_cells int + use_unicode_placeholder bool // for error reporting err error diff --git a/tools/cmd/icat/transmit.go b/tools/cmd/icat/transmit.go index 5db9ac49a..38f04d3a6 100644 --- a/tools/cmd/icat/transmit.go +++ b/tools/cmd/icat/transmit.go @@ -9,14 +9,17 @@ import ( "errors" "fmt" "io" + "kitty" "math" not_rand "math/rand" "os" "path/filepath" + "strings" "kitty/tools/tui/graphics" "kitty/tools/tui/loop" "kitty/tools/utils" + "kitty/tools/utils/images" "kitty/tools/utils/shm" ) @@ -30,8 +33,16 @@ func gc_for_image(imgd *image_data, frame_num int, frame *image_frame) *graphics if imgd.image_number != 0 { gc.SetImageNumber(imgd.image_number) } + if imgd.image_id != 0 { + gc.SetImageId(imgd.image_id) + } if frame_num == 0 { gc.SetAction(graphics.GRT_action_transmit_and_display) + if imgd.use_unicode_placeholder { + gc.SetUnicodePlaceholder(graphics.GRT_create_unicode_placeholder) + gc.SetColumns(uint64(imgd.width_cells)) + gc.SetRows(uint64(imgd.height_cells)) + } if imgd.cell_x_offset > 0 { gc.SetXOffset(uint64(imgd.cell_x_offset)) } @@ -182,24 +193,26 @@ func calculate_in_cell_x_offset(width, cell_width int) int { } func place_cursor(imgd *image_data) { - cw := int(screen_size.Xpixel) / int(int(screen_size.Col)) + cw := int(screen_size.Xpixel) / int(screen_size.Col) + ch := int(screen_size.Ypixel) / int(screen_size.Row) imgd.cell_x_offset = calculate_in_cell_x_offset(imgd.canvas_width, cw) - num_of_cells_needed := int(math.Ceil(float64(imgd.canvas_width) / float64(cw))) + imgd.width_cells = int(math.Ceil(float64(imgd.canvas_width) / float64(cw))) + imgd.height_cells = int(math.Ceil(float64(imgd.canvas_height) / float64(ch))) if place == nil { switch opts.Align { case "center": - imgd.move_x_by = (int(screen_size.Col) - num_of_cells_needed) / 2 + imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells) / 2 case "right": - imgd.move_x_by = (int(screen_size.Col) - num_of_cells_needed) + imgd.move_x_by = (int(screen_size.Col) - imgd.width_cells) } } else { imgd.move_to.x = place.left + 1 imgd.move_to.y = place.top + 1 switch opts.Align { case "center": - imgd.move_to.x += (place.width - num_of_cells_needed) / 2 + imgd.move_to.x += (place.width - imgd.width_cells) / 2 case "right": - imgd.move_to.x += (place.width - num_of_cells_needed) + imgd.move_to.x += (place.width - imgd.width_cells) } } } @@ -217,6 +230,35 @@ func next_random() (ans uint32) { return ans } +func write_unicode_placeholder(imgd *image_data) { + prefix := "" + foreground := fmt.Sprintf("\033[38:2:%d:%d:%dm", (imgd.image_id>>16)&255, (imgd.image_id>>8)&255, imgd.image_id&255) + os.Stdout.WriteString(loop.SAVE_PRIVATE_MODE_VALUES + foreground) + restore := "\033[39m" + loop.RESTORE_PRIVATE_MODE_VALUES + if imgd.move_to.y > 0 { + fmt.Print(loop.SAVE_CURSOR) + restore += loop.RESTORE_CURSOR + } else if imgd.move_x_by > 0 { + prefix = strings.Repeat(" ", imgd.move_x_by) + } + defer func() { os.Stdout.WriteString(restore) }() + if imgd.move_to.y > 0 { + fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, 0) + } + id_char := string(images.NumberToDiacritic[(imgd.image_id>>24)&255]) + for r := 0; r < imgd.height_cells; r++ { + if imgd.move_to.x > 0 { + fmt.Printf("\x1b[%dC", imgd.move_to.x) + } else { + os.Stdout.WriteString(prefix) + } + for c := 0; c < imgd.width_cells; c++ { + os.Stdout.WriteString(string(kitty.ImagePlaceholderChar) + string(images.NumberToDiacritic[r]) + string(images.NumberToDiacritic[c]) + id_char) + } + os.Stdout.WriteString("\n\r") + } +} + func transmit_image(imgd *image_data) { defer func() { for _, frame := range imgd.frames { @@ -252,27 +294,49 @@ func transmit_image(imgd *image_data) { if f == nil { f = transmit_stream } - if len(imgd.frames) > 1 { - for imgd.image_number == 0 { - imgd.image_number = next_random() + if imgd.use_unicode_placeholder { + for imgd.image_id&0xFF000000 == 0 || imgd.image_id&0x00FFFF00 == 0 { + // Generate a 32-bit image id using rejection sampling such that the most + // significant byte and the two bytes in the middle are non-zero to avoid + // collisions with applications that cannot represent non-zero most + // significant bytes (which is represented by the third combining character) + // or two non-zero bytes in the middle (which requires 24-bit color mode). + imgd.image_id = next_random() + } + } else { + if len(imgd.frames) > 1 { + for imgd.image_number == 0 { + imgd.image_number = next_random() + } } } place_cursor(imgd) - fmt.Print("\r") - if imgd.move_x_by > 0 { - fmt.Printf("\x1b[%dC", imgd.move_x_by) + if imgd.use_unicode_placeholder && utils.Max(imgd.width_cells, imgd.height_cells) >= len(images.NumberToDiacritic) { + imgd.err = fmt.Errorf("Image too large to be displayed using Unicode placeholders. Maximum size is %dx%d cells", len(images.NumberToDiacritic), len(images.NumberToDiacritic)) + return } - if imgd.move_to.x > 0 { - fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, imgd.move_to.x) + fmt.Print("\r") + if !imgd.use_unicode_placeholder { + if imgd.move_x_by > 0 { + fmt.Printf("\x1b[%dC", imgd.move_x_by) + } + if imgd.move_to.x > 0 { + fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, imgd.move_to.x) + } } frame_control_cmd := graphics.GraphicsCommand{} - frame_control_cmd.SetAction(graphics.GRT_action_animate).SetImageNumber(imgd.image_number) + frame_control_cmd.SetAction(graphics.GRT_action_animate) + if imgd.image_id != 0 { + frame_control_cmd.SetImageId(imgd.image_id) + } else { + frame_control_cmd.SetImageNumber(imgd.image_number) + } is_animated := len(imgd.frames) > 1 for frame_num, frame := range imgd.frames { err := f(imgd, frame_num, frame) if err != nil { - print_error("\rFailed to transmit %s with error: %v", imgd.source_name, err) + imgd.err = err return } if is_animated { @@ -296,6 +360,9 @@ func transmit_image(imgd *image_data) { } } } + if imgd.use_unicode_placeholder { + write_unicode_placeholder(imgd) + } if is_animated { c := frame_control_cmd c.SetAnimationControl(3) // set animation to normal mode diff --git a/tools/tui/graphics/command.go b/tools/tui/graphics/command.go index 9b1d91ea4..6fa9f1efb 100644 --- a/tools/tui/graphics/command.go +++ b/tools/tui/graphics/command.go @@ -258,6 +258,28 @@ func GRT_C_from_string(a string) (ans GRT_C, err error) { return } +type GRT_U int + +const ( + GRT_no_unicode_placeholder GRT_U = iota + GRT_create_unicode_placeholder +) + +func (self GRT_U) String() string { + return strconv.Itoa(int(self)) +} + +func GRT_U_from_string(a string) (ans GRT_U, err error) { + switch a { + case "0": + case "1": + ans = GRT_create_unicode_placeholder + default: + err = fmt.Errorf("Not a valid cursor movement value: %#v", a) + } + return +} + type CompositionMode int const ( @@ -437,6 +459,7 @@ type GraphicsCommand struct { m GRT_m C GRT_C d GRT_d + U GRT_U s, v, S, O, x, y, w, h, X, Y, c, r uint64 @@ -464,6 +487,7 @@ func (self *GraphicsCommand) serialize_non_default_fields() (ans []string) { write_key('o', self.o, null.o) write_key('m', self.m, null.m) write_key('C', self.C, null.C) + write_key('U', self.U, null.U) write_key('d', self.d, null.d) write_key('s', self.s, null.s) @@ -637,6 +661,8 @@ func (self *GraphicsCommand) SetString(key byte, value string) (err error) { err = set_val(&self.m, GRT_m_from_string, value) case 'C': err = set_val(&self.C, GRT_C_from_string, value) + case 'U': + err = set_val(&self.U, GRT_U_from_string, value) case 'd': err = set_val(&self.d, GRT_d_from_string, value) case 's': @@ -776,6 +802,15 @@ func (self *GraphicsCommand) SetCursorMovement(c GRT_C) *GraphicsCommand { return self } +func (self *GraphicsCommand) UnicodePlaceholder() GRT_U { + return self.U +} + +func (self *GraphicsCommand) SetUnicodePlaceholder(U GRT_U) *GraphicsCommand { + self.U = U + return self +} + func (self *GraphicsCommand) Format() GRT_f { return self.f }