mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
381 lines
9.8 KiB
Go
381 lines
9.8 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
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)
|
|
}
|