shell.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package at
  3. import (
  4. "errors"
  5. "fmt"
  6. "io"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. "time"
  11. "kitty/tools/cli"
  12. "kitty/tools/cli/markup"
  13. "kitty/tools/tui/loop"
  14. "kitty/tools/tui/readline"
  15. "kitty/tools/utils"
  16. "kitty/tools/utils/shlex"
  17. )
  18. var _ = fmt.Print
  19. var formatter *markup.Context
  20. const prompt = "🐱 "
  21. var ErrExec = errors.New("Execute command")
  22. func shell_loop(rl *readline.Readline, kill_if_signaled bool) (int, error) {
  23. lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors)
  24. if err != nil {
  25. return 1, err
  26. }
  27. rl.ChangeLoopAndResetText(lp)
  28. lp.OnInitialize = func() (string, error) {
  29. rl.Start()
  30. return "", nil
  31. }
  32. lp.OnFinalize = func() string { rl.End(); return "" }
  33. lp.OnResumeFromStop = func() error {
  34. rl.Start()
  35. return nil
  36. }
  37. lp.OnResize = rl.OnResize
  38. lp.OnKeyEvent = func(event *loop.KeyEvent) error {
  39. err := rl.OnKeyEvent(event)
  40. if err != nil {
  41. if err == io.EOF {
  42. lp.Quit(0)
  43. return nil
  44. }
  45. if err == readline.ErrAcceptInput {
  46. if strings.HasSuffix(rl.TextBeforeCursor(), "\\") && rl.CursorAtEndOfLine() {
  47. rl.OnText("\n", false, false)
  48. rl.Redraw()
  49. return nil
  50. }
  51. rl.MoveCursorToEnd()
  52. rl.Redraw()
  53. lp.ClearToEndOfScreen()
  54. return ErrExec
  55. }
  56. return err
  57. }
  58. if event.Handled {
  59. rl.Redraw()
  60. return nil
  61. }
  62. return nil
  63. }
  64. lp.OnText = func(text string, from_key_event, in_bracketed_paste bool) error {
  65. err := rl.OnText(text, from_key_event, in_bracketed_paste)
  66. if err == nil {
  67. rl.Redraw()
  68. }
  69. return err
  70. }
  71. err = lp.Run()
  72. if err != nil {
  73. return 1, err
  74. }
  75. ds := lp.DeathSignalName()
  76. if ds != "" {
  77. if kill_if_signaled {
  78. lp.KillIfSignalled()
  79. return 1, nil
  80. }
  81. return 1, fmt.Errorf("Killed by signal: %s", ds)
  82. }
  83. return 0, nil
  84. }
  85. func show_basic_help() {
  86. output := strings.Builder{}
  87. fmt.Fprintln(&output, "Control kitty by sending it commands.")
  88. fmt.Fprintln(&output)
  89. fmt.Fprintln(&output, formatter.Title("Commands")+":")
  90. r := EntryPoint(cli.NewRootCommand())
  91. for _, g := range r.SubCommandGroups {
  92. for _, sc := range g.SubCommands {
  93. fmt.Fprintln(&output, " ", formatter.Green(sc.Name))
  94. fmt.Fprintln(&output, " ", sc.ShortDescription)
  95. }
  96. }
  97. fmt.Fprintln(&output, " ", formatter.Green("help"))
  98. fmt.Fprintln(&output, " ", "Show this help")
  99. fmt.Fprintln(&output, " ", formatter.Green("exit"))
  100. fmt.Fprintln(&output, " ", "Exit this shell")
  101. cli.ShowHelpInPager(output.String())
  102. }
  103. func exec_command(at_root_command *cli.Command, rl *readline.Readline, cmdline string) bool {
  104. parsed_cmdline, err := shlex.Split(cmdline)
  105. if err != nil {
  106. fmt.Fprintln(os.Stderr, "Could not parse cmdline:", err)
  107. return true
  108. }
  109. if len(parsed_cmdline) == 0 {
  110. return true
  111. }
  112. cwd, _ := os.Getwd()
  113. hi := readline.HistoryItem{Timestamp: time.Now(), Cmd: rl.AllText(), ExitCode: -1, Cwd: cwd}
  114. switch parsed_cmdline[0] {
  115. case "exit":
  116. hi.ExitCode = 0
  117. rl.AddHistoryItem(hi)
  118. return false
  119. case "help":
  120. hi.ExitCode = 0
  121. defer rl.AddHistoryItem(hi)
  122. if len(parsed_cmdline) == 1 {
  123. show_basic_help()
  124. return true
  125. }
  126. switch parsed_cmdline[1] {
  127. case "exit":
  128. fmt.Println("Exit this shell")
  129. case "help":
  130. fmt.Println("Show help")
  131. default:
  132. sc := at_root_command.FindSubCommand(parsed_cmdline[1])
  133. if sc == nil {
  134. hi.ExitCode = 1
  135. fmt.Fprintln(os.Stderr, "No command named", formatter.BrightRed(parsed_cmdline[1])+". Type help for a list of commands")
  136. } else {
  137. sc.ShowHelpWithCommandString(sc.Name)
  138. }
  139. }
  140. return true
  141. default:
  142. if at_root_command.FindSubCommand(parsed_cmdline[0]) == nil {
  143. hi.ExitCode = 1
  144. fmt.Fprintln(os.Stderr, "No command named", formatter.BrightRed(parsed_cmdline[0])+". Type help for a list of commands")
  145. return true
  146. }
  147. cmdline := []string{"kitten", "@"}
  148. cmdline = append(cmdline, parsed_cmdline...)
  149. root := cli.NewRootCommand()
  150. EntryPoint(root)
  151. hi.ExitCode = root.ExecArgs(cmdline)
  152. hi.Duration = time.Now().Sub(hi.Timestamp)
  153. rl.AddHistoryItem(hi)
  154. }
  155. return true
  156. }
  157. func completions(before_cursor, after_cursor string) (ans *cli.Completions) {
  158. const prefix = "kitten @ "
  159. text := prefix + before_cursor
  160. argv, position_of_last_arg := shlex.SplitForCompletion(text)
  161. if len(argv) == 0 || position_of_last_arg < len(prefix) {
  162. return
  163. }
  164. root := cli.NewRootCommand()
  165. c := root.AddSubCommand(&cli.Command{Name: "kitten"})
  166. EntryPoint(c)
  167. a := c.FindSubCommand("@")
  168. add_sc := func(cmd, desc string) {
  169. var x *cli.Command
  170. if x = a.FindSubCommand(cmd); x == nil {
  171. x = a.AddSubCommand(&cli.Command{Name: cmd})
  172. }
  173. x.ShortDescription = desc
  174. }
  175. add_sc("help", "Show help")
  176. add_sc("exit", "Exit the kitty shell")
  177. root.Validate()
  178. ans = root.GetCompletions(argv, nil)
  179. ans.CurrentWordIdx = position_of_last_arg - len(prefix)
  180. return
  181. }
  182. func shell_main(cmd *cli.Command, args []string) (int, error) {
  183. err := setup_global_options(cmd)
  184. if err != nil {
  185. return 1, err
  186. }
  187. running_shell = true
  188. formatter = markup.New(true)
  189. fmt.Println("Welcome to the kitty shell!")
  190. fmt.Println("Use", formatter.Green("help"), "for assistance or", formatter.Green("exit"), "to quit.")
  191. if atwid := os.Getenv("KITTY_SHELL_ACTIVE_WINDOW_ID"); atwid != "" {
  192. amsg := "Previously active window id: " + atwid
  193. os.Unsetenv("KITTY_SHELL_ACTIVE_WINDOW_ID")
  194. if attid := os.Getenv("KITTY_SHELL_ACTIVE_TAB_ID"); attid != "" {
  195. os.Unsetenv("KITTY_SHELL_ACTIVE_TAB_ID")
  196. amsg += " and tab id: " + attid
  197. }
  198. fmt.Println(amsg)
  199. }
  200. var rl *readline.Readline
  201. combined_completer := func(before_cursor, after_cursor string) *cli.Completions {
  202. ans := completions(before_cursor, after_cursor)
  203. history := rl.HistoryCompleter(before_cursor, after_cursor)
  204. for _, group := range history.Groups {
  205. ans.MergeMatchGroup(group)
  206. }
  207. return ans
  208. }
  209. rl = readline.New(nil, readline.RlInit{Prompt: prompt, Completer: combined_completer, HistoryPath: filepath.Join(utils.CacheDir(), "shell.history.json")})
  210. defer func() {
  211. rl.Shutdown()
  212. }()
  213. for {
  214. rc, err := shell_loop(rl, true)
  215. if err != nil {
  216. if err == ErrExec {
  217. cmdline := rl.AllText()
  218. cmdline = strings.ReplaceAll(cmdline, "\\\n", "")
  219. if !exec_command(cmd, rl, cmdline) {
  220. return 0, nil
  221. }
  222. continue
  223. }
  224. }
  225. return rc, err
  226. }
  227. }