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