na 1.2.37 → 1.2.39

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35e6d784b55e397b8843663130837bcebb9041640a7833d650cedc9b783289e2
4
- data.tar.gz: a1ba45d7ba0d77e122667bc5c93629832dc379ee5d2275076182849055c8d72e
3
+ metadata.gz: e55b998c33447bd876e7c26c1fcf7f136a5f3de847715f8565882b0df4d078f2
4
+ data.tar.gz: 8ce2a06c64b3e45291c185ffe9a38f9a6cb0fa3dc4b0eaf9f2de9a38bc295701
5
5
  SHA512:
6
- metadata.gz: 1ee63a7b2bfd58496bf2047cd80a16f30bf255ca1edc8b0aa5a84a9abc8e4d1ccdd8082cd8f01fb8db60d5b73c7e2014f4f847369ac0d39274b5929ef8173f07
7
- data.tar.gz: d0321cddedf590c96a18cbd1096c7982240cb8c172c9f3491e0569ecfdf8882955898056ff3fd56d9e529d91be2a9ca56f3003bdbf6f1517a9658d08a302ca30
6
+ metadata.gz: cca3d6c0f98d5c30fdc5c22f6a10958b6888e08f2c22b92d13d698152465f756e491258fd4290f42f85062b7718068deabc43a07e4ea92891370f0ba35024a00
7
+ data.tar.gz: e20114aef91f0c9aa2edb15b95d335d86afa5aee03b6a4e214b2fdca7fa6acbc7b178e6f00d4348dd3329d6f88122f044591271f2886b732f8d046673e628403
data/CHANGELOG.md CHANGED
@@ -1,3 +1,42 @@
1
+ ### 1.2.39
2
+
3
+ 2023-09-06 04:25
4
+
5
+ #### NEW
6
+
7
+ - Add `--save NAME` to `na next` to save more complex queries and run with `na saved NAME` (or just `na NAME`)
8
+ - `na saved --select` flag to allow interactive selection of search(es)
9
+
10
+ #### IMPROVED
11
+
12
+ - Allow `na saved --delete` to handle multiple arguments
13
+ - Allow wildcards when deleting saved searches
14
+ - Refactor request for input, no change to user experience
15
+ - Refined wildcard (?*) handling
16
+ - When displaying actions wider than the screen, wrap at words and indent 2 spaces from start of action (after prefix)
17
+
18
+ ### 1.2.38
19
+
20
+ 2023-09-03 11:25
21
+
22
+ #### NEW
23
+
24
+ - Open the todos database in an editor with `na todos --edit`
25
+ - A theme file is written to ~/.local/share/na/theme.yaml where you can modify the colors used for all displays
26
+ - Allow tag=~PATTERN comparison for regex matching
27
+
28
+ #### IMPROVED
29
+
30
+ - Better error message for `na next` when no todo is matched
31
+ - If STDOUT isn't a TTY, don't enable pagination, regardless of global setting
32
+ - Allow --find or --grep as synonyms for --search
33
+
34
+ #### FIXED
35
+
36
+ - Date tags containing hyphens triggered OR searches because they were initially interpreted as negative tag searches
37
+ - Templating irregularities
38
+ - Error thrown when running without $EDITOR variable defined in environment
39
+
1
40
  ### 1.2.37
2
41
 
3
42
  2023-09-01 12:42
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.36)
4
+ na (1.2.39)
5
5
  chronic (~> 0.10, >= 0.10.2)
6
6
  gli (~> 2.21.0)
7
7
  mdless (~> 1.0, >= 1.0.32)
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
  _If you're one of the rare people like me who find this useful, feel free to
10
10
  [buy me some coffee][donate]._
11
11
 
12
- The current version of `na` is 1.2.37
12
+ The current version of `na` is 1.2.39
13
13
  .
14
14
 
