From 3a76ccf10b23b6da0fe9ff6d8f51f607d268e216 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 25 Jun 2023 20:34:48 +0530 Subject: [PATCH] Port the fish setup function to Go --- tools/tui/run.go | 8 ++-- tools/tui/shell_integration/api.go | 62 +++++++++++++++++++++---- tools/tui/shell_integration/api_test.go | 42 +++++++++++++++++ 3 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 tools/tui/shell_integration/api_test.go diff --git a/tools/tui/run.go b/tools/tui/run.go index 3714f2ac8..41ae2c642 100644 --- a/tools/tui/run.go +++ b/tools/tui/run.go @@ -125,7 +125,7 @@ func RunShell(shell_cmd []string, shell_integration_env_var_val string) (err err env[k] = v } } - argv, env, err := shell_integration.Setup(shell_name, shell_cmd, env) + argv, env, err := shell_integration.Setup(shell_name, shell_integration_env_var_val, shell_cmd, env) if err != nil { return err } @@ -134,18 +134,20 @@ func RunShell(shell_cmd []string, shell_integration_env_var_val string) (err err } exe := shell_cmd[0] if runtime.GOOS == "darwin" { - // ensure shell runs in login mode + // 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)) + 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)) return unix.Exec(utils.FindExe(exe), shell_cmd, env) } diff --git a/tools/tui/shell_integration/api.go b/tools/tui/shell_integration/api.go index d51e42a3e..45a00e8df 100644 --- a/tools/tui/shell_integration/api.go +++ b/tools/tui/shell_integration/api.go @@ -4,14 +4,20 @@ package shell_integration import ( "archive/tar" + "bytes" "fmt" + "kitty/tools/utils" "os" "path/filepath" + "strings" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" ) var _ = fmt.Print -type integration_setup_func = func(argv []string, env map[string]string) ([]string, map[string]string, error) +type integration_setup_func = func(shell_integration_dir string, argv []string, env map[string]string) ([]string, map[string]string, error) func extract_shell_integration_for(shell_name string, dest_dir string) (err error) { d := Data() @@ -32,7 +38,10 @@ func extract_shell_integration_for(shell_name string, dest_dir string) (err erro return } case tar.TypeReg: - if err = os.WriteFile(dest, entry.Data, 0o644); err != nil { + if existing, rerr := os.ReadFile(dest); rerr == nil && bytes.Equal(existing, entry.Data) { + continue + } + if err = utils.AtomicWriteFile(dest, entry.Data, 0o644); err != nil { return } } @@ -40,15 +49,44 @@ func extract_shell_integration_for(shell_name string, dest_dir string) (err erro return } -func zsh_setup_func(argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { +func EnsureShellIntegrationFilesFor(shell_name string) (shell_integration_dir string, err error) { + if kid := os.Getenv("KITTY_INSTALLATION_DIR"); kid != "" { + if s, e := os.Stat(kid); e == nil && s.IsDir() { + q := filepath.Join(kid, "shell-integration", shell_name) + if s, e := os.Stat(q); e == nil && s.IsDir() { + return q, nil + } + } + } + base := filepath.Join(utils.CacheDir(), "extracted-ksi") + if err = os.MkdirAll(base, 0o755); err != nil { + return "", err + } + if err = extract_shell_integration_for(shell_name, base); err != nil { + return "", err + } + return filepath.Join(base, "shell-integration"), nil +} + +func zsh_setup_func(shell_integration_dir string, argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { return } -func fish_setup_func(argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { - return +func fish_setup_func(shell_integration_dir string, argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { + shell_integration_dir = filepath.Dir(shell_integration_dir) + val := env[`XDG_DATA_DIRS`] + env[`KITTY_FISH_XDG_DATA_DIR`] = shell_integration_dir + if val == "" { + env[`XDG_DATA_DIRS`] = shell_integration_dir + } else { + dirs := utils.Filter(strings.Split(val, string(filepath.ListSeparator)), func(x string) bool { return x != "" }) + dirs = append([]string{shell_integration_dir}, dirs...) + env[`XDG_DATA_DIRS`] = strings.Join(dirs, string(filepath.ListSeparator)) + } + return argv, env, nil } -func bash_setup_func(argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { +func bash_setup_func(shell_integration_dir string, argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) { return } @@ -66,6 +104,14 @@ func setup_func_for_shell(shell_name string) integration_setup_func { func IsSupportedShell(shell_name string) bool { return setup_func_for_shell(shell_name) != nil } -func Setup(shell_name string, argv []string, env map[string]string) ([]string, map[string]string, error) { - return setup_func_for_shell(shell_name)(argv, env) +func Setup(shell_name string, ksi_var string, argv []string, env map[string]string) ([]string, map[string]string, error) { + ksi_dir, err := EnsureShellIntegrationFilesFor(shell_name) + if err != nil { + return nil, nil, err + } + argv, env, err = setup_func_for_shell(shell_name)(ksi_dir, slices.Clone(argv), maps.Clone(env)) + if err == nil { + env[`KITTY_SHELL_INTEGRATION`] = ksi_var + } + return argv, env, err } diff --git a/tools/tui/shell_integration/api_test.go b/tools/tui/shell_integration/api_test.go new file mode 100644 index 000000000..c53965273 --- /dev/null +++ b/tools/tui/shell_integration/api_test.go @@ -0,0 +1,42 @@ +// License: GPLv3 Copyright: 2023, Kovid Goyal, + +package shell_integration + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" +) + +var _ = fmt.Print + +func TestExtractShellIntegration(t *testing.T) { + tdir := t.TempDir() + if err := extract_shell_integration_for("zsh", tdir); err != nil { + t.Fatal(err) + } + kzsh := filepath.Join(tdir, "shell-integration", "zsh", "kitty.zsh") + if _, err := os.Stat(kzsh); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(filepath.Join(tdir, "shell-integration", "zsh", "completions", "_kitty")); err != nil { + t.Fatal(err) + } + orig, err := os.ReadFile(kzsh) + if err != nil { + t.Fatal(err) + } + os.WriteFile(kzsh, []byte("changed"), 0o644) + if err := extract_shell_integration_for("zsh", tdir); err != nil { + t.Fatal(err) + } + changed, err := os.ReadFile(kzsh) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(changed, orig) { + t.Fatalf("Failed to update shell integration file") + } +}