run.go 6.4 KB

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