15
15
  `na` ("next action") is a command line tool designed to make it easy to see what your next actions are for any project, right from the command line. It works with TaskPaper-formatted files (but any plain text format will do), looking for `@na` tags (or whatever you specify) in todo files in your current folder.
@@ -77,7 +77,7 @@ SYNOPSIS
77
77
  na [global options] command [command options] [arguments...]
78
78
 
79
79
  VERSION
80
- 1.2.37
80
+ 1.2.39
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -114,6 +114,7 @@ COMMANDS
114
114
  prompt - Show or install prompt hooks for the current shell
115
115
  restore, unfinish - Find and remove @done tag from an action
116
116
  saved - Execute a saved search
117
+ tag - Add tags to matching action(s)
117
118
  tagged - Find actions matching a tag
118
119
  todos - Show list of known todo files
119
120
  undo - Undo the last change
@@ -291,7 +292,8 @@ COMMAND OPTIONS
291
292
  --omnifocus - Output actions nested by file and project
292
293
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
293
294
  --regex - Search query is regular expression
294
- --search=QUERY - Filter results using search terms (may be used more than once, default: none)
295
+ --save=TITLE - Save this search for future use (default: none)
296
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
295
297
  -t, --tag=TAG - Alternate tag to search for (default: none)
296
298
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
297
299
 
@@ -352,6 +354,7 @@ DESCRIPTION
352
354
  COMMAND OPTIONS
353
355
  -d, --delete - Delete the specified search definition
354
356
  -e, --edit - Open the saved search file in $EDITOR
357
+ -s, --select - Interactively select a saved search to run
355
358
 
356
359
  EXAMPLES
357
360
 
@@ -402,7 +405,8 @@ COMMAND OPTIONS
402
405
  --omnifocus - Output actions nested by file and project
403
406
  --proj, --project=PROJECT[/SUBPROJECT] - Show actions from a specific project (default: none)
404
407
  --regex - Search query is regular expression
405
- --search=QUERY - Filter results using search terms (may be used more than once, default: none)
408
+ --save=TITLE - Save this search for future use (default: none)
409
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
406
410
  -t, --tag=TAG - Alternate tag to search for (default: none)
407
411
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
408
412
 
@@ -428,10 +432,13 @@ NAME
428
432
 
429
433
  SYNOPSIS
430
434
 
431
- na [global options] todos [QUERY]
435
+ na [global options] todos [command options] [QUERY]
432
436
 
433
437
  DESCRIPTION
434
- Arguments will be interpreted as a query against which the list of todos will be fuzzy matched. Separate directories with /, :, or a space, e.g. `na todos code/marked`
438
+ Arguments will be interpreted as a query against which the list of todos will be fuzzy matched. Separate directories with /, :, or a space, e.g. `na todos code/marked`
439
+
440
+ COMMAND OPTIONS
441
+ -e, --[no-]edit - Open the todo database in an editor for manual modification
435
442
  ```
436
443
 
437
444
  ##### update
@@ -589,6 +596,75 @@ EXAMPLE
589
596
  na archive "An existing task"
590
597
  ```
591
598
 
