main.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package icat
  3. import (
  4. "fmt"
  5. "os"
  6. "runtime"
  7. "strconv"
  8. "strings"
  9. "sync/atomic"
  10. "time"
  11. "kitty/tools/cli"
  12. "kitty/tools/tty"
  13. "kitty/tools/tui"
  14. "kitty/tools/tui/graphics"
  15. "kitty/tools/utils"
  16. "kitty/tools/utils/images"
  17. "kitty/tools/utils/style"
  18. "golang.org/x/sys/unix"
  19. )
  20. var _ = fmt.Print
  21. type Place struct {
  22. width, height, left, top int
  23. }
  24. var opts *Options
  25. var place *Place
  26. var z_index int32
  27. var remove_alpha *images.NRGBColor
  28. var flip, flop bool
  29. type transfer_mode int
  30. const (
  31. unknown transfer_mode = iota
  32. unsupported
  33. supported
  34. )
  35. var transfer_by_file, transfer_by_memory transfer_mode
  36. var files_channel chan input_arg
  37. var output_channel chan *image_data
  38. var num_of_items int
  39. var keep_going *atomic.Bool
  40. var screen_size *unix.Winsize
  41. func send_output(imgd *image_data) {
  42. output_channel <- imgd
  43. }
  44. func parse_mirror() (err error) {
  45. flip = opts.Mirror == "both" || opts.Mirror == "vertical"
  46. flop = opts.Mirror == "both" || opts.Mirror == "horizontal"
  47. return
  48. }
  49. func parse_background() (err error) {
  50. if opts.Background == "" || opts.Background == "none" {
  51. return nil
  52. }
  53. col, err := style.ParseColor(opts.Background)
  54. if err != nil {
  55. return fmt.Errorf("Invalid value for --background: %w", err)
  56. }
  57. remove_alpha = &images.NRGBColor{R: col.Red, G: col.Green, B: col.Blue}
  58. return
  59. }
  60. func parse_z_index() (err error) {
  61. val := opts.ZIndex
  62. var origin int32
  63. if strings.HasPrefix(val, "--") {
  64. origin = -1073741824
  65. val = val[1:]
  66. }
  67. i, err := strconv.ParseInt(val, 10, 32)
  68. if err != nil {
  69. return fmt.Errorf("Invalid value for --z-index with error: %w", err)
  70. }
  71. z_index = int32(i) + origin
  72. return
  73. }
  74. func parse_place() (err error) {
  75. if opts.Place == "" {
  76. return nil
  77. }
  78. area, pos, found := strings.Cut(opts.Place, "@")
  79. if !found {
  80. return fmt.Errorf("Invalid --place specification: %s", opts.Place)
  81. }
  82. w, h, found := strings.Cut(area, "x")
  83. if !found {
  84. return fmt.Errorf("Invalid --place specification: %s", opts.Place)
  85. }
  86. l, t, found := strings.Cut(pos, "x")
  87. if !found {
  88. return fmt.Errorf("Invalid --place specification: %s", opts.Place)
  89. }
  90. place = &Place{}
  91. place.width, err = strconv.Atoi(w)
  92. if err != nil {
  93. return err
  94. }
  95. place.height, err = strconv.Atoi(h)
  96. if err != nil {
  97. return err
  98. }
  99. place.left, err = strconv.Atoi(l)
  100. if err != nil {
  101. return err
  102. }
  103. place.top, err = strconv.Atoi(t)
  104. if err != nil {
  105. return err
  106. }
  107. return nil
  108. }
  109. func print_error(format string, args ...any) {
  110. fmt.Fprintf(os.Stderr, format, args...)
  111. fmt.Fprintln(os.Stderr)
  112. }
  113. func main(cmd *cli.Command, o *Options, args []string) (rc int, err error) {
  114. opts = o
  115. err = parse_place()
  116. if err != nil {
  117. return 1, err
  118. }
  119. err = parse_z_index()
  120. if err != nil {
  121. return 1, err
  122. }
  123. err = parse_background()
  124. if err != nil {
  125. return 1, err
  126. }
  127. err = parse_mirror()
  128. if err != nil {
  129. return 1, err
  130. }
  131. if opts.UseWindowSize == "" {
  132. if tty.IsTerminal(os.Stdout.Fd()) {
  133. screen_size, err = tty.GetSize(int(os.Stdout.Fd()))
  134. } else {
  135. t, oerr := tty.OpenControllingTerm()
  136. if oerr != nil {
  137. return 1, fmt.Errorf("Failed to open controlling terminal with error: %w", oerr)
  138. }
  139. screen_size, err = t.GetSize()
  140. }
  141. if err != nil {
  142. return 1, fmt.Errorf("Failed to query terminal using TIOCGWINSZ with error: %w", err)
  143. }
  144. } else {
  145. parts := strings.SplitN(opts.UseWindowSize, ",", 4)
  146. if len(parts) != 4 {
  147. return 1, fmt.Errorf("Invalid size specification: " + opts.UseWindowSize)
  148. }
  149. screen_size = &unix.Winsize{}
  150. t := 0
  151. if t, err = strconv.Atoi(parts[0]); err != nil || t < 1 {
  152. return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err)
  153. }
  154. screen_size.Col = uint16(t)
  155. if t, err = strconv.Atoi(parts[1]); err != nil || t < 1 {
  156. return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err)
  157. }
  158. screen_size.Row = uint16(t)
  159. if t, err = strconv.Atoi(parts[2]); err != nil || t < 1 {
  160. return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err)
  161. }
  162. screen_size.Xpixel = uint16(t)
  163. if t, err = strconv.Atoi(parts[3]); err != nil || t < 1 {
  164. return 1, fmt.Errorf("Invalid size specification: %s with error: %w", opts.UseWindowSize, err)
  165. }
  166. screen_size.Ypixel = uint16(t)
  167. if screen_size.Xpixel < screen_size.Col {
  168. return 1, fmt.Errorf("Invalid size specification: %s with error: The pixel width is smaller than the number of columns", opts.UseWindowSize)
  169. }
  170. if screen_size.Ypixel < screen_size.Row {
  171. return 1, fmt.Errorf("Invalid size specification: %s with error: The pixel height is smaller than the number of rows", opts.UseWindowSize)
  172. }
  173. }
  174. if opts.PrintWindowSize {
  175. fmt.Printf("%dx%d", screen_size.Xpixel, screen_size.Ypixel)
  176. return 0, nil
  177. }
  178. if opts.Clear {
  179. cc := &graphics.GraphicsCommand{}
  180. cc.SetAction(graphics.GRT_action_delete).SetDelete(graphics.GRT_free_visible)
  181. if err = cc.WriteWithPayloadTo(os.Stdout, nil); err != nil {
  182. return 1, err
  183. }
  184. }
  185. if screen_size.Xpixel == 0 || screen_size.Ypixel == 0 {
  186. return 1, fmt.Errorf("Terminal does not support reporting screen sizes in pixels, use a terminal such as kitty, WezTerm, Konsole, etc. that does.")
  187. }
  188. items, err := process_dirs(args...)
  189. if err != nil {
  190. return 1, err
  191. }
  192. if opts.Place != "" && len(items) > 1 {
  193. return 1, fmt.Errorf("The --place option can only be used with a single image, not %d", len(items))
  194. }
  195. files_channel = make(chan input_arg, len(items))
  196. for _, ia := range items {
  197. files_channel <- ia
  198. }
  199. num_of_items = len(items)
  200. output_channel = make(chan *image_data, 1)
  201. keep_going = &atomic.Bool{}
  202. keep_going.Store(true)
  203. if !opts.DetectSupport && num_of_items > 0 {
  204. num_workers := utils.Max(1, utils.Min(num_of_items, runtime.NumCPU()))
  205. for i := 0; i < num_workers; i++ {
  206. go run_worker()
  207. }
  208. }
  209. passthrough_mode := no_passthrough
  210. switch opts.Passthrough {
  211. case "tmux":
  212. passthrough_mode = tmux_passthrough
  213. case "detect":
  214. if tui.TmuxSocketAddress() != "" {
  215. passthrough_mode = tmux_passthrough
  216. }
  217. }
  218. if passthrough_mode == no_passthrough && (opts.TransferMode == "detect" || opts.DetectSupport) {
  219. memory, files, direct, err := DetectSupport(time.Duration(opts.DetectionTimeout * float64(time.Second)))
  220. if err != nil {
  221. return 1, err
  222. }
  223. if !direct {
  224. keep_going.Store(false)
  225. 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.")
  226. }
  227. if memory {
  228. transfer_by_memory = supported
  229. } else {
  230. transfer_by_memory = unsupported
  231. }
  232. if files {
  233. transfer_by_file = supported
  234. } else {
  235. transfer_by_file = unsupported
  236. }
  237. }
  238. if passthrough_mode != no_passthrough {
  239. // tmux doesn't allow responses from the terminal so we can't detect if memory or file based transferring is supported
  240. transfer_by_memory = unsupported
  241. transfer_by_file = unsupported
  242. }
  243. if opts.DetectSupport {
  244. if transfer_by_memory == supported {
  245. print_error("memory")
  246. } else if transfer_by_file == supported {
  247. print_error("files")
  248. } else {
  249. print_error("stream")
  250. }
  251. return 0, nil
  252. }
  253. use_unicode_placeholder := opts.UnicodePlaceholder
  254. if passthrough_mode != no_passthrough {
  255. use_unicode_placeholder = true
  256. }
  257. base_id := uint32(opts.ImageId)
  258. for num_of_items > 0 {
  259. imgd := <-output_channel
  260. if base_id != 0 {
  261. imgd.image_id = base_id
  262. base_id++
  263. if base_id == 0 {
  264. base_id++
  265. }
  266. }
  267. imgd.use_unicode_placeholder = use_unicode_placeholder
  268. imgd.passthrough_mode = passthrough_mode
  269. num_of_items--
  270. if imgd.err != nil {
  271. print_error("Failed to process \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err)
  272. } else {
  273. transmit_image(imgd, opts.NoTrailingNewline)
  274. if imgd.err != nil {
  275. print_error("Failed to transmit \x1b[31m%s\x1b[39m: %s\r\n", imgd.source_name, imgd.err)
  276. }
  277. }
  278. }
  279. keep_going.Store(false)
  280. if opts.Hold {
  281. fmt.Print("\r")
  282. if opts.Place != "" {
  283. fmt.Println()
  284. }
  285. tui.HoldTillEnter(false)
  286. }
  287. return 0, nil
  288. }
  289. func EntryPoint(parent *cli.Command) {
  290. create_cmd(parent, main)
  291. }