na 1.2.32 → 1.2.33

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: 440413f1b22a8745d119a8b55098ad3fd9c36096f930ad5f25d6091a9c9cb496
4
- data.tar.gz: 02e42b27ea06069b669fdded2929b4351990b9e3be35add61cd734a50d2fa28a
3
+ metadata.gz: d77588ce09a5a3cfc680b1866081dba5f56e75c8b51301cf0e1efee60c32f8f7
4
+ data.tar.gz: 06d73082ad5371cda4760df90c929c74963d76843b7b979dde59e46fdb933389
5
5
  SHA512:
6
- metadata.gz: 3fae03d1566494bff4c792fa6921d975e94e9984688873e05e8936e97a3a0dc1f2134f6677218f66d53d6be87481100835fd66d4e30a27e1db42bf3cb09805c8
7
- data.tar.gz: 3d3c6482675e0c880c51d7f0ea460225c869376db8d84a245c296ec23dceabd9bf481387fac05c06c88d73e914529beb4effd540ea90b9d558e30be1e1ffcc9d
6
+ metadata.gz: 07ee85d5f2f95ba2542ff15ca3b6c3736bd85d4a5b1bd9c76a7099296b3976d9b0e2478d6e2b8b84a824787c9fc05050b89153c316b7878c9743e3cd4cad3a8a
7
+ data.tar.gz: 0af252e545f390a539279747f7d89e87b5efaceb82db3adfa57ce037cd6e4b139e18aa2d90340dec14d9eb90f78c3b8963c5f7e92d59258f1711ea1dbb76462c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### 1.2.33
2
+
3
+ 2023-08-29 13:45
4
+
5
+ #### NEW
6
+
7
+ - `na update --edit` flag to open a single task in your default $EDITOR for modification and notes
8
+ - Expand natural language dates in recognized date-based tags (@due, @start, @deferred, etc.)
9
+ - Edit command now opens editor on matched action, previous edit
10
+
11
+ #### IMPROVED
12
+
13
+ - Allow multiple add or remove tags using comma-separated list
14
+
15
+ #### FIXED
16
+
17
+ - `na update --restore` irregularities
18
+ - Failing if using gum to input search string
19
+
1
20
  ### 1.2.32
2
21
 
3
22
  2023-08-29 10:59
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- na (1.2.32)
4
+ na (1.2.33)
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.32
12
+ The current version of `na` is 1.2.33
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.32
80
+ 1.2.33
81
81
 
82
82
  GLOBAL OPTIONS
83
83
  -a, --add - Add a next action (deprecated, for backwards compatibility)
@@ -103,12 +103,13 @@ COMMANDS
103
103
  changes, changelog - Display the changelog
104
104
  complete, finish - Find and mark an action as @done
105
105
  completed, finished - Display completed actions
106
- edit - Open a todo file in the default editor
106
+ edit - Edit an existing action
107
107
  find, grep - Find actions matching a search pattern
108
108
  help - Shows a list of commands or help for one command
109
109
  init, create - Create a new todo file in the current directory
110
110
  initconfig - Initialize the config file using current global options
111
111
  next, show - Show next actions
112
+ open - Open a todo file in the default editor
112
113
  projects - Show list of projects for a file
113
114
  prompt - Show or install prompt hooks for the current shell
114
115
  saved - Execute a saved search
@@ -170,28 +171,28 @@ EXAMPLES
170
171
 