599
+ ##### tag
600
+
601
+ Add, remove, or modify tags.
602
+
603
+ Use `na tag TAGNAME --[search|tagged] SEARCH_STRING` to add a tag to matching action (use `--all` to apply to all matching actions). If you use `!TAGNAME` it will remove that tag (regardless of value). To change the value of an existing tag (or add it if it doesn't exist), use `~TAGNAME(NEW VALUE)`.
604
+
605
+ ```
606
+ NAME
607
+ tag - Add tags to matching action(s)
608
+
609
+ SYNOPSIS
610
+
611
+ na [global options] tag [command options] TAG
612
+
613
+ DESCRIPTION
614
+ Provides an easy way to tag existing actions. Use !tag to remove a tag, use ~tag(new value) to change a tag or add a value. If multiple todo files are found in the current directory, a menu will allow you to pick which file to act on, or use --all to apply to all matches.
615
+
616
+ COMMAND OPTIONS
617
+ --all - Act on all matches immediately (no menu)
618
+ -d, --depth=DEPTH - Search for files X directories deep (default: 1)
619
+ --[no-]done - Include @done actions
620
+ -e, --regex - Interpret search pattern as regular expression
621
+ --file=PATH - Specify the file to search for the task (default: none)
622
+ --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
623
+ --search, --find, --grep=QUERY - Filter results using search terms (may be used more than once, default: none)
624
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
625
+ -x, --exact - Match pattern exactly
626
+
627
+ EXAMPLES
628
+
629
+ # Find "An existing task" action and add @project(warpspeed) to it
630
+ na tag "project(warpspeed)" --search "An existing task"
631
+
632
+ # Find all actions tagged @project2 and remove @project1 from them
633
+ na tag "!project1" --tagged project2 --all
634
+
635
+ # Remove @project2 from all actions
636
+ na tag "!project2" --all
637
+
638
+ # Find "An existing task" and change (or add) its @project tag value to "dirt nap"
639
+ na tag "~project(dirt nap)" --search "An existing task"
640
+ ```
641
+
642
+ ##### undo
643
+
644
+ Undoes the last file change resulting from an add or update command. If no argument is given, it undoes whatever the last change in history was. If an argument is provided, it's used to match against the change history, finding a specific file to restore from backup.
645
+
646
+ Only the most recent change can be undone.
647
+
648
+ ```
649
+ NAME
650
+ undo - Undo the last change
651
+
652
+ SYNOPSIS
653
+
654
+ na [global options] undo [FILE]...
655
+
656
+ DESCRIPTION
657
+ Run without argument to undo most recent change
658
+
659
+ EXAMPLES
660
+
661
+ # Undo the last change
662
+ na undo
663
+
664
+ # Undo the last change to a file matching "myproject"
665
+ na undo myproject
666
+ ```
667
+
592
668
  ### Configuration
593
669
 
594
670
  Global options such as todo extension and default next action tag can be stored permanently by using the `na initconfig` command. Run na with the global options you'd like to set, and add `initconfig` at the end of the command. A file will be written to `~/.na.rc`. You can edit this manually, or just update it using the `initconfig --force` command to overwrite it with new settings.
data/bin/commands/add.rb CHANGED
@@ -62,25 +62,23 @@ class App
62
62
  if NA.global_file
63
63
  target = File.expand_path(NA.global_file)
64
64
  unless File.exist?(target)
65
- res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
65
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create it"), default: true)
66
66
  if res
67
67
  basename = File.basename(target, ".#{NA.extension}")
68
68
  NA.create_todo(target, basename, template: global_options[:template])
69
69
  else
70
- puts NA::Color.template('{r}Cancelled{x}')
71
- Process.exit 1
70
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
72
71
  end
73
72
  end
74
73
  elsif options[:file]
75
74
  target = File.expand_path(options[:file])
76
75
  unless File.exist?(target)
77
- res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
76
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create it"), default: true)
78
77
  if res
79
78
  basename = File.basename(target, ".#{NA.extension}")
80
79
  NA.create_todo(target, basename, template: global_options[:template])
81
80
  else
82
- puts NA::Color.template('{r}Cancelled{x}')
83
- Process.exit 1
81
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1)
84
82
  end
85
83
  end
86
84
  elsif options[:todo]
@@ -102,8 +100,8 @@ class App
102
100
  target = File.expand_path(todo)
103
101
  unless File.exist?(target)
104
102
 
105
- res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
106
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless res
103
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}Specified file not found, create #{todo}"), default: true)
104
+ NA.notify("#{NA.theme[:error]}Cancelled{x}", exit_code: 1) unless res
107
105
 
108
106
  basename = File.basename(target, ".#{NA.extension}")
109
107
  NA.create_todo(target, basename, template: global_options[:template])
@@ -113,7 +111,7 @@ class App
113
111
  else
