Simplify icat code and write to STDOUT rather than the terminal device

The terminal device is now used only for detection.
This commit is contained in:
Kovid Goyal 2023-01-06 16:16:15 +05:30
parent 2205bf4426
commit 3743ae50e7
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
5 changed files with 216 additions and 241 deletions

121
tools/cmd/icat/detect.go Normal file
View File

@ -0,0 +1,121 @@
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
package icat
import (
"errors"
"fmt"
"os"
"time"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/shm"
)
var _ = fmt.Print
func DetectSupport(timeout time.Duration) (memory, files, direct bool, err error) {
var direct_query_id, file_query_id, memory_query_id uint32
lp, e := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if e != nil {
err = e
return
}
print_error := func(format string, args ...any) {
lp.Println(fmt.Sprintf(format, args...))
}
lp.OnInitialize = func() (string, error) {
var iid uint32
lp.AddTimer(timeout, false, func(loop.IdType) error {
return fmt.Errorf("Timed out waiting for a response form the terminal: %w", os.ErrDeadlineExceeded)
})
g := func(t graphics.GRT_t, payload string) uint32 {
iid += 1
g1 := &graphics.GraphicsCommand{}
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(iid).SetDataWidth(1).SetDataHeight(1).SetFormat(
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
return iid
}
direct_query_id = g(graphics.GRT_transmission_direct, "123")
tf, err := graphics.CreateTempInRAM()
if err == nil {
file_query_id = g(graphics.GRT_transmission_tempfile, tf.Name())
temp_files_to_delete = append(temp_files_to_delete, tf.Name())
tf.Write([]byte{1, 2, 3})
tf.Close()
} else {
print_error("Failed to create temporary file for data transfer, file based transfer is disabled. Error: %v", err)
}
sf, err := shm.CreateTemp("icat-", 3)
if err == nil {
memory_query_id = g(graphics.GRT_transmission_sharedmem, sf.Name())
shm_files_to_delete = append(shm_files_to_delete, sf)
copy(sf.Slice(), []byte{1, 2, 3})
sf.Close()
} else {
var ens *shm.ErrNotSupported
if !errors.As(err, &ens) {
print_error("Failed to create SHM for data transfer, memory based transfer is disabled. Error: %v", err)
}
}
lp.QueueWriteString("\x1b[c")
return "", nil
}
lp.OnEscapeCode = func(etype loop.EscapeCodeType, payload []byte) (err error) {
switch etype {
case loop.CSI:
if len(payload) > 3 && payload[0] == '?' && payload[len(payload)-1] == 'c' {
lp.Quit(0)
return nil
}
case loop.APC:
g := graphics.GraphicsCommandFromAPC(payload)
if g != nil {
if g.ResponseMessage() == "OK" {
switch g.ImageId() {
case direct_query_id:
direct = true
case file_query_id:
files = true
case memory_query_id:
memory = true
}
}
return
}
}
return
}
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") {
event.Handled = true
print_error("Waiting for response from terminal, aborting now could lead to corruption")
}
if event.MatchesPressOrRepeat("ctrl+z") {
event.Handled = true
}
return nil
}
err = lp.Run()
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
return
}

View File