171
172
  ```
172
173
  NAME
173
- edit - Open a todo file in the default editor
174
+ edit - Edit an existing action
174
175
 
175
176
  SYNOPSIS
176
177
 
177
- na [global options] edit [command options]
178
+ na [global options] edit [command options] ACTION
178
179
 
179
180
  DESCRIPTION
180
- Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim). If more than one todo file is found, a menu is displayed.
181
+ Open a matching action in your default $EDITOR. If multiple todo files are found in the current directory, a menu will allow you to pick which file to act on. Natural language dates are expanded in known date-based tags.
181
182
 
182
183
  COMMAND OPTIONS
183
- -a, --app=EDITOR - Specify a Mac app (default: none)
184
- -d, --depth=DEPTH - Recurse to depth (default: 1)
185
- -e, --editor=EDITOR - Specify an editor CLI (default: none)
186
-
187
- EXAMPLES
188
-
189
- # Open the main todo file in the default editor
190
- na edit
191
-
192
- # Display a menu of all todo files three levels deep from the
193
- current directory, open selection in vim.
194
- na edit -d 3 -a vim
184
+ -d, --depth=DEPTH - Search for files X directories deep (default: 1)
185
+ --[no-]done - Include @done actions
186
+ -e, --regex - Interpret search pattern as regular expression
187
+ --file=PATH - Specify the file to search for the task (default: none)
188
+ --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
189
+ --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
190
+ -x, --exact - Match pattern exactly
191
+
192
+ EXAMPLE
193
+
194
+ # Find "An existing task" action and open it for editing
195
+ na edit "An existing task"
195
196
  ```
196
197
 
197
198
  ##### find
@@ -484,15 +485,16 @@ COMMAND OPTIONS
484
485
  --delete - Delete an action
485
486
  --[no-]done - Include @done actions
486
487
  -e, --regex - Interpret search pattern as regular expression
488
+ --edit - Open action in editor (vim). Natural language dates will be parsed and converted in date-based tags.
487
489
  -f, --finish - Add a @done tag to action
488
490
  --file=PATH - Specify the file to search for the task (default: none)
489
491
  --in, --todo=TODO_FILE - Use a known todo file, partial matches allowed (default: none)
490
492
  -n, --note - Prompt for additional notes. Input will be appended to any existing note. If STDIN input (piped) is detected, it will be used as a note.
491
493
  -o, --overwrite - Overwrite note instead of appending
492
494
  -p, --priority=PRIO - Add/change a priority level 1-5 (default: 0)
493
- -r, --remove=TAG - Remove a tag to the action (may be used more than once, default: none)
495
+ -r, --remove=TAG - Remove a tag from the action, use multiple times or combine multiple tags with a comma, wildcards (* and ?) allowed (may be used more than once, default: none)
494
496
  --restore - Remove @done tag from action
495
- -t, --tag=TAG - Add a tag to the action, @tag(values) allowed (may be used more than once, default: none)
497
+ -t, --tag=TAG - Add a tag to the action, @tag(values) allowed, use multiple times or combine multiple tags with a comma (may be used more than once, default: none)
496
498
  --tagged=TAG - Match actions containing tag. Allows value comparisons (may be used more than once, default: none)
497
499
  --to, --project, --proj=PROJECT - Move action to specific project (default: none)
498
500
  -x, --exact - Match pattern exactly
data/bin/commands/edit.rb CHANGED
@@ -2,46 +2,149 @@
2
2
 
3
3
  class App
4
4
  extend GLI::App
5
- desc 'Open a todo file in the default editor'
6
- long_desc 'Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
7
- If more than one todo file is found, a menu is displayed.'
5
+ desc 'Edit an existing action'
6
+ long_desc 'Open a matching action in your default $EDITOR.
7
+
8
+ If multiple todo files are found in the current directory, a menu will
9
+ allow you to pick which file to act on.
10
+
11
+ Natural language dates are expanded in known date-based tags.'
12
+ arg_name 'ACTION'
8
13
  command %i[edit] do |c|
9
- c.example 'na edit', desc: 'Open the main todo file in the default editor'
10
- c.example 'na edit -d 3 -a vim', desc: 'Display a menu of all todo files three levels deep from the
11
- current directory, open selection in vim.'
14
+ c.example 'na edit "An existing task"',
15
+ desc: 'Find "An existing task" action and open it for editing'
16
+
17
+ c.desc 'Use a known todo file, partial matches allowed'
18
+ c.arg_name 'TODO_FILE'
19
+ c.flag %i[in todo]
20
+
21
+ c.desc 'Include @done actions'
22
+ c.switch %i[done]
12
23
 