114
112
  files = NA.find_files(depth: options[:depth])
115
113
  if files.count.zero?
116
- res = NA.yn(NA::Color.template('{by}No todo file found, create one'), default: true)
114
+ res = NA.yn(NA::Color.template("#{NA.theme[:warning]}No todo file found, create one"), default: true)
117
115
  if res
118
116
  basename = File.expand_path('.').split('/').last
119
117
  target = "#{basename}.#{NA.extension}"
@@ -122,17 +120,14 @@ class App
122
120
  end
123
121
  end
124
122
  target = files.count > 1 ? NA.select_file(files) : files[0]
125
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive? && File.exist?(target)
123
+ NA.notify("#{NA.theme[:error]}Cancelled{x}", exit_code: 1) unless files.count.positive? && File.exist?(target)
126
124
 
127
125
  end
128
126
 
129
127
  action = if args.count.positive?
130
128
  args.join(' ').strip
131
- elsif $stdin.isatty && TTY::Which.exist?('gum')
132
- `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
133
- elsif $stdin.isatty
134
- puts NA::Color.template('{bm}Enter task:{x}')
135
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
129
+ else
130
+ NA.request_input(options, prompt: 'Enter a task')
136
131
  end
137
132
 
138
133
  if action.nil? || action.empty?
@@ -171,7 +166,7 @@ class App
171
166
  args << '--width $(tput cols)'
172
167
  `gum write #{args.join(' ')}`.strip.split("\n")
173
168
  else
174
- puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
169
+ NA.notify("#{NA.theme[:prompt]}Enter a note, {bw}CTRL-d#{NA.theme[:prompt]} to end editing#{NA.theme[:action]}")
175
170
  reader.read_multiline
176
171
  end
177
172
  end
data/bin/commands/edit.rb CHANGED
@@ -43,19 +43,11 @@ class App
43
43
  options[:edit] = true
44
44
  action = if args.count.positive?
45
45
  args.join(' ').strip
46
- elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
47
- opts = [
48
- %(--placeholder "Enter a task to search for"),
49
- '--char-limit=500',
50
- "--width=#{TTY::Screen.columns}"
51
- ]
52
- `gum input #{opts.join(' ')}`.strip
53
- elsif $stdin.isatty && options[:tagged].empty?
54
- puts NA::Color.template('{bm}Enter search string:{x}')
55
- reader.read_line(NA::Color.template('{by}> {bw}')).strip
46
+ else
47
+ NA.request_input(options, prompt: 'Enter a task to search for')
56
48
  end
57
49
 
58
- NA.notify('{br}Empty input{x}', exit_code: 1) unless action
50
+ NA.notify("#{NA.theme[:error]}Empty input", exit_code: 1) if (action.nil? || action.empty?) && options[:tagged].empty?
59
51
 
60
52
  if action
61
53
  tokens = nil
@@ -79,32 +71,28 @@ class App
79
71
  end
80
72
 
81
73
  if (action.nil? || action.empty?) && options[:tagged].empty?
82
- NA.notify('{br}Empty input, cancelled{x}', exit_code: 1)
74
+ NA.notify("#{NA.theme[:error]}Empty input, cancelled", exit_code: 1)
83
75
  end
84
76
 
85
- all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
77
+ all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
86
78
  tags = []
87
79
  options[:tagged].join(',').split(/ *, */).each do |arg|
88
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
80
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^~]+?)(?:(?<op>[=<>~]{1,2}|[*$\^]=)(?<val>.*?))?$/)
89
81
 
90
82
  tags.push({
91
83
  tag: m['tag'].wildcard_to_rx,
92
84
  comp: m['op'],
93
85
  value: m['val'],
94
86
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
95
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
87
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/
96
88
  })
97
89
  end
98
90
 
99
- target_proj = if NA.cwd_is == :project
100
- NA.cwd
101
- else
102
- nil
103
- end
91
+ target_proj = NA.cwd_is == :project ? NA.cwd : nil
104
92
 
