diff --git a/go.mod b/go.mod index 2cb75ecfe..0815f6d29 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/spf13/pflag v1.0.5 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 ) require ( diff --git a/go.sum b/go.sum index aa928e16b..e07abef29 100644 --- a/go.sum +++ b/go.sum @@ -21,7 +21,10 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/kitty/remote_control.py b/kitty/remote_control.py index 8f78bb869..9e3063032 100644 --- a/kitty/remote_control.py +++ b/kitty/remote_control.py @@ -201,7 +201,7 @@ global_options_spec = partial('''\ An address for the kitty instance to control. Corresponds to the address given to the kitty instance via the :option:`kitty --listen-on` option or the :opt:`listen_on` setting in :file:`kitty.conf`. If not specified, the -environment variable :env:`KITTY_LISTEN_ON` is checked. If that is also not +environment variable :envvar:`KITTY_LISTEN_ON` is checked. If that is also not found, messages are sent to the controlling terminal for this process, i.e. they will only work if this process is run within a kitty window. diff --git a/tools/cli/infrastructure.go b/tools/cli/infrastructure.go index febb879d3..a2c418937 100644 --- a/tools/cli/infrastructure.go +++ b/tools/cli/infrastructure.go @@ -30,17 +30,17 @@ func GetTTYSize() (*unix.Winsize, error) { return nil, fmt.Errorf("STDOUT is not a TTY") } -func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) { - flags.String(name, choices[0], usage) +func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) *string { cmd.Annotations["choices-"+name] = strings.Join(choices, "\000") + return flags.String(name, choices[0], usage) } -func Choices(cmd *cobra.Command, name string, usage string, choices ...string) { - add_choices(cmd, cmd.Flags(), choices, name, usage) +func Choices(cmd *cobra.Command, name string, usage string, choices ...string) *string { + return add_choices(cmd, cmd.Flags(), choices, name, usage) } -func PersistentChoices(cmd *cobra.Command, name string, usage string, choices ...string) { - add_choices(cmd, cmd.PersistentFlags(), choices, name, usage) +func PersistentChoices(cmd *cobra.Command, name string, usage string, choices ...string) *string { + return add_choices(cmd, cmd.PersistentFlags(), choices, name, usage) } func key_in_slice(vals []string, key string) bool { @@ -333,43 +333,53 @@ func show_usage(cmd *cobra.Command) error { } output_text := output.String() // fmt.Printf("%#v\n", output_text) - if stdout_is_terminal && cmd.Annotations["allow-pager"] != "no" { + if cmd.Annotations["use-pager-for-usage"] == "true" && stdout_is_terminal && cmd.Annotations["allow-pager"] != "no" { pager := exec.Command(kitty.DefaultPager[0], kitty.DefaultPager[1:]...) pager.Stdin = strings.NewReader(output_text) pager.Stdout = os.Stdout pager.Stderr = os.Stderr pager.Run() } else { - print(output_text) + cmd.OutOrStdout().Write([]byte(output_text)) } return nil } func CreateCommand(cmd *cobra.Command) *cobra.Command { cmd.Annotations = make(map[string]string) - if cmd.Run == nil { - cmd.Run = SubCommandRequired + if cmd.Run == nil && cmd.RunE == nil { + cmd.RunE = func(cmd *cobra.Command, args []string) error { + if len(cmd.Commands()) > 0 { + return fmt.Errorf("%s. Use %s -h to get a list of available sub-commands", err_fmt("No sub-command specified"), full_command_name(cmd)) + } + return nil + } } + cmd.SilenceErrors = true + cmd.SilenceUsage = true + orig_pre_run := cmd.PersistentPreRunE + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + err := ValidateChoices(cmd, args) + if err != nil || orig_pre_run == nil { + return err + } + return orig_pre_run(cmd, args) + } + cmd.PersistentFlags().SortFlags = false cmd.Flags().SortFlags = false return cmd } -func UsageAndError(cmd *cobra.Command, err string) { - cmd.Annotations["usage-suffix"] = err - cmd.Usage() -} - -func SubCommandRequired(cmd *cobra.Command, args []string) { - UsageAndError(cmd, "No command specified: "+exe_fmt(full_command_name(cmd))+err_fmt(" add-a-command-here")) - os.Exit(1) +func show_help(cmd *cobra.Command, args []string) { + cmd.Annotations["use-pager-for-usage"] = "true" + show_usage(cmd) } func Init(root *cobra.Command) { stdout_is_terminal = isatty.IsTerminal(os.Stdout.Fd()) RootCmd = root root.Version = kitty.VersionString - root.PersistentPreRunE = ValidateChoices root.SetUsageFunc(show_usage) - root.SetHelpTemplate("{{.UsageString}}") + root.SetHelpFunc(show_help) } diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index 44fcf88eb..e8928e068 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -1,7 +1,15 @@ package at import ( + "fmt" + "io/ioutil" + "os" + "strings" + + "github.com/mattn/go-isatty" "github.com/spf13/cobra" + "golang.org/x/sys/unix" + "golang.org/x/term" "kitty/tools/cli" "kitty/tools/crypto" @@ -9,36 +17,100 @@ import ( var encrypt_cmd = crypto.Encrypt_cmd +type GlobalOptions struct { + to, password, use_password string + to_from_env bool +} + +var global_options GlobalOptions + +func get_password(password string, password_file string, password_env string, use_password string) (string, error) { + if use_password == "never" { + return "", nil + } + ans := "" + if password != "" { + ans = password + } + if ans == "" && password_file != "" { + if password_file == "-" { + if isatty.IsTerminal(os.Stdin.Fd()) { + q, err := term.ReadPassword(int(os.Stdin.Fd())) + if err != nil { + ans = string(q) + } + } else { + q, err := ioutil.ReadAll(os.Stdin) + if err != nil { + ans = strings.TrimRight(string(q), " \n\t") + } + ttyf, err := os.Open("/dev/tty") + if err != nil { + err = unix.Dup2(int(ttyf.Fd()), int(os.Stdin.Fd())) + ttyf.Close() + } + } + } else { + q, err := ioutil.ReadFile(password_file) + if err != nil { + ans = strings.TrimRight(string(q), " \n\t") + } + } + } + if ans == "" && password_env != "" { + ans = os.Getenv(password_env) + } + if ans == "" && use_password == "always" { + return ans, fmt.Errorf("No password was found") + } + if len(ans) > 1024 { + return ans, fmt.Errorf("Specified password is too long") + } + return ans, nil +} + func EntryPoint(tool_root *cobra.Command) *cobra.Command { + var to, password, password_file, password_env, use_password *string var root = cli.CreateCommand(&cobra.Command{ Use: "@ [global options] command [command options] [command args]", Short: "Control kitty remotely", Long: "Control kitty by sending it commands. Set the allow_remote_control option in :file:`kitty.conf` or use a password, for this to work.", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + if *to == "" { + *to = os.Getenv("KITTY_LISTEN_ON") + global_options.to_from_env = true + } + global_options.to = *to + global_options.use_password = *use_password + q, err := get_password(*password, *password_file, *password_env, *use_password) + global_options.password = q + return err + }, }) root.Annotations["options_title"] = "Global options" - root.PersistentFlags().String("to", "", + to = root.PersistentFlags().String("to", "", "An address for the kitty instance to control. Corresponds to the address given"+ " to the kitty instance via the :option:`kitty --listen-on` option or the :opt:`listen_on` setting in :file:`kitty.conf`. If not"+ - " specified, the environment variable :env:`KITTY_LISTEN_ON` is checked. If that"+ + " specified, the environment variable :envvar:`KITTY_LISTEN_ON` is checked. If that"+ " is also not found, messages are sent to the controlling terminal for this"+ " process, i.e. they will only work if this process is run within a kitty window.") - root.PersistentFlags().String("password", "", + password = root.PersistentFlags().String("password", "", "A password to use when contacting kitty. This will cause kitty to ask the user"+ " for permission to perform the specified action, unless the password has been"+ " accepted before or is pre-configured in :file:`kitty.conf`.") - root.PersistentFlags().String("password-file", "rc-pass", + password_file = root.PersistentFlags().String("password-file", "rc-pass", "A file from which to read the password. Trailing whitespace is ignored. Relative"+ " paths are resolved from the kitty configuration directory. Use - to read from STDIN."+ " Used if no :option:`--password` is supplied. Defaults to checking for the"+ " :file:`rc-pass` file in the kitty configuration directory.") - root.PersistentFlags().String("password-env", "KITTY_RC_PASSWORD", + password_env = root.PersistentFlags().String("password-env", "KITTY_RC_PASSWORD", "The name of an environment variable to read the password from."+ " Used if no :option:`--password-file` or :option:`--password` is supplied.") - cli.PersistentChoices(root, "use-password", "If no password is available, kitty will usually just send the remote control command without a password. This option can be used to force it to always or never use the supplied password.", "if-available", "always", "never") + use_password = cli.PersistentChoices(root, "use-password", "If no password is available, kitty will usually just send the remote control command without a password. This option can be used to force it to always or never use the supplied password.", "if-available", "always", "never") return root } diff --git a/tools/cmd/main.go b/tools/cmd/main.go index bc44c5b78..f09e0a016 100644 --- a/tools/cmd/main.go +++ b/tools/cmd/main.go @@ -19,7 +19,7 @@ func main() { cli.Init(root) if err := root.Execute(); err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } }