main.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package diff
  3. import (
  4. "archive/tar"
  5. "bytes"
  6. "fmt"
  7. "io"
  8. "io/fs"
  9. "os"
  10. "os/exec"
  11. "path/filepath"
  12. "strings"
  13. "kitty/kittens/ssh"
  14. "kitty/tools/cli"
  15. "kitty/tools/config"
  16. "kitty/tools/tui/loop"
  17. "kitty/tools/utils"
  18. )
  19. var _ = fmt.Print
  20. func load_config(opts *Options) (ans *Config, err error) {
  21. ans = NewConfig()
  22. p := config.ConfigParser{LineHandler: ans.Parse}
  23. err = p.LoadConfig("diff.conf", opts.Config, opts.Override)
  24. if err != nil {
  25. return nil, err
  26. }
  27. ans.KeyboardShortcuts = config.ResolveShortcuts(ans.KeyboardShortcuts)
  28. return ans, nil
  29. }
  30. var conf *Config
  31. var opts *Options
  32. var lp *loop.Loop
  33. var temp_files []string
  34. func resolve_path(path string) (ans string, is_dir bool, err error) {
  35. var s fs.FileInfo
  36. if s, err = os.Stat(path); err != nil {
  37. return
  38. } else {
  39. if s.Mode()&fs.ModeNamedPipe != 0 {
  40. var src, dest *os.File
  41. if src, err = os.Open(path); err != nil {
  42. return
  43. }
  44. defer src.Close()
  45. if dest, err = os.CreateTemp("", fmt.Sprintf("*-pipe-%s", filepath.Base(path))); err != nil {
  46. return
  47. }
  48. defer dest.Close()
  49. temp_files = append(temp_files, dest.Name())
  50. if _, err = io.Copy(dest, src); err != nil {
  51. return
  52. }
  53. return dest.Name(), false, nil
  54. } else {
  55. return path, s.IsDir(), nil
  56. }
  57. }
  58. }
  59. func get_ssh_file(hostname, rpath string) (string, error) {
  60. tdir, err := os.MkdirTemp("", "*-"+hostname)
  61. if err != nil {
  62. return "", err
  63. }
  64. add_remote_dir(tdir)
  65. is_abs := strings.HasPrefix(rpath, "/")
  66. for strings.HasPrefix(rpath, "/") {
  67. rpath = rpath[1:]
  68. }
  69. cmd := []string{ssh.SSHExe(), hostname, "tar", "--dereference", "--create", "--file", "-"}
  70. if is_abs {
  71. cmd = append(cmd, "-C", "/")
  72. }
  73. cmd = append(cmd, rpath)
  74. c := exec.Command(cmd[0], cmd[1:]...)
  75. c.Stdin, c.Stderr = os.Stdin, os.Stderr
  76. stdout, err := c.Output()
  77. if err != nil {
  78. return "", fmt.Errorf("Failed to ssh into remote host %s to get file %s with error: %w", hostname, rpath, err)
  79. }
  80. tf := tar.NewReader(bytes.NewReader(stdout))
  81. count, err := utils.ExtractAllFromTar(tf, tdir)
  82. if err != nil {
  83. return "", fmt.Errorf("Failed to untar data from remote host %s to get file %s with error: %w", hostname, rpath, err)
  84. }
  85. ans := filepath.Join(tdir, rpath)
  86. if count == 1 {
  87. if err = filepath.WalkDir(tdir, func(path string, d fs.DirEntry, err error) error {
  88. if !d.IsDir() {
  89. ans = path
  90. return fs.SkipAll
  91. }
  92. return nil
  93. }); err != nil {
  94. return "", err
  95. }
  96. }
  97. return ans, nil
  98. }
  99. func get_remote_file(path string) (string, error) {
  100. if strings.HasPrefix(path, "ssh:") {
  101. parts := strings.SplitN(path, ":", 3)
  102. if len(parts) == 3 {
  103. return get_ssh_file(parts[1], parts[2])
  104. }
  105. }
  106. return path, nil
  107. }
  108. func main(_ *cli.Command, opts_ *Options, args []string) (rc int, err error) {
  109. opts = opts_
  110. conf, err = load_config(opts)
  111. if err != nil {
  112. return 1, err
  113. }
  114. if len(args) != 2 {
  115. return 1, fmt.Errorf("You must specify exactly two files/directories to compare")
  116. }
  117. if err = set_diff_command(conf.Diff_cmd); err != nil {
  118. return 1, err
  119. }
  120. switch conf.Color_scheme {
  121. case Color_scheme_light:
  122. use_light_colors = true
  123. case Color_scheme_dark:
  124. use_light_colors = false
  125. case Color_scheme_auto:
  126. use_light_colors = false
  127. }
  128. init_caches()
  129. defer func() {
  130. for tdir := range remote_dirs {
  131. os.RemoveAll(tdir)
  132. }
  133. }()
  134. left, err := get_remote_file(args[0])
  135. if err != nil {
  136. return 1, err
  137. }
  138. right, err := get_remote_file(args[1])
  139. if err != nil {
  140. return 1, err
  141. }
  142. defer func() {
  143. for _, path := range temp_files {
  144. os.Remove(path)
  145. }
  146. }()
  147. var left_is_dir, right_is_dir bool
  148. if left, left_is_dir, err = resolve_path(left); err != nil {
  149. return 1, err
  150. }
  151. if right, right_is_dir, err = resolve_path(right); err != nil {
  152. return 1, err
  153. }
  154. if left_is_dir != right_is_dir {
  155. return 1, fmt.Errorf("The items to be diffed should both be either directories or files. Comparing a directory to a file is not valid.'")
  156. }
  157. lp, err = loop.New()
  158. loop.MouseTrackingMode(lp, loop.BUTTONS_AND_DRAG_MOUSE_TRACKING)
  159. if err != nil {
  160. return 1, err
  161. }
  162. lp.ColorSchemeChangeNotifications()
  163. h := Handler{left: left, right: right, lp: lp}
  164. lp.OnInitialize = func() (string, error) {
  165. lp.SetCursorVisible(false)
  166. lp.SetCursorShape(loop.BAR_CURSOR, true)
  167. lp.AllowLineWrapping(false)
  168. lp.SetWindowTitle(fmt.Sprintf("%s vs. %s", left, right))
  169. lp.QueryCapabilities()
  170. h.initialize()
  171. return "", nil
  172. }
  173. lp.OnCapabilitiesReceived = func(tc loop.TerminalCapabilities) error {
  174. if !tc.KeyboardProtocol {
  175. return fmt.Errorf("This terminal does not support the kitty keyboard protocol, or you are running inside a terminal multiplexer that is blocking querying for kitty keyboard protocol support. The diff kitten cannot function without it.")
  176. }
  177. h.on_capabilities_received(tc)
  178. return nil
  179. }
  180. lp.OnWakeup = h.on_wakeup
  181. lp.OnFinalize = func() string {
  182. lp.SetCursorVisible(true)
  183. lp.SetCursorShape(loop.BLOCK_CURSOR, true)
  184. h.finalize()
  185. return ""
  186. }
  187. lp.OnResize = h.on_resize
  188. lp.OnKeyEvent = h.on_key_event
  189. lp.OnText = h.on_text
  190. lp.OnMouseEvent = h.on_mouse_event
  191. err = lp.Run()
  192. if err != nil {
  193. return 1, err
  194. }
  195. ds := lp.DeathSignalName()
  196. if ds != "" {
  197. fmt.Println("Killed by signal: ", ds)
  198. lp.KillIfSignalled()
  199. return 1, nil
  200. }
  201. return
  202. }
  203. func EntryPoint(parent *cli.Command) {
  204. create_cmd(parent, main)
  205. }