105
93
  if options[:file]
106
94
  file = File.expand_path(options[:file])
107
- NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
95
+ NA.notify("#{NA.theme[:error]}File not found", exit_code: 1) unless File.exist?(file)
108
96
 
109
97
  targets = [file]
110
98
  elsif options[:todo]
@@ -114,7 +102,7 @@ class App
114
102
  todo.push({
115
103
  token: m['tok'],
116
104
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
117
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
105
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/
118
106
  })
119
107
  end
120
108
  dirs = NA.match_working_dir(todo)
@@ -123,21 +111,21 @@ class App
123
111
  targets = [dirs[0]]
124
112
  elsif dirs.count.positive?
125
113
  targets = NA.select_file(dirs, multiple: true)
126
- NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
114
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless targets && targets.count.positive?
127
115
  else
128
- NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
116
+ NA.notify("#{NA.theme[:error]}Todo not found", exit_code: 1) unless targets && targets.count.positive?
129
117
 
130
118
  end
131
119
  else
132
120
  files = NA.find_files(depth: options[:depth])
133
- NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
121
+ NA.notify("#{NA.theme[:error]}No todo file found", exit_code: 1) if files.count.zero?
134
122
 
135
123
  targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
136
- NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
124
+ NA.notify("#{NA.theme[:error]}Cancelled", exit_code: 1) unless files.count.positive?
137
125
 
138
126
  end
139
127
 
140
- NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
128
+ NA.notify("#{NA.theme[:error]}No search terms provided", exit_code: 1) if tokens.nil? && options[:tagged].empty?
141
129
 
142
130
  targets.each do |target|
