mirror of
https://github.com/mickael-menu/zk
synced 2024-11-07 15:20:21 +00:00
Add named filters (#17)
This commit is contained in:
parent
695a3bc351
commit
fa66e78068
@ -18,6 +18,15 @@ All notable changes to this project will be documented in this file.
|
||||
* Find every note whose title is mentioned in the note you are working on with `--mentioned-by file.md`.
|
||||
* To refer to a note using several names, you can use the [YAML frontmatter key `aliases`](https://publish.obsidian.md/help/How+to/Add+aliases+to+note). For example the note titled "Artificial Intelligence" might have: `aliases: [AI, robot]`
|
||||
* To find only unlinked mentions, pair it with `--no-linked-by`, e.g. `--mentioned-by file.md --no-linked-by file.md`.
|
||||
* Declare [named filters](docs/config-filter.md) in the configuration file to reuse [note filtering options](docs/note-filtering.md) used frequently together, for example:
|
||||
```toml
|
||||
[filter]
|
||||
recents = "--sort created- --created-after 'last two weeks'"
|
||||
```
|
||||
```sh
|
||||
$ zk list recents --limit 10
|
||||
$ zk edit recents --interactive
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
|
@ -14,7 +14,7 @@
|
||||
* [Creating notes from templates](docs/note-creation.md)
|
||||
* [Advanced search and filtering capabilities](docs/note-filtering.md) including [tags](docs/tags.md), links and mentions
|
||||
* [Interactive browser](docs/tool-fzf), powered by `fzf`
|
||||
* [Git-style command aliases](docs/config-alias.md)
|
||||
* [Git-style command aliases](docs/config-alias.md) and [named filters](docs/config-filter.md)
|
||||
* [Made with automation in mind](docs/automation.md)
|
||||
* [Notebook housekeeping](docs/notebook-housekeeping.md)
|
||||
* [Future-proof, thanks to Markdown](docs/future-proof.md)
|
||||
|
@ -14,9 +14,7 @@ import (
|
||||
// Edit opens notes matching a set of criteria with the user editor.
|
||||
type Edit struct {
|
||||
Force bool `short:f help:"Do not confirm before editing many notes at the same time."`
|
||||
|
||||
Filtering
|
||||
Sorting
|
||||
}
|
||||
|
||||
func (cmd *Edit) Run(container *Container) error {
|
||||
@ -25,7 +23,7 @@ func (cmd *Edit) Run(container *Container) error {
|
||||
return err
|
||||
}
|
||||
|
||||
opts, err := NewFinderOpts(zk, cmd.Filtering, cmd.Sorting)
|
||||
opts, err := NewFinderOpts(zk, cmd.Filtering)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "incorrect criteria")
|
||||
}
|
||||
|
@ -1,12 +1,17 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/alecthomas/kong"
|
||||
"github.com/kballard/go-shellquote"
|
||||
"github.com/mickael-menu/zk/core/note"
|
||||
"github.com/mickael-menu/zk/core/zk"
|
||||
"github.com/mickael-menu/zk/util/errors"
|
||||
"github.com/mickael-menu/zk/util/opt"
|
||||
"github.com/mickael-menu/zk/util/strings"
|
||||
"github.com/tj/go-naturaldate"
|
||||
)
|
||||
|
||||
@ -19,14 +24,14 @@ type Filtering struct {
|
||||
Match string `group:filter short:m placeholder:QUERY help:"Terms to search for in the notes."`
|
||||
Exclude []string `group:filter short:x placeholder:PATH help:"Ignore notes matching the given path, including its descendants."`
|
||||
Tag []string `group:filter short:t help:"Find notes tagged with the given tags."`
|
||||
Mention []string `group:filter placeholder:PATH help:"Find notes mentioning the title of the given ones." xor:mention`
|
||||
MentionedBy []string `group:filter placeholder:PATH help:"Find notes whose title is mentioned in the given ones." xor:mention`
|
||||
LinkTo []string `group:filter short:l placeholder:PATH help:"Find notes which are linking to the given ones." xor:link`
|
||||
NoLinkTo []string `group:filter placeholder:PATH help:"Find notes which are not linking to the given notes." xor:link`
|
||||
LinkedBy []string `group:filter short:L placeholder:PATH help:"Find notes which are linked by the given ones." xor:link`
|
||||
NoLinkedBy []string `group:filter placeholder:PATH help:"Find notes which are not linked by the given ones." xor:link`
|
||||
Orphan bool `group:filter help:"Find notes which are not linked by any other note." xor:link`
|
||||
Related []string `group:filter placeholder:PATH help:"Find notes which might be related to the given ones." xor:link`
|
||||
Mention []string `group:filter placeholder:PATH help:"Find notes mentioning the title of the given ones."`
|
||||
MentionedBy []string `group:filter placeholder:PATH help:"Find notes whose title is mentioned in the given ones."`
|
||||
LinkTo []string `group:filter short:l placeholder:PATH help:"Find notes which are linking to the given ones."`
|
||||
NoLinkTo []string `group:filter placeholder:PATH help:"Find notes which are not linking to the given notes."`
|
||||
LinkedBy []string `group:filter short:L placeholder:PATH help:"Find notes which are linked by the given ones."`
|
||||
NoLinkedBy []string `group:filter placeholder:PATH help:"Find notes which are not linked by the given ones."`
|
||||
Orphan bool `group:filter help:"Find notes which are not linked by any other note."`
|
||||
Related []string `group:filter placeholder:PATH help:"Find notes which might be related to the given ones."`
|
||||
MaxDistance int `group:filter placeholder:COUNT help:"Maximum distance between two linked notes."`
|
||||
Recursive bool `group:filter short:r help:"Follow links recursively."`
|
||||
Created string `group:filter placeholder:DATE help:"Find notes created on the given date."`
|
||||
@ -35,15 +40,102 @@ type Filtering struct {
|
||||
Modified string `group:filter placeholder:DATE help:"Find notes modified on the given date."`
|
||||
ModifiedBefore string `group:filter placeholder:DATE help:"Find notes modified before the given date."`
|
||||
ModifiedAfter string `group:filter placeholder:DATE help:"Find notes modified after the given date."`
|
||||
}
|
||||
|
||||
// Sorting holds sorting options to order notes.
|
||||
type Sorting struct {
|
||||
Sort []string `group:sort short:s placeholder:TERM help:"Order the notes by the given criterion."`
|
||||
}
|
||||
|
||||
// ExpandNamedFilters expands recursively any named filter found in the Path field.
|
||||
func (f Filtering) ExpandNamedFilters(filters map[string]string, expandedFilters []string) (Filtering, error) {
|
||||
actualPaths := []string{}
|
||||
|
||||
for _, path := range f.Path {
|
||||
if filter, ok := filters[path]; ok && !strings.InList(expandedFilters, path) {
|
||||
wrap := errors.Wrapperf("failed to expand named filter `%v`", path)
|
||||
|
||||
var parsedFilter Filtering
|
||||
parser, err := kong.New(&parsedFilter)
|
||||
if err != nil {
|
||||
return f, wrap(err)
|
||||
}
|
||||
args, err := shellquote.Split(filter)
|
||||
if err != nil {
|
||||
return f, wrap(err)
|
||||
}
|
||||
_, err = parser.Parse(args)
|
||||
if err != nil {
|
||||
return f, wrap(err)
|
||||
}
|
||||
|
||||
// Expand recursively, but prevent infinite loops by registering
|
||||
// the current filter in the list of expanded filters.
|
||||
parsedFilter, err = parsedFilter.ExpandNamedFilters(filters, append(expandedFilters, path))
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
actualPaths = append(actualPaths, parsedFilter.Path...)
|
||||
f.Exclude = append(f.Exclude, parsedFilter.Exclude...)
|
||||
f.Tag = append(f.Tag, parsedFilter.Tag...)
|
||||
f.Mention = append(f.Mention, parsedFilter.Mention...)
|
||||
f.MentionedBy = append(f.MentionedBy, parsedFilter.MentionedBy...)
|
||||
f.LinkTo = append(f.LinkTo, parsedFilter.LinkTo...)
|
||||
f.NoLinkTo = append(f.NoLinkTo, parsedFilter.NoLinkTo...)
|
||||
f.LinkedBy = append(f.LinkedBy, parsedFilter.LinkedBy...)
|
||||
f.NoLinkedBy = append(f.NoLinkedBy, parsedFilter.NoLinkedBy...)
|
||||
f.Related = append(f.Related, parsedFilter.Related...)
|
||||
f.Sort = append(f.Sort, parsedFilter.Sort...)
|
||||
|
||||
f.Interactive = f.Interactive || parsedFilter.Interactive
|
||||
f.Orphan = f.Orphan || parsedFilter.Orphan
|
||||
f.Recursive = f.Recursive || parsedFilter.Recursive
|
||||
|
||||
if f.Limit == 0 {
|
||||
f.Limit = parsedFilter.Limit
|
||||
}
|
||||
if f.MaxDistance == 0 {
|
||||
f.MaxDistance = parsedFilter.MaxDistance
|
||||
}
|
||||
if f.Created == "" {
|
||||
f.Created = parsedFilter.Created
|
||||
}
|
||||
if f.CreatedBefore == "" {
|
||||
f.CreatedBefore = parsedFilter.CreatedBefore
|
||||
}
|
||||
if f.CreatedAfter == "" {
|
||||
f.CreatedAfter = parsedFilter.CreatedAfter
|
||||
}
|
||||
if f.Modified == "" {
|
||||
f.Modified = parsedFilter.Modified
|
||||
}
|
||||
if f.ModifiedBefore == "" {
|
||||
f.ModifiedBefore = parsedFilter.ModifiedBefore
|
||||
}
|
||||
if f.ModifiedAfter == "" {
|
||||
f.ModifiedAfter = parsedFilter.ModifiedAfter
|
||||
}
|
||||
|
||||
if f.Match == "" {
|
||||
f.Match = parsedFilter.Match
|
||||
} else if parsedFilter.Match != "" {
|
||||
f.Match = fmt.Sprintf("(%s) AND (%s)", f.Match, parsedFilter.Match)
|
||||
}
|
||||
|
||||
} else {
|
||||
actualPaths = append(actualPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
f.Path = actualPaths
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// NewFinderOpts creates an instance of note.FinderOpts from a set of user flags.
|
||||
func NewFinderOpts(zk *zk.Zk, filtering Filtering, sorting Sorting) (*note.FinderOpts, error) {
|
||||
func NewFinderOpts(zk *zk.Zk, filtering Filtering) (*note.FinderOpts, error) {
|
||||
filtering, err := filtering.ExpandNamedFilters(zk.Config.Filters, []string{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
opts := note.FinderOpts{}
|
||||
|
||||
opts.Match = opt.NewNotEmptyString(filtering.Match)
|
||||
@ -152,7 +244,7 @@ func NewFinderOpts(zk *zk.Zk, filtering Filtering, sorting Sorting) (*note.Finde
|
||||
|
||||
opts.Interactive = filtering.Interactive
|
||||
|
||||
sorters, err := note.SortersFromStrings(sorting.Sort)
|
||||
sorters, err := note.SortersFromStrings(filtering.Sort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
204
cmd/finder_opts_test.go
Normal file
204
cmd/finder_opts_test.go
Normal file
@ -0,0 +1,204 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mickael-menu/zk/util/test/assert"
|
||||
)
|
||||
|
||||
func TestExpandNamedFiltersNone(t *testing.T) {
|
||||
f := Filtering{
|
||||
Path: []string{"path1"},
|
||||
Limit: 10,
|
||||
Interactive: true,
|
||||
Match: "match query",
|
||||
Exclude: []string{"excl-path1", "excl-path2"},
|
||||
Tag: []string{"tag1", "tag2"},
|
||||
Mention: []string{"mention1", "mention2"},
|
||||
MentionedBy: []string{"note1", "note2"},
|
||||
LinkTo: []string{"link1", "link2"},
|
||||
NoLinkTo: []string{"link3", "link4"},
|
||||
LinkedBy: []string{"linked1", "linked2"},
|
||||
NoLinkedBy: []string{"linked3", "linked4"},
|
||||
Related: []string{"related1", "related2"},
|
||||
MaxDistance: 2,
|
||||
Created: "yesterday",
|
||||
CreatedBefore: "two days ago",
|
||||
CreatedAfter: "three days ago",
|
||||
Modified: "tomorrow",
|
||||
ModifiedBefore: "two days",
|
||||
ModifiedAfter: "three days",
|
||||
Sort: []string{"title", "created"},
|
||||
}
|
||||
|
||||
res, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"recents": "--created-after '2 weeks ago'",
|
||||
"journal": "log --sort created",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res, f)
|
||||
}
|
||||
|
||||
// ExpandNamedFilters: list options are concatenated.
|
||||
func TestExpandNamedFiltersJoinLists(t *testing.T) {
|
||||
f := Filtering{
|
||||
Path: []string{"path1", "f1", "f2"},
|
||||
Exclude: []string{"excl-path1", "excl-path2"},
|
||||
Tag: []string{"tag1", "tag2"},
|
||||
Mention: []string{"mention1", "mention2"},
|
||||
MentionedBy: []string{"note1", "note2"},
|
||||
LinkTo: []string{"link1", "link2"},
|
||||
NoLinkTo: []string{"link3", "link4"},
|
||||
LinkedBy: []string{"linked1", "linked2"},
|
||||
NoLinkedBy: []string{"linked3", "linked4"},
|
||||
Related: []string{"related1", "related2"},
|
||||
Sort: []string{"title", "created"},
|
||||
}
|
||||
|
||||
res, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "path2 --exclude excl-path3 -x excl-path4 --tag tag3 -t tag4 --mention mention3,mention4 --mentioned-by note3",
|
||||
"f2": "--link-to link5 --no-link-to link6 --linked-by linked5 --no-linked-by linked6 --related related3 --related related4 --sort random-",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res.Path, []string{"path1", "path2"})
|
||||
assert.Equal(t, res.Exclude, []string{"excl-path1", "excl-path2", "excl-path3", "excl-path4"})
|
||||
assert.Equal(t, res.Tag, []string{"tag1", "tag2", "tag3", "tag4"})
|
||||
assert.Equal(t, res.Mention, []string{"mention1", "mention2", "mention3", "mention4"})
|
||||
assert.Equal(t, res.MentionedBy, []string{"note1", "note2", "note3"})
|
||||
assert.Equal(t, res.LinkTo, []string{"link1", "link2", "link5"})
|
||||
assert.Equal(t, res.NoLinkTo, []string{"link3", "link4", "link6"})
|
||||
assert.Equal(t, res.LinkedBy, []string{"linked1", "linked2", "linked5"})
|
||||
assert.Equal(t, res.NoLinkedBy, []string{"linked3", "linked4", "linked6"})
|
||||
assert.Equal(t, res.Related, []string{"related1", "related2", "related3", "related4"})
|
||||
assert.Equal(t, res.Sort, []string{"title", "created", "random-"})
|
||||
}
|
||||
|
||||
// ExpandNamedFilters: boolean options are computed with disjunction.
|
||||
func TestExpandNamedFiltersJoinBools(t *testing.T) {
|
||||
f := Filtering{
|
||||
Path: []string{"path1", "f1", "f2"},
|
||||
}
|
||||
|
||||
res, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "--interactive --orphan",
|
||||
"f2": "--recursive",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, res.Interactive)
|
||||
assert.True(t, res.Orphan)
|
||||
assert.True(t, res.Recursive)
|
||||
}
|
||||
|
||||
// ExpandNamedFilters: non-zero integer and non-empty string options take precedence over named filters.
|
||||
func TestExpandNamedFiltersJoinLitterals(t *testing.T) {
|
||||
f1 := Filtering{Path: []string{"f1", "f2"}}
|
||||
res1, err := f1.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "--limit 42 --created 'yesterday' --created-before '2 days ago' --created-after '3 days ago'",
|
||||
"f2": "--max-distance 24 --modified 'tomorrow' --modified-before '2 days' --modified-after '3 days'",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res1.Limit, 42)
|
||||
assert.Equal(t, res1.MaxDistance, 24)
|
||||
assert.Equal(t, res1.Created, "yesterday")
|
||||
assert.Equal(t, res1.CreatedBefore, "2 days ago")
|
||||
assert.Equal(t, res1.CreatedAfter, "3 days ago")
|
||||
assert.Equal(t, res1.Modified, "tomorrow")
|
||||
assert.Equal(t, res1.ModifiedBefore, "2 days")
|
||||
assert.Equal(t, res1.ModifiedAfter, "3 days")
|
||||
|
||||
f2 := Filtering{
|
||||
Path: []string{"f1", "f2"},
|
||||
Limit: 10,
|
||||
MaxDistance: 20,
|
||||
Created: "last week",
|
||||
CreatedBefore: "two weeks ago",
|
||||
CreatedAfter: "three weeks ago",
|
||||
Modified: "next week",
|
||||
ModifiedBefore: "two weeks",
|
||||
ModifiedAfter: "three weeks",
|
||||
}
|
||||
res2, err := f2.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "--limit 42 --created 'yesterday' --created-before '2 days ago' --created-after '3 days ago'",
|
||||
"f2": "--max-distance 24 --modified 'tomorrow' --modified-before '2 days' --modified-after '3 days'",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res2.Limit, 10)
|
||||
assert.Equal(t, res2.MaxDistance, 20)
|
||||
assert.Equal(t, res2.Created, "last week")
|
||||
assert.Equal(t, res2.CreatedBefore, "two weeks ago")
|
||||
assert.Equal(t, res2.CreatedAfter, "three weeks ago")
|
||||
assert.Equal(t, res2.Modified, "next week")
|
||||
assert.Equal(t, res2.ModifiedBefore, "two weeks")
|
||||
assert.Equal(t, res2.ModifiedAfter, "three weeks")
|
||||
}
|
||||
|
||||
// ExpandNamedFilters: Match option predicates are cumulated with AND.
|
||||
func TestExpandNamedFiltersJoinMatch(t *testing.T) {
|
||||
f := Filtering{
|
||||
Path: []string{"f1", "f2"},
|
||||
Match: "(chocolate OR caramel)",
|
||||
}
|
||||
|
||||
res, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "--match banana",
|
||||
"f2": "--match apple",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res.Match, "(((chocolate OR caramel)) AND (banana)) AND (apple)")
|
||||
}
|
||||
|
||||
func TestExpandNamedFiltersExpandsRecursively(t *testing.T) {
|
||||
f := Filtering{
|
||||
Path: []string{"path1", "journal", "recents"},
|
||||
}
|
||||
|
||||
res, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"recents": "--created-after '2 weeks ago'",
|
||||
"journal": "journal sort-created",
|
||||
"sort-created": "--sort created",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, res.Path, []string{"path1", "journal"})
|
||||
assert.Equal(t, res.CreatedAfter, "2 weeks ago")
|
||||
assert.Equal(t, res.Sort, []string{"created"})
|
||||
}
|
||||
|
||||
func TestExpandNamedFiltersReportsParsingError(t *testing.T) {
|
||||
f := Filtering{Path: []string{"f1"}}
|
||||
|
||||
_, err := f.ExpandNamedFilters(
|
||||
map[string]string{
|
||||
"f1": "--test",
|
||||
},
|
||||
[]string{},
|
||||
)
|
||||
|
||||
assert.Err(t, err, "failed to expand named filter `f1`: unknown flag --test")
|
||||
}
|
@ -19,9 +19,7 @@ type List struct {
|
||||
Delimiter0 bool "group:format short:0 name:delimiter0 help:\"Print notes delimited by ASCII NUL characters. This is useful when used in conjunction with `xargs -0`.\""
|
||||
NoPager bool `group:format short:P help:"Do not pipe output into a pager."`
|
||||
Quiet bool `group:format short:q help:"Do not print the total number of notes found."`
|
||||
|
||||
Filtering
|
||||
Sorting
|
||||
}
|
||||
|
||||
func (cmd *List) Run(container *Container) error {
|
||||
@ -34,7 +32,7 @@ func (cmd *List) Run(container *Container) error {
|
||||
return err
|
||||
}
|
||||
|
||||
opts, err := NewFinderOpts(zk, cmd.Filtering, cmd.Sorting)
|
||||
opts, err := NewFinderOpts(zk, cmd.Filtering)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ type Config struct {
|
||||
Groups map[string]GroupConfig
|
||||
Format FormatConfig
|
||||
Tool ToolConfig
|
||||
Filters map[string]string
|
||||
Aliases map[string]string
|
||||
Extra map[string]string
|
||||
// Base directories for the relative template paths used in NoteConfig.
|
||||
@ -45,6 +46,7 @@ func NewDefaultConfig() Config {
|
||||
MultiwordTags: false,
|
||||
},
|
||||
},
|
||||
Filters: map[string]string{},
|
||||
Aliases: map[string]string{},
|
||||
Extra: map[string]string{},
|
||||
TemplatesDirs: []string{},
|
||||
@ -256,6 +258,13 @@ func ParseConfig(content []byte, path string, parentConfig Config) (Config, erro
|
||||
config.Tool.FzfPreview = opt.NewStringWithPtr(tool.FzfPreview)
|
||||
}
|
||||
|
||||
// Filters
|
||||
if tomlConf.Filters != nil {
|
||||
for k, v := range tomlConf.Filters {
|
||||
config.Filters[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Aliases
|
||||
if tomlConf.Aliases != nil {
|
||||
for k, v := range tomlConf.Aliases {
|
||||
@ -322,6 +331,7 @@ type tomlConfig struct {
|
||||
Format tomlFormatConfig
|
||||
Tool tomlToolConfig
|
||||
Extra map[string]string
|
||||
Filters map[string]string `toml:"filter"`
|
||||
Aliases map[string]string `toml:"alias"`
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ func TestParseDefaultConfig(t *testing.T) {
|
||||
Pager: opt.NullString,
|
||||
FzfPreview: opt.NullString,
|
||||
},
|
||||
Filters: make(map[string]string),
|
||||
Aliases: make(map[string]string),
|
||||
Extra: make(map[string]string),
|
||||
TemplatesDirs: []string{".zk/templates"},
|
||||
@ -81,6 +82,10 @@ func TestParseComplete(t *testing.T) {
|
||||
hello = "world"
|
||||
salut = "le monde"
|
||||
|
||||
[filter]
|
||||
recents = "--created-after '2 weeks ago'"
|
||||
journal = "journal --sort created"
|
||||
|
||||
[alias]
|
||||
ls = "zk list $@"
|
||||
ed = "zk edit $@"
|
||||
@ -194,6 +199,10 @@ func TestParseComplete(t *testing.T) {
|
||||
Pager: opt.NewString("less"),
|
||||
FzfPreview: opt.NewString("bat {1}"),
|
||||
},
|
||||
Filters: map[string]string{
|
||||
"recents": "--created-after '2 weeks ago'",
|
||||
"journal": "journal --sort created",
|
||||
},
|
||||
Aliases: map[string]string{
|
||||
"ls": "zk list $@",
|
||||
"ed": "zk edit $@",
|
||||
@ -298,6 +307,7 @@ func TestParseMergesGroupConfig(t *testing.T) {
|
||||
MultiwordTags: false,
|
||||
},
|
||||
},
|
||||
Filters: make(map[string]string),
|
||||
Aliases: make(map[string]string),
|
||||
Extra: map[string]string{
|
||||
"hello": "world",
|
||||
|
@ -118,6 +118,18 @@ hashtags = true
|
||||
#fzf-preview = "bat -p --color always {-1}"
|
||||
|
||||
|
||||
# NAMED FILTERS
|
||||
#
|
||||
# A named filter is a set of note filtering options used frequently together.
|
||||
#
|
||||
[filter]
|
||||
|
||||
# Matches the notes created the last two weeks. For example:
|
||||
# $ zk list recents --limit 15
|
||||
# $ zk edit recents --interactive
|
||||
#recents = "--sort created- --created-after 'last two weeks'"
|
||||
|
||||
|
||||
# COMMAND ALIASES
|
||||
#
|
||||
# Aliases are user commands called with ` + "`" + `zk <alias> [<flags>] [<args>]` + "`" + `.
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
`zk` was designed with automation in mind and strive to be [a good Unix citizen](https://en.wikipedia.org/wiki/Unix_philosophy). As such, it offers a number of ways to interface with other programs:
|
||||
|
||||
* [write command aliases](config-alias.md) for repeated complex commands
|
||||
* write [command aliases](config-alias.md) or [named filters](config-filter.md) for repeated complex commands
|
||||
* [call `zk` from other programs](external-call.md)
|
||||
* [send notes for processing by other programs](external-processing.md)
|
||||
* [create a note with initial content](note-creation.md) from a standard input pipe
|
||||
|
@ -68,6 +68,8 @@ In this case, additional arguments do not necessarily make sense, so we omit the
|
||||
recent = "zk edit --sort created- --created-after 'last two weeks' --interactive"
|
||||
```
|
||||
|
||||
This kind of alias might be more useful as a [named filter](config-filter.md).
|
||||
|
||||
### Edit the configuration file
|
||||
|
||||
Here's a concrete example using environment variables, in particular `ZK_NOTEBOOK_DIR`. Note the double quotes around the path.
|
||||
|
49
docs/config-filter.md
Normal file
49
docs/config-filter.md
Normal file
@ -0,0 +1,49 @@
|
||||
# Named filter
|
||||
|
||||
A named filter is a set of [note filtering options](note-filtering.md) used frequently together, declared in the [configuration file](config.md).
|
||||
|
||||
For example, if you use regularly the following command to list your most recent notes:
|
||||
|
||||
```sh
|
||||
$ zk list --sort created- --created-after "last two weeks"
|
||||
```
|
||||
|
||||
You can create a new named filter in the configuration file to avoid repeating yourself.
|
||||
|
||||
```toml
|
||||
[filter]
|
||||
recents = "--sort created- --created-after 'last two weeks'"
|
||||
```
|
||||
|
||||
Then, you can use the name as an argument of `zk list`, with any additional option.
|
||||
|
||||
```sh
|
||||
$ zk list recents --limit 10
|
||||
```
|
||||
|
||||
Named filters are similar to [command aliases](config-alias.md), as they simplify frequent commands. However, named filters can be used with any command accepting filtering options.
|
||||
|
||||
```sh
|
||||
$ zk edit recents --interactive
|
||||
```
|
||||
|
||||
## Filter named after a directory
|
||||
|
||||
In filtering commands, named filters take precedence over path arguments. As a nice side effect, this means you can customize the default filtering options for a directory by naming a filter after it.
|
||||
|
||||
For example, by default `zk` sorts notes by their titles. However, if you keep daily notes under a `journal/` directory, you may want to sort them by creation date instead. You can use the following named filter for this:
|
||||
|
||||
```
|
||||
[filter]
|
||||
journal = "--sort created journal"
|
||||
```
|
||||
|
||||
Named filters cannot call themselves recursively, so by adding the `journal` argument to the filter, we are actually selecting the `journal/` directory. This means that the following commands are equivalent:
|
||||
|
||||
```sh
|
||||
# Without the filter
|
||||
$ zk list --sort created journal
|
||||
|
||||
# With the filter
|
||||
$ zk list journal
|
||||
```
|
@ -10,6 +10,7 @@ Each [notebook](notebook.md) contains a configuration file used to customize you
|
||||
* [your default editor](tool-editor.md)
|
||||
* [your default pager](tool-pager.md)
|
||||
* [`fzf`](tool-fzf.md)
|
||||
* `[filter]` declares your [named filters](config-filter.md)
|
||||
* `[alias]` holds your [command aliases](config-alias.md)
|
||||
|
||||
## Global configuration file
|
||||
@ -88,6 +89,9 @@ pager = "less -FIRX"
|
||||
# Command used to preview a note during interactive fzf mode.
|
||||
fzf-preview = "bat -p --color always {-1}"
|
||||
|
||||
# NAMED FILTERS
|
||||
[filter]
|
||||
recents = "--sort created- --created-after 'last two weeks'"
|
||||
|
||||
# COMMAND ALIASES
|
||||
[alias]
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Searching and filtering notes
|
||||
|
||||
A few commands are built upon `zk`'s powerful note filtering capabilities, such as `edit` and `list`. They accept any option described here.
|
||||
A few commands are built upon `zk`'s powerful note filtering capabilities, such as `edit` and `list`. They accept any option described here. You may also declare [named filters](config-filter.md) in the [configuration file](config.md) for the same set of options you use frequently.
|
||||
|
||||
## Filter by path
|
||||
|
||||
|
@ -91,3 +91,13 @@ func RemoveDuplicates(strings []string) []string {
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// InList returns whether the string is part of the given list of strings.
|
||||
func InList(strings []string, s string) bool {
|
||||
for _, c := range strings {
|
||||
if c == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -93,3 +93,16 @@ func TestRemoveDuplicates(t *testing.T) {
|
||||
test([]string{"Two", "One", "Two", "One"}, []string{"Two", "One"})
|
||||
test([]string{"One", "Two", "OneTwo"}, []string{"One", "Two", "OneTwo"})
|
||||
}
|
||||
|
||||
func TestInList(t *testing.T) {
|
||||
test := func(items []string, s string, expected bool) {
|
||||
assert.Equal(t, InList(items, s), expected)
|
||||
}
|
||||
|
||||
test([]string{}, "", false)
|
||||
test([]string{}, "none", false)
|
||||
test([]string{"one"}, "none", false)
|
||||
test([]string{"one"}, "one", true)
|
||||
test([]string{"one", "two"}, "one", true)
|
||||
test([]string{"one", "two"}, "three", false)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user