@ -3,7 +3,6 @@
package icat
import (
"errors"
"fmt"
"os"
"runtime"
@ -16,11 +15,12 @@ import (
"kitty/tools/tty"
"kitty/tools/tui"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/images"
"kitty/tools/utils/shm"
"kitty/tools/utils/style"
"golang.org/x/sys/unix"
)
var _ = fmt.Print
@ -30,7 +30,6 @@ type Place struct {
}
var opts *Options
var lp *loop.Loop
var place *Place
var z_index int32
var remove_alpha *images.NRGBColor
@ -48,15 +47,17 @@ var transfer_by_file, transfer_by_memory, transfer_by_stream transfer_mode
var temp_files_to_delete []string
var shm_files_to_delete []shm.MMap
var direct_query_id, file_query_id, memory_query_id uint32
var stderr_is_tty bool
var query_in_flight bool
var stream_response string
var files_channel chan input_arg
var output_channel chan *image_data
var num_of_items int
var keep_going *atomic.Bool
var screen_size loop.ScreenSize
var screen_size *unix.Winsize
func send_output(imgd *image_data) {
output_channel <- imgd
}
func parse_mirror() (err error) {
flip = opts.Mirror == "both" || opts.Mirror == "vertical"
@ -128,143 +129,8 @@ func parse_place() (err error) {
}
func print_error(format string, args ...any) {
if lp == nil || !stderr_is_tty {
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
} else {
lp.QueueWriteString("\r")
lp.ClearToEndOfLine()
for _, line := range utils.Splitlines(fmt.Sprintf(format, args...)) {
lp.Println(line)
}
}
}
func on_detect_timeout(timer_id loop.IdType) error {
if query_in_flight {
return fmt.Errorf("Timed out waiting for a response form the terminal")
}
return nil
}
func on_initialize() (string, error) {
var iid uint32
sz, err := lp.ScreenSize()
if err != nil {
return "", fmt.Errorf("Failed to query terminal for screen size with error: %w", err)
}
if sz.WidthPx == 0 || sz.HeightPx == 0 {
return "", fmt.Errorf("Terminal does not support reporting screen sizes in pixels, use a terminal such as kitty, WezTerm, Konsole, etc. that does.")
}
if opts.Clear {
cc := &graphics.GraphicsCommand{}
cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_visible)
cc.WriteWithPayloadToLoop(lp, nil)
}
lp.AddTimer(time.Duration(opts.DetectionTimeout*float64(time.Second)), false, on_detect_timeout)
g := func(t graphics.GRT_t, payload string) uint32 {
iid += 1
g1 := &graphics.GraphicsCommand{}
g1.SetTransmission(t).SetAction(graphics.GRT_action_query).SetImageId(iid).SetDataWidth(1).SetDataHeight(1).SetFormat(
graphics.GRT_format_rgb).SetDataSize(uint64(len(payload)))
g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
return iid
}
keep_going.Store(true)
screen_size = sz
if !opts.DetectSupport && num_of_items > 0 {
num_workers := utils.Max(1, utils.Min(num_of_items, runtime.NumCPU()))
for i := 0; i < num_workers; i++ {
go run_worker()
}
}
if opts.TransferMode != "detect" {
return "", nil
}
query_in_flight = true
direct_query_id = g(graphics.GRT_transmission_direct, "123")
tf, err := graphics.CreateTempInRAM()
if err == nil {
file_query_id = g(graphics.GRT_transmission_tempfile, tf.Name())
temp_files_to_delete = append(temp_files_to_delete, tf.Name())
tf.Write([]byte{1, 2, 3})
tf.Close()
} else {
transfer_by_file = unsupported
print_error("Failed to create temporary file for data transfer, file based transfer is disabled. Error: %v", err)
}
sf, err := shm.CreateTemp("icat-", 3)
if err == nil {
memory_query_id = g(graphics.GRT_transmission_sharedmem, sf.Name())
shm_files_to_delete = append(shm_files_to_delete, sf)
copy(sf.Slice(), []byte{1, 2, 3})
sf.Close()
} else {
transfer_by_memory = unsupported
var ens *shm.ErrNotSupported
if !errors.As(err, &ens) {
print_error("Failed to create SHM for data transfer, memory based transfer is disabled. Error: %v", err)
}
}
lp.QueueWriteString("\x1b[c")
return "", nil
}
func on_query_finished() (err error) {
query_in_flight = false
if transfer_by_stream != supported {
return fmt.Errorf("This terminal emulator does not support the graphics protocol, use a terminal emulator such as kitty that does support it")
}
if opts.DetectSupport {
switch {
case transfer_by_memory == supported:
print_error("memory")
case transfer_by_file == supported:
print_error("file")
default:
print_error("stream")
}
quit_loop()
return
}
return on_wakeup()
}
func on_query_response(g *graphics.GraphicsCommand) (err error) {
var tm *transfer_mode
switch g.ImageId() {
case direct_query_id:
tm = &transfer_by_stream
case file_query_id:
tm = &transfer_by_file
case memory_query_id:
tm = &transfer_by_memory
}
if g.ResponseMessage() == "OK" {
*tm = supported
} else {
*tm = unsupported
}
return
}
func on_escape_code(etype loop.EscapeCodeType, payload []byte) (err error) {
switch etype {
case loop.CSI:
if len(payload) > 3 && payload[0] == '?' && payload[len(payload)-1] == 'c' {
return on_query_finished()
}
case loop.APC:
g := graphics.GraphicsCommandFromAPC(payload)
if g != nil {
if query_in_flight {
return on_query_response(g)
}
}
}
return
fmt.Fprintf(os.Stderr, format, args...)
fmt.Fprintln(os.Stderr)
}
func on_finalize() string {
@ -281,55 +147,6 @@ func on_finalize() string {
return ""
}
var errors_occurred bool = false
func quit_loop() {
if errors_occurred {
lp.Quit(1)
} else {
lp.Quit(0)
}
}
func on_wakeup() error {
if query_in_flight {
return nil
}
have_more := true
for have_more {
select {
case imgd := <-output_channel:
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)
} else {
transmit_image(imgd)
}
default:
have_more = false
}
}
if num_of_items <= 0 && !query_in_flight {
quit_loop()
}
return nil
}
func on_key_event(event *loop.KeyEvent) error {
if event.MatchesPressOrRepeat("ctrl+c") {
event.Handled = true
if query_in_flight {
print_error("Waiting for response from terminal, aborting now could lead to corruption")
return nil
}
return fmt.Errorf("Aborted by user")
}
if event.MatchesPressOrRepeat("ctrl+z") {
event.Handled = true
}
return nil
}
func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
opts = o
err = parse_place()
@ -349,28 +166,30 @@ func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
return 1, err
}
stderr_is_tty = tty.IsTerminal(os.Stderr.Fd())
t, err := tty.OpenControllingTerm()
if err != nil {
return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", err)
}
screen_size, err = t.GetSize()
if err != nil {
return 1, fmt.Errorf("Failed to query terminal using TIOCGWINSZ with error: %w", err)
}
if opts.PrintWindowSize {
t, err := tty.OpenControllingTerm()
if err != nil {
return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", err)
}
sz, err := t.GetSize()
if err != nil {
return 1, fmt.Errorf("Failed to query terminal using TIOCGWINSZ with error: %w", err)
}
fmt.Printf("%dx%d", sz.Xpixel, sz.Ypixel)
fmt.Printf("%dx%d", screen_size.Xpixel, screen_size.Ypixel)
return 0, nil
}
if opts.Clear {
cc := &graphics.GraphicsCommand{}
cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_visible)
cc.WriteWithPayloadTo(os.Stdout, nil)
}
if screen_size.Xpixel == 0 || screen_size.Ypixel == 0 {
return 1, fmt.Errorf("Terminal does not support reporting screen sizes in pixels, use a terminal such as kitty, WezTerm, Konsole, etc. that does.")
}
temp_files_to_delete = make([]string, 0, 8)
shm_files_to_delete = make([]shm.MMap, 0, 8)
lp, err = loop.New(loop.NoAlternateScreen, loop.NoRestoreColors, loop.NoMouseTracking)
if err != nil {
return
}
lp.OnInitialize = on_initialize
lp.OnFinalize = on_finalize
lp.OnEscapeCode = on_escape_code
lp.OnWakeup = on_wakeup
items, err := process_dirs(args...)
if err != nil {
return 1, err
@ -385,18 +204,55 @@ func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
num_of_items = len(items)
output_channel = make(chan *image_data, 1)
keep_going = &atomic.Bool{}
keep_going.Store(true)
if !opts.DetectSupport && num_of_items > 0 {
num_workers := utils.Max(1, utils.Min(num_of_items, runtime.NumCPU()))
for i := 0; i < num_workers; i++ {
go run_worker()
}
}
err = lp.Run()
defer on_finalize()
if opts.TransferMode == "detect" || opts.DetectSupport {
memory, files, direct, err := DetectSupport(time.Duration(opts.DetectionTimeout * float64(time.Second)))
if err != nil {
return 1, err
}
if !direct {
return 1, fmt.Errorf("This terminal does not support the graphics protocol use a terminal such as kitty, WezTerm or Konsole that does")
}
if memory {
transfer_by_memory = supported
} else {
transfer_by_memory = unsupported
}
if files {
transfer_by_file = supported
} else {
transfer_by_file = unsupported
}
}
if opts.DetectSupport {
if transfer_by_memory == supported {
print_error("memory")
} else if transfer_by_file == supported {
print_error("files")
} else {
print_error("stream")
}
return 0, nil
}
for num_of_items > 0 {
imgd := <-output_channel
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)
} else {
transmit_image(imgd)
}
}
keep_going.Store(false)
if err != nil {
return
}
ds := lp.DeathSignalName()
if ds != "" {
fmt.Println("Killed by signal: ", ds)
lp.KillIfSignalled()
return
}
if opts.Hold {
fmt.Print("\r")
if opts.Place != "" {

View File

@ -194,21 +194,16 @@ func set_basic_metadata(imgd *image_data) {
if imgd.frames == nil {
imgd.frames = make([]*image_frame, 0, 32)
}
imgd.available_width = int(screen_size.WidthPx)
imgd.available_width = int(screen_size.Xpixel)
imgd.available_height = 10 * imgd.canvas_height
if place != nil {
imgd.available_width = place.width * int(screen_size.CellWidth)
imgd.available_height = place.height * int(screen_size.CellHeight)
imgd.available_width = place.width * int(screen_size.Xpixel) / int(screen_size.Col)
imgd.available_height = place.height * int(screen_size.Ypixel) / int(screen_size.Row)
}
imgd.needs_scaling = imgd.canvas_width > imgd.available_width || imgd.canvas_height > imgd.available_height || opts.ScaleUp
imgd.needs_conversion = imgd.needs_scaling || remove_alpha != nil || flip || flop || imgd.format_uppercase != "PNG"
}
func send_output(imgd *image_data) {
output_channel <- imgd
lp.WakeupMainThread()
}
func report_error(source_name, msg string, err error) {
imgd := image_data{source_name: source_name, err: fmt.Errorf("%s: %w", msg, err)}
send_output(&imgd)

View File

@ -13,6 +13,7 @@ import (
"path/filepath"
"kitty/tools/tui/graphics"
"kitty/tools/tui/loop"
"kitty/tools/utils"
"kitty/tools/utils/shm"
)
@ -95,7 +96,7 @@ func transmit_shm(imgd *image_data, frame_num int, frame *image_frame) (err erro
gc := gc_for_image(imgd, frame_num, frame)
gc.SetTransmission(graphics.GRT_transmission_sharedmem)
gc.SetDataSize(uint64(data_size))
gc.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(mmap.Name()))
gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(mmap.Name()))
mmap.Close()
return nil
@ -141,7 +142,7 @@ func transmit_file(imgd *image_data, frame_num int, frame *image_frame) (err err
if data_size > 0 {
gc.SetDataSize(uint64(data_size))
}
gc.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(fname))
gc.WriteWithPayloadTo(os.Stdout, utils.UnsafeStringToBytes(fname))
return nil
}
@ -159,7 +160,7 @@ func transmit_stream(imgd *image_data, frame_num int, frame *image_frame) (err e
}
}
gc := gc_for_image(imgd, frame_num, frame)
gc.WriteWithPayloadToLoop(lp, data)
gc.WriteWithPayloadTo(os.Stdout, data)
return nil
}
@ -179,15 +180,15 @@ func calculate_in_cell_x_offset(width, cell_width int) int {
}
func place_cursor(imgd *image_data) {
cw := int(screen_size.CellWidth)
cw := int(screen_size.Xpixel) / int(int(screen_size.Col))
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)))
if place == nil {
switch opts.Align {
case "center":
imgd.move_x_by = (int(screen_size.WidthCells) - num_of_cells_needed) / 2
imgd.move_x_by = (int(screen_size.Col) - num_of_cells_needed) / 2
case "right":
imgd.move_x_by = (int(screen_size.WidthCells) - num_of_cells_needed)
imgd.move_x_by = (int(screen_size.Col) - num_of_cells_needed)
}
} else {
imgd.move_to.x = place.left + 1
@ -242,12 +243,12 @@ func transmit_image(imgd *image_data) {
}
}
place_cursor(imgd)
lp.QueueWriteString("\r")
fmt.Print("\r")
if imgd.move_x_by > 0 {
lp.MoveCursorHorizontally(imgd.move_x_by)
fmt.Printf("\x1b[%dC", imgd.move_x_by)
}
if imgd.move_to.x > 0 {
lp.MoveCursorTo(imgd.move_to.x, imgd.move_to.y)
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)
@ -272,20 +273,20 @@ func transmit_image(imgd *image_data) {
case opts.Loop > 0:
c.SetNumberOfLoops(uint64(opts.Loop) + 1)
}
c.WriteWithPayloadToLoop(lp, nil)
c.WriteWithPayloadTo(os.Stdout, nil)
case 1:
c := frame_control_cmd
c.SetAnimationControl(2) // set animation to loading mode
c.WriteWithPayloadToLoop(lp, nil)
c.WriteWithPayloadTo(os.Stdout, nil)
}
}
}
if is_animated {
c := frame_control_cmd
c.SetAnimationControl(3) // set animation to normal mode
c.WriteWithPayloadToLoop(lp, nil)
c.WriteWithPayloadTo(os.Stdout, nil)
}
if imgd.move_to.x == 0 {
lp.Println() // ensure cursor is on new line
fmt.Println() // ensure cursor is on new line
}
}

View File

@ -261,9 +261,11 @@ func (self *Loop) SetCursorVisible(visible bool) {
}
}
const MoveCursorToTemplate = "\x1b[%d;%dH"
func (self *Loop) MoveCursorTo(x, y int) {
if x > 0 && y > 0 {
self.QueueWriteString(fmt.Sprintf("\x1b[%d;%dH", y, x))
self.QueueWriteString(fmt.Sprintf(MoveCursorToTemplate, y, x))
}
}