doing 1.0.90 → 2.0.2.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 +590 -0
- data/COMMANDS.md +1181 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +110 -0
- data/LICENSE +23 -0
- data/README.md +14 -697
- data/Rakefile +79 -0
- data/_config.yml +1 -0
- data/bin/doing +1037 -481
- 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 +310 -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 +346 -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 +1838 -2266
- data/lib/doing/wwidfile.rb +117 -0
- data/lib/doing.rb +43 -2
- data/lib/examples/commands/wiki.rb +80 -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 -6
- data/lib/doing/helpers.rb +0 -121
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
|
46
87
|
|
47
|
-
desc '
|
48
|
-
|
88
|
+
desc 'Silence info messages'
|
89
|
+
switch %i[q quiet], default_value: false, negatable: false
|
90
|
+
|
91
|
+
desc 'Verbose output'
|
92
|
+
switch %i[debug], default_value: false, negatable: false
|
93
|
+
|
94
|
+
desc 'Use a specific configuration file. Deprecated, set $DOING_CONFIG instead.'
|
95
|
+
flag [:config_file], default_value: config.config_file
|
49
96
|
|
50
97
|
desc 'Specify a different doing_file'
|
51
98
|
flag %i[f doing_file]
|
52
99
|
|
53
100
|
desc 'Add an entry'
|
101
|
+
long_desc %(Record what you're starting now, or backdate the start time using natural language.
|
102
|
+
|
103
|
+
A parenthetical at the end of the entry will be converted to a note.
|
104
|
+
|
105
|
+
Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
|
54
106
|
arg_name 'ENTRY'
|
55
107
|
command %i[now next] do |c|
|
108
|
+
c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
|
109
|
+
c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
|
110
|
+
c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
|
111
|
+
c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
|
112
|
+
c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
|
113
|
+
|
56
114
|
c.desc 'Section'
|
57
115
|
c.arg_name 'NAME'
|
58
116
|
c.flag %i[s section]
|
59
117
|
|
60
|
-
c.desc "Edit entry with #{
|
118
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
61
119
|
c.switch %i[e editor], negatable: false, default_value: false
|
62
120
|
|
63
121
|
c.desc 'Backdate start time [4pm|20m|2h|yesterday noon]'
|
@@ -67,7 +125,7 @@ command %i[now next] do |c|
|
|
67
125
|
c.desc 'Timed entry, marks last entry in section as @done'
|
68
126
|
c.switch %i[f finish_last], negatable: false, default_value: false
|
69
127
|
|
70
|
-
c.desc '
|
128
|
+
c.desc 'Include a note'
|
71
129
|
c.arg_name 'TEXT'
|
72
130
|
c.flag %i[n note]
|
73
131
|
|
@@ -77,9 +135,9 @@ command %i[now next] do |c|
|
|
77
135
|
|
78
136
|
c.action do |_global_options, options, args|
|
79
137
|
if options[:back]
|
80
|
-
date = wwid.chronify(options[:back])
|
138
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
81
139
|
|
82
|
-
|
140
|
+
raise 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'
|
119
177
|
end
|
120
178
|
end
|
121
179
|
end
|
122
180
|
|
181
|
+
desc 'Reset the start time of an entry'
|
182
|
+
command %i[reset begin] do |c|
|
183
|
+
c.desc 'Set the start date of an item to now'
|
184
|
+
c.arg_name 'NAME'
|
185
|
+
c.flag %i[s section], default_value: 'All'
|
186
|
+
|
187
|
+
c.desc 'Resume entry (remove @done)'
|
188
|
+
c.switch %i[r resume], default_value: true
|
189
|
+
|
190
|
+
c.desc 'Reset last entry matching tag'
|
191
|
+
c.arg_name 'TAG'
|
192
|
+
c.flag [:tag]
|
193
|
+
|
194
|
+
c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
195
|
+
c.arg_name 'QUERY'
|
196
|
+
c.flag [:search]
|
197
|
+
|
198
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
199
|
+
c.arg_name 'BOOLEAN'
|
200
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
201
|
+
|
202
|
+
c.desc 'Select from a menu of matching entries'
|
203
|
+
c.switch %i[i interactive]
|
204
|
+
|
205
|
+
c.action do |global_options, options, args|
|
206
|
+
if options[:section]
|
207
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
208
|
+
end
|
209
|
+
|
210
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
211
|
+
|
212
|
+
items = wwid.filter_items([], opt: options)
|
213
|
+
|
214
|
+
if options[:interactive]
|
215
|
+
last_entry = wwid.choose_from_items(items, {
|
216
|
+
menu: true,
|
217
|
+
header: '',
|
218
|
+
prompt: 'Select an entry to start/reset > ',
|
219
|
+
multiple: false,
|
220
|
+
sort: false,
|
221
|
+
show_if_single: true
|
222
|
+
}, include_section: options[:section].nil? )
|
223
|
+
else
|
224
|
+
last_entry = items.last
|
225
|
+
end
|
226
|
+
|
227
|
+
unless last_entry
|
228
|
+
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
229
|
+
return
|
230
|
+
end
|
231
|
+
|
232
|
+
wwid.reset_item(last_entry, resume: options[:resume])
|
233
|
+
|
234
|
+
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
235
|
+
|
236
|
+
wwid.write(wwid.doing_file)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
|
123
241
|
desc 'Add a note to the last entry'
|
124
242
|
long_desc %(
|
125
|
-
If -r is provided with no other arguments, the last note is removed.
|
243
|
+
If -r is provided with no other arguments, the last note is removed.
|
244
|
+
If new content is specified through arguments or STDIN, any previous
|
245
|
+
note will be replaced with the new one.
|
126
246
|
|
127
247
|
Use -e to load the last entry in a text editor where you can append a note.
|
128
248
|
)
|
@@ -132,47 +252,77 @@ command :note do |c|
|
|
132
252
|
c.arg_name 'NAME'
|
133
253
|
c.flag %i[s section], default_value: 'All'
|
134
254
|
|
135
|
-
c.desc "Edit entry with #{
|
255
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
136
256
|
c.switch %i[e editor], negatable: false, default_value: false
|
137
257
|
|
138
258
|
c.desc "Replace/Remove last entry's note (default append)"
|
139
259
|
c.switch %i[r remove], negatable: false, default_value: false
|
140
260
|
|
261
|
+
c.desc 'Add/remove note from last entry matching tag'
|
262
|
+
c.arg_name 'TAG'
|
263
|
+
c.flag [:tag]
|
264
|
+
|
265
|
+
c.desc 'Add/remove note from last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
266
|
+
c.arg_name 'QUERY'
|
267
|
+
c.flag [:search]
|
268
|
+
|
269
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
270
|
+
c.arg_name 'BOOLEAN'
|
271
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
272
|
+
|
273
|
+
c.desc 'Select item for new note from a menu of matching entries'
|
274
|
+
c.switch %i[i interactive]
|
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
|
144
279
|
end
|
145
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
|
288
|
+
end
|
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
|
@@ -243,33 +393,60 @@ command :meanwhile do |c|
|
|
243
393
|
end
|
244
394
|
end
|
245
395
|
|
246
|
-
desc 'Output HTML and
|
396
|
+
desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
|
247
397
|
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
|
-
|
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
|
257
420
|
|
258
|
-
|
259
|
-
|
260
|
-
$stdout.puts wwid.haml_template
|
261
|
-
when /css/i
|
262
|
-
$stdout.puts wwid.css_template
|
421
|
+
if args.empty?
|
422
|
+
type = wwid.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
|
263
423
|
else
|
264
|
-
|
424
|
+
type = args[0]
|
265
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
|
266
441
|
end
|
267
442
|
end
|
268
443
|
|
269
|
-
desc 'Display an interactive menu to perform operations
|
444
|
+
desc 'Display an interactive menu to perform operations'
|
270
445
|
long_desc 'List all entries and select with typeahead fuzzy matching.
|
271
446
|
|
272
|
-
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.'
|
273
450
|
command :select do |c|
|
274
451
|
c.desc 'Select from a specific section'
|
275
452
|
c.arg_name 'SECTION'
|
@@ -294,7 +471,7 @@ command :select do |c|
|
|
294
471
|
|
295
472
|
c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
|
296
473
|
c.arg_name 'QUERY'
|
297
|
-
c.flag %i[q query]
|
474
|
+
c.flag %i[q query search]
|
298
475
|
|
299
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.'
|
300
477
|
c.switch %i[menu], negatable: true, default_value: true
|
@@ -321,12 +498,17 @@ command :select do |c|
|
|
321
498
|
c.arg_name 'FILE'
|
322
499
|
c.flag %i[save_to]
|
323
500
|
|
324
|
-
c.desc
|
501
|
+
c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
|
325
502
|
c.arg_name 'FORMAT'
|
326
|
-
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]
|
327
507
|
|
328
508
|
c.action do |_global_options, options, args|
|
329
|
-
|
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]
|
330
512
|
|
331
513
|
wwid.interactive(options)
|
332
514
|
end
|
@@ -335,7 +517,7 @@ end
|
|
335
517
|
desc 'Add an item to the Later section'
|
336
518
|
arg_name 'ENTRY'
|
337
519
|
command :later do |c|
|
338
|
-
c.desc "Edit entry with #{
|
520
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
339
521
|
c.switch %i[e editor], negatable: false, default_value: false
|
340
522
|
|
341
523
|
c.desc 'Backdate start time to date string [4pm|20m|2h|yesterday noon]'
|
@@ -348,18 +530,18 @@ command :later do |c|
|
|
348
530
|
|
349
531
|
c.action do |_global_options, options, args|
|
350
532
|
if options[:back]
|
351
|
-
date = wwid.chronify(options[:back])
|
352
|
-
|
533
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
534
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
353
535
|
else
|
354
536
|
date = Time.now
|
355
537
|
end
|
356
538
|
|
357
539
|
if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
|
358
|
-
|
540
|
+
raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
359
541
|
|
360
542
|
input = args.empty? ? '' : args.join(' ')
|
361
543
|
input = wwid.fork_editor(input).strip
|
362
|
-
|
544
|
+
raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
|
363
545
|
|
364
546
|
title, note = wwid.format_input(input)
|
365
547
|
note.push(options[:n]) if options[:n]
|
@@ -376,7 +558,7 @@ command :later do |c|
|
|
376
558
|
wwid.add_item(title.cap_first, 'Later', { note: note, back: date })
|
377
559
|
wwid.write(wwid.doing_file)
|
378
560
|
else
|
379
|
-
|
561
|
+
raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
|
380
562
|
end
|
381
563
|
end
|
382
564
|
end
|
@@ -412,31 +594,39 @@ command %i[done did] do |c|
|
|
412
594
|
c.arg_name 'NAME'
|
413
595
|
c.flag %i[s section]
|
414
596
|
|
415
|
-
c.desc "Edit entry with #{
|
597
|
+
c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
|
416
598
|
c.switch %i[e editor], negatable: false, default_value: false
|
417
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
|
+
|
418
607
|
# c.desc "Edit entry with specified app"
|
419
608
|
# c.arg_name 'editor_app'
|
420
609
|
# # c.flag [:a, :app]
|
421
610
|
|
422
611
|
c.action do |_global_options, options, args|
|
423
612
|
took = 0
|
613
|
+
donedate = nil
|
424
614
|
|
425
615
|
if options[:took]
|
426
616
|
took = wwid.chronify_qty(options[:took])
|
427
|
-
|
617
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
428
618
|
end
|
429
619
|
|
430
620
|
if options[:back]
|
431
|
-
date = wwid.chronify(options[:back])
|
432
|
-
|
621
|
+
date = wwid.chronify(options[:back], guess: :begin)
|
622
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
433
623
|
else
|
434
624
|
date = options[:took] ? Time.now - took : Time.now
|
435
625
|
end
|
436
626
|
|
437
627
|
if options[:at]
|
438
|
-
finish_date = wwid.chronify(options[:at])
|
439
|
-
|
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?
|
440
630
|
|
441
631
|
date = options[:took] ? finish_date - took : finish_date
|
442
632
|
elsif options[:took]
|
@@ -447,58 +637,116 @@ command %i[done did] do |c|
|
|
447
637
|
finish_date = Time.now
|
448
638
|
end
|
449
639
|
|
450
|
-
if
|
451
|
-
donedate =
|
640
|
+
if options[:date]
|
641
|
+
donedate = finish_date.strftime('%F %R')
|
452
642
|
end
|
453
643
|
|
454
644
|
if options[:section]
|
455
645
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
456
646
|
else
|
457
|
-
section =
|
647
|
+
section = settings['current_section']
|
458
648
|
end
|
459
649
|
|
650
|
+
note = Doing::Note.new
|
651
|
+
note.add(options[:note]) if options[:note]
|
652
|
+
|
460
653
|
if options[:editor]
|
461
|
-
|
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
|
462
672
|
|
463
|
-
input = ''
|
464
|
-
input += args.join(' ') unless args.empty?
|
465
673
|
input = wwid.fork_editor(input).strip
|
466
|
-
|
674
|
+
raise Doing::Errors::EmptyInput, 'No content' unless input && !input.empty?
|
467
675
|
|
468
676
|
title, note = wwid.format_input(input)
|
469
|
-
title
|
470
|
-
|
471
|
-
|
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
|
+
|
472
696
|
wwid.write(wwid.doing_file)
|
473
697
|
elsif args.empty? && $stdin.stat.size.zero?
|
474
698
|
if options[:r]
|
475
699
|
wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
476
700
|
else
|
477
|
-
|
701
|
+
note = options[:note] ? Doing::Note.new(options[:note]) : nil
|
702
|
+
opt = {
|
478
703
|
archive: options[:a],
|
479
704
|
back: finish_date,
|
480
705
|
count: 1,
|
481
706
|
date: options[:date],
|
707
|
+
note: note,
|
482
708
|
section: section,
|
483
|
-
|
709
|
+
tags: ['done'],
|
710
|
+
took: took == 0 ? nil : took,
|
711
|
+
unfinished: options[:unfinished]
|
484
712
|
}
|
485
|
-
wwid.tag_last(
|
713
|
+
wwid.tag_last(opt)
|
486
714
|
end
|
487
715
|
elsif !args.empty?
|
488
|
-
|
716
|
+
note = Doing::Note.new(options[:note])
|
717
|
+
title, new_note = wwid.format_input([args.join(' '), note.to_s].join("\n"))
|
489
718
|
title.chomp!
|
490
|
-
title += " @done#{donedate}"
|
491
719
|
section = 'Archive' if options[:a]
|
492
|
-
|
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)
|
493
729
|
wwid.write(wwid.doing_file)
|
730
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
494
731
|
elsif $stdin.stat.size.positive?
|
495
732
|
title, note = wwid.format_input($stdin.read)
|
496
|
-
|
733
|
+
note.add(options[:note]) if options[:note]
|
497
734
|
section = options[:a] ? 'Archive' : section
|
498
|
-
|
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)
|
499
746
|
wwid.write(wwid.doing_file)
|
747
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
500
748
|
else
|
501
|
-
|
749
|
+
raise Doing::Errors::EmptyInput, 'You must provide content when creating a new entry'
|
502
750
|
end
|
503
751
|
end
|
504
752
|
end
|
@@ -520,39 +768,37 @@ command :cancel do |c|
|
|
520
768
|
|
521
769
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
522
770
|
c.arg_name 'BOOLEAN'
|
523
|
-
c.flag [:bool], must_match:
|
771
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
524
772
|
|
525
773
|
c.desc 'Cancel last entry (or entries) not already marked @done'
|
526
774
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
527
775
|
|
776
|
+
c.desc 'Select item(s) to cancel from a menu of matching entries'
|
777
|
+
c.switch %i[i interactive]
|
778
|
+
|
528
779
|
c.action do |_global_options, options, args|
|
529
780
|
if options[:section]
|
530
781
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
531
782
|
else
|
532
|
-
section =
|
783
|
+
section = settings['current_section']
|
533
784
|
end
|
534
785
|
|
535
786
|
if options[:tag].nil?
|
536
787
|
tags = []
|
537
788
|
else
|
538
|
-
tags = options[:tag].
|
539
|
-
options[:bool] = case options[:bool]
|
540
|
-
when /(and|all)/i
|
541
|
-
'AND'
|
542
|
-
when /(any|or)/i
|
543
|
-
'OR'
|
544
|
-
when /(not|none)/i
|
545
|
-
'NOT'
|
546
|
-
else
|
547
|
-
'AND'
|
548
|
-
end
|
789
|
+
tags = options[:tag].to_tags
|
549
790
|
end
|
550
791
|
|
551
|
-
|
792
|
+
raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
|
552
793
|
|
553
|
-
|
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
|
554
801
|
|
555
|
-
count = args[0] ? args[0].to_i : 1
|
556
802
|
opts = {
|
557
803
|
archive: options[:a],
|
558
804
|
count: count,
|
@@ -560,10 +806,12 @@ command :cancel do |c|
|
|
560
806
|
section: section,
|
561
807
|
sequential: false,
|
562
808
|
tag: tags,
|
563
|
-
tag_bool: options[:bool],
|
809
|
+
tag_bool: options[:bool].normalize_bool,
|
564
810
|
tags: ['done'],
|
565
|
-
unfinished: options[:unfinished]
|
811
|
+
unfinished: options[:unfinished],
|
812
|
+
interactive: options[:interactive]
|
566
813
|
}
|
814
|
+
|
567
815
|
wwid.tag_last(opts)
|
568
816
|
end
|
569
817
|
end
|
@@ -592,13 +840,16 @@ command :finish do |c|
|
|
592
840
|
c.arg_name 'TAG'
|
593
841
|
c.flag [:tag]
|
594
842
|
|
595
|
-
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")'
|
596
844
|
c.arg_name 'QUERY'
|
597
845
|
c.flag [:search]
|
598
846
|
|
599
847
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
600
848
|
c.arg_name 'BOOLEAN'
|
601
|
-
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
|
602
853
|
|
603
854
|
c.desc 'Finish last entry (or entries) not already marked @done'
|
604
855
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
@@ -615,32 +866,29 @@ command :finish do |c|
|
|
615
866
|
c.arg_name 'NAME'
|
616
867
|
c.flag %i[s section]
|
617
868
|
|
618
|
-
c.
|
619
|
-
|
620
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
621
|
-
else
|
622
|
-
section = wwid.config['current_section']
|
623
|
-
end
|
869
|
+
c.desc 'Select item(s) to finish from a menu of matching entries'
|
870
|
+
c.switch %i[i interactive]
|
624
871
|
|
872
|
+
c.action do |_global_options, options, args|
|
625
873
|
unless options[:auto]
|
626
874
|
if options[:took]
|
627
875
|
took = wwid.chronify_qty(options[:took])
|
628
|
-
|
876
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
629
877
|
end
|
630
878
|
|
631
|
-
|
879
|
+
raise Doing::Errors::InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
632
880
|
|
633
|
-
|
881
|
+
raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
634
882
|
|
635
883
|
if options[:at]
|
636
|
-
finish_date = wwid.chronify(options[:at])
|
637
|
-
|
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?
|
638
886
|
|
639
887
|
date = options[:took] ? finish_date - took : finish_date
|
640
888
|
elsif options[:back]
|
641
889
|
date = wwid.chronify(options[:back])
|
642
890
|
|
643
|
-
|
891
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
644
892
|
elsif options[:took]
|
645
893
|
date = wwid.chronify_qty(options[:took])
|
646
894
|
else
|
@@ -651,44 +899,42 @@ command :finish do |c|
|
|
651
899
|
if options[:tag].nil?
|
652
900
|
tags = []
|
653
901
|
else
|
654
|
-
tags = options[:tag].
|
655
|
-
options[:bool] = case options[:bool]
|
656
|
-
when /(and|all)/i
|
657
|
-
'AND'
|
658
|
-
when /(any|or)/i
|
659
|
-
'OR'
|
660
|
-
when /(not|none)/i
|
661
|
-
'NOT'
|
662
|
-
else
|
663
|
-
'AND'
|
664
|
-
end
|
902
|
+
tags = options[:tag].to_tags
|
665
903
|
end
|
666
904
|
|
667
|
-
|
905
|
+
raise Doing::Errors::InvalidArgument, 'Only one argument allowed' if args.length > 1
|
668
906
|
|
669
|
-
|
907
|
+
raise Doing::Errors::InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
908
|
+
|
909
|
+
if options[:interactive]
|
910
|
+
count = 0
|
911
|
+
else
|
912
|
+
count = args[0] ? args[0].to_i : 1
|
913
|
+
end
|
670
914
|
|
671
|
-
count = args[0] ? args[0].to_i : 1
|
672
915
|
opts = {
|
673
|
-
archive: options[:
|
916
|
+
archive: options[:archive],
|
674
917
|
back: date,
|
675
918
|
count: count,
|
676
919
|
date: options[:date],
|
677
920
|
search: options[:search],
|
678
|
-
section: section,
|
921
|
+
section: options[:section],
|
679
922
|
sequential: options[:auto],
|
680
923
|
tag: tags,
|
681
|
-
tag_bool: options[:bool],
|
924
|
+
tag_bool: options[:bool].normalize_bool,
|
682
925
|
tags: ['done'],
|
683
|
-
unfinished: options[:unfinished]
|
926
|
+
unfinished: options[:unfinished],
|
927
|
+
remove: options[:remove],
|
928
|
+
interactive: options[:interactive]
|
684
929
|
}
|
930
|
+
|
685
931
|
wwid.tag_last(opts)
|
686
932
|
end
|
687
933
|
end
|
688
934
|
|
689
935
|
desc 'Repeat last entry as new entry'
|
690
|
-
command [
|
691
|
-
c.desc '
|
936
|
+
command %i[again resume] do |c|
|
937
|
+
c.desc 'Get last entry from a specific section'
|
692
938
|
c.arg_name 'NAME'
|
693
939
|
c.flag %i[s section], default_value: 'All'
|
694
940
|
|
@@ -701,52 +947,68 @@ command [:again, :resume] do |c|
|
|
701
947
|
c.flag [:tag]
|
702
948
|
|
703
949
|
c.desc 'Repeat last entry matching search. Surround with
|
704
|
-
slashes for regex (e.g. "/query/").'
|
950
|
+
slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
|
705
951
|
c.arg_name 'QUERY'
|
706
952
|
c.flag [:search]
|
707
953
|
|
708
954
|
c.desc 'Boolean used to combine multiple tags'
|
709
955
|
c.arg_name 'BOOLEAN'
|
710
|
-
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
|
711
960
|
|
712
961
|
c.desc 'Note'
|
713
962
|
c.arg_name 'TEXT'
|
714
963
|
c.flag %i[n note]
|
715
964
|
|
965
|
+
c.desc 'Select item to resume from a menu of matching entries'
|
966
|
+
c.switch %i[i interactive]
|
967
|
+
|
716
968
|
c.action do |_global_options, options, _args|
|
717
|
-
tags = options[:tag].nil? ? [] : options[:tag].
|
718
|
-
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
'NOT'
|
725
|
-
else
|
726
|
-
'AND'
|
727
|
-
end
|
728
|
-
opts = {
|
729
|
-
in: options[:in],
|
730
|
-
note: options[:n],
|
731
|
-
search: options[:search],
|
732
|
-
section: options[:s],
|
733
|
-
tag: tags,
|
734
|
-
tag_bool: options[:bool]
|
735
|
-
}
|
736
|
-
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)
|
737
976
|
end
|
738
977
|
end
|
739
978
|
|
740
979
|
desc 'Add tag(s) to last entry'
|
980
|
+
long_desc 'Add (or remove) tags from the last entry, or from multiple entries
|
981
|
+
(with `--count`), entries matching a search (with `--search`), or entries
|
982
|
+
containing another tag (with `--tag`).
|
983
|
+
|
984
|
+
When removing tags with `-r`, wildcards are allowed (`*` to match
|
985
|
+
multiple characters, `?` to match a single character). With `--regex`,
|
986
|
+
regular expressions will be interpreted instead of wildcards.
|
987
|
+
|
988
|
+
For all tag removals the match is case insensitive by default, but if
|
989
|
+
the tag search string contains any uppercase letters, the match will
|
990
|
+
become case sensitive automatically.
|
991
|
+
|
992
|
+
Tag name arguments do not need to be prefixed with @.'
|
741
993
|
arg_name 'TAG', :multiple
|
742
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
|
+
|
743
1001
|
c.desc 'Section'
|
744
1002
|
c.arg_name 'SECTION_NAME'
|
745
1003
|
c.flag %i[s section], default_value: 'All'
|
746
1004
|
|
747
1005
|
c.desc 'How many recent entries to tag (0 for all)'
|
748
1006
|
c.arg_name 'COUNT'
|
749
|
-
c.flag %i[c count], default_value: 1
|
1007
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
1008
|
+
|
1009
|
+
c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
|
1010
|
+
c.arg_name 'ORIG_TAG'
|
1011
|
+
c.flag %i[rename]
|
750
1012
|
|
751
1013
|
c.desc 'Don\'t ask permission to tag all entries when count is 0'
|
752
1014
|
c.switch %i[force], negatable: false, default_value: false
|
@@ -757,6 +1019,9 @@ command :tag do |c|
|
|
757
1019
|
c.desc 'Remove given tag(s)'
|
758
1020
|
c.switch %i[r remove], negatable: false, default_value: false
|
759
1021
|
|
1022
|
+
c.desc 'Interpret tag string as regular expression (with --remove)'
|
1023
|
+
c.switch %i[regex], negatable: false, default_value: false
|
1024
|
+
|
760
1025
|
c.desc 'Tag last entry (or entries) not marked @done'
|
761
1026
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
762
1027
|
|
@@ -768,18 +1033,21 @@ command :tag do |c|
|
|
768
1033
|
c.arg_name 'TAG'
|
769
1034
|
c.flag [:tag]
|
770
1035
|
|
771
|
-
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")'
|
772
1037
|
c.arg_name 'QUERY'
|
773
1038
|
c.flag [:search]
|
774
1039
|
|
775
1040
|
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
776
1041
|
c.arg_name 'BOOLEAN'
|
777
|
-
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]
|
778
1046
|
|
779
1047
|
c.action do |_global_options, options, args|
|
780
|
-
|
1048
|
+
raise Doing::Errors::MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:a]
|
781
1049
|
|
782
|
-
|
1050
|
+
raise Doing::Errors::InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
783
1051
|
|
784
1052
|
section = 'All'
|
785
1053
|
|
@@ -791,17 +1059,7 @@ command :tag do |c|
|
|
791
1059
|
if options[:tag].nil?
|
792
1060
|
search_tags = []
|
793
1061
|
else
|
794
|
-
search_tags = options[:tag].
|
795
|
-
options[:bool] = case options[:bool]
|
796
|
-
when /(and|all)/i
|
797
|
-
'AND'
|
798
|
-
when /(any|or)/i
|
799
|
-
'OR'
|
800
|
-
when /(not|none)/i
|
801
|
-
'NOT'
|
802
|
-
else
|
803
|
-
'AND'
|
804
|
-
end
|
1062
|
+
search_tags = options[:tag].to_tags
|
805
1063
|
end
|
806
1064
|
|
807
1065
|
if options[:autotag]
|
@@ -816,7 +1074,13 @@ command :tag do |c|
|
|
816
1074
|
tags.map! { |tag| tag.sub(/^@/, '').strip }
|
817
1075
|
end
|
818
1076
|
|
819
|
-
|
1077
|
+
if options[:interactive]
|
1078
|
+
count = 0
|
1079
|
+
options[:force] = true
|
1080
|
+
else
|
1081
|
+
count = options[:count].to_i
|
1082
|
+
end
|
1083
|
+
|
820
1084
|
|
821
1085
|
if count.zero? && !options[:force]
|
822
1086
|
if options[:search]
|
@@ -843,42 +1107,125 @@ command :tag do |c|
|
|
843
1107
|
exit_now! 'Cancelled' unless res
|
844
1108
|
end
|
845
1109
|
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
850
|
-
|
851
|
-
|
852
|
-
|
853
|
-
tag: search_tags,
|
854
|
-
tag_bool: options[:bool],
|
855
|
-
tags: tags,
|
856
|
-
unfinished: options[:unfinished]
|
857
|
-
}
|
858
|
-
wwid.tag_last(opts)
|
1110
|
+
options[:count] = count
|
1111
|
+
options[:section] = section
|
1112
|
+
options[:tag] = search_tags
|
1113
|
+
options[:tags] = tags
|
1114
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
1115
|
+
|
1116
|
+
wwid.tag_last(options)
|
859
1117
|
end
|
860
1118
|
end
|
861
1119
|
|
862
|
-
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'
|
863
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
|
+
|
864
1139
|
c.desc 'Section'
|
865
|
-
c.arg_name '
|
866
|
-
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
|
867
1146
|
|
868
|
-
c.desc '
|
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
|
1149
|
+
|
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'
|
869
1154
|
c.switch %i[r remove], negatable: false, default_value: false
|
870
1155
|
|
871
|
-
c.desc '
|
1156
|
+
c.desc 'Flag last entry (or entries) not marked @done'
|
872
1157
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
873
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]
|
1174
|
+
|
874
1175
|
c.action do |_global_options, options, _args|
|
875
|
-
mark =
|
876
|
-
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
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)
|
882
1229
|
end
|
883
1230
|
end
|
884
1231
|
|
@@ -889,13 +1236,19 @@ long_desc %(
|
|
889
1236
|
)
|
890
1237
|
arg_name '[SECTION|@TAGS]'
|
891
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
|
+
|
892
1245
|
c.desc 'Tag filter, combine multiple tags with a comma. Added for compatibility with other commands.'
|
893
1246
|
c.arg_name 'TAG'
|
894
1247
|
c.flag [:tag]
|
895
1248
|
|
896
1249
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
897
1250
|
c.arg_name 'BOOLEAN'
|
898
|
-
c.flag %i[b bool], must_match:
|
1251
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
899
1252
|
|
900
1253
|
c.desc 'Max count to show'
|
901
1254
|
c.arg_name 'MAX'
|
@@ -913,13 +1266,13 @@ command :show do |c|
|
|
913
1266
|
c.arg_name 'DATE_STRING'
|
914
1267
|
c.flag [:after]
|
915
1268
|
|
916
|
-
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")'
|
917
1270
|
c.arg_name 'QUERY'
|
918
1271
|
c.flag [:search]
|
919
1272
|
|
920
1273
|
c.desc 'Sort order (asc/desc)'
|
921
1274
|
c.arg_name 'ORDER'
|
922
|
-
c.flag %i[s sort], must_match:
|
1275
|
+
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
923
1276
|
|
924
1277
|
c.desc %(
|
925
1278
|
Date range to show, or a single day to filter date on.
|
@@ -937,21 +1290,26 @@ command :show do |c|
|
|
937
1290
|
|
938
1291
|
c.desc 'Sort tags by (name|time)'
|
939
1292
|
default = 'time'
|
940
|
-
default =
|
1293
|
+
default = settings['tag_sort'] || 'name'
|
941
1294
|
c.arg_name 'KEY'
|
942
1295
|
c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
|
943
1296
|
|
944
1297
|
c.desc 'Tag sort direction (asc|desc)'
|
945
1298
|
c.arg_name 'DIRECTION'
|
946
|
-
c.flag [:tag_order], must_match:
|
1299
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
947
1300
|
|
948
1301
|
c.desc 'Only show items with recorded time intervals'
|
949
1302
|
c.switch [:only_timed], default_value: false, negatable: false
|
950
1303
|
|
951
|
-
c.desc '
|
1304
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1305
|
+
c.switch %i[i interactive]
|
1306
|
+
|
1307
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
952
1308
|
c.arg_name 'FORMAT'
|
953
|
-
c.flag %i[o output]
|
1309
|
+
c.flag %i[o output]
|
954
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
|
+
|
955
1313
|
tag_filter = false
|
956
1314
|
tags = []
|
957
1315
|
if args.length.positive?
|
@@ -966,7 +1324,7 @@ command :show do |c|
|
|
966
1324
|
section = 'All'
|
967
1325
|
else
|
968
1326
|
section = wwid.guess_section(args[0])
|
969
|
-
|
1327
|
+
raise Doing::Errors::InvalidSection, "No such section: #{args[0]}" unless section
|
970
1328
|
|
971
1329
|
args.shift
|
972
1330
|
end
|
@@ -978,72 +1336,51 @@ command :show do |c|
|
|
978
1336
|
end
|
979
1337
|
end
|
980
1338
|
else
|
981
|
-
section =
|
1339
|
+
section = settings['current_section']
|
982
1340
|
end
|
983
1341
|
|
984
|
-
tags.concat(options[:tag].
|
985
|
-
options[:bool] = case options[:bool]
|
986
|
-
when /(and|all)/i
|
987
|
-
'AND'
|
988
|
-
when /(any|or)/i
|
989
|
-
'OR'
|
990
|
-
when /(not|none)/i
|
991
|
-
'NOT'
|
992
|
-
else
|
993
|
-
'AND'
|
994
|
-
end
|
1342
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
995
1343
|
|
996
1344
|
unless tags.empty?
|
997
1345
|
tag_filter = {
|
998
1346
|
'tags' => tags,
|
999
|
-
'bool' => options[:bool]
|
1347
|
+
'bool' => options[:bool].normalize_bool
|
1000
1348
|
}
|
1001
1349
|
end
|
1002
1350
|
|
1003
1351
|
if options[:from]
|
1352
|
+
|
1004
1353
|
date_string = options[:from]
|
1005
1354
|
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
1006
1355
|
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
1007
|
-
start = wwid.chronify(dates[0])
|
1008
|
-
finish = wwid.chronify(dates[2])
|
1356
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1357
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1009
1358
|
else
|
1010
|
-
start = wwid.chronify(date_string)
|
1359
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1011
1360
|
finish = false
|
1012
1361
|
end
|
1013
|
-
|
1362
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1014
1363
|
dates = [start, finish]
|
1015
1364
|
end
|
1016
1365
|
|
1017
1366
|
options[:times] = true if options[:totals]
|
1018
1367
|
|
1019
|
-
tags_color =
|
1368
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1020
1369
|
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
order: options[:s],
|
1036
|
-
output: options[:output],
|
1037
|
-
search: options[:search],
|
1038
|
-
section: section,
|
1039
|
-
sort_tags: options[:sort_tags],
|
1040
|
-
tag_filter: tag_filter,
|
1041
|
-
tag_order: tag_order,
|
1042
|
-
tags_color: tags_color,
|
1043
|
-
times: options[:t],
|
1044
|
-
totals: options[:totals]
|
1045
|
-
}
|
1046
|
-
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)
|
1047
1384
|
end
|
1048
1385
|
end
|
1049
1386
|
|
@@ -1055,7 +1392,12 @@ long_desc <<~'EODESC'
|
|
1055
1392
|
EODESC
|
1056
1393
|
|
1057
1394
|
arg_name 'SEARCH_PATTERN'
|
1058
|
-
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
|
+
|
1059
1401
|
c.desc 'Section'
|
1060
1402
|
c.arg_name 'NAME'
|
1061
1403
|
c.flag %i[s section], default_value: 'All'
|
@@ -1068,9 +1410,9 @@ command [:grep, :search] do |c|
|
|
1068
1410
|
c.arg_name 'DATE_STRING'
|
1069
1411
|
c.flag [:after]
|
1070
1412
|
|
1071
|
-
c.desc
|
1413
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1072
1414
|
c.arg_name 'FORMAT'
|
1073
|
-
c.flag %i[o output]
|
1415
|
+
c.flag %i[o output]
|
1074
1416
|
|
1075
1417
|
c.desc 'Show time intervals on @done tasks'
|
1076
1418
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1080,36 +1422,31 @@ command [:grep, :search] do |c|
|
|
1080
1422
|
|
1081
1423
|
c.desc 'Sort tags by (name|time)'
|
1082
1424
|
default = 'time'
|
1083
|
-
default =
|
1425
|
+
default = settings['tag_sort'] || 'name'
|
1084
1426
|
c.arg_name 'KEY'
|
1085
1427
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1086
1428
|
|
1087
1429
|
c.desc 'Only show items with recorded time intervals'
|
1088
1430
|
c.switch [:only_timed], default_value: false, negatable: false
|
1089
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
|
+
|
1090
1435
|
c.action do |_global_options, options, args|
|
1091
|
-
|
1436
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1092
1437
|
|
1093
|
-
|
1438
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1439
|
+
|
1440
|
+
section = wwid.guess_section(options[:section]) if options[:section]
|
1094
1441
|
|
1095
1442
|
options[:times] = true if options[:totals]
|
1096
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
|
1097
1448
|
|
1098
|
-
|
1099
|
-
after: options[:after],
|
1100
|
-
before: options[:before],
|
1101
|
-
highlight: true,
|
1102
|
-
only_timed: options[:only_timed],
|
1103
|
-
output: options[:output],
|
1104
|
-
search: args.join(' '),
|
1105
|
-
section: section,
|
1106
|
-
sort_tags: options[:sort_tags],
|
1107
|
-
tags_color: tags_color,
|
1108
|
-
times: options[:times],
|
1109
|
-
totals: options[:totals]
|
1110
|
-
}
|
1111
|
-
|
1112
|
-
puts wwid.list_section(opts)
|
1449
|
+
Doing::Pager.page wwid.list_section(options)
|
1113
1450
|
end
|
1114
1451
|
end
|
1115
1452
|
|
@@ -1117,6 +1454,11 @@ desc 'List recent entries'
|
|
1117
1454
|
default_value 10
|
1118
1455
|
arg_name 'COUNT'
|
1119
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
|
+
|
1120
1462
|
c.desc 'Section'
|
1121
1463
|
c.arg_name 'NAME'
|
1122
1464
|
c.flag %i[s section], default_value: 'All'
|
@@ -1129,32 +1471,42 @@ command :recent do |c|
|
|
1129
1471
|
|
1130
1472
|
c.desc 'Sort tags by (name|time)'
|
1131
1473
|
default = 'time'
|
1132
|
-
default =
|
1474
|
+
default = settings['tag_sort'] || 'name'
|
1133
1475
|
c.arg_name 'KEY'
|
1134
1476
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1135
1477
|
|
1478
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1479
|
+
c.switch %i[i interactive]
|
1480
|
+
|
1136
1481
|
c.action do |global_options, options, args|
|
1137
1482
|
section = wwid.guess_section(options[:s]) || options[:s].cap_first
|
1138
1483
|
|
1139
1484
|
unless global_options[:version]
|
1140
|
-
if
|
1141
|
-
config_count =
|
1485
|
+
if settings['templates']['recent'].key?('count')
|
1486
|
+
config_count = settings['templates']['recent']['count'].to_i
|
1142
1487
|
else
|
1143
1488
|
config_count = 10
|
1144
1489
|
end
|
1145
|
-
|
1490
|
+
|
1491
|
+
if options[:interactive]
|
1492
|
+
count = 0
|
1493
|
+
else
|
1494
|
+
count = args.empty? ? config_count : args[0].to_i
|
1495
|
+
end
|
1496
|
+
|
1146
1497
|
options[:t] = true if options[:totals]
|
1147
1498
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1148
|
-
tags_color =
|
1499
|
+
tags_color = settings.key?('tags_color') ? settings['tags_color'] : nil
|
1149
1500
|
|
1150
1501
|
opts = {
|
1151
1502
|
sort_tags: options[:sort_tags],
|
1152
1503
|
tags_color: tags_color,
|
1153
1504
|
times: options[:t],
|
1154
|
-
totals: options[:totals]
|
1505
|
+
totals: options[:totals],
|
1506
|
+
interactive: options[:interactive]
|
1155
1507
|
}
|
1156
1508
|
|
1157
|
-
|
1509
|
+
Doing::Pager::page wwid.recent(count, section.cap_first, opts)
|
1158
1510
|
|
1159
1511
|
end
|
1160
1512
|
end
|
@@ -1162,6 +1514,11 @@ end
|
|
1162
1514
|
|
1163
1515
|
desc 'List entries from today'
|
1164
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
|
+
|
1165
1522
|
c.desc 'Specify a section'
|
1166
1523
|
c.arg_name 'NAME'
|
1167
1524
|
c.flag %i[s section], default_value: 'All'
|
@@ -1174,13 +1531,13 @@ command :today do |c|
|
|
1174
1531
|
|
1175
1532
|
c.desc 'Sort tags by (name|time)'
|
1176
1533
|
default = 'time'
|
1177
|
-
default =
|
1534
|
+
default = settings['tag_sort'] || 'name'
|
1178
1535
|
c.arg_name 'KEY'
|
1179
1536
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1180
1537
|
|
1181
|
-
c.desc
|
1538
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1182
1539
|
c.arg_name 'FORMAT'
|
1183
|
-
c.flag %i[o output]
|
1540
|
+
c.flag %i[o output]
|
1184
1541
|
|
1185
1542
|
c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
|
1186
1543
|
c.arg_name 'TIME_STRING'
|
@@ -1191,6 +1548,8 @@ command :today do |c|
|
|
1191
1548
|
c.flag [:after]
|
1192
1549
|
|
1193
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
|
+
|
1194
1553
|
options[:t] = true if options[:totals]
|
1195
1554
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1196
1555
|
opt = {
|
@@ -1198,9 +1557,10 @@ command :today do |c|
|
|
1198
1557
|
before: options[:before],
|
1199
1558
|
section: options[:section],
|
1200
1559
|
sort_tags: options[:sort_tags],
|
1201
|
-
totals: options[:totals]
|
1560
|
+
totals: options[:totals],
|
1561
|
+
order: settings.dig('templates', 'today', 'order')
|
1202
1562
|
}
|
1203
|
-
|
1563
|
+
Doing::Pager.page wwid.today(options[:times], options[:output], opt).chomp
|
1204
1564
|
end
|
1205
1565
|
end
|
1206
1566
|
|
@@ -1210,6 +1570,10 @@ and "2d" would be interpreted as "two days ago." If you use "to" or "through" be
|
|
1210
1570
|
it will create a range.)
|
1211
1571
|
arg_name 'DATE_STRING'
|
1212
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
|
+
|
1213
1577
|
c.desc 'Section'
|
1214
1578
|
c.arg_name 'NAME'
|
1215
1579
|
c.flag %i[s section], default_value: 'All'
|
@@ -1222,38 +1586,40 @@ command :on do |c|
|
|
1222
1586
|
|
1223
1587
|
c.desc 'Sort tags by (name|time)'
|
1224
1588
|
default = 'time'
|
1225
|
-
default =
|
1589
|
+
default = settings['tag_sort'] || 'name'
|
1226
1590
|
c.arg_name 'KEY'
|
1227
1591
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1228
1592
|
|
1229
|
-
c.desc
|
1593
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1230
1594
|
c.arg_name 'FORMAT'
|
1231
|
-
c.flag %i[o output]
|
1595
|
+
c.flag %i[o output]
|
1232
1596
|
|
1233
1597
|
c.action do |_global_options, options, args|
|
1234
|
-
|
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?
|
1235
1601
|
|
1236
1602
|
date_string = args.join(' ')
|
1237
1603
|
|
1238
1604
|
if date_string =~ / (to|through|thru) /
|
1239
1605
|
dates = date_string.split(/ (to|through|thru) /)
|
1240
|
-
start = wwid.chronify(dates[0])
|
1241
|
-
finish = wwid.chronify(dates[2])
|
1606
|
+
start = wwid.chronify(dates[0], guess: :begin)
|
1607
|
+
finish = wwid.chronify(dates[2], guess: :end)
|
1242
1608
|
else
|
1243
|
-
start = wwid.chronify(date_string)
|
1609
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1244
1610
|
finish = false
|
1245
1611
|
end
|
1246
1612
|
|
1247
|
-
|
1613
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1248
1614
|
|
1249
1615
|
message = "Date interpreted as #{start}"
|
1250
1616
|
message += " to #{finish}" if finish
|
1251
|
-
|
1617
|
+
Doing.logger.debug(message)
|
1252
1618
|
|
1253
1619
|
options[:t] = true if options[:totals]
|
1254
1620
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1255
1621
|
|
1256
|
-
|
1622
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1257
1623
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1258
1624
|
end
|
1259
1625
|
end
|
@@ -1263,6 +1629,9 @@ long_desc %(Date argument can be natural language and are always interpreted as
|
|
1263
1629
|
and "2d" would be interpreted as "two days ago.")
|
1264
1630
|
arg_name 'DATE_STRING'
|
1265
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
|
+
|
1266
1635
|
c.desc 'Section'
|
1267
1636
|
c.arg_name 'NAME'
|
1268
1637
|
c.flag %i[s section], default_value: 'All'
|
@@ -1275,47 +1644,52 @@ command :since do |c|
|
|
1275
1644
|
|
1276
1645
|
c.desc 'Sort tags by (name|time)'
|
1277
1646
|
default = 'time'
|
1278
|
-
default =
|
1647
|
+
default = settings['tag_sort'] || 'name'
|
1279
1648
|
c.arg_name 'KEY'
|
1280
1649
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1281
1650
|
|
1282
|
-
c.desc
|
1651
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1283
1652
|
c.arg_name 'FORMAT'
|
1284
|
-
c.flag %i[o output]
|
1653
|
+
c.flag %i[o output]
|
1285
1654
|
|
1286
1655
|
c.action do |_global_options, options, args|
|
1287
|
-
|
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?
|
1288
1659
|
|
1289
1660
|
date_string = args.join(' ')
|
1290
1661
|
|
1291
|
-
date_string
|
1292
|
-
date_string.sub!(/(
|
1662
|
+
date_string.sub!(/(day) (\d)/, '\1 at \2')
|
1663
|
+
date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
|
1293
1664
|
|
1294
|
-
start = wwid.chronify(date_string)
|
1665
|
+
start = wwid.chronify(date_string, guess: :begin)
|
1295
1666
|
finish = Time.now
|
1296
1667
|
|
1297
|
-
|
1668
|
+
raise Doing::Errors::InvalidTimeExpression, 'Unrecognized date string' unless start
|
1298
1669
|
|
1299
|
-
|
1300
|
-
wwid.results.push(message)
|
1670
|
+
Doing.logger.debug("Date interpreted as #{start} through the current time")
|
1301
1671
|
|
1302
1672
|
options[:t] = true if options[:totals]
|
1303
1673
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1304
1674
|
|
1305
|
-
|
1675
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:s], options[:t], options[:output],
|
1306
1676
|
{ totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
1307
1677
|
end
|
1308
1678
|
end
|
1309
1679
|
|
1310
1680
|
desc 'List entries from yesterday'
|
1311
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
|
+
|
1312
1686
|
c.desc 'Specify a section'
|
1313
1687
|
c.arg_name 'NAME'
|
1314
1688
|
c.flag %i[s section], default_value: 'All'
|
1315
1689
|
|
1316
|
-
c.desc
|
1690
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1317
1691
|
c.arg_name 'FORMAT'
|
1318
|
-
c.flag %i[o output]
|
1692
|
+
c.flag %i[o output]
|
1319
1693
|
|
1320
1694
|
c.desc 'Show time intervals on @done tasks'
|
1321
1695
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1325,7 +1699,7 @@ command :yesterday do |c|
|
|
1325
1699
|
|
1326
1700
|
c.desc 'Sort tags by (name|time)'
|
1327
1701
|
default = 'time'
|
1328
|
-
default =
|
1702
|
+
default = settings['tag_sort'] || 'name'
|
1329
1703
|
c.arg_name 'KEY'
|
1330
1704
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
1331
1705
|
|
@@ -1339,33 +1713,40 @@ command :yesterday do |c|
|
|
1339
1713
|
|
1340
1714
|
c.desc 'Tag sort direction (asc|desc)'
|
1341
1715
|
c.arg_name 'DIRECTION'
|
1342
|
-
c.flag [:tag_order], must_match:
|
1716
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
1343
1717
|
|
1344
1718
|
c.action do |_global_options, options, _args|
|
1345
|
-
|
1346
|
-
|
1347
|
-
else
|
1348
|
-
'asc'
|
1349
|
-
end
|
1719
|
+
raise InvalidExportType, "Invalid export type: #{options[:output]}" if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
1720
|
+
|
1350
1721
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
1722
|
+
|
1351
1723
|
opt = {
|
1352
1724
|
after: options[:after],
|
1353
1725
|
before: options[:before],
|
1354
1726
|
sort_tags: options[:sort_tags],
|
1355
|
-
tag_order: options[:tag_order],
|
1356
|
-
totals: options[:totals]
|
1727
|
+
tag_order: options[:tag_order].normalize_order,
|
1728
|
+
totals: options[:totals],
|
1729
|
+
order: settings.dig('templates', 'today', 'order')
|
1357
1730
|
}
|
1358
|
-
|
1731
|
+
Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
|
1359
1732
|
end
|
1360
1733
|
end
|
1361
1734
|
|
1362
1735
|
desc 'Show the last entry, optionally edit'
|
1363
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
|
+
|
1364
1745
|
c.desc 'Specify a section'
|
1365
1746
|
c.arg_name 'NAME'
|
1366
1747
|
c.flag %i[s section], default_value: 'All'
|
1367
1748
|
|
1368
|
-
c.desc "Edit entry with #{
|
1749
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
1369
1750
|
c.switch %i[e editor], negatable: false, default_value: false
|
1370
1751
|
|
1371
1752
|
c.desc 'Tag filter, combine multiple tags with a comma.'
|
@@ -1374,19 +1755,19 @@ command :last do |c|
|
|
1374
1755
|
|
1375
1756
|
c.desc 'Tag boolean'
|
1376
1757
|
c.arg_name 'BOOLEAN'
|
1377
|
-
c.flag [:bool], must_match:
|
1758
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1378
1759
|
|
1379
|
-
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")'
|
1380
1761
|
c.arg_name 'QUERY'
|
1381
1762
|
c.flag [:search]
|
1382
1763
|
|
1383
|
-
c.action do |
|
1384
|
-
|
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]
|
1385
1766
|
|
1386
1767
|
if options[:tag].nil?
|
1387
1768
|
tags = []
|
1388
1769
|
else
|
1389
|
-
tags = options[:tag].
|
1770
|
+
tags = options[:tag].to_tags
|
1390
1771
|
options[:bool] = case options[:bool]
|
1391
1772
|
when /(any|or)/i
|
1392
1773
|
:or
|
@@ -1401,7 +1782,7 @@ command :last do |c|
|
|
1401
1782
|
if options[:editor]
|
1402
1783
|
wwid.edit_last(section: options[:s], options: { search: options[:search], tag: tags, tag_bool: options[:bool] })
|
1403
1784
|
else
|
1404
|
-
|
1785
|
+
Doing::Pager::page wwid.last(times: true, section: options[:s],
|
1405
1786
|
options: { search: options[:search], tag: tags, tag_bool: options[:bool] }).strip
|
1406
1787
|
end
|
1407
1788
|
end
|
@@ -1423,17 +1804,19 @@ command :choose do |c|
|
|
1423
1804
|
c.action do |_global_options, _options, _args|
|
1424
1805
|
section = wwid.choose_section
|
1425
1806
|
|
1426
|
-
|
1807
|
+
Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
|
1427
1808
|
end
|
1428
1809
|
end
|
1429
1810
|
|
1430
1811
|
desc 'Add a new section to the "doing" file'
|
1431
1812
|
arg_name 'SECTION_NAME'
|
1432
1813
|
command :add_section do |c|
|
1814
|
+
c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
|
1815
|
+
|
1433
1816
|
c.action do |_global_options, _options, args|
|
1434
|
-
|
1817
|
+
raise Doing::Errors::InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
1435
1818
|
|
1436
|
-
wwid.add_section(args
|
1819
|
+
wwid.add_section(args.join(' ').cap_first)
|
1437
1820
|
wwid.write(wwid.doing_file)
|
1438
1821
|
end
|
1439
1822
|
end
|
@@ -1441,18 +1824,42 @@ end
|
|
1441
1824
|
desc 'List available color variables for configuration templates and views'
|
1442
1825
|
command :colors do |c|
|
1443
1826
|
c.action do |_global_options, _options, _args|
|
1444
|
-
clrs = wwid.colors
|
1445
1827
|
bgs = []
|
1446
1828
|
fgs = []
|
1447
|
-
|
1448
|
-
if
|
1449
|
-
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}")
|
1450
1832
|
else
|
1451
|
-
fgs.push("#{
|
1833
|
+
fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
|
1452
1834
|
end
|
1453
1835
|
end
|
1454
|
-
|
1455
|
-
|
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)
|
1456
1863
|
end
|
1457
1864
|
end
|
1458
1865
|
|
@@ -1460,6 +1867,9 @@ desc 'Display a user-created view'
|
|
1460
1867
|
long_desc 'Command line options override view configuration'
|
1461
1868
|
arg_name 'VIEW_NAME'
|
1462
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
|
+
|
1463
1873
|
c.desc 'Section'
|
1464
1874
|
c.arg_name 'NAME'
|
1465
1875
|
c.flag %i[s section]
|
@@ -1468,9 +1878,9 @@ command :view do |c|
|
|
1468
1878
|
c.arg_name 'COUNT'
|
1469
1879
|
c.flag %i[c count], must_match: /^\d+$/, type: Integer
|
1470
1880
|
|
1471
|
-
c.desc
|
1881
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
1472
1882
|
c.arg_name 'FORMAT'
|
1473
|
-
c.flag %i[o output]
|
1883
|
+
c.flag %i[o output]
|
1474
1884
|
|
1475
1885
|
c.desc 'Show time intervals on @done tasks'
|
1476
1886
|
c.switch %i[t times], default_value: true, negatable: true
|
@@ -1487,9 +1897,9 @@ command :view do |c|
|
|
1487
1897
|
|
1488
1898
|
c.desc 'Tag boolean (AND,OR,NOT)'
|
1489
1899
|
c.arg_name 'BOOLEAN'
|
1490
|
-
c.flag %i[b bool], must_match:
|
1900
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'OR'
|
1491
1901
|
|
1492
|
-
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")'
|
1493
1903
|
c.arg_name 'QUERY'
|
1494
1904
|
c.flag [:search]
|
1495
1905
|
|
@@ -1499,7 +1909,7 @@ command :view do |c|
|
|
1499
1909
|
|
1500
1910
|
c.desc 'Tag sort direction (asc|desc)'
|
1501
1911
|
c.arg_name 'DIRECTION'
|
1502
|
-
c.flag [:tag_order], must_match:
|
1912
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER
|
1503
1913
|
|
1504
1914
|
c.desc 'View entries older than date'
|
1505
1915
|
c.arg_name 'DATE_STRING'
|
@@ -1512,8 +1922,13 @@ command :view do |c|
|
|
1512
1922
|
c.desc 'Only show items with recorded time intervals (override view settings)'
|
1513
1923
|
c.switch [:only_timed], default_value: false, negatable: false
|
1514
1924
|
|
1925
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
1926
|
+
c.switch %i[i interactive]
|
1927
|
+
|
1515
1928
|
c.action do |_global_options, options, args|
|
1516
|
-
|
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]
|
1517
1932
|
|
1518
1933
|
title = if args.empty?
|
1519
1934
|
wwid.choose_view
|
@@ -1524,11 +1939,12 @@ command :view do |c|
|
|
1524
1939
|
if options[:section]
|
1525
1940
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1526
1941
|
else
|
1527
|
-
section =
|
1942
|
+
section = settings['current_section']
|
1528
1943
|
end
|
1529
1944
|
|
1530
1945
|
view = wwid.get_view(title)
|
1531
1946
|
if view
|
1947
|
+
page_title = view.key?('title') ? view['title'] : title.cap_first
|
1532
1948
|
only_timed = if (view.key?('only_timed') && view['only_timed']) || options[:only_timed]
|
1533
1949
|
true
|
1534
1950
|
else
|
@@ -1536,7 +1952,7 @@ command :view do |c|
|
|
1536
1952
|
end
|
1537
1953
|
|
1538
1954
|
template = view.key?('template') ? view['template'] : nil
|
1539
|
-
|
1955
|
+
date_format = view.key?('date_format') ? view['date_format'] : nil
|
1540
1956
|
tags_color = view.key?('tags_color') ? view['tags_color'] : nil
|
1541
1957
|
tag_filter = false
|
1542
1958
|
if options[:tag]
|
@@ -1554,7 +1970,7 @@ command :view do |c|
|
|
1554
1970
|
end
|
1555
1971
|
|
1556
1972
|
# If the -o/--output flag was specified, override any default in the view template
|
1557
|
-
options[:
|
1973
|
+
options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
|
1558
1974
|
|
1559
1975
|
count = if options[:c]
|
1560
1976
|
options[:c]
|
@@ -1564,18 +1980,23 @@ command :view do |c|
|
|
1564
1980
|
section = if options[:s]
|
1565
1981
|
section
|
1566
1982
|
else
|
1567
|
-
view.key?('section') ? view['section'] :
|
1983
|
+
view.key?('section') ? view['section'] : settings['current_section']
|
1568
1984
|
end
|
1569
|
-
order = view.key?('order') ? view['order'] : 'asc'
|
1985
|
+
order = view.key?('order') ? view['order'].normalize_order : 'asc'
|
1570
1986
|
|
1571
1987
|
totals = if options[:totals]
|
1572
1988
|
true
|
1573
1989
|
else
|
1574
1990
|
view.key?('totals') ? view['totals'] : false
|
1575
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
|
1576
1997
|
|
1577
1998
|
options[:t] = true if totals
|
1578
|
-
options[:output]&.downcase
|
1999
|
+
output_format = options[:output]&.downcase || 'template'
|
1579
2000
|
|
1580
2001
|
options[:sort_tags] = if options[:tag_sort]
|
1581
2002
|
options[:tag_sort] =~ /^n/i ? true : false
|
@@ -1584,40 +2005,50 @@ command :view do |c|
|
|
1584
2005
|
else
|
1585
2006
|
false
|
1586
2007
|
end
|
2008
|
+
if view.key?('after') && !options[:after]
|
2009
|
+
options[:after] = view['after']
|
2010
|
+
end
|
1587
2011
|
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
view['tag_order'] =~ /^d/i ? 'desc' : 'asc'
|
1592
|
-
else
|
1593
|
-
'asc'
|
1594
|
-
end
|
2012
|
+
if view.key?('before') && !options[:before]
|
2013
|
+
options[:before] = view['before']
|
2014
|
+
end
|
1595
2015
|
|
1596
|
-
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1600
|
-
|
1601
|
-
|
1602
|
-
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1607
|
-
|
1608
|
-
|
1609
|
-
tag_order: tag_order,
|
1610
|
-
tags_color: tags_color,
|
1611
|
-
template: template,
|
1612
|
-
times: options[:t],
|
1613
|
-
totals: totals
|
1614
|
-
}
|
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
|
1615
2029
|
|
1616
|
-
|
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)
|
1617
2048
|
elsif title.instance_of?(FalseClass)
|
1618
2049
|
exit_now! 'Cancelled'
|
1619
2050
|
else
|
1620
|
-
|
2051
|
+
raise Doing::Errors::InvalidView, "View #{title} not found in config"
|
1621
2052
|
end
|
1622
2053
|
end
|
1623
2054
|
end
|
@@ -1634,9 +2065,18 @@ command :views do |c|
|
|
1634
2065
|
end
|
1635
2066
|
|
1636
2067
|
desc 'Move entries between sections'
|
1637
|
-
|
1638
|
-
|
1639
|
-
|
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
|
+
|
1640
2080
|
c.desc 'How many items to keep (ignored if archiving by tag or search)'
|
1641
2081
|
c.arg_name 'X'
|
1642
2082
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1654,7 +2094,7 @@ command :archive do |c|
|
|
1654
2094
|
|
1655
2095
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1656
2096
|
c.arg_name 'BOOLEAN'
|
1657
|
-
c.flag [:bool], must_match:
|
2097
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1658
2098
|
|
1659
2099
|
c.desc 'Search filter'
|
1660
2100
|
c.arg_name 'QUERY'
|
@@ -1667,7 +2107,7 @@ command :archive do |c|
|
|
1667
2107
|
|
1668
2108
|
c.action do |_global_options, options, args|
|
1669
2109
|
if args.empty?
|
1670
|
-
section =
|
2110
|
+
section = settings['current_section']
|
1671
2111
|
tags = []
|
1672
2112
|
elsif args[0] =~ /^all/i
|
1673
2113
|
section = 'all'
|
@@ -1679,34 +2119,25 @@ command :archive do |c|
|
|
1679
2119
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
1680
2120
|
end
|
1681
2121
|
|
1682
|
-
|
2122
|
+
raise Doing::Errors::InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
1683
2123
|
|
1684
|
-
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
|
1685
2130
|
|
1686
|
-
options[:bool] = case options[:bool]
|
1687
|
-
when /(and|all)/i
|
1688
|
-
'AND'
|
1689
|
-
when /(any|or)/i
|
1690
|
-
'OR'
|
1691
|
-
when /(not|none)/i
|
1692
|
-
'NOT'
|
1693
|
-
else
|
1694
|
-
'AND'
|
1695
|
-
end
|
1696
|
-
opts = {
|
1697
|
-
before: options[:before],
|
1698
|
-
bool: options[:bool],
|
1699
|
-
destination: options[:to],
|
1700
|
-
keep: options[:keep],
|
1701
|
-
search: options[:search],
|
1702
|
-
tags: tags
|
1703
|
-
}
|
1704
2131
|
wwid.archive(section, opts)
|
1705
2132
|
end
|
1706
2133
|
end
|
1707
2134
|
|
1708
2135
|
desc 'Move entries to archive file'
|
1709
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
|
+
|
1710
2141
|
c.desc 'How many items to keep in each section (most recent)'
|
1711
2142
|
c.arg_name 'X'
|
1712
2143
|
c.flag %i[k keep], must_match: /^\d+$/, type: Integer
|
@@ -1721,7 +2152,7 @@ command :rotate do |c|
|
|
1721
2152
|
|
1722
2153
|
c.desc 'Tag boolean (AND|OR|NOT)'
|
1723
2154
|
c.arg_name 'BOOLEAN'
|
1724
|
-
c.flag [:bool], must_match:
|
2155
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'AND'
|
1725
2156
|
|
1726
2157
|
c.desc 'Search filter'
|
1727
2158
|
c.arg_name 'QUERY'
|
@@ -1737,23 +2168,14 @@ command :rotate do |c|
|
|
1737
2168
|
options[:section] = wwid.guess_section(options[:section])
|
1738
2169
|
end
|
1739
2170
|
|
1740
|
-
options[:bool] =
|
1741
|
-
when /(and|all)/i
|
1742
|
-
'AND'
|
1743
|
-
when /(any|or)/i
|
1744
|
-
'OR'
|
1745
|
-
when /(not|none)/i
|
1746
|
-
'NOT'
|
1747
|
-
else
|
1748
|
-
'AND'
|
1749
|
-
end
|
2171
|
+
options[:bool] = options[:bool].normalize_bool
|
1750
2172
|
|
1751
2173
|
wwid.rotate(options)
|
1752
2174
|
end
|
1753
2175
|
end
|
1754
2176
|
|
1755
2177
|
desc 'Open the "doing" file in an editor'
|
1756
|
-
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'})."
|
1757
2179
|
command :open do |c|
|
1758
2180
|
if `uname` =~ /Darwin/
|
1759
2181
|
c.desc 'Open with app name'
|
@@ -1764,8 +2186,6 @@ command :open do |c|
|
|
1764
2186
|
c.arg_name 'BUNDLE_ID'
|
1765
2187
|
c.flag %i[b bundle_id]
|
1766
2188
|
end
|
1767
|
-
c.desc "Open with $EDITOR (#{ENV['EDITOR']})"
|
1768
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
1769
2189
|
|
1770
2190
|
c.action do |_global_options, options, _args|
|
1771
2191
|
params = options.dup
|
@@ -1777,30 +2197,54 @@ command :open do |c|
|
|
1777
2197
|
system %(open -a "#{options[:a]}" "#{File.expand_path(wwid.doing_file)}")
|
1778
2198
|
elsif options[:bundle_id]
|
1779
2199
|
system %(open -b "#{options[:b]}" "#{File.expand_path(wwid.doing_file)}")
|
1780
|
-
elsif
|
1781
|
-
|
1782
|
-
|
1783
|
-
|
1784
|
-
|
1785
|
-
|
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
|
1786
2207
|
else
|
1787
2208
|
system %(open "#{File.expand_path(wwid.doing_file)}")
|
1788
2209
|
end
|
1789
|
-
|
1790
2210
|
else
|
1791
|
-
|
2211
|
+
raise Doing::Errors::MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
1792
2212
|
|
1793
|
-
system %(
|
2213
|
+
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
1794
2214
|
end
|
1795
2215
|
end
|
1796
2216
|
end
|
1797
2217
|
|
1798
|
-
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'
|
1799
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
|
+
|
1800
2234
|
c.desc 'Editor to use'
|
1801
2235
|
c.arg_name 'EDITOR'
|
1802
2236
|
c.flag %i[e editor], default_value: nil
|
1803
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
|
+
|
1804
2248
|
if `uname` =~ /Darwin/
|
1805
2249
|
c.desc 'Application to use'
|
1806
2250
|
c.arg_name 'APP_NAME'
|
@@ -1810,31 +2254,84 @@ command :config do |c|
|
|
1810
2254
|
c.arg_name 'BUNDLE_ID'
|
1811
2255
|
c.flag [:b]
|
1812
2256
|
|
1813
|
-
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'})"
|
1814
2258
|
c.switch [:x]
|
1815
2259
|
end
|
1816
2260
|
|
1817
|
-
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
|
+
|
1818
2301
|
if `uname` =~ /Darwin/
|
1819
2302
|
if options[:x]
|
1820
|
-
|
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
|
1821
2313
|
elsif options[:a] || options[:b]
|
1822
2314
|
if options[:a]
|
1823
|
-
`open -a "#{options[:a]}" "#{
|
2315
|
+
`open -a "#{options[:a]}" "#{config_file}"`
|
1824
2316
|
elsif options[:b]
|
1825
|
-
`open -b #{options[:b]} "#{
|
2317
|
+
`open -b #{options[:b]} "#{config_file}"`
|
1826
2318
|
end
|
1827
2319
|
else
|
1828
|
-
|
2320
|
+
editor = options[:e] || Doing::Util.find_default_editor('config')
|
1829
2321
|
|
1830
|
-
editor
|
1831
|
-
|
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
|
1832
2329
|
end
|
1833
2330
|
else
|
1834
|
-
|
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)
|
1835
2333
|
|
1836
|
-
editor
|
1837
|
-
system %(#{editor} "#{wwid.config_file}")
|
2334
|
+
system %(#{editor} "#{config_file}")
|
1838
2335
|
end
|
1839
2336
|
end
|
1840
2337
|
end
|
@@ -1852,12 +2349,19 @@ command :undo do |c|
|
|
1852
2349
|
end
|
1853
2350
|
|
1854
2351
|
desc 'Import entries from an external source'
|
1855
|
-
long_desc
|
2352
|
+
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
|
1856
2353
|
arg_name 'PATH'
|
1857
2354
|
command :import do |c|
|
1858
|
-
c.desc
|
2355
|
+
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
1859
2356
|
c.arg_name 'TYPE'
|
1860
|
-
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
|
1861
2365
|
|
1862
2366
|
c.desc 'Target section'
|
1863
2367
|
c.arg_name 'NAME'
|
@@ -1874,53 +2378,67 @@ command :import do |c|
|
|
1874
2378
|
c.arg_name 'PREFIX'
|
1875
2379
|
c.flag :prefix
|
1876
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
|
+
|
1877
2397
|
c.desc 'Allow entries that overlap existing times'
|
1878
2398
|
c.switch [:overlap], negatable: true
|
1879
2399
|
|
1880
2400
|
c.action do |_global_options, options, args|
|
1881
2401
|
|
1882
2402
|
if options[:section]
|
1883
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1884
|
-
else
|
1885
|
-
section = wwid.config['current_section']
|
2403
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
1886
2404
|
end
|
1887
2405
|
|
1888
|
-
if options[:
|
1889
|
-
|
1890
|
-
|
1891
|
-
|
1892
|
-
|
1893
|
-
|
1894
|
-
|
1895
|
-
|
1896
|
-
|
1897
|
-
wwid.import_timing(path, options)
|
1898
|
-
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
|
1899
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)
|
1900
2425
|
else
|
1901
|
-
|
2426
|
+
raise Doing::Errors::InvalidPluginType, "Invalid import type: #{options[:type]}"
|
1902
2427
|
end
|
1903
2428
|
end
|
1904
2429
|
end
|
1905
2430
|
|
1906
2431
|
pre do |global, _command, _options, _args|
|
1907
|
-
|
1908
|
-
wwid.config_file = global[:config_file]
|
1909
|
-
wwid.configure({ ignore_local: true })
|
1910
|
-
# wwid.results.push("Override config file #{wwid.config_file}")
|
1911
|
-
end
|
1912
|
-
|
1913
|
-
if global[:doing_file]
|
1914
|
-
wwid.init_doing_file(global[:doing_file])
|
1915
|
-
else
|
1916
|
-
wwid.init_doing_file
|
1917
|
-
end
|
2432
|
+
# global[:pager] ||= settings['paginate']
|
1918
2433
|
|
1919
|
-
|
1920
|
-
|
1921
|
-
wwid.config[:include_notes] = false unless global[:notes]
|
2434
|
+
Doing::Pager.paginate = global[:pager]
|
1922
2435
|
|
1923
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
|
1924
2442
|
|
1925
2443
|
# Return true to proceed; false to abort and not call the
|
1926
2444
|
# chosen command
|
@@ -1929,19 +2447,57 @@ pre do |global, _command, _options, _args|
|
|
1929
2447
|
true
|
1930
2448
|
end
|
1931
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
|
+
|
1932
2461
|
post do |global, _command, _options, _args|
|
1933
2462
|
# Use skips_post before a command to skip this
|
1934
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)
|
2470
|
+
|
1935
2471
|
if global[:stdout]
|
1936
|
-
$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])
|
1937
2490
|
else
|
1938
|
-
|
2491
|
+
wwid.init_doing_file
|
1939
2492
|
end
|
1940
|
-
end
|
1941
2493
|
|
1942
|
-
|
1943
|
-
|
1944
|
-
|
2494
|
+
wwid.auto_tag = !global[:noauto]
|
2495
|
+
|
2496
|
+
settings[:include_notes] = false unless global[:notes]
|
2497
|
+
|
2498
|
+
global[:wwid] = wwid
|
2499
|
+
|
2500
|
+
code.call
|
1945
2501
|
end
|
1946
2502
|
|
1947
2503
|
exit run(ARGV)
|