na 1.2.27 → 1.2.29

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Show next actions'
4
+ long_desc 'Next actions are actions which contain the next action tag (default @na),
5
+ do not contain @done, and are not in the Archive project.
6
+
7
+ Arguments will target a todo file from history, whether it\'s in the current
8
+ directory or not. Todo file queries can include path components separated by /
9
+ or :, and may use wildcards (`*` to match any text, `?` to match a single character). Multiple queries allowed (separate arguments or separated by comma).'
10
+ arg_name 'QUERY', optional: true
11
+ command %i[next show] do |c|
12
+ c.example 'na next', desc: 'display the next actions from any todo files in the current directory'
13
+ c.example 'na next -d 3', desc: 'display the next actions from the current directory, traversing 3 levels deep'
14
+ c.example 'na next marked', desc: 'display next actions for a project you visited in the past'
15
+
16
+ c.desc 'Recurse to depth'
17
+ c.arg_name 'DEPTH'
18
+ c.flag %i[d depth], type: :integer, must_match: /^[1-9]$/
19
+
20
+ c.desc 'Display matches from a known todo file'
21
+ c.arg_name 'TODO_FILE'
22
+ c.flag %i[in todo], multiple: true
23
+
24
+ c.desc 'Alternate tag to search for'
25
+ c.arg_name 'TAG'
26
+ c.flag %i[t tag]
27
+
28
+ c.desc 'Show actions from a specific project'
29
+ c.arg_name 'PROJECT[/SUBPROJECT]'
30
+ c.flag %i[proj project]
31
+
32
+ c.desc 'Match actions containing tag. Allows value comparisons'
33
+ c.arg_name 'TAG'
34
+ c.flag %i[tagged], multiple: true
35
+
36
+ c.desc 'Filter results using search terms'
37
+ c.arg_name 'QUERY'
38
+ c.flag %i[search], multiple: true
39
+
40
+ c.desc 'Search query is regular expression'
41
+ c.switch %i[regex], negatable: false
42
+
43
+ c.desc 'Search query is exact text match (not tokens)'
44
+ c.switch %i[exact], negatable: false
45
+
46
+ c.desc 'Include notes in output'
47
+ c.switch %i[notes], negatable: true, default_value: false
48
+
49
+ c.desc 'Include @done actions'
50
+ c.switch %i[done]
51
+
52
+ c.desc 'Output actions nested by file'
53
+ c.switch %[nest], negatable: false
54
+
55
+ c.desc 'Output actions nested by file and project'
56
+ c.switch %[omnifocus], negatable: false
57
+
58
+ c.action do |global_options, options, args|
59
+ if global_options[:add]
60
+ cmd = ['add']
61
+ cmd.push('--note') if global_options[:note]
62
+ cmd.concat(['--priority', global_options[:priority]]) if global_options[:priority]
63
+ cmd.push(NA.command_line) if NA.command_line.count > 1
64
+ cmd.unshift(*NA.globals)
65
+ exit run(cmd)
66
+ end
67
+
68
+ options[:nest] = true if options[:omnifocus]
69
+
70
+ depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
71
+ 3
72
+ else
73
+ options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
74
+ end
75
+
76
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
77
+ tags = []
78
+ options[:tagged].join(',').split(/ *, */).each do |arg|
79
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
80
+
81
+ tags.push({
82
+ tag: m['tag'].wildcard_to_rx,
83
+ comp: m['op'],
84
+ value: m['val'],
85
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
86
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
87
+ })
88
+ end
89
+
90
+ args.concat(options[:in])
91
+ if args.count.positive?
92
+ all_req = args.join(' ') !~ /[+!\-]/
93
+
94
+ tokens = []
95
+ args.each do |arg|
96
+ arg.split(/ *, */).each do |a|
97
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
98
+ tokens.push({
99
+ token: m['tok'],
100
+ required: !m['req'].nil? && m['req'] == '+',
101
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
102
+ })
103
+ end
104
+ end
105
+ end
106
+
107
+ search = nil
108
+ if options[:search]
109
+ if options[:exact]
110
+ search = options[:search].join(' ')
111
+ elsif options[:regex]
112
+ search = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
113
+ else
114
+ search = []
115
+ all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
116
+
117
+ options[:search].join(' ').split(/ /).each do |arg|
118
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
119
+ search.push({
120
+ token: m['tok'],
121
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
122
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
123
+ })
124
+ end
125
+ end
126
+ end
127
+
128
+ NA.na_tag = options[:tag] unless options[:tag].nil?
129
+ require_na = true
130
+
131
+ tag = [{ tag: NA.na_tag, value: nil }, { tag: 'done', value: nil, negate: true }]
132
+ tag.concat(tags)
133
+ files, actions, = NA.parse_actions(depth: depth,
134
+ done: options[:done],
135
+ query: tokens,
136
+ tag: tag,
137
+ search: search,
138
+ project: options[:project],
139
+ require_na: require_na)
140
+
141
+ NA.output_actions(actions, depth, files: files, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
142
+ end
143
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Show list of projects for a file'
4
+ long_desc 'Arguments will be interpreted as a query for a known todo file,
5
+ fuzzy matched. Separate directories with /, :, or a space, e.g. `na projects code/marked`'
6
+ arg_name 'QUERY', optional: true
7
+ command %i[projects] do |c|
8
+ c.desc 'Search for files X directories deep'
9
+ c.arg_name 'DEPTH'
10
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
11
+
12
+ c.desc 'Output projects as paths instead of hierarchy'
13
+ c.switch %i[p paths], negatable: false
14
+
15
+ c.action do |_global_options, options, args|
16
+ if args.count.positive?
17
+ all_req = args.join(' ') !~ /[+!-]/
18
+
19
+ tokens = [{ token: '*', required: all_req, negate: false }]
20
+ args.each do |arg|
21
+ arg.split(/ *, */).each do |a|
22
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
23
+ tokens.push({
24
+ token: m['tok'],
25
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
26
+ negate: !m['req'].nil? && m['req'] =~ /[!-]/
27
+ })
28
+ end
29
+ end
30
+ end
31
+
32
+ NA.list_projects(query: tokens, depth: options[:depth], paths: options[:paths])
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Show or install prompt hooks for the current shell'
4
+ long_desc 'Installing the prompt hook allows you to automatically
5
+ list next actions when you cd into a directory'
6
+ command %i[prompt] do |c|
7
+ c.desc 'Output the prompt hook for the current shell to STDOUT. Pass an argument to
8
+ specify a shell (zsh, bash, fish)'
9
+ c.arg_name 'SHELL', optional: true
10
+ c.command %i[show] do |s|
11
+ s.action do |_global_options, _options, args|
12
+ shell = if args.count.positive?
13
+ args[0]
14
+ else
15
+ File.basename(ENV['SHELL'])
16
+ end
17
+
18
+ case shell
19
+ when /^f/i
20
+ NA::Prompt.show_prompt_hook(:fish)
21
+ when /^z/i
22
+ NA::Prompt.show_prompt_hook(:zsh)
23
+ when /^b/i
24
+ NA::Prompt.show_prompt_hook(:bash)
25
+ end
26
+ end
27
+ end
28
+
29
+ c.desc 'Install the hook for the current shell to the appropriate startup file.'
30
+ c.arg_name 'SHELL', optional: true
31
+ c.command %i[install] do |s|
32
+ s.action do |_global_options, _options, args|
33
+ shell = if args.count.positive?
34
+ args[0]
35
+ else
36
+ File.basename(ENV['SHELL'])
37
+ end
38
+
39
+ case shell
40
+ when /^f/i
41
+ NA::Prompt.install_prompt_hook(:fish)
42
+ when /^z/i
43
+ NA::Prompt.install_prompt_hook(:zsh)
44
+ when /^b/i
45
+ NA::Prompt.install_prompt_hook(:bash)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Execute a saved search'
4
+ long_desc 'Run without argument to list saved searches'
5
+ arg_name 'SEARCH_TITLE', optional: true
6
+ command %i[saved] do |c|
7
+ c.example 'na tagged "+maybe,+priority<=3" --save maybelater', description: 'save a search called "maybelater"'
8
+ c.example 'na saved maybelater', description: 'perform the search named "maybelater"'
9
+ c.example 'na saved maybe',
10
+ description: 'perform the search named "maybelater", assuming no other searches match "maybe"'
11
+ c.example 'na maybe',
12
+ description: 'na run with no command and a single argument automatically performs a matching saved search'
13
+ c.example 'na saved', description: 'list available searches'
14
+
15
+ c.desc 'Open the saved search file in $EDITOR'
16
+ c.switch %i[e edit], negatable: false
17
+
18
+ c.desc 'Delete the specified search definition'
19
+ c.switch %i[d delete], negatable: false
20
+
21
+ c.action do |_global_options, options, args|
22
+ NA.edit_searches if options[:edit]
23
+
24
+ searches = NA.load_searches
25
+ if args.empty?
26
+ NA.notify("{bg}Saved searches stored in {bw}#{NA.database_path(file: 'saved_searches.yml')}")
27
+ NA.notify(searches.map { |k, v| "{y}#{k}: {w}#{v}" }.join("\n"), exit_code: 0)
28
+ else
29
+ NA.delete_search(args) if options[:delete]
30
+
31
+ keys = searches.keys.delete_if { |k| k !~ /#{args[0]}/ }
32
+ NA.notify("{r}Search #{args[0]} not found", exit_code: 1) if keys.empty?
33
+
34
+ key = keys[0]
35
+ cmd = Shellwords.shellsplit(searches[key])
36
+ exit run(cmd)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Find actions matching a tag'
4
+ long_desc 'Finds actions with tags matching the arguments. An action is shown if it
5
+ contains all of the tags listed. Add a + before a tag to make it required
6
+ and others optional. You can specify values using TAG=VALUE pairs.
7
+ Use <, >, and = for numeric comparisons, and *=, ^=, and $= for text comparisons.
8
+ Date comparisons use natural language (`na tagged "due<=today"`) and
9
+ are detected automatically.'
10
+ arg_name 'TAG[=VALUE]'
11
+ command %i[tagged] do |c|
12
+ c.example 'na tagged maybe', desc: 'Show all actions tagged @maybe'
13
+ c.example 'na tagged -d 3 "feature, idea"', desc: 'Show all actions tagged @feature AND @idea, recurse 3 levels'
14
+ c.example 'na tagged --or "feature, idea"', desc: 'Show all actions tagged @feature OR @idea'
15
+ c.example 'na tagged "priority>=4"', desc: 'Show actions with @priority(4) or @priority(5)'
16
+ c.example 'na tagged "due<in 2 days"', desc: 'Show actions with a due date coming up in the next 2 days'
17
+
18
+ c.desc 'Recurse to depth'
19
+ c.arg_name 'DEPTH'
20
+ c.default_value 1
21
+ c.flag %i[d depth], type: :integer, must_match: /^\d+$/
22
+
23
+ c.desc 'Show actions from a specific todo file in history. May use wildcards (* and ?)'
24
+ c.arg_name 'TODO_PATH'
25
+ c.flag %i[in]
26
+
27
+ c.desc 'Include notes in output'
28
+ c.switch %i[notes], negatable: true, default_value: false
29
+
30
+ c.desc 'Combine tags with OR, displaying actions matching ANY of the tags'
31
+ c.switch %i[o or], negatable: false
32
+
33
+ c.desc 'Show actions from a specific project'
34
+ c.arg_name 'PROJECT[/SUBPROJECT]'
35
+ c.flag %i[proj project]
36
+
37
+ c.desc 'Filter results using search terms'
38
+ c.arg_name 'QUERY'
39
+ c.flag %i[search], multiple: true
40
+
41
+ c.desc 'Search query is regular expression'
42
+ c.switch %i[regex], negatable: false
43
+
44
+ c.desc 'Search query is exact text match (not tokens)'
45
+ c.switch %i[exact], negatable: false
46
+
47
+ c.desc 'Include @done actions'
48
+ c.switch %i[done]
49
+
50
+ c.desc 'Show actions not matching tags'
51
+ c.switch %i[v invert], negatable: false
52
+
53
+ c.desc 'Save this search for future use'
54
+ c.arg_name 'TITLE'
55
+ c.flag %i[save]
56
+
57
+ c.desc 'Output actions nested by file'
58
+ c.switch %[nest], negatable: false
59
+
60
+ c.desc 'Output actions nested by file and project'
61
+ c.switch %[omnifocus], negatable: false
62
+
63
+ c.action do |global_options, options, args|
64
+ options[:nest] = true if options[:omnifocus]
65
+
66
+ if options[:save]
67
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
68
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
69
+ end
70
+
71
+ depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
72
+ 3
73
+ else
74
+ options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
75
+ end
76
+
77
+ tags = []
78
+
79
+ all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
80
+ args.join(',').split(/ *, */).each do |arg|
81
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
82
+
83
+ tags.push({
84
+ tag: m['tag'].wildcard_to_rx,
85
+ comp: m['op'],
86
+ value: m['val'],
87
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
88
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
89
+ })
90
+ end
91
+
92
+ search_for_done = false
93
+ tags.each { |tag| search_for_done = true if tag[:tag] =~ /done/ }
94
+ tags.push({ tag: 'done', value: nil, negate: true}) unless search_for_done
95
+
96
+ tokens = nil
97
+ if options[:search]
98
+ if options[:exact]
99
+ tokens = options[:search].join(' ')
100
+ elsif options[:regex]
101
+ tokens = Regexp.new(options[:search].join(' '), Regexp::IGNORECASE)
102
+ else
103
+ tokens = []
104
+ all_req = options[:search].join(' ') !~ /[+!\-]/ && !options[:or]
105
+
106
+ options[:search].join(' ').split(/ /).each do |arg|
107
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
108
+ tokens.push({
109
+ token: m['tok'],
110
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
111
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
112
+ })
113
+ end
114
+ end
115
+ end
116
+
117
+ todo = nil
118
+ if options[:in]
119
+ todo = []
120
+ options[:in].split(/ *, */).each do |a|
121
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
122
+ todo.push({
123
+ token: m['tok'],
124
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
125
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
126
+ })
127
+ end
128
+ end
129
+
130
+ files, actions, = NA.parse_actions(depth: depth,
131
+ done: options[:done],
132
+ query: todo,
133
+ search: tokens,
134
+ tag: tags,
135
+ negate: options[:invert],
136
+ project: options[:project],
137
+ require_na: false)
138
+ # regexes = tags.delete_if { |token| token[:negate] }.map { |token| token[:token] }
139
+ regexes = if tokens.is_a?(Array)
140
+ tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
141
+ else
142
+ [tokens]
143
+ end
144
+ NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
145
+ end
146
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Show list of known todo files'
4
+ long_desc 'Arguments will be interpreted as a query against which the
5
+ list of todos will be fuzzy matched. Separate directories with
6
+ /, :, or a space, e.g. `na todos code/marked`'
7
+ arg_name 'QUERY', optional: true
8
+ command %i[todos] do |c|
9
+ c.action do |_global_options, _options, args|
10
+ if args.count.positive?
11
+ all_req = args.join(' ') !~ /[+!\-]/
12
+
13
+ tokens = [{ token: '*', required: all_req, negate: false }]
14
+ args.each do |arg|
15
+ arg.split(/ *, */).each do |a|
16
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
17
+ tokens.push({
18
+ token: m['tok'],
19
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
20
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
21
+ })
22
+ end
23
+ end
24
+ end
25
+
26
+ NA.list_todos(query: tokens)
27
+ end
28
+ end
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Update an existing action'
4
+ long_desc 'Provides an easy way to complete, prioritize, and tag existing actions.
5
+
6
+ If multiple todo files are found in the current directory, a menu will
7
+ allow you to pick which file to act on.'
8
+ arg_name 'ACTION'
9
+ command %i[update] do |c|
10
+ c.example 'na update --remove na "An existing task"',
11
+ desc: 'Find "An existing task" action and remove the @na tag from it'
12
+ c.example 'na update --tag waiting "A bug I need to fix" -p 4 -n',
13
+ desc: 'Find "A bug..." action, add @waiting, add/update @priority(4), and prompt for an additional note'
14
+ c.example 'na update --archive My cool action',
15
+ desc: 'Add @done to "My cool action" and immediately move to Archive'
16
+
17
+ c.desc 'Prompt for additional notes. Input will be appended to any existing note.
18
+ If STDIN input (piped) is detected, it will be used as a note.'
19
+ c.switch %i[n note], negatable: false
20
+
21
+ c.desc 'Overwrite note instead of appending'
22
+ c.switch %i[o overwrite], negatable: false
23
+
24
+ c.desc 'Add/change a priority level 1-5'
25
+ c.arg_name 'PRIO'
26
+ c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
27
+
28
+ c.desc 'When moving task, add at [s]tart or [e]nd of target project'
29
+ c.arg_name 'POSITION'
30
+ c.flag %i[at], must_match: /^[sbea].*?$/i
31
+
32
+ c.desc 'Move action to specific project'
33
+ c.arg_name 'PROJECT'
34
+ c.flag %i[to project proj]
35
+
36
+ c.desc 'Use a known todo file, partial matches allowed'
37
+ c.arg_name 'TODO_FILE'
38
+ c.flag %i[in todo]
39
+
40
+ c.desc 'Include @done actions'
41
+ c.switch %i[done]
42
+
43
+ c.desc 'Add a tag to the action, @tag(values) allowed'
44
+ c.arg_name 'TAG'
45
+ c.flag %i[t tag], multiple: true
46
+
47
+ c.desc 'Remove a tag to the action'
48
+ c.arg_name 'TAG'
49
+ c.flag %i[r remove], multiple: true
50
+
51
+ c.desc 'Add a @done tag to action'
52
+ c.switch %i[f finish], negatable: false
53
+
54
+ c.desc 'Add a @done tag to action and move to Archive'
55
+ c.switch %i[a archive], negatable: false
56
+
57
+ c.desc 'Delete an action'
58
+ c.switch %i[delete], negatable: false
59
+
60
+ c.desc 'Specify the file to search for the task'
61
+ c.arg_name 'PATH'
62
+ c.flag %i[file]
63
+
64
+ c.desc 'Search for files X directories deep'
65
+ c.arg_name 'DEPTH'
66
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
67
+
68
+ c.desc 'Match actions containing tag. Allows value comparisons'
69
+ c.arg_name 'TAG'
70
+ c.flag %i[tagged], multiple: true
71
+
72
+ c.desc 'Act on all matches immediately (no menu)'
73
+ c.switch %i[all], negatable: false
74
+
75
+ c.desc 'Interpret search pattern as regular expression'
76
+ c.switch %i[e regex], negatable: false
77
+
78
+ c.desc 'Match pattern exactly'
79
+ c.switch %i[x exact], negatable: false
80
+
81
+ c.action do |global_options, options, args|
82
+ reader = TTY::Reader.new
83
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
84
+
85
+ action = if args.count.positive?
86
+ args.join(' ').strip
87
+ elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
88
+ options = [
89
+ %(--placeholder "Enter a task to search for"),
90
+ '--char-limit=500',
91
+ "--width=#{TTY::Screen.columns}"
92
+ ]
93
+ `gum input #{options.join(' ')}`.strip
94
+ elsif $stdin.isatty && options[:tagged].empty?
95
+ puts NA::Color.template('{bm}Enter search string:{x}')
96
+ reader.read_line(NA::Color.template('{by}> {bw}')).strip
97
+ end
98
+
99
+ if action
100
+ tokens = nil
101
+ if options[:exact]
102
+ tokens = action
103
+ elsif options[:regex]
104
+ tokens = Regexp.new(action, Regexp::IGNORECASE)
105
+ else
106
+ tokens = []
107
+ all_req = action !~ /[+!\-]/ && !options[:or]
108
+
109
+ action.split(/ /).each do |arg|
110
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
111
+ tokens.push({
112
+ token: m['tok'],
113
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
114
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
115
+ })
116
+ end
117
+ end
118
+ end
119
+
120
+ if (action.nil? || action.empty?) && options[:tagged].empty?
121
+ puts 'Empty input, cancelled'
122
+ Process.exit 1
123
+ end
124
+
125
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
126
+ tags = []
127
+ options[:tagged].join(',').split(/ *, */).each do |arg|
128
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
129
+
130
+ tags.push({
131
+ tag: m['tag'].wildcard_to_rx,
132
+ comp: m['op'],
133
+ value: m['val'],
134
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
135
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
136
+ })
137
+ end
138
+
139
+ priority = options[:priority].to_i if options[:priority]&.to_i&.positive?
140
+ add_tags = options[:tag] ? options[:tag].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
141
+ remove_tags = options[:remove] ? options[:remove].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
142
+
143
+ stdin_note = NA.stdin ? NA.stdin.split("\n") : []
144
+
145
+ line_note = if options[:note] && $stdin.isatty
146
+ puts stdin_note unless stdin_note.nil?
147
+ if TTY::Which.exist?('gum')
148
+ args = ['--placeholder "Enter a note, CTRL-d to save"']
149
+ args << '--char-limit 0'
150
+ args << '--width $(tput cols)'
151
+ `gum write #{args.join(' ')}`.strip.split("\n")
152
+ else
153
+ puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
154
+ reader.read_multiline
155
+ end
156
+ end
157
+
158
+ note = stdin_note.empty? ? [] : stdin_note
159
+ note.concat(line_note) unless line_note.nil? || line_note.empty?
160
+
161
+ target_proj = if options[:project]
162
+ options[:project]
163
+ elsif NA.cwd_is == :project
164
+ NA.cwd
165
+ else
166
+ nil
167
+ end
168
+
169
+ if options[:file]
170
+ file = File.expand_path(options[:file])
171
+ NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
172
+
173
+ targets = [file]
174
+ elsif options[:todo]
175
+ todo = []
176
+ options[:todo].split(/ *, */).each do |a|
177
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
178
+ todo.push({
179
+ token: m['tok'],
180
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
181
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
182
+ })
183
+ end
184
+ dirs = NA.match_working_dir(todo)
185
+
186
+ if dirs.count == 1
187
+ targets = [dirs[0]]
188
+ elsif dirs.count.positive?
189
+ targets = NA.select_file(dirs, multiple: true)
190
+ NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
191
+ else
192
+ NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
193
+
194
+ end
195
+ else
196
+ files = NA.find_files(depth: options[:depth])
197
+ NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
198
+
199
+ targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
200
+ NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
201
+
202
+ end
203
+
204
+ if options[:archive]
205
+ options[:finish] = true
206
+ options[:project] = 'Archive'
207
+ end
208
+
209
+ NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
210
+
211
+ targets.each do |target|
212
+ NA.update_action(target, tokens,
213
+ priority: priority,
214
+ add_tag: add_tags,
215
+ remove_tag: remove_tags,
216
+ finish: options[:finish],
217
+ project: target_proj,
218
+ delete: options[:delete],
219
+ note: note,
220
+ overwrite: options[:overwrite],
221
+ tagged: tags,
222
+ all: options[:all],
223
+ done: options[:done],
224
+ append: append)
225
+ end
226
+ end
227
+ end