mirror of https://github.com/mickael-menu/zk
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.
178 lines
4.9 KiB
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
|
|
}
|