na 1.2.27 → 1.2.28

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: 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