Refactor fzf integration

pull/6/head
Mickaël Menu 3 years ago
parent fe6b062309
commit 6c95ff43c1
No known key found for this signature in database
GPG Key ID: 53D73664CD359895

@ -1,12 +1,11 @@
package fzf
import (
"fmt"
"io"
"strings"
"os"
"github.com/mickael-menu/zk/core/note"
"github.com/mickael-menu/zk/core/style"
"github.com/mickael-menu/zk/util/opt"
stringsutil "github.com/mickael-menu/zk/util/strings"
)
@ -24,28 +23,41 @@ func (f *NoteFinder) Find(opts note.FinderOpts) ([]note.Match, error) {
isInteractive, opts := popInteractiveFilter(opts)
matches, err := f.finder.Find(opts)
if !isInteractive || err != nil {
if !isInteractive || err != nil || len(matches) == 0 {
return matches, err
}
selectedMatches := make([]note.Match, 0)
selection, err := withFzf(func(fzf io.Writer) error {
for _, match := range matches {
fmt.Fprintf(fzf, "%v\x01 %v %v\n",
match.Path,
f.styler.MustStyle(match.Title, style.Rule("yellow")),
f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.Rule("faint")),
)
}
return nil
zkBin, err := os.Executable()
if err != nil {
return selectedMatches, err
}
fzf, err := New(Opts{
// PreviewCmd: opt.NewString("bat -p --theme Nord --color always {1}"),
PreviewCmd: opt.NewString(zkBin + " list -f {{raw-content}} {1}"),
Padding: 2,
})
if err != nil {
return selectedMatches, err
}
for _, match := range matches {
fzf.Add([]string{
match.Path,
f.styler.MustStyle(match.Title, style.Rule("yellow")),
f.styler.MustStyle(stringsutil.JoinLines(match.Body), style.Rule("faint")),
})
}
selection, err := fzf.Selection()
if err != nil {
return selectedMatches, err
}
for _, s := range selection {
path := strings.Split(s, "\x01")[0]
path := s[0]
for _, m := range matches {
if m.Path == path {
selectedMatches = append(selectedMatches, m)

@ -1,57 +1,168 @@
package fzf
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"github.com/mickael-menu/zk/util/strings"
"github.com/mickael-menu/zk/util/errors"
"github.com/mickael-menu/zk/util/opt"
stringsutil "github.com/mickael-menu/zk/util/strings"
)
func withFzf(callback func(fzf io.Writer) error) ([]string, error) {
zkBin, err := os.Executable()
if err != nil {
return []string{}, err
// fzf exit codes
var (
exitInterrupted = 130
exitNoMatch = 1
)
// Opts holds the options used to run fzf.
type Opts struct {
// Preview command executed by fzf when hovering a line.
PreviewCmd opt.String
// Amount of space between two non-empty fields.
Padding int
// Delimiter used by fzf between fields.
Delimiter string
}
// Fzf filters a set of fields using fzf.
//
// After adding all the fields with Add, use Selection to get the filtered
// results.
type Fzf struct {
opts Opts
// Fields selection or error result.
err error
selection [][]string
done chan bool
cmd *exec.Cmd
pipe io.WriteCloser
closeOnce sync.Once
}
// New runs a fzf instance.
//
// To show a preview of each line, provide a previewCmd which will be executed
// by fzf.
func New(opts Opts) (*Fzf, error) {
// \x01 is a convenient delimiter because not visible in the output and
// most likely not part of the fields themselves.
if opts.Delimiter == "" {
opts.Delimiter = "\x01"
}
cmd := exec.Command(
"fzf",
"--delimiter", "\x01",
args := []string{
"--delimiter", opts.Delimiter,
"--tiebreak", "begin",
"--ansi",
"--exact",
"--tabstop", "4",
"--height", "100%",
"--layout", "reverse",
// FIXME: Use it to create a new note? Like notational velocity
// "--print-query",
// Make sure the path and titles are always visible
"--no-hscroll",
"--tabstop", "4",
// Don't highlight search terms
"--color", "hl:-1,hl+:-1",
// "--preview", `bat -p --theme Nord --color always {1}`,
"--preview", zkBin+" list -f {{raw-content}} {1}",
"--preview-window", "noborder:wrap",
)
// "--preview-window", "noborder:wrap",
}
if !opts.PreviewCmd.IsNull() {
args = append(args, "--preview", opts.PreviewCmd.String())
}
cmd := exec.Command("fzf", args...)
cmd.Stderr = os.Stderr
w, err := cmd.StdinPipe()
pipe, err := cmd.StdinPipe()
if err != nil {
return []string{}, err
return nil, err
}
done := make(chan bool)
f := Fzf{
opts: opts,
cmd: cmd,
pipe: pipe,
closeOnce: sync.Once{},
done: done,
selection: make([][]string, 0),
}
var callbackErr error
go func() {
callbackErr = callback(w)
w.Close()
defer func() {
close(done)
f.close()
}()
output, err := cmd.Output()
if err != nil {
if err, ok := err.(*exec.ExitError); ok &&
err.ExitCode() != exitInterrupted &&
err.ExitCode() != exitNoMatch {
f.err = errors.Wrap(err, "failed to filter interactively the output with fzf, try again without --interactive or make sure you have a working fzf installation")
}
} else {
f.parseSelection(string(output))
}
}()
output, err := cmd.Output()
if callbackErr != nil {
return []string{}, callbackErr
return &f, nil
}
// parseSelection extracts the fields from fzf's output.
func (f *Fzf) parseSelection(output string) {
f.selection = make([][]string, 0)
lines := stringsutil.SplitLines(string(output))
for _, line := range lines {
fields := strings.Split(line, f.opts.Delimiter)
// Trim padding
for i, field := range fields {
fields[i] = strings.TrimSpace(field)
}
f.selection = append(f.selection, fields)
}
if err != nil {
return []string{}, err
}
// Add appends a new line of fields to fzf input.
func (f *Fzf) Add(fields []string) error {
line := ""
for i, field := range fields {
if i > 0 {
line += f.opts.Delimiter
if field != "" && f.opts.Padding > 0 {
line += strings.Repeat(" ", f.opts.Padding)
}
}
line += field
}
if line == "" {
return nil
}
_, err := fmt.Fprintln(f.pipe, line)
return err
}
// Selection returns the field lines selected by the user through fzf.
func (f *Fzf) Selection() ([][]string, error) {
f.close()
<-f.done
return f.selection, f.err
}
return strings.SplitLines(string(output)), nil
func (f *Fzf) close() error {
var err error
f.closeOnce.Do(func() {
err = f.pipe.Close()
})
return err
}

@ -15,6 +15,9 @@ func NewStyler() *Styler {
}
func (s *Styler) Style(text string, rules ...style.Rule) (string, error) {
if text == "" {
return text, nil
}
attrs, err := s.attributes(expandThemeAliases(rules))
if err != nil {
return "", err

@ -36,6 +36,12 @@ func TestStyleUnknownRule(t *testing.T) {
assert.Err(t, err, "unknown styling rule: unknown")
}
func TestStyleEmptyString(t *testing.T) {
res, err := createStyler().Style("", style.Rule("bold"))
assert.Nil(t, err)
assert.Equal(t, res, "")
}
func TestStyleAllRules(t *testing.T) {
styler := createStyler()
test := func(rule string, expected string) {

@ -80,10 +80,7 @@ func (cmd *List) Run(container *Container) error {
return err
}
_, err = fmt.Fprintf(out, "%v\n", ft)
if err != nil {
return err
}
fmt.Fprintf(out, "%v\n", ft)
}
return nil
@ -91,7 +88,7 @@ func (cmd *List) Run(container *Container) error {
}
if err == nil {
fmt.Printf("\nFound %d %s\n", count, strings.Pluralize("result", count))
fmt.Printf("\nFound %d %s\n", count, strings.Pluralize("note", count))
}
return err

@ -1,70 +0,0 @@
package note
import (
"fmt"
"io"
"os"
"os/exec"
"github.com/mickael-menu/zk/adapter/tty"
"github.com/mickael-menu/zk/core/style"
"github.com/mickael-menu/zk/util/strings"
)
func WithMatchFilter(callback func(func(Match) error)) ([]string, error) {
styler := tty.NewStyler()
return withFilter(func(w io.Writer) {
callback(func(m Match) error {
fmt.Fprintf(w, "%v\x01 %v %v\n",
m.Path,
styler.MustStyle(m.Title, style.Rule("yellow")),
styler.MustStyle(strings.JoinLines(m.Body), style.Rule("faint")),
)
return nil
})
})
}
func withFilter(callback func(w io.Writer)) ([]string, error) {
zkBin, err := os.Executable()
if err != nil {
return []string{}, err
}
cmd := exec.Command(
"fzf",
"--delimiter", "\x01",
"--tiebreak", "begin",
"--ansi",
"--exact",
"--height", "100%",
// FIXME: Use it to create a new note? Like notational velocity
// "--print-query",
// Make sure the path and titles are always visible
"--no-hscroll",
"--tabstop", "4",
// Don't highlight search terms
"--color", "hl:-1,hl+:-1",
// "--preview", `bat -p --theme Nord --color always {1}`,
"--preview", zkBin+" list -f {{raw-content}} {1}",
"--preview-window", "noborder:wrap",
)
cmd.Stderr = os.Stderr
w, err := cmd.StdinPipe()
if err != nil {
return []string{}, err
}
go func() {
callback(w)
w.Close()
}()
output, err := cmd.Output()
if err != nil {
return []string{}, err
}
return strings.SplitLines(string(output)), nil
}
Loading…
Cancel
Save