13
- c.desc 'Recurse to depth'
24
+ c.desc 'Specify the file to search for the task'
25
+ c.arg_name 'PATH'
26
+ c.flag %i[file]
27
+
28
+ c.desc 'Search for files X directories deep'
14
29
  c.arg_name 'DEPTH'
15
- c.default_value 1
16
- c.flag %i[d depth], type: :integer, must_match: /^\d+$/
30
+ c.flag %i[d depth], must_match: /^[1-9]$/, type: :integer, default_value: 1
31
+
32
+ c.desc 'Match actions containing tag. Allows value comparisons'
33
+ c.arg_name 'TAG'
34
+ c.flag %i[tagged], multiple: true
17
35
 
18
- c.desc 'Specify an editor CLI'
19
- c.arg_name 'EDITOR'
20
- c.flag %i[e editor]
36
+ c.desc 'Interpret search pattern as regular expression'
37
+ c.switch %i[e regex], negatable: false
21
38
 
22
- c.desc 'Specify a Mac app'
23
- c.arg_name 'EDITOR'
24
- c.flag %i[a app]
39
+ c.desc 'Match pattern exactly'
40
+ c.switch %i[x exact], negatable: false
25
41
 
26
42
  c.action do |global_options, options, args|
27
- depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
28
- 3
29
- else
30
- options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
31
- end
32
- files = NA.find_files(depth: depth)
33
- files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
34
-
35
- file = if files.count > 1
36
- NA.select_file(files)
37
- else
38
- files[0]
39
- end
40
-
41
- if options[:editor]
42
- system options[:editor], file
43
+ options[:edit] = true
44
+ action = if args.count.positive?
45
+ args.join(' ').strip
46
+ elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
47
+ opts = [
48
+ %(--placeholder "Enter a task to search for"),
49
+ '--char-limit=500',
50
+ "--width=#{TTY::Screen.columns}"
51
+ ]
52
+ `gum input #{opts.join(' ')}`.strip
53
+ elsif $stdin.isatty && options[:tagged].empty?
54
+ puts NA::Color.template('{bm}Enter search string:{x}')
55
+ reader.read_line(NA::Color.template('{by}> {bw}')).strip
56
+ end
57
+
58
+ NA.notify('{br}Empty input{x}', exit_code: 1) unless action
59
+
60
+ if action
61
+ tokens = nil
62
+ if options[:exact]
63
+ tokens = action
64
+ elsif options[:regex]
65
+ tokens = Regexp.new(action, Regexp::IGNORECASE)
66
+ else
67
+ tokens = []
68
+ all_req = action !~ /[+!\-]/ && !options[:or]
69
+
70
+ action.split(/ /).each do |arg|
71
+ m = arg.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
72
+ tokens.push({
73
+ token: m['tok'],
74
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
75
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
76
+ })
77
+ end
78
+ end
79
+ end
80
+
81
+ if (action.nil? || action.empty?) && options[:tagged].empty?
82
+ NA.notify('{br}Empty input, cancelled{x}', exit_code: 1)
83
+ end
84
+
85
+ all_req = options[:tagged].join(' ') !~ /[+!\-]/ && !options[:or]
86
+ tags = []
87
+ options[:tagged].join(',').split(/ *, */).each do |arg|
88
+ m = arg.match(/^(?<req>[+\-!])?(?<tag>[^ =<>$\^]+?)(?:(?<op>[=<>]{1,2}|[*$\^]=)(?<val>.*?))?$/)
89
+
90
+ tags.push({
91
+ tag: m['tag'].wildcard_to_rx,
92
+ comp: m['op'],
93
+ value: m['val'],
94
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
95
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
96
+ })
97
+ end
98
+
99
+ target_proj = if NA.cwd_is == :project
100
+ NA.cwd
101
+ else
102
+ nil
103
+ end
104
+
105
+ if options[:file]
106
+ file = File.expand_path(options[:file])
107
+ NA.notify('{r}File not found', exit_code: 1) unless File.exist?(file)
108
+
109
+ targets = [file]
110
+ elsif options[:todo]
111
+ todo = []
112
+ options[:todo].split(/ *, */).each do |a|
113
+ m = a.match(/^(?<req>[+\-!])?(?<tok>.*?)$/)
114
+ todo.push({
115
+ token: m['tok'],
116
+ required: all_req || (!m['req'].nil? && m['req'] == '+'),
117
+ negate: !m['req'].nil? && m['req'] =~ /[!\-]/
118
+ })
119
+ end
120
+ dirs = NA.match_working_dir(todo)
121
+
122
+ if dirs.count == 1
123
+ targets = [dirs[0]]
124
+ elsif dirs.count.positive?
125
+ targets = NA.select_file(dirs, multiple: true)
126
+ NA.notify('{r}Cancelled', exit_code: 1) unless targets && targets.count.positive?
127
+ else
128
+ NA.notify('{r}Todo not found', exit_code: 1) unless targets && targets.count.positive?
129
+
130
+ end
43
131
  else
