na 1.2.27 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1ae4b30284b89cff9f9e5469ec923668c2b314e907b9f8c398b6b6cc46dee37
4
- data.tar.gz: 4b4b84c3b995e38a1d02e487129324316a3949d36003418b09602a214a2d560e
3
+ metadata.gz: a331d16e9572f0a63e251c2de46348880b73dbce47838968ac9ac987e6a053f5
4
+ data.tar.gz: dec5319c89b04f65eb039867a5e01f6e1906c7287af2542bc7feab40f93dbf97
5
5
  SHA512:
6
- metadata.gz: f1d145af86c822a521d991ff1dcc2e759a96a88d86b317249ed48eba1e60d1a5425f91c09d5a452418e75162b249fc1c55a74f3dc9ba2d6736dbe2dd3489d23a
7
- data.tar.gz: 0aae383f45f4f73e96d15dd24d1e0002b2dcaca77ebf5e6dd7a0a6d956f3233c01e959497b8f60cd328c6d0a690f2575583f7760cb433a9ea5a60d33268425f6
6
+ metadata.gz: 4ac0842dbf415b87ffa200662eea10ac9fd382406d2e953342f7c81777504e05710cf6bf622fc831309d58178c8d1622103713b82f81bfe5df07001366fb5058
7
+ data.tar.gz: 975091dac71d91659df9c1c9b69722aad42cb74a69d6a030b7a2a335d0273925e72e87cce0afb9cc4507d7c5f376bf304a6f8a806dbeb25b7d03ff259e8031a1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ### 1.2.28
2
+
3
+ 2023-08-21 11:01
4
+
1
5
  ### 1.2.27
2
6
 
