mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-09 13:45:26 +02:00
Work on code to run shells from a kitten with shell integration
This commit is contained in:
182
tools/tui/run.go
Normal file
182
tools/tui/run.go
Normal file
@@ -0,0 +1,182 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package tui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"kitty"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"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 = utils.Once(func() KittyOpts {
|
||||
return read_relevant_kitty_opts(filepath.Join(utils.ConfigDir(), "kitty.conf"))
|
||||
})
|
||||
|
||||
func ResolveShell(shell string) []string {
|
||||
if shell == "" {
|
||||
shell = relevant_kitty_opts().Shell
|
||||
if shell == "." {
|
||||
s, e := utils.LoginShellForCurrentUser()
|
||||
if e != nil {
|
||||
shell = "/bin/sh"
|
||||
} else {
|
||||
shell = s
|
||||
}
|
||||
}
|
||||
}
|
||||
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]
|
||||
}
|
||||
if strings.HasPrefix(ans, "-") {
|
||||
ans = ans[1:]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func rc_modification_allowed(ksi string) bool {
|
||||
for _, x := range strings.Split(ksi, " ") {
|
||||
switch x {
|
||||
case "disabled", "no-rc":
|
||||
return false
|
||||
}
|
||||
}
|
||||
return ksi != ""
|
||||
}
|
||||
|
||||
func RunShell(shell_cmd []string, shell_integration_env_var_val string) (err error) {
|
||||
shell_name := get_shell_name(shell_cmd[0])
|
||||
var shell_env map[string]string
|
||||
if rc_modification_allowed(shell_integration_env_var_val) && shell_integration.IsSupportedShell(shell_name) {
|
||||
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
|
||||
}
|
||||
}
|
||||
argv, env, err := shell_integration.Setup(shell_name, shell_cmd, env)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
shell_cmd = argv
|
||||
shell_env = env
|
||||
}
|
||||
exe := shell_cmd[0]
|
||||
if runtime.GOOS == "darwin" {
|
||||
// ensure shell runs in login mode
|
||||
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()
|
||||
}
|
||||
return unix.Exec(utils.FindExe(exe), shell_cmd, env)
|
||||
}
|
||||
|
||||
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 {
|
||||
term.WriteString(loop.SAVE_PRIVATE_MODE_VALUES)
|
||||
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()
|
||||
}
|
||||
}
|
||||
err = c.Run()
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, cmd[0], "failed with error:", err)
|
||||
}
|
||||
}
|
||||
71
tools/tui/shell_integration/api.go
Normal file
71
tools/tui/shell_integration/api.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package shell_integration
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
type integration_setup_func = func(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()
|
||||
for _, fname := range d.FilesMatching("shell-integration/" + shell_name + "/") {
|
||||
entry := d[fname]
|
||||
dest := filepath.Join(dest_dir, fname)
|
||||
ddir := filepath.Dir(dest)
|
||||
if err = os.MkdirAll(ddir, 0o755); err != nil {
|
||||
return
|
||||
}
|
||||
switch entry.Metadata.Typeflag {
|
||||
case tar.TypeDir:
|
||||
if err = os.MkdirAll(dest, 0o755); err != nil {
|
||||
return
|
||||
}
|
||||
case tar.TypeSymlink:
|
||||
if err = os.Symlink(entry.Metadata.Linkname, dest); err != nil {
|
||||
return
|
||||
}
|
||||
case tar.TypeReg:
|
||||
if err = os.WriteFile(dest, entry.Data, 0o644); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func zsh_setup_func(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 bash_setup_func(argv []string, env map[string]string) (final_argv []string, final_env map[string]string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func setup_func_for_shell(shell_name string) integration_setup_func {
|
||||
switch shell_name {
|
||||
case "zsh":
|
||||
return zsh_setup_func
|
||||
case "fish":
|
||||
return fish_setup_func
|
||||
case "bash":
|
||||
return bash_setup_func
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
69
tools/tui/shell_integration/data.go
Normal file
69
tools/tui/shell_integration/data.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// License: GPLv3 Copyright: 2023, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||
|
||||
package shell_integration
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
_ "embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"kitty/tools/utils"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
//go:embed data_generated.bin
|
||||
var embedded_data string
|
||||
|
||||
type Entry struct {
|
||||
Metadata *tar.Header
|
||||
Data []byte
|
||||
}
|
||||
|
||||
type Container map[string]Entry
|
||||
|
||||
var Data = utils.Once(func() Container {
|
||||
tr := tar.NewReader(utils.ReaderForCompressedEmbeddedData(embedded_data))
|
||||
ans := make(Container, 64)
|
||||
for {
|
||||
hdr, err := tr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
data, err := utils.ReadAll(tr, int(hdr.Size))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ans[hdr.Name] = Entry{hdr, data}
|
||||
}
|
||||
return ans
|
||||
})
|
||||
|
||||
func (self Container) FilesMatching(prefix string, exclude_patterns ...string) []string {
|
||||
ans := make([]string, 0, len(self))
|
||||
patterns := make([]*regexp.Regexp, len(exclude_patterns))
|
||||
for i, exp := range exclude_patterns {
|
||||
patterns[i] = regexp.MustCompile(exp)
|
||||
}
|
||||
for name := range self {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
excluded := false
|
||||
for _, pat := range patterns {
|
||||
if matched := pat.FindString(name); matched != "" {
|
||||
excluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !excluded {
|
||||
ans = append(ans, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ans
|
||||
}
|
||||
Reference in New Issue
Block a user