44
- NA.edit_file(file: file, app: options[:app])
132
+ files = NA.find_files(depth: options[:depth])
133
+ NA.notify('{r}No todo file found', exit_code: 1) if files.count.zero?
134
+
135
+ targets = files.count > 1 ? NA.select_file(files, multiple: true) : [files[0]]
136
+ NA.notify('{r}Cancelled{x}', exit_code: 1) unless files.count.positive?
137
+
138
+ end
139
+
140
+ NA.notify('{r}No search terms provided', exit_code: 1) if tokens.nil? && options[:tagged].empty?
141
+
142
+ targets.each do |target|
143
+ NA.update_action(target, tokens,
144
+ done: options[:done],
145
+ edit: options[:edit],
146
+ project: target_proj,
147
+ tagged: tags)
45
148
  end
46
149
  end
47
150
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class App
4
+ extend GLI::App
5
+ desc 'Open a todo file in the default editor'
6
+ long_desc 'Let the system choose the defualt, (e.g. TaskPaper), or specify a command line utility (e.g. vim).
7
+ If more than one todo file is found, a menu is displayed.'
8
+ command %i[open] do |c|
9
+ c.example 'na open', desc: 'Open the main todo file in the default editor'
10
+ c.example 'na open -d 3 -a vim', desc: 'Display a menu of all todo files three levels deep from the
11
+ current directory, open selection in vim.'
12
+
13
+ c.desc 'Recurse to depth'
14
+ c.arg_name 'DEPTH'
15
+ c.default_value 1
16
+ c.flag %i[d depth], type: :integer, must_match: /^\d+$/
17
+
18
+ c.desc 'Specify an editor CLI'
19
+ c.arg_name 'EDITOR'
20
+ c.flag %i[e editor]
21
+
22
+ c.desc 'Specify a Mac app'
23
+ c.arg_name 'EDITOR'
24
+ c.flag %i[a app]
25
+
26
+ c.action do |global_options, options, args|
27
+ depth = if global_options[:recurse] && options[:depth].nil? && global_options[:depth] == 1
28
+ 3
29
+ else
30
+ options[:depth].nil? ? global_options[:depth].to_i : options[:depth].to_i
31
+ end
32
+ files = NA.find_files(depth: depth)
33
+ files.delete_if { |f| f !~ /.*?(#{args.join('|')}).*?.#{NA.extension}/ } if args.count.positive?
34
+
35
+ file = if files.count > 1
36
+ NA.select_file(files)
37
+ else
38
+ files[0]
39
+ end
40
+
41
+ if options[:editor]
42
+ system options[:editor], file
43
+ else
44
+ NA.edit_file(file: file, app: options[:app])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -42,11 +42,12 @@ class App
42
42
  c.desc 'Include @done actions'
43
43
  c.switch %i[done]
44
44
 
45
- c.desc 'Add a tag to the action, @tag(values) allowed'
45
+ c.desc 'Add a tag to the action, @tag(values) allowed, use multiple times or combine multiple tags with a comma'
46
46
  c.arg_name 'TAG'
47
47
  c.flag %i[t tag], multiple: true
48
48
 
49
- c.desc 'Remove a tag to the action'
49
+ c.desc 'Remove a tag from the action, use multiple times or combine multiple tags with a comma,
50
+ wildcards (* and ?) allowed'
50
51
  c.arg_name 'TAG'
51
52
  c.flag %i[r remove], multiple: true
52
53
 
@@ -62,6 +63,10 @@ class App
62
63
  c.desc 'Delete an action'
63
64
  c.switch %i[delete], negatable: false
64
65
 
66
+ c.desc "Open action in editor (#{NA.default_editor}).
67
+ Natural language dates will be parsed and converted in date-based tags."
68
+ c.switch %i[edit], negatable: false
69
+
65
70
  c.desc 'Specify the file to search for the task'
66
71
  c.arg_name 'PATH'
67
72
  c.flag %i[file]
@@ -87,17 +92,22 @@ class App
87
92
  reader = TTY::Reader.new
88
93
  append = options[:at] ? options[:at] =~ /^[ae]/i : global_options[:add_at] =~ /^[ae]/i
89
94
 
90
- options[:done] = true if options[:restore] || options[:remove] =~ /^done/
95
+ if options[:restore] || (!options[:remove].nil? && options[:remove].include?('done'))
96
+ options[:done] = true
97
+ options[:tagged] << '+done'
98
+ elsif !options[:remove].nil? && !options[:remove].empty?
99
+ options[:tagged].concat(options[:remove])
100
+ end
91
101
 
92
102
  action = if args.count.positive?
93
103
  args.join(' ').strip
94
104
  elsif $stdin.isatty && TTY::Which.exist?('gum') && options[:tagged].empty?
95
- options = [
105
+ opts = [
96
106
  %(--placeholder "Enter a task to search for"),
97
107
  '--char-limit=500',
98
108
  "--width=#{TTY::Screen.columns}"
99
109
  ]
100
- `gum input #{options.join(' ')}`.strip
110
+ `gum input #{opts.join(' ')}`.strip
101
111
  elsif $stdin.isatty && options[:tagged].empty?
102
112
  puts NA::Color.template('{bm}Enter search string:{x}')
103
113
  reader.read_line(NA::Color.template('{by}> {bw}')).strip
@@ -144,8 +154,9 @@ class App
144
154
  end
145
155
 
146
156
  priority = options[:priority].to_i if options[:priority]&.to_i&.positive?
147
- add_tags = options[:tag] ? options[:tag].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
148
- remove_tags = options[:remove] ? options[:remove].map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
157
+ add_tags = options[:tag] ? options[:tag].join(',').split(/ *, */).map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
158
+ remove_tags = options[:remove] ? options[:remove].join(',').split(/ *, */).map { |t| t.sub(/^@/, '').wildcard_to_rx } : []
159
+ remove_tags << 'done' if options[:restore]
149
160
 
150
161
  stdin_note = NA.stdin ? NA.stdin.split("\n") : []
151
162
 
@@ -217,18 +228,19 @@ class App
217
228
 
218
229
  targets.each do |target|
219
230
  NA.update_action(target, tokens,
220
- priority: priority,
221
231
  add_tag: add_tags,
222
- remove_tag: remove_tags,
223
- finish: options[:finish],
224
- project: target_proj,
232
+ all: options[:all],
233
+ append: append,
225
234
  delete: options[:delete],
235
+ done: options[:done],
236
+ edit: options[:edit],
237
+ finish: options[:finish],
226
238
  note: note,
227
239
  overwrite: options[:overwrite],
228
- tagged: tags,
229
- all: options[:all],
230
- done: options[:done],
231
- append: append)
240
+ priority: priority,
241
+ project: target_proj,
242
+ remove_tag: remove_tags,
243
+ tagged: tags)
232
244
  end
233
245
  end
234
246
  end
data/bin/na CHANGED
@@ -6,6 +6,7 @@ require 'gli'
6
6
  require 'na/help_monkey_patch'
7
7
  require 'na'
8
8
  require 'fcntl'
9
+ require 'tempfile'
9
10
 
10
11
  # Main application
11
12
  class App
@@ -45,6 +45,125 @@ module NA
45
45
  res.empty? ? default : res =~ /y/i
46
46
  end
47
47
 
48
+ def default_editor
49
+ editor ||= ENV['NA_EDITOR'] || ENV['GIT_EDITOR'] || ENV['EDITOR']
50
+
51
+ if editor.good? && TTY::Which.exist?(editor)
52
+ return editor
53
+ end
54
+
55
+ notify('No EDITOR environment variable, testing available editors', debug: true)
56
+ editors = %w[vim vi code subl mate mvim nano emacs]
57
+ editors.each do |ed|
58
+ try = TTY::Which.which(ed)
59
+ if try
60
+ notify("Using editor #{try}", debug: true)
61
+ return try
62
+ end
63
+ end
64
+
65
+ notify('{br}No editor found{x}', exit_code: 5)
66
+
67
+ nil
68
+ end
69
+
70
+ def editor_with_args
71
+ args_for_editor(default_editor)
72
+ end
73
+
74
+ def args_for_editor(editor)
75
+ return editor if editor =~ /-\S/
76
+
77
+ args = case editor
78
+ when /^(subl|code|mate)$/
79
+ ['-w']
80
+ when /^(vim|mvim)$/
81
+ ['-f']
82
+ else
83
+ []
84
+ end
85
+ "#{editor} #{args.join(' ')}"
86
+ end
87
+
88
+ ##
89
+ ## Create a process for an editor and wait for the file handle to return
90
+ ##
91
+ ## @param input [String] Text input for editor
92
+ ##
93
+ def fork_editor(input = '', message: :default)
94
+ # raise NonInteractive, 'Non-interactive terminal' unless $stdout.isatty || ENV['DOING_EDITOR_TEST']
95
+
96
+ notify('{br}No EDITOR variable defined in environment{x}', exit_code: 5) if default_editor.nil?
97
+
98
+ tmpfile = Tempfile.new(['na_temp', '.na'])
99
+
100
+ File.open(tmpfile.path, 'w+') do |f|
101
+ f.puts input
102
+ unless message.nil?
103
+ f.puts message == :default ? '# First line is the action, lines after are added as a note' : message
104
+ end
105
+ end
106
+
107
+ pid = Process.fork { system("#{editor_with_args} #{tmpfile.path}") }
108
+
109
+ trap('INT') do
110
+ begin
111
+ Process.kill(9, pid)
112
+ rescue StandardError
113
+ Errno::ESRCH
114
+ end
115
+ tmpfile.unlink
116
+ tmpfile.close!
117
+ exit 0
118
+ end
119
+
120
+ Process.wait(pid)
121
+
122
+ begin
123
+ if $?.exitstatus == 0
124
+ input = IO.read(tmpfile.path)
125
+ else
126
+ exit_now! 'Cancelled'
127
+ end
128
+ ensure
129
+ tmpfile.close
130
+ tmpfile.unlink
131
+ end
132
+
133
+ input.split(/\n/).delete_if(&:ignore?).join("\n")
134
+ end
135
+
136
+ ##
137
+ ## Takes a multi-line string and formats it as an entry
138
+ ##
139
+ ## @param input [String] The string to parse
140
+ ##
141
+ ## @return [Array] [[String]title, [Note]note]
142
+ ##
143
+ def format_input(input)
144
+ notify('No content in entry', exit_code: 1) if input.nil? || input.strip.empty?
145
+
146
+ input_lines = input.split(/[\n\r]+/).delete_if(&:ignore?)
147
+ title = input_lines[0]&.strip
148
+ notify('{br}No content in first line{x}', exit_code: 1) if title.nil? || title.strip.empty?
149
+
150
+ title.expand_date_tags
151
+
152
+ note = if input_lines.length > 1
153
+ input_lines[1..-1]
154
+ else
155
+ []
156
+ end
157
+
158
+
159
+ unless note.empty?
160
+ note.map!(&:strip)
161
+ note.delete_if { |l| l =~ /^\s*$/ || l =~ /^#/ }
162
+ end
163
+
164
+ [title, note]
165
+ end
166
+
48
167
  ##
49
168
  ## Helper function to colorize the Y/N prompt
50
169
  ##
@@ -247,6 +366,8 @@ module NA
247
366
  string = "#{string.strip} @done(#{Time.now.strftime('%Y-%m-%d %H:%M')})" if finish && string !~ /(?<=\A| )@done/
248
367
 
249
368
  action.action = string
369
+ action.action.expand_date_tags
370
+ action.note = note unless note.empty?
250
371
 
251
372
  action
252
373
  end
@@ -254,18 +375,19 @@ module NA
254
375
  def update_action(target,
255
376
  search,
256
377
  add: nil,
257
- priority: 0,
258
378
  add_tag: [],
259
- remove_tag: [],
260
- finish: false,
261
- project: nil,
379
+ all: false,
380
+ append: false,
262
381
  delete: false,
382
+ done: false,
383
+ edit: false,
384
+ finish: false,
263
385
  note: [],
264
386
  overwrite: false,
265
- tagged: nil,
266
- all: false,
267
- done: false,
268
- append: false)
387
+ priority: 0,
388
+ project: nil,
389
+ remove_tag: [],
390
+ tagged: nil)
269
391
 
270
392
  projects = find_projects(target)
271
393
 
@@ -339,6 +461,12 @@ module NA
339
461
 
340
462
  projects = shift_index_after(projects, action.line, action.note.count + 1)
341
463
 
464
+ if edit
465
+ new_action, new_note = format_input(fork_editor("#{action.action}\n#{action.note.join("\n")}"))
466
+ action.action = new_action
467
+ action.note = new_note
468
+ end
469
+
342
470
  action = process_action(action, priority: priority, finish: finish, add_tag: add_tag, remove_tag: remove_tag)
343
471
 
344
472
  target_proj = if target_proj
data/lib/na/string.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ REGEX_DAY = /^(mon|tue|wed|thur?|fri|sat|sun)(\w+(day)?)?$/i.freeze
4
+ REGEX_CLOCK = '(?:\d{1,2}+(?::\d{1,2}+)?(?: *(?:am|pm))?|midnight|noon)'
5
+ REGEX_TIME = /^#{REGEX_CLOCK}$/i.freeze
6
+
3
7
  # String helpers
4
8
  class ::String
5
9
  ##
@@ -12,6 +16,16 @@ class ::String
12
16
  !strip.empty?
13
17
  end
14
18
 
19
+ ##
20
+ ## Test if line should be ignored
21
+ ##
22
+ ## @return [Boolean] line is empty or comment
23
+ ##
24
+ def ignore?
25
+ line = self
26
+ line =~ /^#/ || line.strip.empty?
27
+ end
28
+
15
29
  def read_file
16
30
  file = File.expand_path(self)
17
31
  raise "Missing file #{file}" unless File.exist?(file)
@@ -186,6 +200,105 @@ class ::String
186
200
  sub(/^#{ENV['HOME']}/, '~')
187
201
  end
188
202
 
203
+ ##
204
+ ## Convert (chronify) natural language dates
205
+ ## within configured date tags (tags whose value is
206
+ ## expected to be a date). Modifies string in place.
207
+ ##
208
+ ## @param additional_tags [Array] An array of
209
+ ## additional tags to
210
+ ## consider date_tags
211
+ ##
212
+ def expand_date_tags(additional_tags = nil)
213
+ iso_rx = /\d{4}-\d\d-\d\d \d\d:\d\d/
214
+
215
+ watch_tags = [
216
+ 'due',
217
+ 'start(?:ed)?',
218
+ 'beg[ia]n',
219
+ 'done',
220
+ 'finished',
221
+ 'completed?',
222
+ 'waiting',
223
+ 'defer(?:red)?'
224
+ ]
225
+
226
+ if additional_tags
227
+ date_tags = additional_tags
228
+ date_tags = date_tags.split(/ *, */) if date_tags.is_a?(String)
229
+ date_tags.map! do |tag|
230
+ tag.sub(/^@/, '').gsub(/\((?!\?:)(.*?)\)/, '(?:\1)').strip
231
+ end
232
+ watch_tags.concat(date_tags).uniq!
233
+ end
234
+
235
+ done_rx = /(?<=^| )@(?<tag>#{watch_tags.join('|')})\((?<date>.*?)\)/i
236
+
237
+ gsub!(done_rx) do
238
+ m = Regexp.last_match
239
+ t = m['tag']
240
+ d = m['date']
241
+ future = t =~ /^(done|complete)/ ? false : true
242
+ parsed_date = d =~ iso_rx ? Time.parse(d) : d.chronify(guess: :begin, future: future)
243
+ parsed_date.nil? ? m[0] : "@#{t}(#{parsed_date.strftime('%F %R')})"
244
+ end
245
+ end
246
+
247
+ ##
248
+ ## Converts input string into a Time object when input
249
+ ## takes on the following formats:
250
+ ## - interval format e.g. '1d2h30m', '45m'
251
+ ## etc.
252
+ ## - a semantic phrase e.g. 'yesterday
253
+ ## 5:30pm'
254
+ ## - a strftime e.g. '2016-03-15 15:32:04
255
+ ## PDT'
256
+ ##
257
+ ## @param options Additional options
258
+ ##
259
+ ## @option options :future [Boolean] assume future date
260
+ ## (default: false)
261
+ ##
262
+ ## @option options :guess [Symbol] :begin or :end to
263
+ ## assume beginning or end of
264
+ ## arbitrary time range
265
+ ##
266
+ ## @return [DateTime] result
267
+ ##
268
+ def chronify(**options)
269
+ now = Time.now
270
+ raise StandardError, "Invalid time expression #{inspect}" if to_s.strip == ''
271
+
272
+ secs_ago = if match(/^(\d+)$/)
273
+ # plain number, assume minutes
274
+ Regexp.last_match(1).to_i * 60
275
+ elsif (m = match(/^(?:(?<day>\d+)d)? *(?:(?<hour>\d+)h)? *(?:(?<min>\d+)m)?$/i))
276
+ # day/hour/minute format e.g. 1d2h30m
277
+ [[m['day'], 24 * 3600],
278
+ [m['hour'], 3600],
279
+ [m['min'], 60]].map { |qty, secs| qty ? (qty.to_i * secs) : 0 }.reduce(0, :+)
280
+ end
281
+
282
+ if secs_ago
283
+ res = now - secs_ago
284
+ notify(%(date/time string "#{self}" interpreted as #{res} (#{secs_ago} seconds ago)), debug: true)
285
+ else
286
+ date_string = dup
287
+ date_string = 'today' if date_string.match(REGEX_DAY) && now.strftime('%a') =~ /^#{Regexp.last_match(1)}/i
288
+ date_string = "#{options[:context].to_s} #{date_string}" if date_string =~ REGEX_TIME && options[:context]
289
+
290
+ res = Chronic.parse(date_string, {
291
+ guess: options.fetch(:guess, :begin),
292
+ context: options.fetch(:future, false) ? :future : :past,
293
+ ambiguous_time_range: 8
294
+ })
295
+
296
+ NA.notify(%(date/time string "#{self}" interpreted as #{res}), debug: true)
297
+ end
298
+
299
+ res
300
+ end
301
+
189
302
  private
190
303
 
191
304
  def matches_none(regexes)
data/lib/na/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Na
2
- VERSION = '1.2.32'
2
+ VERSION = '1.2.33'
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.31<!--END VER-->.
12
+ The current version of `na` is <!--VER-->1.2.32<!--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.32
4
+ version: 1.2.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Terpstra
@@ -215,6 +215,7 @@ files:
215
215
  - bin/commands/find.rb
216
216
  - bin/commands/init.rb
217
217
  - bin/commands/next.rb
218
+ - bin/commands/open.rb
218
219
  - bin/commands/projects.rb
219
220
  - bin/commands/prompt.rb
220
221
  - bin/commands/saved.rb