123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253 |
- // License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
- package tui
- import (
- "fmt"
- "kitty"
- "os"
- "os/exec"
- "os/signal"
- "path/filepath"
- "runtime"
- "strings"
- "sync"
- "github.com/shirou/gopsutil/v3/process"
- "golang.org/x/sys/unix"
- "kitty/tools/config"
- "kitty/tools/tty"
- "kitty/tools/tui/loop"
- "kitty/tools/tui/shell_integration"
- "kitty/tools/utils"
- "kitty/tools/utils/shlex"
- )
- var _ = fmt.Print
- type KittyOpts struct {
- Shell, Shell_integration string
- }
- func read_relevant_kitty_opts(path string) KittyOpts {
- ans := KittyOpts{Shell: kitty.KittyConfigDefaults.Shell, Shell_integration: kitty.KittyConfigDefaults.Shell_integration}
- handle_line := func(key, val string) error {
- switch key {
- case "shell":
- ans.Shell = strings.TrimSpace(val)
- case "shell_integration":
- ans.Shell_integration = strings.TrimSpace(val)
- }
- return nil
- }
- cp := config.ConfigParser{LineHandler: handle_line}
- _ = cp.ParseFiles(path)
- if ans.Shell == "" {
- ans.Shell = kitty.KittyConfigDefaults.Shell
- }
- return ans
- }
- func get_effective_ksi_env_var(x string) string {
- parts := strings.Split(strings.TrimSpace(strings.ToLower(x)), " ")
- current := utils.NewSetWithItems(parts...)
- if current.Has("disabled") {
- return ""
- }
- allowed := utils.NewSetWithItems(kitty.AllowedShellIntegrationValues...)
- if !current.IsSubsetOf(allowed) {
- return relevant_kitty_opts().Shell_integration
- }
- return x
- }
- var relevant_kitty_opts = sync.OnceValue(func() KittyOpts {
- return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf"))
- })
- func get_shell_from_kitty_conf() (shell string) {
- shell = relevant_kitty_opts().Shell
- if shell == "." {
- s, e := utils.LoginShellForCurrentUser()
- if e != nil {
- shell = "/bin/sh"
- } else {
- shell = s
- }
- }
- return
- }
- func find_shell_parent_process() string {
- var p *process.Process
- var err error
- for {
- if p == nil {
- p, err = process.NewProcess(int32(os.Getppid()))
- } else {
- p, err = p.Parent()
- }
- if err != nil {
- return ""
- }
- if cmdline, err := p.CmdlineSlice(); err == nil && len(cmdline) > 0 {
- exe := get_shell_name(filepath.Base(cmdline[0]))
- if shell_integration.IsSupportedShell(exe) {
- return exe
- }
- }
- }
- }
- func ResolveShell(shell string) []string {
- switch shell {
- case "":
- shell = get_shell_from_kitty_conf()
- case ".":
- if shell = find_shell_parent_process(); shell == "" {
- shell = get_shell_from_kitty_conf()
- }
- }
- shell_cmd, err := shlex.Split(shell)
- if err != nil {
- shell_cmd = []string{shell}
- }
- exe := utils.FindExe(shell_cmd[0])
- if unix.Access(exe, unix.X_OK) != nil {
- shell_cmd = []string{"/bin/sh"}
- }
- return shell_cmd
- }
- func ResolveShellIntegration(shell_integration string) string {
- if shell_integration == "" {
- shell_integration = relevant_kitty_opts().Shell_integration
- }
- return get_effective_ksi_env_var(shell_integration)
- }
- func get_shell_name(argv0 string) (ans string) {
- ans = filepath.Base(argv0)
- if strings.HasSuffix(strings.ToLower(ans), ".exe") {
- ans = ans[:len(ans)-4]
- }
- return strings.TrimPrefix(ans, "-")
- }
- func rc_modification_allowed(ksi string) (allowed bool, set_ksi_env_var bool) {
- allowed = ksi != ""
- set_ksi_env_var = true
- for _, x := range strings.Split(ksi, " ") {
- switch x {
- case "disabled":
- allowed = false
- set_ksi_env_var = false
- break
- case "no-rc":
- allowed = false
- break
- }
- }
- return
- }
- func copy_os_env_as_dict() map[string]string {
- oenv := os.Environ()
- env := make(map[string]string, len(oenv))
- for _, x := range oenv {
- if k, v, found := strings.Cut(x, "="); found {
- env[k] = v
- }
- }
- return env
- }
- func RunShell(shell_cmd []string, shell_integration_env_var_val, cwd string) (err error) {
- shell_name := get_shell_name(shell_cmd[0])
- var shell_env map[string]string
- if shell_integration.IsSupportedShell(shell_name) {
- rc_mod_allowed, set_ksi_env_var := rc_modification_allowed(shell_integration_env_var_val)
- if rc_mod_allowed {
- // KITTY_SHELL_INTEGRATION is always set by this function
- argv, env, err := shell_integration.Setup(shell_name, shell_integration_env_var_val, shell_cmd, copy_os_env_as_dict())
- if err != nil {
- return err
- }
- shell_cmd = argv
- shell_env = env
- } else if set_ksi_env_var {
- shell_env = copy_os_env_as_dict()
- shell_env["KITTY_SHELL_INTEGRATION"] = shell_integration_env_var_val
- }
- }
- exe := shell_cmd[0]
- if runtime.GOOS == "darwin" && (os.Getenv("KITTY_RUNNING_SHELL_INTEGRATION_TEST") != "1" || os.Getenv("KITTY_RUNNING_BASH_INTEGRATION_TEST") != "") {
- // ensure shell runs in login mode. On macOS lots of people use ~/.bash_profile instead of ~/.bashrc
- // which means they expect the shell to run in login mode always. Le Sigh.
- shell_cmd[0] = "-" + filepath.Base(shell_cmd[0])
- }
- var env []string
- if shell_env != nil {
- env = make([]string, 0, len(shell_env))
- for k, v := range shell_env {
- env = append(env, fmt.Sprintf("%s=%s", k, v))
- }
- } else {
- env = os.Environ()
- }
- // fmt.Println(fmt.Sprintf("%s %v\n%#v", utils.FindExe(exe), shell_cmd, env))
- if cwd != "" {
- _ = os.Chdir(cwd)
- }
- return unix.Exec(utils.FindExe(exe), shell_cmd, env)
- }
- var debugprintln = tty.DebugPrintln
- var _ = debugprintln
- func RunCommandRestoringTerminalToSaneStateAfter(cmd []string) {
- exe := utils.FindExe(cmd[0])
- c := exec.Command(exe, cmd[1:]...)
- c.Stdout = os.Stdout
- c.Stdin = os.Stdin
- c.Stderr = os.Stderr
- term, err := tty.OpenControllingTerm()
- if err == nil {
- var state_before unix.Termios
- if term.Tcgetattr(&state_before) == nil {
- if _, err = term.WriteString(loop.SAVE_PRIVATE_MODE_VALUES); err != nil {
- fmt.Fprintln(os.Stderr, "failed to write to controlling terminal with error:", err)
- return
- }
- defer func() {
- _, _ = term.WriteString(strings.Join([]string{
- loop.RESTORE_PRIVATE_MODE_VALUES,
- "\x1b[=u", // reset kitty keyboard protocol to legacy
- "\x1b[1 q", // blinking block cursor
- loop.DECTCEM.EscapeCodeToSet(), // cursor visible
- "\x1b]112\a", // reset cursor color
- }, ""))
- _ = term.Tcsetattr(tty.TCSANOW, &state_before)
- term.Close()
- }()
- } else {
- defer term.Close()
- }
- }
- func() {
- if err = c.Start(); err != nil {
- fmt.Fprintln(os.Stderr, cmd[0], "failed to start with error:", err)
- return
- }
- // Ignore SIGINT as the kernel tends to send it to us as well as the
- // subprocess on Ctrl+C
- signal.Ignore(os.Interrupt)
- defer signal.Reset(os.Interrupt)
- err = c.Wait()
- }()
- if err != nil {
- fmt.Fprintln(os.Stderr, cmd[0], "failed with error:", err)
- }
- }
|