2021-04-14 18:14:01 +00:00
package cli
2021-01-23 14:39:47 +00:00
import (
2021-03-24 20:06:32 +00:00
"fmt"
2021-01-23 14:39:47 +00:00
"time"
2021-03-24 20:06:32 +00:00
"github.com/alecthomas/kong"
"github.com/kballard/go-shellquote"
2021-04-14 18:14:01 +00:00
"github.com/mickael-menu/zk/internal/core"
2021-05-11 19:53:19 +00:00
dateutil "github.com/mickael-menu/zk/internal/util/date"
2021-04-14 18:14:01 +00:00
"github.com/mickael-menu/zk/internal/util/errors"
"github.com/mickael-menu/zk/internal/util/opt"
"github.com/mickael-menu/zk/internal/util/strings"
2021-01-23 14:39:47 +00:00
)
// Filtering holds filtering options to select notes.
type Filtering struct {
2021-12-11 17:25:40 +00:00
Path [ ] string ` kong:"group='filter',arg,optional,placeholder='PATH',help='Find notes matching the given path, including its descendants.'" json:"hrefs" `
2021-02-03 21:31:00 +00:00
2021-12-11 17:25:40 +00:00
Interactive bool ` kong:"group='filter',short='i',help='Select notes interactively with fzf.'" json:"-" `
Limit int ` kong:"group='filter',short='n',placeholder='COUNT',help='Limit the number of notes found.'" json:"limit" `
Match string ` kong:"group='filter',short='m',placeholder='QUERY',help='Terms to search for in the notes.'" json:"match" `
ExactMatch bool ` kong:"group='filter',short='e',help='Search for exact occurrences of the --match argument (case insensitive).'" json:"exactMatch" `
Exclude [ ] string ` kong:"group='filter',short='x',placeholder='PATH',help='Ignore notes matching the given path, including its descendants.'" json:"excludeHrefs" `
Tag [ ] string ` kong:"group='filter',short='t',help='Find notes tagged with the given tags.'" json:"tags" `
Mention [ ] string ` kong:"group='filter',placeholder='PATH',help='Find notes mentioning the title of the given ones.'" json:"mention" `
MentionedBy [ ] string ` kong:"group='filter',placeholder='PATH',help='Find notes whose title is mentioned in the given ones.'" json:"mentionedBy" `
LinkTo [ ] string ` kong:"group='filter',short='l',placeholder='PATH',help='Find notes which are linking to the given ones.'" json:"linkTo" `
NoLinkTo [ ] string ` kong:"group='filter',placeholder='PATH',help='Find notes which are not linking to the given notes.'" json:"-" `
LinkedBy [ ] string ` kong:"group='filter',short='L',placeholder='PATH',help='Find notes which are linked by the given ones.'" json:"linkedBy" `
NoLinkedBy [ ] string ` kong:"group='filter',placeholder='PATH',help='Find notes which are not linked by the given ones.'" json:"-" `
Orphan bool ` kong:"group='filter',help='Find notes which are not linked by any other note.'" json:"orphan" `
Related [ ] string ` kong:"group='filter',placeholder='PATH',help='Find notes which might be related to the given ones.'" json:"related" `
MaxDistance int ` kong:"group='filter',placeholder='COUNT',help='Maximum distance between two linked notes.'" json:"maxDistance" `
Recursive bool ` kong:"group='filter',short='r',help='Follow links recursively.'" json:"recursive" `
Created string ` kong:"group='filter',placeholder='DATE',help:'Find notes created on the given date.'" json:"created" `
CreatedBefore string ` kong:"group='filter',placeholder='DATE',help='Find notes created before the given date.'" json:"createdBefore" `
CreatedAfter string ` kong:"group='filter',placeholder='DATE',help='Find notes created after the given date.'" json:"createdAfter" `
Modified string ` kong:"group='filter',placeholder='DATE',help='Find notes modified on the given date.'" json:"modified" `
ModifiedBefore string ` kong:"group='filter',placeholder='DATE',help='Find notes modified before the given date.'" json:"modifiedBefore" `
ModifiedAfter string ` kong:"group='filter',placeholder='DATE',help='Find notes modified after the given date.'" json:"modifiedAfter" `
2021-01-23 14:39:47 +00:00
2021-12-11 17:25:40 +00:00
Sort [ ] string ` kong:"group='sort',short='s',placeholder='TERM',help='Order the notes by the given criterion.'" json:"sort" `
2021-01-23 14:39:47 +00:00
}
2021-03-24 20:06:32 +00:00
// 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 {
2022-01-08 15:42:54 +00:00
if filter , ok := filters [ path ] ; ok && ! strings . Contains ( expandedFilters , path ) {
2021-03-24 20:06:32 +00:00
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 ... )
2021-04-17 11:06:23 +00:00
f . ExactMatch = f . ExactMatch || parsedFilter . ExactMatch
2021-03-24 20:06:32 +00:00
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
}
2021-04-14 18:14:01 +00:00
// NewNoteFindOpts creates an instance of core.NoteFindOpts from a set of user flags.
func ( f Filtering ) NewNoteFindOpts ( notebook * core . Notebook ) ( core . NoteFindOpts , error ) {
opts := core . NoteFindOpts { }
f , err := f . ExpandNamedFilters ( notebook . Config . Filters , [ ] string { } )
2021-03-24 20:06:32 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-03-24 20:06:32 +00:00
}
2021-04-14 18:14:01 +00:00
opts . Match = opt . NewNotEmptyString ( f . Match )
2021-04-17 11:06:23 +00:00
opts . ExactMatch = f . ExactMatch
2021-03-13 14:31:05 +00:00
2021-04-14 18:14:01 +00:00
if paths , ok := relPaths ( notebook , f . Path ) ; ok {
2021-11-28 20:50:21 +00:00
opts . IncludeHrefs = paths
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
if paths , ok := relPaths ( notebook , f . Exclude ) ; ok {
2021-11-28 20:50:21 +00:00
opts . ExcludeHrefs = paths
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
if len ( f . Tag ) > 0 {
opts . Tags = f . Tag
2021-03-07 16:00:09 +00:00
}
2021-04-14 18:14:01 +00:00
if len ( f . Mention ) > 0 {
opts . Mention = f . Mention
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
if len ( f . MentionedBy ) > 0 {
opts . MentionedBy = f . MentionedBy
2021-03-20 18:17:46 +00:00
}
2021-04-14 18:14:01 +00:00
if paths , ok := relPaths ( notebook , f . LinkedBy ) ; ok {
opts . LinkedBy = & core . LinkFilter {
2021-11-28 20:50:21 +00:00
Hrefs : paths ,
2021-03-13 14:31:05 +00:00
Negate : false ,
2021-04-14 18:14:01 +00:00
Recursive : f . Recursive ,
MaxDistance : f . MaxDistance ,
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
} else if paths , ok := relPaths ( notebook , f . NoLinkedBy ) ; ok {
opts . LinkedBy = & core . LinkFilter {
2021-11-28 20:50:21 +00:00
Hrefs : paths ,
2021-03-13 14:31:05 +00:00
Negate : true ,
2021-01-23 14:39:47 +00:00
}
}
2021-04-14 18:14:01 +00:00
if paths , ok := relPaths ( notebook , f . LinkTo ) ; ok {
opts . LinkTo = & core . LinkFilter {
2021-11-28 20:50:21 +00:00
Hrefs : paths ,
2021-03-13 14:31:05 +00:00
Negate : false ,
2021-04-14 18:14:01 +00:00
Recursive : f . Recursive ,
MaxDistance : f . MaxDistance ,
2021-03-13 14:31:05 +00:00
}
2021-04-14 18:14:01 +00:00
} else if paths , ok := relPaths ( notebook , f . NoLinkTo ) ; ok {
opts . LinkTo = & core . LinkFilter {
2021-11-28 20:50:21 +00:00
Hrefs : paths ,
2021-03-13 14:31:05 +00:00
Negate : true ,
2021-01-23 14:39:47 +00:00
}
}
2021-04-14 18:14:01 +00:00
if paths , ok := relPaths ( notebook , f . Related ) ; ok {
2021-03-13 14:31:05 +00:00
opts . Related = paths
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
opts . Orphan = f . Orphan
2021-03-13 14:31:05 +00:00
2021-04-14 18:14:01 +00:00
if f . Created != "" {
start , end , err := parseDayRange ( f . Created )
2021-01-23 14:39:47 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-01-23 14:39:47 +00:00
}
2021-03-13 14:31:05 +00:00
opts . CreatedStart = & start
opts . CreatedEnd = & end
} else {
2021-04-14 18:14:01 +00:00
if f . CreatedBefore != "" {
2021-05-11 19:53:19 +00:00
date , err := dateutil . TimeFromNatural ( f . CreatedBefore )
2021-03-13 14:31:05 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-03-13 14:31:05 +00:00
}
opts . CreatedEnd = & date
}
2021-04-14 18:14:01 +00:00
if f . CreatedAfter != "" {
2021-05-11 19:53:19 +00:00
date , err := dateutil . TimeFromNatural ( f . CreatedAfter )
2021-03-13 14:31:05 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-03-13 14:31:05 +00:00
}
opts . CreatedStart = & date
}
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
if f . Modified != "" {
start , end , err := parseDayRange ( f . Modified )
2021-01-23 14:39:47 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-01-23 14:39:47 +00:00
}
2021-03-13 14:31:05 +00:00
opts . ModifiedStart = & start
opts . ModifiedEnd = & end
} else {
2021-04-14 18:14:01 +00:00
if f . ModifiedBefore != "" {
2021-05-11 19:53:19 +00:00
date , err := dateutil . TimeFromNatural ( f . ModifiedBefore )
2021-03-13 14:31:05 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-03-13 14:31:05 +00:00
}
opts . ModifiedEnd = & date
}
2021-04-14 18:14:01 +00:00
if f . ModifiedAfter != "" {
2021-05-11 19:53:19 +00:00
date , err := dateutil . TimeFromNatural ( f . ModifiedAfter )
2021-03-13 14:31:05 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-03-13 14:31:05 +00:00
}
opts . ModifiedStart = & date
}
2021-01-29 19:40:06 +00:00
}
2021-04-14 18:14:01 +00:00
sorters , err := core . NoteSortersFromStrings ( f . Sort )
2021-01-23 14:39:47 +00:00
if err != nil {
2021-04-14 18:14:01 +00:00
return opts , err
2021-01-23 14:39:47 +00:00
}
2021-03-13 14:31:05 +00:00
opts . Sorters = sorters
2021-04-14 18:14:01 +00:00
opts . Limit = f . Limit
2021-01-23 14:39:47 +00:00
2021-04-14 18:14:01 +00:00
return opts , nil
2021-01-23 14:39:47 +00:00
}
2021-04-14 18:14:01 +00:00
func relPaths ( notebook * core . Notebook , paths [ ] string ) ( [ ] string , bool ) {
2021-01-23 14:39:47 +00:00
relPaths := make ( [ ] string , 0 )
for _ , p := range paths {
2021-04-14 18:14:01 +00:00
path , err := notebook . RelPath ( p )
2021-01-23 14:39:47 +00:00
if err == nil {
relPaths = append ( relPaths , path )
}
}
return relPaths , len ( relPaths ) > 0
}
2021-03-13 14:31:05 +00:00
func parseDayRange ( date string ) ( start time . Time , end time . Time , err error ) {
2021-05-11 19:53:19 +00:00
day , err := dateutil . TimeFromNatural ( date )
2021-03-13 14:31:05 +00:00
if err != nil {
return
}
start = startOfDay ( day )
end = start . AddDate ( 0 , 0 , 1 )
return start , end , nil
}
func startOfDay ( t time . Time ) time . Time {
year , month , day := t . Date ( )
return time . Date ( year , month , day , 0 , 0 , 0 , 0 , t . Location ( ) )
}