Implement unicode placeholders in icat

This commit is contained in:
Kovid Goyal 2023-03-04 11:54:22 +05:30
parent ed8a88e009
commit 1218a152bf
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
6 changed files with 139 additions and 17 deletions

View File

@ -504,6 +504,10 @@ the desired number of lines and columns::
<ESC>_Ga=p,U=1,i=<image_id>,c=<columns>,r=<rows><ESC>\ <ESC>_Ga=p,U=1,i=<image_id>,c=<columns>,r=<rows><ESC>\
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 The image will eventually be fit to the specified rectangle, its aspect ratio
preserved. Finally, the image can be actually displayed by using the preserved. Finally, the image can be actually displayed by using the
placeholder character, encoding the image ID in its foreground color. The row 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 ``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. ``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. ``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 ``z`` 32-bit integer ``0`` The *z-index* vertical stacking order of the image
**Keys for animation frame loading** **Keys for animation frame loading**

View File

@ -5,6 +5,7 @@ import bz2
import io import io
import json import json
import os import os
import re
import struct import struct
import subprocess import subprocess
import sys import sys
@ -459,6 +460,10 @@ def generate_constants() -> str:
from kitty.options.types import Options from kitty.options.types import Options
from kitty.options.utils import allowed_shell_integration_values from kitty.options.utils import allowed_shell_integration_values
ref_map = load_ref_map() 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)) dp = ", ".join(map(lambda x: f'"{serialize_as_go_string(x)}"', kc.default_pager_for_help))
return f'''\ return f'''\
package kitty package kitty
@ -468,6 +473,7 @@ type VersionType struct {{
}} }}
const VersionString string = "{kc.str_version}" const VersionString string = "{kc.str_version}"
const WebsiteBaseURL string = "{kc.website_base_url}" const WebsiteBaseURL string = "{kc.website_base_url}"
const ImagePlaceholderChar rune = {placeholder_char}
const VCSRevision string = "" const VCSRevision string = ""
const SSHControlMasterTemplate = "{kc.ssh_control_master_template}" const SSHControlMasterTemplate = "{kc.ssh_control_master_template}"
const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}" const RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}"

View File

@ -220,13 +220,18 @@ func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
} }
return 0, nil return 0, nil
} }
use_unicode_placeholder := opts.UnicodePlaceholder
for num_of_items > 0 { for num_of_items > 0 {
imgd := <-output_channel imgd := <-output_channel
imgd.use_unicode_placeholder = use_unicode_placeholder
num_of_items-- num_of_items--
if imgd.err != nil { 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 { } else {
transmit_image(imgd) 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) keep_going.Store(false)

View File

@ -181,9 +181,12 @@ type image_data struct {
scaled_frac struct{ x, y float64 } scaled_frac struct{ x, y float64 }
frames []*image_frame frames []*image_frame
image_number uint32 image_number uint32
image_id uint32
cell_x_offset int cell_x_offset int
move_x_by int move_x_by int
move_to struct{ x, y int } move_to struct{ x, y int }
width_cells, height_cells int
use_unicode_placeholder bool
// for error reporting // for error reporting
err error err error

View File

@ -9,14 +9,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"kitty"
"math" "math"
not_rand "math/rand" not_rand "math/rand"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"kitty/tools/tui/graphics" "kitty/tools/tui/graphics"
"kitty/tools/tui/loop" "kitty/tools/tui/loop"
"kitty/tools/utils" "kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm" "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 { if imgd.image_number != 0 {
gc.SetImageNumber(imgd.image_number) gc.SetImageNumber(imgd.image_number)
} }
if imgd.image_id != 0 {
gc.SetImageId(imgd.image_id)
}
if frame_num == 0 { if frame_num == 0 {
gc.SetAction(graphics.GRT_action_transmit_and_display) 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 { if imgd.cell_x_offset > 0 {
gc.SetXOffset(uint64(imgd.cell_x_offset)) 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) { 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) 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 { if place == nil {
switch opts.Align { switch opts.Align {
case "center": 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": 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 { } else {
imgd.move_to.x = place.left + 1 imgd.move_to.x = place.left + 1
imgd.move_to.y = place.top + 1 imgd.move_to.y = place.top + 1
switch opts.Align { switch opts.Align {
case "center": 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": 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 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) { func transmit_image(imgd *image_data) {
defer func() { defer func() {
for _, frame := range imgd.frames { for _, frame := range imgd.frames {
@ -252,27 +294,49 @@ func transmit_image(imgd *image_data) {
if f == nil { if f == nil {
f = transmit_stream f = transmit_stream
} }
if len(imgd.frames) > 1 { if imgd.use_unicode_placeholder {
for imgd.image_number == 0 { for imgd.image_id&0xFF000000 == 0 || imgd.image_id&0x00FFFF00 == 0 {
imgd.image_number = next_random() // 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) place_cursor(imgd)
fmt.Print("\r") if imgd.use_unicode_placeholder && utils.Max(imgd.width_cells, imgd.height_cells) >= len(images.NumberToDiacritic) {
if imgd.move_x_by > 0 { 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))
fmt.Printf("\x1b[%dC", imgd.move_x_by) return
} }
if imgd.move_to.x > 0 { fmt.Print("\r")
fmt.Printf(loop.MoveCursorToTemplate, imgd.move_to.y, imgd.move_to.x) 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 := 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 is_animated := len(imgd.frames) > 1
for frame_num, frame := range imgd.frames { for frame_num, frame := range imgd.frames {
err := f(imgd, frame_num, frame) err := f(imgd, frame_num, frame)
if err != nil { if err != nil {
print_error("\rFailed to transmit %s with error: %v", imgd.source_name, err) imgd.err = err
return return
} }
if is_animated { 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 { if is_animated {
c := frame_control_cmd c := frame_control_cmd
c.SetAnimationControl(3) // set animation to normal mode c.SetAnimationControl(3) // set animation to normal mode

View File

@ -258,6 +258,28 @@ func GRT_C_from_string(a string) (ans GRT_C, err error) {
return 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 type CompositionMode int
const ( const (
@ -437,6 +459,7 @@ type GraphicsCommand struct {
m GRT_m m GRT_m
C GRT_C C GRT_C
d GRT_d d GRT_d
U GRT_U
s, v, S, O, x, y, w, h, X, Y, c, r uint64 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('o', self.o, null.o)
write_key('m', self.m, null.m) write_key('m', self.m, null.m)
write_key('C', self.C, null.C) write_key('C', self.C, null.C)
write_key('U', self.U, null.U)
write_key('d', self.d, null.d) write_key('d', self.d, null.d)
write_key('s', self.s, null.s) 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) err = set_val(&self.m, GRT_m_from_string, value)
case 'C': case 'C':
err = set_val(&self.C, GRT_C_from_string, value) err = set_val(&self.C, GRT_C_from_string, value)
case 'U':
err = set_val(&self.U, GRT_U_from_string, value)
case 'd': case 'd':
err = set_val(&self.d, GRT_d_from_string, value) err = set_val(&self.d, GRT_d_from_string, value)
case 's': case 's':
@ -776,6 +802,15 @@ func (self *GraphicsCommand) SetCursorMovement(c GRT_C) *GraphicsCommand {
return self 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 { func (self *GraphicsCommand) Format() GRT_f {
return self.f return self.f
} }