Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding skim support #312

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
10 changes: 6 additions & 4 deletions cmd/kubectx/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
}
Expand Down
43 changes: 10 additions & 33 deletions cmd/kubectx/fzf.go → cmd/kubectx/fuzzy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 == "" {
Expand Down Expand Up @@ -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")
Expand Down
5 changes: 3 additions & 2 deletions cmd/kubens/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
}
Expand Down
23 changes: 4 additions & 19 deletions cmd/kubens/fzf.go → cmd/kubens/fuzzy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 == "" {
Expand Down
2 changes: 1 addition & 1 deletion cmd/kubens/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/kubens/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
65 changes: 55 additions & 10 deletions internal/cmdutil/interactive.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
package cmdutil

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"

Expand All @@ -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
}
54 changes: 54 additions & 0 deletions internal/cmdutil/interactive_test.go
Original file line number Diff line number Diff line change
@@ -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()
}
})
}
}
8 changes: 8 additions & 0 deletions internal/env/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this is not set then fzf is taken as default picker.

My expectation of this feature was that people can set this to anything and we can feature-proof the tool. e.g. imagine a tool named abc is developed in the future, I should be able to say PICKER=abc --opt1 -opt2.

I think this is how more common env vars like EDITOR or PAGER in posix systems work. The caller has no information about the program it's calling other than it knows how to pass it the information (either via xargs or via stdin).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this way, we can retain fzf logic and have an override via PICKER. The source code does not need to know about sk at all.

EnvPicker = `PICKER`

// EnvSKIgnore describes the environment variable to disable interactive context
// selection when skim is installed
EnvSKIgnore = `KUBECTX_IGNORE_SK`
)