doing 1.0.92 → 2.0.5.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 +596 -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 +1012 -486
- data/doing.fish +278 -0
- data/doing.gemspec +34 -0
- data/doing.rdoc +1759 -0
- data/example_plugin.rb +209 -0
- data/generate_completions.sh +4 -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 +151 -0
- data/lib/completion/doing.bash +416 -0
- data/lib/completion/doing.fish +278 -0
- data/lib/doing/array.rb +8 -0
- data/lib/doing/cli_status.rb +66 -0
- data/lib/doing/colors.rb +136 -0
- data/lib/doing/configuration.rb +312 -0
- data/lib/doing/errors.rb +102 -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 +342 -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 +347 -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 -2356
- data/lib/doing/wwidfile.rb +117 -0
- data/lib/doing.rb +44 -4
- 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 +210 -0
- data/scripts/generate_fish_completions.rb +201 -0
- data/scripts/generate_zsh_completions.rb +164 -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 config.settings.dig('plugins', 'command_path')
|
61
|
+
commands_from File.expand_path(config.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
|
87
|
+
|
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
|
46
93
|
|
47
|
-
desc 'Use a specific configuration file'
|
48
|
-
flag [:config_file], default_value:
|
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 Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' 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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
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
|
119
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)
|
120
237
|
end
|
121
238
|
end
|
122
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 Doing::Errors::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 Doing::Errors::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,7 +334,7 @@ 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'
|
@@ -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 Doing::Errors::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 Doing::Errors::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]
|
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 Doing::Errors::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]
|
329
507
|
|
330
508
|
c.action do |_global_options, options, args|
|
331
|
-
|
509
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
510
|
+
|
511
|
+
raise Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
|
554
793
|
|
555
|
-
|
794
|
+
raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
795
|
+
|
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 Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
631
877
|
end
|
632
878
|
|
633
|
-
|
879
|
+
raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
634
880
|
|
635
|
-
|
881
|
+
raise Doing::Errors::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 Doing::Errors::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 Doing::Errors::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 Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
|
906
|
+
|
907
|
+
raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
670
908
|
|
671
|
-
|
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 Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
|
803
1049
|
|
804
|
-
|
1050
|
+
raise Doing::Errors::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,121 @@ 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 '
|
1120
|
+
# desc 'Autotag last X entries'
|
1121
|
+
# arg_name 'COUNT'
|
1122
|
+
# command :autotag do |c|
|
1123
|
+
# c.action do |global_options, options, args|
|
1124
|
+
# options = {
|
1125
|
+
# autotag: true,
|
1126
|
+
# count: args[0].to_i
|
1127
|
+
# }
|
1128
|
+
# cmd = commands[:tag]
|
1129
|
+
# cmd.action.(global_options, options, [])
|
1130
|
+
# end
|
1131
|
+
# end
|
1132
|
+
|
1133
|
+
desc 'Mark last entry as flagged'
|
892
1134
|
command [:mark, :flag] do |c|
|
1135
|
+
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
1136
|
+
c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
|
1137
|
+
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'
|
1138
|
+
|
893
1139
|
c.desc 'Section'
|
894
|
-
c.arg_name '
|
895
|
-
c.flag %i[s section]
|
1140
|
+
c.arg_name 'SECTION_NAME'
|
1141
|
+
c.flag %i[s section], default_value: 'All'
|
1142
|
+
|
1143
|
+
c.desc 'How many recent entries to tag (0 for all)'
|
1144
|
+
c.arg_name 'COUNT'
|
1145
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
1146
|
+
|
1147
|
+
c.desc 'Don\'t ask permission to flag all entries when count is 0'
|
1148
|
+
c.switch %i[force], negatable: false, default_value: false
|
896
1149
|
|
897
|
-
c.desc '
|
1150
|
+
c.desc 'Include current date/time with tag'
|
1151
|
+
c.switch %i[d date], negatable: false, default_value: false
|
1152
|
+
|
1153
|
+
c.desc 'Remove flag'
|
898
1154
|
c.switch %i[r remove], negatable: false, default_value: false
|
899
1155
|
|
900
|
-
c.desc '
|
1156
|
+
c.desc 'Flag last entry (or entries) not marked @done'
|
901
1157
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
902
1158
|
|
1159
|
+
c.desc 'Flag the last entry containing TAG.
|
1160
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool'
|
1161
|
+
c.arg_name 'TAG'
|
1162
|
+
c.flag [:tag]
|
1163
|
+
|
1164
|
+
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")'
|
1165
|
+
c.arg_name 'QUERY'
|
1166
|
+
c.flag [:search]
|
1167
|
+
|
1168
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
1169
|
+
c.arg_name 'BOOLEAN'
|
1170
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1171
|
+
|
1172
|
+
c.desc 'Select item(s) to flag from a menu of matching entries'
|
1173
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1174
|
+
|
903
1175
|
c.action do |_global_options, options, _args|
|
904
|
-
mark =
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
1176
|
+
mark = settings['marker_tag'] || 'flagged'
|
1177
|
+
|
1178
|
+
raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
1179
|
+
|
1180
|
+
section = 'All'
|
1181
|
+
|
1182
|
+
if options[:section]
|
1183
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
if options[:tag].nil?
|
1187
|
+
search_tags = []
|
1188
|
+
else
|
1189
|
+
search_tags = options[:tag].to_tags
|
1190
|
+
end
|
1191
|
+
|
1192
|
+
if options[:interactive]
|
1193
|
+
count = 0
|
1194
|
+
options[:force] = true
|
1195
|
+
else
|
1196
|
+
count = options[:count].to_i
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
if count.zero? && !options[:force]
|
1200
|
+
if options[:search]
|
1201
|
+
section_q = ' matching your search terms'
|
1202
|
+
elsif options[:tag]
|
1203
|
+
section_q = ' matching your tag search'
|
1204
|
+
elsif section == 'All'
|
1205
|
+
section_q = ''
|
1206
|
+
else
|
1207
|
+
section_q = " in section #{section}"
|
1208
|
+
end
|
1209
|
+
|
1210
|
+
|
1211
|
+
question = if options[:remove]
|
1212
|
+
"Are you sure you want to unflag all entries#{section_q}"
|
1213
|
+
else
|
1214
|
+
"Are you sure you want to flag all records#{section_q}"
|
1215
|
+
end
|
1216
|
+
|
1217
|
+
res = wwid.yn(question, default_response: false)
|
1218
|
+
|
1219
|
+
exit_now! 'Cancelled' unless res
|
1220
|
+
end
|
1221
|
+
|
1222
|
+
options[:count] = count
|
1223
|
+
options[:section] = section
|
1224
|
+
options[:tag] = search_tags
|
1225
|
+
options[:tags] = [mark]
|
1226
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
1227
|
+
|
1228
|
+
wwid.tag_last(options)
|
911
1229
|
end
|
912
1230
|
end
|
913
1231
|
|
@@ -918,13 +1236,19 @@ long_desc %(
|
|
918
1236
|
)
|
919
1237
|
arg_name '[SECTION|@TAGS]'
|
920
1238
|
command :show do |c|
|
1239
|
+
c.example 'doing show Currently', desc: 'Show entries in the Currently section'
|
1240
|
+
c.example 'doing show @project1', desc: 'Show entries tagged @project1'
|
1241
|
+
c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
|
1242
|
+
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.'
|
1243
|
+
c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
|
1244
|
+
|
921
1245
|
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
922
1246
|
c.arg_name 'TAG'
|
923
1247
|
c.flag [:tag]
|
924
1248
|
|
925
1249
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
926
1250
|
c.arg_name 'BOOLEAN'
|
927
|
-
c.flag %i[b bool], must_match:
|
1251
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
928
1252
|
|
929
1253
|
c.desc 'Max count to show'
|
930
1254
|
c.arg_name 'MAX'
|
@@ -942,13 +1266,13 @@ command :show do |c|
|
|
942
1266
|
c.arg_name 'DATE_STRING'
|
943
1267
|
c.flag [:after]
|
944
1268
|
|
945
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1269
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
946
1270
|
c.arg_name 'QUERY'
|
947
1271
|
c.flag [:search]
|
948
1272
|
|
949
1273
|
c.desc 'Sort order (asc/desc)'
|
950
1274
|
c.arg_name 'ORDER'
|
951
|
-
c.flag %i[s sort], must_match:
|
1275
|
+
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
952
1276
|
|
953
1277
|
c.desc %(
|
954
1278
|
Date range to show, or a single day to filter date on.
|
@@ -966,21 +1290,26 @@ command :show do |c|
|
|
966
1290
|
|
967
1291
|
c.desc 'Sort tags by (name|time)'
|
968
1292
|
default = 'time'
|
969
|
-
default =
|
1293
|
+
default = settings['tag_sort'] || 'name'
|
970
1294
|
c.arg_name 'KEY'
|
971
1295
|
c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
|
972
1296
|
|
973
1297
|
c.desc 'Tag sort direction (asc|desc)'
|
974
1298
|
c.arg_name 'DIRECTION'
|
975
|
-
c.flag [:tag_order], must_match:
|
1299
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
976
1300
|
|
977
1301
|
c.desc 'Only show items with recorded time intervals'
|
978
1302
|
c.switch [:only_timed], default_value: false, negatable: false
|
979
1303
|
|
980
|
-
c.desc '
|
1304
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1305
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1306
|
+
|
1307
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
981
1308
|
c.arg_name 'FORMAT'
|
982
|
-
c.flag %i[o output]
|
1309
|
+
c.flag %i[o output]
|
983
1310
|
c.action do |_global_options, options, args|
|
1311
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1312
|
+
|
984
1313
|
tag_filter = false
|
985
1314
|
tags = []
|
986
1315
|
if args.length.positive?
|
@@ -995,7 +1324,7 @@ command :show do |c|
|
|
995
1324
|
section = 'All'
|
996
1325
|
else
|
997
1326
|
section = wwid.guess_section(args[0])
|
998
|
-
|
1327
|
+
raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
|
999
1328
|
|
1000
1329
|
args.shift
|
1001
1330
|
end
|
@@ -1007,72 +1336,51 @@ command :show do |c|
|
|
1007
1336
|
end
|
1008
1337
|
end
|
1009
1338
|
else
|
1010
|
-
section =
|
1339
|
+
section = settings['current_section']
|
1011
1340
|
end
|
1012
1341
|
|
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
|
1342
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
1024
1343
|
|
1025
1344
|
unless tags.empty?
|
1026
1345
|
tag_filter = {
|
1027
1346
|
'tags' => tags,
|
1028
|
-
'bool' => options[:bool]
|
1347
|
+
'bool' => options[:bool].normalize_bool
|
1029
1348
|
}
|
1030
1349
|
end
|
1031
1350
|
|
1032
1351
|
if options[:from]
|
1352
|
+
|
1033
1353
|
date_string = options[:from]
|
1034
1354
|
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
1035
1355
|
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
1036
|
-
start = wwid.chronify(dates[0])
|
1037
|
-
finish = wwid.chronify(dates[2])
|
1356
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1357
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1038
1358
|
else
|
1039
|
-
start = wwid.chronify(date_string)
|
1359
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1040
1360
|
finish = false
|
1041
1361
|
end
|
1042
|
-
|
1362
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1043
1363
|
dates = [start, finish]
|
1044
1364
|
end
|
1045
1365
|
|
1046
1366
|
options[:times] = true if options[:totals]
|
1047
1367
|
|
1048
|
-
tags_color =
|
1368
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1049
1369
|
|
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)
|
1370
|
+
opt = options.dup
|
1371
|
+
|
1372
|
+
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1373
|
+
opt[:count] = options[:count].to_i
|
1374
|
+
opt[:date_filter] = dates
|
1375
|
+
opt[:highlight] = true
|
1376
|
+
opt[:order] = options[:sort].normalize_order
|
1377
|
+
opt[:section] = section
|
1378
|
+
opt[:tag] = nil
|
1379
|
+
opt[:tag_filter] = tag_filter
|
1380
|
+
opt[:tag_order] = options[:tag_order].normalize_order
|
1381
|
+
opt[:tags_color] = tags_color
|
1382
|
+
|
1383
|
+
Doing::Pager.page wwid.list_section(opt)
|
1076
1384
|
end
|
1077
1385
|
end
|
1078
1386
|
|
@@ -1084,7 +1392,12 @@ long_desc <<~'EODESC'
|
|
1084
1392
|
EODESC
|
1085
1393
|
|
1086
1394
|
arg_name 'SEARCH_PATTERN'
|
1087
|
-
command [
|
1395
|
+
command %i[grep search] do |c|
|
1396
|
+
c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
|
1397
|
+
c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
|
1398
|
+
c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
|
1399
|
+
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'
|
1400
|
+
|
1088
1401
|
c.desc 'Section'
|
1089
1402
|
c.arg_name 'NAME'
|
1090
1403
|
c.flag %i[s section], default_value: 'All'
|
@@ -1097,9 +1410,9 @@ command [:grep, :search] do |c|
|
|
1097
1410
|
c.arg_name 'DATE_STRING'
|
1098
1411
|
c.flag [:after]
|
1099
1412
|
|
1100
|
-
c.desc
|
1413
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1101
1414
|
c.arg_name 'FORMAT'
|
1102
|
-
c.flag %i[o output]
|
1415
|
+
c.flag %i[o output]
|
1103
1416
|
|
1104
1417
|
c.desc 'Show time intervals on @done tasks'
|
1105
1418
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1109,36 +1422,31 @@ command [:grep, :search] do |c|
|
|
1109
1422
|
|
1110
1423
|
c.desc 'Sort tags by (name|time)'
|
1111
1424
|
default = 'time'
|
1112
|
-
default =
|
1425
|
+
default = settings['tag_sort'] || 'name'
|
1113
1426
|
c.arg_name 'KEY'
|
1114
1427
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1115
1428
|
|
1116
1429
|
c.desc 'Only show items with recorded time intervals'
|
1117
1430
|
c.switch [:only_timed], default_value: false, negatable: false
|
1118
1431
|
|
1432
|
+
c.desc 'Display an interactive menu of results to perform further operations'
|
1433
|
+
c.switch %i[i interactive], default_value: false, negatable: false
|
1434
|
+
|
1119
1435
|
c.action do |_global_options, options, args|
|
1120
|
-
|
1436
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1437
|
+
|
1438
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1121
1439
|
|
1122
|
-
section = wwid.guess_section(options[:
|
1440
|
+
section = wwid.guess_section(options[:section]) if options[:section]
|
1123
1441
|
|
1124
1442
|
options[:times] = true if options[:totals]
|
1125
1443
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1444
|
+
options[:highlight] = true
|
1445
|
+
options[:search] = args.join(' ')
|
1446
|
+
options[:section] = section
|
1447
|
+
options[:tags_color] = tags_color
|
1126
1448
|
|
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)
|
1449
|
+
Doing::Pager.page wwid.list_section(options)
|
1142
1450
|
end
|
1143
1451
|
end
|
1144
1452
|
|
@@ -1146,6 +1454,11 @@ desc 'List recent entries'
|
|
1146
1454
|
default_value 10
|
1147
1455
|
arg_name 'COUNT'
|
1148
1456
|
command :recent do |c|
|
1457
|
+
c.example 'doing recent', desc: 'Show the 10 most recent entries across all sections'
|
1458
|
+
c.example 'doing recent 20', desc: 'Show the 20 most recent entries across all sections'
|
1459
|
+
c.example 'doing recent --section Currently 20', desc: 'List the 20 most recent entries from the Currently section'
|
1460
|
+
c.example 'doing recent --interactive 20', desc: 'Create a menu from the 20 most recent entries to perform batch actions on'
|
1461
|
+
|
1149
1462
|
c.desc 'Section'
|
1150
1463
|
c.arg_name 'NAME'
|
1151
1464
|
c.flag %i[s section], default_value: 'All'
|
@@ -1158,32 +1471,42 @@ command :recent do |c|
|
|
1158
1471
|
|
1159
1472
|
c.desc 'Sort tags by (name|time)'
|
1160
1473
|
default = 'time'
|
1161
|
-
default =
|
1474
|
+
default = settings['tag_sort'] || 'name'
|
1162
1475
|
c.arg_name 'KEY'
|
1163
1476
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1164
1477
|
|
1478
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1479
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1480
|
+
|
1165
1481
|
c.action do |global_options, options, args|
|
1166
1482
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
1167
1483
|
|
1168
1484
|
unless global_options[:version]
|
1169
|
-
if
|
1170
|
-
config_count =
|
1485
|
+
if settings['templates']['recent'].key?('count')
|
1486
|
+
config_count = settings['templates']['recent']['count'].to_i
|
1171
1487
|
else
|
1172
1488
|
config_count = 10
|
1173
1489
|
end
|
1174
|
-
|
1490
|
+
|
1491
|
+
if options[:interactive]
|
1492
|
+
count = 0
|
1493
|
+
else
|
1494
|
+
count = args.empty? ? config_count : args[0].to_i
|
1495
|
+
end
|
1496
|
+
|
1175
1497
|
options[:t] = true if options[:totals]
|
1176
1498
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1177
|
-
tags_color =
|
1499
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1178
1500
|
|
1179
1501
|
opts = {
|
1180
1502
|
sort_tags: options[:sort_tags],
|
1181
1503
|
tags_color: tags_color,
|
1182
1504
|
times: options[:t],
|
1183
|
-
totals: options[:totals]
|
1505
|
+
totals: options[:totals],
|
1506
|
+
interactive: options[:interactive]
|
1184
1507
|
}
|
1185
1508
|
|
1186
|
-
|
1509
|
+
Doing::Pager::page wwid.recent(count, section.cap_first, opts)
|
1187
1510
|
|
1188
1511
|
end
|
1189
1512
|
end
|
@@ -1191,6 +1514,11 @@ end
|
|
1191
1514
|
|
1192
1515
|
desc 'List entries from today'
|
1193
1516
|
command :today do |c|
|
1517
|
+
c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
|
1518
|
+
c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
|
1519
|
+
c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
|
1520
|
+
c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
|
1521
|
+
|
1194
1522
|
c.desc 'Specify a section'
|
1195
1523
|
c.arg_name 'NAME'
|
1196
1524
|
c.flag %i[s section], default_value: 'All'
|
@@ -1203,13 +1531,13 @@ command :today do |c|
|
|
1203
1531
|
|
1204
1532
|
c.desc 'Sort tags by (name|time)'
|
1205
1533
|
default = 'time'
|
1206
|
-
default =
|
1534
|
+
default = settings['tag_sort'] || 'name'
|
1207
1535
|
c.arg_name 'KEY'
|
1208
1536
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1209
1537
|
|
1210
|
-
c.desc
|
1538
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1211
1539
|
c.arg_name 'FORMAT'
|
1212
|
-
c.flag %i[o output]
|
1540
|
+
c.flag %i[o output]
|
1213
1541
|
|
1214
1542
|
c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
|
1215
1543
|
c.arg_name 'TIME_STRING'
|
@@ -1220,6 +1548,8 @@ command :today do |c|
|
|
1220
1548
|
c.flag [:after]
|
1221
1549
|
|
1222
1550
|
c.action do |_global_options, options, _args|
|
1551
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1552
|
+
|
1223
1553
|
options[:t] = true if options[:totals]
|
1224
1554
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1225
1555
|
opt = {
|
@@ -1227,9 +1557,10 @@ command :today do |c|
|
|
1227
1557
|
before: options[:before],
|
1228
1558
|
section: options[:section],
|
1229
1559
|
sort_tags: options[:sort_tags],
|
1230
|
-
totals: options[:totals]
|
1560
|
+
totals: options[:totals],
|
1561
|
+
order: settings.dig('templates', 'today', 'order')
|
1231
1562
|
}
|
1232
|
-
|
1563
|
+
Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
|
1233
1564
|
end
|
1234
1565
|
end
|
1235
1566
|
|
@@ -1239,6 +1570,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
|
|
1239
1570
|
it will create a range.)
|
1240
1571
|
arg_name 'DATE_STRING'
|
1241
1572
|
command :on do |c|
|
1573
|
+
c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
|
1574
|
+
c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
|
1575
|
+
c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
|
1576
|
+
|
1242
1577
|
c.desc 'Section'
|
1243
1578
|
c.arg_name 'NAME'
|
1244
1579
|
c.flag %i[s section], default_value: 'All'
|
@@ -1251,38 +1586,40 @@ command :on do |c|
|
|
1251
1586
|
|
1252
1587
|
c.desc 'Sort tags by (name|time)'
|
1253
1588
|
default = 'time'
|
1254
|
-
default =
|
1589
|
+
default = settings['tag_sort'] || 'name'
|
1255
1590
|
c.arg_name 'KEY'
|
1256
1591
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1257
1592
|
|
1258
|
-
c.desc
|
1593
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1259
1594
|
c.arg_name 'FORMAT'
|
1260
|
-
c.flag %i[o output]
|
1595
|
+
c.flag %i[o output]
|
1261
1596
|
|
1262
1597
|
c.action do |_global_options, options, args|
|
1263
|
-
|
1598
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1599
|
+
|
1600
|
+
raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
|
1264
1601
|
|
1265
1602
|
date_string = args.join(' ')
|
1266
1603
|
|
1267
1604
|
if date_string =~ / (to|through|thru) /
|
1268
1605
|
dates = date_string.split(/ (to|through|thru) /)
|
1269
|
-
start = wwid.chronify(dates[0])
|
1270
|
-
finish = wwid.chronify(dates[2])
|
1606
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1607
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1271
1608
|
else
|
1272
|
-
start = wwid.chronify(date_string)
|
1609
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1273
1610
|
finish = false
|
1274
1611
|
end
|
1275
1612
|
|
1276
|
-
|
1613
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1277
1614
|
|
1278
1615
|
message = "Date interpreted as #{start}"
|
1279
1616
|
message += " to #{finish}" if finish
|
1280
|
-
|
1617
|
+
Doing.logger.debug(message)
|
1281
1618
|
|
1282
1619
|
options[:t] = true if options[:totals]
|
1283
1620
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1284
1621
|
|
1285
|
-
|
1622
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1286
1623
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1287
1624
|
end
|
1288
1625
|
end
|
@@ -1292,6 +1629,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
|
|
1292
1629
|
and "2d" would be interpreted as "two days ago.")
|
1293
1630
|
arg_name 'DATE_STRING'
|
1294
1631
|
command :since do |c|
|
1632
|
+
c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
|
1633
|
+
c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
|
1634
|
+
|
1295
1635
|
c.desc 'Section'
|
1296
1636
|
c.arg_name 'NAME'
|
1297
1637
|
c.flag %i[s section], default_value: 'All'
|
@@ -1304,47 +1644,52 @@ command :since do |c|
|
|
1304
1644
|
|
1305
1645
|
c.desc 'Sort tags by (name|time)'
|
1306
1646
|
default = 'time'
|
1307
|
-
default =
|
1647
|
+
default = settings['tag_sort'] || 'name'
|
1308
1648
|
c.arg_name 'KEY'
|
1309
1649
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1310
1650
|
|
1311
|
-
c.desc
|
1651
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1312
1652
|
c.arg_name 'FORMAT'
|
1313
|
-
c.flag %i[o output]
|
1653
|
+
c.flag %i[o output]
|
1314
1654
|
|
1315
1655
|
c.action do |_global_options, options, args|
|
1316
|
-
|
1656
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1657
|
+
|
1658
|
+
raise Doing::Errors::MissingArgument, 'Missing date argument' if args.empty?
|
1317
1659
|
|
1318
1660
|
date_string = args.join(' ')
|
1319
1661
|
|
1320
|
-
date_string
|
1321
|
-
date_string.sub!(/(
|
1662
|
+
date_string.sub!(/(day) (\d)/, '\1 at \2')
|
1663
|
+
date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
|
1322
1664
|
|
1323
|
-
start = wwid.chronify(date_string)
|
1665
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1324
1666
|
finish = Time.now
|
1325
1667
|
|
1326
|
-
|
1668
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1327
1669
|
|
1328
|
-
|
1329
|
-
wwid.results.push(message)
|
1670
|
+
Doing.logger.debug("Date interpreted as #{start} through the current time")
|
1330
1671
|
|
1331
1672
|
options[:t] = true if options[:totals]
|
1332
1673
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1333
1674
|
|
1334
|
-
|
1675
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1335
1676
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1336
1677
|
end
|
1337
1678
|
end
|
1338
1679
|
|
1339
1680
|
desc 'List entries from yesterday'
|
1340
1681
|
command :yesterday do |c|
|
1682
|
+
c.example 'doing yesterday', desc: 'List all entries from the previous day'
|
1683
|
+
c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
|
1684
|
+
c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
|
1685
|
+
|
1341
1686
|
c.desc 'Specify a section'
|
1342
1687
|
c.arg_name 'NAME'
|
1343
1688
|
c.flag %i[s section], default_value: 'All'
|
1344
1689
|
|
1345
|
-
c.desc
|
1690
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1346
1691
|
c.arg_name 'FORMAT'
|
1347
|
-
c.flag %i[o output]
|
1692
|
+
c.flag %i[o output]
|
1348
1693
|
|
1349
1694
|
c.desc 'Show time intervals on @done tasks'
|
1350
1695
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1354,7 +1699,7 @@ command :yesterday do |c|
|
|
1354
1699
|
|
1355
1700
|
c.desc 'Sort tags by (name|time)'
|
1356
1701
|
default = 'time'
|
1357
|
-
default =
|
1702
|
+
default = settings['tag_sort'] || 'name'
|
1358
1703
|
c.arg_name 'KEY'
|
1359
1704
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1360
1705
|
|
@@ -1368,33 +1713,40 @@ command :yesterday do |c|
|
|
1368
1713
|
|
1369
1714
|
c.desc 'Tag sort direction (asc|desc)'
|
1370
1715
|
c.arg_name 'DIRECTION'
|
1371
|
-
c.flag [:tag_order], must_match:
|
1716
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1372
1717
|
|
1373
1718
|
c.action do |_global_options, options, _args|
|
1374
|
-
|
1375
|
-
|
1376
|
-
else
|
1377
|
-
'asc'
|
1378
|
-
end
|
1719
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1720
|
+
|
1379
1721
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1722
|
+
|
1380
1723
|
opt = {
|
1381
1724
|
after: options[:after],
|
1382
1725
|
before: options[:before],
|
1383
1726
|
sort_tags: options[:sort_tags],
|
1384
|
-
tag_order: options[:tag_order],
|
1385
|
-
totals: options[:totals]
|
1727
|
+
tag_order: options[:tag_order].normalize_order,
|
1728
|
+
totals: options[:totals],
|
1729
|
+
order: settings.dig('templates', 'today', 'order')
|
1386
1730
|
}
|
1387
|
-
|
1731
|
+
Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
|
1388
1732
|
end
|
1389
1733
|
end
|
1390
1734
|
|
1391
1735
|
desc 'Show the last entry, optionally edit'
|
1392
1736
|
command :last do |c|
|
1737
|
+
c.example 'doing last', desc: 'Show the most recent entry in all sections'
|
1738
|
+
c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
|
1739
|
+
c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
|
1740
|
+
c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
|
1741
|
+
c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
|
1742
|
+
c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
|
1743
|
+
c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
|
1744
|
+
|
1393
1745
|
c.desc 'Specify a section'
|
1394
1746
|
c.arg_name 'NAME'
|
1395
1747
|
c.flag %i[s section], default_value: 'All'
|
1396
1748
|
|
1397
|
-
c.desc "Edit entry with #{
|
1749
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
1398
1750
|
c.switch %i[e editor], negatable: false, default_value: false
|
1399
1751
|
|
1400
1752
|
c.desc 'Tag filter, combine multiple tags with a comma.'
|
@@ -1403,19 +1755,19 @@ command :last do |c|
|
|
1403
1755
|
|
1404
1756
|
c.desc 'Tag boolean'
|
1405
1757
|
c.arg_name 'BOOLEAN'
|
1406
|
-
c.flag [:bool], must_match:
|
1758
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1407
1759
|
|
1408
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1760
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
1409
1761
|
c.arg_name 'QUERY'
|
1410
1762
|
c.flag [:search]
|
1411
1763
|
|
1412
|
-
c.action do |
|
1413
|
-
|
1764
|
+
c.action do |global_options, options, _args|
|
1765
|
+
raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1414
1766
|
|
1415
1767
|
if options[:tag].nil?
|
1416
1768
|
tags = []
|
1417
1769
|
else
|
1418
|
-
tags = options[:tag].
|
1770
|
+
tags = options[:tag].to_tags
|
1419
1771
|
options[:bool] = case options[:bool]
|
1420
1772
|
when /(any|or)/i
|
1421
1773
|
:or
|
@@ -1430,7 +1782,7 @@ command :last do |c|
|
|
1430
1782
|
if options[:editor]
|
1431
1783
|
wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
|
1432
1784
|
else
|
1433
|
-
|
1785
|
+
Doing::Pager::page wwid.last(times: true, section: options[:s],
|
1434
1786
|
options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
|
1435
1787
|
end
|
1436
1788
|
end
|
@@ -1452,17 +1804,19 @@ command :choose do |c|
|
|
1452
1804
|
c.action do |_global_options, _options, _args|
|
1453
1805
|
section = wwid.choose_section
|
1454
1806
|
|
1455
|
-
|
1807
|
+
Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
|
1456
1808
|
end
|
1457
1809
|
end
|
1458
1810
|
|
1459
1811
|
desc 'Add a new section to the "doing" file'
|
1460
1812
|
arg_name 'SECTION_NAME'
|
1461
1813
|
command :add_section do |c|
|
1814
|
+
c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
|
1815
|
+
|
1462
1816
|
c.action do |_global_options, _options, args|
|
1463
|
-
|
1817
|
+
raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
1464
1818
|
|
1465
|
-
wwid.add_section(args
|
1819
|
+
wwid.add_section(args.join(' ').cap_first)
|
1466
1820
|
wwid.write(wwid.doing_file)
|
1467
1821
|
end
|
1468
1822
|
end
|
@@ -1470,18 +1824,42 @@ end
|
|
1470
1824
|
desc 'List available color variables for configuration templates and views'
|
1471
1825
|
command :colors do |c|
|
1472
1826
|
c.action do |_global_options, _options, _args|
|
1473
|
-
clrs = wwid.colors
|
1474
1827
|
bgs = []
|
1475
1828
|
fgs = []
|
1476
|
-
|
1477
|
-
if
|
1478
|
-
bgs.push("#{
|
1829
|
+
colors::attributes.each do |color|
|
1830
|
+
if color.to_s =~ /bg/
|
1831
|
+
bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
|
1479
1832
|
else
|
1480
|
-
fgs.push("#{
|
1833
|
+
fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
|
1481
1834
|
end
|
1482
1835
|
end
|
1483
|
-
|
1484
|
-
|
1836
|
+
out = []
|
1837
|
+
out << fgs.join("\n")
|
1838
|
+
out << bgs.join("\n")
|
1839
|
+
Doing::Pager.page out.join("\n")
|
1840
|
+
end
|
1841
|
+
end
|
1842
|
+
|
1843
|
+
desc 'List installed plugins'
|
1844
|
+
long_desc %(Lists available plugins, including user-installed plugins.
|
1845
|
+
|
1846
|
+
Export plugins are available with the `--output` flag on commands that support it.
|
1847
|
+
|
1848
|
+
Import plugins are available using `doing import --type PLUGIN`.
|
1849
|
+
)
|
1850
|
+
command :plugins do |c|
|
1851
|
+
c.example 'doing plugins', desc: 'List all plugins'
|
1852
|
+
c.example 'doing plugins -t import', desc: 'List all import plugins'
|
1853
|
+
|
1854
|
+
c.desc 'List plugins of type (import, export)'
|
1855
|
+
c.arg_name 'TYPE'
|
1856
|
+
c.flag %i[t type], must_match: /^[iea].*$/i, default_value: 'all'
|
1857
|
+
|
1858
|
+
c.desc 'List in single column for completion'
|
1859
|
+
c.switch %i[c column], default_value: false
|
1860
|
+
|
1861
|
+
c.action do |_global_options, options, _args|
|
1862
|
+
Doing::Plugins.list_plugins(options)
|
1485
1863
|
end
|
1486
1864
|
end
|
1487
1865
|
|
@@ -1489,6 +1867,9 @@ desc 'Display a user-created view'
|
|
1489
1867
|
long_desc 'Command line options override view configuration'
|
1490
1868
|
arg_name 'VIEW_NAME'
|
1491
1869
|
command :view do |c|
|
1870
|
+
c.example 'doing view color', desc: 'Display entries according to config for view "color"'
|
1871
|
+
c.example 'doing view color --section Archive --count 10', desc: 'Display view "color", overriding some configured settings'
|
1872
|
+
|
1492
1873
|
c.desc 'Section'
|
1493
1874
|
c.arg_name 'NAME'
|
1494
1875
|
c.flag %i[s section]
|
@@ -1497,9 +1878,9 @@ command :view do |c|
|
|
1497
1878
|
c.arg_name 'COUNT'
|
1498
1879
|
c.flag %i[c count], must_match: /^\d+$/, type: Integer
|
1499
1880
|
|
1500
|
-
c.desc
|
1881
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1501
1882
|
c.arg_name 'FORMAT'
|
1502
|
-
c.flag %i[o output]
|
1883
|
+
c.flag %i[o output]
|
1503
1884
|
|
1504
1885
|
c.desc 'Show time intervals on @done tasks'
|
1505
1886
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1516,9 +1897,9 @@ command :view do |c|
|
|
1516
1897
|
|
1517
1898
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
1518
1899
|
c.arg_name 'BOOLEAN'
|
1519
|
-
c.flag %i[b bool], must_match:
|
1900
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
1520
1901
|
|
1521
|
-
c.desc 'Search filter, surround with slashes for regex (/query/)'
|
1902
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
1522
1903
|
c.arg_name 'QUERY'
|
1523
1904
|
c.flag [:search]
|
1524
1905
|
|
@@ -1528,7 +1909,7 @@ command :view do |c|
|
|
1528
1909
|
|
1529
1910
|
c.desc 'Tag sort direction (asc|desc)'
|
1530
1911
|
c.arg_name 'DIRECTION'
|
1531
|
-
c.flag [:tag_order], must_match:
|
1912
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER
|
1532
1913
|
|
1533
1914
|
c.desc 'View entries older than date'
|
1534
1915
|
c.arg_name 'DATE_STRING'
|
@@ -1541,8 +1922,13 @@ command :view do |c|
|
|
1541
1922
|
c.desc 'Only show items with recorded time intervals (override view settings)'
|
1542
1923
|
c.switch [:only_timed], default_value: false, negatable: false
|
1543
1924
|
|
1925
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1926
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
1927
|
+
|
1544
1928
|
c.action do |_global_options, options, args|
|
1545
|
-
|
1929
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1930
|
+
|
1931
|
+
raise Doing::Errors::InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
1546
1932
|
|
1547
1933
|
title = if args.empty?
|
1548
1934
|
wwid.choose_view
|
@@ -1553,11 +1939,12 @@ command :view do |c|
|
|
1553
1939
|
if options[:section]
|
1554
1940
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1555
1941
|
else
|
1556
|
-
section =
|
1942
|
+
section = settings['current_section']
|
1557
1943
|
end
|
1558
1944
|
|
1559
1945
|
view = wwid.get_view(title)
|
1560
1946
|
if view
|
1947
|
+
page_title = view.key?('title') ? view['title'] : title.cap_first
|
1561
1948
|
only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
|
1562
1949
|
true
|
1563
1950
|
else
|
@@ -1565,7 +1952,7 @@ command :view do |c|
|
|
1565
1952
|
end
|
1566
1953
|
|
1567
1954
|
template = view.key?('template') ? view['template'] : nil
|
1568
|
-
|
1955
|
+
date_format = view.key?('date_format') ? view['date_format'] : nil
|
1569
1956
|
tags_color = view.key?('tags_color') ? view['tags_color'] : nil
|
1570
1957
|
tag_filter = false
|
1571
1958
|
if options[:tag]
|
@@ -1583,7 +1970,7 @@ command :view do |c|
|
|
1583
1970
|
end
|
1584
1971
|
|
1585
1972
|
# If the -o/--output flag was specified, override any default in the view template
|
1586
|
-
options[:
|
1973
|
+
options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
|
1587
1974
|
|
1588
1975
|
count = if options[:c]
|
1589
1976
|
options[:c]
|
@@ -1593,18 +1980,23 @@ command :view do |c|
|
|
1593
1980
|
section = if options[:s]
|
1594
1981
|
section
|
1595
1982
|
else
|
1596
|
-
view.key?('section') ? view['section'] :
|
1983
|
+
view.key?('section') ? view['section'] : settings['current_section']
|
1597
1984
|
end
|
1598
|
-
order = view.key?('order') ? view['order'] : 'asc'
|
1985
|
+
order = view.key?('order') ? view['order'].normalize_order : 'asc'
|
1599
1986
|
|
1600
1987
|
totals = if options[:totals]
|
1601
1988
|
true
|
1602
1989
|
else
|
1603
1990
|
view.key?('totals') ? view['totals'] : false
|
1604
1991
|
end
|
1992
|
+
tag_order = if options[:tag_order]
|
1993
|
+
options[:tag_order].normalize_order
|
1994
|
+
else
|
1995
|
+
view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
|
1996
|
+
end
|
1605
1997
|
|
1606
1998
|
options[:t] = true if totals
|
1607
|
-
options[:output]&.downcase
|
1999
|
+
output_format = options[:output]&.downcase || 'template'
|
1608
2000
|
|
1609
2001
|
options[:sort_tags] = if options[:tag_sort]
|
1610
2002
|
options[:tag_sort] =~ /^n/i ? true : false
|
@@ -1613,40 +2005,50 @@ command :view do |c|
|
|
1613
2005
|
else
|
1614
2006
|
false
|
1615
2007
|
end
|
2008
|
+
if view.key?('after') && !options[:after]
|
2009
|
+
options[:after] = view['after']
|
2010
|
+
end
|
1616
2011
|
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1620
|
-
view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
|
1621
|
-
else
|
1622
|
-
'asc'
|
1623
|
-
end
|
2012
|
+
if view.key?('before') && !options[:before]
|
2013
|
+
options[:before] = view['before']
|
2014
|
+
end
|
1624
2015
|
|
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
|
-
}
|
2016
|
+
if view.key?('from')
|
2017
|
+
date_string = view['from']
|
2018
|
+
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2019
|
+
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2020
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
2021
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
2022
|
+
else
|
2023
|
+
start = wwid.chronify(date_string, guess: :begin)
|
2024
|
+
finish = false
|
2025
|
+
end
|
2026
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
2027
|
+
dates = [start, finish]
|
2028
|
+
end
|
1644
2029
|
|
1645
|
-
|
2030
|
+
opts = options
|
2031
|
+
opts[:output] = output_format
|
2032
|
+
opts[:count] = count
|
2033
|
+
opts[:format] = date_format
|
2034
|
+
opts[:highlight] = options[:color]
|
2035
|
+
opts[:only_timed] = only_timed
|
2036
|
+
opts[:order] = order
|
2037
|
+
opts[:section] = section
|
2038
|
+
opts[:tag_filter] = tag_filter
|
2039
|
+
opts[:tag_order] = tag_order
|
2040
|
+
opts[:tags_color] = tags_color
|
2041
|
+
opts[:template] = template
|
2042
|
+
opts[:totals] = totals
|
2043
|
+
opts[:page_title] = page_title
|
2044
|
+
opts[:date_filter] = dates
|
2045
|
+
opts[:output] = options[:interactive] ? nil : options[:output]
|
2046
|
+
|
2047
|
+
Doing::Pager.page wwid.list_section(opts)
|
1646
2048
|
elsif title.instance_of?(FalseClass)
|
1647
2049
|
exit_now! 'Cancelled'
|
1648
2050
|
else
|
1649
|
-
|
2051
|
+
raise Doing::Errors::InvalidView, "View #{title} not found in config"
|
1650
2052
|
end
|
1651
2053
|
end
|
1652
2054
|
end
|
@@ -1663,9 +2065,18 @@ command :views do |c|
|
|
1663
2065
|
end
|
1664
2066
|
|
1665
2067
|
desc 'Move entries between sections'
|
1666
|
-
|
1667
|
-
|
1668
|
-
|
2068
|
+
long_desc %(Argument can be a section name to move all entries from a section,
|
2069
|
+
or start with an "@" to move entries matching a tag.
|
2070
|
+
|
2071
|
+
Default with no argument moves items from the "#{settings['current_section']}" section to Archive.)
|
2072
|
+
arg_name 'SECTION_OR_TAG'
|
2073
|
+
default_value settings['current_section']
|
2074
|
+
command %i[archive move] do |c|
|
2075
|
+
c.example 'doing archive Currently', desc: 'Move all entries in the Currently section to Archive section'
|
2076
|
+
c.example 'doing archive @done', desc: 'Move all entries tagged @done to Archive'
|
2077
|
+
c.example 'doing archive --to Later @project1', desc: 'Move all entries tagged @project1 to Later section'
|
2078
|
+
c.example 'doing move Later --tag project1 --to Currently', desc: 'Move entries in Later tagged @project1 to Currently (move is an alias for archive)'
|
2079
|
+
|
1669
2080
|
c.desc 'How many items to keep (ignored if archiving by tag or search)'
|
1670
2081
|
c.arg_name 'X'
|
1671
2082
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1683,7 +2094,7 @@ command :archive do |c|
|
|
1683
2094
|
|
1684
2095
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1685
2096
|
c.arg_name 'BOOLEAN'
|
1686
|
-
c.flag [:bool], must_match:
|
2097
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1687
2098
|
|
1688
2099
|
c.desc 'Search filter'
|
1689
2100
|
c.arg_name 'QUERY'
|
@@ -1696,7 +2107,7 @@ command :archive do |c|
|
|
1696
2107
|
|
1697
2108
|
c.action do |_global_options, options, args|
|
1698
2109
|
if args.empty?
|
1699
|
-
section =
|
2110
|
+
section = settings['current_section']
|
1700
2111
|
tags = []
|
1701
2112
|
elsif args[0] =~ /^all/i
|
1702
2113
|
section = 'all'
|
@@ -1708,34 +2119,25 @@ command :archive do |c|
|
|
1708
2119
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
1709
2120
|
end
|
1710
2121
|
|
1711
|
-
|
2122
|
+
raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
1712
2123
|
|
1713
|
-
tags.concat(options[:tag].
|
2124
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
2125
|
+
|
2126
|
+
opts = options
|
2127
|
+
opts[:bool] = options[:bool].normalize_bool
|
2128
|
+
opts[:destination] = options[:to]
|
2129
|
+
opts[:tags] = tags
|
1714
2130
|
|
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
2131
|
wwid.archive(section, opts)
|
1734
2132
|
end
|
1735
2133
|
end
|
1736
2134
|
|
1737
2135
|
desc 'Move entries to archive file'
|
1738
2136
|
command :rotate do |c|
|
2137
|
+
c.example 'doing rotate', desc: 'Move all entries in doing file to a dated secondary file'
|
2138
|
+
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'
|
2139
|
+
c.example 'doing rotate --tag project1,done --bool AND', desc: 'Move entries tagged @project1 and @done to a secondary file'
|
2140
|
+
|
1739
2141
|
c.desc 'How many items to keep in each section (most recent)'
|
1740
2142
|
c.arg_name 'X'
|
1741
2143
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1750,7 +2152,7 @@ command :rotate do |c|
|
|
1750
2152
|
|
1751
2153
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1752
2154
|
c.arg_name 'BOOLEAN'
|
1753
|
-
c.flag [:bool], must_match:
|
2155
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1754
2156
|
|
1755
2157
|
c.desc 'Search filter'
|
1756
2158
|
c.arg_name 'QUERY'
|
@@ -1766,23 +2168,14 @@ command :rotate do |c|
|
|
1766
2168
|
options[:section] = wwid.guess_section(options[:section])
|
1767
2169
|
end
|
1768
2170
|
|
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
|
2171
|
+
options[:bool] = options[:bool].normalize_bool
|
1779
2172
|
|
1780
2173
|
wwid.rotate(options)
|
1781
2174
|
end
|
1782
2175
|
end
|
1783
2176
|
|
1784
2177
|
desc 'Open the "doing" file in an editor'
|
1785
|
-
long_desc "`doing open` defaults to using the editor_app setting in #{
|
2178
|
+
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
2179
|
command :open do |c|
|
1787
2180
|
if `uname` =~ /Darwin/
|
1788
2181
|
c.desc 'Open with app name'
|
@@ -1793,8 +2186,6 @@ command :open do |c|
|
|
1793
2186
|
c.arg_name 'BUNDLE_ID'
|
1794
2187
|
c.flag %i[b bundle_id]
|
1795
2188
|
end
|
1796
|
-
c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
|
1797
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
1798
2189
|
|
1799
2190
|
c.action do |_global_options, options, _args|
|
1800
2191
|
params = options.dup
|
@@ -1806,30 +2197,54 @@ command :open do |c|
|
|
1806
2197
|
system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
|
1807
2198
|
elsif options[:bundle_id]
|
1808
2199
|
system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
|
1809
|
-
elsif
|
1810
|
-
|
1811
|
-
|
1812
|
-
|
1813
|
-
|
1814
|
-
|
2200
|
+
elsif Doing::Util.find_default_editor('doing_file')
|
2201
|
+
editor = Doing::Util.find_default_editor('doing_file')
|
2202
|
+
if Doing::Util.exec_available(editor)
|
2203
|
+
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
2204
|
+
else
|
2205
|
+
system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
|
2206
|
+
end
|
1815
2207
|
else
|
1816
2208
|
system %(open "#{File.expand_path(wwid.doing_file)}")
|
1817
2209
|
end
|
1818
|
-
|
1819
2210
|
else
|
1820
|
-
|
2211
|
+
raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
1821
2212
|
|
1822
|
-
system %(
|
2213
|
+
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
1823
2214
|
end
|
1824
2215
|
end
|
1825
2216
|
end
|
1826
2217
|
|
1827
|
-
desc 'Edit the configuration file'
|
2218
|
+
desc 'Edit the configuration file or output a value from it'
|
2219
|
+
long_desc %(Run without arguments, `doing config` opens your `.doingrc` in an editor.
|
2220
|
+
If local configurations are found in the path between the current directory
|
2221
|
+
and `~/.doingrc`, a menu will allow you to select which to open in the editor.
|
2222
|
+
|
2223
|
+
It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
|
2224
|
+
|
2225
|
+
Use `doing config -d` to output the configuration to the terminal, and
|
2226
|
+
provide a dot-separated key path to get a specific value. Shows the current value
|
2227
|
+
including keys/overrides set by local configs.)
|
2228
|
+
arg_name 'KEY_PATH'
|
1828
2229
|
command :config do |c|
|
2230
|
+
c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
|
2231
|
+
c.example 'doing config -d doing_file', desc: 'Output the value of a config key as YAML'
|
2232
|
+
c.example 'doing config -d plugins.say.say_voice -o json', desc: 'Output the value of a key path as JSON'
|
2233
|
+
|
1829
2234
|
c.desc 'Editor to use'
|
1830
2235
|
c.arg_name 'EDITOR'
|
1831
2236
|
c.flag %i[e editor], default_value: nil
|
1832
2237
|
|
2238
|
+
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.'
|
2239
|
+
c.switch %i[d dump]
|
2240
|
+
|
2241
|
+
c.desc 'Format for --dump (json|yaml|raw)'
|
2242
|
+
c.arg_name 'FORMAT'
|
2243
|
+
c.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
|
2244
|
+
|
2245
|
+
c.desc 'Update config file with missing configuration options'
|
2246
|
+
c.switch %i[u update], default_value: false, negatable: false
|
2247
|
+
|
1833
2248
|
if `uname` =~ /Darwin/
|
1834
2249
|
c.desc 'Application to use'
|
1835
2250
|
c.arg_name 'APP_NAME'
|
@@ -1839,31 +2254,84 @@ command :config do |c|
|
|
1839
2254
|
c.arg_name 'BUNDLE_ID'
|
1840
2255
|
c.flag [:b]
|
1841
2256
|
|
1842
|
-
c.desc "Use the config_editor_app defined in ~/.doingrc (#{
|
2257
|
+
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
2258
|
c.switch [:x]
|
1844
2259
|
end
|
1845
2260
|
|
1846
|
-
c.action do |_global_options, options,
|
2261
|
+
c.action do |_global_options, options, args|
|
2262
|
+
if options[:update]
|
2263
|
+
config.configure({rewrite: true, ignore_local: true})
|
2264
|
+
# Doing.logger.warn("Config file rewritten: #{config.config_file}")
|
2265
|
+
return
|
2266
|
+
end
|
2267
|
+
|
2268
|
+
if options[:dump]
|
2269
|
+
keypath = args.join('.')
|
2270
|
+
cfg = config.value_for_key(keypath)
|
2271
|
+
|
2272
|
+
if cfg
|
2273
|
+
$stdout.puts case options[:output]
|
2274
|
+
when /^j/
|
2275
|
+
JSON.pretty_generate(cfg)
|
2276
|
+
when /^r/
|
2277
|
+
cfg
|
2278
|
+
else
|
2279
|
+
# cfg = { last_key => cfg } unless last_key.nil?
|
2280
|
+
YAML.dump(cfg)
|
2281
|
+
end
|
2282
|
+
else
|
2283
|
+
Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
|
2284
|
+
end
|
2285
|
+
Doing.logger.output_results
|
2286
|
+
return
|
2287
|
+
end
|
2288
|
+
|
2289
|
+
if config.additional_configs.count.positive?
|
2290
|
+
choices = [config.config_file]
|
2291
|
+
choices.concat(config.additional_configs)
|
2292
|
+
res = wwid.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to edit > ')
|
2293
|
+
|
2294
|
+
raise Doing::Errors::UserCancelled, 'Cancelled' unless res
|
2295
|
+
|
2296
|
+
config_file = res.strip || config.config_file
|
2297
|
+
else
|
2298
|
+
config_file = config.config_file
|
2299
|
+
end
|
2300
|
+
|
1847
2301
|
if `uname` =~ /Darwin/
|
1848
2302
|
if options[:x]
|
1849
|
-
|
2303
|
+
editor = Doing::Util.find_default_editor('config')
|
2304
|
+
if editor
|
2305
|
+
if Doing::Util.exec_available(editor)
|
2306
|
+
system %(#{editor} "#{config_file}")
|
2307
|
+
else
|
2308
|
+
`open -a "#{editor}" "#{config_file}"`
|
2309
|
+
end
|
2310
|
+
else
|
2311
|
+
raise Doing::Errors::InvalidArgument, 'No viable editor found in config or environment.'
|
2312
|
+
end
|
1850
2313
|
elsif options[:a] || options[:b]
|
1851
2314
|
if options[:a]
|
1852
|
-
`open -a "#{options[:a]}" "#{
|
2315
|
+
`open -a "#{options[:a]}" "#{config_file}"`
|
1853
2316
|
elsif options[:b]
|
1854
|
-
`open -b #{options[:b]} "#{
|
2317
|
+
`open -b #{options[:b]} "#{config_file}"`
|
1855
2318
|
end
|
1856
2319
|
else
|
1857
|
-
|
2320
|
+
editor = options[:e] || Doing::Util.find_default_editor('config')
|
1858
2321
|
|
1859
|
-
editor
|
1860
|
-
|
2322
|
+
raise Doing::Errors::MissingEditor, 'No viable editor defined in config or environment' unless editor
|
2323
|
+
|
2324
|
+
if Doing::Util.exec_available(editor)
|
2325
|
+
system %(#{editor} "#{config_file}")
|
2326
|
+
else
|
2327
|
+
`open -a "#{editor}" "#{config_file}"`
|
2328
|
+
end
|
1861
2329
|
end
|
1862
2330
|
else
|
1863
|
-
|
2331
|
+
editor = options[:e] || Doing::Util.default_editor
|
2332
|
+
raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor)
|
1864
2333
|
|
1865
|
-
editor
|
1866
|
-
system %(#{editor} "#{wwid.config_file}")
|
2334
|
+
system %(#{editor} "#{config_file}")
|
1867
2335
|
end
|
1868
2336
|
end
|
1869
2337
|
end
|
@@ -1881,12 +2349,19 @@ command :undo do |c|
|
|
1881
2349
|
end
|
1882
2350
|
|
1883
2351
|
desc 'Import entries from an external source'
|
1884
|
-
long_desc
|
2352
|
+
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
|
1885
2353
|
arg_name 'PATH'
|
1886
2354
|
command :import do |c|
|
1887
|
-
c.desc
|
2355
|
+
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
1888
2356
|
c.arg_name 'TYPE'
|
1889
|
-
c.flag :type, default_value: '
|
2357
|
+
c.flag :type, default_value: 'doing'
|
2358
|
+
|
2359
|
+
c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
2360
|
+
c.arg_name 'QUERY'
|
2361
|
+
c.flag [:search]
|
2362
|
+
|
2363
|
+
c.desc 'Only import items with recorded time intervals'
|
2364
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
1890
2365
|
|
1891
2366
|
c.desc 'Target section'
|
1892
2367
|
c.arg_name 'NAME'
|
@@ -1903,53 +2378,67 @@ command :import do |c|
|
|
1903
2378
|
c.arg_name 'PREFIX'
|
1904
2379
|
c.flag :prefix
|
1905
2380
|
|
2381
|
+
c.desc 'Import entries older than date'
|
2382
|
+
c.arg_name 'DATE_STRING'
|
2383
|
+
c.flag [:before]
|
2384
|
+
|
2385
|
+
c.desc 'Import entries newer than date'
|
2386
|
+
c.arg_name 'DATE_STRING'
|
2387
|
+
c.flag [:after]
|
2388
|
+
|
2389
|
+
c.desc %(
|
2390
|
+
Date range to import. Date range argument should be quoted. Date specifications can be natural language.
|
2391
|
+
To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
|
2392
|
+
Has no effect unless the import plugin has implemented date range filtering.
|
2393
|
+
)
|
2394
|
+
c.arg_name 'DATE_OR_RANGE'
|
2395
|
+
c.flag %i[f from]
|
2396
|
+
|
1906
2397
|
c.desc 'Allow entries that overlap existing times'
|
1907
2398
|
c.switch [:overlap], negatable: true
|
1908
2399
|
|
1909
2400
|
c.action do |_global_options, options, args|
|
1910
2401
|
|
1911
2402
|
if options[:section]
|
1912
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1913
|
-
else
|
1914
|
-
section = wwid.config['current_section']
|
2403
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1915
2404
|
end
|
1916
2405
|
|
1917
|
-
if options[:
|
1918
|
-
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
|
1925
|
-
|
1926
|
-
wwid.import_timing(path, options)
|
1927
|
-
wwid.write(wwid.doing_file)
|
2406
|
+
if options[:from]
|
2407
|
+
date_string = options[:from]
|
2408
|
+
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
2409
|
+
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
2410
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
2411
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
2412
|
+
else
|
2413
|
+
start = wwid.chronify(date_string, guess: :begin)
|
2414
|
+
finish = false
|
1928
2415
|
end
|
2416
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
2417
|
+
dates = [start, finish]
|
2418
|
+
end
|
2419
|
+
|
2420
|
+
if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
|
2421
|
+
options[:no_overlap] = !options[:overlap]
|
2422
|
+
options[:date_filter] = dates
|
2423
|
+
wwid.import(args, options)
|
2424
|
+
wwid.write(wwid.doing_file)
|
1929
2425
|
else
|
1930
|
-
|
2426
|
+
raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
|
1931
2427
|
end
|
1932
2428
|
end
|
1933
2429
|
end
|
1934
2430
|
|
1935
2431
|
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
|
2432
|
+
# global[:pager] ||= settings['paginate']
|
1941
2433
|
|
1942
|
-
|
1943
|
-
wwid.init_doing_file(global[:doing_file])
|
1944
|
-
else
|
1945
|
-
wwid.init_doing_file
|
1946
|
-
end
|
1947
|
-
|
1948
|
-
wwid.auto_tag = !global[:noauto]
|
1949
|
-
|
1950
|
-
wwid.config[:include_notes] = false unless global[:notes]
|
2434
|
+
Doing::Pager.paginate = global[:pager]
|
1951
2435
|
|
1952
2436
|
$stdout.puts "doing v#{Doing::VERSION}" if global[:version]
|
2437
|
+
unless STDOUT.isatty
|
2438
|
+
Doing::Color::coloring = global[:pager] ? global[:color] : false
|
2439
|
+
else
|
2440
|
+
Doing::Color::coloring = global[:color]
|
2441
|
+
end
|
1953
2442
|
|
1954
2443
|
# Return true to proceed; false to abort and not call the
|
1955
2444
|
# chosen command
|
@@ -1958,20 +2447,57 @@ pre do |global, _command, _options, _args|
|
|
1958
2447
|
true
|
1959
2448
|
end
|
1960
2449
|
|
2450
|
+
on_error do |exception|
|
2451
|
+
# if exception.kind_of?(SystemExit)
|
2452
|
+
# false
|
2453
|
+
# else
|
2454
|
+
# p exception.inspect
|
2455
|
+
# Doing.logger.output_results
|
2456
|
+
# true
|
2457
|
+
# end
|
2458
|
+
false
|
2459
|
+
end
|
2460
|
+
|
1961
2461
|
post do |global, _command, _options, _args|
|
1962
2462
|
# Use skips_post before a command to skip this
|
1963
2463
|
# block on that command only
|
2464
|
+
Doing.logger.output_results
|
2465
|
+
end
|
2466
|
+
|
2467
|
+
around do |global, command, options, arguments, code|
|
2468
|
+
# Doing.logger.debug('Pager:', "Global: #{global[:pager]}, Config: #{settings['paginate']}, Pager: #{Doing::Pager.paginate}")
|
2469
|
+
Doing.logger.adjust_verbosity(global)
|
1964
2470
|
|
1965
2471
|
if global[:stdout]
|
1966
|
-
$stdout
|
2472
|
+
Doing.logger.logdev = $stdout
|
2473
|
+
end
|
2474
|
+
|
2475
|
+
wwid.default_option = global[:default]
|
2476
|
+
|
2477
|
+
if global[:config_file] && global[:config_file] != config.config_file
|
2478
|
+
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))
|
2479
|
+
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))
|
2480
|
+
|
2481
|
+
cf = File.expand_path(global[:config_file])
|
2482
|
+
raise MissingConfigFile, "Config file not found (#{global[:config_file]})" unless File.exist?(cf)
|
2483
|
+
|
2484
|
+
config.config_file = cf
|
2485
|
+
settings = config.configure({ ignore_local: true })
|
2486
|
+
end
|
2487
|
+
|
2488
|
+
if global[:doing_file]
|
2489
|
+
wwid.init_doing_file(global[:doing_file])
|
1967
2490
|
else
|
1968
|
-
|
2491
|
+
wwid.init_doing_file
|
1969
2492
|
end
|
1970
|
-
end
|
1971
2493
|
|
1972
|
-
|
1973
|
-
|
1974
|
-
|
2494
|
+
wwid.auto_tag = !global[:noauto]
|
2495
|
+
|
2496
|
+
settings[:include_notes] = false unless global[:notes]
|
2497
|
+
|
2498
|
+
global[:wwid] = wwid
|
2499
|
+
|
2500
|
+
code.call
|
1975
2501
|
end
|
1976
2502
|
|
1977
2503
|
exit run(ARGV)
|