main.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package run_shell
  3. import (
  4. "fmt"
  5. "kitty"
  6. "os"
  7. "path/filepath"
  8. "strings"
  9. "kitty/tools/cli"
  10. "kitty/tools/tty"
  11. "kitty/tools/tui"
  12. "kitty/tools/tui/shell_integration"
  13. "kitty/tools/utils"
  14. "golang.org/x/exp/slices"
  15. "golang.org/x/sys/unix"
  16. )
  17. var _ = fmt.Print
  18. type Options struct {
  19. Shell string
  20. ShellIntegration string
  21. Env []string
  22. Cwd string
  23. InjectSelfOntoPath string
  24. }
  25. func inject_self_onto_path() {
  26. if exe, err := os.Executable(); err == nil {
  27. if exe_dir, err := filepath.Abs(exe); err == nil {
  28. realpath := func(x string) string {
  29. if ans, err := filepath.EvalSymlinks(x); err == nil {
  30. return ans
  31. }
  32. return x
  33. }
  34. exe_dir = realpath(filepath.Dir(exe_dir))
  35. path_items := strings.Split(os.Getenv("PATH"), string(os.PathListSeparator))
  36. realpath_items := utils.Map(realpath, path_items)
  37. done := false
  38. changed := false
  39. is_executable_file := func(q string) bool {
  40. if unix.Access(q, unix.X_OK) != nil {
  41. return false
  42. }
  43. if s, err := os.Stat(q); err == nil && !s.IsDir() {
  44. return true
  45. }
  46. return false
  47. }
  48. for i, x := range realpath_items {
  49. q := filepath.Join(x, filepath.Base(exe))
  50. if is_executable_file(q) {
  51. // some kitten already in path
  52. if utils.Samefile(q, exe) {
  53. done = true
  54. break
  55. }
  56. path_items = slices.Insert(path_items, i, exe_dir)
  57. changed, done = true, true
  58. break
  59. }
  60. }
  61. if !done {
  62. path_items = append(path_items, exe_dir)
  63. changed = true
  64. }
  65. if changed {
  66. os.Setenv("PATH", strings.Join(path_items, string(os.PathListSeparator)))
  67. }
  68. }
  69. }
  70. }
  71. func main(args []string, opts *Options) (rc int, err error) {
  72. if len(args) > 0 {
  73. tui.RunCommandRestoringTerminalToSaneStateAfter(args)
  74. }
  75. env_before := os.Environ()
  76. changed := false
  77. for _, entry := range opts.Env {
  78. k, v, found := strings.Cut(entry, "=")
  79. if found {
  80. if err := os.Setenv(k, v); err != nil {
  81. return 1, fmt.Errorf("Failed to set the env var %s with error: %w", k, err)
  82. }
  83. } else {
  84. if err := os.Unsetenv(k); err != nil {
  85. return 1, fmt.Errorf("Failed to unset the env var %s with error: %w", k, err)
  86. }
  87. }
  88. changed = true
  89. }
  90. if os.Getenv("TERM") == "" {
  91. os.Setenv("TERM", kitty.DefaultTermName)
  92. }
  93. if opts.InjectSelfOntoPath == "always" || (opts.InjectSelfOntoPath == "unless-root" && os.Geteuid() != 0) {
  94. inject_self_onto_path()
  95. }
  96. if term := os.Getenv("TERM"); term == kitty.DefaultTermName && shell_integration.PathToTerminfoDb(term) == "" {
  97. if terminfo_dir, err := shell_integration.EnsureTerminfoFiles(); err == nil {
  98. os.Unsetenv("TERMINFO")
  99. existing := os.Getenv("TERMINFO_DIRS")
  100. if existing != "" {
  101. existing = string(os.PathListSeparator) + existing
  102. }
  103. os.Setenv("TERMINFO_DIRS", terminfo_dir+existing)
  104. }
  105. }
  106. err = tui.RunShell(tui.ResolveShell(opts.Shell), tui.ResolveShellIntegration(opts.ShellIntegration), opts.Cwd)
  107. if changed {
  108. os.Clearenv()
  109. for _, entry := range env_before {
  110. k, v, _ := strings.Cut(entry, "=")
  111. os.Setenv(k, v)
  112. }
  113. }
  114. if err != nil {
  115. rc = 1
  116. }
  117. return
  118. }
  119. var debugprintln = tty.DebugPrintln
  120. var _ = debugprintln
  121. func EntryPoint(root *cli.Command) *cli.Command {
  122. sc := root.AddSubCommand(&cli.Command{
  123. Name: "run-shell",
  124. Usage: "[options] [optional cmd to run before running the shell ...]",
  125. ShortDescription: "Run the user's shell with shell integration enabled",
  126. HelpText: "Run the users's configured shell. If the shell supports shell integration, enable it based on the user's configured shell_integration setting.",
  127. Run: func(cmd *cli.Command, args []string) (ret int, err error) {
  128. opts := &Options{}
  129. err = cmd.GetOptionValues(opts)
  130. if err != nil {
  131. return 1, err
  132. }
  133. return main(args, opts)
  134. },
  135. })
  136. sc.Add(cli.OptionSpec{
  137. Name: "--shell-integration",
  138. Help: "Specify a value for the :opt:`shell_integration` option, overriding the one from :file:`kitty.conf`.",
  139. })
  140. sc.Add(cli.OptionSpec{
  141. Name: "--shell",
  142. Default: ".",
  143. Help: "Specify the shell command to run. The default value of :code:`.` will use the parent shell if recognized, falling back to the value of the :opt:`shell` option from :file:`kitty.conf`.",
  144. })
  145. sc.Add(cli.OptionSpec{
  146. Name: "--env",
  147. Help: "Specify an env var to set before running the shell. Of the form KEY=VAL. Can be specified multiple times. If no = is present KEY is unset.",
  148. Type: "list",
  149. })
  150. sc.Add(cli.OptionSpec{
  151. Name: "--cwd",
  152. Help: "The working directory to use when executing the shell.",
  153. })
  154. sc.Add(cli.OptionSpec{
  155. Name: "--inject-self-onto-path",
  156. Help: "Add the directory containing this kitten binary to PATH. Directory is added only if not already present.",
  157. Default: "always",
  158. Choices: "always,never,unless-root",
  159. })
  160. return sc
  161. }