Customize the format of fzf's lines with your own template (#36)

pull/38/head
Mickaël Menu 3 years ago committed by GitHub
parent fa8226b263
commit 83b14ca827
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -2,7 +2,20 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
<!--## Unreleased--> ## Unreleased
### Added
* Customize the format of `fzf`'s lines [with your own template](docs/tool-fzf.md).
```toml
[tool]
fzf-line = "{{style 'green' path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
```
### Fixed
* Creating a new note from `fzf` in a directory containing spaces.
## 0.4.0 ## 0.4.0

@ -23,3 +23,38 @@ Or, if you prefer to preview more metadata, you can use a nested `zk` command.
[tool] [tool]
fzf-preview = "zk list --quiet --format full --limit 1 {-1}" fzf-preview = "zk list --quiet --format full --limit 1 {-1}"
``` ```
## Line format
With the `fzf-line` setting property, you can provide your own [template](template.md) to customize the format of each `fzf` line. The lines are used by `fzf` for the fuzzy matching, so if you want to search in the full note content, do not forget to add `{{body}}` in your custom template.
The default line template is `{{style "title" title-or-path}} {{style "understate" body}}`.
Here's an example using different colors and showing the list of tags as #hashtags:
```toml
[tool]
fzf-line = "{{style 'blue' rel-path}}{{#each tags}} #{{this}}{{/each}} {{style 'black' body}}"
```
### Template context
The following variables are available in the line template.
| Variable | Type | Description |
|-----------------|----------|--------------------------------------------------------------------|
| `path` | string | File path to the note, relative to the notebook root |
| `abs-path` | string | Absolute file path to the note |
| `rel-path` | string | File path to the note, relative to the current directory |
| `title` | string | Note title |
| `title-or-path` | string | Note title or path if empty |
| `body` | string | All of the note content, minus the heading |
| `raw-content` | string | The full raw content of the note file |
| `word-count` | int | Number of words in the note |
| `tags` | [string] | List of tags found in the note |
| `metadata` | map | YAML frontmatter metadata, e.g. `metadata.description`<sup>1</sup> |
| `created` | date | Date of creation of the note |
| `modified` | date | Last date of modification of the note |
| `checksum` | string | SHA-256 checksum of the note file |
1. YAML keys are normalized to lower case.

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/mickael-menu/zk/internal/adapter/term" "github.com/mickael-menu/zk/internal/adapter/term"
"github.com/mickael-menu/zk/internal/core" "github.com/mickael-menu/zk/internal/core"
@ -13,9 +14,10 @@ import (
// NoteFilter uses fzf to filter interactively a set of notes. // NoteFilter uses fzf to filter interactively a set of notes.
type NoteFilter struct { type NoteFilter struct {
opts NoteFilterOpts opts NoteFilterOpts
fs core.FileStorage fs core.FileStorage
terminal *term.Terminal terminal *term.Terminal
templateLoader core.TemplateLoader
} }
// NoteFilterOpts holds the configuration for the fzf notes filtering. // NoteFilterOpts holds the configuration for the fzf notes filtering.
@ -28,6 +30,8 @@ type NoteFilterOpts struct {
Interactive bool Interactive bool
// Indicates whether fzf is opened for every query, even if empty. // Indicates whether fzf is opened for every query, even if empty.
AlwaysFilter bool 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. // Preview command to run when selecting a note.
PreviewCmd opt.String PreviewCmd opt.String
// When non null, a "create new note from query" binding will be added to // When non null, a "create new note from query" binding will be added to
@ -37,11 +41,12 @@ type NoteFilterOpts struct {
NotebookDir string NotebookDir string
} }
func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal) *NoteFilter { func NewNoteFilter(opts NoteFilterOpts, fs core.FileStorage, terminal *term.Terminal, templateLoader core.TemplateLoader) *NoteFilter {
return &NoteFilter{ return &NoteFilter{
opts: opts, opts: opts,
fs: fs, fs: fs,
terminal: terminal, terminal: terminal,
templateLoader: templateLoader,
} }
} }
@ -55,9 +60,19 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
return notes, nil return notes, nil
} }
lineTemplate, err := f.templateLoader.LoadTemplate(f.opts.LineTemplate.OrString(defaultLineTemplate).String())
if err != nil {
return selectedNotes, err
}
for _, note := range notes { for _, note := range notes {
absPaths = append(absPaths, filepath.Join(f.opts.NotebookDir, note.Path)) absPath := filepath.Join(f.opts.NotebookDir, note.Path)
relPaths = append(relPaths, 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() zkBin, err := os.Executable()
@ -76,7 +91,7 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
bindings = append(bindings, Binding{ bindings = append(bindings, Binding{
Keys: "Ctrl-N", Keys: "Ctrl-N",
Description: "create a note with the query as title" + suffix, 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), Action: fmt.Sprintf(`abort+execute("%s" new "%s" --title {q} < /dev/tty > /dev/tty)`, zkBin, dir.Path),
}) })
} }
@ -92,15 +107,34 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
} }
for i, note := range notes { for i, note := range notes {
title := note.Title context := lineRenderContext{
if title == "" { Path: note.Path,
title = relPaths[i] 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,
} }
fzf.Add([]string{ if context.TitleOrPath == "" {
f.terminal.MustStyle(title, core.StyleYellow), context.TitleOrPath = note.Path
f.terminal.MustStyle(stringsutil.JoinLines(note.Body), core.StyleUnderstate), }
f.terminal.MustStyle(absPaths[i], core.StyleUnderstate),
}) 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() selection, err := fzf.Selection()
@ -119,3 +153,21 @@ func (f *NoteFilter) Apply(notes []core.ContextualNote) ([]core.ContextualNote,
return selectedNotes, nil return selectedNotes, nil
} }
var defaultLineTemplate = `{{style "title" title-or-path}} {{style "understate" body}}`
type lineRenderContext struct {
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
}

@ -36,7 +36,6 @@ func (cmd *Edit) Run(container *cli.Container) error {
filter := container.NewNoteFilter(fzf.NoteFilterOpts{ filter := container.NewNoteFilter(fzf.NoteFilterOpts{
Interactive: cmd.Interactive, Interactive: cmd.Interactive,
AlwaysFilter: true, AlwaysFilter: true,
PreviewCmd: container.Config.Tool.FzfPreview,
NewNoteDir: cmd.newNoteDir(notebook), NewNoteDir: cmd.newNoteDir(notebook),
NotebookDir: notebook.Path, NotebookDir: notebook.Path,
}) })

@ -50,7 +50,6 @@ func (cmd *List) Run(container *cli.Container) error {
filter := container.NewNoteFilter(fzf.NoteFilterOpts{ filter := container.NewNoteFilter(fzf.NoteFilterOpts{
Interactive: cmd.Interactive, Interactive: cmd.Interactive,
AlwaysFilter: false, AlwaysFilter: false,
PreviewCmd: container.Config.Tool.FzfPreview,
NotebookDir: notebook.Path, NotebookDir: notebook.Path,
}) })

@ -33,6 +33,7 @@ type Container struct {
Logger *util.ProxyLogger Logger *util.ProxyLogger
Terminal *term.Terminal Terminal *term.Terminal
FS *fs.FileStorage FS *fs.FileStorage
TemplateLoader core.TemplateLoader
WorkingDir string WorkingDir string
Notebooks *core.NotebookStore Notebooks *core.NotebookStore
currentNotebook *core.Notebook currentNotebook *core.Notebook
@ -49,6 +50,13 @@ func NewContainer(version string) (*Container, error) {
config := core.NewDefaultConfig() config := core.NewDefaultConfig()
handlebars.Init(term.SupportsUTF8(), logger) handlebars.Init(term.SupportsUTF8(), logger)
// Template loader used for embedded templates (e.g. default config, fzf
// line, etc.).
templateLoader := handlebars.NewLoader(handlebars.LoaderOpts{
LookupPaths: []string{},
Styler: styler,
})
templateLoader.RegisterHelper("style", hbhelpers.NewStyleHelper(styler, logger))
// Load global user config // Load global user config
configPath, err := locateGlobalConfig() configPath, err := locateGlobalConfig()
@ -63,21 +71,15 @@ func NewContainer(version string) (*Container, error) {
} }
return &Container{ return &Container{
Version: version, Version: version,
Config: config, Config: config,
Logger: logger, Logger: logger,
Terminal: term, Terminal: term,
FS: fs, FS: fs,
TemplateLoader: templateLoader,
Notebooks: core.NewNotebookStore(config, core.NotebookStorePorts{ Notebooks: core.NewNotebookStore(config, core.NotebookStorePorts{
FS: fs, FS: fs,
TemplateLoaderFactory: func(language string) (core.TemplateLoader, error) { TemplateLoader: templateLoader,
loader := handlebars.NewLoader(handlebars.LoaderOpts{
LookupPaths: []string{},
Styler: styler,
})
return loader, nil
},
NotebookFactory: func(path string, config core.Config) (*core.Notebook, error) { NotebookFactory: func(path string, config core.Config) (*core.Notebook, error) {
dbPath := filepath.Join(path, ".zk/notebook.db") dbPath := filepath.Join(path, ".zk/notebook.db")
db, err := sqlite.Open(dbPath) db, err := sqlite.Open(dbPath)
@ -192,7 +194,9 @@ func (c *Container) CurrentNotebook() (*core.Notebook, error) {
} }
func (c *Container) NewNoteFilter(opts fzf.NoteFilterOpts) *fzf.NoteFilter { func (c *Container) NewNoteFilter(opts fzf.NoteFilterOpts) *fzf.NoteFilter {
return fzf.NewNoteFilter(opts, c.FS, c.Terminal) opts.PreviewCmd = c.Config.Tool.FzfPreview
opts.LineTemplate = c.Config.Tool.FzfLine
return fzf.NewNoteFilter(opts, c.FS, c.Terminal, c.TemplateLoader)
} }
func (c *Container) NewNoteEditor(notebook *core.Notebook) (*editor.Editor, error) { func (c *Container) NewNoteEditor(notebook *core.Notebook) (*editor.Editor, error) {

@ -121,6 +121,7 @@ type ToolConfig struct {
Editor opt.String Editor opt.String
Pager opt.String Pager opt.String
FzfPreview opt.String FzfPreview opt.String
FzfLine opt.String
} }
// NoteConfig holds the user configuration used when generating new notes. // NoteConfig holds the user configuration used when generating new notes.
@ -270,6 +271,9 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro
if tool.FzfPreview != nil { if tool.FzfPreview != nil {
config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview) config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview)
} }
if tool.FzfLine != nil {
config.Tool.FzfLine = opt.NewNotEmptyString(*tool.FzfLine)
}
// Filters // Filters
if tomlConf.Filters != nil { if tomlConf.Filters != nil {
@ -380,6 +384,7 @@ type tomlToolConfig struct {
Editor *string Editor *string
Pager *string Pager *string
FzfPreview *string `toml:"fzf-preview"` FzfPreview *string `toml:"fzf-preview"`
FzfLine *string `toml:"fzf-line"`
} }
func charsetFromString(charset string) Charset { func charsetFromString(charset string) Charset {

@ -41,6 +41,7 @@ func TestParseDefaultConfig(t *testing.T) {
Editor: opt.NullString, Editor: opt.NullString,
Pager: opt.NullString, Pager: opt.NullString,
FzfPreview: opt.NullString, FzfPreview: opt.NullString,
FzfLine: opt.NullString,
}, },
Filters: make(map[string]string), Filters: make(map[string]string),
Aliases: make(map[string]string), Aliases: make(map[string]string),
@ -79,6 +80,7 @@ func TestParseComplete(t *testing.T) {
editor = "vim" editor = "vim"
pager = "less" pager = "less"
fzf-preview = "bat {1}" fzf-preview = "bat {1}"
fzf-line = "{{title}}"
[extra] [extra]
hello = "world" hello = "world"
@ -203,6 +205,7 @@ func TestParseComplete(t *testing.T) {
Editor: opt.NewString("vim"), Editor: opt.NewString("vim"),
Pager: opt.NewString("less"), Pager: opt.NewString("less"),
FzfPreview: opt.NewString("bat {1}"), FzfPreview: opt.NewString("bat {1}"),
FzfLine: opt.NewString("{{title}}"),
}, },
Filters: map[string]string{ Filters: map[string]string{
"recents": "--created-after '2 weeks ago'", "recents": "--created-after '2 weeks ago'",

@ -9,30 +9,30 @@ import (
// NotebookStore retrieves or creates new notebooks. // NotebookStore retrieves or creates new notebooks.
type NotebookStore struct { type NotebookStore struct {
config Config config Config
notebookFactory NotebookFactory notebookFactory NotebookFactory
templateLoaderFactory TemplateLoaderFactory templateLoader TemplateLoader
fs FileStorage fs FileStorage
// Cached opened notebooks. // Cached opened notebooks.
notebooks map[string]*Notebook notebooks map[string]*Notebook
} }
type NotebookStorePorts struct { type NotebookStorePorts struct {
NotebookFactory NotebookFactory NotebookFactory NotebookFactory
TemplateLoaderFactory TemplateLoaderFactory TemplateLoader TemplateLoader
FS FileStorage FS FileStorage
} }
// NewNotebookStore creates a new NotebookStore instance using the given // NewNotebookStore creates a new NotebookStore instance using the given
// options and port implementations. // options and port implementations.
func NewNotebookStore(config Config, ports NotebookStorePorts) *NotebookStore { func NewNotebookStore(config Config, ports NotebookStorePorts) *NotebookStore {
return &NotebookStore{ return &NotebookStore{
config: config, config: config,
notebookFactory: ports.NotebookFactory, notebookFactory: ports.NotebookFactory,
templateLoaderFactory: ports.TemplateLoaderFactory, templateLoader: ports.TemplateLoader,
fs: ports.FS, fs: ports.FS,
notebooks: map[string]*Notebook{}, notebooks: map[string]*Notebook{},
} }
} }
@ -160,11 +160,7 @@ func (ns *NotebookStore) locateNotebook(path string) (string, error) {
} }
func (ns *NotebookStore) generateConfig(options InitOpts) (string, error) { func (ns *NotebookStore) generateConfig(options InitOpts) (string, error) {
loader, err := ns.templateLoaderFactory("en") template, err := ns.templateLoader.LoadTemplate(defaultConfig)
if err != nil {
return "", err
}
template, err := loader.LoadTemplate(defaultConfig)
if err != nil { if err != nil {
return "", err return "", err
} }

Loading…
Cancel
Save