na 1.2.32 → 1.2.33
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +1 -1
- data/README.md +22 -20
- data/bin/commands/edit.rb +135 -32
- data/bin/commands/open.rb +48 -0
- data/bin/commands/update.rb +27 -15
- data/bin/na +1 -0
- data/lib/na/next_action.rb +136 -8
- data/lib/na/string.rb +113 -0
- data/lib/na/version.rb +1 -1
- data/src/_README.md +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d77588ce09a5a3cfc680b1866081dba5f56e75c8b51301cf0e1efee60c32f8f7
|
4
|
+
data.tar.gz: 06d73082ad5371cda4760df90c929c74963d76843b7b979dde59e46fdb933389
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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.
|
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.
|
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 -
|
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 -
|
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
|
-
|
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
|
-
-
|
184
|
-
|
185
|
-
-e, --
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
na edit
|
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
|
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 '
|
6
|
-
long_desc '
|
7
|
-
|
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
|
10
|
-
|
11
|
-
|
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 '
|
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
|
-
|
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 '
|
19
|
-
c.
|
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 '
|
23
|
-
c.
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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.
|
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
|
data/bin/commands/update.rb
CHANGED
@@ -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
|
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[:
|
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
|
-
|
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 #{
|
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
|
-
|
223
|
-
|
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
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
data/lib/na/next_action.rb
CHANGED
@@ -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
|
-
|
260
|
-
|
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
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
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
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.
|
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.
|
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
|