collection.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package graphics
  3. import (
  4. "errors"
  5. "fmt"
  6. "os"
  7. "strings"
  8. "sync"
  9. "sync/atomic"
  10. "kitty/tools/tui"
  11. "kitty/tools/tui/loop"
  12. "kitty/tools/utils"
  13. "kitty/tools/utils/images"
  14. "kitty/tools/utils/shm"
  15. )
  16. var _ = fmt.Print
  17. type Size struct{ Width, Height int }
  18. type rendering struct {
  19. img *images.ImageData
  20. image_id uint32
  21. }
  22. type temp_resource struct {
  23. path string
  24. mmap shm.MMap
  25. }
  26. func (self *temp_resource) remove() {
  27. if self.path != "" {
  28. os.Remove(self.path)
  29. self.path = ""
  30. }
  31. if self.mmap != nil {
  32. _ = self.mmap.Unlink()
  33. self.mmap = nil
  34. }
  35. }
  36. type Image struct {
  37. src struct {
  38. path string
  39. data *images.ImageData
  40. size Size
  41. loaded bool
  42. }
  43. renderings map[Size]*rendering
  44. err error
  45. }
  46. func NewImage() *Image {
  47. return &Image{
  48. renderings: make(map[Size]*rendering),
  49. }
  50. }
  51. type ImageCollection struct {
  52. Shm_supported, Files_supported atomic.Bool
  53. detection_file_id, detection_shm_id uint32
  54. temp_file_map map[uint32]*temp_resource
  55. running_in_tmux bool
  56. mutex sync.Mutex
  57. image_id_counter uint32
  58. images map[string]*Image
  59. }
  60. var ErrNotFound = errors.New("not found")
  61. func (self *ImageCollection) GetSizeIfAvailable(key string, page_size Size) (Size, error) {
  62. if !self.mutex.TryLock() {
  63. return Size{}, ErrNotFound
  64. }
  65. defer self.mutex.Unlock()
  66. img := self.images[key]
  67. if img == nil {
  68. return Size{}, ErrNotFound
  69. }
  70. ans := img.renderings[page_size]
  71. if ans == nil {
  72. if img.err != nil {
  73. return Size{}, img.err
  74. }
  75. return Size{}, ErrNotFound
  76. }
  77. return Size{ans.img.Width, ans.img.Height}, img.err
  78. }
  79. func (self *ImageCollection) ResolutionOf(key string) Size {
  80. if !self.mutex.TryLock() {
  81. return Size{-1, -1}
  82. }
  83. defer self.mutex.Unlock()
  84. i := self.images[key]
  85. if i == nil {
  86. return Size{-2, -2}
  87. }
  88. return i.src.size
  89. }
  90. func (self *ImageCollection) AddPaths(paths ...string) {
  91. self.mutex.Lock()
  92. defer self.mutex.Unlock()
  93. for _, path := range paths {
  94. if self.images[path] == nil {
  95. i := NewImage()
  96. i.src.path = path
  97. self.images[path] = i
  98. }
  99. }
  100. }
  101. func (self *Image) ResizeForPageSize(width, height int) {
  102. sz := Size{width, height}
  103. if self.renderings[sz] != nil {
  104. return
  105. }
  106. final_width, final_height := images.FitImage(self.src.size.Width, self.src.size.Height, width, height)
  107. if final_width == self.src.size.Width && final_height == self.src.data.Height {
  108. self.renderings[sz] = &rendering{img: self.src.data}
  109. return
  110. }
  111. x_frac, y_frac := float64(final_width)/float64(self.src.size.Width), float64(final_height)/float64(self.src.size.Height)
  112. self.renderings[sz] = &rendering{img: self.src.data.Resize(x_frac, y_frac)}
  113. }
  114. func (self *ImageCollection) ResizeForPageSize(width, height int) {
  115. self.mutex.Lock()
  116. defer self.mutex.Unlock()
  117. ctx := images.Context{}
  118. keys := utils.Keys(self.images)
  119. ctx.Parallel(0, len(keys), func(nums <-chan int) {
  120. for i := range nums {
  121. img := self.images[keys[i]]
  122. if img.src.loaded && img.err == nil {
  123. img.ResizeForPageSize(width, height)
  124. }
  125. }
  126. })
  127. }
  128. func (self *ImageCollection) DeleteAllVisiblePlacements(lp *loop.Loop) {
  129. g := self.new_graphics_command()
  130. g.SetAction(GRT_action_delete).SetDelete(GRT_delete_visible)
  131. _ = g.WriteWithPayloadToLoop(lp, nil)
  132. }
  133. func (self *ImageCollection) PlaceImageSubRect(lp *loop.Loop, key string, page_size Size, left, top, width, height int) {
  134. self.mutex.Lock()
  135. defer self.mutex.Unlock()
  136. img := self.images[key]
  137. if img == nil {
  138. return
  139. }
  140. r := img.renderings[page_size]
  141. if r == nil {
  142. return
  143. }
  144. if r.image_id == 0 {
  145. self.transmit_rendering(lp, r)
  146. }
  147. if width < 0 {
  148. width = r.img.Width
  149. }
  150. if height < 0 {
  151. height = r.img.Height
  152. }
  153. width = utils.Max(0, utils.Min(r.img.Width-left, width))
  154. height = utils.Max(0, utils.Min(r.img.Height-top, height))
  155. gc := self.new_graphics_command()
  156. gc.SetAction(GRT_action_display).SetLeftEdge(uint64(left)).SetTopEdge(uint64(top)).SetWidth(uint64(width)).SetHeight(uint64(height))
  157. gc.SetImageId(r.image_id).SetPlacementId(1).SetCursorMovement(GRT_cursor_static)
  158. _ = gc.WriteWithPayloadToLoop(lp, nil)
  159. }
  160. func (self *ImageCollection) Initialize(lp *loop.Loop) {
  161. tmux := tui.TmuxSocketAddress()
  162. if tmux != "" && tui.TmuxAllowPassthrough() == nil {
  163. self.running_in_tmux = true
  164. }
  165. if !self.running_in_tmux {
  166. g := func(t GRT_t, payload string) uint32 {
  167. self.image_id_counter++
  168. g1 := self.new_graphics_command()
  169. g1.SetTransmission(t).SetAction(GRT_action_query).SetImageId(self.image_id_counter).SetDataWidth(1).SetDataHeight(1).SetFormat(
  170. GRT_format_rgb).SetDataSize(uint64(len(payload)))
  171. _ = g1.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(payload))
  172. return self.image_id_counter
  173. }
  174. tf, err := images.CreateTempInRAM()
  175. if err == nil {
  176. if _, err = tf.Write([]byte{1, 2, 3}); err == nil {
  177. self.detection_file_id = g(GRT_transmission_tempfile, tf.Name())
  178. self.temp_file_map[self.detection_file_id] = &temp_resource{path: tf.Name()}
  179. }
  180. tf.Close()
  181. }
  182. sf, err := shm.CreateTemp("icat-", 3)
  183. if err == nil {
  184. copy(sf.Slice(), []byte{1, 2, 3})
  185. sf.Close()
  186. self.detection_shm_id = g(GRT_transmission_sharedmem, sf.Name())
  187. self.temp_file_map[self.detection_shm_id] = &temp_resource{mmap: sf}
  188. }
  189. }
  190. }
  191. func (self *ImageCollection) Finalize(lp *loop.Loop) {
  192. for _, tr := range self.temp_file_map {
  193. tr.remove()
  194. }
  195. for _, img := range self.images {
  196. for _, r := range img.renderings {
  197. if r.image_id > 0 {
  198. g := self.new_graphics_command()
  199. g.SetAction(GRT_action_delete).SetDelete(GRT_free_by_id).SetImageId(r.image_id)
  200. _ = g.WriteWithPayloadToLoop(lp, nil)
  201. }
  202. }
  203. img.renderings = nil
  204. }
  205. self.images = nil
  206. }
  207. func (self *ImageCollection) mark_img_as_needing_transmission(id uint32) bool {
  208. self.mutex.Lock()
  209. defer self.mutex.Unlock()
  210. for _, img := range self.images {
  211. for _, r := range img.renderings {
  212. if r.image_id == id {
  213. r.image_id = 0
  214. return true
  215. }
  216. }
  217. }
  218. return false
  219. }
  220. // Handle graphics response. Returns false if an image needs re-transmission because
  221. // the terminal replied with ENOENT for a placement
  222. func (self *ImageCollection) HandleGraphicsCommand(gc *GraphicsCommand) bool {
  223. switch gc.ImageId() {
  224. case self.detection_file_id:
  225. if gc.ResponseMessage() == "OK" {
  226. self.Files_supported.Store(true)
  227. } else {
  228. if tr := self.temp_file_map[gc.ImageId()]; tr != nil {
  229. tr.remove()
  230. }
  231. }
  232. delete(self.temp_file_map, gc.ImageId())
  233. self.detection_file_id = 0
  234. return true
  235. case self.detection_shm_id:
  236. if gc.ResponseMessage() == "OK" {
  237. self.Shm_supported.Store(true)
  238. } else {
  239. if tr := self.temp_file_map[gc.ImageId()]; tr != nil {
  240. tr.remove()
  241. }
  242. }
  243. delete(self.temp_file_map, gc.ImageId())
  244. self.detection_shm_id = 0
  245. return true
  246. }
  247. if is_transmission_response := gc.PlacementId() == 0; is_transmission_response {
  248. if gc.ResponseMessage() != "OK" {
  249. // this should never happen but lets cleanup anyway
  250. if tr := self.temp_file_map[gc.ImageId()]; tr != nil {
  251. tr.remove()
  252. delete(self.temp_file_map, gc.ImageId())
  253. }
  254. }
  255. return true
  256. }
  257. if gc.ResponseMessage() != "OK" && gc.PlacementId() != 0 {
  258. if self.mark_img_as_needing_transmission(gc.ImageId()) {
  259. return false
  260. }
  261. }
  262. return true
  263. }
  264. func (self *ImageCollection) LoadAll() {
  265. self.mutex.Lock()
  266. defer self.mutex.Unlock()
  267. ctx := images.Context{}
  268. all := utils.Values(self.images)
  269. ctx.Parallel(0, len(self.images), func(nums <-chan int) {
  270. for i := range nums {
  271. img := all[i]
  272. if !img.src.loaded {
  273. img.src.data, img.err = images.OpenImageFromPath(img.src.path)
  274. if img.err == nil {
  275. img.src.size.Width, img.src.size.Height = img.src.data.Width, img.src.data.Height
  276. }
  277. img.src.loaded = true
  278. }
  279. }
  280. })
  281. }
  282. func NewImageCollection(paths ...string) *ImageCollection {
  283. items := make(map[string]*Image, len(paths))
  284. for _, path := range paths {
  285. i := NewImage()
  286. i.src.path = path
  287. items[path] = i
  288. }
  289. return &ImageCollection{images: items, temp_file_map: make(map[uint32]*temp_resource)}
  290. }
  291. func (self *ImageCollection) new_graphics_command() *GraphicsCommand {
  292. gc := GraphicsCommand{}
  293. if self.running_in_tmux {
  294. gc.WrapPrefix = "\033Ptmux;"
  295. gc.WrapSuffix = "\033\\"
  296. gc.EncodeSerializedDataFunc = func(x string) string { return strings.ReplaceAll(x, "\033", "\033\033") }
  297. }
  298. return &gc
  299. }
  300. func transmit_by_escape_code(lp *loop.Loop, image_id uint32, temp_file_map map[uint32]*temp_resource, frame *images.ImageFrame, gc *GraphicsCommand) {
  301. atomic := lp.IsAtomicUpdateActive()
  302. lp.EndAtomicUpdate()
  303. gc.SetTransmission(GRT_transmission_direct)
  304. _ = gc.WriteWithPayloadToLoop(lp, frame.Data())
  305. if atomic {
  306. lp.StartAtomicUpdate()
  307. }
  308. }
  309. func transmit_by_shm(lp *loop.Loop, image_id uint32, temp_file_map map[uint32]*temp_resource, frame *images.ImageFrame, gc *GraphicsCommand) {
  310. mmap, err := frame.DataAsSHM("kdiff-img-*")
  311. if err != nil {
  312. transmit_by_escape_code(lp, image_id, temp_file_map, frame, gc)
  313. return
  314. }
  315. mmap.Close()
  316. temp_file_map[image_id] = &temp_resource{mmap: mmap}
  317. gc.SetTransmission(GRT_transmission_sharedmem)
  318. _ = gc.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(mmap.Name()))
  319. }
  320. func transmit_by_file(lp *loop.Loop, image_id uint32, temp_file_map map[uint32]*temp_resource, frame *images.ImageFrame, gc *GraphicsCommand) {
  321. f, err := images.CreateTempInRAM()
  322. if err != nil {
  323. transmit_by_escape_code(lp, image_id, temp_file_map, frame, gc)
  324. return
  325. }
  326. defer f.Close()
  327. temp_file_map[image_id] = &temp_resource{path: f.Name()}
  328. _, err = f.Write(frame.Data())
  329. if err != nil {
  330. transmit_by_escape_code(lp, image_id, temp_file_map, frame, gc)
  331. return
  332. }
  333. gc.SetTransmission(GRT_transmission_tempfile)
  334. _ = gc.WriteWithPayloadToLoop(lp, utils.UnsafeStringToBytes(f.Name()))
  335. }
  336. func (self *ImageCollection) transmit_rendering(lp *loop.Loop, r *rendering) {
  337. if r.image_id == 0 {
  338. self.image_id_counter++
  339. r.image_id = self.image_id_counter
  340. }
  341. is_animated := len(r.img.Frames) > 0
  342. transmit := transmit_by_escape_code
  343. if self.Shm_supported.Load() {
  344. transmit = transmit_by_shm
  345. } else if self.Files_supported.Load() {
  346. transmit = transmit_by_file
  347. }
  348. frame_control_cmd := self.new_graphics_command()
  349. frame_control_cmd.SetAction(GRT_action_animate).SetImageId(r.image_id)
  350. for frame_num, frame := range r.img.Frames {
  351. gc := self.new_graphics_command()
  352. gc.SetImageId(r.image_id)
  353. gc.SetDataWidth(uint64(frame.Width)).SetDataHeight(uint64(frame.Height))
  354. if frame.Is_opaque {
  355. gc.SetFormat(GRT_format_rgb)
  356. }
  357. switch frame_num {
  358. case 0:
  359. gc.SetAction(GRT_action_transmit)
  360. gc.SetCursorMovement(GRT_cursor_static)
  361. default:
  362. gc.SetAction(GRT_action_frame)
  363. gc.SetGap(frame.Delay_ms)
  364. if frame.Compose_onto > 0 {
  365. gc.SetOverlaidFrame(uint64(frame.Compose_onto))
  366. }
  367. gc.SetLeftEdge(uint64(frame.Left)).SetTopEdge(uint64(frame.Top))
  368. }
  369. transmit(lp, r.image_id, self.temp_file_map, frame, gc)
  370. if is_animated {
  371. switch frame_num {
  372. case 0:
  373. // set gap for the first frame and number of loops for the animation
  374. c := frame_control_cmd
  375. c.SetTargetFrame(uint64(frame.Number))
  376. c.SetGap(int32(frame.Delay_ms))
  377. c.SetNumberOfLoops(1)
  378. _ = c.WriteWithPayloadToLoop(lp, nil)
  379. case 1:
  380. c := frame_control_cmd
  381. c.SetAnimationControl(2) // set animation to loading mode
  382. _ = c.WriteWithPayloadToLoop(lp, nil)
  383. }
  384. }
  385. }
  386. if is_animated {
  387. c := frame_control_cmd
  388. c.SetAnimationControl(3) // set animation to normal mode
  389. _ = c.WriteWithPayloadToLoop(lp, nil)
  390. }
  391. }