// License: GPLv3 Copyright: 2022, Kovid Goyal, package icat import ( "fmt" "math" "os" "runtime" "slices" "strconv" "strings" "sync/atomic" "time" "github.com/kovidgoyal/imaging" "github.com/kovidgoyal/kitty/tools/cli" "github.com/kovidgoyal/kitty/tools/tty" "github.com/kovidgoyal/kitty/tools/tui" "github.com/kovidgoyal/kitty/tools/tui/graphics" "github.com/kovidgoyal/kitty/tools/utils" "github.com/kovidgoyal/kitty/tools/utils/style" "golang.org/x/sys/unix" ) var _ = fmt.Print type Place struct { width, height, left, top int } var opts *Options var place *Place var z_index int32 var remove_alpha *imaging.NRGBColor var flip, flop bool type transfer_mode int const ( unknown transfer_mode = iota unsupported supported ) type fit_t int const ( fit_none fit_t = iota fit_width fit_height fit_both ) var transfer_by_file, transfer_by_memory transfer_mode 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 *unix.Winsize var fit_mode fit_t func send_output(imgd *image_data) { output_channel <- imgd } func parse_mirror() (err error) { flip = opts.Mirror == "both" || opts.Mirror == "vertical" flop = opts.Mirror == "both" || opts.Mirror == "horizontal" return } func parse_background() (err error) { if opts.Background == "" || opts.Background == "none" { return nil } col, err := style.ParseColor(opts.Background) if err != nil { return fmt.Errorf("Invalid value for --background: %w", err) } remove_alpha = &imaging.NRGBColor{R: col.Red, G: col.Green, B: col.Blue} return } func parse_z_index() (err error) { val := opts.ZIndex var origin int32 if strings.HasPrefix(val, "--") { origin = -1073741824 val = val[1:] } i, err := strconv.ParseInt(val, 10, 32) if err != nil { return fmt.Errorf("Invalid value for --z-index with error: %w", err) } z_index = int32(i) + origin return } func parse_fit() (err error) { switch strings.ToLower(opts.Fit) { case "width": fit_mode = fit_width case "height": fit_mode = fit_height case "none", "neither": fit_mode = fit_none case "both": fit_mode = fit_both default: return fmt.Errorf("unknown fit specification: %#v", opts.Fit) } return nil } func parse_place() (err error) { if opts.Place == "" { return nil } area, pos, found := strings.Cut(opts.Place, "@") if !found { return fmt.Errorf("Invalid --place specification: %s", opts.Place) } w, h, found := strings.Cut(area, "x") if !found { return fmt.Errorf("Invalid --place specification: %s", opts.Place) } l, t, found := strings.Cut(pos, "x") if !found { return fmt.Errorf("Invalid --place specification: %s", opts.Place) } place = &Place{} place.width, err = strconv.Atoi(w) if err != nil { return err } place.height, err = strconv.Atoi(h) if err != nil { return err } place.left, err = strconv.Atoi(l) if err != nil { return err } place.top, err = strconv.Atoi(t) if err != nil { return err } return nil } func print_error(format string, args ...any) { fmt.Fprintf(os.Stderr, format, args...) fmt.Fprintln(os.Stderr) } func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) { opts = o if err = parse_place(); err != nil { return 1, err } if err = parse_fit(); err != nil { return 1, err } err = parse_z_index() if err != nil { return 1, err } err = parse_background() if err != nil { return 1, err } err = parse_mirror() if err != nil { return 1, err } if opts.UseWindowSize == "" { if tty.IsTerminal(os.Stdout.Fd()) { screen_size, err = tty.GetSize(int(os.Stdout.Fd())) } else { t, oerr := tty.OpenControllingTerm() if oerr != nil { return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", oerr) } screen_size, err = t.GetSize() } if err != nil { return 1, fmt.Errorf("Failed to query terminal using TIOCGWINSZ with error: %w", err) } } else { parts := strings.SplitN(opts.UseWindowSize, ",", 4) if len(parts) != 4 { return 1, fmt.Errorf("Invalid size specification: %s", opts.UseWindowSize) } screen_size = &unix.Winsize{} var t uint64 if t, err = strconv.ParseUint(parts[0], 10, 16); err != nil || t < 1 { return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err) } screen_size.Col = uint16(t) if t, err = strconv.ParseUint(parts[1], 10, 16); err != nil || t < 1 { return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err) } screen_size.Row = uint16(t) if t, err = strconv.ParseUint(parts[2], 10, 16); err != nil || t < 1 { return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err) } screen_size.Xpixel = uint16(t) if t, err = strconv.ParseUint(parts[3], 10, 16); err != nil || t < 1 { return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err) } screen_size.Ypixel = uint16(t) if screen_size.Xpixel < screen_size.Col { return 1, fmt.Errorf("Invalid size specification: %s with error: The pixel width is smaller than the number of columns", opts.UseWindowSize) } if screen_size.Ypixel < screen_size.Row { return 1, fmt.Errorf("Invalid size specification: %s with error: The pixel height is smaller than the number of rows", opts.UseWindowSize) } } if opts.PrintWindowSize { 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) if err = cc.WriteWithPayloadTo(os.Stdout, nil); err != nil { return 1, err } } switch { case opts.ClearAll: cc := &graphics.GraphicsCommand{} cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_by_range).SetLeftEdge(0).SetTopEdge(math.MaxUint32) if err = cc.WriteWithPayloadTo(os.Stdout, nil); err != nil { return 1, err } case opts.Clear: cc := &graphics.GraphicsCommand{} cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_visible) if err = cc.WriteWithPayloadTo(os.Stdout, nil); err != nil { return 1, err } } 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.") } items, err := process_dirs(args...) if err != nil { return 1, err } if opts.Place != "" && len(items) > 1 { return 1, fmt.Errorf("The --place option can only be used with a single image, not %d", len(items)) } files_channel = make(chan input_arg, len(items)) for i, ia := range items { ia.index = i files_channel <- ia } 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 range num_workers { go run_worker() } } passthrough_mode := no_passthrough switch opts.Passthrough { case "tmux": passthrough_mode = tmux_passthrough case "detect": if tui.TmuxSocketAddress() != "" { passthrough_mode = tmux_passthrough } } if passthrough_mode == no_passthrough && (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 { keep_going.Store(false) return 1, fmt.Errorf("This terminal does not support the graphics protocol use a terminal such as kitty, WezTerm or Konsole that does. If you are running inside a terminal multiplexer such as tmux or screen that might be interfering as well.") } if memory { transfer_by_memory = supported } else { transfer_by_memory = unsupported } if files { transfer_by_file = supported } else { transfer_by_file = unsupported } } if passthrough_mode != no_passthrough { // tmux doesn't allow responses from the terminal so we can't detect if memory or file based transferring is supported transfer_by_memory = unsupported 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 } use_unicode_placeholder := opts.UnicodePlaceholder if passthrough_mode != no_passthrough { use_unicode_placeholder = true } base_id := uint32(opts.ImageId) expecting_input_sequence_number := 0 pending := make([]*image_data, 0, num_of_items) do_one := func(imgd *image_data) { expecting_input_sequence_number++ if base_id != 0 { imgd.image_id = base_id base_id++ if base_id == 0 { base_id++ } } imgd.use_unicode_placeholder = use_unicode_placeholder imgd.passthrough_mode = passthrough_mode if imgd.err != nil { print_error("Failed to process \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err) } else { transmit_image(imgd, opts.NoTrailingNewline) if imgd.err != nil { print_error("Failed to transmit \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err) } } } for num_of_items > 0 { imgd := <-output_channel num_of_items-- if imgd.input_sequence_number == expecting_input_sequence_number { do_one(imgd) } else { index, _ := slices.BinarySearchFunc(pending, imgd.input_sequence_number, func(x *image_data, n int) int { return x.input_sequence_number - n }) pending = slices.Insert(pending, index, imgd) } for len(pending) > 0 && pending[0].input_sequence_number == expecting_input_sequence_number { do_one(pending[0]) pending = pending[1:] } } for _, x := range pending { do_one(x) } keep_going.Store(false) if opts.Hold { fmt.Print("\r") if opts.Place != "" { fmt.Println() } tui.HoldTillEnter(false) } return 0, nil } func EntryPoint(parent *cli.Command) { create_cmd(parent, main) }