210 lines
4.8 KiB
Go
210 lines
4.8 KiB
Go
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
|
|
|
package at
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/shlex"
|
|
|
|
"kitty/tools/cli"
|
|
"kitty/tools/cli/markup"
|
|
"kitty/tools/tui/loop"
|
|
"kitty/tools/tui/readline"
|
|
"kitty/tools/utils"
|
|
)
|
|
|
|
var _ = fmt.Print
|
|
|
|
var formatter *markup.Context
|
|
|
|
const prompt = "🐱 "
|
|
|
|
var ErrExec = errors.New("Execute command")
|
|
|
|
func shell_loop(rl *readline.Readline, kill_if_signaled bool) (int, error) {
|
|
lp, err := loop.New(loop.NoAlternateScreen, loop.NoRestoreColors)
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
rl.ChangeLoopAndResetText(lp)
|
|
|
|
lp.OnInitialize = func() (string, error) {
|
|
rl.Start()
|
|
return "", nil
|
|
}
|
|
lp.OnFinalize = func() string { rl.End(); return "" }
|
|
|
|
lp.OnResumeFromStop = func() error {
|
|
rl.Start()
|
|
return nil
|
|
}
|
|
|
|
lp.OnResize = rl.OnResize
|
|
|
|
lp.OnKeyEvent = func(event *loop.KeyEvent) error {
|
|
err := rl.OnKeyEvent(event)
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
lp.Quit(0)
|
|
return nil
|
|
}
|
|
if err == readline.ErrAcceptInput {
|
|
if strings.HasSuffix(rl.TextBeforeCursor(), "\\") && rl.CursorAtEndOfLine() {
|
|
rl.OnText("\n", false, false)
|
|
rl.Redraw()
|
|
return nil
|
|
}
|
|
return ErrExec
|
|
}
|
|
return err
|
|
}
|
|
if event.Handled {
|
|
rl.Redraw()
|
|
return nil
|
|
}
|
|
return nil
|
|
}
|
|
|
|
lp.OnText = func(text string, from_key_event, in_bracketed_paste bool) error {
|
|
err := rl.OnText(text, from_key_event, in_bracketed_paste)
|
|
if err == nil {
|
|
rl.Redraw()
|
|
}
|
|
return err
|
|
}
|
|
|
|
err = lp.Run()
|
|
if err != nil {
|
|
return 1, err
|
|
}
|
|
ds := lp.DeathSignalName()
|
|
if ds != "" {
|
|
if kill_if_signaled {
|
|
lp.KillIfSignalled()
|
|
return 1, nil
|
|
}
|
|
return 1, fmt.Errorf("Killed by signal: %s", ds)
|
|
}
|
|
return 0, nil
|
|
}
|
|
|
|
func print_basic_help() {
|
|
fmt.Println("Control kitty by sending it commands.")
|
|
fmt.Println()
|
|
fmt.Println(formatter.Title("Commands") + ":")
|
|
r := EntryPoint(cli.NewRootCommand())
|
|
for _, g := range r.SubCommandGroups {
|
|
for _, sc := range g.SubCommands {
|
|
fmt.Println(" ", formatter.Green(sc.Name))
|
|
fmt.Println(" ", sc.ShortDescription)
|
|
}
|
|
}
|
|
fmt.Println(" ", formatter.Green("exit"))
|
|
fmt.Println(" ", "Exit this shell")
|
|
}
|
|
|
|
func exec_command(rl *readline.Readline, cmdline string) bool {
|
|
parsed_cmdline, err := shlex.Split(cmdline)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "Could not parse cmdline:", err)
|
|
return true
|
|
}
|
|
if len(parsed_cmdline) == 0 {
|
|
return true
|
|
}
|
|
hi := readline.HistoryItem{Timestamp: time.Now(), Cmd: cmdline, ExitCode: -1}
|
|
switch parsed_cmdline[0] {
|
|
case "exit":
|
|
hi.ExitCode = 0
|
|
rl.AddHistoryItem(hi)
|
|
return false
|
|
case "help":
|
|
hi.ExitCode = 0
|
|
defer rl.AddHistoryItem(hi)
|
|
if len(parsed_cmdline) == 1 {
|
|
print_basic_help()
|
|
return true
|
|
}
|
|
switch parsed_cmdline[1] {
|
|
case "exit":
|
|
fmt.Println("Exit this shell")
|
|
case "help":
|
|
fmt.Println("Show help")
|
|
default:
|
|
r := EntryPoint(cli.NewRootCommand())
|
|
sc := r.FindSubCommand(parsed_cmdline[1])
|
|
if sc == nil {
|
|
hi.ExitCode = 1
|
|
fmt.Fprintln(os.Stderr, "No command named", formatter.BrightRed(parsed_cmdline[1]), ". Type help for a list of commands")
|
|
} else {
|
|
sc.ShowHelpWithCommandString(sc.Name)
|
|
}
|
|
}
|
|
return true
|
|
default:
|
|
exe, err := os.Executable()
|
|
if err != nil {
|
|
exe, err = exec.LookPath("kitty-tool")
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "Could not find the kitty-tool executable")
|
|
return false
|
|
}
|
|
}
|
|
cmdline := []string{"kitty-tool", "@"}
|
|
cmdline = append(cmdline, parsed_cmdline...)
|
|
cmd := exec.Cmd{Path: exe, Args: cmdline, Stdin: os.Stdin, Stdout: os.Stdout, Stderr: os.Stderr}
|
|
err = cmd.Run()
|
|
hi.Duration = time.Now().Sub(hi.Timestamp)
|
|
hi.ExitCode = 0
|
|
if err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok {
|
|
hi.ExitCode = exitError.ExitCode()
|
|
}
|
|
fmt.Fprintln(os.Stderr, err)
|
|
}
|
|
rl.AddHistoryItem(hi)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func shell_main(cmd *cli.Command, args []string) (int, error) {
|
|
formatter = markup.New(true)
|
|
fmt.Println("Welcome to the kitty shell!")
|
|
fmt.Println("Use", formatter.Green("help"), "for assistance or", formatter.Green("exit"), "to quit.")
|
|
if atwid := os.Getenv("KITTY_SHELL_ACTIVE_WINDOW_ID"); atwid != "" {
|
|
amsg := "Previously active window id: " + atwid
|
|
os.Unsetenv("KITTY_SHELL_ACTIVE_WINDOW_ID")
|
|
if attid := os.Getenv("KITTY_SHELL_ACTIVE_TAB_ID"); attid != "" {
|
|
os.Unsetenv("KITTY_SHELL_ACTIVE_TAB_ID")
|
|
amsg += " and tab id: " + attid
|
|
}
|
|
fmt.Println(amsg)
|
|
}
|
|
rl := readline.New(nil, readline.RlInit{Prompt: prompt, HistoryPath: filepath.Join(utils.CacheDir(), "shell.history.json")})
|
|
defer func() {
|
|
rl.Shutdown()
|
|
}()
|
|
for {
|
|
rc, err := shell_loop(rl, true)
|
|
if err != nil {
|
|
if err == ErrExec {
|
|
cmdline := rl.AllText()
|
|
cmdline = strings.ReplaceAll(cmdline, "\\\n", "")
|
|
if !exec_command(rl, cmdline) {
|
|
return 0, nil
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
return rc, err
|
|
}
|
|
}
|