3
7
  2023-08-21 10:58
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.27)
4
+ na (1.2.28)
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.27
12
+ The current version of `na` is 1.2.28
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.27
80
+ 1.2.28
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -0,0 +1,183 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Add a new next action'
4
+ long_desc 'Provides an easy way to store todos while you work. Add quick
5
+ reminders and (if you set up Prompt Hooks) they\'ll automatically display
6
+ next time you enter the directory.
7
+
8
+ If multiple todo files are found in the current directory, a menu will
9
+ allow you to pick to which file the action gets added.'
10
+ arg_name 'ACTION'
11
+ command :add do |c|
12
+ c.example 'na add "A cool feature I thought of @idea"', desc: 'Add a new action to the Inbox, including a tag'
13
+ c.example 'na add "A bug I need to fix" -p 4 -n',
14
+ desc: 'Add a new action to the Inbox, set its @priority to 4, and prompt for an additional note.'
15
+ c.example 'na add "An action item (with a note)"',
16
+ desc: 'A parenthetical at the end of an action is interpreted as a note'
17
+
18
+ c.desc 'Prompt for additional notes. STDIN input (piped) will be treated as a note if present.'
19
+ c.switch %i[n note], negatable: false
20
+
21
+ c.desc 'Add a priority level 1-5'
22
+ c.arg_name 'PRIO'
23
+ c.flag %i[p priority], must_match: /[1-5]/, type: :integer, default_value: 0
24
+
25
+ c.desc 'Add action to specific project'
26
+ c.arg_name 'PROJECT'
27
+ c.default_value 'Inbox'
28
+ c.flag %i[to project proj]
29
+
30
+ c.desc 'Add task at [s]tart or [e]nd of target project'
31
+ c.arg_name 'POSITION'
32
+ c.flag %i[at], must_match: /^[sbea].*?$/i
33
+
34
+ c.desc 'Add to a known todo file, partial matches allowed'
35
+ c.arg_name 'TODO_FILE'
36
+ c.flag %i[in todo]
37
+
38
+ c.desc 'Use a tag other than the default next action tag'
39
+ c.arg_name 'TAG'
40
+ c.flag %i[t tag]
41
+
42
+ c.desc 'Don\'t add next action tag to new entry'
43
+ c.switch %i[x], negatable: false
44
+
45
+ c.desc 'Specify the file to which the task should be added'
46
+ c.arg_name 'PATH'
47
+ c.flag %i[f file]
48
+
49
+ c.desc 'Mark task as @done with date'
50
+ c.switch %i[finish done], negatable: false
51
+
52
+ c.desc 'Search for files X directories deep'
53
+ c.arg_name 'DEPTH'
54
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
55
+
56
+ c.action do |global_options, options, args|
57
+ reader = TTY::Reader.new
58
+ append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/
59
+
60
+ if NA.global_file
61
+ target = File.expand_path(NA.global_file)
62
+ unless File.exist?(target)
63
+ res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
64
+ if res
65
+ basename = File.basename(target, ".#{NA.extension}")
66
+ NA.create_todo(target, basename, template: global_options[:template])
67
+ else
68
+ puts NA::Color.template('{r}Cancelled{x}')
69
+ Process.exit 1
70
+ end
71
+ end
72
+ elsif options[:file]
73
+ target = File.expand_path(options[:file])
74
+ unless File.exist?(target)
75
+ res = NA.yn(NA::Color.template('{by}Specified file not found, create it'), default: true)
76
+ if res
77
+ basename = File.basename(target, ".#{NA.extension}")
78
+ NA.create_todo(target, basename, template: global_options[:template])
79
+ else
80
+ puts NA::Color.template('{r}Cancelled{x}')
81
+ Process.exit 1
82
+ end
83
+ end
84
+ elsif options[:todo]
85
+ todo = []
86
+ all_req = options[:todo] !~ /[+!\-]/
87
+ options[:todo].split(/ *, */).each do |a|
88
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
89
+ todo.push({
90
+ token: m['tok'],
91
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
92
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
93
+ })
94
+ end
95
+ dirs = NA.match_working_dir(todo)
96
+ if dirs.count.positive?
97
+ target = dirs[0]
98
+ else
99
+ todo = "#{options[:todo].sub(/#{NA.extension}$/, '')}.#{NA.extension}"
100
+ target = File.expand_path(todo)
101
+ unless File.exist?(target)
102
+
103
+ res = NA.yn(NA::Color.template("{by}Specified file not found, create #{todo}"), default: true)
104
+ NA.notify('{r}Cancelled{x}', exit_code: 1) unless res
105
+
106
+ basename = File.basename(target, ".#{NA.extension}")
107
+ NA.create_todo(target, basename, template: global_options[:template])
108
+ end
109
+
110
+ end
111
+ else
112
+ files = NA.find_files(depth: options[:depth])
113
+ if files.count.zero?
114
+ res = NA.yn(NA::Color.template('{by}No todo file found, create one'), default: true)
115
+ if res
116
+ basename = File.expand_path('.').split('/').last
117
+ target = "#{basename}.#{NA.extension}"
118
+ NA.create_todo(target, basename, template: global_options[:template])
119
+ files = NA.find_files(depth: 1)
120
+ end
121
+ end
122
+ target = files.count > 1 ? NA.select_file(files) : files[0]
123
+ NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive? && File.exist?(target)
124
+
125
+ end
126
+
127
+ action = if args.count.positive?
128
+ args.join(' ').strip
129
+ elsif $stdin.isatty && TTY::Which.exist?('gum')
130
+ `gum input --placeholder "Enter a task" --char-limit=500 --width=#{TTY::Screen.columns}`.strip
131
+ elsif $stdin.isatty
132
+ puts NA::Color.template('{bm}Enter task:{x}')
133
+ reader.read_line(NA::Color.template('{by}> {bw}')).strip
134
+ end
135
+
136
+ if action.nil? || action.empty?
137
+ puts 'Empty input, cancelled'
138
+ Process.exit 1
139
+ end
140
+
141
+ if options[:priority]&.to_i&.positive?
142
+ action = "#{action.gsub(/@priority\(\d+\)/, '')} @priority(#{options[:priority]})"
143
+ end
144
+
145
+ note_rx = /^(.+) \((.*?)\)$/
146
+ split_note = if action =~ note_rx
147
+ n = Regexp.last_match(2)
148
+ action.sub!(note_rx, '\1').strip!
149
+ n
150
+ end
151
+
152
+ na_tag = NA.na_tag
153
+ if options[:x]
154
+ na_tag = ''
155
+ else
156
+ na_tag = options[:tag] unless options[:tag].nil?
157
+ na_tag = " @#{na_tag}"
158
+ end
159
+
160
+ action = "#{action.gsub(/#{na_tag}\b/, '')}#{na_tag}"
161
+
162
+ stdin_note = NA.stdin ? NA.stdin.split("\n") : []
163
+
164
+ line_note = if options[:note] && $stdin.isatty
165
+ puts stdin_note unless stdin_note.nil?
166
+ if TTY::Which.exist?('gum')
167
+ args = ['--placeholder "Enter additional note, CTRL-d to save"']
168
+ args << '--char-limit 0'
169
+ args << '--width $(tput cols)'
170
+ `gum write #{args.join(' ')}`.strip.split("\n")
171
+ else
172
+ puts NA::Color.template('{bm}Enter a note, {bw}CTRL-d{bm} to end editing{bw}')
173
+ reader.read_multiline
174
+ end
175
+ end
176
+
177
+ note = stdin_note.empty? ? [] : stdin_note
178
+ note.concat(split_note) unless split_note.nil?
179
+ note.concat(line_note) unless line_note.nil?
180
+
181
+ NA.add_action(target, options[:project], action, note, finish: options[:finish], append: append)
182
+ end
183
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Mark an action as @done and archive'
4
+ arg_name 'ACTION'
5
+ command %i[archive] do |c|
6
+ c.example 'na archive "An existing task"',
7
+ desc: 'Find "An existing task", mark @done if needed, and move to archive'
8
+
9
+ c.desc 'Prompt for additional notes. Input will be appended to any existing note.
10
+ If STDIN input (piped) is detected, it will be used as a note.'
11
+ c.switch %i[n note], negatable: false
12
+
13
+ c.desc 'Overwrite note instead of appending'
14
+ c.switch %i[o overwrite], negatable: false
15
+
16
+ c.desc 'Archive all done tasks'
17
+ c.switch %i[done], negatable: false
18
+
19
+ c.desc 'Specify the file to search for the task'
20
+ c.arg_name 'PATH'
21
+ c.flag %i[file]
22
+
23
+ c.desc 'Search for files X directories deep'
24
+ c.arg_name 'DEPTH'
25
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
26
+
27
+ c.desc 'Match actions containing tag. Allows value comparisons'
28
+ c.arg_name 'TAG'
29
+ c.flag %i[tagged], multiple: true
30
+
31
+ c.desc 'Act on all matches immediately (no menu)'
32
+ c.switch %i[all], negatable: false
33
+
34
+ c.desc 'Interpret search pattern as regular expression'
35
+ c.switch %i[e regex], negatable: false
36
+
37
+ c.desc 'Match pattern exactly'
38
+ c.switch %i[x exact], negatable: false
39
+
40
+ c.action do |global, options, args|
41
+ if options[:done]
42
+ options[:tagged] = ['done']
43
+ options[:all] = true
44
+ end
45
+
46
+ options[:done] = true
47
+ options[:finish] = true
48
+ options[:project] = 'Archive'
49
+ options[:archive] = true
50
+ options[:a] = true
51
+
52
+ cmd = commands[:update]
53
+ action = cmd.send(:get_action, nil)
54
+ action.call(global, options, args)
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Display the changelog'
4
+ command %i[changes changelog] do |c|
5
+ c.action do |_, _, _|
6
+ changelog = File.expand_path(File.join(File.dirname(__FILE__), '..', 'CHANGELOG.md'))
7
+ pagers = [
8
+ 'mdless',
9
+ 'mdcat',
10
+ 'bat',
11
+ ENV['PAGER'],
12
+ 'less -FXr',
13
+ ENV['GIT_PAGER'],
14
+ 'more -r'
15
+ ]
16
+ pager = pagers.find { |cmd| TTY::Which.exist?(cmd.split.first) }
17
+ system %(#{pager} "#{changelog}")
18
+ end
19
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Find and mark an action as @done'
4
+ arg_name 'ACTION'
5
+ command %i[complete finish] do |c|
6
+ c.example 'na complete "An existing task"',
7
+ desc: 'Find "An existing task" and mark @done'
8
+ c.example 'na finish "An existing task"',
9
+ desc: 'Alias for complete'
10
+
11
+ c.desc 'Prompt for additional notes. Input will be appended to any existing note.
12
+ If STDIN input (piped) is detected, it will be used as a note.'
13
+ c.switch %i[n note], negatable: false
14
+
15
+ c.desc 'Overwrite note instead of appending'
16
+ c.switch %i[o overwrite], negatable: false
17
+
18
+ c.desc 'Add a @done tag to action and move to Archive'
19
+ c.switch %i[a archive], negatable: false
20
+
21
+ c.desc 'Specify the file to search for the task'
22
+ c.arg_name 'PATH'
23
+ c.flag %i[file]
24
+
25
+ c.desc 'Search for files X directories deep'
26
+ c.arg_name 'DEPTH'
27
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
28
+
29
+ c.desc 'Match actions containing tag. Allows value comparisons'
30
+ c.arg_name 'TAG'
31
+ c.flag %i[tagged], multiple: true
32
+
33
+ c.desc 'Act on all matches immediately (no menu)'
34
+ c.switch %i[all], negatable: false
35
+
36
+ c.desc 'Interpret search pattern as regular expression'
37
+ c.switch %i[e regex], negatable: false
38
+
39
+ c.desc 'Match pattern exactly'
40
+ c.switch %i[x exact], negatable: false
41
+
42
+ c.action do |global, options, args|
43
+ options[:finish] = true
44
+ options[:f] = true
45
+ options[:project] = 'Archive' if options[:archive]
46
+
47
+ cmd = commands[:update]
48
+ action = cmd.send(:get_action, nil)
49
+ action.call(global, options, args)
50
+ end
51
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Open a todo file in the default editor'
4
+ long_desc 'Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
5
+ If more than one todo file is found, a menu is displayed.'
6
+ command %i[edit] do |c|
7
+ c.example 'na edit', desc: 'Open the main todo file in the default editor'
8
+ c.example 'na edit -d 3 -a vim', desc: 'Display a menu of all todo files three levels deep from the
9
+ current directory, open selection in vim.'
10
+
11
+ c.desc 'Recurse to depth'
12
+ c.arg_name 'DEPTH'
13
+ c.default_value 1
14
+ c.flag %i[d depth], type: :integer, must_match: /^\d+$/
15
+
16
+ c.desc 'Specify an editor CLI'
17
+ c.arg_name 'EDITOR'
18
+ c.flag %i[e editor]
19
+
20
+ c.desc 'Specify a Mac app'
21
+ c.arg_name 'EDITOR'
22
+ c.flag %i[a app]
23
+
24
+ c.action do |global_options, options, args|
25
+ depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
26
+ 3
27
+ else
28
+ options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
29
+ end
30
+ files = NA.find_files(depth: depth)
31
+ files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
32
+
33
+ file = if files.count > 1
34
+ NA.select_file(files)
35
+ else
36
+ files[0]
37
+ end
38
+
39
+ if options[:editor]
40
+ system options[:editor], file
41
+ else
42
+ NA.edit_file(file: file, app: options[:app])
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Find actions matching a search pattern'
4
+ long_desc 'Search tokens are separated by spaces. Actions matching all tokens in the pattern will be shown
5
+ (partial matches allowed). Add a + before a token to make it required, e.g. `na find +feature +maybe`,
6
+ add a - or ! to ignore matches containing that token.'
7
+ arg_name 'PATTERN'
8
+ command %i[find grep] do |c|
9
+ c.example 'na find feature idea swift', desc: 'Find all actions containing feature, idea, and swift'
10
+ c.example 'na find feature idea -swift', desc: 'Find all actions containing feature and idea but NOT swift'
11
+ c.example 'na find -x feature idea', desc: 'Find all actions containing the exact text "feature idea"'
12
+
13
+ c.desc 'Interpret search pattern as regular expression'
14
+ c.switch %i[e regex], negatable: false
15
+
16
+ c.desc 'Match pattern exactly'
17
+ c.switch %i[x exact], negatable: false
18
+
19
+ c.desc 'Recurse to depth'
20
+ c.arg_name 'DEPTH'
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 search tokens with OR, displaying actions matching ANY of the terms'
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 'Match actions containing tag. Allows value comparisons'
38
+ c.arg_name 'TAG'
39
+ c.flag %i[tagged], multiple: true
40
+
41
+ c.desc 'Include @done actions'
42
+ c.switch %i[done]
43
+
44
+ c.desc 'Show actions not matching search pattern'
45
+ c.switch %i[v invert], negatable: false
46
+
47
+ c.desc 'Save this search for future use'
48
+ c.arg_name 'TITLE'
49
+ c.flag %i[save]
50
+
51
+ c.desc 'Output actions nested by file'
52
+ c.switch %[nest], negatable: false
53
+
54
+ c.desc 'Output actions nested by file and project'
55
+ c.switch %[omnifocus], negatable: false
56
+
57
+ c.action do |global_options, options, args|
58
+ options[:nest] = true if options[:omnifocus]
59
+
60
+ if options[:save]
61
+ title = options[:save].gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
62
+ NA.save_search(title, "#{NA.command_line.join(' ').sub(/ --save[= ]*\S+/, '').split(' ').map { |t| %("#{t}") }.join(' ')}")
63
+ end
64
+
65
+
66
+ depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
67
+ 3
68
+ else
69
+ options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
70
+ end
71
+
72
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
73
+ tags = []
74
+ options[:tagged].join(',').split(/ *, */).each do |arg|
75
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
76
+
77
+ tags.push({
78
+ tag: m['tag'].wildcard_to_rx,
79
+ comp: m['op'],
80
+ value: m['val'],
81
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
82
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
83
+ })
84
+ end
85
+
86
+ tokens = nil
87
+ if options[:exact]
88
+ tokens = args.join(' ')
89
+ elsif options[:regex]
90
+ tokens = Regexp.new(args.join(' '), Regexp::IGNORECASE)
91
+ else
92
+ tokens = []
93
+ all_req = args.join(' ') !~ /[+!\-]/ && !options[:or]
94
+
95
+ args.join(' ').split(/ /).each do |arg|
96
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
97
+ tokens.push({
98
+ token: m['tok'],
99
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
100
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
101
+ })
102
+ end
103
+ end
104
+
105
+ todo = nil
106
+ if options[:in]
107
+ todo = []
108
+ options[:in].split(/ *, */).each do |a|
109
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
110
+ todo.push({
111
+ token: m['tok'],
112
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
113
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
114
+ })
115
+ end
116
+ end
117
+
118
+ files, actions, = NA.parse_actions(depth: depth,
119
+ done: options[:done],
120
+ query: todo,
121
+ search: tokens,
122
+ tag: tags,
123
+ negate: options[:invert],
124
+ regex: options[:regex],
125
+ project: options[:project],
126
+ require_na: false)
127
+ regexes = if tokens.is_a?(Array)
128
+ tokens.delete_if { |token| token[:negate] }.map { |token| token[:token] }
129
+ else
130
+ [tokens]
131
+ end
132
+
133
+ NA.output_actions(actions, depth, files: files, regexes: regexes, notes: options[:notes], nest: options[:nest], nest_projects: options[:omnifocus])
134
+ end
135
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc 'Create a new todo file in the current directory'
4
+ arg_name 'PROJECT', optional: true
5
+ command %i[init create] do |c|
6
+ c.example 'na init', desc: 'Generate a new todo file, prompting for project name'
7
+ c.example 'na init warpspeed', desc: 'Generate a new todo for a project called warpspeed'
8
+
9
+ c.action do |global_options, _options, args|
10
+ reader = TTY::Reader.new
11
+ if args.count.positive?
12
+ project = args.join(' ')
13
+ elsif
14
+ project = File.expand_path('.').split('/').last
15
+ project = reader.read_line(NA::Color.template('{y}Project name {bw}> {x}'), value: project).strip if $stdin.isatty
16
+ end
17
+
18
+ target = "#{project}.#{NA.extension}"
19
+
20
+ if File.exist?(target)
21
+ res = NA.yn(NA::Color.template("{r}File {bw}#{target}{r} already exists, overwrite it"), default: false)
22
+ Process.exit 1 unless res
23
+
24
+ end
25
+
26
+ NA.create_todo(target, project, template: global_options[:template])
27
+ end
28
+ end
@@ -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
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.27'
2
+ VERSION = '1.2.28'
3
3
  end
data/src/_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 <!--VER-->1.2.26<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.27<!--END VER-->.
13
13
 
14
14
  `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.
15
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: na
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.27
4
+ version: 1.2.28
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -206,6 +206,20 @@ files:
206
206
  - README.md
207
207
  - README.rdoc
208
208
  - Rakefile
209
+ - bin/commands/add.rb
210
+ - bin/commands/archive.rb
211
+ - bin/commands/changes.rb
212
+ - bin/commands/complete.rb
213
+ - bin/commands/edit.rb
214
+ - bin/commands/find.rb
215
+ - bin/commands/init.rb
216
+ - bin/commands/next.rb
217
+ - bin/commands/projects.rb
218
+ - bin/commands/prompt.rb
219
+ - bin/commands/saved.rb
220
+ - bin/commands/tagged.rb
221
+ - bin/commands/todos.rb
222
+ - bin/commands/update.rb
209
223
  - bin/na
210
224
  - lib/na.rb
211
225
  - lib/na/action.rb