diff --git a/README.md b/README.md index 7a73bd95..71761bbd 100644 --- a/README.md +++ b/README.md @@ -231,14 +231,19 @@ sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens ### Interactive mode If you want `kubectx` and `kubens` commands to present you an interactive menu -with fuzzy searching, you just need to [install +with fuzzy searching, you can do in either of two ways + +* [Install `fzf`](https://github.com/junegunn/fzf) in your PATH. + * OR [Install `sk`](https://github.com/lotabout/skim) in your PATH and set environment variable `PICKER` to `sk` ![kubectx interactive search with fzf](img/kubectx-interactive.gif) If you have `fzf` installed, but want to opt out of using this feature, set the environment variable `KUBECTX_IGNORE_FZF=1`. -If you want to keep `fzf` interactive mode but need the default behavior of the command, you can do it using Unix composability: +> Note: skim support in kubectx and kubens is only in go binary. + +If you want to keep `fzf` or `sk` interactive mode but need the default behavior of the command, you can do it using Unix composability: ``` kubectx | cat ``` diff --git a/cmd/kubectx/flags.go b/cmd/kubectx/flags.go index 060cd1c5..f46946c1 100644 --- a/cmd/kubectx/flags.go +++ b/cmd/kubectx/flags.go @@ -34,16 +34,18 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error { // and decides which operation should be taken. func parseArgs(argv []string) Op { if len(argv) == 0 { - if cmdutil.IsInteractiveMode(os.Stdout) { - return InteractiveSwitchOp{SelfCmd: os.Args[0]} + picker, interactive := cmdutil.IsInteractiveMode(os.Stdout) + if interactive { + return InteractiveSwitchOp{SelfCmd: os.Args[0], Picker: picker} } return ListOp{} } if argv[0] == "-d" { if len(argv) == 1 { - if cmdutil.IsInteractiveMode(os.Stdout) { - return InteractiveDeleteOp{SelfCmd: os.Args[0]} + picker, interactive := cmdutil.IsInteractiveMode(os.Stdout) + if interactive { + return InteractiveDeleteOp{SelfCmd: os.Args[0], Picker: picker} } else { return UnsupportedOp{Err: fmt.Errorf("'-d' needs arguments")} } diff --git a/cmd/kubectx/fzf.go b/cmd/kubectx/fuzzy.go similarity index 76% rename from cmd/kubectx/fzf.go rename to cmd/kubectx/fuzzy.go index 5006129d..4e8bb2d0 100644 --- a/cmd/kubectx/fzf.go +++ b/cmd/kubectx/fuzzy.go @@ -15,27 +15,24 @@ package main import ( - "bytes" - "fmt" "io" - "os" - "os/exec" "strings" "github.com/pkg/errors" "github.com/ahmetb/kubectx/internal/cmdutil" - "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string + Picker string } type InteractiveDeleteOp struct { SelfCmd string + Picker string } func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { @@ -50,19 +47,10 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { } kc.Close() - cmd := exec.Command("fzf", "--ansi", "--no-preview") - var out bytes.Buffer - cmd.Stdin = os.Stdin - cmd.Stderr = stderr - cmd.Stdout = &out - - cmd.Env = append(os.Environ(), - fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), - fmt.Sprintf("%s=1", env.EnvForceColor)) - if err := cmd.Run(); err != nil { - if _, ok := err.(*exec.ExitError); !ok { - return err - } + // Launch fuzzy search window. + out, err := cmdutil.InteractiveSearch(op.Picker, op.SelfCmd, stderr) + if err != nil { + return err } choice := strings.TrimSpace(out.String()) if choice == "" { @@ -91,22 +79,11 @@ func (op InteractiveDeleteOp) Run(_, stderr io.Writer) error { if len(kc.ContextNames()) == 0 { return errors.New("no contexts found in config") } - - cmd := exec.Command("fzf", "--ansi", "--no-preview") - var out bytes.Buffer - cmd.Stdin = os.Stdin - cmd.Stderr = stderr - cmd.Stdout = &out - - cmd.Env = append(os.Environ(), - fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), - fmt.Sprintf("%s=1", env.EnvForceColor)) - if err := cmd.Run(); err != nil { - if _, ok := err.(*exec.ExitError); !ok { - return err - } + // Launch fuzzy search window. + out, err := cmdutil.InteractiveSearch(op.Picker, op.SelfCmd, stderr) + if err != nil { + return err } - choice := strings.TrimSpace(out.String()) if choice == "" { return errors.New("you did not choose any of the options") diff --git a/cmd/kubens/flags.go b/cmd/kubens/flags.go index fc3c64d2..64295295 100644 --- a/cmd/kubens/flags.go +++ b/cmd/kubens/flags.go @@ -34,8 +34,9 @@ func (op UnsupportedOp) Run(_, _ io.Writer) error { // and decides which operation should be taken. func parseArgs(argv []string) Op { if len(argv) == 0 { - if cmdutil.IsInteractiveMode(os.Stdout) { - return InteractiveSwitchOp{SelfCmd: os.Args[0]} + picker, interactive := cmdutil.IsInteractiveMode(os.Stdout) + if interactive { + return InteractiveSwitchOp{SelfCmd: os.Args[0], Picker: picker} } return ListOp{} } diff --git a/cmd/kubens/fzf.go b/cmd/kubens/fuzzy.go similarity index 79% rename from cmd/kubens/fzf.go rename to cmd/kubens/fuzzy.go index 8a497704..b1a465cd 100644 --- a/cmd/kubens/fzf.go +++ b/cmd/kubens/fuzzy.go @@ -15,23 +15,19 @@ package main import ( - "bytes" - "fmt" "io" - "os" - "os/exec" "strings" "github.com/pkg/errors" "github.com/ahmetb/kubectx/internal/cmdutil" - "github.com/ahmetb/kubectx/internal/env" "github.com/ahmetb/kubectx/internal/kubeconfig" "github.com/ahmetb/kubectx/internal/printer" ) type InteractiveSwitchOp struct { SelfCmd string + Picker string } // TODO(ahmetb) This method is heavily repetitive vs kubectx/fzf.go. @@ -46,20 +42,9 @@ func (op InteractiveSwitchOp) Run(_, stderr io.Writer) error { return errors.Wrap(err, "kubeconfig error") } defer kc.Close() - - cmd := exec.Command("fzf", "--ansi", "--no-preview") - var out bytes.Buffer - cmd.Stdin = os.Stdin - cmd.Stderr = stderr - cmd.Stdout = &out - - cmd.Env = append(os.Environ(), - fmt.Sprintf("FZF_DEFAULT_COMMAND=%s", op.SelfCmd), - fmt.Sprintf("%s=1", env.EnvForceColor)) - if err := cmd.Run(); err != nil { - if _, ok := err.(*exec.ExitError); !ok { - return err - } + out, err := cmdutil.InteractiveSearch(op.Picker, op.SelfCmd, stderr) + if err != nil { + return err } choice := strings.TrimSpace(out.String()) if choice == "" { diff --git a/cmd/kubens/help.go b/cmd/kubens/help.go index 1337b0bd..70b917fa 100644 --- a/cmd/kubens/help.go +++ b/cmd/kubens/help.go @@ -27,7 +27,7 @@ import ( // HelpOp describes printing help. type HelpOp struct{} -func (_ HelpOp) Run(stdout, _ io.Writer) error { +func (HelpOp) Run(stdout, _ io.Writer) error { return printUsage(stdout) } diff --git a/cmd/kubens/version.go b/cmd/kubens/version.go index 964f3345..ceec3986 100644 --- a/cmd/kubens/version.go +++ b/cmd/kubens/version.go @@ -14,7 +14,7 @@ var ( // VersionOps describes printing version string. type VersionOp struct{} -func (_ VersionOp) Run(stdout, _ io.Writer) error { +func (VersionOp) Run(stdout, _ io.Writer) error { _, err := fmt.Fprintf(stdout, "%s\n", version) return errors.Wrap(err, "write error") } diff --git a/internal/cmdutil/interactive.go b/internal/cmdutil/interactive.go index c562c121..f59b5a10 100644 --- a/internal/cmdutil/interactive.go +++ b/internal/cmdutil/interactive.go @@ -15,6 +15,9 @@ package cmdutil import ( + "bytes" + "fmt" + "io" "os" "os/exec" @@ -28,17 +31,59 @@ func isTerminal(fd *os.File) bool { return isatty.IsTerminal(fd.Fd()) } -// fzfInstalled determines if fzf(1) is in PATH. -func fzfInstalled() bool { - v, _ := exec.LookPath("fzf") - if v != "" { - return true +// pickerInstalled determines if picker(fzf or sk) is in PATH. +func pickerInstalled(p string) bool { + v, _ := exec.LookPath(p) + return v != "" +} + +// IsInteractiveMode determines the picker and whether we can do interactive choosing +// with it. +func IsInteractiveMode(stdout *os.File) (string, bool) { + p := fuzzyPicker() + if p == "fzf" { + v := os.Getenv(env.EnvFZFIgnore) + return p, v == "" && isTerminal(stdout) && pickerInstalled(p) + } + // if picker is sk + v := os.Getenv(env.EnvSKIgnore) + return p, v == "" && isTerminal(stdout) && pickerInstalled(p) +} + +// fuzzyPicker picks up picker (fzf or sk) from env `PICKER`. If EnvPicker is not +// set or has value other than sk then it by default picks fzf. +func fuzzyPicker() string { + p := os.Getenv(env.EnvPicker) + if p == "sk" { + return p } - return false + // for now it only supports fzf and sk. + return "fzf" } -// IsInteractiveMode determines if we can do choosing with fzf. -func IsInteractiveMode(stdout *os.File) bool { - v := os.Getenv(env.EnvFZFIgnore) - return v == "" && isTerminal(stdout) && fzfInstalled() +// InteractiveSearch launches fuzzy search either (fzf or sk) basis the picker. +func InteractiveSearch(picker, selfCmd string, stderr io.Writer) (bytes.Buffer, error) { + + var defaultCmd string + if picker == "fzf" { + defaultCmd = "FZF_DEFAULT_COMMAND" + } else { + defaultCmd = "SKIM_DEFAULT_COMMAND" + } + + cmd := exec.Command(picker, "--ansi") + cmd.Env = append(os.Environ(), + fmt.Sprintf("%s=%s", defaultCmd, selfCmd), + fmt.Sprintf("%s=1", env.EnvForceColor)) + var out bytes.Buffer + cmd.Stdin = os.Stdin + cmd.Stderr = stderr + cmd.Stdout = &out + + if err := cmd.Run(); err != nil { + if _, ok := err.(*exec.ExitError); !ok { + return out, err + } + } + return out, nil } diff --git a/internal/cmdutil/interactive_test.go b/internal/cmdutil/interactive_test.go new file mode 100644 index 00000000..ace10540 --- /dev/null +++ b/internal/cmdutil/interactive_test.go @@ -0,0 +1,54 @@ +package cmdutil + +import ( + "testing" + + "github.com/ahmetb/kubectx/internal/testutil" +) + +func Test_fuzzyPicker(t *testing.T) { + type env struct{ k, v string } + + cases := []struct { + name string + envs []env + want string + }{ + { + name: "PICKER is fzf", + envs: []env{ + {"PICKER", "fzf"}, + }, + want: "fzf", + }, { + name: "PICKER is sk", + envs: []env{ + {"PICKER", "sk"}, + }, + want: "sk", + }, { + name: "PICKER is not set", + envs: []env{}, + want: "fzf", + }, { + name: "PICKER is other than fzf and sk", + envs: []env{{"PICKER", "other-fuzzer"}}, + want: "fzf", + }, + } + for _, c := range cases { + t.Run(c.name, func(tt *testing.T) { + var unsets []func() + for _, e := range c.envs { + unsets = append(unsets, testutil.WithEnvVar(e.k, e.v)) + } + got := fuzzyPicker() + if got != c.want { + t.Errorf("want: %s, got: %s", c.want, got) + } + for _, u := range unsets { + u() + } + }) + } +} diff --git a/internal/env/constants.go b/internal/env/constants.go index e6a2a923..66d20006 100644 --- a/internal/env/constants.go +++ b/internal/env/constants.go @@ -29,4 +29,12 @@ const ( // EnvDebug describes the internal environment variable for more verbose logging. EnvDebug = `DEBUG` + + // EnvPicker describes the environment variable for fuzzy support, It can value + // fzf or sk. If this is not set then fzf is taken as default picker. + EnvPicker = `PICKER` + + // EnvSKIgnore describes the environment variable to disable interactive context + // selection when skim is installed + EnvSKIgnore = `KUBECTX_IGNORE_SK` )