You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
zk/internal/adapter/fzf/note_filter.go

178 lines
4.9 KiB
Go

package fzf
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/mickael-menu/zk/internal/adapter/term"
"github.com/mickael-menu/zk/internal/core"
"github.com/mickael-menu/zk/internal/util/opt"
stringsutil "github.com/mickael-menu/zk/internal/util/strings"
)
// NoteFilter uses fzf to filter interactively a set of notes.
type NoteFilter struct {
opts NoteFilterOpts
fs core.FileStorage
terminal *term.Terminal
templateLoader core.TemplateLoader
}
// NoteFilterOpts holds the configuration for the fzf notes filtering.
//
// The absolute path to the notebook (NotebookDir) and the working directory
// (WorkingDir) are used to make the path of each note relative to the working
// directory.
type NoteFilterOpts struct {
// Indicates whether the filtering is interactive. If not, fzf is bypassed.
Interactive bool
// Indicates whether fzf is opened for every query, even if empty.
AlwaysFilter bool
// Format for a single line, taken from the config `fzf-line` property.
LineTemplate opt.String
// Preview command to run when selecting a note.
PreviewCmd opt.String
// When non null, a "create new note from query" binding will be added to
// fzf to create a note in this directory.
NewNoteDir *core.Dir
// Absolute path to the notebook.
NotebookDir string
}
func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal, templateLoader core.TemplateLoader) *NoteFilter {
return &NoteFilter{
opts: opts,
fs: fs,
terminal: terminal,
templateLoader: templateLoader,
}
}
// Apply filters the given notes with fzf.
func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote, error) {
selectedNotes := make([]core.ContextualNote, 0)
relPaths := []string{}
absPaths := []string{}
if !f.opts.Interactive || !f.terminal.IsInteractive() || (!f.opts.AlwaysFilter && len(notes) == 0) {
return notes, nil
}
lineTemplate, err := f.templateLoader.LoadTemplate(f.opts.LineTemplate.OrString(defaultLineTemplate).String())
if err != nil {
return selectedNotes, err
}
for _, note := range notes {
absPath := filepath.Join(f.opts.NotebookDir, note.Path)
absPaths = append(absPaths, absPath)
if relPath, err := f.fs.Rel(absPath); err == nil {
relPaths = append(relPaths, relPath)
} else {
relPaths = append(relPaths, note.Path)
}
}
zkBin, err := os.Executable()
if err != nil {
return selectedNotes, err
}
bindings := []Binding{}
if dir := f.opts.NewNoteDir; dir != nil {
suffix := ""
if dir.Name != "" {
suffix = " in " + dir.Name + "/"
}
bindings = append(bindings, Binding{
Keys: "Ctrl-N",
Description: "create a note with the query as title" + suffix,
Action: fmt.Sprintf(`abort+execute("%s" new "%s" --title {q} < /dev/tty > /dev/tty)`, zkBin, dir.Path),
})
}
previewCmd := f.opts.PreviewCmd.OrString("cat {-1}").Unwrap()
fzf, err := New(Opts{
PreviewCmd: opt.NewNotEmptyString(previewCmd),
Padding: 2,
Bindings: bindings,
})
if err != nil {
return selectedNotes, err
}
for i, note := range notes {
context := lineRenderContext{
Filename: note.Filename(),
FilenameStem: note.FilenameStem(),
Path: note.Path,
AbsPath: absPaths[i],
RelPath: relPaths[i],
Title: note.Title,
TitleOrPath: note.Title,
Body: stringsutil.JoinLines(note.Body),
RawContent: stringsutil.JoinLines(note.RawContent),
WordCount: note.WordCount,
Tags: note.Tags,
Metadata: note.Metadata,
Created: note.Created,
Modified: note.Modified,
Checksum: note.Checksum,
}
if context.TitleOrPath == "" {
context.TitleOrPath = note.Path
}
line, err := lineTemplate.Render(context)
if err != nil {
return selectedNotes, err
}
// The absolute path is appended at the end of the line to be used in
// the preview command.
absPathField := f.terminal.MustStyle(context.AbsPath, core.StyleUnderstate)
fzf.Add([]string{line, absPathField})
}
selection, err := fzf.Selection()
if err != nil {
return selectedNotes, err
}
for _, s := range selection {
path := s[len(s)-1]
for i, m := range notes {
if absPaths[i] == path {
selectedNotes = append(selectedNotes, m)
}
}
}
return selectedNotes, nil
}
var defaultLineTemplate = `{{style "title" title-or-path}} {{style "understate" body}}`
type lineRenderContext struct {
Filename string
FilenameStem string `handlebars:"filename-stem"`
Path string
AbsPath string `handlebars:"abs-path"`
RelPath string `handlebars:"rel-path"`
Title string
TitleOrPath string `handlebars:"title-or-path"`
Body string
RawContent string `handlebars:"raw-content"`
WordCount int `handlebars:"word-count"`
Tags []string
Metadata map[string]interface{}
Created time.Time
Modified time.Time
Checksum string
}