diff --git a/tools/cmd/icat/detect.go b/tools/cmd/icat/detect.go new file mode 100644 index 000000000..f5b1457f1 --- /dev/null +++ b/tools/cmd/icat/detect.go @@ -0,0 +1,121 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +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 +} diff --git a/tools/cmd/icat/main.go b/tools/cmd/icat/main.go index b3beceed3..d1f76cde2 100644 --- a/tools/cmd/icat/main.go +++ b/tools/cmd/icat/main.go @@ -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 != "" { diff --git a/tools/cmd/icat/process_images.go b/tools/cmd/icat/process_images.go index c2c052663..074951f6f 100644 --- a/tools/cmd/icat/process_images.go +++ b/tools/cmd/icat/process_images.go @@ -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) diff --git a/tools/cmd/icat/transmit.go b/tools/cmd/icat/transmit.go index 05be0b132..f95c89850 100644 --- a/tools/cmd/icat/transmit.go +++ b/tools/cmd/icat/transmit.go @@ -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 } } diff --git a/tools/tui/loop/api.go b/tools/tui/loop/api.go index 490097e68..cae026281 100644 --- a/tools/tui/loop/api.go +++ b/tools/tui/loop/api.go @@ -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)) } }