143
131
  NA.update_action(target,
data/bin/commands/find.rb CHANGED
@@ -61,7 +61,8 @@ class App
61
61
 
62
62
  if options[:save]
63
63
  title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
64
- NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
64
+ cmd = NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')
65
+ NA.save_search(title, cmd)
65
66
  end
66
67
 
67
68
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -73,7 +74,12 @@ class App
73
74
  if options[:exact] || options[:regex]
74
75
  search = args.join(' ')
75
76
  else
76
- search = args.join(' ').gsub(/(?<=\A|[ ,])(?<req>[+\-!])?@(?<tag>[^ *=<>$\^,@(]+)(?:\((?<value>.*?)\)| *(?<op>[=<>]{1,2}|[*$\^]=) *(?<val>.*?(?=\Z|[,@])))?/) do |arg|
77
+ rx = [
78
+ '(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$*\^,@(]+)',
79
+ '(?:\((?<value>.*?)\)| *(?<op>[=<>~]{1,2}|[*$\^]=) *',
80
+ '(?<val>.*?(?=\Z|[,@])))?'
81
+ ].join('')
82
+ search = args.join(' ').gsub(Regexp.new(rx)) do
77
83
  m = Regexp.last_match
78
84
  string = if m['value']
79
85
  "#{m['req']}#{m['tag']}=#{m['value']}"
@@ -87,17 +93,17 @@ class App
87
93
 
88
94
  search = search.gsub(/ +/, ' ').strip
89
95
 
90
- all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
96
+ all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
91
97
  tags = []
92
98
  options[:tagged].join(',').split(/ *, */).each do |arg|
93
- m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?) *(?:(?<op>[=<>]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
99
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
94
100
 
95
101
  tags.push({
96
102
  tag: m['tag'].wildcard_to_rx,
97
103
  comp: m['op'],
98
104
  value: m['val'],
99
105
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
100
- negate: !m['req'].nil? && m['req'] =~ /[!\-]/
106
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
101
107
  })
102
108
  end
103
109
 
@@ -112,14 +118,15 @@ class App
112
118
  tokens = Regexp.new(search, Regexp::IGNORECASE)
113
119
  else
114
120
  tokens = []
115
- all_req = search !~ /[+!-]/ && !options[:or]
121
+ all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
116
122
 
117
123
  search.split(/ /).each do |arg|
118
124
  m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
125
+
119
126
  tokens.push({
120
- token: Regexp.escape(m['tok']),
127
+ token: m['tok'],
121
128
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
122
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
129
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
123
130
  })
124
131
  end
125
132
  end
@@ -150,7 +157,7 @@ class App
150
157
  })
151
158
 
152
159
  regexes = if tokens.is_a?(Array)
153
- tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
160
+ tokens.delete_if { |token| token[:negate] }.map { |token| token[:token].wildcard_to_rx }
154
161
  else
155
162
  [tokens]
156
163
  end
data/bin/commands/init.rb CHANGED
@@ -14,7 +14,7 @@ class App
14
14
  project = args.join(' ')
15
15
  elsif
16
16
  project = File.expand_path('.').split('/').last
17
- project = reader.read_line(NA::Color.template('{y}Project name {bw}> {x}'), value: project).strip if $stdin.isatty
17
+ project = reader.read_line(NA::Color.template("#{NA.theme[:prompt]}Project name #{NA.theme[:filename]}> "), value: project).strip if $stdin.isatty
18
18
  end
19
19
 
20
20
  target = "#{project}.#{NA.extension}"
data/bin/commands/next.rb CHANGED
@@ -37,7 +37,7 @@ class App
37
37
 
38
38
  c.desc 'Filter results using search terms'
39
39
  c.arg_name 'QUERY'
40
- c.flag %i[search], multiple: true
40
+ c.flag %i[search find grep], multiple: true
41
41
 
42
42
  c.desc 'Search query is regular expression'
43
43
  c.switch %i[regex], negatable: false
@@ -57,6 +57,10 @@ class App
57
57
  c.desc 'Output actions nested by file and project'
58
58
  c.switch %i[omnifocus], negatable: false
59
59
 
60
+ c.desc 'Save this search for future use'
61
+ c.arg_name 'TITLE'
62
+ c.flag %i[save]
63
+
60
64
  c.action do |global_options, options, args|
61
65
  if global_options[:add]
62
66
  cmd = ['add']
@@ -67,6 +71,11 @@ class App
67
71
  exit run(cmd)
68
72
  end
69
73
 
74
+ if options[:save]
75
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
76
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
77
+ end
78
+
70
79
  options[:nest] = true if options[:omnifocus]
71
80
 
72
81
  depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
@@ -75,71 +84,100 @@ class App
75
84
  options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
76
85
  end
77
86
 
78
- all_req = options[:tagged].join(' ') !~ /[+!-]/ && !options[:or]
87
+ if options[:exact] || options[:regex]
88
+ search = options[:search].join(' ')
89
+ else
90
+ rx = [
91
+ '(?<=\A|[ ,])(?<req>[+!-])?@(?<tag>[^ *=<>$~\^,@(]+)',
92
+ '(?:\((?<value>.*?)\)| *(?<op>=~|[=<>~]{1,2}|[*$\^]=) *',
93
+ '(?<val>.*?(?=\Z|[,@])))?'
94
+ ].join('')
95
+ search = options[:search].join(' ').gsub(Regexp.new(rx)) do
96
+ m = Regexp.last_match
97
+ string = if m['value']
98
+ "#{m['req']}#{m['tag']}=#{m['value']}"
99
+ else
100
+ m[0]
101
+ end
102
+ options[:tagged] << string.sub(/@/, '')
103
+ ''
104
+ end
105
+ end
106
+
107
+ search = search.gsub(/,/, '').gsub(/ +/, ' ') unless search.nil?
108
+
109
+ all_req = options[:tagged].join(' ') !~ /(?<=[, ])[+!-]/ && !options[:or]
79
110
  tags = []
80
111
  options[:tagged].join(',').split(/ *, */).each do |arg|
81
- m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
112
+ m = arg.match(/^(?<req>[+!-])?(?<tag>[^ =<>$~\^]+?) *(?:(?<op>[=<>~]{1,2}|[*$\^]=) *(?<val>.*?))?$/)
82
113
 
83
114
  tags.push({
84
115
  tag: m['tag'].wildcard_to_rx,
85
116
  comp: m['op'],
86
117
  value: m['val'],
87
118
  required: all_req || (!m['req'].nil? && m['req'] == '+'),
88
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
119
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
89
120
  })
90
121
  end
91
122
 
92
123
  args.concat(options[:in])
93
124
  if args.count.positive?
94
- all_req = args.join(' ') !~ /[+!-]/
125
+ all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
95
126
 
96
127
  tokens = []
97
128
  args.each do |arg|
98
129
  arg.split(/ *, */).each do |a|
99
- m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
130
+ m = a.match(/^(?<req>[+!-])?(?<tok>.*?)$/)
100
131
  tokens.push({
101
132
  token: m['tok'],
102
133
  required: !m['req'].nil? && m['req'] == '+',
103
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
134
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
104
135
  })
105
136
  end
106
137
  end
107
138
  end
108
139
 
109
- search = nil
110
- if options[:search]
111
- if options[:exact]
112
- search = options[:search].join(' ')
113
- elsif options[:regex]
114
- search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
115
- else
116
- search = []
117
- all_req = options[:search].join(' ') !~ /[+!-]/ && !options[:or]
118
-
119
- options[:search].join(' ').split(/ /).each do |arg|
120
- m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
121
- search.push({
122
- token: m['tok'],
123
- required: all_req || (!m['req'].nil? && m['req'] == '+'),
124
- negate: !m['req'].nil? && m['req'] =~ /[!-]/
125
- })
126
- end
140
+ search_for_done = false
141
+ tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
142
+ options[:done] = true if search_for_done
143
+
144
+ search_tokens = nil
145
+ if options[:exact]
146
+ search_tokens = search
147
+ elsif options[:regex]
148
+ search_tokens = Regexp.new(search, Regexp::IGNORECASE)
149
+ else
150
+ search_tokens = []
151
+ all_req = search !~ /(?<=[, ])[+!-]/ && !options[:or]
152
+
153
+ search.split(/ /).each do |arg|
154
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
155
+ search_tokens.push({
156
+ token: m['tok'],
157
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
158
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/ ? true : false
159
+ })
127
160
  end
128
161
  end
129
162
 
130
163
  NA.na_tag = options[:tag] unless options[:tag].nil?
131
164
  require_na = true
132
165
 
133
- tag = [{ tag: NA.na_tag, value: nil }]
166
+ tag = [{ tag: NA.na_tag, value: nil, required: true, negate: false }]
134
167
  tag << { tag: 'done', value: nil, negate: true } unless options[:done]
135
168
  tag.concat(tags)
169
+
136
170
  todo = NA::Todo.new({ depth: depth,
137
171
  done: options[:done],
138
172
  query: tokens,
139
173
  tag: tag,
140
- search: search,
174
+ search: search_tokens,
141
175
  project: options[:project],
142
176
  require_na: require_na })
177
+ if todo.files.empty?
178
+ NA.notify("#{NA.theme[:error]}No matches found for #{tokens[0][:token]}.
179
+ Run `na todos` to see available todo files.")
180
+ end
143
181
  NA::Pager.paginate = false if options[:omnifocus]
144
182
  todo.actions.output(depth,
145
183
  files: todo.files,
@@ -16,7 +16,7 @@ class App
16
16
 
17
17
  c.action do |_global_options, options, args|
18
18
  if args.count.positive?
19
- all_req = args.join(' ') !~ /[+!-]/
19
+ all_req = args.join(' ') !~ /(?<=[, ])[+!-]/
20
20
 
21
21
  tokens = [{ token: '*', required: all_req, negate: false }]
22
22
  args.each do |arg|