run.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package tui
  3. import (
  4. "fmt"
  5. "kitty"
  6. "os"
  7. "os/exec"
  8. "os/signal"
  9. "path/filepath"
  10. "runtime"
  11. "strings"
  12. "sync"
  13. "github.com/shirou/gopsutil/v3/process"
  14. "golang.org/x/sys/unix"
  15. "kitty/tools/config"
  16. "kitty/tools/tty"
  17. "kitty/tools/tui/loop"
  18. "kitty/tools/tui/shell_integration"
  19. "kitty/tools/utils"
  20. "kitty/tools/utils/shlex"
  21. )
  22. var _ = fmt.Print
  23. type KittyOpts struct {
  24. Shell, Shell_integration string
  25. }
  26. func read_relevant_kitty_opts(path string) KittyOpts {
  27. ans := KittyOpts{Shell: kitty.KittyConfigDefaults.Shell, Shell_integration: kitty.KittyConfigDefaults.Shell_integration}
  28. handle_line := func(key, val string) error {
  29. switch key {
  30. case "shell":
  31. ans.Shell = strings.TrimSpace(val)
  32. case "shell_integration":
  33. ans.Shell_integration = strings.TrimSpace(val)
  34. }
  35. return nil
  36. }
  37. cp := config.ConfigParser{LineHandler: handle_line}
  38. _ = cp.ParseFiles(path)
  39. if ans.Shell == "" {
  40. ans.Shell = kitty.KittyConfigDefaults.Shell
  41. }
  42. return ans
  43. }
  44. func get_effective_ksi_env_var(x string) string {
  45. parts := strings.Split(strings.TrimSpace(strings.ToLower(x)), " ")
  46. current := utils.NewSetWithItems(parts...)
  47. if current.Has("disabled") {
  48. return ""
  49. }
  50. allowed := utils.NewSetWithItems(kitty.AllowedShellIntegrationValues...)
  51. if !current.IsSubsetOf(allowed) {
  52. return relevant_kitty_opts().Shell_integration
  53. }
  54. return x
  55. }
  56. var relevant_kitty_opts = sync.OnceValue(func() KittyOpts {
  57. return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf"))
  58. })
  59. func get_shell_from_kitty_conf() (shell string) {
  60. shell = relevant_kitty_opts().Shell
  61. if shell == "." {
  62. s, e := utils.LoginShellForCurrentUser()
  63. if e != nil {
  64. shell = "/bin/sh"
  65. } else {
  66. shell = s
  67. }
  68. }
  69. return
  70. }
  71. func find_shell_parent_process() string {
  72. var p *process.Process
  73. var err error
  74. for {
  75. if p == nil {
  76. p, err = process.NewProcess(int32(os.Getppid()))
  77. } else {
  78. p, err = p.Parent()
  79. }
  80. if err != nil {
  81. return ""
  82. }
  83. if cmdline, err := p.CmdlineSlice(); err == nil && len(cmdline) > 0 {
  84. exe := get_shell_name(filepath.Base(cmdline[0]))
  85. if shell_integration.IsSupportedShell(exe) {
  86. return exe
  87. }
  88. }
  89. }
  90. }
  91. func ResolveShell(shell string) []string {
  92. switch shell {
  93. case "":
  94. shell = get_shell_from_kitty_conf()
  95. case ".":
  96. if shell = find_shell_parent_process(); shell == "" {
  97. shell = get_shell_from_kitty_conf()
  98. }
  99. }
  100. shell_cmd, err := shlex.Split(shell)
  101. if err != nil {
  102. shell_cmd = []string{shell}
  103. }
  104. exe := utils.FindExe(shell_cmd[0])
  105. if unix.Access(exe, unix.X_OK) != nil {
  106. shell_cmd = []string{"/bin/sh"}
  107. }
  108. return shell_cmd
  109. }
  110. func ResolveShellIntegration(shell_integration string) string {
  111. if shell_integration == "" {
  112. shell_integration = relevant_kitty_opts().Shell_integration
  113. }
  114. return get_effective_ksi_env_var(shell_integration)
  115. }
  116. func get_shell_name(argv0 string) (ans string) {
  117. ans = filepath.Base(argv0)
  118. if strings.HasSuffix(strings.ToLower(ans), ".exe") {
  119. ans = ans[:len(ans)-4]
  120. }
  121. return strings.TrimPrefix(ans, "-")
  122. }
  123. func rc_modification_allowed(ksi string) (allowed bool, set_ksi_env_var bool) {
  124. allowed = ksi != ""
  125. set_ksi_env_var = true
  126. for _, x := range strings.Split(ksi, " ") {
  127. switch x {
  128. case "disabled":
  129. allowed = false
  130. set_ksi_env_var = false
  131. break
  132. case "no-rc":
  133. allowed = false
  134. break
  135. }
  136. }
  137. return
  138. }
  139. func copy_os_env_as_dict() map[string]string {
  140. oenv := os.Environ()
  141. env := make(map[string]string, len(oenv))
  142. for _, x := range oenv {
  143. if k, v, found := strings.Cut(x, "="); found {
  144. env[k] = v
  145. }
  146. }
  147. return env
  148. }
  149. func RunShell(shell_cmd []string, shell_integration_env_var_val, cwd string) (err error) {
  150. shell_name := get_shell_name(shell_cmd[0])
  151. var shell_env map[string]string
  152. if shell_integration.IsSupportedShell(shell_name) {
  153. rc_mod_allowed, set_ksi_env_var := rc_modification_allowed(shell_integration_env_var_val)
  154. if rc_mod_allowed {
  155. // KITTY_SHELL_INTEGRATION is always set by this function
  156. argv, env, err := shell_integration.Setup(shell_name, shell_integration_env_var_val, shell_cmd, copy_os_env_as_dict())
  157. if err != nil {
  158. return err
  159. }
  160. shell_cmd = argv
  161. shell_env = env
  162. } else if set_ksi_env_var {
  163. shell_env = copy_os_env_as_dict()
  164. shell_env["KITTY_SHELL_INTEGRATION"] = shell_integration_env_var_val
  165. }
  166. }
  167. exe := shell_cmd[0]
  168. if runtime.GOOS == "darwin" && (os.Getenv("KITTY_RUNNING_SHELL_INTEGRATION_TEST") != "1" || os.Getenv("KITTY_RUNNING_BASH_INTEGRATION_TEST") != "") {
  169. // ensure shell runs in login mode. On macOS lots of people use ~/.bash_profile instead of ~/.bashrc
  170. // which means they expect the shell to run in login mode always. Le Sigh.
  171. shell_cmd[0] = "-" + filepath.Base(shell_cmd[0])
  172. }
  173. var env []string
  174. if shell_env != nil {
  175. env = make([]string, 0, len(shell_env))
  176. for k, v := range shell_env {
  177. env = append(env, fmt.Sprintf("%s=%s", k, v))
  178. }
  179. } else {
  180. env = os.Environ()
  181. }
  182. // fmt.Println(fmt.Sprintf("%s %v\n%#v", utils.FindExe(exe), shell_cmd, env))
  183. if cwd != "" {
  184. _ = os.Chdir(cwd)
  185. }
  186. return unix.Exec(utils.FindExe(exe), shell_cmd, env)
  187. }
  188. var debugprintln = tty.DebugPrintln
  189. var _ = debugprintln
  190. func RunCommandRestoringTerminalToSaneStateAfter(cmd []string) {
  191. exe := utils.FindExe(cmd[0])
  192. c := exec.Command(exe, cmd[1:]...)
  193. c.Stdout = os.Stdout
  194. c.Stdin = os.Stdin
  195. c.Stderr = os.Stderr
  196. term, err := tty.OpenControllingTerm()
  197. if err == nil {
  198. var state_before unix.Termios
  199. if term.Tcgetattr(&state_before) == nil {
  200. if _, err = term.WriteString(loop.SAVE_PRIVATE_MODE_VALUES); err != nil {
  201. fmt.Fprintln(os.Stderr, "failed to write to controlling terminal with error:", err)
  202. return
  203. }
  204. defer func() {
  205. _, _ = term.WriteString(strings.Join([]string{
  206. loop.RESTORE_PRIVATE_MODE_VALUES,
  207. "\x1b[=u", // reset kitty keyboard protocol to legacy
  208. "\x1b[1 q", // blinking block cursor
  209. loop.DECTCEM.EscapeCodeToSet(), // cursor visible
  210. "\x1b]112\a", // reset cursor color
  211. }, ""))
  212. _ = term.Tcsetattr(tty.TCSANOW, &state_before)
  213. term.Close()
  214. }()
  215. } else {
  216. defer term.Close()
  217. }
  218. }
  219. func() {
  220. if err = c.Start(); err != nil {
  221. fmt.Fprintln(os.Stderr, cmd[0], "failed to start with error:", err)
  222. return
  223. }
  224. // Ignore SIGINT as the kernel tends to send it to us as well as the
  225. // subprocess on Ctrl+C
  226. signal.Ignore(os.Interrupt)
  227. defer signal.Reset(os.Interrupt)
  228. err = c.Wait()
  229. }()
  230. if err != nil {
  231. fmt.Fprintln(os.Stderr, cmd[0], "failed with error:", err)
  232. }
  233. }