doing 1.0.93 → 2.0.6.pre
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/AUTHORS +19 -0
- data/CHANGELOG.md +616 -0
- data/COMMANDS.md +1181 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +110 -0
- data/LICENSE +23 -0
- data/README.md +15 -699
- data/Rakefile +79 -0
- data/_config.yml +1 -0
- data/bin/doing +1055 -494
- data/doing.gemspec +34 -0
- data/doing.rdoc +1839 -0
- data/example_plugin.rb +209 -0
- data/generate_completions.sh +5 -0
- data/img/doing-colors.jpg +0 -0
- data/img/doing-printf-wrap-800.jpg +0 -0
- data/img/doing-show-note-formatting-800.jpg +0 -0
- data/lib/completion/_doing.zsh +203 -0
- data/lib/completion/doing.bash +449 -0
- data/lib/completion/doing.fish +329 -0
- data/lib/doing/array.rb +8 -0
- data/lib/doing/cli_status.rb +70 -0
- data/lib/doing/colors.rb +136 -0
- data/lib/doing/configuration.rb +312 -0
- data/lib/doing/errors.rb +109 -0
- data/lib/doing/hash.rb +31 -0
- data/lib/doing/hooks.rb +59 -0
- data/lib/doing/item.rb +155 -0
- data/lib/doing/log_adapter.rb +344 -0
- data/lib/doing/markdown_document_listener.rb +174 -0
- data/lib/doing/note.rb +59 -0
- data/lib/doing/pager.rb +95 -0
- data/lib/doing/plugin_manager.rb +208 -0
- data/lib/doing/plugins/export/csv_export.rb +48 -0
- data/lib/doing/plugins/export/html_export.rb +83 -0
- data/lib/doing/plugins/export/json_export.rb +140 -0
- data/lib/doing/plugins/export/markdown_export.rb +85 -0
- data/lib/doing/plugins/export/taskpaper_export.rb +34 -0
- data/lib/doing/plugins/export/template_export.rb +141 -0
- data/lib/doing/plugins/import/cal_to_json.scpt +0 -0
- data/lib/doing/plugins/import/calendar_import.rb +76 -0
- data/lib/doing/plugins/import/doing_import.rb +144 -0
- data/lib/doing/plugins/import/timing_import.rb +78 -0
- data/lib/doing/string.rb +348 -0
- data/lib/doing/symbol.rb +16 -0
- data/lib/doing/time.rb +18 -0
- data/lib/doing/util.rb +186 -0
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +1868 -2349
- data/lib/doing/wwidfile.rb +117 -0
- data/lib/doing.rb +43 -3
- data/lib/examples/commands/autotag.rb +63 -0
- data/lib/examples/commands/wiki.rb +81 -0
- data/lib/examples/plugins/hooks.rb +22 -0
- data/lib/examples/plugins/say_export.rb +202 -0
- data/lib/examples/plugins/templates/wiki.css +169 -0
- data/lib/examples/plugins/templates/wiki.haml +27 -0
- data/lib/examples/plugins/templates/wiki_index.haml +18 -0
- data/lib/examples/plugins/wiki_export.rb +87 -0
- data/lib/templates/doing-markdown.erb +5 -0
- data/man/doing.1 +964 -0
- data/man/doing.1.html +711 -0
- data/man/doing.1.ronn +600 -0
- data/package-lock.json +3 -0
- data/rdoc_to_mmd.rb +42 -0
- data/rdocfixer.rb +13 -0
- data/scripts/generate_bash_completions.rb +211 -0
- data/scripts/generate_fish_completions.rb +204 -0
- data/scripts/generate_zsh_completions.rb +168 -0
- metadata +82 -7
- data/lib/doing/helpers.rb +0 -191
- data/lib/doing/markdown_export.rb +0 -16
data/bin/doing
CHANGED
@@ -20,17 +20,49 @@ if class_exists? 'Encoding'
|
|
20
20
|
end
|
21
21
|
|
22
22
|
include GLI::App
|
23
|
+
include Doing::Errors
|
23
24
|
version Doing::VERSION
|
25
|
+
hide_commands_without_desc true
|
26
|
+
autocomplete_commands true
|
27
|
+
|
28
|
+
REGEX_BOOL = /^(?:and|all|any|or|not|none)$/i
|
29
|
+
REGEX_SORT_ORDER = /^(?:a(?:sc)?|d(?:esc)?)$/i
|
30
|
+
|
31
|
+
InvalidExportType = Class.new(RuntimeError)
|
32
|
+
MissingConfigFile = Class.new(RuntimeError)
|
33
|
+
|
34
|
+
colors = Doing::Color
|
35
|
+
wwid = Doing::WWID.new
|
36
|
+
|
37
|
+
Doing.logger.log_level = :info
|
38
|
+
|
39
|
+
if ENV['DOING_LOG_LEVEL'] || ENV['DOING_DEBUG'] || ENV['DOING_QUIET'] || ENV['DOING_VERBOSE'] || ENV['DOING_PLUGIN_DEBUG']
|
40
|
+
# Quiet always wins
|
41
|
+
if ENV['DOING_QUIET'] && ENV['DOING_QUIET'].truthy?
|
42
|
+
Doing.logger.log_level = :error
|
43
|
+
elsif (ENV['DOING_PLUGIN_DEBUG'] && ENV['DOING_PLUGIN_DEBUG'].truthy?)
|
44
|
+
Doing.logger.log_level = :debug
|
45
|
+
elsif (ENV['DOING_DEBUG'] && ENV['DOING_DEBUG'].truthy?)
|
46
|
+
Doing.logger.log_level = :debug
|
47
|
+
elsif ENV['DOING_LOG_LEVEL']
|
48
|
+
Doing.logger.log_level = ENV['DOING_LOG_LEVEL']
|
49
|
+
end
|
50
|
+
end
|
24
51
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
wwid.
|
52
|
+
if ENV['DOING_CONFIG']
|
53
|
+
Doing.config_with(ENV['DOING_CONFIG'], { ignore_local: true })
|
54
|
+
end
|
55
|
+
|
56
|
+
config = Doing.config
|
57
|
+
settings = config.settings
|
58
|
+
wwid.config = settings
|
59
|
+
|
60
|
+
if settings.dig('plugins', 'command_path')
|
61
|
+
commands_from File.expand_path(settings.dig('plugins', 'command_path'))
|
62
|
+
end
|
32
63
|
|
33
64
|
program_desc 'A CLI for a What Was I Doing system'
|
65
|
+
program_long_desc %(Doing uses a TaskPaper-like formatting to keep a plain text record of what you've been doing, complete with tag-based time tracking. The command line tool allows you to add entries, annotate with tags and notes, and view your entries with myriad options, with a focus on a "natural" language syntax.)
|
34
66
|
|
35
67
|
default_command :recent
|
36
68
|
# sort_help :manually
|
@@ -41,23 +73,49 @@ switch [:notes], default_value: true, negatable: true
|
|
41
73
|
desc 'Send results report to STDOUT instead of STDERR'
|
42
74
|
switch [:stdout], default_value: false, negatable: false
|
43
75
|
|
76
|
+
desc 'Use a pager when output is longer than screen'
|
77
|
+
switch %i[p pager], default_value: settings['paginate']
|
78
|
+
|
79
|
+
desc 'Answer yes/no menus with default option'
|
80
|
+
switch [:default], default_value: false
|
81
|
+
|
44
82
|
desc 'Exclude auto tags and default tags'
|
45
|
-
switch %i[x noauto], default_value: false
|
83
|
+
switch %i[x noauto], default_value: false, negatable: false
|
84
|
+
|
85
|
+
desc 'Colored output'
|
86
|
+
switch %i[color], default_value: true
|
46
87
|
|
47
|
-
desc '
|
48
|
-
|
88
|
+
desc 'Silence info messages'
|
89
|
+
switch %i[q quiet], default_value: false, negatable: false
|
90
|
+
|
91
|
+
desc 'Verbose output'
|
92
|
+
switch %i[debug], default_value: false, negatable: false
|
93
|
+
|
94
|
+
desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
|
95
|
+
flag [:config_file], default_value: config.config_file
|
49
96
|
|
50
97
|
desc 'Specify a different doing_file'
|
51
98
|
flag %i[f doing_file]
|
52
99
|
|
53
100
|
desc 'Add an entry'
|
101
|
+
long_desc %(Record what you're starting now, or backdate the start time using natural language.
|
102
|
+
|
103
|
+
A parenthetical at the end of the entry will be converted to a note.
|
104
|
+
|
105
|
+
Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
|
54
106
|
arg_name 'ENTRY'
|
55
107
|
command %i[now next] do |c|
|
108
|
+
c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
|
109
|
+
c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
|
110
|
+
c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
|
111
|
+
c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
|
112
|
+
c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
|
113
|
+
|
56
114
|
c.desc 'Section'
|
57
115
|
c.arg_name 'NAME'
|
58
116
|
c.flag %i[s section]
|
59
117
|
|
60
|
-
c.desc "Edit entry with #{
|
118
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
61
119
|
c.switch %i[e editor], negatable: false, default_value: false
|
62
120
|
|
63
121
|
c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
|
@@ -67,7 +125,7 @@ command %i[now next] do |c|
|
|
67
125
|
c.desc 'Timed entry, marks last entry in section as @done'
|
68
126
|
c.switch %i[f finish_last], negatable: false, default_value: false
|
69
127
|
|
70
|
-
c.desc '
|
128
|
+
c.desc 'Include a note'
|
71
129
|
c.arg_name 'TEXT'
|
72
130
|
c.flag %i[n note]
|
73
131
|
|
@@ -77,9 +135,9 @@ command %i[now next] do |c|
|
|
77
135
|
|
78
136
|
c.action do |_global_options, options, args|
|
79
137
|
if options[:back]
|
80
|
-
date = wwid.chronify(options[:back])
|
138
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
81
139
|
|
82
|
-
|
140
|
+
raise InvalidTimeExpression.new('unable to parse date string', topic: 'Date parser:') if date.nil?
|
83
141
|
else
|
84
142
|
date = Time.now
|
85
143
|
end
|
@@ -87,17 +145,17 @@ command %i[now next] do |c|
|
|
87
145
|
if options[:section]
|
88
146
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
89
147
|
else
|
90
|
-
options[:section] =
|
148
|
+
options[:section] = settings['current_section']
|
91
149
|
end
|
92
150
|
|
93
151
|
if options[:e] || (args.empty? && $stdin.stat.size.zero?)
|
94
|
-
|
152
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
95
153
|
|
96
154
|
input = ''
|
97
155
|
input += args.join(' ') unless args.empty?
|
98
156
|
input = wwid.fork_editor(input).strip
|
99
157
|
|
100
|
-
|
158
|
+
raise EmptyInput, 'No content' if input.empty?
|
101
159
|
|
102
160
|
title, note = wwid.format_input(input)
|
103
161
|
note.push(options[:n]) if options[:n]
|
@@ -115,14 +173,76 @@ command %i[now next] do |c|
|
|
115
173
|
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:f] })
|
116
174
|
wwid.write(wwid.doing_file)
|
117
175
|
else
|
118
|
-
|
176
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
119
177
|
end
|
120
178
|
end
|
121
179
|
end
|
122
180
|
|
181
|
+
desc 'Reset the start time of an entry'
|
182
|
+
command %i[reset begin] do |c|
|
183
|
+
c.desc 'Set the start date of an item to now'
|
184
|
+
c.arg_name 'NAME'
|
185
|
+
c.flag %i[s section], default_value: 'All'
|
186
|
+
|
187
|
+
c.desc 'Resume entry (remove @done)'
|
188
|
+
c.switch %i[r resume], default_value: true
|
189
|
+
|
190
|
+
c.desc 'Reset last entry matching tag'
|
191
|
+
c.arg_name 'TAG'
|
192
|
+
c.flag [:tag]
|
193
|
+
|
194
|
+
c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
195
|
+
c.arg_name 'QUERY'
|
196
|
+
c.flag [:search]
|
197
|
+
|
198
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
199
|
+
c.arg_name 'BOOLEAN'
|
200
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
201
|
+
|
202
|
+
c.desc 'Select from a menu of matching entries'
|
203
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
204
|
+
|
205
|
+
c.action do |global_options, options, args|
|
206
|
+
if options[:section]
|
207
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
208
|
+
end
|
209
|
+
|
210
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
211
|
+
|
212
|
+
items = wwid.filter_items([], opt: options)
|
213
|
+
|
214
|
+
if options[:interactive]
|
215
|
+
last_entry = wwid.choose_from_items(items, {
|
216
|
+
menu: true,
|
217
|
+
header: '',
|
218
|
+
prompt: 'Select an entry to start/reset > ',
|
219
|
+
multiple: false,
|
220
|
+
sort: false,
|
221
|
+
show_if_single: true
|
222
|
+
}, include_section: options[:section].nil? )
|
223
|
+
else
|
224
|
+
last_entry = items.last
|
225
|
+
end
|
226
|
+
|
227
|
+
unless last_entry
|
228
|
+
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
229
|
+
return
|
230
|
+
end
|
231
|
+
|
232
|
+
wwid.reset_item(last_entry, resume: options[:resume])
|
233
|
+
|
234
|
+
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
235
|
+
|
236
|
+
wwid.write(wwid.doing_file)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
|
123
241
|
desc 'Add a note to the last entry'
|
124
242
|
long_desc %(
|
125
|
-
If -r is provided with no other arguments, the last note is removed.
|
243
|
+
If -r is provided with no other arguments, the last note is removed.
|
244
|
+
If new content is specified through arguments or STDIN, any previous
|
245
|
+
note will be replaced with the new one.
|
126
246
|
|
127
247
|
Use -e to load the last entry in a text editor where you can append a note.
|
128
248
|
)
|
@@ -132,47 +252,77 @@ command :note do |c|
|
|
132
252
|
c.arg_name 'NAME'
|
133
253
|
c.flag %i[s section], default_value: 'All'
|
134
254
|
|
135
|
-
c.desc "Edit entry with #{
|
255
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
136
256
|
c.switch %i[e editor], negatable: false, default_value: false
|
137
257
|
|
138
258
|
c.desc "Replace/Remove last entry's note (default append)"
|
139
259
|
c.switch %i[r remove], negatable: false, default_value: false
|
140
260
|
|
261
|
+
c.desc 'Add/remove note from last entry matching tag'
|
262
|
+
c.arg_name 'TAG'
|
263
|
+
c.flag [:tag]
|
264
|
+
|
265
|
+
c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
266
|
+
c.arg_name 'QUERY'
|
267
|
+
c.flag [:search]
|
268
|
+
|
269
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
270
|
+
c.arg_name 'BOOLEAN'
|
271
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
272
|
+
|
273
|
+
c.desc 'Select item for new note from a menu of matching entries'
|
274
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
275
|
+
|
141
276
|
c.action do |_global_options, options, args|
|
142
277
|
if options[:section]
|
143
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
278
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
279
|
+
end
|
280
|
+
|
281
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
282
|
+
|
283
|
+
last_entry = wwid.last_entry(options)
|
284
|
+
|
285
|
+
unless last_entry
|
286
|
+
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
287
|
+
return
|
144
288
|
end
|
145
289
|
|
290
|
+
last_note = last_entry.note || Doing::Note.new
|
291
|
+
new_note = Doing::Note.new
|
292
|
+
|
146
293
|
if options[:e] || (args.empty? && $stdin.stat.size.zero? && !options[:r])
|
147
|
-
|
294
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
148
295
|
|
149
296
|
input = !args.empty? ? args.join(' ') : ''
|
150
297
|
|
151
|
-
|
152
|
-
|
153
|
-
|
298
|
+
if options[:remove]
|
299
|
+
prev_input = Doing::Note.new
|
300
|
+
else
|
301
|
+
prev_input = last_entry.note || Doing::Note.new
|
302
|
+
end
|
154
303
|
|
155
|
-
input =
|
156
|
-
exit_now! 'No content, cancelled' unless input
|
304
|
+
input = prev_input.add(input)
|
157
305
|
|
306
|
+
input = wwid.fork_editor([last_entry.title, '### Edit below this line', input.to_s].join("\n")).strip
|
158
307
|
_title, note = wwid.format_input(input)
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
wwid.note_last(section, note, replace: true)
|
308
|
+
options[:remove] = true
|
309
|
+
new_note.add(note)
|
163
310
|
elsif !args.empty?
|
164
|
-
|
165
|
-
note.insert(0, title)
|
166
|
-
wwid.note_last(section, note, replace: options[:r])
|
311
|
+
new_note.add(args.join(' '))
|
167
312
|
elsif $stdin.stat.size.positive?
|
168
|
-
|
169
|
-
note.insert(0, title)
|
170
|
-
wwid.note_last(section, note, replace: options[:r])
|
171
|
-
elsif options[:r]
|
172
|
-
wwid.note_last(section, [], replace: true)
|
313
|
+
new_note.add($stdin.read)
|
173
314
|
else
|
174
|
-
|
315
|
+
raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
|
175
316
|
end
|
317
|
+
|
318
|
+
if last_note.equal?(new_note)
|
319
|
+
Doing.logger.debug('Skipped:', 'No note change')
|
320
|
+
else
|
321
|
+
last_note.add(new_note, replace: options[:remove])
|
322
|
+
Doing.logger.info('Entry updated:', last_entry.title)
|
323
|
+
end
|
324
|
+
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
325
|
+
|
176
326
|
wwid.write(wwid.doing_file)
|
177
327
|
end
|
178
328
|
end
|
@@ -184,11 +334,11 @@ command :meanwhile do |c|
|
|
184
334
|
c.arg_name 'NAME'
|
185
335
|
c.flag %i[s section]
|
186
336
|
|
187
|
-
c.desc "Edit entry with #{
|
337
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
188
338
|
c.switch %i[e editor], negatable: false, default_value: false
|
189
339
|
|
190
340
|
c.desc 'Archive previous @meanwhile entry'
|
191
|
-
c.switch %i[a archive], default_value: false
|
341
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
192
342
|
|
193
343
|
c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
|
194
344
|
c.arg_name 'DATE_STRING'
|
@@ -200,9 +350,9 @@ command :meanwhile do |c|
|
|
200
350
|
|
201
351
|
c.action do |_global_options, options, args|
|
202
352
|
if options[:back]
|
203
|
-
date = wwid.chronify(options[:back])
|
353
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
204
354
|
|
205
|
-
|
355
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
206
356
|
else
|
207
357
|
date = Time.now
|
208
358
|
end
|
@@ -210,12 +360,12 @@ command :meanwhile do |c|
|
|
210
360
|
if options[:section]
|
211
361
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
212
362
|
else
|
213
|
-
section =
|
363
|
+
section = settings['current_section']
|
214
364
|
end
|
215
365
|
input = ''
|
216
366
|
|
217
367
|
if options[:e]
|
218
|
-
|
368
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
219
369
|
|
220
370
|
input += args.join(' ') unless args.empty?
|
221
371
|
input = wwid.fork_editor(input).strip
|
@@ -248,30 +398,55 @@ long_desc %(
|
|
248
398
|
Templates are printed to STDOUT for piping to a file.
|
249
399
|
Save them and use them in the configuration file under html_template.
|
250
400
|
|
251
|
-
Example `doing template
|
401
|
+
Example `doing template haml > ~/styles/my_doing.haml`
|
252
402
|
)
|
253
|
-
arg_name 'TYPE', must_match:
|
403
|
+
arg_name 'TYPE', must_match: Doing::Plugins.template_regex
|
254
404
|
command :template do |c|
|
405
|
+
c.desc 'List all available templates'
|
406
|
+
c.switch %i[l list], negatable: false
|
407
|
+
|
408
|
+
c.desc 'List in single column for completion'
|
409
|
+
c.switch %i[c]
|
410
|
+
|
255
411
|
c.action do |_global_options, options, args|
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
412
|
+
if options[:list] || options[:c]
|
413
|
+
if options[:c]
|
414
|
+
$stdout.print Doing::Plugins.plugin_templates.join("\n")
|
415
|
+
else
|
416
|
+
$stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
|
417
|
+
end
|
418
|
+
return
|
419
|
+
end
|
420
|
+
|
421
|
+
if args.empty?
|
422
|
+
type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
|
265
423
|
else
|
266
|
-
|
424
|
+
type = args[0]
|
267
425
|
end
|
426
|
+
|
427
|
+
raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
|
428
|
+
|
429
|
+
$stdout.puts Doing::Plugins.template_for_trigger(type)
|
430
|
+
|
431
|
+
# case args[0]
|
432
|
+
# when /html|haml/i
|
433
|
+
# $stdout.puts wwid.haml_template
|
434
|
+
# when /css/i
|
435
|
+
# $stdout.puts wwid.css_template
|
436
|
+
# when /markdown|md|erb/i
|
437
|
+
# $stdout.puts wwid.markdown_template
|
438
|
+
# else
|
439
|
+
# exit_now! 'Invalid type specified, must be HAML or CSS'
|
440
|
+
# end
|
268
441
|
end
|
269
442
|
end
|
270
443
|
|
271
|
-
desc 'Display an interactive menu to perform operations
|
444
|
+
desc 'Display an interactive menu to perform operations'
|
272
445
|
long_desc 'List all entries and select with typeahead fuzzy matching.
|
273
446
|
|
274
|
-
Multiple selections are allowed, hit tab to add the highlighted entry to the
|
447
|
+
Multiple selections are allowed, hit tab to add the highlighted entry to the
|
448
|
+
selection, and use ctrl-a to select all visible items. Return processes the
|
449
|
+
selected entries.'
|
275
450
|
command :select do |c|
|
276
451
|
c.desc 'Select from a specific section'
|
277
452
|
c.arg_name 'SECTION'
|
@@ -296,7 +471,7 @@ command :select do |c|
|
|
296
471
|
|
297
472
|
c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
|
298
473
|
c.arg_name 'QUERY'
|
299
|
-
c.flag %i[q query]
|
474
|
+
c.flag %i[q query search]
|
300
475
|
|
301
476
|
c.desc 'Use --no-menu to skip the interactive menu. Use with --query to filter items and act on results automatically. Test with `--output doing` to preview matches.'
|
302
477
|
c.switch %i[menu], negatable: true, default_value: true
|
@@ -323,12 +498,17 @@ command :select do |c|
|
|
323
498
|
c.arg_name 'FILE'
|
324
499
|
c.flag %i[save_to]
|
325
500
|
|
326
|
-
c.desc
|
501
|
+
c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
|
327
502
|
c.arg_name 'FORMAT'
|
328
|
-
c.flag %i[o output]
|
503
|
+
c.flag %i[o output]
|
504
|
+
|
505
|
+
c.desc "Copy selection as a new entry with current time and no @done tag. Only works with single selections. Can be combined with --editor."
|
506
|
+
c.switch %i[again resume], negatable: false, default_value: false
|
329
507
|
|
330
508
|
c.action do |_global_options, options, args|
|
331
|
-
|
509
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
510
|
+
|
511
|
+
raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
|
332
512
|
|
333
513
|
wwid.interactive(options)
|
334
514
|
end
|
@@ -337,7 +517,7 @@ end
|
|
337
517
|
desc 'Add an item to the Later section'
|
338
518
|
arg_name 'ENTRY'
|
339
519
|
command :later do |c|
|
340
|
-
c.desc "Edit entry with #{
|
520
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
341
521
|
c.switch %i[e editor], negatable: false, default_value: false
|
342
522
|
|
343
523
|
c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
|
@@ -350,18 +530,18 @@ command :later do |c|
|
|
350
530
|
|
351
531
|
c.action do |_global_options, options, args|
|
352
532
|
if options[:back]
|
353
|
-
date = wwid.chronify(options[:back])
|
354
|
-
|
533
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
534
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
355
535
|
else
|
356
536
|
date = Time.now
|
357
537
|
end
|
358
538
|
|
359
539
|
if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
|
360
|
-
|
540
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
361
541
|
|
362
542
|
input = args.empty? ? '' : args.join(' ')
|
363
543
|
input = wwid.fork_editor(input).strip
|
364
|
-
|
544
|
+
raise EmptyInput, 'No content' unless input && !input.empty?
|
365
545
|
|
366
546
|
title, note = wwid.format_input(input)
|
367
547
|
note.push(options[:n]) if options[:n]
|
@@ -378,7 +558,7 @@ command :later do |c|
|
|
378
558
|
wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
|
379
559
|
wwid.write(wwid.doing_file)
|
380
560
|
else
|
381
|
-
|
561
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
382
562
|
end
|
383
563
|
end
|
384
564
|
end
|
@@ -414,31 +594,39 @@ command %i[done did] do |c|
|
|
414
594
|
c.arg_name 'NAME'
|
415
595
|
c.flag %i[s section]
|
416
596
|
|
417
|
-
c.desc "Edit entry with #{
|
597
|
+
c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
|
418
598
|
c.switch %i[e editor], negatable: false, default_value: false
|
419
599
|
|
600
|
+
c.desc 'Include a note'
|
601
|
+
c.arg_name 'TEXT'
|
602
|
+
c.flag %i[n note]
|
603
|
+
|
604
|
+
c.desc 'Finish last entry not already marked @done'
|
605
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
606
|
+
|
420
607
|
# c.desc "Edit entry with specified app"
|
421
608
|
# c.arg_name 'editor_app'
|
422
609
|
# # c.flag [:a, :app]
|
423
610
|
|
424
611
|
c.action do |_global_options, options, args|
|
425
612
|
took = 0
|
613
|
+
donedate = nil
|
426
614
|
|
427
615
|
if options[:took]
|
428
616
|
took = wwid.chronify_qty(options[:took])
|
429
|
-
|
617
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
430
618
|
end
|
431
619
|
|
432
620
|
if options[:back]
|
433
|
-
date = wwid.chronify(options[:back])
|
434
|
-
|
621
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
622
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
435
623
|
else
|
436
624
|
date = options[:took] ? Time.now - took : Time.now
|
437
625
|
end
|
438
626
|
|
439
627
|
if options[:at]
|
440
|
-
finish_date = wwid.chronify(options[:at])
|
441
|
-
|
628
|
+
finish_date = wwid.chronify(options[:at], guess: :begin)
|
629
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
442
630
|
|
443
631
|
date = options[:took] ? finish_date - took : finish_date
|
444
632
|
elsif options[:took]
|
@@ -449,58 +637,116 @@ command %i[done did] do |c|
|
|
449
637
|
finish_date = Time.now
|
450
638
|
end
|
451
639
|
|
452
|
-
if
|
453
|
-
donedate =
|
640
|
+
if options[:date]
|
641
|
+
donedate = finish_date.strftime('%F %R')
|
454
642
|
end
|
455
643
|
|
456
644
|
if options[:section]
|
457
645
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
458
646
|
else
|
459
|
-
section =
|
647
|
+
section = settings['current_section']
|
460
648
|
end
|
461
649
|
|
650
|
+
note = Doing::Note.new
|
651
|
+
note.add(options[:note]) if options[:note]
|
652
|
+
|
462
653
|
if options[:editor]
|
463
|
-
|
654
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
655
|
+
is_new = false
|
656
|
+
|
657
|
+
if args.empty?
|
658
|
+
last_entry = wwid.filter_items([], opt: {unfinished: options[:unfinished], section: section, count: 1, age: 'new'}).max_by { |item| item.date }
|
659
|
+
|
660
|
+
unless last_entry
|
661
|
+
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
662
|
+
raise NoResults, 'No results'
|
663
|
+
end
|
664
|
+
|
665
|
+
old_entry = last_entry.dup
|
666
|
+
last_entry.note.add(note)
|
667
|
+
input = [last_entry.title, last_entry.note.to_s].join("\n")
|
668
|
+
else
|
669
|
+
is_new = true
|
670
|
+
input = [args.join(' '), note.to_s].join("\n")
|
671
|
+
end
|
464
672
|
|
465
|
-
input = ''
|
466
|
-
input += args.join(' ') unless args.empty?
|
467
673
|
input = wwid.fork_editor(input).strip
|
468
|
-
|
674
|
+
raise EmptyInput, 'No content' unless input && !input.empty?
|
469
675
|
|
470
676
|
title, note = wwid.format_input(input)
|
471
|
-
title
|
472
|
-
|
473
|
-
|
677
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
678
|
+
if new_entry.should_finish?
|
679
|
+
if new_entry.should_time?
|
680
|
+
new_entry.tag('done', value: donedate)
|
681
|
+
else
|
682
|
+
new_entry.tag('done')
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
if (is_new)
|
687
|
+
wwid.content[section][:items].push(new_entry)
|
688
|
+
else
|
689
|
+
wwid.update_item(old_entry, new_entry)
|
690
|
+
end
|
691
|
+
|
692
|
+
if options[:a]
|
693
|
+
wwid.move_item(new_entry, 'Archive', label: true)
|
694
|
+
end
|
695
|
+
|
474
696
|
wwid.write(wwid.doing_file)
|
475
697
|
elsif args.empty? && $stdin.stat.size.zero?
|
476
698
|
if options[:r]
|
477
699
|
wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
478
700
|
else
|
479
|
-
|
701
|
+
note = options[:note] ? Doing::Note.new(options[:note]) : nil
|
702
|
+
opt = {
|
480
703
|
archive: options[:a],
|
481
704
|
back: finish_date,
|
482
705
|
count: 1,
|
483
706
|
date: options[:date],
|
707
|
+
note: note,
|
484
708
|
section: section,
|
485
|
-
|
709
|
+
tags: ['done'],
|
710
|
+
took: took == 0 ? nil : took,
|
711
|
+
unfinished: options[:unfinished]
|
486
712
|
}
|
487
|
-
wwid.tag_last(
|
713
|
+
wwid.tag_last(opt)
|
488
714
|
end
|
489
715
|
elsif !args.empty?
|
490
|
-
|
716
|
+
note = Doing::Note.new(options[:note])
|
717
|
+
title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
|
491
718
|
title.chomp!
|
492
|
-
title += " @done#{donedate}"
|
493
719
|
section = 'Archive' if options[:a]
|
494
|
-
|
720
|
+
new_entry = Doing::Item.new(date, title, section, new_note)
|
721
|
+
if new_entry.should_finish?
|
722
|
+
if new_entry.should_time?
|
723
|
+
new_entry.tag('done', value: donedate)
|
724
|
+
else
|
725
|
+
new_entry.tag('done')
|
726
|
+
end
|
727
|
+
end
|
728
|
+
wwid.content[section][:items].push(new_entry)
|
495
729
|
wwid.write(wwid.doing_file)
|
730
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
496
731
|
elsif $stdin.stat.size.positive?
|
497
732
|
title, note = wwid.format_input($stdin.read)
|
498
|
-
|
733
|
+
note.add(options[:note]) if options[:note]
|
499
734
|
section = options[:a] ? 'Archive' : section
|
500
|
-
|
735
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
736
|
+
|
737
|
+
if new_entry.should_finish?
|
738
|
+
if new_entry.should_time?
|
739
|
+
new_entry.tag('done', value: donedate)
|
740
|
+
else
|
741
|
+
new_entry.tag('done')
|
742
|
+
end
|
743
|
+
end
|
744
|
+
|
745
|
+
wwid.content[section][:items].push(new_entry)
|
501
746
|
wwid.write(wwid.doing_file)
|
747
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
502
748
|
else
|
503
|
-
|
749
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
504
750
|
end
|
505
751
|
end
|
506
752
|
end
|
@@ -522,39 +768,37 @@ command :cancel do |c|
|
|
522
768
|
|
523
769
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
524
770
|
c.arg_name 'BOOLEAN'
|
525
|
-
c.flag [:bool], must_match:
|
771
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
526
772
|
|
527
773
|
c.desc 'Cancel last entry (or entries) not already marked @done'
|
528
774
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
529
775
|
|
776
|
+
c.desc 'Select item(s) to cancel from a menu of matching entries'
|
777
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
778
|
+
|
530
779
|
c.action do |_global_options, options, args|
|
531
780
|
if options[:section]
|
532
781
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
533
782
|
else
|
534
|
-
section =
|
783
|
+
section = settings['current_section']
|
535
784
|
end
|
536
785
|
|
537
786
|
if options[:tag].nil?
|
538
787
|
tags = []
|
539
788
|
else
|
540
|
-
tags = options[:tag].
|
541
|
-
options[:bool] = case options[:bool]
|
542
|
-
when /(and|all)/i
|
543
|
-
'AND'
|
544
|
-
when /(any|or)/i
|
545
|
-
'OR'
|
546
|
-
when /(not|none)/i
|
547
|
-
'NOT'
|
548
|
-
else
|
549
|
-
'AND'
|
550
|
-
end
|
789
|
+
tags = options[:tag].to_tags
|
551
790
|
end
|
552
791
|
|
553
|
-
|
792
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
793
|
+
|
794
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
554
795
|
|
555
|
-
|
796
|
+
if options[:interactive]
|
797
|
+
count = 0
|
798
|
+
else
|
799
|
+
count = args[0] ? args[0].to_i : 1
|
800
|
+
end
|
556
801
|
|
557
|
-
count = args[0] ? args[0].to_i : 1
|
558
802
|
opts = {
|
559
803
|
archive: options[:a],
|
560
804
|
count: count,
|
@@ -562,10 +806,12 @@ command :cancel do |c|
|
|
562
806
|
section: section,
|
563
807
|
sequential: false,
|
564
808
|
tag: tags,
|
565
|
-
tag_bool: options[:bool],
|
809
|
+
tag_bool: options[:bool].normalize_bool,
|
566
810
|
tags: ['done'],
|
567
|
-
unfinished: options[:unfinished]
|
811
|
+
unfinished: options[:unfinished],
|
812
|
+
interactive: options[:interactive]
|
568
813
|
}
|
814
|
+
|
569
815
|
wwid.tag_last(opts)
|
570
816
|
end
|
571
817
|
end
|
@@ -594,13 +840,16 @@ command :finish do |c|
|
|
594
840
|
c.arg_name 'TAG'
|
595
841
|
c.flag [:tag]
|
596
842
|
|
597
|
-
c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
|
843
|
+
c.desc 'Finish the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
598
844
|
c.arg_name 'QUERY'
|
599
845
|
c.flag [:search]
|
600
846
|
|
601
847
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
602
848
|
c.arg_name 'BOOLEAN'
|
603
|
-
c.flag [:bool], must_match:
|
849
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
850
|
+
|
851
|
+
c.desc 'Remove done tag'
|
852
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
604
853
|
|
605
854
|
c.desc 'Finish last entry (or entries) not already marked @done'
|
606
855
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
@@ -617,32 +866,29 @@ command :finish do |c|
|
|
617
866
|
c.arg_name 'NAME'
|
618
867
|
c.flag %i[s section]
|
619
868
|
|
620
|
-
c.
|
621
|
-
|
622
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
623
|
-
else
|
624
|
-
section = wwid.config['current_section']
|
625
|
-
end
|
869
|
+
c.desc 'Select item(s) to finish from a menu of matching entries'
|
870
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
626
871
|
|
872
|
+
c.action do |_global_options, options, args|
|
627
873
|
unless options[:auto]
|
628
874
|
if options[:took]
|
629
875
|
took = wwid.chronify_qty(options[:took])
|
630
|
-
|
876
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
631
877
|
end
|
632
878
|
|
633
|
-
|
879
|
+
raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
634
880
|
|
635
|
-
|
881
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
636
882
|
|
637
883
|
if options[:at]
|
638
|
-
finish_date = wwid.chronify(options[:at])
|
639
|
-
|
884
|
+
finish_date = wwid.chronify(options[:at], guess: :begin)
|
885
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
640
886
|
|
641
887
|
date = options[:took] ? finish_date - took : finish_date
|
642
888
|
elsif options[:back]
|
643
889
|
date = wwid.chronify(options[:back])
|
644
890
|
|
645
|
-
|
891
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
646
892
|
elsif options[:took]
|
647
893
|
date = wwid.chronify_qty(options[:took])
|
648
894
|
else
|
@@ -653,44 +899,42 @@ command :finish do |c|
|
|
653
899
|
if options[:tag].nil?
|
654
900
|
tags = []
|
655
901
|
else
|
656
|
-
tags = options[:tag].
|
657
|
-
options[:bool] = case options[:bool]
|
658
|
-
when /(and|all)/i
|
659
|
-
'AND'
|
660
|
-
when /(any|or)/i
|
661
|
-
'OR'
|
662
|
-
when /(not|none)/i
|
663
|
-
'NOT'
|
664
|
-
else
|
665
|
-
'AND'
|
666
|
-
end
|
902
|
+
tags = options[:tag].to_tags
|
667
903
|
end
|
668
904
|
|
669
|
-
|
905
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
670
906
|
|
671
|
-
|
907
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
908
|
+
|
909
|
+
if options[:interactive]
|
910
|
+
count = 0
|
911
|
+
else
|
912
|
+
count = args[0] ? args[0].to_i : 1
|
913
|
+
end
|
672
914
|
|
673
|
-
count = args[0] ? args[0].to_i : 1
|
674
915
|
opts = {
|
675
|
-
archive: options[:
|
916
|
+
archive: options[:archive],
|
676
917
|
back: date,
|
677
918
|
count: count,
|
678
919
|
date: options[:date],
|
679
920
|
search: options[:search],
|
680
|
-
section: section,
|
921
|
+
section: options[:section],
|
681
922
|
sequential: options[:auto],
|
682
923
|
tag: tags,
|
683
|
-
tag_bool: options[:bool],
|
924
|
+
tag_bool: options[:bool].normalize_bool,
|
684
925
|
tags: ['done'],
|
685
|
-
unfinished: options[:unfinished]
|
926
|
+
unfinished: options[:unfinished],
|
927
|
+
remove: options[:remove],
|
928
|
+
interactive: options[:interactive]
|
686
929
|
}
|
930
|
+
|
687
931
|
wwid.tag_last(opts)
|
688
932
|
end
|
689
933
|
end
|
690
934
|
|
691
935
|
desc 'Repeat last entry as new entry'
|
692
|
-
command [
|
693
|
-
c.desc '
|
936
|
+
command %i[again resume] do |c|
|
937
|
+
c.desc 'Get last entry from a specific section'
|
694
938
|
c.arg_name 'NAME'
|
695
939
|
c.flag %i[s section], default_value: 'All'
|
696
940
|
|
@@ -703,45 +947,38 @@ command [:again, :resume] do |c|
|
|
703
947
|
c.flag [:tag]
|
704
948
|
|
705
949
|
c.desc 'Repeat last entry matching search. Surround with
|
706
|
-
slashes for regex (e.g. "/query/").'
|
950
|
+
slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
|
707
951
|
c.arg_name 'QUERY'
|
708
952
|
c.flag [:search]
|
709
953
|
|
710
954
|
c.desc 'Boolean used to combine multiple tags'
|
711
955
|
c.arg_name 'BOOLEAN'
|
712
|
-
c.flag [:bool], must_match:
|
956
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
957
|
+
|
958
|
+
c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
|
959
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
713
960
|
|
714
961
|
c.desc 'Note'
|
715
962
|
c.arg_name 'TEXT'
|
716
963
|
c.flag %i[n note]
|
717
964
|
|
965
|
+
c.desc 'Select item to resume from a menu of matching entries'
|
966
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
967
|
+
|
718
968
|
c.action do |_global_options, options, _args|
|
719
|
-
tags = options[:tag].nil? ? [] : options[:tag].
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
'NOT'
|
727
|
-
else
|
728
|
-
'AND'
|
729
|
-
end
|
730
|
-
opts = {
|
731
|
-
in: options[:in],
|
732
|
-
note: options[:n],
|
733
|
-
search: options[:search],
|
734
|
-
section: options[:s],
|
735
|
-
tag: tags,
|
736
|
-
tag_bool: options[:bool]
|
737
|
-
}
|
738
|
-
wwid.restart_last(opts)
|
969
|
+
tags = options[:tag].nil? ? [] : options[:tag].to_tags
|
970
|
+
opts = options
|
971
|
+
opts[:tag] = tags
|
972
|
+
opts[:tag_bool] = options[:bool].normalize_bool
|
973
|
+
opts[:interactive] = options[:interactive]
|
974
|
+
|
975
|
+
wwid.repeat_last(opts)
|
739
976
|
end
|
740
977
|
end
|
741
978
|
|
742
979
|
desc 'Add tag(s) to last entry'
|
743
980
|
long_desc 'Add (or remove) tags from the last entry, or from multiple entries
|
744
|
-
(with `--count`), entries matching a search (with `--search), or entries
|
981
|
+
(with `--count`), entries matching a search (with `--search`), or entries
|
745
982
|
containing another tag (with `--tag`).
|
746
983
|
|
747
984
|
When removing tags with `-r`, wildcards are allowed (`*` to match
|
@@ -755,13 +992,19 @@ long_desc 'Add (or remove) tags from the last entry, or from multiple entries
|
|
755
992
|
Tag name arguments do not need to be prefixed with @.'
|
756
993
|
arg_name 'TAG', :multiple
|
757
994
|
command :tag do |c|
|
995
|
+
c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created'
|
996
|
+
c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created'
|
997
|
+
c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries'
|
998
|
+
c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)'
|
999
|
+
c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding'
|
1000
|
+
|
758
1001
|
c.desc 'Section'
|
759
1002
|
c.arg_name 'SECTION_NAME'
|
760
1003
|
c.flag %i[s section], default_value: 'All'
|
761
1004
|
|
762
1005
|
c.desc 'How many recent entries to tag (0 for all)'
|
763
1006
|
c.arg_name 'COUNT'
|
764
|
-
c.flag %i[c count], default_value: 1
|
1007
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
765
1008
|
|
766
1009
|
c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
|
767
1010
|
c.arg_name 'ORIG_TAG'
|
@@ -790,18 +1033,21 @@ command :tag do |c|
|
|
790
1033
|
c.arg_name 'TAG'
|
791
1034
|
c.flag [:tag]
|
792
1035
|
|
793
|
-
c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/")'
|
1036
|
+
c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
794
1037
|
c.arg_name 'QUERY'
|
795
1038
|
c.flag [:search]
|
796
1039
|
|
797
1040
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
798
1041
|
c.arg_name 'BOOLEAN'
|
799
|
-
c.flag [:bool], must_match:
|
1042
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1043
|
+
|
1044
|
+
c.desc 'Select item(s) to tag from a menu of matching entries'
|
1045
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
800
1046
|
|
801
1047
|
c.action do |_global_options, options, args|
|
802
|
-
|
1048
|
+
raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
|
803
1049
|
|
804
|
-
|
1050
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
805
1051
|
|
806
1052
|
section = 'All'
|
807
1053
|
|
@@ -813,17 +1059,7 @@ command :tag do |c|
|
|
813
1059
|
if options[:tag].nil?
|
814
1060
|
search_tags = []
|
815
1061
|
else
|
816
|
-
search_tags = options[:tag].
|
817
|
-
options[:bool] = case options[:bool]
|
818
|
-
when /(and|all)/i
|
819
|
-
'AND'
|
820
|
-
when /(any|or)/i
|
821
|
-
'OR'
|
822
|
-
when /(not|none)/i
|
823
|
-
'NOT'
|
824
|
-
else
|
825
|
-
'AND'
|
826
|
-
end
|
1062
|
+
search_tags = options[:tag].to_tags
|
827
1063
|
end
|
828
1064
|
|
829
1065
|
if options[:autotag]
|
@@ -838,7 +1074,13 @@ command :tag do |c|
|
|
838
1074
|
tags.map! { |tag| tag.sub(/^@/, '').strip }
|
839
1075
|
end
|
840
1076
|
|
841
|
-
|
1077
|
+
if options[:interactive]
|
1078
|
+
count = 0
|
1079
|
+
options[:force] = true
|
1080
|
+
else
|
1081
|
+
count = options[:count].to_i
|
1082
|
+
end
|
1083
|
+
|
842
1084
|
|
843
1085
|
if count.zero? && !options[:force]
|
844
1086
|
if options[:search]
|
@@ -869,45 +1111,108 @@ command :tag do |c|
|
|
869
1111
|
options[:section] = section
|
870
1112
|
options[:tag] = search_tags
|
871
1113
|
options[:tags] = tags
|
872
|
-
options[:tag_bool] = options[:bool]
|
873
|
-
|
874
|
-
# opts = {
|
875
|
-
# autotag: options[:a],
|
876
|
-
# count: count,
|
877
|
-
# date: options[:date],
|
878
|
-
# iregex: options[:iregex]
|
879
|
-
# remove: options[:r],
|
880
|
-
# search: options[:search],
|
881
|
-
# section: section,
|
882
|
-
# tag: search_tags,
|
883
|
-
# tag_bool: options[:bool],
|
884
|
-
# tags: tags,
|
885
|
-
# unfinished: options[:unfinished]
|
886
|
-
# }
|
1114
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
1115
|
+
|
887
1116
|
wwid.tag_last(options)
|
888
1117
|
end
|
889
1118
|
end
|
890
1119
|
|
891
|
-
desc 'Mark last entry as
|
1120
|
+
desc 'Mark last entry as flagged'
|
892
1121
|
command [:mark, :flag] do |c|
|
1122
|
+
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
1123
|
+
c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
|
1124
|
+
c.example 'doing flag --interactive --search "/(develop|cod)ing/"', desc: 'Find entries matching regular expression and create a menu allowing multiple selections, selected items will be @flagged'
|
1125
|
+
|
893
1126
|
c.desc 'Section'
|
894
|
-
c.arg_name '
|
895
|
-
c.flag %i[s section]
|
1127
|
+
c.arg_name 'SECTION_NAME'
|
1128
|
+
c.flag %i[s section], default_value: 'All'
|
896
1129
|
|
897
|
-
c.desc '
|
1130
|
+
c.desc 'How many recent entries to tag (0 for all)'
|
1131
|
+
c.arg_name 'COUNT'
|
1132
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
1133
|
+
|
1134
|
+
c.desc 'Don\'t ask permission to flag all entries when count is 0'
|
1135
|
+
c.switch %i[force], negatable: false, default_value: false
|
1136
|
+
|
1137
|
+
c.desc 'Include current date/time with tag'
|
1138
|
+
c.switch %i[d date], negatable: false, default_value: false
|
1139
|
+
|
1140
|
+
c.desc 'Remove flag'
|
898
1141
|
c.switch %i[r remove], negatable: false, default_value: false
|
899
1142
|
|
900
|
-
c.desc '
|
1143
|
+
c.desc 'Flag last entry (or entries) not marked @done'
|
901
1144
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
902
1145
|
|
1146
|
+
c.desc 'Flag the last entry containing TAG.
|
1147
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
1148
|
+
c.arg_name 'TAG'
|
1149
|
+
c.flag [:tag]
|
1150
|
+
|
1151
|
+
c.desc 'Flag the last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
1152
|
+
c.arg_name 'QUERY'
|
1153
|
+
c.flag [:search]
|
1154
|
+
|
1155
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1156
|
+
c.arg_name 'BOOLEAN'
|
1157
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1158
|
+
|
1159
|
+
c.desc 'Select item(s) to flag from a menu of matching entries'
|
1160
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1161
|
+
|
903
1162
|
c.action do |_global_options, options, _args|
|
904
|
-
mark =
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
1163
|
+
mark = settings['marker_tag'] || 'flagged'
|
1164
|
+
|
1165
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
1166
|
+
|
1167
|
+
section = 'All'
|
1168
|
+
|
1169
|
+
if options[:section]
|
1170
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1171
|
+
end
|
1172
|
+
|
1173
|
+
if options[:tag].nil?
|
1174
|
+
search_tags = []
|
1175
|
+
else
|
1176
|
+
search_tags = options[:tag].to_tags
|
1177
|
+
end
|
1178
|
+
|
1179
|
+
if options[:interactive]
|
1180
|
+
count = 0
|
1181
|
+
options[:force] = true
|
1182
|
+
else
|
1183
|
+
count = options[:count].to_i
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
if count.zero? && !options[:force]
|
1187
|
+
if options[:search]
|
1188
|
+
section_q = ' matching your search terms'
|
1189
|
+
elsif options[:tag]
|
1190
|
+
section_q = ' matching your tag search'
|
1191
|
+
elsif section == 'All'
|
1192
|
+
section_q = ''
|
1193
|
+
else
|
1194
|
+
section_q = " in section #{section}"
|
1195
|
+
end
|
1196
|
+
|
1197
|
+
|
1198
|
+
question = if options[:remove]
|
1199
|
+
"Are you sure you want to unflag all entries#{section_q}"
|
1200
|
+
else
|
1201
|
+
"Are you sure you want to flag all records#{section_q}"
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
res = wwid.yn(question, default_response: false)
|
1205
|
+
|
1206
|
+
exit_now! 'Cancelled' unless res
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
options[:count] = count
|
1210
|
+
options[:section] = section
|
1211
|
+
options[:tag] = search_tags
|
1212
|
+
options[:tags] = [mark]
|
1213
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
1214
|
+
|
1215
|
+
wwid.tag_last(options)
|
911
1216
|
end
|
912
1217
|
end
|
913
1218
|
|
@@ -918,13 +1223,19 @@ long_desc %(
|
|
918
1223
|
)
|
919
1224
|
arg_name '[SECTION|@TAGS]'
|
920
1225
|
command :show do |c|
|
1226
|
+
c.example 'doing show Currently', desc: 'Show entries in the Currently section'
|
1227
|
+
c.example 'doing show @project1', desc: 'Show entries tagged @project1'
|
1228
|
+
c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
|
1229
|
+
c.example 'doing show Ideas --from "mon to fri" --tag doing', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
|
1230
|
+
c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
|
1231
|
+
|
921
1232
|
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
922
1233
|
c.arg_name 'TAG'
|
923
1234
|
c.flag [:tag]
|
924
1235
|
|
925
1236
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
926
1237
|
c.arg_name 'BOOLEAN'
|
927
|
-
c.flag %i[b bool], must_match:
|
1238
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
928
1239
|
|
929
1240
|
c.desc 'Max count to show'
|
930
1241
|
c.arg_name 'MAX'
|
@@ -942,13 +1253,13 @@ command :show do |c|
|
|
942
1253
|
c.arg_name 'DATE_STRING'
|
943
1254
|
c.flag [:after]
|
944
1255
|
|
945
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1256
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
946
1257
|
c.arg_name 'QUERY'
|
947
1258
|
c.flag [:search]
|
948
1259
|
|
949
1260
|
c.desc 'Sort order (asc/desc)'
|
950
1261
|
c.arg_name 'ORDER'
|
951
|
-
c.flag %i[s sort], must_match:
|
1262
|
+
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
952
1263
|
|
953
1264
|
c.desc %(
|
954
1265
|
Date range to show, or a single day to filter date on.
|
@@ -966,21 +1277,26 @@ command :show do |c|
|
|
966
1277
|
|
967
1278
|
c.desc 'Sort tags by (name|time)'
|
968
1279
|
default = 'time'
|
969
|
-
default =
|
1280
|
+
default = settings['tag_sort'] || 'name'
|
970
1281
|
c.arg_name 'KEY'
|
971
1282
|
c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
|
972
1283
|
|
973
1284
|
c.desc 'Tag sort direction (asc|desc)'
|
974
1285
|
c.arg_name 'DIRECTION'
|
975
|
-
c.flag [:tag_order], must_match:
|
1286
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
976
1287
|
|
977
1288
|
c.desc 'Only show items with recorded time intervals'
|
978
1289
|
c.switch [:only_timed], default_value: false, negatable: false
|
979
1290
|
|
980
|
-
c.desc '
|
1291
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1292
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1293
|
+
|
1294
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
981
1295
|
c.arg_name 'FORMAT'
|
982
|
-
c.flag %i[o output]
|
983
|
-
c.action do |
|
1296
|
+
c.flag %i[o output]
|
1297
|
+
c.action do |global_options, options, args|
|
1298
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1299
|
+
|
984
1300
|
tag_filter = false
|
985
1301
|
tags = []
|
986
1302
|
if args.length.positive?
|
@@ -994,8 +1310,15 @@ command :show do |c|
|
|
994
1310
|
when /^@/
|
995
1311
|
section = 'All'
|
996
1312
|
else
|
997
|
-
|
998
|
-
|
1313
|
+
begin
|
1314
|
+
section = wwid.guess_section(args[0])
|
1315
|
+
rescue WrongCommand => exception
|
1316
|
+
cmd = commands[:view]
|
1317
|
+
action = cmd.send(:get_action, nil)
|
1318
|
+
return action.call(global_options, options, args)
|
1319
|
+
end
|
1320
|
+
|
1321
|
+
raise InvalidSection, "No such section: #{args[0]}" unless section
|
999
1322
|
|
1000
1323
|
args.shift
|
1001
1324
|
end
|
@@ -1007,72 +1330,51 @@ command :show do |c|
|
|
1007
1330
|
end
|
1008
1331
|
end
|
1009
1332
|
else
|
1010
|
-
section =
|
1333
|
+
section = settings['current_section']
|
1011
1334
|
end
|
1012
1335
|
|
1013
|
-
tags.concat(options[:tag].
|
1014
|
-
options[:bool] = case options[:bool]
|
1015
|
-
when /(and|all)/i
|
1016
|
-
'AND'
|
1017
|
-
when /(any|or)/i
|
1018
|
-
'OR'
|
1019
|
-
when /(not|none)/i
|
1020
|
-
'NOT'
|
1021
|
-
else
|
1022
|
-
'AND'
|
1023
|
-
end
|
1336
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
1024
1337
|
|
1025
1338
|
unless tags.empty?
|
1026
1339
|
tag_filter = {
|
1027
1340
|
'tags' => tags,
|
1028
|
-
'bool' => options[:bool]
|
1341
|
+
'bool' => options[:bool].normalize_bool
|
1029
1342
|
}
|
1030
1343
|
end
|
1031
1344
|
|
1032
1345
|
if options[:from]
|
1346
|
+
|
1033
1347
|
date_string = options[:from]
|
1034
1348
|
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
1035
1349
|
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
1036
|
-
start = wwid.chronify(dates[0])
|
1037
|
-
finish = wwid.chronify(dates[2])
|
1350
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1351
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1038
1352
|
else
|
1039
|
-
start = wwid.chronify(date_string)
|
1353
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1040
1354
|
finish = false
|
1041
1355
|
end
|
1042
|
-
|
1356
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1043
1357
|
dates = [start, finish]
|
1044
1358
|
end
|
1045
1359
|
|
1046
1360
|
options[:times] = true if options[:totals]
|
1047
1361
|
|
1048
|
-
tags_color =
|
1362
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1049
1363
|
|
1050
|
-
|
1051
|
-
|
1052
|
-
|
1053
|
-
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
|
1060
|
-
|
1061
|
-
|
1062
|
-
|
1063
|
-
|
1064
|
-
order: options[:s],
|
1065
|
-
output: options[:output],
|
1066
|
-
search: options[:search],
|
1067
|
-
section: section,
|
1068
|
-
sort_tags: options[:sort_tags],
|
1069
|
-
tag_filter: tag_filter,
|
1070
|
-
tag_order: tag_order,
|
1071
|
-
tags_color: tags_color,
|
1072
|
-
times: options[:t],
|
1073
|
-
totals: options[:totals]
|
1074
|
-
}
|
1075
|
-
puts wwid.list_section(opts)
|
1364
|
+
opt = options.dup
|
1365
|
+
|
1366
|
+
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1367
|
+
opt[:count] = options[:count].to_i
|
1368
|
+
opt[:date_filter] = dates
|
1369
|
+
opt[:highlight] = true
|
1370
|
+
opt[:order] = options[:sort].normalize_order
|
1371
|
+
opt[:section] = section
|
1372
|
+
opt[:tag] = nil
|
1373
|
+
opt[:tag_filter] = tag_filter
|
1374
|
+
opt[:tag_order] = options[:tag_order].normalize_order
|
1375
|
+
opt[:tags_color] = tags_color
|
1376
|
+
|
1377
|
+
Doing::Pager.page wwid.list_section(opt)
|
1076
1378
|
end
|
1077
1379
|
end
|
1078
1380
|
|
@@ -1084,7 +1386,12 @@ long_desc <<~'EODESC'
|
|
1084
1386
|
EODESC
|
1085
1387
|
|
1086
1388
|
arg_name 'SEARCH_PATTERN'
|
1087
|
-
command [
|
1389
|
+
command %i[grep search] do |c|
|
1390
|
+
c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
|
1391
|
+
c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
|
1392
|
+
c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
|
1393
|
+
c.example 'doing search --before 12/21 "doing wiki"', desc: 'Find entries containing "doing wiki" with entry dates before 12/21 of the current year'
|
1394
|
+
|
1088
1395
|
c.desc 'Section'
|
1089
1396
|
c.arg_name 'NAME'
|
1090
1397
|
c.flag %i[s section], default_value: 'All'
|
@@ -1097,9 +1404,9 @@ command [:grep, :search] do |c|
|
|
1097
1404
|
c.arg_name 'DATE_STRING'
|
1098
1405
|
c.flag [:after]
|
1099
1406
|
|
1100
|
-
c.desc
|
1407
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1101
1408
|
c.arg_name 'FORMAT'
|
1102
|
-
c.flag %i[o output]
|
1409
|
+
c.flag %i[o output]
|
1103
1410
|
|
1104
1411
|
c.desc 'Show time intervals on @done tasks'
|
1105
1412
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1109,36 +1416,31 @@ command [:grep, :search] do |c|
|
|
1109
1416
|
|
1110
1417
|
c.desc 'Sort tags by (name|time)'
|
1111
1418
|
default = 'time'
|
1112
|
-
default =
|
1419
|
+
default = settings['tag_sort'] || 'name'
|
1113
1420
|
c.arg_name 'KEY'
|
1114
1421
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1115
1422
|
|
1116
1423
|
c.desc 'Only show items with recorded time intervals'
|
1117
1424
|
c.switch [:only_timed], default_value: false, negatable: false
|
1118
1425
|
|
1426
|
+
c.desc 'Display an interactive menu of results to perform further operations'
|
1427
|
+
c.switch %i[i interactive], default_value: false, negatable: false
|
1428
|
+
|
1119
1429
|
c.action do |_global_options, options, args|
|
1120
|
-
|
1430
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1121
1431
|
|
1122
|
-
|
1432
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1433
|
+
|
1434
|
+
section = wwid.guess_section(options[:section]) if options[:section]
|
1123
1435
|
|
1124
1436
|
options[:times] = true if options[:totals]
|
1125
1437
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1438
|
+
options[:highlight] = true
|
1439
|
+
options[:search] = args.join(' ')
|
1440
|
+
options[:section] = section
|
1441
|
+
options[:tags_color] = tags_color
|
1126
1442
|
|
1127
|
-
|
1128
|
-
after: options[:after],
|
1129
|
-
before: options[:before],
|
1130
|
-
highlight: true,
|
1131
|
-
only_timed: options[:only_timed],
|
1132
|
-
output: options[:output],
|
1133
|
-
search: args.join(' '),
|
1134
|
-
section: section,
|
1135
|
-
sort_tags: options[:sort_tags],
|
1136
|
-
tags_color: tags_color,
|
1137
|
-
times: options[:times],
|
1138
|
-
totals: options[:totals]
|
1139
|
-
}
|
1140
|
-
|
1141
|
-
puts wwid.list_section(opts)
|
1443
|
+
Doing::Pager.page wwid.list_section(options)
|
1142
1444
|
end
|
1143
1445
|
end
|
1144
1446
|
|
@@ -1146,6 +1448,11 @@ desc 'List recent entries'
|
|
1146
1448
|
default_value 10
|
1147
1449
|
arg_name 'COUNT'
|
1148
1450
|
command :recent do |c|
|
1451
|
+
c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
|
1452
|
+
c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
|
1453
|
+
c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
|
1454
|
+
c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
|
1455
|
+
|
1149
1456
|
c.desc 'Section'
|
1150
1457
|
c.arg_name 'NAME'
|
1151
1458
|
c.flag %i[s section], default_value: 'All'
|
@@ -1158,32 +1465,42 @@ command :recent do |c|
|
|
1158
1465
|
|
1159
1466
|
c.desc 'Sort tags by (name|time)'
|
1160
1467
|
default = 'time'
|
1161
|
-
default =
|
1468
|
+
default = settings['tag_sort'] || 'name'
|
1162
1469
|
c.arg_name 'KEY'
|
1163
1470
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1164
1471
|
|
1472
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1473
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1474
|
+
|
1165
1475
|
c.action do |global_options, options, args|
|
1166
1476
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
1167
1477
|
|
1168
1478
|
unless global_options[:version]
|
1169
|
-
if
|
1170
|
-
config_count =
|
1479
|
+
if settings['templates']['recent'].key?('count')
|
1480
|
+
config_count = settings['templates']['recent']['count'].to_i
|
1171
1481
|
else
|
1172
1482
|
config_count = 10
|
1173
1483
|
end
|
1174
|
-
|
1484
|
+
|
1485
|
+
if options[:interactive]
|
1486
|
+
count = 0
|
1487
|
+
else
|
1488
|
+
count = args.empty? ? config_count : args[0].to_i
|
1489
|
+
end
|
1490
|
+
|
1175
1491
|
options[:t] = true if options[:totals]
|
1176
1492
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1177
|
-
tags_color =
|
1493
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1178
1494
|
|
1179
1495
|
opts = {
|
1180
1496
|
sort_tags: options[:sort_tags],
|
1181
1497
|
tags_color: tags_color,
|
1182
1498
|
times: options[:t],
|
1183
|
-
totals: options[:totals]
|
1499
|
+
totals: options[:totals],
|
1500
|
+
interactive: options[:interactive]
|
1184
1501
|
}
|
1185
1502
|
|
1186
|
-
|
1503
|
+
Doing::Pager::page wwid.recent(count, section.cap_first, opts)
|
1187
1504
|
|
1188
1505
|
end
|
1189
1506
|
end
|
@@ -1191,6 +1508,11 @@ end
|
|
1191
1508
|
|
1192
1509
|
desc 'List entries from today'
|
1193
1510
|
command :today do |c|
|
1511
|
+
c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
|
1512
|
+
c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
|
1513
|
+
c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
|
1514
|
+
c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
|
1515
|
+
|
1194
1516
|
c.desc 'Specify a section'
|
1195
1517
|
c.arg_name 'NAME'
|
1196
1518
|
c.flag %i[s section], default_value: 'All'
|
@@ -1203,13 +1525,13 @@ command :today do |c|
|
|
1203
1525
|
|
1204
1526
|
c.desc 'Sort tags by (name|time)'
|
1205
1527
|
default = 'time'
|
1206
|
-
default =
|
1528
|
+
default = settings['tag_sort'] || 'name'
|
1207
1529
|
c.arg_name 'KEY'
|
1208
1530
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1209
1531
|
|
1210
|
-
c.desc
|
1532
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1211
1533
|
c.arg_name 'FORMAT'
|
1212
|
-
c.flag %i[o output]
|
1534
|
+
c.flag %i[o output]
|
1213
1535
|
|
1214
1536
|
c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
|
1215
1537
|
c.arg_name 'TIME_STRING'
|
@@ -1220,6 +1542,8 @@ command :today do |c|
|
|
1220
1542
|
c.flag [:after]
|
1221
1543
|
|
1222
1544
|
c.action do |_global_options, options, _args|
|
1545
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1546
|
+
|
1223
1547
|
options[:t] = true if options[:totals]
|
1224
1548
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1225
1549
|
opt = {
|
@@ -1227,9 +1551,10 @@ command :today do |c|
|
|
1227
1551
|
before: options[:before],
|
1228
1552
|
section: options[:section],
|
1229
1553
|
sort_tags: options[:sort_tags],
|
1230
|
-
totals: options[:totals]
|
1554
|
+
totals: options[:totals],
|
1555
|
+
order: settings.dig('templates', 'today', 'order')
|
1231
1556
|
}
|
1232
|
-
|
1557
|
+
Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
|
1233
1558
|
end
|
1234
1559
|
end
|
1235
1560
|
|
@@ -1239,6 +1564,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
|
|
1239
1564
|
it will create a range.)
|
1240
1565
|
arg_name 'DATE_STRING'
|
1241
1566
|
command :on do |c|
|
1567
|
+
c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
|
1568
|
+
c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
|
1569
|
+
c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
|
1570
|
+
|
1242
1571
|
c.desc 'Section'
|
1243
1572
|
c.arg_name 'NAME'
|
1244
1573
|
c.flag %i[s section], default_value: 'All'
|
@@ -1251,38 +1580,40 @@ command :on do |c|
|
|
1251
1580
|
|
1252
1581
|
c.desc 'Sort tags by (name|time)'
|
1253
1582
|
default = 'time'
|
1254
|
-
default =
|
1583
|
+
default = settings['tag_sort'] || 'name'
|
1255
1584
|
c.arg_name 'KEY'
|
1256
1585
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1257
1586
|
|
1258
|
-
c.desc
|
1587
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1259
1588
|
c.arg_name 'FORMAT'
|
1260
|
-
c.flag %i[o output]
|
1589
|
+
c.flag %i[o output]
|
1261
1590
|
|
1262
1591
|
c.action do |_global_options, options, args|
|
1263
|
-
|
1592
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1593
|
+
|
1594
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
1264
1595
|
|
1265
1596
|
date_string = args.join(' ')
|
1266
1597
|
|
1267
1598
|
if date_string =~ / (to|through|thru) /
|
1268
1599
|
dates = date_string.split(/ (to|through|thru) /)
|
1269
|
-
start = wwid.chronify(dates[0])
|
1270
|
-
finish = wwid.chronify(dates[2])
|
1600
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1601
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1271
1602
|
else
|
1272
|
-
start = wwid.chronify(date_string)
|
1603
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1273
1604
|
finish = false
|
1274
1605
|
end
|
1275
1606
|
|
1276
|
-
|
1607
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1277
1608
|
|
1278
1609
|
message = "Date interpreted as #{start}"
|
1279
1610
|
message += " to #{finish}" if finish
|
1280
|
-
|
1611
|
+
Doing.logger.debug(message)
|
1281
1612
|
|
1282
1613
|
options[:t] = true if options[:totals]
|
1283
1614
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1284
1615
|
|
1285
|
-
|
1616
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1286
1617
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1287
1618
|
end
|
1288
1619
|
end
|
@@ -1292,6 +1623,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
|
|
1292
1623
|
and "2d" would be interpreted as "two days ago.")
|
1293
1624
|
arg_name 'DATE_STRING'
|
1294
1625
|
command :since do |c|
|
1626
|
+
c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
|
1627
|
+
c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
|
1628
|
+
|
1295
1629
|
c.desc 'Section'
|
1296
1630
|
c.arg_name 'NAME'
|
1297
1631
|
c.flag %i[s section], default_value: 'All'
|
@@ -1304,47 +1638,52 @@ command :since do |c|
|
|
1304
1638
|
|
1305
1639
|
c.desc 'Sort tags by (name|time)'
|
1306
1640
|
default = 'time'
|
1307
|
-
default =
|
1641
|
+
default = settings['tag_sort'] || 'name'
|
1308
1642
|
c.arg_name 'KEY'
|
1309
1643
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1310
1644
|
|
1311
|
-
c.desc
|
1645
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1312
1646
|
c.arg_name 'FORMAT'
|
1313
|
-
c.flag %i[o output]
|
1647
|
+
c.flag %i[o output]
|
1314
1648
|
|
1315
1649
|
c.action do |_global_options, options, args|
|
1316
|
-
|
1650
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1651
|
+
|
1652
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
1317
1653
|
|
1318
1654
|
date_string = args.join(' ')
|
1319
1655
|
|
1320
|
-
date_string
|
1321
|
-
date_string.sub!(/(
|
1656
|
+
date_string.sub!(/(day) (\d)/, '\1 at \2')
|
1657
|
+
date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
|
1322
1658
|
|
1323
|
-
start = wwid.chronify(date_string)
|
1659
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1324
1660
|
finish = Time.now
|
1325
1661
|
|
1326
|
-
|
1662
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
1327
1663
|
|
1328
|
-
|
1329
|
-
wwid.results.push(message)
|
1664
|
+
Doing.logger.debug("Date interpreted as #{start} through the current time")
|
1330
1665
|
|
1331
1666
|
options[:t] = true if options[:totals]
|
1332
1667
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1333
1668
|
|
1334
|
-
|
1669
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1335
1670
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1336
1671
|
end
|
1337
1672
|
end
|
1338
1673
|
|
1339
1674
|
desc 'List entries from yesterday'
|
1340
1675
|
command :yesterday do |c|
|
1676
|
+
c.example 'doing yesterday', desc: 'List all entries from the previous day'
|
1677
|
+
c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
|
1678
|
+
c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
|
1679
|
+
|
1341
1680
|
c.desc 'Specify a section'
|
1342
1681
|
c.arg_name 'NAME'
|
1343
1682
|
c.flag %i[s section], default_value: 'All'
|
1344
1683
|
|
1345
|
-
c.desc
|
1684
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1346
1685
|
c.arg_name 'FORMAT'
|
1347
|
-
c.flag %i[o output]
|
1686
|
+
c.flag %i[o output]
|
1348
1687
|
|
1349
1688
|
c.desc 'Show time intervals on @done tasks'
|
1350
1689
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1353,8 +1692,7 @@ command :yesterday do |c|
|
|
1353
1692
|
c.switch [:totals], default_value: false, negatable: false
|
1354
1693
|
|
1355
1694
|
c.desc 'Sort tags by (name|time)'
|
1356
|
-
default = '
|
1357
|
-
default = wwid.config['tag_sort'] if wwid.config.key?('tag_sort')
|
1695
|
+
default = settings['tag_sort'] || 'name'
|
1358
1696
|
c.arg_name 'KEY'
|
1359
1697
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1360
1698
|
|
@@ -1368,33 +1706,40 @@ command :yesterday do |c|
|
|
1368
1706
|
|
1369
1707
|
c.desc 'Tag sort direction (asc|desc)'
|
1370
1708
|
c.arg_name 'DIRECTION'
|
1371
|
-
c.flag [:tag_order], must_match:
|
1709
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1372
1710
|
|
1373
1711
|
c.action do |_global_options, options, _args|
|
1374
|
-
|
1375
|
-
|
1376
|
-
else
|
1377
|
-
'asc'
|
1378
|
-
end
|
1712
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1713
|
+
|
1379
1714
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1715
|
+
|
1380
1716
|
opt = {
|
1381
1717
|
after: options[:after],
|
1382
1718
|
before: options[:before],
|
1383
1719
|
sort_tags: options[:sort_tags],
|
1384
|
-
tag_order: options[:tag_order],
|
1385
|
-
totals: options[:totals]
|
1720
|
+
tag_order: options[:tag_order].normalize_order,
|
1721
|
+
totals: options[:totals],
|
1722
|
+
order: settings.dig('templates', 'today', 'order')
|
1386
1723
|
}
|
1387
|
-
|
1724
|
+
Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
|
1388
1725
|
end
|
1389
1726
|
end
|
1390
1727
|
|
1391
1728
|
desc 'Show the last entry, optionally edit'
|
1392
1729
|
command :last do |c|
|
1730
|
+
c.example 'doing last', desc: 'Show the most recent entry in all sections'
|
1731
|
+
c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
|
1732
|
+
c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
|
1733
|
+
c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
|
1734
|
+
c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
|
1735
|
+
c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
|
1736
|
+
c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
|
1737
|
+
|
1393
1738
|
c.desc 'Specify a section'
|
1394
1739
|
c.arg_name 'NAME'
|
1395
1740
|
c.flag %i[s section], default_value: 'All'
|
1396
1741
|
|
1397
|
-
c.desc "Edit entry with #{
|
1742
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
1398
1743
|
c.switch %i[e editor], negatable: false, default_value: false
|
1399
1744
|
|
1400
1745
|
c.desc 'Tag filter, combine multiple tags with a comma.'
|
@@ -1403,19 +1748,19 @@ command :last do |c|
|
|
1403
1748
|
|
1404
1749
|
c.desc 'Tag boolean'
|
1405
1750
|
c.arg_name 'BOOLEAN'
|
1406
|
-
c.flag [:bool], must_match:
|
1751
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1407
1752
|
|
1408
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1753
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
1409
1754
|
c.arg_name 'QUERY'
|
1410
1755
|
c.flag [:search]
|
1411
1756
|
|
1412
|
-
c.action do |
|
1413
|
-
|
1757
|
+
c.action do |global_options, options, _args|
|
1758
|
+
raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1414
1759
|
|
1415
1760
|
if options[:tag].nil?
|
1416
1761
|
tags = []
|
1417
1762
|
else
|
1418
|
-
tags = options[:tag].
|
1763
|
+
tags = options[:tag].to_tags
|
1419
1764
|
options[:bool] = case options[:bool]
|
1420
1765
|
when /(any|or)/i
|
1421
1766
|
:or
|
@@ -1430,7 +1775,7 @@ command :last do |c|
|
|
1430
1775
|
if options[:editor]
|
1431
1776
|
wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
|
1432
1777
|
else
|
1433
|
-
|
1778
|
+
Doing::Pager::page wwid.last(times: true, section: options[:s],
|
1434
1779
|
options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
|
1435
1780
|
end
|
1436
1781
|
end
|
@@ -1439,7 +1784,7 @@ end
|
|
1439
1784
|
desc 'List sections'
|
1440
1785
|
command :sections do |c|
|
1441
1786
|
c.desc 'List in single column'
|
1442
|
-
c.switch %i[c column], default_value: false
|
1787
|
+
c.switch %i[c column], negatable: false, default_value: false
|
1443
1788
|
|
1444
1789
|
c.action do |_global_options, options, _args|
|
1445
1790
|
joiner = options[:c] ? "\n" : "\t"
|
@@ -1452,17 +1797,19 @@ command :choose do |c|
|
|
1452
1797
|
c.action do |_global_options, _options, _args|
|
1453
1798
|
section = wwid.choose_section
|
1454
1799
|
|
1455
|
-
|
1800
|
+
Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
|
1456
1801
|
end
|
1457
1802
|
end
|
1458
1803
|
|
1459
1804
|
desc 'Add a new section to the "doing" file'
|
1460
1805
|
arg_name 'SECTION_NAME'
|
1461
1806
|
command :add_section do |c|
|
1807
|
+
c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
|
1808
|
+
|
1462
1809
|
c.action do |_global_options, _options, args|
|
1463
|
-
|
1810
|
+
raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
1464
1811
|
|
1465
|
-
wwid.add_section(args
|
1812
|
+
wwid.add_section(args.join(' ').cap_first)
|
1466
1813
|
wwid.write(wwid.doing_file)
|
1467
1814
|
end
|
1468
1815
|
end
|
@@ -1470,18 +1817,80 @@ end
|
|
1470
1817
|
desc 'List available color variables for configuration templates and views'
|
1471
1818
|
command :colors do |c|
|
1472
1819
|
c.action do |_global_options, _options, _args|
|
1473
|
-
clrs = wwid.colors
|
1474
1820
|
bgs = []
|
1475
1821
|
fgs = []
|
1476
|
-
|
1477
|
-
if
|
1478
|
-
bgs.push("#{
|
1822
|
+
colors::attributes.each do |color|
|
1823
|
+
if color.to_s =~ /bg/
|
1824
|
+
bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
|
1479
1825
|
else
|
1480
|
-
fgs.push("#{
|
1826
|
+
fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
|
1827
|
+
end
|
1828
|
+
end
|
1829
|
+
out = []
|
1830
|
+
out << fgs.join("\n")
|
1831
|
+
out << bgs.join("\n")
|
1832
|
+
Doing::Pager.page out.join("\n")
|
1833
|
+
end
|
1834
|
+
end
|
1835
|
+
|
1836
|
+
desc 'List installed plugins'
|
1837
|
+
long_desc %(Lists available plugins, including user-installed plugins.
|
1838
|
+
|
1839
|
+
Export plugins are available with the `--output` flag on commands that support it.
|
1840
|
+
|
1841
|
+
Import plugins are available using `doing import --type PLUGIN`.
|
1842
|
+
)
|
1843
|
+
command :plugins do |c|
|
1844
|
+
c.example 'doing plugins', desc: 'List all plugins'
|
1845
|
+
c.example 'doing plugins -t import', desc: 'List all import plugins'
|
1846
|
+
|
1847
|
+
c.desc 'List plugins of type (import, export)'
|
1848
|
+
c.arg_name 'TYPE'
|
1849
|
+
c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
|
1850
|
+
|
1851
|
+
c.desc 'List in single column for completion'
|
1852
|
+
c.switch %i[c column], negatable: false, default_value: false
|
1853
|
+
|
1854
|
+
c.action do |_global_options, options, _args|
|
1855
|
+
Doing::Plugins.list_plugins(options)
|
1856
|
+
end
|
1857
|
+
end
|
1858
|
+
|
1859
|
+
desc 'Generate shell completion scripts'
|
1860
|
+
command :completion do |c|
|
1861
|
+
c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
|
1862
|
+
c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
|
1863
|
+
c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
|
1864
|
+
c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
|
1865
|
+
|
1866
|
+
c.desc 'Shell to generate for (bash, zsh, fish)'
|
1867
|
+
c.arg_name 'SHELL'
|
1868
|
+
c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
|
1869
|
+
|
1870
|
+
c.desc 'File to write output to'
|
1871
|
+
c.arg_name 'PATH'
|
1872
|
+
c.flag %i[f file], default_value: 'stdout'
|
1873
|
+
|
1874
|
+
c.action do |_global_options, options, _args|
|
1875
|
+
script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
|
1876
|
+
|
1877
|
+
case options[:type]
|
1878
|
+
when /^b/
|
1879
|
+
result = `ruby #{File.join(script_dir, 'generate_bash_completions.rb')}`
|
1880
|
+
when /^z/
|
1881
|
+
result = `ruby #{File.join(script_dir, 'generate_zsh_completions.rb')}`
|
1882
|
+
when /^f/
|
1883
|
+
result = `ruby #{File.join(script_dir, 'generate_fish_completions.rb')}`
|
1884
|
+
end
|
1885
|
+
|
1886
|
+
if options[:file] =~ /^stdout$/i
|
1887
|
+
$stdout.puts result
|
1888
|
+
else
|
1889
|
+
File.open(File.expand_path(options[:file]), 'w') do |f|
|
1890
|
+
f.puts result
|
1481
1891
|
end
|
1892
|
+
Doing.logger.warn('File written:', "#{options[:type]} completions written to #{options[:file]}")
|
1482
1893
|
end
|
1483
|
-
puts fgs.join("\n")
|
1484
|
-
puts bgs.join("\n")
|
1485
1894
|
end
|
1486
1895
|
end
|
1487
1896
|
|
@@ -1489,6 +1898,9 @@ desc 'Display a user-created view'
|
|
1489
1898
|
long_desc 'Command line options override view configuration'
|
1490
1899
|
arg_name 'VIEW_NAME'
|
1491
1900
|
command :view do |c|
|
1901
|
+
c.example 'doing view color', desc: 'Display entries according to config for view "color"'
|
1902
|
+
c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'
|
1903
|
+
|
1492
1904
|
c.desc 'Section'
|
1493
1905
|
c.arg_name 'NAME'
|
1494
1906
|
c.flag %i[s section]
|
@@ -1497,9 +1909,9 @@ command :view do |c|
|
|
1497
1909
|
c.arg_name 'COUNT'
|
1498
1910
|
c.flag %i[c count], must_match: /^\d+$/, type: Integer
|
1499
1911
|
|
1500
|
-
c.desc
|
1912
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1501
1913
|
c.arg_name 'FORMAT'
|
1502
|
-
c.flag %i[o output]
|
1914
|
+
c.flag %i[o output]
|
1503
1915
|
|
1504
1916
|
c.desc 'Show time intervals on @done tasks'
|
1505
1917
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1516,9 +1928,9 @@ command :view do |c|
|
|
1516
1928
|
|
1517
1929
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
1518
1930
|
c.arg_name 'BOOLEAN'
|
1519
|
-
c.flag %i[b bool], must_match:
|
1931
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
1520
1932
|
|
1521
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1933
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
1522
1934
|
c.arg_name 'QUERY'
|
1523
1935
|
c.flag [:search]
|
1524
1936
|
|
@@ -1528,7 +1940,7 @@ command :view do |c|
|
|
1528
1940
|
|
1529
1941
|
c.desc 'Tag sort direction (asc|desc)'
|
1530
1942
|
c.arg_name 'DIRECTION'
|
1531
|
-
c.flag [:tag_order], must_match:
|
1943
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER
|
1532
1944
|
|
1533
1945
|
c.desc 'View entries older than date'
|
1534
1946
|
c.arg_name 'DATE_STRING'
|
@@ -1541,23 +1953,36 @@ command :view do |c|
|
|
1541
1953
|
c.desc 'Only show items with recorded time intervals (override view settings)'
|
1542
1954
|
c.switch [:only_timed], default_value: false, negatable: false
|
1543
1955
|
|
1544
|
-
c.
|
1545
|
-
|
1956
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1957
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1958
|
+
|
1959
|
+
c.action do |global_options, options, args|
|
1960
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1961
|
+
|
1962
|
+
raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1546
1963
|
|
1547
1964
|
title = if args.empty?
|
1548
1965
|
wwid.choose_view
|
1549
1966
|
else
|
1550
|
-
|
1967
|
+
begin
|
1968
|
+
wwid.guess_view(args[0])
|
1969
|
+
rescue WrongCommand => exception
|
1970
|
+
cmd = commands[:show]
|
1971
|
+
options[:sort] = 'asc'
|
1972
|
+
action = cmd.send(:get_action, nil)
|
1973
|
+
return action.call(global_options, options, args)
|
1974
|
+
end
|
1551
1975
|
end
|
1552
1976
|
|
1553
1977
|
if options[:section]
|
1554
1978
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1555
1979
|
else
|
1556
|
-
section =
|
1980
|
+
section = settings['current_section']
|
1557
1981
|
end
|
1558
1982
|
|
1559
1983
|
view = wwid.get_view(title)
|
1560
1984
|
if view
|
1985
|
+
page_title = view.key?('title') ? view['title'] : title.cap_first
|
1561
1986
|
only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
|
1562
1987
|
true
|
1563
1988
|
else
|
@@ -1565,7 +1990,7 @@ command :view do |c|
|
|
1565
1990
|
end
|
1566
1991
|
|
1567
1992
|
template = view.key?('template') ? view['template'] : nil
|
1568
|
-
|
1993
|
+
date_format = view.key?('date_format') ? view['date_format'] : nil
|
1569
1994
|
tags_color = view.key?('tags_color') ? view['tags_color'] : nil
|
1570
1995
|
tag_filter = false
|
1571
1996
|
if options[:tag]
|
@@ -1583,7 +2008,7 @@ command :view do |c|
|
|
1583
2008
|
end
|
1584
2009
|
|
1585
2010
|
# If the -o/--output flag was specified, override any default in the view template
|
1586
|
-
options[:
|
2011
|
+
options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
|
1587
2012
|
|
1588
2013
|
count = if options[:c]
|
1589
2014
|
options[:c]
|
@@ -1593,18 +2018,23 @@ command :view do |c|
|
|
1593
2018
|
section = if options[:s]
|
1594
2019
|
section
|
1595
2020
|
else
|
1596
|
-
view.key?('section') ? view['section'] :
|
2021
|
+
view.key?('section') ? view['section'] : settings['current_section']
|
1597
2022
|
end
|
1598
|
-
order = view.key?('order') ? view['order'] : 'asc'
|
2023
|
+
order = view.key?('order') ? view['order'].normalize_order : 'asc'
|
1599
2024
|
|
1600
2025
|
totals = if options[:totals]
|
1601
2026
|
true
|
1602
2027
|
else
|
1603
2028
|
view.key?('totals') ? view['totals'] : false
|
1604
2029
|
end
|
2030
|
+
tag_order = if options[:tag_order]
|
2031
|
+
options[:tag_order].normalize_order
|
2032
|
+
else
|
2033
|
+
view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
|
2034
|
+
end
|
1605
2035
|
|
1606
2036
|
options[:t] = true if totals
|
1607
|
-
options[:output]&.downcase
|
2037
|
+
output_format = options[:output]&.downcase || 'template'
|
1608
2038
|
|
1609
2039
|
options[:sort_tags] = if options[:tag_sort]
|
1610
2040
|
options[:tag_sort] =~ /^n/i ? true : false
|
@@ -1613,40 +2043,50 @@ command :view do |c|
|
|
1613
2043
|
else
|
1614
2044
|
false
|
1615
2045
|
end
|
2046
|
+
if view.key?('after') && !options[:after]
|
2047
|
+
options[:after] = view['after']
|
2048
|
+
end
|
1616
2049
|
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
|
1621
|
-
else
|
1622
|
-
'asc'
|
1623
|
-
end
|
2050
|
+
if view.key?('before') && !options[:before]
|
2051
|
+
options[:before] = view['before']
|
2052
|
+
end
|
1624
2053
|
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1632
|
-
|
1633
|
-
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
1638
|
-
tag_order: tag_order,
|
1639
|
-
tags_color: tags_color,
|
1640
|
-
template: template,
|
1641
|
-
times: options[:t],
|
1642
|
-
totals: totals
|
1643
|
-
}
|
2054
|
+
if view.key?('from')
|
2055
|
+
date_string = view['from']
|
2056
|
+
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2057
|
+
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2058
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
2059
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
2060
|
+
else
|
2061
|
+
start = wwid.chronify(date_string, guess: :begin)
|
2062
|
+
finish = false
|
2063
|
+
end
|
2064
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
2065
|
+
dates = [start, finish]
|
2066
|
+
end
|
1644
2067
|
|
1645
|
-
|
2068
|
+
opts = options
|
2069
|
+
opts[:output] = output_format
|
2070
|
+
opts[:count] = count
|
2071
|
+
opts[:format] = date_format
|
2072
|
+
opts[:highlight] = options[:color]
|
2073
|
+
opts[:only_timed] = only_timed
|
2074
|
+
opts[:order] = order
|
2075
|
+
opts[:section] = section
|
2076
|
+
opts[:tag_filter] = tag_filter
|
2077
|
+
opts[:tag_order] = tag_order
|
2078
|
+
opts[:tags_color] = tags_color
|
2079
|
+
opts[:template] = template
|
2080
|
+
opts[:totals] = totals
|
2081
|
+
opts[:page_title] = page_title
|
2082
|
+
opts[:date_filter] = dates
|
2083
|
+
opts[:output] = options[:interactive] ? nil : options[:output]
|
2084
|
+
|
2085
|
+
Doing::Pager.page wwid.list_section(opts)
|
1646
2086
|
elsif title.instance_of?(FalseClass)
|
1647
|
-
|
2087
|
+
raise UserCancelled, 'Cancelled' unless res
|
1648
2088
|
else
|
1649
|
-
|
2089
|
+
raise InvalidView, "View #{title} not found in config"
|
1650
2090
|
end
|
1651
2091
|
end
|
1652
2092
|
end
|
@@ -1663,9 +2103,18 @@ command :views do |c|
|
|
1663
2103
|
end
|
1664
2104
|
|
1665
2105
|
desc 'Move entries between sections'
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
2106
|
+
long_desc %(Argument can be a section name to move all entries from a section,
|
2107
|
+
or start with an "@" to move entries matching a tag.
|
2108
|
+
|
2109
|
+
Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
|
2110
|
+
arg_name 'SECTION_OR_TAG'
|
2111
|
+
default_value settings['current_section']
|
2112
|
+
command %i[archive move] do |c|
|
2113
|
+
c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
|
2114
|
+
c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
|
2115
|
+
c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
|
2116
|
+
c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
|
2117
|
+
|
1669
2118
|
c.desc 'How many items to keep (ignored if archiving by tag or search)'
|
1670
2119
|
c.arg_name 'X'
|
1671
2120
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1683,7 +2132,7 @@ command :archive do |c|
|
|
1683
2132
|
|
1684
2133
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1685
2134
|
c.arg_name 'BOOLEAN'
|
1686
|
-
c.flag [:bool], must_match:
|
2135
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1687
2136
|
|
1688
2137
|
c.desc 'Search filter'
|
1689
2138
|
c.arg_name 'QUERY'
|
@@ -1696,7 +2145,7 @@ command :archive do |c|
|
|
1696
2145
|
|
1697
2146
|
c.action do |_global_options, options, args|
|
1698
2147
|
if args.empty?
|
1699
|
-
section =
|
2148
|
+
section = settings['current_section']
|
1700
2149
|
tags = []
|
1701
2150
|
elsif args[0] =~ /^all/i
|
1702
2151
|
section = 'all'
|
@@ -1708,34 +2157,25 @@ command :archive do |c|
|
|
1708
2157
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
1709
2158
|
end
|
1710
2159
|
|
1711
|
-
|
2160
|
+
raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
1712
2161
|
|
1713
|
-
tags.concat(options[:tag].
|
2162
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
2163
|
+
|
2164
|
+
opts = options
|
2165
|
+
opts[:bool] = options[:bool].normalize_bool
|
2166
|
+
opts[:destination] = options[:to]
|
2167
|
+
opts[:tags] = tags
|
1714
2168
|
|
1715
|
-
options[:bool] = case options[:bool]
|
1716
|
-
when /(and|all)/i
|
1717
|
-
'AND'
|
1718
|
-
when /(any|or)/i
|
1719
|
-
'OR'
|
1720
|
-
when /(not|none)/i
|
1721
|
-
'NOT'
|
1722
|
-
else
|
1723
|
-
'AND'
|
1724
|
-
end
|
1725
|
-
opts = {
|
1726
|
-
before: options[:before],
|
1727
|
-
bool: options[:bool],
|
1728
|
-
destination: options[:to],
|
1729
|
-
keep: options[:keep],
|
1730
|
-
search: options[:search],
|
1731
|
-
tags: tags
|
1732
|
-
}
|
1733
2169
|
wwid.archive(section, opts)
|
1734
2170
|
end
|
1735
2171
|
end
|
1736
2172
|
|
1737
2173
|
desc 'Move entries to archive file'
|
1738
2174
|
command :rotate do |c|
|
2175
|
+
c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
|
2176
|
+
c.example 'doing rotate --section Archive --keep 10', desc: 'Move entries in the Archive section to a secondary file, keeping the most recent 10 entries'
|
2177
|
+
c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'
|
2178
|
+
|
1739
2179
|
c.desc 'How many items to keep in each section (most recent)'
|
1740
2180
|
c.arg_name 'X'
|
1741
2181
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1750,7 +2190,7 @@ command :rotate do |c|
|
|
1750
2190
|
|
1751
2191
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1752
2192
|
c.arg_name 'BOOLEAN'
|
1753
|
-
c.flag [:bool], must_match:
|
2193
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1754
2194
|
|
1755
2195
|
c.desc 'Search filter'
|
1756
2196
|
c.arg_name 'QUERY'
|
@@ -1766,23 +2206,14 @@ command :rotate do |c|
|
|
1766
2206
|
options[:section] = wwid.guess_section(options[:section])
|
1767
2207
|
end
|
1768
2208
|
|
1769
|
-
options[:bool] =
|
1770
|
-
when /(and|all)/i
|
1771
|
-
'AND'
|
1772
|
-
when /(any|or)/i
|
1773
|
-
'OR'
|
1774
|
-
when /(not|none)/i
|
1775
|
-
'NOT'
|
1776
|
-
else
|
1777
|
-
'AND'
|
1778
|
-
end
|
2209
|
+
options[:bool] = options[:bool].normalize_bool
|
1779
2210
|
|
1780
2211
|
wwid.rotate(options)
|
1781
2212
|
end
|
1782
2213
|
end
|
1783
2214
|
|
1784
2215
|
desc 'Open the "doing" file in an editor'
|
1785
|
-
long_desc "`doing open` defaults to using the editor_app setting in #{
|
2216
|
+
long_desc "`doing open` defaults to using the editor_app setting in #{config.config_file} (#{settings.key?('editor_app') ? settings['editor_app'] : 'not set'})."
|
1786
2217
|
command :open do |c|
|
1787
2218
|
if `uname` =~ /Darwin/
|
1788
2219
|
c.desc 'Open with app name'
|
@@ -1793,8 +2224,6 @@ command :open do |c|
|
|
1793
2224
|
c.arg_name 'BUNDLE_ID'
|
1794
2225
|
c.flag %i[b bundle_id]
|
1795
2226
|
end
|
1796
|
-
c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
|
1797
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
1798
2227
|
|
1799
2228
|
c.action do |_global_options, options, _args|
|
1800
2229
|
params = options.dup
|
@@ -1806,30 +2235,54 @@ command :open do |c|
|
|
1806
2235
|
system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
|
1807
2236
|
elsif options[:bundle_id]
|
1808
2237
|
system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
|
1809
|
-
elsif
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
2238
|
+
elsif Doing::Util.find_default_editor('doing_file')
|
2239
|
+
editor = Doing::Util.find_default_editor('doing_file')
|
2240
|
+
if Doing::Util.exec_available(editor)
|
2241
|
+
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
2242
|
+
else
|
2243
|
+
system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
|
2244
|
+
end
|
1815
2245
|
else
|
1816
2246
|
system %(open "#{File.expand_path(wwid.doing_file)}")
|
1817
2247
|
end
|
1818
|
-
|
1819
2248
|
else
|
1820
|
-
|
2249
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
1821
2250
|
|
1822
|
-
system %(
|
2251
|
+
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
1823
2252
|
end
|
1824
2253
|
end
|
1825
2254
|
end
|
1826
2255
|
|
1827
|
-
desc 'Edit the configuration file'
|
2256
|
+
desc 'Edit the configuration file or output a value from it'
|
2257
|
+
long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
|
2258
|
+
If local configurations are found in the path between the current directory
|
2259
|
+
and `~/.doingrc`, a menu will allow you to select which to open in the editor.
|
2260
|
+
|
2261
|
+
It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
|
2262
|
+
|
2263
|
+
Use `doing config -d` to output the configuration to the terminal, and
|
2264
|
+
provide a dot-separated key path to get a specific value. Shows the current value
|
2265
|
+
including keys/overrides set by local configs.)
|
2266
|
+
arg_name 'KEY_PATH'
|
1828
2267
|
command :config do |c|
|
2268
|
+
c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
|
2269
|
+
c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
|
2270
|
+
c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'
|
2271
|
+
|
1829
2272
|
c.desc 'Editor to use'
|
1830
2273
|
c.arg_name 'EDITOR'
|
1831
2274
|
c.flag %i[e editor], default_value: nil
|
1832
2275
|
|
2276
|
+
c.desc 'Show a config key value based on arguments. Separate key paths with colons or dots, e.g. "export_templates.html". Empty arguments outputs the entire config.'
|
2277
|
+
c.switch %i[d dump], negatable: false
|
2278
|
+
|
2279
|
+
c.desc 'Format for --dump (json|yaml|raw)'
|
2280
|
+
c.arg_name 'FORMAT'
|
2281
|
+
c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
|
2282
|
+
|
2283
|
+
c.desc 'Update config file with missing configuration options'
|
2284
|
+
c.switch %i[u update], default_value: false, negatable: false
|
2285
|
+
|
1833
2286
|
if `uname` =~ /Darwin/
|
1834
2287
|
c.desc 'Application to use'
|
1835
2288
|
c.arg_name 'APP_NAME'
|
@@ -1839,31 +2292,82 @@ command :config do |c|
|
|
1839
2292
|
c.arg_name 'BUNDLE_ID'
|
1840
2293
|
c.flag [:b]
|
1841
2294
|
|
1842
|
-
c.desc "Use the config_editor_app defined in ~/.doingrc (#{
|
2295
|
+
c.desc "Use the config_editor_app defined in ~/.doingrc (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
|
1843
2296
|
c.switch [:x]
|
1844
2297
|
end
|
1845
2298
|
|
1846
|
-
c.action do |_global_options, options,
|
2299
|
+
c.action do |_global_options, options, args|
|
2300
|
+
if options[:update]
|
2301
|
+
config.configure({rewrite: true, ignore_local: true})
|
2302
|
+
return
|
2303
|
+
end
|
2304
|
+
|
2305
|
+
if options[:dump]
|
2306
|
+
keypath = args.join('.')
|
2307
|
+
cfg = config.value_for_key(keypath)
|
2308
|
+
|
2309
|
+
if cfg
|
2310
|
+
$stdout.puts case options[:output]
|
2311
|
+
when /^j/
|
2312
|
+
JSON.pretty_generate(cfg)
|
2313
|
+
when /^r/
|
2314
|
+
cfg
|
2315
|
+
else
|
2316
|
+
YAML.dump(cfg)
|
2317
|
+
end
|
2318
|
+
else
|
2319
|
+
Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
|
2320
|
+
end
|
2321
|
+
Doing.logger.output_results
|
2322
|
+
return
|
2323
|
+
end
|
2324
|
+
|
2325
|
+
if config.additional_configs.count.positive?
|
2326
|
+
choices = [config.config_file]
|
2327
|
+
choices.concat(config.additional_configs)
|
2328
|
+
res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
|
2329
|
+
|
2330
|
+
raise UserCancelled, 'Cancelled' unless res
|
2331
|
+
|
2332
|
+
config_file = res.strip || config.config_file
|
2333
|
+
else
|
2334
|
+
config_file = config.config_file
|
2335
|
+
end
|
2336
|
+
|
1847
2337
|
if `uname` =~ /Darwin/
|
1848
2338
|
if options[:x]
|
1849
|
-
|
2339
|
+
editor = Doing::Util.find_default_editor('config')
|
2340
|
+
if editor
|
2341
|
+
if Doing::Util.exec_available(editor)
|
2342
|
+
system %(#{editor} "#{config_file}")
|
2343
|
+
else
|
2344
|
+
`open -a "#{editor}" "#{config_file}"`
|
2345
|
+
end
|
2346
|
+
else
|
2347
|
+
raise InvalidArgument, 'No viable editor found in config or environment.'
|
2348
|
+
end
|
1850
2349
|
elsif options[:a] || options[:b]
|
1851
2350
|
if options[:a]
|
1852
|
-
`open -a "#{options[:a]}" "#{
|
2351
|
+
`open -a "#{options[:a]}" "#{config_file}"`
|
1853
2352
|
elsif options[:b]
|
1854
|
-
`open -b #{options[:b]} "#{
|
2353
|
+
`open -b #{options[:b]} "#{config_file}"`
|
1855
2354
|
end
|
1856
2355
|
else
|
1857
|
-
|
2356
|
+
editor = options[:e] || Doing::Util.find_default_editor('config')
|
1858
2357
|
|
1859
|
-
editor
|
1860
|
-
|
2358
|
+
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
2359
|
+
|
2360
|
+
if Doing::Util.exec_available(editor)
|
2361
|
+
system %(#{editor} "#{config_file}")
|
2362
|
+
else
|
2363
|
+
`open -a "#{editor}" "#{config_file}"`
|
2364
|
+
end
|
1861
2365
|
end
|
1862
2366
|
else
|
1863
|
-
|
2367
|
+
editor = options[:e] || Doing::Util.default_editor
|
2368
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
|
1864
2369
|
|
1865
|
-
editor
|
1866
|
-
system %(#{editor} "#{wwid.config_file}")
|
2370
|
+
system %(#{editor} "#{config_file}")
|
1867
2371
|
end
|
1868
2372
|
end
|
1869
2373
|
end
|
@@ -1881,12 +2385,19 @@ command :undo do |c|
|
|
1881
2385
|
end
|
1882
2386
|
|
1883
2387
|
desc 'Import entries from an external source'
|
1884
|
-
long_desc
|
2388
|
+
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
|
1885
2389
|
arg_name 'PATH'
|
1886
2390
|
command :import do |c|
|
1887
|
-
c.desc
|
2391
|
+
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
1888
2392
|
c.arg_name 'TYPE'
|
1889
|
-
c.flag :type, default_value: '
|
2393
|
+
c.flag :type, default_value: 'doing'
|
2394
|
+
|
2395
|
+
c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
2396
|
+
c.arg_name 'QUERY'
|
2397
|
+
c.flag [:search]
|
2398
|
+
|
2399
|
+
c.desc 'Only import items with recorded time intervals'
|
2400
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
1890
2401
|
|
1891
2402
|
c.desc 'Target section'
|
1892
2403
|
c.arg_name 'NAME'
|
@@ -1903,53 +2414,67 @@ command :import do |c|
|
|
1903
2414
|
c.arg_name 'PREFIX'
|
1904
2415
|
c.flag :prefix
|
1905
2416
|
|
2417
|
+
c.desc 'Import entries older than date'
|
2418
|
+
c.arg_name 'DATE_STRING'
|
2419
|
+
c.flag [:before]
|
2420
|
+
|
2421
|
+
c.desc 'Import entries newer than date'
|
2422
|
+
c.arg_name 'DATE_STRING'
|
2423
|
+
c.flag [:after]
|
2424
|
+
|
2425
|
+
c.desc %(
|
2426
|
+
Date range to import. Date range argument should be quoted. Date specifications can be natural language.
|
2427
|
+
To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
|
2428
|
+
Has no effect unless the import plugin has implemented date range filtering.
|
2429
|
+
)
|
2430
|
+
c.arg_name 'DATE_OR_RANGE'
|
2431
|
+
c.flag %i[f from]
|
2432
|
+
|
1906
2433
|
c.desc 'Allow entries that overlap existing times'
|
1907
2434
|
c.switch [:overlap], negatable: true
|
1908
2435
|
|
1909
2436
|
c.action do |_global_options, options, args|
|
1910
2437
|
|
1911
2438
|
if options[:section]
|
1912
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1913
|
-
else
|
1914
|
-
section = wwid.config['current_section']
|
2439
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1915
2440
|
end
|
1916
2441
|
|
1917
|
-
if options[:
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
wwid.import_timing(path, options)
|
1927
|
-
wwid.write(wwid.doing_file)
|
2442
|
+
if options[:from]
|
2443
|
+
date_string = options[:from]
|
2444
|
+
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2445
|
+
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2446
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
2447
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
2448
|
+
else
|
2449
|
+
start = wwid.chronify(date_string, guess: :begin)
|
2450
|
+
finish = false
|
1928
2451
|
end
|
2452
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
2453
|
+
dates = [start, finish]
|
2454
|
+
end
|
2455
|
+
|
2456
|
+
if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
|
2457
|
+
options[:no_overlap] = !options[:overlap]
|
2458
|
+
options[:date_filter] = dates
|
2459
|
+
wwid.import(args, options)
|
2460
|
+
wwid.write(wwid.doing_file)
|
1929
2461
|
else
|
1930
|
-
|
2462
|
+
raise InvalidPluginType, "Invalid import type: #{options[:type]}"
|
1931
2463
|
end
|
1932
2464
|
end
|
1933
2465
|
end
|
1934
2466
|
|
1935
2467
|
pre do |global, _command, _options, _args|
|
1936
|
-
|
1937
|
-
wwid.config_file = global[:config_file]
|
1938
|
-
wwid.configure({ ignore_local: true })
|
1939
|
-
# wwid.results.push("Override config file #{wwid.config_file}")
|
1940
|
-
end
|
1941
|
-
|
1942
|
-
if global[:doing_file]
|
1943
|
-
wwid.init_doing_file(global[:doing_file])
|
1944
|
-
else
|
1945
|
-
wwid.init_doing_file
|
1946
|
-
end
|
1947
|
-
|
1948
|
-
wwid.auto_tag = !global[:noauto]
|
2468
|
+
# global[:pager] ||= settings['paginate']
|
1949
2469
|
|
1950
|
-
|
2470
|
+
Doing::Pager.paginate = global[:pager]
|
1951
2471
|
|
1952
2472
|
$stdout.puts "doing v#{Doing::VERSION}" if global[:version]
|
2473
|
+
unless STDOUT.isatty
|
2474
|
+
Doing::Color::coloring = global[:pager] ? global[:color] : false
|
2475
|
+
else
|
2476
|
+
Doing::Color::coloring = global[:color]
|
2477
|
+
end
|
1953
2478
|
|
1954
2479
|
# Return true to proceed; false to abort and not call the
|
1955
2480
|
# chosen command
|
@@ -1958,20 +2483,56 @@ pre do |global, _command, _options, _args|
|
|
1958
2483
|
true
|
1959
2484
|
end
|
1960
2485
|
|
2486
|
+
on_error do |exception|
|
2487
|
+
if exception.kind_of?(SystemExit)
|
2488
|
+
false
|
2489
|
+
else
|
2490
|
+
# Doing.logger.error('Fatal:', exception)
|
2491
|
+
Doing.logger.output_results
|
2492
|
+
true
|
2493
|
+
end
|
2494
|
+
end
|
2495
|
+
|
1961
2496
|
post do |global, _command, _options, _args|
|
1962
2497
|
# Use skips_post before a command to skip this
|
1963
2498
|
# block on that command only
|
2499
|
+
Doing.logger.output_results
|
2500
|
+
end
|
2501
|
+
|
2502
|
+
around do |global, command, options, arguments, code|
|
2503
|
+
# Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
|
2504
|
+
Doing.logger.adjust_verbosity(global)
|
1964
2505
|
|
1965
2506
|
if global[:stdout]
|
1966
|
-
$stdout
|
2507
|
+
Doing.logger.logdev = $stdout
|
2508
|
+
end
|
2509
|
+
|
2510
|
+
wwid.default_option = global[:default]
|
2511
|
+
|
2512
|
+
if global[:config_file] && global[:config_file] != config.config_file
|
2513
|
+
Doing.logger.warn(format('%sWARNING:%s %sThe use of --config_file is deprecated, please set the environment variable DOING_CONFIG instead.', colors.flamingo, colors.default, colors.boldred))
|
2514
|
+
Doing.logger.warn(format('%sTo set it just for the current command, use: %sDOING_CONFIG=/path/to/doingrc doing [command]%s', colors.red, colors.boldwhite, colors.default))
|
2515
|
+
|
2516
|
+
cf = File.expand_path(global[:config_file])
|
2517
|
+
raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)
|
2518
|
+
|
2519
|
+
config.config_file = cf
|
2520
|
+
settings = config.configure({ ignore_local: true })
|
2521
|
+
end
|
2522
|
+
|
2523
|
+
if global[:doing_file]
|
2524
|
+
wwid.init_doing_file(global[:doing_file])
|
1967
2525
|
else
|
1968
|
-
|
2526
|
+
wwid.init_doing_file
|
1969
2527
|
end
|
1970
|
-
end
|
1971
2528
|
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
2529
|
+
wwid.auto_tag = !global[:noauto]
|
2530
|
+
|
2531
|
+
settings[:include_notes] = false unless global[:notes]
|
2532
|
+
|
2533
|
+
global[:wwid] = wwid
|
2534
|
+
|
2535
|
+
code.call
|
1975
2536
|
end
|
1976
2537
|
|
1977
2538
|
exit run(ARGV)
|