doing 2.1.7 → 2.1.11
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/.yardoc/checksums +9 -9
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +39 -1
- data/Dockerfile +9 -0
- data/Dockerfile-2.6 +9 -0
- data/Dockerfile-2.7 +8 -0
- data/Dockerfile-3.0 +8 -0
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +51 -6
- data/bin/doing +2098 -1944
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +2 -2
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +1 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +132 -18
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +2 -2
- data/docs/doc/Doing/Util/Backup.html +79 -2
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +90 -77
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +97 -1
- data/docs/doc/Symbol.html +36 -2
- data/docs/doc/Time.html +1 -1
- data/docs/doc/_index.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +299 -235
- data/docs/doc/top-level-namespace.html +1 -1
- data/docs/index.md +1 -1
- data/doing.rdoc +9 -2
- data/generate_completions.sh +1 -3
- data/lib/completion/_doing.zsh +1 -1
- data/lib/completion/doing.bash +2 -2
- data/lib/completion/doing.fish +2 -1
- data/lib/doing/completion/bash_completion.rb +2 -2
- data/lib/doing/completion/fish_completion.rb +2 -2
- data/lib/doing/completion/zsh_completion.rb +2 -2
- data/lib/doing/completion.rb +12 -2
- data/lib/doing/configuration.rb +19 -9
- data/lib/doing/hooks.rb +10 -5
- data/lib/doing/items.rb +16 -1
- data/lib/doing/log_adapter.rb +1 -0
- data/lib/doing/pager.rb +2 -20
- data/lib/doing/plugins/import/calendar_import.rb +5 -0
- data/lib/doing/plugins/import/doing_import.rb +2 -0
- data/lib/doing/plugins/import/timing_import.rb +5 -0
- data/lib/doing/prompt.rb +47 -8
- data/lib/doing/string.rb +20 -0
- data/lib/doing/symbol.rb +4 -0
- data/lib/doing/util_backup.rb +38 -8
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +211 -106
- data/lib/doing.rb +1 -0
- data/lib/examples/plugins/hooks.rb +31 -0
- data/scripts/generate_bash_completions.rb +2 -2
- data/scripts/sort_commands.rb +59 -0
- metadata +7 -3
- data/lib/helpers/fuzzyfilefinder +0 -0
data/bin/doing
CHANGED
|
@@ -61,6 +61,7 @@ Doing.logger.benchmark(:configure, :start)
|
|
|
61
61
|
config = Doing.config
|
|
62
62
|
Doing.logger.benchmark(:configure, :finish)
|
|
63
63
|
|
|
64
|
+
config.settings['backup_dir'] = ENV['DOING_BACKUP_DIR'] if ENV['DOING_BACKUP_DIR']
|
|
64
65
|
settings = config.settings
|
|
65
66
|
wwid.config = settings
|
|
66
67
|
|
|
@@ -77,6 +78,8 @@ view your entries with myriad options, with a focus on a "natural" language synt
|
|
|
77
78
|
default_command :recent
|
|
78
79
|
# sort_help :manually
|
|
79
80
|
|
|
81
|
+
## Global options
|
|
82
|
+
|
|
80
83
|
desc 'Output notes if included in the template'
|
|
81
84
|
switch [:notes], default_value: true, negatable: true
|
|
82
85
|
|
|
@@ -113,115 +116,32 @@ flag [:config_file], default_value: config.config_file
|
|
|
113
116
|
desc 'Specify a different doing_file'
|
|
114
117
|
flag %i[f doing_file]
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
long_desc %(Record what you're starting now, or backdate the start time using natural language.
|
|
118
|
-
|
|
119
|
-
A parenthetical at the end of the entry will be converted to a note.
|
|
120
|
-
|
|
121
|
-
Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
|
|
122
|
-
arg_name 'ENTRY'
|
|
123
|
-
command %i[now next] do |c|
|
|
124
|
-
c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
|
|
125
|
-
c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
|
|
126
|
-
c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
|
|
127
|
-
c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
|
|
128
|
-
c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
|
|
129
|
-
|
|
130
|
-
c.desc 'Section'
|
|
131
|
-
c.arg_name 'NAME'
|
|
132
|
-
c.flag %i[s section]
|
|
133
|
-
|
|
134
|
-
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
135
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
|
136
|
-
|
|
137
|
-
c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
|
|
138
|
-
c.arg_name 'DATE_STRING'
|
|
139
|
-
c.flag %i[b back started]
|
|
140
|
-
|
|
141
|
-
c.desc 'Timed entry, marks last entry in section as @done'
|
|
142
|
-
c.switch %i[f finish_last], negatable: false, default_value: false
|
|
143
|
-
|
|
144
|
-
c.desc 'Include a note'
|
|
145
|
-
c.arg_name 'TEXT'
|
|
146
|
-
c.flag %i[n note]
|
|
147
|
-
|
|
148
|
-
# c.desc "Edit entry with specified app"
|
|
149
|
-
# c.arg_name 'editor_app'
|
|
150
|
-
# # c.flag [:a, :app]
|
|
151
|
-
|
|
152
|
-
c.action do |_global_options, options, args|
|
|
153
|
-
if options[:back]
|
|
154
|
-
date = options[:back].chronify(guess: :begin)
|
|
155
|
-
|
|
156
|
-
raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
|
|
157
|
-
else
|
|
158
|
-
date = Time.now
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
if options[:section]
|
|
162
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
163
|
-
else
|
|
164
|
-
options[:section] = settings['current_section']
|
|
165
|
-
end
|
|
166
|
-
|
|
167
|
-
if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
|
|
168
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
169
|
-
|
|
170
|
-
input = date.strftime('%F %R | ')
|
|
171
|
-
input += args.join(' ') unless args.empty?
|
|
172
|
-
input = wwid.fork_editor(input).strip
|
|
173
|
-
|
|
174
|
-
raise EmptyInput, 'No content' if input.empty?
|
|
175
|
-
|
|
176
|
-
date, title, note = wwid.format_input(input)
|
|
177
|
-
note.add(options[:note]) if options[:note]
|
|
178
|
-
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
179
|
-
wwid.write(wwid.doing_file)
|
|
180
|
-
elsif args.length.positive?
|
|
181
|
-
d, title, note = wwid.format_input(args.join(' '))
|
|
182
|
-
date = d.nil? ? date : d
|
|
183
|
-
note.add(options[:note]) if options[:note]
|
|
184
|
-
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
185
|
-
wwid.write(wwid.doing_file)
|
|
186
|
-
elsif $stdin.stat.size.positive?
|
|
187
|
-
input = $stdin.read.strip
|
|
188
|
-
d, title, note = wwid.format_input(input)
|
|
189
|
-
unless d.nil?
|
|
190
|
-
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
191
|
-
date = d
|
|
192
|
-
end
|
|
193
|
-
note.add(options[:note]) if options[:note]
|
|
194
|
-
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
195
|
-
wwid.write(wwid.doing_file)
|
|
196
|
-
else
|
|
197
|
-
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|
|
119
|
+
## Add/modify commands
|
|
201
120
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
c.example 'doing
|
|
209
|
-
c.example 'doing
|
|
210
|
-
c.example 'doing
|
|
211
|
-
c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
|
|
121
|
+
# @@again @@resume
|
|
122
|
+
desc 'Repeat last entry as new entry'
|
|
123
|
+
long_desc 'This command is designed to allow multiple time intervals to be created for an entry by duplicating it with a new start (and end, eventually) time.'
|
|
124
|
+
command %i[again resume] do |c|
|
|
125
|
+
c.example 'doing resume', desc: 'Duplicate the most recent entry with a new start time, removing any @done tag'
|
|
126
|
+
c.example 'doing again', desc: 'again is an alias for resume'
|
|
127
|
+
c.example 'doing resume --editor', desc: 'Repeat the last entry, opening the new entry in the default editor'
|
|
128
|
+
c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
|
|
129
|
+
c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
|
|
212
130
|
|
|
213
|
-
c.desc '
|
|
131
|
+
c.desc 'Get last entry from a specific section'
|
|
214
132
|
c.arg_name 'NAME'
|
|
215
133
|
c.flag %i[s section], default_value: 'All'
|
|
216
134
|
|
|
217
|
-
c.desc '
|
|
218
|
-
c.
|
|
135
|
+
c.desc 'Add new entry to section (default: same section as repeated entry)'
|
|
136
|
+
c.arg_name 'SECTION_NAME'
|
|
137
|
+
c.flag [:in]
|
|
219
138
|
|
|
220
|
-
c.desc '
|
|
139
|
+
c.desc 'Repeat last entry matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
|
221
140
|
c.arg_name 'TAG'
|
|
222
141
|
c.flag [:tag]
|
|
223
142
|
|
|
224
|
-
c.desc '
|
|
143
|
+
c.desc 'Repeat last entry matching search. Surround with
|
|
144
|
+
slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
|
|
225
145
|
c.arg_name 'QUERY'
|
|
226
146
|
c.flag [:search]
|
|
227
147
|
|
|
@@ -231,34 +151,30 @@ command %i[reset begin] do |c|
|
|
|
231
151
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
232
152
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
233
153
|
|
|
234
|
-
c.desc '
|
|
154
|
+
c.desc 'Resume items that *don\'t* match search/tag filters'
|
|
235
155
|
c.switch [:not], default_value: false, negatable: false
|
|
236
156
|
|
|
237
157
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
238
158
|
c.arg_name 'TYPE'
|
|
239
159
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
240
160
|
|
|
241
|
-
c.desc 'Boolean
|
|
161
|
+
c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
|
|
242
162
|
c.arg_name 'BOOLEAN'
|
|
243
163
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
244
164
|
|
|
245
|
-
c.desc
|
|
246
|
-
c.switch %i[
|
|
165
|
+
c.desc "Edit duplicated entry with #{Doing::Util.default_editor} before adding"
|
|
166
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
247
167
|
|
|
248
|
-
c.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
raise InvalidArgument, 'Invalid date string' unless reset_date
|
|
252
|
-
else
|
|
253
|
-
reset_date = Time.now
|
|
254
|
-
end
|
|
168
|
+
c.desc 'Note'
|
|
169
|
+
c.arg_name 'TEXT'
|
|
170
|
+
c.flag %i[n note]
|
|
255
171
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
259
|
-
end
|
|
172
|
+
c.desc 'Select item to resume from a menu of matching entries'
|
|
173
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
260
174
|
|
|
261
|
-
|
|
175
|
+
c.action do |_global_options, options, _args|
|
|
176
|
+
options[:fuzzy] = false
|
|
177
|
+
tags = options[:tag].nil? ? [] : options[:tag].to_tags
|
|
262
178
|
|
|
263
179
|
options[:case] = options[:case].normalize_case
|
|
264
180
|
|
|
@@ -268,65 +184,40 @@ command %i[reset begin] do |c|
|
|
|
268
184
|
options[:search] = search
|
|
269
185
|
end
|
|
270
186
|
|
|
187
|
+
opts = options.dup
|
|
271
188
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
last_entry = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
|
|
276
|
-
menu: true,
|
|
277
|
-
header: '',
|
|
278
|
-
prompt: 'Select an entry to start/reset > ',
|
|
279
|
-
multiple: false,
|
|
280
|
-
sort: false,
|
|
281
|
-
show_if_single: true)
|
|
282
|
-
else
|
|
283
|
-
last_entry = items.reverse.last
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
unless last_entry
|
|
287
|
-
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
|
288
|
-
return
|
|
289
|
-
end
|
|
290
|
-
|
|
291
|
-
wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
|
|
292
|
-
|
|
293
|
-
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
|
189
|
+
opts[:tag] = tags
|
|
190
|
+
opts[:tag_bool] = options[:bool].normalize_bool
|
|
191
|
+
opts[:interactive] = options[:interactive]
|
|
294
192
|
|
|
295
|
-
wwid.
|
|
193
|
+
wwid.repeat_last(opts)
|
|
296
194
|
end
|
|
297
195
|
end
|
|
298
196
|
|
|
197
|
+
# @@cancel
|
|
198
|
+
desc 'End last X entries with no time tracked'
|
|
199
|
+
long_desc 'Adds @done tag without datestamp so no elapsed time is recorded. Alias for `doing finish --no-date`.'
|
|
200
|
+
arg_name 'COUNT'
|
|
201
|
+
command :cancel do |c|
|
|
202
|
+
c.example 'doing cancel', desc: 'Cancel the last entry'
|
|
203
|
+
c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
|
|
299
204
|
|
|
300
|
-
desc '
|
|
301
|
-
|
|
302
|
-
If -r is provided with no other arguments, the last note is removed.
|
|
303
|
-
If new content is specified through arguments or STDIN, any previous
|
|
304
|
-
note will be replaced with the new one.
|
|
305
|
-
|
|
306
|
-
Use -e to load the last entry in a text editor where you can append a note.
|
|
307
|
-
)
|
|
308
|
-
arg_name 'NOTE_TEXT'
|
|
309
|
-
command :note do |c|
|
|
310
|
-
c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
|
|
311
|
-
c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
|
|
312
|
-
c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
|
|
313
|
-
c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
|
|
205
|
+
c.desc 'Archive entries'
|
|
206
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
314
207
|
|
|
315
208
|
c.desc 'Section'
|
|
316
209
|
c.arg_name 'NAME'
|
|
317
|
-
c.flag %i[s section]
|
|
318
|
-
|
|
319
|
-
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
320
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
|
321
|
-
|
|
322
|
-
c.desc "Replace/Remove last entry's note (default append)"
|
|
323
|
-
c.switch %i[r remove], negatable: false, default_value: false
|
|
210
|
+
c.flag %i[s section]
|
|
324
211
|
|
|
325
|
-
c.desc '
|
|
212
|
+
c.desc 'Cancel the last X entries containing TAG. Separate multiple tags with comma (--tag=tag1,tag2). Wildcards allowed (*, ?).'
|
|
326
213
|
c.arg_name 'TAG'
|
|
327
214
|
c.flag [:tag]
|
|
328
215
|
|
|
329
|
-
c.desc '
|
|
216
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
|
217
|
+
c.arg_name 'BOOLEAN'
|
|
218
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
219
|
+
|
|
220
|
+
c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
|
330
221
|
c.arg_name 'QUERY'
|
|
331
222
|
c.flag [:search]
|
|
332
223
|
|
|
@@ -336,121 +227,154 @@ command :note do |c|
|
|
|
336
227
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
337
228
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
338
229
|
|
|
339
|
-
c.desc '
|
|
230
|
+
c.desc 'Finish items that *don\'t* match search/tag filters'
|
|
340
231
|
c.switch [:not], default_value: false, negatable: false
|
|
341
232
|
|
|
342
233
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
343
234
|
c.arg_name 'TYPE'
|
|
344
235
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
345
236
|
|
|
346
|
-
c.desc '
|
|
347
|
-
c.
|
|
348
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
237
|
+
c.desc 'Cancel last entry (or entries) not already marked @done'
|
|
238
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
349
239
|
|
|
350
|
-
c.desc 'Select item
|
|
240
|
+
c.desc 'Select item(s) to cancel from a menu of matching entries'
|
|
351
241
|
c.switch %i[i interactive], negatable: false, default_value: false
|
|
352
242
|
|
|
353
243
|
c.action do |_global_options, options, args|
|
|
354
244
|
options[:fuzzy] = false
|
|
355
245
|
if options[:section]
|
|
356
|
-
|
|
246
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
247
|
+
else
|
|
248
|
+
section = settings['current_section']
|
|
357
249
|
end
|
|
358
250
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
if options[:search]
|
|
364
|
-
search = options[:search]
|
|
365
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
366
|
-
options[:search] = search
|
|
251
|
+
if options[:tag].nil?
|
|
252
|
+
tags = []
|
|
253
|
+
else
|
|
254
|
+
tags = options[:tag].to_tags
|
|
367
255
|
end
|
|
368
256
|
|
|
257
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
|
369
258
|
|
|
370
|
-
|
|
259
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
|
371
260
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
261
|
+
if options[:interactive]
|
|
262
|
+
count = 0
|
|
263
|
+
else
|
|
264
|
+
count = args[0] ? args[0].to_i : 1
|
|
375
265
|
end
|
|
376
266
|
|
|
377
|
-
|
|
378
|
-
new_note = Doing::Note.new
|
|
379
|
-
|
|
380
|
-
if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove])
|
|
381
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
267
|
+
search = nil
|
|
382
268
|
|
|
383
|
-
|
|
269
|
+
if options[:search]
|
|
270
|
+
search = options[:search]
|
|
271
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
272
|
+
end
|
|
384
273
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
else
|
|
402
|
-
raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
if last_note.equal?(new_note)
|
|
406
|
-
Doing.logger.debug('Skipped:', 'No note change')
|
|
407
|
-
else
|
|
408
|
-
last_note.add(new_note, replace: options[:remove])
|
|
409
|
-
Doing.logger.info('Entry updated:', last_entry.title)
|
|
410
|
-
end
|
|
411
|
-
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
|
274
|
+
opts = {
|
|
275
|
+
archive: options[:archive],
|
|
276
|
+
case: options[:case].normalize_case,
|
|
277
|
+
count: count,
|
|
278
|
+
date: false,
|
|
279
|
+
fuzzy: options[:fuzzy],
|
|
280
|
+
interactive: options[:interactive],
|
|
281
|
+
not: options[:not],
|
|
282
|
+
search: search,
|
|
283
|
+
section: section,
|
|
284
|
+
sequential: false,
|
|
285
|
+
tag: tags,
|
|
286
|
+
tag_bool: options[:bool].normalize_bool,
|
|
287
|
+
tags: ['done'],
|
|
288
|
+
unfinished: options[:unfinished]
|
|
289
|
+
}
|
|
412
290
|
|
|
413
|
-
wwid.
|
|
291
|
+
wwid.tag_last(opts)
|
|
414
292
|
end
|
|
415
293
|
end
|
|
416
294
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
295
|
+
# @@done @@did
|
|
296
|
+
desc 'Add a completed item with @done(date). No argument finishes last entry.'
|
|
297
|
+
long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
|
|
298
|
+
You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
|
|
299
|
+
way to add entries in post and maintain accurate (albeit manual) time tracking.'
|
|
422
300
|
arg_name 'ENTRY'
|
|
423
|
-
command
|
|
424
|
-
c.example 'doing
|
|
425
|
-
c.example 'doing
|
|
426
|
-
c.example 'doing
|
|
427
|
-
c.example 'doing
|
|
301
|
+
command %i[done did] do |c|
|
|
302
|
+
c.example 'doing done', desc: 'Tag the last entry @done'
|
|
303
|
+
c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
|
|
304
|
+
c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
|
|
305
|
+
c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
|
|
428
306
|
|
|
429
|
-
c.desc '
|
|
430
|
-
c.
|
|
431
|
-
c.flag %i[s section]
|
|
307
|
+
c.desc 'Remove @done tag'
|
|
308
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
432
309
|
|
|
433
|
-
c.desc
|
|
434
|
-
c.switch
|
|
310
|
+
c.desc 'Include date'
|
|
311
|
+
c.switch [:date], negatable: true, default_value: true
|
|
435
312
|
|
|
436
|
-
c.desc '
|
|
313
|
+
c.desc 'Immediately archive the entry'
|
|
437
314
|
c.switch %i[a archive], negatable: false, default_value: false
|
|
438
315
|
|
|
439
|
-
c.desc
|
|
316
|
+
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
|
|
317
|
+
If used, ignores --back. Used with --took, backdates start date)
|
|
440
318
|
c.arg_name 'DATE_STRING'
|
|
441
|
-
c.flag
|
|
319
|
+
c.flag [:at]
|
|
442
320
|
|
|
443
|
-
c.desc '
|
|
321
|
+
c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
|
|
322
|
+
c.arg_name 'DATE_STRING'
|
|
323
|
+
c.flag %i[b back started]
|
|
324
|
+
|
|
325
|
+
c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
|
|
326
|
+
If used without the --back option, the start date will be moved back to allow
|
|
327
|
+
the completion date to be the current time.)
|
|
328
|
+
c.arg_name 'INTERVAL'
|
|
329
|
+
c.flag %i[t took]
|
|
330
|
+
|
|
331
|
+
c.desc 'Section'
|
|
332
|
+
c.arg_name 'NAME'
|
|
333
|
+
c.flag %i[s section]
|
|
334
|
+
|
|
335
|
+
c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
|
|
336
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
337
|
+
|
|
338
|
+
c.desc 'Include a note'
|
|
444
339
|
c.arg_name 'TEXT'
|
|
445
340
|
c.flag %i[n note]
|
|
446
341
|
|
|
342
|
+
c.desc 'Finish last entry not already marked @done'
|
|
343
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
344
|
+
|
|
345
|
+
# c.desc "Edit entry with specified app"
|
|
346
|
+
# c.arg_name 'editor_app'
|
|
347
|
+
# # c.flag [:a, :app]
|
|
348
|
+
|
|
447
349
|
c.action do |_global_options, options, args|
|
|
350
|
+
took = 0
|
|
351
|
+
donedate = nil
|
|
352
|
+
|
|
353
|
+
if options[:took]
|
|
354
|
+
took = options[:took].chronify_qty
|
|
355
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
356
|
+
end
|
|
357
|
+
|
|
448
358
|
if options[:back]
|
|
449
359
|
date = options[:back].chronify(guess: :begin)
|
|
360
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
|
361
|
+
else
|
|
362
|
+
date = options[:took] ? Time.now - took : Time.now
|
|
363
|
+
end
|
|
450
364
|
|
|
451
|
-
|
|
365
|
+
if options[:at]
|
|
366
|
+
finish_date = options[:at].chronify(guess: :begin)
|
|
367
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
368
|
+
|
|
369
|
+
date = options[:took] ? finish_date - took : finish_date
|
|
370
|
+
elsif options[:took]
|
|
371
|
+
finish_date = date + took
|
|
452
372
|
else
|
|
453
|
-
|
|
373
|
+
finish_date = Time.now
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
if options[:date]
|
|
377
|
+
donedate = finish_date.strftime('%F %R')
|
|
454
378
|
end
|
|
455
379
|
|
|
456
380
|
if options[:section]
|
|
@@ -458,217 +382,273 @@ command :meanwhile do |c|
|
|
|
458
382
|
else
|
|
459
383
|
section = settings['current_section']
|
|
460
384
|
end
|
|
461
|
-
|
|
385
|
+
|
|
386
|
+
note = Doing::Note.new
|
|
387
|
+
note.add(options[:note]) if options[:note]
|
|
462
388
|
|
|
463
389
|
if options[:editor]
|
|
464
390
|
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
465
|
-
|
|
466
|
-
|
|
391
|
+
is_new = false
|
|
392
|
+
|
|
393
|
+
if args.empty?
|
|
394
|
+
last_entry = wwid.filter_items([], opt: { unfinished: options[:unfinished], section: section, count: 1, age: :newest }).max_by { |item| item.date }
|
|
395
|
+
|
|
396
|
+
unless last_entry
|
|
397
|
+
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
|
398
|
+
raise NoResults, 'No results'
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
old_entry = last_entry.dup
|
|
402
|
+
last_entry.note.add(note)
|
|
403
|
+
input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
|
|
404
|
+
else
|
|
405
|
+
is_new = true
|
|
406
|
+
input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
|
|
407
|
+
end
|
|
408
|
+
|
|
467
409
|
input = wwid.fork_editor(input).strip
|
|
410
|
+
raise EmptyInput, 'No content' unless input && !input.empty?
|
|
411
|
+
|
|
412
|
+
d, title, note = wwid.format_input(input)
|
|
413
|
+
date = d.nil? ? date : d
|
|
414
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
415
|
+
if new_entry.should_finish?
|
|
416
|
+
if new_entry.should_time?
|
|
417
|
+
new_entry.tag('done', value: donedate)
|
|
418
|
+
else
|
|
419
|
+
new_entry.tag('done')
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
if (is_new)
|
|
424
|
+
Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
|
|
425
|
+
wwid.content.push(new_entry)
|
|
426
|
+
Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
|
|
427
|
+
else
|
|
428
|
+
wwid.content.update_item(old_entry, new_entry)
|
|
429
|
+
Doing::Hooks.trigger :post_entry_updated, wwid, new_entry unless options[:archive]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
if options[:archive]
|
|
433
|
+
wwid.move_item(new_entry, 'Archive', label: true)
|
|
434
|
+
Doing::Hooks.trigger :post_entry_updated, wwid, new_entry
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
wwid.write(wwid.doing_file)
|
|
438
|
+
elsif args.empty? && $stdin.stat.size.zero?
|
|
439
|
+
if options[:remove]
|
|
440
|
+
wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
|
441
|
+
else
|
|
442
|
+
note = options[:note] ? Doing::Note.new(options[:note]) : nil
|
|
443
|
+
opt = {
|
|
444
|
+
archive: options[:archive],
|
|
445
|
+
back: finish_date,
|
|
446
|
+
count: 1,
|
|
447
|
+
date: options[:date],
|
|
448
|
+
note: note,
|
|
449
|
+
section: section,
|
|
450
|
+
tags: ['done'],
|
|
451
|
+
took: took == 0 ? nil : took,
|
|
452
|
+
unfinished: options[:unfinished]
|
|
453
|
+
}
|
|
454
|
+
wwid.tag_last(opt)
|
|
455
|
+
end
|
|
468
456
|
elsif !args.empty?
|
|
469
|
-
|
|
457
|
+
note = Doing::Note.new(options[:note])
|
|
458
|
+
d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
|
|
459
|
+
date = d.nil? ? date : d
|
|
460
|
+
title.chomp!
|
|
461
|
+
section = 'Archive' if options[:archive]
|
|
462
|
+
new_entry = Doing::Item.new(date, title, section, new_note)
|
|
463
|
+
if new_entry.should_finish?
|
|
464
|
+
if new_entry.should_time?
|
|
465
|
+
new_entry.tag('done', value: donedate)
|
|
466
|
+
else
|
|
467
|
+
new_entry.tag('done')
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
|
|
471
|
+
wwid.content.push(new_entry)
|
|
472
|
+
Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
|
|
473
|
+
wwid.write(wwid.doing_file)
|
|
474
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
|
470
475
|
elsif $stdin.stat.size.positive?
|
|
471
|
-
|
|
472
|
-
end
|
|
473
|
-
|
|
474
|
-
if input && !input.empty?
|
|
475
|
-
d, input, note = wwid.format_input(input)
|
|
476
|
+
d, title, note = wwid.format_input($stdin.read.strip)
|
|
476
477
|
unless d.nil?
|
|
477
478
|
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
478
479
|
date = d
|
|
479
480
|
end
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
end
|
|
481
|
+
note.add(options[:note]) if options[:note]
|
|
482
|
+
section = options[:archive] ? 'Archive' : section
|
|
483
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
484
484
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
485
|
+
if new_entry.should_finish?
|
|
486
|
+
if new_entry.should_time?
|
|
487
|
+
new_entry.tag('done', value: donedate)
|
|
488
|
+
else
|
|
489
|
+
new_entry.tag('done')
|
|
490
|
+
end
|
|
491
|
+
end
|
|
490
492
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
+
Doing::Hooks.trigger :pre_entry_add, wwid, new_entry
|
|
494
|
+
wwid.content.push(new_entry)
|
|
495
|
+
Doing::Hooks.trigger :post_entry_added, wwid, new_entry.dup
|
|
496
|
+
|
|
497
|
+
wwid.write(wwid.doing_file)
|
|
498
|
+
Doing.logger.info('Entry Added:', new_entry.title)
|
|
499
|
+
else
|
|
500
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
501
|
+
end
|
|
493
502
|
end
|
|
494
503
|
end
|
|
495
504
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
c.example 'doing
|
|
504
|
-
|
|
505
|
-
c.desc 'List all available templates'
|
|
506
|
-
c.switch %i[l list], negatable: false
|
|
507
|
-
|
|
508
|
-
c.desc 'List in single column for completion'
|
|
509
|
-
c.switch %i[c column]
|
|
510
|
-
|
|
511
|
-
c.desc 'Save template to file instead of STDOUT'
|
|
512
|
-
c.switch %i[s save], default_value: false, negatable: false
|
|
513
|
-
|
|
514
|
-
c.desc 'Save template to alternate location'
|
|
515
|
-
c.arg_name 'DIRECTORY'
|
|
516
|
-
c.flag %i[p path], default_value: File.join(Doing::Util.user_home, '.config', 'doing', 'templates')
|
|
505
|
+
# @@finish
|
|
506
|
+
desc 'Mark last X entries as @done'
|
|
507
|
+
long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
|
|
508
|
+
arg_name 'COUNT'
|
|
509
|
+
command :finish do |c|
|
|
510
|
+
c.example 'doing finish', desc: 'Mark the last entry @done'
|
|
511
|
+
c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
|
|
512
|
+
c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
|
|
517
513
|
|
|
518
|
-
c.
|
|
519
|
-
|
|
520
|
-
if options[:column]
|
|
521
|
-
$stdout.print Doing::Plugins.plugin_templates.join("\n")
|
|
522
|
-
else
|
|
523
|
-
$stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
|
|
524
|
-
end
|
|
525
|
-
return
|
|
526
|
-
end
|
|
514
|
+
c.desc 'Include date'
|
|
515
|
+
c.switch [:date], negatable: true, default_value: true
|
|
527
516
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
options[:save] = Doing::Prompt.yn("Save to #{options[:path]}? (No outputs to STDOUT)", default_response: false)
|
|
532
|
-
else
|
|
533
|
-
type = args[0]
|
|
534
|
-
end
|
|
517
|
+
c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
|
|
518
|
+
c.arg_name 'DATE_STRING'
|
|
519
|
+
c.flag %i[b back]
|
|
535
520
|
|
|
536
|
-
|
|
521
|
+
c.desc 'Set the completed date to the start date plus XX[hmd]'
|
|
522
|
+
c.arg_name 'INTERVAL'
|
|
523
|
+
c.flag %i[t took]
|
|
537
524
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
$stdout.puts Doing::Plugins.template_for_trigger(type, save_to: nil)
|
|
542
|
-
end
|
|
525
|
+
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
|
|
526
|
+
c.arg_name 'DATE_STRING'
|
|
527
|
+
c.flag [:at]
|
|
543
528
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
# $stdout.puts wwid.css_template
|
|
549
|
-
# when /markdown|md|erb/i
|
|
550
|
-
# $stdout.puts wwid.markdown_template
|
|
551
|
-
# else
|
|
552
|
-
# exit_now! 'Invalid type specified, must be HAML or CSS'
|
|
553
|
-
# end
|
|
554
|
-
end
|
|
555
|
-
end
|
|
529
|
+
c.desc 'Finish the last X entries containing TAG.
|
|
530
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
531
|
+
c.arg_name 'TAG'
|
|
532
|
+
c.flag [:tag]
|
|
556
533
|
|
|
557
|
-
desc '
|
|
558
|
-
|
|
534
|
+
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")'
|
|
535
|
+
c.arg_name 'QUERY'
|
|
536
|
+
c.flag [:search]
|
|
559
537
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
selected entries.
|
|
538
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
539
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
563
540
|
|
|
564
|
-
|
|
541
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
542
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
565
543
|
|
|
566
|
-
|
|
544
|
+
c.desc 'Finish items that *don\'t* match search/tag filters'
|
|
545
|
+
c.switch [:not], default_value: false, negatable: false
|
|
567
546
|
|
|
568
|
-
|
|
547
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
548
|
+
c.arg_name 'TYPE'
|
|
549
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
569
550
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
c.
|
|
573
|
-
c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
|
|
574
|
-
c.example 'doing select --after "yesterday 12pm" --tag project1', desc: 'Display a menu of entries created after noon yesterday, add @project1 to selected entries'
|
|
575
|
-
c.desc 'Select from a specific section'
|
|
576
|
-
c.arg_name 'SECTION'
|
|
577
|
-
c.flag %i[s section]
|
|
551
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
|
552
|
+
c.arg_name 'BOOLEAN'
|
|
553
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
578
554
|
|
|
579
|
-
c.desc '
|
|
580
|
-
c.
|
|
581
|
-
c.flag %i[t tag]
|
|
555
|
+
c.desc 'Remove done tag'
|
|
556
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
582
557
|
|
|
583
|
-
c.desc '
|
|
584
|
-
c.switch %i[
|
|
558
|
+
c.desc 'Finish last entry (or entries) not already marked @done'
|
|
559
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
585
560
|
|
|
586
|
-
|
|
587
|
-
|
|
561
|
+
c.desc %(Auto-generate finish dates from next entry's start time.
|
|
562
|
+
Automatically generate completion dates 1 minute before next item (in any section) began.
|
|
563
|
+
--auto overrides the --date and --back parameters.)
|
|
564
|
+
c.switch [:auto], negatable: false, default_value: false
|
|
588
565
|
|
|
589
|
-
c.desc 'Archive
|
|
566
|
+
c.desc 'Archive entries'
|
|
590
567
|
c.switch %i[a archive], negatable: false, default_value: false
|
|
591
568
|
|
|
592
|
-
c.desc '
|
|
593
|
-
c.arg_name '
|
|
594
|
-
c.flag %i[
|
|
595
|
-
|
|
596
|
-
c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
|
|
597
|
-
c.arg_name 'QUERY'
|
|
598
|
-
c.flag %i[q query search]
|
|
599
|
-
|
|
600
|
-
c.desc 'Select from entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
601
|
-
c.arg_name 'DATE_STRING'
|
|
602
|
-
c.flag [:before]
|
|
603
|
-
|
|
604
|
-
c.desc 'Select from entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
605
|
-
c.arg_name 'DATE_STRING'
|
|
606
|
-
c.flag [:after]
|
|
607
|
-
|
|
608
|
-
c.desc %(
|
|
609
|
-
Date range to show, or a single day to filter date on.
|
|
610
|
-
Date range argument should be quoted. Date specifications can be natural language.
|
|
611
|
-
To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
|
|
612
|
-
|
|
613
|
-
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
|
614
|
-
by time of day.
|
|
615
|
-
)
|
|
616
|
-
c.arg_name 'DATE_OR_RANGE'
|
|
617
|
-
c.flag [:from]
|
|
618
|
-
|
|
619
|
-
c.desc 'Force exact search string matching (case sensitive)'
|
|
620
|
-
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
569
|
+
c.desc 'Section'
|
|
570
|
+
c.arg_name 'NAME'
|
|
571
|
+
c.flag %i[s section]
|
|
621
572
|
|
|
622
|
-
c.desc 'Select
|
|
623
|
-
c.switch [
|
|
573
|
+
c.desc 'Select item(s) to finish from a menu of matching entries'
|
|
574
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
624
575
|
|
|
625
|
-
c.
|
|
626
|
-
|
|
627
|
-
|
|
576
|
+
c.action do |_global_options, options, args|
|
|
577
|
+
options[:fuzzy] = false
|
|
578
|
+
unless options[:auto]
|
|
579
|
+
if options[:took]
|
|
580
|
+
took = options[:took].chronify_qty
|
|
581
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
582
|
+
end
|
|
628
583
|
|
|
629
|
-
|
|
630
|
-
c.switch %i[menu], negatable: true, default_value: true
|
|
584
|
+
raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
|
631
585
|
|
|
632
|
-
|
|
633
|
-
c.switch %i[c cancel], negatable: false, default_value: false
|
|
586
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
634
587
|
|
|
635
|
-
|
|
636
|
-
|
|
588
|
+
if options[:at]
|
|
589
|
+
finish_date = options[:at].chronify(guess: :begin)
|
|
590
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
637
591
|
|
|
638
|
-
|
|
639
|
-
|
|
592
|
+
date = options[:took] ? finish_date - took : finish_date
|
|
593
|
+
elsif options[:back]
|
|
594
|
+
date = options[:back].chronify()
|
|
640
595
|
|
|
641
|
-
|
|
642
|
-
|
|
596
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
|
597
|
+
else
|
|
598
|
+
date = Time.now
|
|
599
|
+
end
|
|
600
|
+
end
|
|
643
601
|
|
|
644
|
-
|
|
645
|
-
c.switch %i[flag], negatable: false, default_value: false
|
|
602
|
+
options[:took] = options[:took].chronify_qty if options[:took]
|
|
646
603
|
|
|
647
|
-
|
|
648
|
-
|
|
604
|
+
if options[:tag].nil?
|
|
605
|
+
tags = []
|
|
606
|
+
else
|
|
607
|
+
tags = options[:tag].to_tags
|
|
608
|
+
end
|
|
649
609
|
|
|
650
|
-
|
|
651
|
-
c.arg_name 'FILE'
|
|
652
|
-
c.flag %i[save_to]
|
|
610
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
|
653
611
|
|
|
654
|
-
|
|
655
|
-
c.arg_name 'FORMAT'
|
|
656
|
-
c.flag %i[o output]
|
|
612
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
|
657
613
|
|
|
658
|
-
|
|
659
|
-
|
|
614
|
+
if options[:interactive]
|
|
615
|
+
count = 0
|
|
616
|
+
else
|
|
617
|
+
count = args[0] ? args[0].to_i : 1
|
|
618
|
+
end
|
|
660
619
|
|
|
661
|
-
|
|
662
|
-
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
620
|
+
search = nil
|
|
663
621
|
|
|
664
|
-
|
|
622
|
+
if options[:search]
|
|
623
|
+
search = options[:search]
|
|
624
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
625
|
+
end
|
|
665
626
|
|
|
666
|
-
|
|
627
|
+
opts = {
|
|
628
|
+
archive: options[:archive],
|
|
629
|
+
back: date,
|
|
630
|
+
case: options[:case].normalize_case,
|
|
631
|
+
count: count,
|
|
632
|
+
date: options[:date],
|
|
633
|
+
fuzzy: options[:fuzzy],
|
|
634
|
+
interactive: options[:interactive],
|
|
635
|
+
not: options[:not],
|
|
636
|
+
remove: options[:remove],
|
|
637
|
+
search: search,
|
|
638
|
+
section: options[:section],
|
|
639
|
+
sequential: options[:auto],
|
|
640
|
+
tag: tags,
|
|
641
|
+
tag_bool: options[:bool].normalize_bool,
|
|
642
|
+
tags: ['done'],
|
|
643
|
+
took: options[:took],
|
|
644
|
+
unfinished: options[:unfinished]
|
|
645
|
+
}
|
|
667
646
|
|
|
668
|
-
wwid.
|
|
647
|
+
wwid.tag_last(opts)
|
|
669
648
|
end
|
|
670
649
|
end
|
|
671
650
|
|
|
651
|
+
# @@later
|
|
672
652
|
desc 'Add an item to the Later section'
|
|
673
653
|
arg_name 'ENTRY'
|
|
674
654
|
command :later do |c|
|
|
@@ -728,229 +708,40 @@ command :later do |c|
|
|
|
728
708
|
end
|
|
729
709
|
end
|
|
730
710
|
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
c.example 'doing
|
|
738
|
-
c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
|
|
739
|
-
c.example 'doing done --back 30m This took me half an hour', desc: 'Add an entry with a start date 30 minutes ago and a @done date of right now'
|
|
740
|
-
c.example 'doing done --at 3pm --took 1h Started and finished this afternoon', desc: 'Add an entry with a @done date of 3pm and a start date of 2pm (3pm - 1h)'
|
|
741
|
-
|
|
742
|
-
c.desc 'Remove @done tag'
|
|
743
|
-
c.switch %i[r remove], negatable: false, default_value: false
|
|
744
|
-
|
|
745
|
-
c.desc 'Include date'
|
|
746
|
-
c.switch [:date], negatable: true, default_value: true
|
|
747
|
-
|
|
748
|
-
c.desc 'Immediately archive the entry'
|
|
749
|
-
c.switch %i[a archive], negatable: false, default_value: false
|
|
750
|
-
|
|
751
|
-
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
|
|
752
|
-
If used, ignores --back. Used with --took, backdates start date)
|
|
753
|
-
c.arg_name 'DATE_STRING'
|
|
754
|
-
c.flag [:at]
|
|
755
|
-
|
|
756
|
-
c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
|
|
757
|
-
c.arg_name 'DATE_STRING'
|
|
758
|
-
c.flag %i[b back started]
|
|
759
|
-
|
|
760
|
-
c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
|
|
761
|
-
If used without the --back option, the start date will be moved back to allow
|
|
762
|
-
the completion date to be the current time.)
|
|
763
|
-
c.arg_name 'INTERVAL'
|
|
764
|
-
c.flag %i[t took]
|
|
711
|
+
# @@mark @@flag
|
|
712
|
+
desc 'Mark last entry as flagged'
|
|
713
|
+
command %i[mark flag] do |c|
|
|
714
|
+
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
|
715
|
+
c.example 'doing mark', desc: 'mark is an alias for flag'
|
|
716
|
+
c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
|
|
717
|
+
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'
|
|
765
718
|
|
|
766
719
|
c.desc 'Section'
|
|
767
|
-
c.arg_name '
|
|
768
|
-
c.flag %i[s section]
|
|
769
|
-
|
|
770
|
-
c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
|
|
771
|
-
c.switch %i[e editor], negatable: false, default_value: false
|
|
772
|
-
|
|
773
|
-
c.desc 'Include a note'
|
|
774
|
-
c.arg_name 'TEXT'
|
|
775
|
-
c.flag %i[n note]
|
|
776
|
-
|
|
777
|
-
c.desc 'Finish last entry not already marked @done'
|
|
778
|
-
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
779
|
-
|
|
780
|
-
# c.desc "Edit entry with specified app"
|
|
781
|
-
# c.arg_name 'editor_app'
|
|
782
|
-
# # c.flag [:a, :app]
|
|
783
|
-
|
|
784
|
-
c.action do |_global_options, options, args|
|
|
785
|
-
took = 0
|
|
786
|
-
donedate = nil
|
|
787
|
-
|
|
788
|
-
if options[:took]
|
|
789
|
-
took = options[:took].chronify_qty
|
|
790
|
-
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
791
|
-
end
|
|
792
|
-
|
|
793
|
-
if options[:back]
|
|
794
|
-
date = options[:back].chronify(guess: :begin)
|
|
795
|
-
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
|
796
|
-
else
|
|
797
|
-
date = options[:took] ? Time.now - took : Time.now
|
|
798
|
-
end
|
|
799
|
-
|
|
800
|
-
if options[:at]
|
|
801
|
-
finish_date = options[:at].chronify(guess: :begin)
|
|
802
|
-
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
803
|
-
|
|
804
|
-
date = options[:took] ? finish_date - took : finish_date
|
|
805
|
-
elsif options[:took]
|
|
806
|
-
finish_date = date + took
|
|
807
|
-
else
|
|
808
|
-
finish_date = Time.now
|
|
809
|
-
end
|
|
810
|
-
|
|
811
|
-
if options[:date]
|
|
812
|
-
donedate = finish_date.strftime('%F %R')
|
|
813
|
-
end
|
|
814
|
-
|
|
815
|
-
if options[:section]
|
|
816
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
817
|
-
else
|
|
818
|
-
section = settings['current_section']
|
|
819
|
-
end
|
|
820
|
-
|
|
821
|
-
note = Doing::Note.new
|
|
822
|
-
note.add(options[:note]) if options[:note]
|
|
823
|
-
|
|
824
|
-
if options[:editor]
|
|
825
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
826
|
-
is_new = false
|
|
827
|
-
|
|
828
|
-
if args.empty?
|
|
829
|
-
last_entry = wwid.filter_items([], opt: {unfinished: options[:unfinished], section: section, count: 1, age: 'new'}).max_by { |item| item.date }
|
|
830
|
-
|
|
831
|
-
unless last_entry
|
|
832
|
-
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
|
833
|
-
raise NoResults, 'No results'
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
old_entry = last_entry.dup
|
|
837
|
-
last_entry.note.add(note)
|
|
838
|
-
input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
|
|
839
|
-
else
|
|
840
|
-
is_new = true
|
|
841
|
-
input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
|
|
842
|
-
end
|
|
843
|
-
|
|
844
|
-
input = wwid.fork_editor(input).strip
|
|
845
|
-
raise EmptyInput, 'No content' unless input && !input.empty?
|
|
846
|
-
|
|
847
|
-
d, title, note = wwid.format_input(input)
|
|
848
|
-
date = d.nil? ? date : d
|
|
849
|
-
new_entry = Doing::Item.new(date, title, section, note)
|
|
850
|
-
if new_entry.should_finish?
|
|
851
|
-
if new_entry.should_time?
|
|
852
|
-
new_entry.tag('done', value: donedate)
|
|
853
|
-
else
|
|
854
|
-
new_entry.tag('done')
|
|
855
|
-
end
|
|
856
|
-
end
|
|
857
|
-
|
|
858
|
-
if (is_new)
|
|
859
|
-
wwid.content.push(new_entry)
|
|
860
|
-
else
|
|
861
|
-
wwid.content.update_item(old_entry, new_entry)
|
|
862
|
-
end
|
|
863
|
-
|
|
864
|
-
if options[:archive]
|
|
865
|
-
wwid.move_item(new_entry, 'Archive', label: true)
|
|
866
|
-
end
|
|
867
|
-
|
|
868
|
-
wwid.write(wwid.doing_file)
|
|
869
|
-
elsif args.empty? && $stdin.stat.size.zero?
|
|
870
|
-
if options[:remove]
|
|
871
|
-
wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
|
872
|
-
else
|
|
873
|
-
note = options[:note] ? Doing::Note.new(options[:note]) : nil
|
|
874
|
-
opt = {
|
|
875
|
-
archive: options[:archive],
|
|
876
|
-
back: finish_date,
|
|
877
|
-
count: 1,
|
|
878
|
-
date: options[:date],
|
|
879
|
-
note: note,
|
|
880
|
-
section: section,
|
|
881
|
-
tags: ['done'],
|
|
882
|
-
took: took == 0 ? nil : took,
|
|
883
|
-
unfinished: options[:unfinished]
|
|
884
|
-
}
|
|
885
|
-
wwid.tag_last(opt)
|
|
886
|
-
end
|
|
887
|
-
elsif !args.empty?
|
|
888
|
-
note = Doing::Note.new(options[:note])
|
|
889
|
-
d, title, new_note = wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
|
|
890
|
-
date = d.nil? ? date : d
|
|
891
|
-
title.chomp!
|
|
892
|
-
section = 'Archive' if options[:archive]
|
|
893
|
-
new_entry = Doing::Item.new(date, title, section, new_note)
|
|
894
|
-
if new_entry.should_finish?
|
|
895
|
-
if new_entry.should_time?
|
|
896
|
-
new_entry.tag('done', value: donedate)
|
|
897
|
-
else
|
|
898
|
-
new_entry.tag('done')
|
|
899
|
-
end
|
|
900
|
-
end
|
|
901
|
-
wwid.content.push(new_entry)
|
|
902
|
-
wwid.write(wwid.doing_file)
|
|
903
|
-
Doing.logger.info('Entry Added:', new_entry.title)
|
|
904
|
-
elsif $stdin.stat.size.positive?
|
|
905
|
-
d, title, note = wwid.format_input($stdin.read.strip)
|
|
906
|
-
unless d.nil?
|
|
907
|
-
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
908
|
-
date = d
|
|
909
|
-
end
|
|
910
|
-
note.add(options[:note]) if options[:note]
|
|
911
|
-
section = options[:archive] ? 'Archive' : section
|
|
912
|
-
new_entry = Doing::Item.new(date, title, section, note)
|
|
720
|
+
c.arg_name 'SECTION_NAME'
|
|
721
|
+
c.flag %i[s section], default_value: 'All'
|
|
913
722
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
else
|
|
918
|
-
new_entry.tag('done')
|
|
919
|
-
end
|
|
920
|
-
end
|
|
723
|
+
c.desc 'How many recent entries to tag (0 for all)'
|
|
724
|
+
c.arg_name 'COUNT'
|
|
725
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
|
921
726
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
Doing.logger.info('Entry Added:', new_entry.title)
|
|
925
|
-
else
|
|
926
|
-
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
927
|
-
end
|
|
928
|
-
end
|
|
929
|
-
end
|
|
727
|
+
c.desc 'Don\'t ask permission to flag all entries when count is 0'
|
|
728
|
+
c.switch %i[force], negatable: false, default_value: false
|
|
930
729
|
|
|
931
|
-
desc '
|
|
932
|
-
|
|
933
|
-
arg_name 'COUNT'
|
|
934
|
-
command :cancel do |c|
|
|
935
|
-
c.example 'doing cancel', desc: 'Cancel the last entry'
|
|
936
|
-
c.example 'doing cancel --tag project1 -u 5', desc: 'Cancel the last 5 unfinished entries containing @project1'
|
|
730
|
+
c.desc 'Include current date/time with tag'
|
|
731
|
+
c.switch %i[d date], negatable: false, default_value: false
|
|
937
732
|
|
|
938
|
-
c.desc '
|
|
939
|
-
c.switch %i[
|
|
733
|
+
c.desc 'Remove flag'
|
|
734
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
940
735
|
|
|
941
|
-
c.desc '
|
|
942
|
-
c.
|
|
943
|
-
c.flag %i[s section]
|
|
736
|
+
c.desc 'Flag last entry (or entries) not marked @done'
|
|
737
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
944
738
|
|
|
945
|
-
c.desc '
|
|
739
|
+
c.desc 'Flag the last entry containing TAG.
|
|
740
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
946
741
|
c.arg_name 'TAG'
|
|
947
742
|
c.flag [:tag]
|
|
948
743
|
|
|
949
|
-
c.desc '
|
|
950
|
-
c.arg_name 'BOOLEAN'
|
|
951
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
952
|
-
|
|
953
|
-
c.desc 'Cancel the last X entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
|
744
|
+
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")'
|
|
954
745
|
c.arg_name 'QUERY'
|
|
955
746
|
c.flag [:search]
|
|
956
747
|
|
|
@@ -960,239 +751,197 @@ command :cancel do |c|
|
|
|
960
751
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
961
752
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
962
753
|
|
|
963
|
-
c.desc '
|
|
754
|
+
c.desc 'Flag items that *don\'t* match search/tag/date filters'
|
|
964
755
|
c.switch [:not], default_value: false, negatable: false
|
|
965
756
|
|
|
966
757
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
967
758
|
c.arg_name 'TYPE'
|
|
968
759
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
969
760
|
|
|
970
|
-
c.desc '
|
|
971
|
-
c.
|
|
761
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
|
762
|
+
c.arg_name 'BOOLEAN'
|
|
763
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
972
764
|
|
|
973
|
-
c.desc 'Select item(s) to
|
|
765
|
+
c.desc 'Select item(s) to flag from a menu of matching entries'
|
|
974
766
|
c.switch %i[i interactive], negatable: false, default_value: false
|
|
975
767
|
|
|
976
|
-
c.action do |_global_options, options,
|
|
768
|
+
c.action do |_global_options, options, _args|
|
|
977
769
|
options[:fuzzy] = false
|
|
770
|
+
mark = settings['marker_tag'] || 'flagged'
|
|
771
|
+
|
|
772
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
773
|
+
|
|
774
|
+
section = 'All'
|
|
775
|
+
|
|
978
776
|
if options[:section]
|
|
979
777
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
980
|
-
else
|
|
981
|
-
section = settings['current_section']
|
|
982
778
|
end
|
|
983
779
|
|
|
984
780
|
if options[:tag].nil?
|
|
985
|
-
|
|
781
|
+
search_tags = []
|
|
986
782
|
else
|
|
987
|
-
|
|
783
|
+
search_tags = options[:tag].to_tags
|
|
988
784
|
end
|
|
989
785
|
|
|
990
|
-
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
|
991
|
-
|
|
992
|
-
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.empty? || args[0] =~ /\d+/
|
|
993
|
-
|
|
994
786
|
if options[:interactive]
|
|
995
787
|
count = 0
|
|
788
|
+
options[:force] = true
|
|
996
789
|
else
|
|
997
|
-
count =
|
|
790
|
+
count = options[:count].to_i
|
|
998
791
|
end
|
|
999
792
|
|
|
1000
|
-
|
|
793
|
+
options[:case] = options[:case].normalize_case
|
|
1001
794
|
|
|
1002
795
|
if options[:search]
|
|
1003
796
|
search = options[:search]
|
|
1004
797
|
search.sub!(/^'?/, "'") if options[:exact]
|
|
798
|
+
options[:search] = search
|
|
1005
799
|
end
|
|
1006
800
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
sequential: false,
|
|
1018
|
-
tag: tags,
|
|
1019
|
-
tag_bool: options[:bool].normalize_bool,
|
|
1020
|
-
tags: ['done'],
|
|
1021
|
-
unfinished: options[:unfinished]
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
wwid.tag_last(opts)
|
|
1025
|
-
end
|
|
1026
|
-
end
|
|
1027
|
-
|
|
1028
|
-
desc 'Mark last X entries as @done'
|
|
1029
|
-
long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
|
|
1030
|
-
arg_name 'COUNT'
|
|
1031
|
-
command :finish do |c|
|
|
1032
|
-
c.example 'doing finish', desc: 'Mark the last entry @done'
|
|
1033
|
-
c.example 'doing finish --auto --section Later 10', desc: 'Add @done to any unfinished entries in the last 10 in Later, setting the finish time based on the start time of the task after it'
|
|
1034
|
-
c.example 'doing finish --search "a specific entry" --at "yesterday 3pm"', desc: 'Search for an entry containing string and set its @done time to yesterday at 3pm'
|
|
1035
|
-
|
|
1036
|
-
c.desc 'Include date'
|
|
1037
|
-
c.switch [:date], negatable: true, default_value: true
|
|
1038
|
-
|
|
1039
|
-
c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
|
|
1040
|
-
c.arg_name 'DATE_STRING'
|
|
1041
|
-
c.flag %i[b back]
|
|
1042
|
-
|
|
1043
|
-
c.desc 'Set the completed date to the start date plus XX[hmd]'
|
|
1044
|
-
c.arg_name 'INTERVAL'
|
|
1045
|
-
c.flag %i[t took]
|
|
1046
|
-
|
|
1047
|
-
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
|
|
1048
|
-
c.arg_name 'DATE_STRING'
|
|
1049
|
-
c.flag [:at]
|
|
1050
|
-
|
|
1051
|
-
c.desc 'Finish the last X entries containing TAG.
|
|
1052
|
-
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
1053
|
-
c.arg_name 'TAG'
|
|
1054
|
-
c.flag [:tag]
|
|
1055
|
-
|
|
1056
|
-
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")'
|
|
1057
|
-
c.arg_name 'QUERY'
|
|
1058
|
-
c.flag [:search]
|
|
1059
|
-
|
|
1060
|
-
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
1061
|
-
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
1062
|
-
|
|
1063
|
-
c.desc 'Force exact search string matching (case sensitive)'
|
|
1064
|
-
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
801
|
+
if count.zero? && !options[:force]
|
|
802
|
+
if options[:search]
|
|
803
|
+
section_q = ' matching your search terms'
|
|
804
|
+
elsif options[:tag]
|
|
805
|
+
section_q = ' matching your tag search'
|
|
806
|
+
elsif section == 'All'
|
|
807
|
+
section_q = ''
|
|
808
|
+
else
|
|
809
|
+
section_q = " in section #{section}"
|
|
810
|
+
end
|
|
1065
811
|
|
|
1066
|
-
c.desc 'Finish items that *don\'t* match search/tag filters'
|
|
1067
|
-
c.switch [:not], default_value: false, negatable: false
|
|
1068
812
|
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
813
|
+
question = if options[:remove]
|
|
814
|
+
"Are you sure you want to unflag all entries#{section_q}"
|
|
815
|
+
else
|
|
816
|
+
"Are you sure you want to flag all records#{section_q}"
|
|
817
|
+
end
|
|
1072
818
|
|
|
1073
|
-
|
|
1074
|
-
c.arg_name 'BOOLEAN'
|
|
1075
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
819
|
+
res = Doing::Prompt.yn(question, default_response: false)
|
|
1076
820
|
|
|
1077
|
-
|
|
1078
|
-
|
|
821
|
+
exit_now! 'Cancelled' unless res
|
|
822
|
+
end
|
|
1079
823
|
|
|
1080
|
-
|
|
1081
|
-
|
|
824
|
+
options[:count] = count
|
|
825
|
+
options[:section] = section
|
|
826
|
+
options[:tag] = search_tags
|
|
827
|
+
options[:tags] = [mark]
|
|
828
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
|
1082
829
|
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
c.switch [:auto], negatable: false, default_value: false
|
|
830
|
+
wwid.tag_last(options)
|
|
831
|
+
end
|
|
832
|
+
end
|
|
1087
833
|
|
|
1088
|
-
|
|
1089
|
-
|
|
834
|
+
# @@meanwhile
|
|
835
|
+
desc 'Finish any running @meanwhile tasks and optionally create a new one'
|
|
836
|
+
long_desc 'The @meanwhile tag allows you to have long-running entries that encompass smaller entries.
|
|
837
|
+
This command makes it easy to start and stop these overarching entries. Just run `doing meanwhile Starting work on this
|
|
838
|
+
big project` to start a @meanwhile entry, add other entries as you work on the project, then use `doing meanwhile` by
|
|
839
|
+
itself to mark the entry as @done.'
|
|
840
|
+
arg_name 'ENTRY'
|
|
841
|
+
command :meanwhile do |c|
|
|
842
|
+
c.example 'doing meanwhile "Long task that will have others after it before it\'s done"', desc: 'Add a new long-running entry, completing any current @meanwhile entry'
|
|
843
|
+
c.example 'doing meanwhile', desc: 'Finish any open @meanwhile entry'
|
|
844
|
+
c.example 'doing meanwhile --archive', desc: 'Finish any open @meanwhile entry and archive it'
|
|
845
|
+
c.example 'doing meanwhile --back 2h "Something I\'ve been working on for a while', desc: 'Add a @meanwhile entry with a start date 2 hours ago'
|
|
1090
846
|
|
|
1091
847
|
c.desc 'Section'
|
|
1092
848
|
c.arg_name 'NAME'
|
|
1093
849
|
c.flag %i[s section]
|
|
1094
850
|
|
|
1095
|
-
c.desc
|
|
1096
|
-
c.switch %i[
|
|
1097
|
-
|
|
1098
|
-
c.action do |_global_options, options, args|
|
|
1099
|
-
options[:fuzzy] = false
|
|
1100
|
-
unless options[:auto]
|
|
1101
|
-
if options[:took]
|
|
1102
|
-
took = options[:took].chronify_qty
|
|
1103
|
-
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
1104
|
-
end
|
|
851
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
852
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1105
853
|
|
|
1106
|
-
|
|
854
|
+
c.desc 'Archive previous @meanwhile entry'
|
|
855
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
1107
856
|
|
|
1108
|
-
|
|
857
|
+
c.desc 'Backdate start date for new entry to date string [4pm|20m|2h|yesterday noon]'
|
|
858
|
+
c.arg_name 'DATE_STRING'
|
|
859
|
+
c.flag %i[b back]
|
|
1109
860
|
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
861
|
+
c.desc 'Note'
|
|
862
|
+
c.arg_name 'TEXT'
|
|
863
|
+
c.flag %i[n note]
|
|
1113
864
|
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
865
|
+
c.action do |_global_options, options, args|
|
|
866
|
+
if options[:back]
|
|
867
|
+
date = options[:back].chronify(guess: :begin)
|
|
1117
868
|
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
end
|
|
869
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
|
870
|
+
else
|
|
871
|
+
date = Time.now
|
|
1122
872
|
end
|
|
1123
873
|
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
if options[:tag].nil?
|
|
1127
|
-
tags = []
|
|
874
|
+
if options[:section]
|
|
875
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
1128
876
|
else
|
|
1129
|
-
|
|
877
|
+
section = settings['current_section']
|
|
1130
878
|
end
|
|
879
|
+
input = ''
|
|
1131
880
|
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
881
|
+
if options[:editor]
|
|
882
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
883
|
+
input += date.strftime('%F %R | ')
|
|
884
|
+
input += args.join(' ') unless args.empty?
|
|
885
|
+
input = wwid.fork_editor(input).strip
|
|
886
|
+
elsif !args.empty?
|
|
887
|
+
input = args.join(' ')
|
|
888
|
+
elsif $stdin.stat.size.positive?
|
|
889
|
+
input = $stdin.read.strip
|
|
890
|
+
end
|
|
1135
891
|
|
|
1136
|
-
if
|
|
1137
|
-
|
|
892
|
+
if input && !input.empty?
|
|
893
|
+
d, input, note = wwid.format_input(input)
|
|
894
|
+
unless d.nil?
|
|
895
|
+
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
896
|
+
date = d
|
|
897
|
+
end
|
|
1138
898
|
else
|
|
1139
|
-
|
|
899
|
+
input = nil
|
|
900
|
+
note = []
|
|
1140
901
|
end
|
|
1141
902
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
903
|
+
if options[:note]
|
|
904
|
+
note.push(options[:note])
|
|
905
|
+
elsif note.empty?
|
|
906
|
+
note = nil
|
|
1147
907
|
end
|
|
1148
908
|
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
back: date,
|
|
1152
|
-
case: options[:case].normalize_case,
|
|
1153
|
-
count: count,
|
|
1154
|
-
date: options[:date],
|
|
1155
|
-
fuzzy: options[:fuzzy],
|
|
1156
|
-
interactive: options[:interactive],
|
|
1157
|
-
not: options[:not],
|
|
1158
|
-
remove: options[:remove],
|
|
1159
|
-
search: search,
|
|
1160
|
-
section: options[:section],
|
|
1161
|
-
sequential: options[:auto],
|
|
1162
|
-
tag: tags,
|
|
1163
|
-
tag_bool: options[:bool].normalize_bool,
|
|
1164
|
-
tags: ['done'],
|
|
1165
|
-
took: options[:took],
|
|
1166
|
-
unfinished: options[:unfinished]
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
wwid.tag_last(opts)
|
|
909
|
+
wwid.stop_start('meanwhile', { new_item: input, back: date, section: section, archive: options[:archive], note: note })
|
|
910
|
+
wwid.write(wwid.doing_file)
|
|
1170
911
|
end
|
|
1171
912
|
end
|
|
1172
913
|
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
c.example 'doing resume --tag project1 --in Projects', desc: 'Repeat the last entry tagged @project1, creating the new entry in the Projects section'
|
|
1180
|
-
c.example 'doing resume --interactive', desc: 'Select the entry to repeat from a menu'
|
|
914
|
+
# @@note
|
|
915
|
+
desc 'Add a note to the last entry'
|
|
916
|
+
long_desc %(
|
|
917
|
+
If -r is provided with no other arguments, the last note is removed.
|
|
918
|
+
If new content is specified through arguments or STDIN, any previous
|
|
919
|
+
note will be replaced with the new one.
|
|
1181
920
|
|
|
1182
|
-
|
|
921
|
+
Use -e to load the last entry in a text editor where you can append a note.
|
|
922
|
+
)
|
|
923
|
+
arg_name 'NOTE_TEXT'
|
|
924
|
+
command :note do |c|
|
|
925
|
+
c.example 'doing note', desc: 'Open the last entry in $EDITOR to append a note'
|
|
926
|
+
c.example 'doing note "Just a quick annotation"', desc: 'Add a quick note to the last entry'
|
|
927
|
+
c.example 'doing note --tag done "Keeping it real or something"', desc: 'Add a note to the last item tagged @done'
|
|
928
|
+
c.example 'doing note --search "late night" -e', desc: 'Open $EDITOR to add a note to the last item containing "late night" (fuzzy matched)'
|
|
929
|
+
|
|
930
|
+
c.desc 'Section'
|
|
1183
931
|
c.arg_name 'NAME'
|
|
1184
932
|
c.flag %i[s section], default_value: 'All'
|
|
1185
933
|
|
|
1186
|
-
c.desc
|
|
1187
|
-
c.
|
|
1188
|
-
c.flag [:in]
|
|
934
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
935
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1189
936
|
|
|
1190
|
-
c.desc
|
|
937
|
+
c.desc "Replace/Remove last entry's note (default append)"
|
|
938
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
939
|
+
|
|
940
|
+
c.desc 'Add/remove note from last entry matching tag. Wildcards allowed (*, ?).'
|
|
1191
941
|
c.arg_name 'TAG'
|
|
1192
942
|
c.flag [:tag]
|
|
1193
943
|
|
|
1194
|
-
c.desc '
|
|
1195
|
-
slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
|
|
944
|
+
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")'
|
|
1196
945
|
c.arg_name 'QUERY'
|
|
1197
946
|
c.flag [:search]
|
|
1198
947
|
|
|
@@ -1202,30 +951,27 @@ command %i[again resume] do |c|
|
|
|
1202
951
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
1203
952
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1204
953
|
|
|
1205
|
-
c.desc '
|
|
954
|
+
c.desc 'Add note to item that *doesn\'t* match search/tag filters'
|
|
1206
955
|
c.switch [:not], default_value: false, negatable: false
|
|
1207
956
|
|
|
1208
957
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1209
958
|
c.arg_name 'TYPE'
|
|
1210
959
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1211
960
|
|
|
1212
|
-
c.desc 'Boolean
|
|
961
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans.'
|
|
1213
962
|
c.arg_name 'BOOLEAN'
|
|
1214
963
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
1215
964
|
|
|
1216
|
-
c.desc
|
|
1217
|
-
c.switch %i[
|
|
965
|
+
c.desc 'Select item for new note from a menu of matching entries'
|
|
966
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
1218
967
|
|
|
1219
|
-
c.
|
|
1220
|
-
c.arg_name 'TEXT'
|
|
1221
|
-
c.flag %i[n note]
|
|
1222
|
-
|
|
1223
|
-
c.desc 'Select item to resume from a menu of matching entries'
|
|
1224
|
-
c.switch %i[i interactive], negatable: false, default_value: false
|
|
1225
|
-
|
|
1226
|
-
c.action do |_global_options, options, _args|
|
|
968
|
+
c.action do |_global_options, options, args|
|
|
1227
969
|
options[:fuzzy] = false
|
|
1228
|
-
|
|
970
|
+
if options[:section]
|
|
971
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
972
|
+
end
|
|
973
|
+
|
|
974
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
|
1229
975
|
|
|
1230
976
|
options[:case] = options[:case].normalize_case
|
|
1231
977
|
|
|
@@ -1235,145 +981,165 @@ command %i[again resume] do |c|
|
|
|
1235
981
|
options[:search] = search
|
|
1236
982
|
end
|
|
1237
983
|
|
|
1238
|
-
opts = options.dup
|
|
1239
984
|
|
|
1240
|
-
|
|
1241
|
-
opts[:tag_bool] = options[:bool].normalize_bool
|
|
1242
|
-
opts[:interactive] = options[:interactive]
|
|
985
|
+
last_entry = wwid.last_entry(options)
|
|
1243
986
|
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
987
|
+
unless last_entry
|
|
988
|
+
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
|
989
|
+
return
|
|
990
|
+
end
|
|
1247
991
|
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
c.desc 'Section'
|
|
1251
|
-
c.arg_name 'SECTION_NAME'
|
|
1252
|
-
c.flag %i[s section], default_value: 'All'
|
|
992
|
+
last_note = last_entry.note || Doing::Note.new
|
|
993
|
+
new_note = Doing::Note.new
|
|
1253
994
|
|
|
1254
|
-
|
|
1255
|
-
|
|
995
|
+
if options[:editor] || (args.empty? && $stdin.stat.size.zero? && !options[:remove])
|
|
996
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
1256
997
|
|
|
1257
|
-
|
|
1258
|
-
c.arg_name 'SORT_ORDER'
|
|
1259
|
-
c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
|
|
998
|
+
input = !args.empty? ? args.join(' ') : ''
|
|
1260
999
|
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1000
|
+
if options[:remove]
|
|
1001
|
+
prev_input = Doing::Note.new
|
|
1002
|
+
else
|
|
1003
|
+
prev_input = last_entry.note || Doing::Note.new
|
|
1004
|
+
end
|
|
1264
1005
|
|
|
1265
|
-
|
|
1266
|
-
c.arg_name 'TAG'
|
|
1267
|
-
c.flag [:tag]
|
|
1006
|
+
input = prev_input.add(input)
|
|
1268
1007
|
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1008
|
+
input = wwid.fork_editor(prev_input.strip_lines.join("\n"), message: nil).strip
|
|
1009
|
+
note = input
|
|
1010
|
+
options[:remove] = true
|
|
1011
|
+
new_note.add(note)
|
|
1012
|
+
elsif !args.empty?
|
|
1013
|
+
new_note.add(args.join(' '))
|
|
1014
|
+
elsif $stdin.stat.size.positive?
|
|
1015
|
+
new_note.add($stdin.read.strip)
|
|
1016
|
+
else
|
|
1017
|
+
raise EmptyInput, 'You must provide content when adding a note' unless options[:remove]
|
|
1018
|
+
end
|
|
1273
1019
|
|
|
1274
|
-
|
|
1275
|
-
|
|
1020
|
+
if last_note.equal?(new_note)
|
|
1021
|
+
Doing.logger.debug('Skipped:', 'No note change')
|
|
1022
|
+
else
|
|
1023
|
+
last_note.add(new_note, replace: options[:remove])
|
|
1024
|
+
Doing.logger.info('Entry updated:', last_entry.title)
|
|
1025
|
+
Doing::Hooks.trigger :post_entry_updated, wwid, last_entry
|
|
1026
|
+
end
|
|
1027
|
+
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
|
1028
|
+
wwid.write(wwid.doing_file)
|
|
1029
|
+
end
|
|
1030
|
+
end
|
|
1276
1031
|
|
|
1277
|
-
|
|
1278
|
-
|
|
1032
|
+
# @@now @@next
|
|
1033
|
+
desc 'Add an entry'
|
|
1034
|
+
long_desc %(Record what you're starting now, or backdate the start time using natural language.
|
|
1279
1035
|
|
|
1280
|
-
|
|
1281
|
-
c.switch [:not], default_value: false, negatable: false
|
|
1036
|
+
A parenthetical at the end of the entry will be converted to a note.
|
|
1282
1037
|
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1038
|
+
Run with no argument to create a new entry using #{Doing::Util.default_editor}.)
|
|
1039
|
+
arg_name 'ENTRY'
|
|
1040
|
+
command %i[now next] do |c|
|
|
1041
|
+
c.example 'doing now', desc: "Open #{Doing::Util.default_editor} to input an entry and optional note."
|
|
1042
|
+
c.example 'doing now working on a new project', desc: 'Add a new entry at the current time'
|
|
1043
|
+
c.example 'doing now debugging @project2', desc: 'Add an entry with a tag'
|
|
1044
|
+
c.example 'doing now adding an entry (with a note)', desc: 'Parenthetical at end is converted to note'
|
|
1045
|
+
c.example 'doing now --back 2pm A thing I started at 2:00 and am still doing...', desc: 'Backdate an entry'
|
|
1286
1046
|
|
|
1287
|
-
c.desc '
|
|
1288
|
-
c.arg_name '
|
|
1289
|
-
c.flag [
|
|
1047
|
+
c.desc 'Section'
|
|
1048
|
+
c.arg_name 'NAME'
|
|
1049
|
+
c.flag %i[s section]
|
|
1290
1050
|
|
|
1291
|
-
c.desc
|
|
1292
|
-
c.switch %i[
|
|
1051
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
1052
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1293
1053
|
|
|
1294
|
-
c.
|
|
1295
|
-
|
|
1054
|
+
c.desc 'Backdate start time [4pm|20m|2h|"yesterday noon"]'
|
|
1055
|
+
c.arg_name 'DATE_STRING'
|
|
1056
|
+
c.flag %i[b back started]
|
|
1296
1057
|
|
|
1297
|
-
|
|
1058
|
+
c.desc 'Timed entry, marks last entry in section as @done'
|
|
1059
|
+
c.switch %i[f finish_last], negatable: false, default_value: false
|
|
1298
1060
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1061
|
+
c.desc 'Include a note'
|
|
1062
|
+
c.arg_name 'TEXT'
|
|
1063
|
+
c.flag %i[n note]
|
|
1301
1064
|
|
|
1302
|
-
|
|
1303
|
-
|
|
1065
|
+
# c.desc "Edit entry with specified app"
|
|
1066
|
+
# c.arg_name 'editor_app'
|
|
1067
|
+
# # c.flag [:a, :app]
|
|
1068
|
+
|
|
1069
|
+
c.action do |_global_options, options, args|
|
|
1070
|
+
if options[:back]
|
|
1071
|
+
date = options[:back].chronify(guess: :begin)
|
|
1072
|
+
|
|
1073
|
+
raise InvalidTimeExpression.new('unable to parse date string', topic: 'Parser:') if date.nil?
|
|
1304
1074
|
else
|
|
1305
|
-
|
|
1075
|
+
date = Time.now
|
|
1306
1076
|
end
|
|
1307
1077
|
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
if options[:counts]
|
|
1311
|
-
tags.each { |t, c| puts "#{t} (#{c})" }
|
|
1078
|
+
if options[:section]
|
|
1079
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
1312
1080
|
else
|
|
1313
|
-
|
|
1081
|
+
options[:section] = settings['current_section']
|
|
1314
1082
|
end
|
|
1315
|
-
end
|
|
1316
|
-
end
|
|
1317
1083
|
|
|
1084
|
+
if options[:editor] || (args.empty? && $stdin.stat.size.zero?)
|
|
1085
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
1318
1086
|
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
containing another tag (with `--tag`).
|
|
1087
|
+
input = date.strftime('%F %R | ')
|
|
1088
|
+
input += args.join(' ') unless args.empty?
|
|
1089
|
+
input = wwid.fork_editor(input).strip
|
|
1323
1090
|
|
|
1324
|
-
|
|
1325
|
-
multiple characters, `?` to match a single character). With `--regex`,
|
|
1326
|
-
regular expressions will be interpreted instead of wildcards.
|
|
1091
|
+
raise EmptyInput, 'No content' if input.empty?
|
|
1327
1092
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1093
|
+
date, title, note = wwid.format_input(input)
|
|
1094
|
+
note.add(options[:note]) if options[:note]
|
|
1095
|
+
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
1096
|
+
wwid.write(wwid.doing_file)
|
|
1097
|
+
elsif args.length.positive?
|
|
1098
|
+
d, title, note = wwid.format_input(args.join(' '))
|
|
1099
|
+
date = d.nil? ? date : d
|
|
1100
|
+
note.add(options[:note]) if options[:note]
|
|
1101
|
+
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
1102
|
+
wwid.write(wwid.doing_file)
|
|
1103
|
+
elsif $stdin.stat.size.positive?
|
|
1104
|
+
input = $stdin.read.strip
|
|
1105
|
+
d, title, note = wwid.format_input(input)
|
|
1106
|
+
unless d.nil?
|
|
1107
|
+
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
1108
|
+
date = d
|
|
1109
|
+
end
|
|
1110
|
+
note.add(options[:note]) if options[:note]
|
|
1111
|
+
wwid.add_item(title.cap_first, section, { note: note, back: date, timed: options[:finish_last] })
|
|
1112
|
+
wwid.write(wwid.doing_file)
|
|
1113
|
+
else
|
|
1114
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
1115
|
+
end
|
|
1116
|
+
end
|
|
1117
|
+
end
|
|
1331
1118
|
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
c.example 'doing
|
|
1119
|
+
# @@reset @@begin
|
|
1120
|
+
desc 'Reset the start time of an entry'
|
|
1121
|
+
long_desc 'Update the start time of the last entry or the last entry matching a tag/search filter.
|
|
1122
|
+
If no argument is provided, the start time will be reset to the current time.
|
|
1123
|
+
If a date string is provided as an argument, the start time will be set to the parsed result.'
|
|
1124
|
+
arg_name 'DATE_STRING'
|
|
1125
|
+
command %i[reset begin] do |c|
|
|
1126
|
+
c.example 'doing reset', desc: 'Reset the start time of the last entry to the current time'
|
|
1127
|
+
c.example 'doing reset --tag project1', desc: 'Reset the start time of the most recent entry tagged @project1 to the current time'
|
|
1128
|
+
c.example 'doing reset 3pm', desc: 'Reset the start time of the last entry to 3pm of the current day'
|
|
1129
|
+
c.example 'doing begin --tag todo --resume', desc: 'alias for reset. Updates the last @todo entry to the current time, removing @done tag.'
|
|
1340
1130
|
|
|
1341
|
-
c.desc '
|
|
1342
|
-
c.arg_name '
|
|
1131
|
+
c.desc 'Limit search to section'
|
|
1132
|
+
c.arg_name 'NAME'
|
|
1343
1133
|
c.flag %i[s section], default_value: 'All'
|
|
1344
1134
|
|
|
1345
|
-
c.desc '
|
|
1346
|
-
c.
|
|
1347
|
-
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
|
1348
|
-
|
|
1349
|
-
c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
|
|
1350
|
-
c.arg_name 'ORIG_TAG'
|
|
1351
|
-
c.flag %i[rename]
|
|
1352
|
-
|
|
1353
|
-
c.desc 'Don\'t ask permission to tag all entries when count is 0'
|
|
1354
|
-
c.switch %i[force], negatable: false, default_value: false
|
|
1355
|
-
|
|
1356
|
-
c.desc 'Include current date/time with tag'
|
|
1357
|
-
c.switch %i[d date], negatable: false, default_value: false
|
|
1358
|
-
|
|
1359
|
-
c.desc 'Remove given tag(s)'
|
|
1360
|
-
c.switch %i[r remove], negatable: false, default_value: false
|
|
1361
|
-
|
|
1362
|
-
c.desc 'Interpret tag string as regular expression (with --remove)'
|
|
1363
|
-
c.switch %i[regex], negatable: false, default_value: false
|
|
1364
|
-
|
|
1365
|
-
c.desc 'Tag last entry (or entries) not marked @done'
|
|
1366
|
-
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
1367
|
-
|
|
1368
|
-
c.desc 'Autotag entries based on autotag configuration in ~/.config/doing/config.yml'
|
|
1369
|
-
c.switch %i[a autotag], negatable: false, default_value: false
|
|
1135
|
+
c.desc 'Resume entry (remove @done)'
|
|
1136
|
+
c.switch %i[r resume], default_value: true
|
|
1370
1137
|
|
|
1371
|
-
c.desc '
|
|
1372
|
-
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
1138
|
+
c.desc 'Reset last entry matching tag. Wildcards allowed (*, ?).'
|
|
1373
1139
|
c.arg_name 'TAG'
|
|
1374
1140
|
c.flag [:tag]
|
|
1375
1141
|
|
|
1376
|
-
c.desc '
|
|
1142
|
+
c.desc 'Reset last entry matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
|
1377
1143
|
c.arg_name 'QUERY'
|
|
1378
1144
|
c.flag [:search]
|
|
1379
1145
|
|
|
@@ -1383,112 +1149,209 @@ command :tag do |c|
|
|
|
1383
1149
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
1384
1150
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1385
1151
|
|
|
1386
|
-
c.desc '
|
|
1152
|
+
c.desc 'Reset items that *don\'t* match search/tag filters'
|
|
1387
1153
|
c.switch [:not], default_value: false, negatable: false
|
|
1388
1154
|
|
|
1389
1155
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1390
1156
|
c.arg_name 'TYPE'
|
|
1391
1157
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1392
1158
|
|
|
1393
|
-
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters
|
|
1159
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters'
|
|
1394
1160
|
c.arg_name 'BOOLEAN'
|
|
1395
1161
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
1396
1162
|
|
|
1397
|
-
c.desc 'Select
|
|
1163
|
+
c.desc 'Select from a menu of matching entries'
|
|
1398
1164
|
c.switch %i[i interactive], negatable: false, default_value: false
|
|
1399
1165
|
|
|
1400
|
-
c.action do |
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1166
|
+
c.action do |global_options, options, args|
|
|
1167
|
+
if args.count > 0
|
|
1168
|
+
reset_date = args.join(' ').chronify(guess: :begin)
|
|
1169
|
+
raise InvalidArgument, 'Invalid date string' unless reset_date
|
|
1170
|
+
else
|
|
1171
|
+
reset_date = Time.now
|
|
1172
|
+
end
|
|
1407
1173
|
|
|
1174
|
+
options[:fuzzy] = false
|
|
1408
1175
|
if options[:section]
|
|
1409
|
-
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
1176
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
1410
1177
|
end
|
|
1411
1178
|
|
|
1179
|
+
options[:bool] = options[:bool].normalize_bool
|
|
1412
1180
|
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1181
|
+
options[:case] = options[:case].normalize_case
|
|
1182
|
+
|
|
1183
|
+
if options[:search]
|
|
1184
|
+
search = options[:search]
|
|
1185
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
1186
|
+
options[:search] = search
|
|
1417
1187
|
end
|
|
1418
1188
|
|
|
1419
|
-
if options[:autotag]
|
|
1420
|
-
tags = []
|
|
1421
|
-
else
|
|
1422
|
-
tags = if args.join('') =~ /,/
|
|
1423
|
-
args.join('').split(/,/)
|
|
1424
|
-
else
|
|
1425
|
-
args.join(' ').split(' ') # in case tags are quoted as one arg
|
|
1426
|
-
end
|
|
1427
1189
|
|
|
1428
|
-
|
|
1429
|
-
end
|
|
1190
|
+
items = wwid.filter_items([], opt: options)
|
|
1430
1191
|
|
|
1431
1192
|
if options[:interactive]
|
|
1432
|
-
|
|
1433
|
-
|
|
1193
|
+
last_entry = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
|
|
1194
|
+
menu: true,
|
|
1195
|
+
header: '',
|
|
1196
|
+
prompt: 'Select an entry to start/reset > ',
|
|
1197
|
+
multiple: false,
|
|
1198
|
+
sort: false,
|
|
1199
|
+
show_if_single: true)
|
|
1434
1200
|
else
|
|
1435
|
-
|
|
1201
|
+
last_entry = items.reverse.last
|
|
1436
1202
|
end
|
|
1437
1203
|
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
if options[:search]
|
|
1442
|
-
search = options[:search]
|
|
1443
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
1444
|
-
options[:search] = search
|
|
1204
|
+
unless last_entry
|
|
1205
|
+
Doing.logger.warn('Not found:', 'No entry matching parameters was found.')
|
|
1206
|
+
return
|
|
1445
1207
|
end
|
|
1446
1208
|
|
|
1447
|
-
options[:
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
options[:tags] = tags
|
|
1451
|
-
options[:tag_bool] = options[:bool].normalize_bool
|
|
1209
|
+
wwid.reset_item(last_entry, date: reset_date, resume: options[:resume])
|
|
1210
|
+
Doing::Hooks.trigger :post_entry_updated, wwid, last_entry
|
|
1211
|
+
# new_entry = Doing::Item.new(last_entry.date, last_entry.title, last_entry.section, new_note)
|
|
1452
1212
|
|
|
1453
|
-
|
|
1454
|
-
|
|
1213
|
+
wwid.write(wwid.doing_file)
|
|
1214
|
+
end
|
|
1215
|
+
end
|
|
1455
1216
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
elsif options[:tag]
|
|
1460
|
-
section_q = ' matching your tag search'
|
|
1461
|
-
elsif section == 'All'
|
|
1462
|
-
section_q = ''
|
|
1463
|
-
else
|
|
1464
|
-
section_q = " in section #{section}"
|
|
1465
|
-
end
|
|
1217
|
+
# @@select
|
|
1218
|
+
desc 'Display an interactive menu to perform operations'
|
|
1219
|
+
long_desc 'List all entries and select with typeahead fuzzy matching.
|
|
1466
1220
|
|
|
1221
|
+
Multiple selections are allowed, hit tab to add the highlighted entry to the
|
|
1222
|
+
selection, and use ctrl-a to select all visible items. Return processes the
|
|
1223
|
+
selected entries.
|
|
1467
1224
|
|
|
1468
|
-
|
|
1469
|
-
"Are you sure you want to autotag #{matches} records#{section_q}"
|
|
1470
|
-
elsif options[:remove]
|
|
1471
|
-
"Are you sure you want to remove #{tags.join(' and ')} from #{matches} records#{section_q}"
|
|
1472
|
-
else
|
|
1473
|
-
"Are you sure you want to add #{tags.join(' and ')} to #{matches} records#{section_q}"
|
|
1474
|
-
end
|
|
1225
|
+
Search in the menu by typing:
|
|
1475
1226
|
|
|
1476
|
-
|
|
1227
|
+
sbtrkt fuzzy-match Items that match s*b*t*r*k*t
|
|
1477
1228
|
|
|
1478
|
-
|
|
1479
|
-
end
|
|
1480
|
-
end
|
|
1229
|
+
\'wild exact-match (quoted) Items that include wild
|
|
1481
1230
|
|
|
1482
|
-
|
|
1231
|
+
!fire inverse-exact-match Items that do not include fire'
|
|
1232
|
+
command :select do |c|
|
|
1233
|
+
c.example 'doing select', desc: 'Select from all entries. A menu of available actions will be presented after confirming the selection.'
|
|
1234
|
+
c.example 'doing select --editor', desc: 'Select entries from a menu and batch edit them in your default editor'
|
|
1235
|
+
c.example 'doing select --after "yesterday 12pm" --tag project1', desc: 'Display a menu of entries created after noon yesterday, add @project1 to selected entries'
|
|
1236
|
+
c.desc 'Select from a specific section'
|
|
1237
|
+
c.arg_name 'SECTION'
|
|
1238
|
+
c.flag %i[s section]
|
|
1239
|
+
|
|
1240
|
+
c.desc 'Tag selected entries'
|
|
1241
|
+
c.arg_name 'TAG'
|
|
1242
|
+
c.flag %i[t tag]
|
|
1243
|
+
|
|
1244
|
+
c.desc 'Reverse -c, -f, --flag, and -t (remove instead of adding)'
|
|
1245
|
+
c.switch %i[r remove], negatable: false
|
|
1246
|
+
|
|
1247
|
+
# c.desc 'Add @done to selected item(s), using start time of next item as the finish time'
|
|
1248
|
+
# c.switch %i[a auto], negatable: false, default_value: false
|
|
1249
|
+
|
|
1250
|
+
c.desc 'Archive selected items'
|
|
1251
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
1252
|
+
|
|
1253
|
+
c.desc 'Move selected items to section'
|
|
1254
|
+
c.arg_name 'SECTION'
|
|
1255
|
+
c.flag %i[m move]
|
|
1256
|
+
|
|
1257
|
+
c.desc 'Initial search query for filtering. Matching is fuzzy. For exact matching, start query with a single quote, e.g. `--query "\'search"'
|
|
1258
|
+
c.arg_name 'QUERY'
|
|
1259
|
+
c.flag %i[q query search]
|
|
1260
|
+
|
|
1261
|
+
c.desc 'Select from entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1262
|
+
c.arg_name 'DATE_STRING'
|
|
1263
|
+
c.flag [:before]
|
|
1264
|
+
|
|
1265
|
+
c.desc 'Select from entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1266
|
+
c.arg_name 'DATE_STRING'
|
|
1267
|
+
c.flag [:after]
|
|
1268
|
+
|
|
1269
|
+
c.desc %(
|
|
1270
|
+
Date range to show, or a single day to filter date on.
|
|
1271
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
|
1272
|
+
To specify a range, use "to" or "through": `doing select --from "monday 8am to friday 5pm"`.
|
|
1273
|
+
|
|
1274
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
|
1275
|
+
by time of day.
|
|
1276
|
+
)
|
|
1277
|
+
c.arg_name 'DATE_OR_RANGE'
|
|
1278
|
+
c.flag [:from]
|
|
1279
|
+
|
|
1280
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
1281
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1282
|
+
|
|
1283
|
+
c.desc 'Select items that *don\'t* match search/tag filters'
|
|
1284
|
+
c.switch [:not], default_value: false, negatable: false
|
|
1285
|
+
|
|
1286
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1287
|
+
c.arg_name 'TYPE'
|
|
1288
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1289
|
+
|
|
1290
|
+
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.'
|
|
1291
|
+
c.switch %i[menu], negatable: true, default_value: true
|
|
1292
|
+
|
|
1293
|
+
c.desc 'Cancel selected items (add @done without timestamp)'
|
|
1294
|
+
c.switch %i[c cancel], negatable: false, default_value: false
|
|
1295
|
+
|
|
1296
|
+
c.desc 'Delete selected items'
|
|
1297
|
+
c.switch %i[d delete], negatable: false, default_value: false
|
|
1298
|
+
|
|
1299
|
+
c.desc 'Edit selected item(s)'
|
|
1300
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1301
|
+
|
|
1302
|
+
c.desc 'Add @done with current time to selected item(s)'
|
|
1303
|
+
c.switch %i[f finish], negatable: false, default_value: false
|
|
1304
|
+
|
|
1305
|
+
c.desc 'Add flag to selected item(s)'
|
|
1306
|
+
c.switch %i[flag], negatable: false, default_value: false
|
|
1307
|
+
|
|
1308
|
+
c.desc 'Perform action without confirmation.'
|
|
1309
|
+
c.switch %i[force], negatable: false, default_value: false
|
|
1310
|
+
|
|
1311
|
+
c.desc 'Save selected entries to file using --output format'
|
|
1312
|
+
c.arg_name 'FILE'
|
|
1313
|
+
c.flag %i[save_to]
|
|
1314
|
+
|
|
1315
|
+
c.desc "Output entries to format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
1316
|
+
c.arg_name 'FORMAT'
|
|
1317
|
+
c.flag %i[o output]
|
|
1318
|
+
|
|
1319
|
+
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."
|
|
1320
|
+
c.switch %i[again resume], negatable: false, default_value: false
|
|
1321
|
+
|
|
1322
|
+
c.action do |_global_options, options, args|
|
|
1323
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
1324
|
+
|
|
1325
|
+
raise InvalidArgument, '--no-menu requires --query' if !options[:menu] && !options[:query]
|
|
1326
|
+
|
|
1327
|
+
options[:case] = options[:case].normalize_case
|
|
1328
|
+
|
|
1329
|
+
wwid.interactive(options) # hooked
|
|
1483
1330
|
end
|
|
1484
1331
|
end
|
|
1485
1332
|
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1333
|
+
# @@tag
|
|
1334
|
+
desc 'Add tag(s) to last entry'
|
|
1335
|
+
long_desc 'Add (or remove) tags from the last entry, or from multiple entries
|
|
1336
|
+
(with `--count`), entries matching a search (with `--search`), or entries
|
|
1337
|
+
containing another tag (with `--tag`).
|
|
1338
|
+
|
|
1339
|
+
When removing tags with `-r`, wildcards are allowed (`*` to match
|
|
1340
|
+
multiple characters, `?` to match a single character). With `--regex`,
|
|
1341
|
+
regular expressions will be interpreted instead of wildcards.
|
|
1342
|
+
|
|
1343
|
+
For all tag removals the match is case insensitive by default, but if
|
|
1344
|
+
the tag search string contains any uppercase letters, the match will
|
|
1345
|
+
become case sensitive automatically.
|
|
1346
|
+
|
|
1347
|
+
Tag name arguments do not need to be prefixed with @.'
|
|
1348
|
+
arg_name 'TAG', :multiple
|
|
1349
|
+
command :tag do |c|
|
|
1350
|
+
c.example 'doing tag mytag', desc: 'Add @mytag to the last entry created'
|
|
1351
|
+
c.example 'doing tag --remove mytag', desc: 'Remove @mytag from the last entry created'
|
|
1352
|
+
c.example 'doing tag --rename "other*" --count 10 newtag', desc: 'Rename tags beginning with "other" (wildcard) to @newtag on the last 10 entries'
|
|
1353
|
+
c.example 'doing tag --search "developing" coding', desc: 'Add @coding to the last entry containing string "developing" (fuzzy matching)'
|
|
1354
|
+
c.example 'doing tag --interactive --tag project1 coding', desc: 'Create an interactive menu from entries tagged @project1, selection(s) will be tagged with @coding'
|
|
1492
1355
|
|
|
1493
1356
|
c.desc 'Section'
|
|
1494
1357
|
c.arg_name 'SECTION_NAME'
|
|
@@ -1498,24 +1361,34 @@ command %i[mark flag] do |c|
|
|
|
1498
1361
|
c.arg_name 'COUNT'
|
|
1499
1362
|
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
|
1500
1363
|
|
|
1501
|
-
c.desc '
|
|
1364
|
+
c.desc 'Replace existing tag with tag argument, wildcards (*,?) allowed, or use with --regex'
|
|
1365
|
+
c.arg_name 'ORIG_TAG'
|
|
1366
|
+
c.flag %i[rename]
|
|
1367
|
+
|
|
1368
|
+
c.desc 'Don\'t ask permission to tag all entries when count is 0'
|
|
1502
1369
|
c.switch %i[force], negatable: false, default_value: false
|
|
1503
1370
|
|
|
1504
1371
|
c.desc 'Include current date/time with tag'
|
|
1505
1372
|
c.switch %i[d date], negatable: false, default_value: false
|
|
1506
1373
|
|
|
1507
|
-
c.desc 'Remove
|
|
1374
|
+
c.desc 'Remove given tag(s)'
|
|
1508
1375
|
c.switch %i[r remove], negatable: false, default_value: false
|
|
1509
1376
|
|
|
1510
|
-
c.desc '
|
|
1377
|
+
c.desc 'Interpret tag string as regular expression (with --remove)'
|
|
1378
|
+
c.switch %i[regex], negatable: false, default_value: false
|
|
1379
|
+
|
|
1380
|
+
c.desc 'Tag last entry (or entries) not marked @done'
|
|
1511
1381
|
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
1512
1382
|
|
|
1513
|
-
c.desc '
|
|
1383
|
+
c.desc 'Autotag entries based on autotag configuration in ~/.config/doing/config.yml'
|
|
1384
|
+
c.switch %i[a autotag], negatable: false, default_value: false
|
|
1385
|
+
|
|
1386
|
+
c.desc 'Tag the last X entries containing TAG.
|
|
1514
1387
|
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
1515
1388
|
c.arg_name 'TAG'
|
|
1516
1389
|
c.flag [:tag]
|
|
1517
1390
|
|
|
1518
|
-
c.desc '
|
|
1391
|
+
c.desc 'Tag entries matching search filter, surround with slashes for regex (e.g. "/query.*/"), start with single quote for exact match ("\'query")'
|
|
1519
1392
|
c.arg_name 'QUERY'
|
|
1520
1393
|
c.flag [:search]
|
|
1521
1394
|
|
|
@@ -1525,7 +1398,7 @@ command %i[mark flag] do |c|
|
|
|
1525
1398
|
c.desc 'Force exact search string matching (case sensitive)'
|
|
1526
1399
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1527
1400
|
|
|
1528
|
-
c.desc '
|
|
1401
|
+
c.desc 'Tag items that *don\'t* match search/tag filters'
|
|
1529
1402
|
c.switch [:not], default_value: false, negatable: false
|
|
1530
1403
|
|
|
1531
1404
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
@@ -1536,12 +1409,12 @@ command %i[mark flag] do |c|
|
|
|
1536
1409
|
c.arg_name 'BOOLEAN'
|
|
1537
1410
|
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
1538
1411
|
|
|
1539
|
-
c.desc 'Select item(s) to
|
|
1412
|
+
c.desc 'Select item(s) to tag from a menu of matching entries'
|
|
1540
1413
|
c.switch %i[i interactive], negatable: false, default_value: false
|
|
1541
1414
|
|
|
1542
|
-
c.action do |_global_options, options,
|
|
1415
|
+
c.action do |_global_options, options, args|
|
|
1543
1416
|
options[:fuzzy] = false
|
|
1544
|
-
|
|
1417
|
+
raise MissingArgument, 'You must specify at least one tag' if args.empty? && !options[:autotag]
|
|
1545
1418
|
|
|
1546
1419
|
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
1547
1420
|
|
|
@@ -1551,12 +1424,25 @@ command %i[mark flag] do |c|
|
|
|
1551
1424
|
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
1552
1425
|
end
|
|
1553
1426
|
|
|
1427
|
+
|
|
1554
1428
|
if options[:tag].nil?
|
|
1555
1429
|
search_tags = []
|
|
1556
1430
|
else
|
|
1557
1431
|
search_tags = options[:tag].to_tags
|
|
1558
1432
|
end
|
|
1559
1433
|
|
|
1434
|
+
if options[:autotag]
|
|
1435
|
+
tags = []
|
|
1436
|
+
else
|
|
1437
|
+
tags = if args.join('') =~ /,/
|
|
1438
|
+
args.join('').split(/,/)
|
|
1439
|
+
else
|
|
1440
|
+
args.join(' ').split(' ') # in case tags are quoted as one arg
|
|
1441
|
+
end
|
|
1442
|
+
|
|
1443
|
+
tags.map! { |tag| tag.sub(/^@/, '').strip }
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1560
1446
|
if options[:interactive]
|
|
1561
1447
|
count = 0
|
|
1562
1448
|
options[:force] = true
|
|
@@ -1564,6 +1450,7 @@ command %i[mark flag] do |c|
|
|
|
1564
1450
|
count = options[:count].to_i
|
|
1565
1451
|
end
|
|
1566
1452
|
|
|
1453
|
+
options[:case] ||= :smart
|
|
1567
1454
|
options[:case] = options[:case].normalize_case
|
|
1568
1455
|
|
|
1569
1456
|
if options[:search]
|
|
@@ -1572,114 +1459,97 @@ command %i[mark flag] do |c|
|
|
|
1572
1459
|
options[:search] = search
|
|
1573
1460
|
end
|
|
1574
1461
|
|
|
1462
|
+
options[:count] = count
|
|
1463
|
+
options[:section] = section
|
|
1464
|
+
options[:tag] = search_tags
|
|
1465
|
+
options[:tags] = tags
|
|
1466
|
+
options[:tag_bool] = options[:bool].normalize_bool
|
|
1467
|
+
|
|
1575
1468
|
if count.zero? && !options[:force]
|
|
1576
|
-
|
|
1577
|
-
section_q = ' matching your search terms'
|
|
1578
|
-
elsif options[:tag]
|
|
1579
|
-
section_q = ' matching your tag search'
|
|
1580
|
-
elsif section == 'All'
|
|
1581
|
-
section_q = ''
|
|
1582
|
-
else
|
|
1583
|
-
section_q = " in section #{section}"
|
|
1584
|
-
end
|
|
1469
|
+
matches = wwid.filter_items([], opt: options).count
|
|
1585
1470
|
|
|
1471
|
+
if matches > 5
|
|
1472
|
+
if options[:search]
|
|
1473
|
+
section_q = ' matching your search terms'
|
|
1474
|
+
elsif options[:tag]
|
|
1475
|
+
section_q = ' matching your tag search'
|
|
1476
|
+
elsif section == 'All'
|
|
1477
|
+
section_q = ''
|
|
1478
|
+
else
|
|
1479
|
+
section_q = " in section #{section}"
|
|
1480
|
+
end
|
|
1586
1481
|
|
|
1587
|
-
question = if options[:remove]
|
|
1588
|
-
"Are you sure you want to unflag all entries#{section_q}"
|
|
1589
|
-
else
|
|
1590
|
-
"Are you sure you want to flag all records#{section_q}"
|
|
1591
|
-
end
|
|
1592
1482
|
|
|
1593
|
-
|
|
1483
|
+
question = if options[:autotag]
|
|
1484
|
+
"Are you sure you want to autotag #{matches} records#{section_q}"
|
|
1485
|
+
elsif options[:remove]
|
|
1486
|
+
"Are you sure you want to remove #{tags.join(' and ')} from #{matches} records#{section_q}"
|
|
1487
|
+
else
|
|
1488
|
+
"Are you sure you want to add #{tags.join(' and ')} to #{matches} records#{section_q}"
|
|
1489
|
+
end
|
|
1594
1490
|
|
|
1595
|
-
|
|
1596
|
-
end
|
|
1491
|
+
res = Doing::Prompt.yn(question, default_response: false)
|
|
1597
1492
|
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
options[:tags] = [mark]
|
|
1602
|
-
options[:tag_bool] = options[:bool].normalize_bool
|
|
1493
|
+
raise UserCancelled unless res
|
|
1494
|
+
end
|
|
1495
|
+
end
|
|
1603
1496
|
|
|
1604
1497
|
wwid.tag_last(options)
|
|
1605
1498
|
end
|
|
1606
1499
|
end
|
|
1607
1500
|
|
|
1608
|
-
|
|
1609
|
-
long_desc %(
|
|
1610
|
-
The argument can be a section name, @tag(s) or both.
|
|
1611
|
-
"pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
|
|
1501
|
+
## View commands
|
|
1612
1502
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
command :show do |c|
|
|
1619
|
-
c.example 'doing show Currently', desc: 'Show entries in the Currently section'
|
|
1620
|
-
c.example 'doing show @project1', desc: 'Show entries tagged @project1'
|
|
1621
|
-
c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
|
|
1622
|
-
c.example 'doing show @oracle @writing --bool and', desc: 'Show entries tagged both @oracle and @writing'
|
|
1623
|
-
c.example 'doing show Currently @devo --bool not', desc: 'Show entries in Currently NOT tagged @devo'
|
|
1624
|
-
c.example 'doing show Ideas @doing --from "mon to fri"', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
|
|
1625
|
-
c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
|
|
1503
|
+
# @@choose
|
|
1504
|
+
desc 'Select a section to display from a menu'
|
|
1505
|
+
command :choose do |c|
|
|
1506
|
+
c.action do |_global_options, _options, _args|
|
|
1507
|
+
section = wwid.choose_section
|
|
1626
1508
|
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1509
|
+
Doing::Pager.page wwid.list_section({ section: section.cap_first, count: 0 }) if section
|
|
1510
|
+
end
|
|
1511
|
+
end
|
|
1630
1512
|
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1513
|
+
# @@grep @@search
|
|
1514
|
+
desc 'Search for entries'
|
|
1515
|
+
long_desc %(
|
|
1516
|
+
Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
|
|
1634
1517
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1518
|
+
To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
|
|
1519
|
+
)
|
|
1520
|
+
arg_name 'SEARCH_PATTERN'
|
|
1521
|
+
command %i[grep search] do |c|
|
|
1522
|
+
c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
|
|
1523
|
+
c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
|
|
1524
|
+
c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
|
|
1525
|
+
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'
|
|
1638
1526
|
|
|
1639
|
-
c.desc '
|
|
1640
|
-
c.arg_name '
|
|
1641
|
-
c.flag %i[
|
|
1527
|
+
c.desc 'Section'
|
|
1528
|
+
c.arg_name 'NAME'
|
|
1529
|
+
c.flag %i[s section], default_value: 'All'
|
|
1642
1530
|
|
|
1643
|
-
c.desc '
|
|
1531
|
+
c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1644
1532
|
c.arg_name 'DATE_STRING'
|
|
1645
1533
|
c.flag [:before]
|
|
1646
1534
|
|
|
1647
|
-
c.desc '
|
|
1535
|
+
c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1648
1536
|
c.arg_name 'DATE_STRING'
|
|
1649
1537
|
c.flag [:after]
|
|
1650
1538
|
|
|
1651
1539
|
c.desc %(
|
|
1652
1540
|
Date range to show, or a single day to filter date on.
|
|
1653
1541
|
Date range argument should be quoted. Date specifications can be natural language.
|
|
1654
|
-
To specify a range, use "to" or "through": `doing
|
|
1542
|
+
To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
|
|
1655
1543
|
|
|
1656
1544
|
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
|
1657
1545
|
by time of day.
|
|
1658
1546
|
)
|
|
1659
|
-
|
|
1660
1547
|
c.arg_name 'DATE_OR_RANGE'
|
|
1661
1548
|
c.flag [:from]
|
|
1662
1549
|
|
|
1663
|
-
c.desc
|
|
1664
|
-
c.arg_name '
|
|
1665
|
-
c.flag [
|
|
1666
|
-
|
|
1667
|
-
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
1668
|
-
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
1669
|
-
|
|
1670
|
-
c.desc 'Force exact search string matching (case sensitive)'
|
|
1671
|
-
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1672
|
-
|
|
1673
|
-
c.desc 'Show items that *don\'t* match search/tag/date filters'
|
|
1674
|
-
c.switch [:not], default_value: false, negatable: false
|
|
1675
|
-
|
|
1676
|
-
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1677
|
-
c.arg_name 'TYPE'
|
|
1678
|
-
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1679
|
-
|
|
1680
|
-
c.desc 'Sort order (asc/desc)'
|
|
1681
|
-
c.arg_name 'ORDER'
|
|
1682
|
-
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
1550
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
1551
|
+
c.arg_name 'FORMAT'
|
|
1552
|
+
c.flag %i[o output]
|
|
1683
1553
|
|
|
1684
1554
|
c.desc 'Show time intervals on @done tasks'
|
|
1685
1555
|
c.switch %i[t times], default_value: true, negatable: true
|
|
@@ -1694,229 +1564,152 @@ command :show do |c|
|
|
|
1694
1564
|
default = 'time'
|
|
1695
1565
|
default = settings['tag_sort'] || 'name'
|
|
1696
1566
|
c.arg_name 'KEY'
|
|
1697
|
-
c.flag [:tag_sort], must_match: /^(?:name|time)
|
|
1698
|
-
|
|
1699
|
-
c.desc 'Tag sort direction (asc|desc)'
|
|
1700
|
-
c.arg_name 'DIRECTION'
|
|
1701
|
-
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
1567
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
1702
1568
|
|
|
1703
1569
|
c.desc 'Only show items with recorded time intervals'
|
|
1704
1570
|
c.switch [:only_timed], default_value: false, negatable: false
|
|
1705
1571
|
|
|
1706
|
-
c.desc '
|
|
1707
|
-
c.switch
|
|
1708
|
-
|
|
1709
|
-
c.desc 'Select from a menu of matching entries to perform additional operations'
|
|
1710
|
-
c.switch %i[i interactive], negatable: false, default_value: false
|
|
1572
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
1573
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
1711
1574
|
|
|
1712
|
-
c.desc
|
|
1713
|
-
c.
|
|
1714
|
-
c.flag %i[o output]
|
|
1715
|
-
c.action do |global_options, options, args|
|
|
1716
|
-
options[:fuzzy] = false
|
|
1717
|
-
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
1575
|
+
c.desc 'Force exact string matching (case sensitive)'
|
|
1576
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1718
1577
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1578
|
+
c.desc 'Show items that *don\'t* match search string'
|
|
1579
|
+
c.switch [:not], default_value: false, negatable: false
|
|
1721
1580
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
section = 'All'
|
|
1726
|
-
args.shift
|
|
1727
|
-
when /^(choose|pick)$/i
|
|
1728
|
-
section = wwid.choose_section(include_all: true)
|
|
1581
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1582
|
+
c.arg_name 'TYPE'
|
|
1583
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1729
1584
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
section = 'All'
|
|
1733
|
-
else
|
|
1734
|
-
begin
|
|
1735
|
-
section = wwid.guess_section(args[0])
|
|
1736
|
-
rescue WrongCommand => exception
|
|
1737
|
-
cmd = commands[:view]
|
|
1738
|
-
action = cmd.send(:get_action, nil)
|
|
1739
|
-
return action.call(global_options, options, args)
|
|
1740
|
-
end
|
|
1585
|
+
c.desc "Edit matching entries with #{Doing::Util.default_editor}"
|
|
1586
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1741
1587
|
|
|
1742
|
-
|
|
1588
|
+
c.desc "Delete matching entries"
|
|
1589
|
+
c.switch %i[d delete], negatable: false, default_value: false
|
|
1743
1590
|
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
if args.length.positive?
|
|
1747
|
-
args.each do |arg|
|
|
1748
|
-
arg.split(/,/).each do |tag|
|
|
1749
|
-
tags.push(tag.strip.sub(/^@/, ''))
|
|
1750
|
-
end
|
|
1751
|
-
end
|
|
1752
|
-
end
|
|
1753
|
-
else
|
|
1754
|
-
section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
|
|
1755
|
-
section ||= 'All'
|
|
1756
|
-
end
|
|
1591
|
+
c.desc 'Display an interactive menu of results to perform further operations'
|
|
1592
|
+
c.switch %i[i interactive], default_value: false, negatable: false
|
|
1757
1593
|
|
|
1758
|
-
|
|
1594
|
+
c.action do |_global_options, options, args|
|
|
1595
|
+
options[:fuzzy] = false
|
|
1596
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
1759
1597
|
|
|
1760
|
-
|
|
1598
|
+
template = settings['templates']['default'].deep_merge(settings)
|
|
1599
|
+
tags_color = template.key?('tags_color') ? template['tags_color'] : nil
|
|
1761
1600
|
|
|
1762
|
-
|
|
1763
|
-
'wrap_width' => settings['wrap_width'] || 0,
|
|
1764
|
-
'date_format' => settings['default_date_format'],
|
|
1765
|
-
'order' => settings['order'] || 'asc',
|
|
1766
|
-
'tags_color' => settings['tags_color']
|
|
1767
|
-
})
|
|
1601
|
+
section = wwid.guess_section(options[:section]) if options[:section]
|
|
1768
1602
|
|
|
1769
1603
|
options[:case] = options[:case].normalize_case
|
|
1770
1604
|
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
1774
|
-
options[:search] = search
|
|
1775
|
-
end
|
|
1605
|
+
search = args.join(' ')
|
|
1606
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
1776
1607
|
|
|
1608
|
+
options[:times] = true if options[:totals]
|
|
1609
|
+
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
1610
|
+
options[:highlight] = true
|
|
1611
|
+
options[:search] = search
|
|
1777
1612
|
options[:section] = section
|
|
1613
|
+
options[:tags_color] = tags_color
|
|
1778
1614
|
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
'bool' => options[:bool].normalize_bool
|
|
1783
|
-
}
|
|
1784
|
-
end
|
|
1615
|
+
Doing::Pager.page wwid.list_section(options)
|
|
1616
|
+
end
|
|
1617
|
+
end
|
|
1785
1618
|
|
|
1786
|
-
|
|
1787
|
-
|
|
1619
|
+
# @@last
|
|
1620
|
+
desc 'Show the last entry, optionally edit'
|
|
1621
|
+
long_desc 'Shows the last entry. Using --search and --tag filters, you can view/edit the last entry matching a filter,
|
|
1622
|
+
allowing `doing last` to target historical entries.'
|
|
1623
|
+
command :last do |c|
|
|
1624
|
+
c.example 'doing last', desc: 'Show the most recent entry in all sections'
|
|
1625
|
+
c.example 'doing last -s Later', desc: 'Show the most recent entry in the Later section'
|
|
1626
|
+
c.example 'doing last --tag project1,work --bool AND', desc: 'Show most recent entry tagged @project1 and @work'
|
|
1627
|
+
c.example 'doing last --search "side hustle"', desc: 'Show most recent entry containing "side hustle" (fuzzy matching)'
|
|
1628
|
+
c.example 'doing last --search "\'side hustle"', desc: 'Show most recent entry containing "side hustle" (exact match)'
|
|
1629
|
+
c.example 'doing last --edit', desc: 'Open the most recent entry in an editor for modifications'
|
|
1630
|
+
c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
|
|
1788
1631
|
|
|
1789
|
-
|
|
1632
|
+
c.desc 'Specify a section'
|
|
1633
|
+
c.arg_name 'NAME'
|
|
1634
|
+
c.flag %i[s section], default_value: 'All'
|
|
1790
1635
|
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
raise UserCancelled unless tag
|
|
1636
|
+
c.desc "Edit entry with #{Doing::Util.default_editor}"
|
|
1637
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
1794
1638
|
|
|
1795
|
-
|
|
1639
|
+
c.desc "Delete the last entry"
|
|
1640
|
+
c.switch %i[d delete], negatable: false, default_value: false
|
|
1796
1641
|
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
'tags' => tags,
|
|
1801
|
-
'bool' => options[:bool].normalize_bool
|
|
1802
|
-
}
|
|
1803
|
-
options[:tag_filter] = tag_filter
|
|
1804
|
-
end
|
|
1805
|
-
end
|
|
1806
|
-
|
|
1807
|
-
opt = options.dup
|
|
1808
|
-
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
1809
|
-
opt[:count] = options[:count].to_i
|
|
1810
|
-
opt[:highlight] = true
|
|
1811
|
-
opt[:order] = options[:sort].normalize_order
|
|
1812
|
-
opt[:tag] = nil
|
|
1813
|
-
opt[:tag_order] = options[:tag_order].normalize_order
|
|
1814
|
-
opt[:tags_color] = template['tags_color']
|
|
1815
|
-
|
|
1816
|
-
Doing::Pager.page wwid.list_section(opt, items: items)
|
|
1817
|
-
end
|
|
1818
|
-
end
|
|
1819
|
-
|
|
1820
|
-
desc 'Search for entries'
|
|
1821
|
-
long_desc %(
|
|
1822
|
-
Search all sections (or limit to a single section) for entries matching text or regular expression. Normal strings are fuzzy matched.
|
|
1823
|
-
|
|
1824
|
-
To search with regular expressions, single quote the string and surround with slashes: `doing search '/\bm.*?x\b/'`
|
|
1825
|
-
)
|
|
1826
|
-
|
|
1827
|
-
arg_name 'SEARCH_PATTERN'
|
|
1828
|
-
command %i[grep search] do |c|
|
|
1829
|
-
c.example 'doing grep "doing wiki"', desc: 'Find entries containing "doing wiki" using fuzzy matching'
|
|
1830
|
-
c.example 'doing search "\'search command"', desc: 'Find entries containing "search command" using exact matching (search is an alias for grep)'
|
|
1831
|
-
c.example 'doing grep "/do.*?wiki.*?@done/"', desc: 'Find entries matching regular expression'
|
|
1832
|
-
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'
|
|
1833
|
-
|
|
1834
|
-
c.desc 'Section'
|
|
1835
|
-
c.arg_name 'NAME'
|
|
1836
|
-
c.flag %i[s section], default_value: 'All'
|
|
1837
|
-
|
|
1838
|
-
c.desc 'Search entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1839
|
-
c.arg_name 'DATE_STRING'
|
|
1840
|
-
c.flag [:before]
|
|
1841
|
-
|
|
1842
|
-
c.desc 'Search entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1843
|
-
c.arg_name 'DATE_STRING'
|
|
1844
|
-
c.flag [:after]
|
|
1845
|
-
|
|
1846
|
-
c.desc %(
|
|
1847
|
-
Date range to show, or a single day to filter date on.
|
|
1848
|
-
Date range argument should be quoted. Date specifications can be natural language.
|
|
1849
|
-
To specify a range, use "to" or "through": `doing search --from "monday 8am to friday 5pm"`.
|
|
1850
|
-
|
|
1851
|
-
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
|
1852
|
-
by time of day.
|
|
1853
|
-
)
|
|
1854
|
-
c.arg_name 'DATE_OR_RANGE'
|
|
1855
|
-
c.flag [:from]
|
|
1642
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
|
1643
|
+
c.arg_name 'TAG'
|
|
1644
|
+
c.flag [:tag]
|
|
1856
1645
|
|
|
1857
|
-
c.desc
|
|
1858
|
-
c.arg_name '
|
|
1859
|
-
c.flag
|
|
1646
|
+
c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
|
|
1647
|
+
c.arg_name 'BOOLEAN'
|
|
1648
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
1860
1649
|
|
|
1861
|
-
c.desc '
|
|
1862
|
-
c.
|
|
1650
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
|
1651
|
+
c.arg_name 'QUERY'
|
|
1652
|
+
c.flag [:search]
|
|
1863
1653
|
|
|
1864
|
-
c.desc 'Show elapsed time
|
|
1654
|
+
c.desc 'Show elapsed time if entry is not tagged @done'
|
|
1865
1655
|
c.switch [:duration]
|
|
1866
1656
|
|
|
1867
|
-
c.desc 'Show intervals with totals at the end of output'
|
|
1868
|
-
c.switch [:totals], default_value: false, negatable: false
|
|
1869
|
-
|
|
1870
|
-
c.desc 'Sort tags by (name|time)'
|
|
1871
|
-
default = 'time'
|
|
1872
|
-
default = settings['tag_sort'] || 'name'
|
|
1873
|
-
c.arg_name 'KEY'
|
|
1874
|
-
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
1875
|
-
|
|
1876
|
-
c.desc 'Only show items with recorded time intervals'
|
|
1877
|
-
c.switch [:only_timed], default_value: false, negatable: false
|
|
1878
|
-
|
|
1879
1657
|
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
1880
1658
|
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
1881
1659
|
|
|
1882
|
-
c.desc 'Force exact string matching (case sensitive)'
|
|
1660
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
1883
1661
|
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
1884
1662
|
|
|
1885
|
-
c.desc 'Show items that *don\'t* match search string'
|
|
1663
|
+
c.desc 'Show items that *don\'t* match search string or tag filter'
|
|
1886
1664
|
c.switch [:not], default_value: false, negatable: false
|
|
1887
1665
|
|
|
1888
1666
|
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1889
1667
|
c.arg_name 'TYPE'
|
|
1890
1668
|
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1891
1669
|
|
|
1892
|
-
c.
|
|
1893
|
-
c.switch %i[i interactive], default_value: false, negatable: false
|
|
1894
|
-
|
|
1895
|
-
c.action do |_global_options, options, args|
|
|
1670
|
+
c.action do |global_options, options, _args|
|
|
1896
1671
|
options[:fuzzy] = false
|
|
1897
|
-
raise
|
|
1898
|
-
|
|
1899
|
-
template = settings['templates']['default'].deep_merge(settings)
|
|
1900
|
-
tags_color = template.key?('tags_color') ? template['tags_color'] : nil
|
|
1672
|
+
raise InvalidArgument, '--tag and --search can not be used together' if options[:tag] && options[:search]
|
|
1901
1673
|
|
|
1902
|
-
|
|
1674
|
+
if options[:tag].nil?
|
|
1675
|
+
options[:tag] = []
|
|
1676
|
+
else
|
|
1677
|
+
options[:tag] = options[:tag].to_tags
|
|
1678
|
+
options[:bool] = options[:bool].normalize_bool
|
|
1679
|
+
end
|
|
1903
1680
|
|
|
1904
1681
|
options[:case] = options[:case].normalize_case
|
|
1905
1682
|
|
|
1906
|
-
search =
|
|
1907
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
1908
|
-
|
|
1909
|
-
options[:times] = true if options[:totals]
|
|
1910
|
-
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
1911
|
-
options[:highlight] = true
|
|
1912
|
-
options[:search] = search
|
|
1913
|
-
options[:section] = section
|
|
1914
|
-
options[:tags_color] = tags_color
|
|
1683
|
+
options[:search] = options[:search].sub(/^'?/, "'") if options[:search] && options[:exact]
|
|
1915
1684
|
|
|
1916
|
-
|
|
1685
|
+
if options[:editor]
|
|
1686
|
+
wwid.edit_last(section: options[:section],
|
|
1687
|
+
options: {
|
|
1688
|
+
search: search,
|
|
1689
|
+
fuzzy: options[:fuzzy],
|
|
1690
|
+
case: options[:case],
|
|
1691
|
+
tag: tags,
|
|
1692
|
+
tag_bool: options[:bool],
|
|
1693
|
+
not: options[:not]
|
|
1694
|
+
})
|
|
1695
|
+
else
|
|
1696
|
+
last = wwid.last(times: true, section: options[:section],
|
|
1697
|
+
options: {
|
|
1698
|
+
duration: options[:duration],
|
|
1699
|
+
search: options[:search],
|
|
1700
|
+
fuzzy: options[:fuzzy],
|
|
1701
|
+
case: options[:case],
|
|
1702
|
+
negate: options[:not],
|
|
1703
|
+
tag: options[:tag],
|
|
1704
|
+
tag_bool: options[:bool],
|
|
1705
|
+
delete: options[:delete]
|
|
1706
|
+
})
|
|
1707
|
+
Doing::Pager::page last.strip if last
|
|
1708
|
+
end
|
|
1917
1709
|
end
|
|
1918
1710
|
end
|
|
1919
1711
|
|
|
1712
|
+
# @@recent
|
|
1920
1713
|
desc 'List recent entries'
|
|
1921
1714
|
default_value 10
|
|
1922
1715
|
arg_name 'COUNT'
|
|
@@ -1985,76 +1778,82 @@ command :recent do |c|
|
|
|
1985
1778
|
end
|
|
1986
1779
|
end
|
|
1987
1780
|
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
|
|
1994
|
-
c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
|
|
1995
|
-
c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
|
|
1996
|
-
|
|
1997
|
-
c.desc 'Specify a section'
|
|
1998
|
-
c.arg_name 'NAME'
|
|
1999
|
-
c.flag %i[s section], default_value: 'All'
|
|
1781
|
+
# @@show
|
|
1782
|
+
desc 'List all entries'
|
|
1783
|
+
long_desc %(
|
|
1784
|
+
The argument can be a section name, @tag(s) or both.
|
|
1785
|
+
"pick" or "choose" as an argument will offer a section menu. Run with `--menu` to get a menu of available tags.
|
|
2000
1786
|
|
|
2001
|
-
|
|
2002
|
-
|
|
1787
|
+
Show tags by passing @tagname arguments. Multiple tags can be combined, and you can specify the boolean used to
|
|
1788
|
+
combine them with `--bool (AND|OR|NOT)`. You can also use @+tagname to require a tag to match, or @-tagname to ignore
|
|
1789
|
+
entries containing tagname. +/- operators require `--bool PATTERN` (which is the default).
|
|
1790
|
+
)
|
|
1791
|
+
arg_name '[SECTION|@TAGS]'
|
|
1792
|
+
command :show do |c|
|
|
1793
|
+
c.example 'doing show Currently', desc: 'Show entries in the Currently section'
|
|
1794
|
+
c.example 'doing show @project1', desc: 'Show entries tagged @project1'
|
|
1795
|
+
c.example 'doing show Later @doing', desc: 'Show entries from the Later section tagged @doing'
|
|
1796
|
+
c.example 'doing show @oracle @writing --bool and', desc: 'Show entries tagged both @oracle and @writing'
|
|
1797
|
+
c.example 'doing show Currently @devo --bool not', desc: 'Show entries in Currently NOT tagged @devo'
|
|
1798
|
+
c.example 'doing show Ideas @doing --from "mon to fri"', desc: 'Show entries tagged @doing from the Ideas section added between monday and friday of the current week.'
|
|
1799
|
+
c.example 'doing show --interactive Later @doing', desc: 'Create a menu from entries from the Later section tagged @doing to perform batch actions'
|
|
2003
1800
|
|
|
2004
|
-
c.desc '
|
|
2005
|
-
c.
|
|
1801
|
+
c.desc 'Tag filter, combine multiple tags with a comma. Use `--tag pick` for a menu of available tags. Wildcards allowed (*, ?). Added for compatibility with other commands.'
|
|
1802
|
+
c.arg_name 'TAG'
|
|
1803
|
+
c.flag [:tag]
|
|
2006
1804
|
|
|
2007
|
-
c.desc '
|
|
2008
|
-
c.
|
|
1805
|
+
c.desc 'Tag boolean (AND,OR,NOT). Use PATTERN to parse + and - as booleans.'
|
|
1806
|
+
c.arg_name 'BOOLEAN'
|
|
1807
|
+
c.flag %i[b bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
2009
1808
|
|
|
2010
|
-
c.desc '
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
c.arg_name 'KEY'
|
|
2014
|
-
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
1809
|
+
c.desc 'Max count to show'
|
|
1810
|
+
c.arg_name 'MAX'
|
|
1811
|
+
c.flag %i[c count], default_value: 0, must_match: /^\d+$/, type: Integer
|
|
2015
1812
|
|
|
2016
|
-
c.desc
|
|
2017
|
-
c.arg_name '
|
|
2018
|
-
c.flag %i[
|
|
1813
|
+
c.desc 'Age (oldest|newest)'
|
|
1814
|
+
c.arg_name 'AGE'
|
|
1815
|
+
c.flag %i[a age], default_value: 'newest'
|
|
2019
1816
|
|
|
2020
|
-
c.desc '
|
|
2021
|
-
c.arg_name '
|
|
1817
|
+
c.desc 'Show entries older than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1818
|
+
c.arg_name 'DATE_STRING'
|
|
2022
1819
|
c.flag [:before]
|
|
2023
1820
|
|
|
2024
|
-
c.desc '
|
|
2025
|
-
c.arg_name '
|
|
1821
|
+
c.desc 'Show entries newer than date. If this is only a time (8am, 1:30pm, 15:00), all dates will be included, but entries will be filtered by time of day.'
|
|
1822
|
+
c.arg_name 'DATE_STRING'
|
|
2026
1823
|
c.flag [:after]
|
|
2027
1824
|
|
|
2028
1825
|
c.desc %(
|
|
2029
|
-
|
|
2030
|
-
|
|
1826
|
+
Date range to show, or a single day to filter date on.
|
|
1827
|
+
Date range argument should be quoted. Date specifications can be natural language.
|
|
1828
|
+
To specify a range, use "to" or "through": `doing show --from "monday 8am to friday 5pm"`.
|
|
1829
|
+
|
|
1830
|
+
If values are only time(s) (6am to noon) all dates will be included, but entries will be filtered
|
|
1831
|
+
by time of day.
|
|
1832
|
+
)
|
|
1833
|
+
|
|
2031
1834
|
c.arg_name 'DATE_OR_RANGE'
|
|
2032
1835
|
c.flag [:from]
|
|
2033
1836
|
|
|
2034
|
-
c.
|
|
2035
|
-
|
|
1837
|
+
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
|
1838
|
+
c.arg_name 'QUERY'
|
|
1839
|
+
c.flag [:search]
|
|
2036
1840
|
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
|
|
1841
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
1842
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
2040
1843
|
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
end
|
|
1844
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
1845
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
2044
1846
|
|
|
2045
|
-
desc '
|
|
2046
|
-
|
|
2047
|
-
and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
|
|
2048
|
-
it will create a range.)
|
|
2049
|
-
arg_name 'DATE_STRING'
|
|
2050
|
-
command :on do |c|
|
|
2051
|
-
c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
|
|
2052
|
-
c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
|
|
2053
|
-
c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
|
|
1847
|
+
c.desc 'Show items that *don\'t* match search/tag/date filters'
|
|
1848
|
+
c.switch [:not], default_value: false, negatable: false
|
|
2054
1849
|
|
|
2055
|
-
c.desc '
|
|
2056
|
-
c.arg_name '
|
|
2057
|
-
c.flag
|
|
1850
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
1851
|
+
c.arg_name 'TYPE'
|
|
1852
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
1853
|
+
|
|
1854
|
+
c.desc 'Sort order (asc/desc)'
|
|
1855
|
+
c.arg_name 'ORDER'
|
|
1856
|
+
c.flag %i[s sort], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
2058
1857
|
|
|
2059
1858
|
c.desc 'Show time intervals on @done tasks'
|
|
2060
1859
|
c.switch %i[t times], default_value: true, negatable: true
|
|
@@ -2062,121 +1861,234 @@ command :on do |c|
|
|
|
2062
1861
|
c.desc 'Show elapsed time on entries without @done tag'
|
|
2063
1862
|
c.switch [:duration]
|
|
2064
1863
|
|
|
2065
|
-
c.desc 'Show
|
|
1864
|
+
c.desc 'Show intervals with totals at the end of output'
|
|
2066
1865
|
c.switch [:totals], default_value: false, negatable: false
|
|
2067
1866
|
|
|
2068
1867
|
c.desc 'Sort tags by (name|time)'
|
|
2069
1868
|
default = 'time'
|
|
2070
1869
|
default = settings['tag_sort'] || 'name'
|
|
2071
1870
|
c.arg_name 'KEY'
|
|
2072
|
-
c.flag [:tag_sort], must_match: /^(?:name|time)
|
|
1871
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)/i, default_value: default
|
|
1872
|
+
|
|
1873
|
+
c.desc 'Tag sort direction (asc|desc)'
|
|
1874
|
+
c.arg_name 'DIRECTION'
|
|
1875
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
1876
|
+
|
|
1877
|
+
c.desc 'Only show items with recorded time intervals'
|
|
1878
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
|
1879
|
+
|
|
1880
|
+
c.desc 'Select section or tag to display from a menu'
|
|
1881
|
+
c.switch %i[m menu], negatable: false, default_value: false
|
|
1882
|
+
|
|
1883
|
+
c.desc 'Select from a menu of matching entries to perform additional operations'
|
|
1884
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
2073
1885
|
|
|
2074
1886
|
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2075
1887
|
c.arg_name 'FORMAT'
|
|
2076
1888
|
c.flag %i[o output]
|
|
2077
|
-
|
|
2078
|
-
|
|
1889
|
+
c.action do |global_options, options, args|
|
|
1890
|
+
options[:fuzzy] = false
|
|
2079
1891
|
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
2080
1892
|
|
|
2081
|
-
|
|
1893
|
+
tag_filter = false
|
|
1894
|
+
tags = []
|
|
2082
1895
|
|
|
2083
|
-
|
|
1896
|
+
if args.length.positive?
|
|
1897
|
+
case args[0]
|
|
1898
|
+
when /^all$/i
|
|
1899
|
+
section = 'All'
|
|
1900
|
+
args.shift
|
|
1901
|
+
when /^(choose|pick)$/i
|
|
1902
|
+
section = wwid.choose_section(include_all: true)
|
|
2084
1903
|
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
1904
|
+
args.shift
|
|
1905
|
+
when /^[@+-]/
|
|
1906
|
+
section = 'All'
|
|
1907
|
+
else
|
|
1908
|
+
begin
|
|
1909
|
+
section = wwid.guess_section(args[0])
|
|
1910
|
+
rescue WrongCommand => exception
|
|
1911
|
+
cmd = commands[:view]
|
|
1912
|
+
action = cmd.send(:get_action, nil)
|
|
1913
|
+
return action.call(global_options, options, args)
|
|
1914
|
+
end
|
|
1915
|
+
|
|
1916
|
+
raise InvalidSection, "No such section: #{args[0]}" unless section
|
|
1917
|
+
|
|
1918
|
+
args.shift
|
|
1919
|
+
end
|
|
1920
|
+
if args.length.positive?
|
|
1921
|
+
args.each do |arg|
|
|
1922
|
+
arg.split(/,/).each do |tag|
|
|
1923
|
+
tags.push(tag.strip.sub(/^@/, ''))
|
|
1924
|
+
end
|
|
1925
|
+
end
|
|
1926
|
+
end
|
|
2089
1927
|
else
|
|
2090
|
-
|
|
2091
|
-
|
|
1928
|
+
section = options[:menu] ? wwid.choose_section(include_all: true) : settings['current_section']
|
|
1929
|
+
section ||= 'All'
|
|
2092
1930
|
end
|
|
2093
1931
|
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
message = "date interpreted as #{start}"
|
|
2097
|
-
message += " to #{finish}" if finish
|
|
2098
|
-
Doing.logger.debug('Interpreter:', message)
|
|
1932
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
|
2099
1933
|
|
|
2100
1934
|
options[:times] = true if options[:totals]
|
|
2101
|
-
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2102
1935
|
|
|
2103
|
-
|
|
2104
|
-
|
|
1936
|
+
template = settings['templates']['default'].deep_merge({
|
|
1937
|
+
'wrap_width' => settings['wrap_width'] || 0,
|
|
1938
|
+
'date_format' => settings['default_date_format'],
|
|
1939
|
+
'order' => settings['order'] || 'asc',
|
|
1940
|
+
'tags_color' => settings['tags_color']
|
|
1941
|
+
})
|
|
1942
|
+
|
|
1943
|
+
options[:case] = options[:case].normalize_case
|
|
1944
|
+
|
|
1945
|
+
if options[:search]
|
|
1946
|
+
search = options[:search]
|
|
1947
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
1948
|
+
options[:search] = search
|
|
1949
|
+
end
|
|
1950
|
+
|
|
1951
|
+
options[:section] = section
|
|
1952
|
+
|
|
1953
|
+
unless tags.empty?
|
|
1954
|
+
tag_filter = {
|
|
1955
|
+
'tags' => tags,
|
|
1956
|
+
'bool' => options[:bool].normalize_bool
|
|
1957
|
+
}
|
|
1958
|
+
end
|
|
1959
|
+
|
|
1960
|
+
options[:tag_filter] = tag_filter
|
|
1961
|
+
options[:tag] = nil
|
|
1962
|
+
|
|
1963
|
+
items = wwid.filter_items([], opt: options)
|
|
1964
|
+
|
|
1965
|
+
if options[:menu]
|
|
1966
|
+
tag = wwid.choose_tag(section, items: items, include_all: true)
|
|
1967
|
+
raise UserCancelled unless tag
|
|
1968
|
+
|
|
1969
|
+
# options[:bool] = :and unless tags.empty?
|
|
1970
|
+
|
|
1971
|
+
tags = tag.split(/ +/).map { |t| t.strip.sub(/^@?/, '') } if tag =~ /^@/
|
|
1972
|
+
unless tags.empty?
|
|
1973
|
+
tag_filter = {
|
|
1974
|
+
'tags' => tags,
|
|
1975
|
+
'bool' => options[:bool].normalize_bool
|
|
1976
|
+
}
|
|
1977
|
+
options[:tag_filter] = tag_filter
|
|
1978
|
+
end
|
|
1979
|
+
end
|
|
1980
|
+
|
|
1981
|
+
options[:age] ||= :newest
|
|
1982
|
+
|
|
1983
|
+
opt = options.dup
|
|
1984
|
+
opt[:age] = options[:age].normalize_age(:newest) if options[:age]
|
|
1985
|
+
opt[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
1986
|
+
opt[:count] = options[:count].to_i
|
|
1987
|
+
opt[:highlight] = true
|
|
1988
|
+
opt[:order] = options[:sort].normalize_order
|
|
1989
|
+
opt[:tag] = nil
|
|
1990
|
+
opt[:tag_order] = options[:tag_order].normalize_order
|
|
1991
|
+
opt[:tags_color] = template['tags_color']
|
|
1992
|
+
|
|
1993
|
+
Doing::Pager.page wwid.list_section(opt, items: items)
|
|
2105
1994
|
end
|
|
2106
1995
|
end
|
|
2107
1996
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
arg_name 'DATE_STRING'
|
|
2112
|
-
command :since do |c|
|
|
2113
|
-
c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
|
|
2114
|
-
c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
|
|
2115
|
-
|
|
1997
|
+
# @@tags
|
|
1998
|
+
desc 'List all tags in the current Doing file'
|
|
1999
|
+
command :tags do |c|
|
|
2116
2000
|
c.desc 'Section'
|
|
2117
|
-
c.arg_name '
|
|
2001
|
+
c.arg_name 'SECTION_NAME'
|
|
2118
2002
|
c.flag %i[s section], default_value: 'All'
|
|
2119
2003
|
|
|
2120
|
-
c.desc 'Show
|
|
2121
|
-
c.switch %i[
|
|
2004
|
+
c.desc 'Show count of occurrences'
|
|
2005
|
+
c.switch %i[c counts]
|
|
2122
2006
|
|
|
2123
|
-
c.desc '
|
|
2124
|
-
c.
|
|
2007
|
+
c.desc 'Sort by name or count'
|
|
2008
|
+
c.arg_name 'SORT_ORDER'
|
|
2009
|
+
c.flag %i[sort], default_value: 'name', must_match: /^(?:n(?:ame)?|c(?:ount)?)$/
|
|
2125
2010
|
|
|
2126
|
-
c.desc '
|
|
2127
|
-
c.
|
|
2011
|
+
c.desc 'Sort order (asc/desc)'
|
|
2012
|
+
c.arg_name 'ORDER'
|
|
2013
|
+
c.flag %i[o order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
2128
2014
|
|
|
2129
|
-
c.desc '
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
c.arg_name 'KEY'
|
|
2133
|
-
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
2015
|
+
c.desc 'Get tags for entries matching tags. Combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
|
2016
|
+
c.arg_name 'TAG'
|
|
2017
|
+
c.flag [:tag]
|
|
2134
2018
|
|
|
2135
|
-
c.desc
|
|
2136
|
-
|
|
2137
|
-
c.
|
|
2019
|
+
c.desc 'Get tags for items matching search. Surround with
|
|
2020
|
+
slashes for regex (e.g. "/query/"), start with a single quote for exact match ("\'query").'
|
|
2021
|
+
c.arg_name 'QUERY'
|
|
2022
|
+
c.flag [:search]
|
|
2138
2023
|
|
|
2139
|
-
c.
|
|
2140
|
-
|
|
2024
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
2025
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
2141
2026
|
|
|
2142
|
-
|
|
2027
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
2028
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
2143
2029
|
|
|
2144
|
-
|
|
2030
|
+
c.desc 'Get tags from items that *don\'t* match search/tag filters'
|
|
2031
|
+
c.switch [:not], default_value: false, negatable: false
|
|
2145
2032
|
|
|
2146
|
-
|
|
2147
|
-
|
|
2033
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
2034
|
+
c.arg_name 'TYPE'
|
|
2035
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
2148
2036
|
|
|
2149
|
-
|
|
2150
|
-
|
|
2037
|
+
c.desc 'Boolean used to combine multiple tags. Use PATTERN to parse + and - as booleans.'
|
|
2038
|
+
c.arg_name 'BOOLEAN'
|
|
2039
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
2151
2040
|
|
|
2152
|
-
|
|
2041
|
+
c.desc 'Select items to scan from a menu of matching entries'
|
|
2042
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
2153
2043
|
|
|
2154
|
-
|
|
2044
|
+
c.action do |_global, options, args|
|
|
2045
|
+
section = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
2155
2046
|
|
|
2156
|
-
|
|
2157
|
-
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2047
|
+
items = wwid.filter_items([], opt: options)
|
|
2158
2048
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2049
|
+
if options[:interactive]
|
|
2050
|
+
items = Doing::Prompt.choose_from_items(items, include_section: options[:section].nil?,
|
|
2051
|
+
menu: true,
|
|
2052
|
+
header: '',
|
|
2053
|
+
prompt: 'Select entries to scan > ',
|
|
2054
|
+
multiple: true,
|
|
2055
|
+
sort: true,
|
|
2056
|
+
show_if_single: true)
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
# items = wwid.content.in_section(section)
|
|
2060
|
+
tags = wwid.all_tags(items, counts: true)
|
|
2061
|
+
|
|
2062
|
+
if options[:sort] =~ /^n/i
|
|
2063
|
+
tags = tags.sort_by { |tag, count| tag }
|
|
2064
|
+
else
|
|
2065
|
+
tags = tags.sort_by { |tag, count| count }
|
|
2066
|
+
end
|
|
2067
|
+
|
|
2068
|
+
tags.reverse! if options[:order].normalize_order == 'desc'
|
|
2069
|
+
|
|
2070
|
+
if options[:counts]
|
|
2071
|
+
tags.each { |t, c| puts "#{t} (#{c})" }
|
|
2072
|
+
else
|
|
2073
|
+
tags.each { |t, c| puts "#{t}" }
|
|
2074
|
+
end
|
|
2161
2075
|
end
|
|
2162
2076
|
end
|
|
2163
2077
|
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
c.example 'doing
|
|
2170
|
-
c.example 'doing
|
|
2078
|
+
# @@today
|
|
2079
|
+
desc 'List entries from today'
|
|
2080
|
+
long_desc 'List entries from the current day. Use --before, --after, and
|
|
2081
|
+
--from to specify time ranges.'
|
|
2082
|
+
command :today do |c|
|
|
2083
|
+
c.example 'doing today', desc: 'List all entries with start dates between 12am and 11:59PM for the current day'
|
|
2084
|
+
c.example 'doing today --section Later', desc: 'List today\'s entries in the Later section'
|
|
2085
|
+
c.example 'doing today --before 3pm --after 12pm', desc: 'List entries with start dates between 12pm and 3pm today'
|
|
2086
|
+
c.example 'doing today --output json', desc: 'Output entries from today in JSON format'
|
|
2171
2087
|
|
|
2172
2088
|
c.desc 'Specify a section'
|
|
2173
2089
|
c.arg_name 'NAME'
|
|
2174
2090
|
c.flag %i[s section], default_value: 'All'
|
|
2175
2091
|
|
|
2176
|
-
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2177
|
-
c.arg_name 'FORMAT'
|
|
2178
|
-
c.flag %i[o output]
|
|
2179
|
-
|
|
2180
2092
|
c.desc 'Show time intervals on @done tasks'
|
|
2181
2093
|
c.switch %i[t times], default_value: true, negatable: true
|
|
2182
2094
|
|
|
@@ -2187,10 +2099,15 @@ command :yesterday do |c|
|
|
|
2187
2099
|
c.switch [:totals], default_value: false, negatable: false
|
|
2188
2100
|
|
|
2189
2101
|
c.desc 'Sort tags by (name|time)'
|
|
2102
|
+
default = 'time'
|
|
2190
2103
|
default = settings['tag_sort'] || 'name'
|
|
2191
2104
|
c.arg_name 'KEY'
|
|
2192
2105
|
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
2193
2106
|
|
|
2107
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2108
|
+
c.arg_name 'FORMAT'
|
|
2109
|
+
c.flag %i[o output]
|
|
2110
|
+
|
|
2194
2111
|
c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
|
|
2195
2112
|
c.arg_name 'TIME_STRING'
|
|
2196
2113
|
c.flag [:before]
|
|
@@ -2200,223 +2117,144 @@ command :yesterday do |c|
|
|
|
2200
2117
|
c.flag [:after]
|
|
2201
2118
|
|
|
2202
2119
|
c.desc %(
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
c.arg_name '
|
|
2120
|
+
Time range to show `doing today --from "12pm to 4pm"`
|
|
2121
|
+
)
|
|
2122
|
+
c.arg_name 'DATE_OR_RANGE'
|
|
2206
2123
|
c.flag [:from]
|
|
2207
2124
|
|
|
2208
|
-
c.desc 'Tag sort direction (asc|desc)'
|
|
2209
|
-
c.arg_name 'DIRECTION'
|
|
2210
|
-
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
2211
|
-
|
|
2212
2125
|
c.action do |_global_options, options, _args|
|
|
2213
2126
|
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
2214
2127
|
|
|
2128
|
+
options[:times] = true if options[:totals]
|
|
2215
2129
|
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2130
|
+
filter_options = %i[after before duration from section sort_tags totals].each_with_object({}) { |k, hsh| hsh[k] = options[k] }
|
|
2216
2131
|
|
|
2217
|
-
|
|
2218
|
-
options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
|
|
2219
|
-
"yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
|
|
2220
|
-
end.join(' to ')
|
|
2221
|
-
end
|
|
2222
|
-
|
|
2223
|
-
opt = {
|
|
2224
|
-
after: options[:after],
|
|
2225
|
-
before: options[:before],
|
|
2226
|
-
duration: options[:duration],
|
|
2227
|
-
from: options[:from],
|
|
2228
|
-
sort_tags: options[:sort_tags],
|
|
2229
|
-
tag_order: options[:tag_order].normalize_order,
|
|
2230
|
-
totals: options[:totals],
|
|
2231
|
-
order: settings.dig('templates', 'today', 'order')
|
|
2232
|
-
}
|
|
2233
|
-
Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
|
|
2132
|
+
Doing::Pager.page wwid.today(options[:times], options[:output], filter_options).chomp
|
|
2234
2133
|
end
|
|
2235
2134
|
end
|
|
2236
2135
|
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
c.example 'doing
|
|
2245
|
-
c.example 'doing
|
|
2246
|
-
c.example 'doing
|
|
2247
|
-
c.example 'doing last --search "\'side hustle" --edit', desc: 'Open most recent entry containing "side hustle" (exact match) in editor'
|
|
2136
|
+
# @on
|
|
2137
|
+
desc 'List entries for a date'
|
|
2138
|
+
long_desc %(Date argument can be natural language. "thursday" would be interpreted as "last thursday,"
|
|
2139
|
+
and "2d" would be interpreted as "two days ago." If you use "to" or "through" between two dates,
|
|
2140
|
+
it will create a range.)
|
|
2141
|
+
arg_name 'DATE_STRING'
|
|
2142
|
+
command :on do |c|
|
|
2143
|
+
c.example 'doing on friday', desc: 'List entries between 12am and 11:59PM last Friday'
|
|
2144
|
+
c.example 'doing on 12/21/2020', desc: 'List entries from Dec 21, 2020'
|
|
2145
|
+
c.example 'doing on "3d to 1d"', desc: 'List entries added between 3 days ago and 1 day ago'
|
|
2248
2146
|
|
|
2249
|
-
c.desc '
|
|
2147
|
+
c.desc 'Section'
|
|
2250
2148
|
c.arg_name 'NAME'
|
|
2251
2149
|
c.flag %i[s section], default_value: 'All'
|
|
2252
2150
|
|
|
2253
|
-
c.desc
|
|
2254
|
-
c.switch %i[
|
|
2255
|
-
|
|
2256
|
-
c.desc 'Tag filter, combine multiple tags with a comma. Wildcards allowed (*, ?).'
|
|
2257
|
-
c.arg_name 'TAG'
|
|
2258
|
-
c.flag [:tag]
|
|
2259
|
-
|
|
2260
|
-
c.desc 'Tag boolean (AND|OR|NOT). Use PATTERN to parse + and - as booleans.'
|
|
2261
|
-
c.arg_name 'BOOLEAN'
|
|
2262
|
-
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
2263
|
-
|
|
2264
|
-
c.desc 'Search filter, surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
|
2265
|
-
c.arg_name 'QUERY'
|
|
2266
|
-
c.flag [:search]
|
|
2151
|
+
c.desc 'Show time intervals on @done tasks'
|
|
2152
|
+
c.switch %i[t times], default_value: true, negatable: true
|
|
2267
2153
|
|
|
2268
|
-
c.desc 'Show elapsed time
|
|
2154
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
|
2269
2155
|
c.switch [:duration]
|
|
2270
2156
|
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
c.desc 'Force exact search string matching (case sensitive)'
|
|
2275
|
-
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
2276
|
-
|
|
2277
|
-
c.desc 'Show items that *don\'t* match search string or tag filter'
|
|
2278
|
-
c.switch [:not], default_value: false, negatable: false
|
|
2279
|
-
|
|
2280
|
-
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
2281
|
-
c.arg_name 'TYPE'
|
|
2282
|
-
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
2157
|
+
c.desc 'Show time totals at the end of output'
|
|
2158
|
+
c.switch [:totals], default_value: false, negatable: false
|
|
2283
2159
|
|
|
2284
|
-
c.
|
|
2285
|
-
|
|
2286
|
-
|
|
2160
|
+
c.desc 'Sort tags by (name|time)'
|
|
2161
|
+
default = 'time'
|
|
2162
|
+
default = settings['tag_sort'] || 'name'
|
|
2163
|
+
c.arg_name 'KEY'
|
|
2164
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
2287
2165
|
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
tags = options[:tag].to_tags
|
|
2292
|
-
options[:bool] = options[:bool].normalize_bool
|
|
2293
|
-
end
|
|
2166
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2167
|
+
c.arg_name 'FORMAT'
|
|
2168
|
+
c.flag %i[o output]
|
|
2294
2169
|
|
|
2295
|
-
|
|
2170
|
+
c.action do |_global_options, options, args|
|
|
2171
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
2296
2172
|
|
|
2297
|
-
|
|
2173
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
|
2298
2174
|
|
|
2299
|
-
|
|
2300
|
-
search = options[:search]
|
|
2301
|
-
search.sub!(/^'?/, "'") if options[:exact]
|
|
2302
|
-
end
|
|
2175
|
+
date_string = args.join(' ')
|
|
2303
2176
|
|
|
2304
|
-
if
|
|
2305
|
-
|
|
2177
|
+
if date_string =~ / (to|through|thru) /
|
|
2178
|
+
dates = date_string.split(/ (to|through|thru) /)
|
|
2179
|
+
start = dates[0].chronify(guess: :begin)
|
|
2180
|
+
finish = dates[2].chronify(guess: :end)
|
|
2306
2181
|
else
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
duration: options[:duration],
|
|
2310
|
-
search: search,
|
|
2311
|
-
fuzzy: options[:fuzzy],
|
|
2312
|
-
case: options[:case],
|
|
2313
|
-
negate: options[:not],
|
|
2314
|
-
tag: tags,
|
|
2315
|
-
tag_bool: options[:bool]
|
|
2316
|
-
}).strip
|
|
2182
|
+
start = date_string.chronify(guess: :begin)
|
|
2183
|
+
finish = false
|
|
2317
2184
|
end
|
|
2318
|
-
end
|
|
2319
|
-
end
|
|
2320
2185
|
|
|
2321
|
-
|
|
2322
|
-
command :sections do |c|
|
|
2323
|
-
c.desc 'List in single column'
|
|
2324
|
-
c.switch %i[c column], negatable: false, default_value: false
|
|
2186
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
2325
2187
|
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
end
|
|
2330
|
-
end
|
|
2188
|
+
message = "date interpreted as #{start}"
|
|
2189
|
+
message += " to #{finish}" if finish
|
|
2190
|
+
Doing.logger.debug('Interpreter:', message)
|
|
2331
2191
|
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
c.action do |_global_options, _options, _args|
|
|
2335
|
-
section = wwid.choose_section
|
|
2192
|
+
options[:times] = true if options[:totals]
|
|
2193
|
+
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2336
2194
|
|
|
2337
|
-
Doing::Pager.page wwid.
|
|
2195
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
|
|
2196
|
+
{ duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
|
2338
2197
|
end
|
|
2339
2198
|
end
|
|
2340
2199
|
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2200
|
+
# @since
|
|
2201
|
+
desc 'List entries since a date'
|
|
2202
|
+
long_desc %(Date argument can be natural language and are always interpreted as being in the past. "thursday" would be interpreted as "last thursday,"
|
|
2203
|
+
and "2d" would be interpreted as "two days ago.")
|
|
2204
|
+
arg_name 'DATE_STRING'
|
|
2205
|
+
command :since do |c|
|
|
2206
|
+
c.example 'doing since 7/30', desc: 'List all entries created since 12am on 7/30 of the current year'
|
|
2207
|
+
c.example 'doing since "monday 3pm" --output json', desc: 'Show entries since 3pm on Monday of the current week, output in JSON format'
|
|
2345
2208
|
|
|
2346
|
-
c.
|
|
2347
|
-
|
|
2209
|
+
c.desc 'Section'
|
|
2210
|
+
c.arg_name 'NAME'
|
|
2211
|
+
c.flag %i[s section], default_value: 'All'
|
|
2348
2212
|
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
end
|
|
2352
|
-
end
|
|
2213
|
+
c.desc 'Show time intervals on @done tasks'
|
|
2214
|
+
c.switch %i[t times], default_value: true, negatable: true
|
|
2353
2215
|
|
|
2354
|
-
desc '
|
|
2355
|
-
|
|
2356
|
-
c.action do |_global_options, _options, _args|
|
|
2357
|
-
bgs = []
|
|
2358
|
-
fgs = []
|
|
2359
|
-
colors::attributes.each do |color|
|
|
2360
|
-
if color.to_s =~ /bg/
|
|
2361
|
-
bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
|
|
2362
|
-
else
|
|
2363
|
-
fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
|
|
2364
|
-
end
|
|
2365
|
-
end
|
|
2366
|
-
out = []
|
|
2367
|
-
out << fgs.join("\n")
|
|
2368
|
-
out << bgs.join("\n")
|
|
2369
|
-
Doing::Pager.page out.join("\n")
|
|
2370
|
-
end
|
|
2371
|
-
end
|
|
2216
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
|
2217
|
+
c.switch [:duration]
|
|
2372
2218
|
|
|
2373
|
-
desc '
|
|
2374
|
-
|
|
2219
|
+
c.desc 'Show time totals at the end of output'
|
|
2220
|
+
c.switch [:totals], default_value: false, negatable: false
|
|
2375
2221
|
|
|
2376
|
-
|
|
2222
|
+
c.desc 'Sort tags by (name|time)'
|
|
2223
|
+
default = 'time'
|
|
2224
|
+
default = settings['tag_sort'] || 'name'
|
|
2225
|
+
c.arg_name 'KEY'
|
|
2226
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
2377
2227
|
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
c.example 'doing plugins', desc: 'List all plugins'
|
|
2382
|
-
c.example 'doing plugins -t import', desc: 'List all import plugins'
|
|
2228
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2229
|
+
c.arg_name 'FORMAT'
|
|
2230
|
+
c.flag %i[o output]
|
|
2383
2231
|
|
|
2384
|
-
c.
|
|
2385
|
-
|
|
2386
|
-
c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
|
|
2232
|
+
c.action do |_global_options, options, args|
|
|
2233
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
2387
2234
|
|
|
2388
|
-
|
|
2389
|
-
c.switch %i[c column], negatable: false, default_value: false
|
|
2235
|
+
raise MissingArgument, 'Missing date argument' if args.empty?
|
|
2390
2236
|
|
|
2391
|
-
|
|
2392
|
-
Doing::Plugins.list_plugins(options)
|
|
2393
|
-
end
|
|
2394
|
-
end
|
|
2237
|
+
date_string = args.join(' ')
|
|
2395
2238
|
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
tab will offer completions of subcommands and their options.'
|
|
2399
|
-
command :completion do |c|
|
|
2400
|
-
c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
|
|
2401
|
-
c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
|
|
2402
|
-
c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
|
|
2403
|
-
c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
|
|
2239
|
+
date_string.sub!(/(day) (\d)/, '\1 at \2')
|
|
2240
|
+
date_string.sub!(/(\d+)d( ago)?/, '\1 days ago')
|
|
2404
2241
|
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
c.flag %i[t type], must_match: /^[bzf](?:[ai]?sh)?$/i, default_value: 'zsh'
|
|
2242
|
+
start = date_string.chronify(guess: :begin)
|
|
2243
|
+
finish = Time.now
|
|
2408
2244
|
|
|
2409
|
-
|
|
2410
|
-
c.arg_name 'PATH'
|
|
2411
|
-
c.flag %i[f file], default_value: 'STDOUT'
|
|
2245
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
2412
2246
|
|
|
2413
|
-
|
|
2414
|
-
script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
|
|
2247
|
+
Doing.logger.debug('Interpreter:', "date interpreted as #{start} through the current time")
|
|
2415
2248
|
|
|
2416
|
-
|
|
2249
|
+
options[:times] = true if options[:totals]
|
|
2250
|
+
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2251
|
+
|
|
2252
|
+
Doing::Pager.page wwid.list_date([start, finish], options[:section], options[:times], options[:output],
|
|
2253
|
+
{ duration: options[:duration], totals: options[:totals], sort_tags: options[:sort_tags] }).chomp
|
|
2417
2254
|
end
|
|
2418
2255
|
end
|
|
2419
2256
|
|
|
2257
|
+
# @@view
|
|
2420
2258
|
desc 'Display a user-created view'
|
|
2421
2259
|
long_desc 'Views are defined in your configuration (use `doing config` to edit).
|
|
2422
2260
|
Command line options override view configuration.'
|
|
@@ -2437,6 +2275,10 @@ command :view do |c|
|
|
|
2437
2275
|
c.arg_name 'FORMAT'
|
|
2438
2276
|
c.flag %i[o output]
|
|
2439
2277
|
|
|
2278
|
+
c.desc 'Age (oldest|newest)'
|
|
2279
|
+
c.arg_name 'AGE'
|
|
2280
|
+
c.flag %i[age], default_value: 'newest'
|
|
2281
|
+
|
|
2440
2282
|
c.desc 'Show time intervals on @done tasks'
|
|
2441
2283
|
c.switch %i[t times], default_value: true, negatable: true
|
|
2442
2284
|
|
|
@@ -2568,89 +2410,507 @@ command :view do |c|
|
|
|
2568
2410
|
end
|
|
2569
2411
|
end
|
|
2570
2412
|
|
|
2571
|
-
# If the -o/--output flag was specified, override any default in the view template
|
|
2572
|
-
options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
|
|
2413
|
+
# If the -o/--output flag was specified, override any default in the view template
|
|
2414
|
+
options[:output] ||= view.key?('output_format') ? view['output_format'] : 'template'
|
|
2415
|
+
|
|
2416
|
+
count = options[:count] ? options[:count] : view.key?('count') ? view['count'] : 10
|
|
2417
|
+
|
|
2418
|
+
section = if options[:section]
|
|
2419
|
+
section
|
|
2420
|
+
else
|
|
2421
|
+
view.key?('section') ? view['section'] : settings['current_section']
|
|
2422
|
+
end
|
|
2423
|
+
order = view.key?('order') ? view['order'].normalize_order : 'asc'
|
|
2424
|
+
|
|
2425
|
+
totals = if options[:totals]
|
|
2426
|
+
true
|
|
2427
|
+
else
|
|
2428
|
+
view.key?('totals') ? view['totals'] : false
|
|
2429
|
+
end
|
|
2430
|
+
tag_order = if options[:tag_order]
|
|
2431
|
+
options[:tag_order].normalize_order
|
|
2432
|
+
else
|
|
2433
|
+
view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
|
|
2434
|
+
end
|
|
2435
|
+
|
|
2436
|
+
options[:times] = true if totals
|
|
2437
|
+
output_format = options[:output]&.downcase || 'template'
|
|
2438
|
+
|
|
2439
|
+
options[:sort_tags] = if options[:tag_sort]
|
|
2440
|
+
options[:tag_sort] =~ /^n/i ? true : false
|
|
2441
|
+
elsif view.key?('tag_sort')
|
|
2442
|
+
view['tag_sort'] =~ /^n/i ? true : false
|
|
2443
|
+
else
|
|
2444
|
+
false
|
|
2445
|
+
end
|
|
2446
|
+
|
|
2447
|
+
%w[before after from duration].each { |k| options[k.to_sym] = view[k] if view.key?(k) && !options[k.to_sym] }
|
|
2448
|
+
|
|
2449
|
+
options[:case] = options[:case].normalize_case
|
|
2450
|
+
|
|
2451
|
+
search = nil
|
|
2452
|
+
|
|
2453
|
+
if options[:search]
|
|
2454
|
+
search = options[:search]
|
|
2455
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
2456
|
+
end
|
|
2457
|
+
|
|
2458
|
+
options[:age] ||= :newest
|
|
2459
|
+
|
|
2460
|
+
opts = options.dup
|
|
2461
|
+
opts[:age] = options[:age].normalize_age(:newest)
|
|
2462
|
+
opts[:view_template] = title
|
|
2463
|
+
opts[:count] = count
|
|
2464
|
+
opts[:format] = date_format
|
|
2465
|
+
opts[:highlight] = options[:color]
|
|
2466
|
+
opts[:only_timed] = only_timed
|
|
2467
|
+
opts[:order] = order
|
|
2468
|
+
opts[:output] = options[:interactive] ? nil : options[:output]
|
|
2469
|
+
opts[:output] = output_format
|
|
2470
|
+
opts[:page_title] = page_title
|
|
2471
|
+
opts[:search] = search
|
|
2472
|
+
opts[:section] = section
|
|
2473
|
+
opts[:tag_filter] = tag_filter
|
|
2474
|
+
opts[:tag_order] = tag_order
|
|
2475
|
+
opts[:tags_color] = tags_color
|
|
2476
|
+
opts[:template] = template
|
|
2477
|
+
opts[:totals] = totals
|
|
2478
|
+
|
|
2479
|
+
Doing::Pager.page wwid.list_section(opts)
|
|
2480
|
+
elsif title.instance_of?(FalseClass)
|
|
2481
|
+
raise UserCancelled, 'Cancelled'
|
|
2482
|
+
else
|
|
2483
|
+
raise InvalidView, "View #{title} not found in config"
|
|
2484
|
+
end
|
|
2485
|
+
end
|
|
2486
|
+
end
|
|
2487
|
+
|
|
2488
|
+
# @@yesterday
|
|
2489
|
+
desc 'List entries from yesterday'
|
|
2490
|
+
long_desc 'Show only entries with start times within the previous 24 hour period. Use --before, --after, and --from to limit to
|
|
2491
|
+
time spans within the day.'
|
|
2492
|
+
command :yesterday do |c|
|
|
2493
|
+
c.example 'doing yesterday', desc: 'List all entries from the previous day'
|
|
2494
|
+
c.example 'doing yesterday --after 8am --before 5pm', desc: 'List entries from the previous day between 8am and 5pm'
|
|
2495
|
+
c.example 'doing yesterday --totals', desc: 'List entries from previous day, including tag timers'
|
|
2496
|
+
|
|
2497
|
+
c.desc 'Specify a section'
|
|
2498
|
+
c.arg_name 'NAME'
|
|
2499
|
+
c.flag %i[s section], default_value: 'All'
|
|
2500
|
+
|
|
2501
|
+
c.desc "Output to export format (#{Doing::Plugins.plugin_names(type: :export)})"
|
|
2502
|
+
c.arg_name 'FORMAT'
|
|
2503
|
+
c.flag %i[o output]
|
|
2504
|
+
|
|
2505
|
+
c.desc 'Show time intervals on @done tasks'
|
|
2506
|
+
c.switch %i[t times], default_value: true, negatable: true
|
|
2507
|
+
|
|
2508
|
+
c.desc 'Show elapsed time on entries without @done tag'
|
|
2509
|
+
c.switch [:duration]
|
|
2510
|
+
|
|
2511
|
+
c.desc 'Show time totals at the end of output'
|
|
2512
|
+
c.switch [:totals], default_value: false, negatable: false
|
|
2513
|
+
|
|
2514
|
+
c.desc 'Sort tags by (name|time)'
|
|
2515
|
+
default = settings['tag_sort'] || 'name'
|
|
2516
|
+
c.arg_name 'KEY'
|
|
2517
|
+
c.flag [:tag_sort], must_match: /^(?:name|time)$/i, default_value: default
|
|
2518
|
+
|
|
2519
|
+
c.desc 'View entries before specified time (e.g. 8am, 12:30pm, 15:00)'
|
|
2520
|
+
c.arg_name 'TIME_STRING'
|
|
2521
|
+
c.flag [:before]
|
|
2522
|
+
|
|
2523
|
+
c.desc 'View entries after specified time (e.g. 8am, 12:30pm, 15:00)'
|
|
2524
|
+
c.arg_name 'TIME_STRING'
|
|
2525
|
+
c.flag [:after]
|
|
2526
|
+
|
|
2527
|
+
c.desc %(
|
|
2528
|
+
Time range to show, e.g. `doing yesterday --from "1am to 8am"`
|
|
2529
|
+
)
|
|
2530
|
+
c.arg_name 'TIME_RANGE'
|
|
2531
|
+
c.flag [:from]
|
|
2532
|
+
|
|
2533
|
+
c.desc 'Tag sort direction (asc|desc)'
|
|
2534
|
+
c.arg_name 'DIRECTION'
|
|
2535
|
+
c.flag [:tag_order], must_match: REGEX_SORT_ORDER, default_value: 'asc'
|
|
2536
|
+
|
|
2537
|
+
c.action do |_global_options, options, _args|
|
|
2538
|
+
raise DoingRuntimeError, %(Invalid output type "#{options[:output]}") if options[:output] && options[:output] !~ Doing::Plugins.plugin_regex(type: :export)
|
|
2539
|
+
|
|
2540
|
+
options[:sort_tags] = options[:tag_sort] =~ /^n/i
|
|
2541
|
+
|
|
2542
|
+
if options[:from]
|
|
2543
|
+
options[:from] = options[:from].split(/ (?:to|through|thru|(?:un)?til|-+) /).map do |time|
|
|
2544
|
+
"yesterday #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}"
|
|
2545
|
+
end.join(' to ')
|
|
2546
|
+
end
|
|
2547
|
+
|
|
2548
|
+
opt = {
|
|
2549
|
+
after: options[:after],
|
|
2550
|
+
before: options[:before],
|
|
2551
|
+
duration: options[:duration],
|
|
2552
|
+
from: options[:from],
|
|
2553
|
+
sort_tags: options[:sort_tags],
|
|
2554
|
+
tag_order: options[:tag_order].normalize_order,
|
|
2555
|
+
totals: options[:totals],
|
|
2556
|
+
order: settings.dig('templates', 'today', 'order')
|
|
2557
|
+
}
|
|
2558
|
+
Doing::Pager.page wwid.yesterday(options[:section], options[:times], options[:output], opt).chomp
|
|
2559
|
+
end
|
|
2560
|
+
end
|
|
2561
|
+
|
|
2562
|
+
## Utility commands
|
|
2563
|
+
|
|
2564
|
+
# @@add_section
|
|
2565
|
+
desc 'Add a new section to the "doing" file'
|
|
2566
|
+
arg_name 'SECTION_NAME'
|
|
2567
|
+
command :add_section do |c|
|
|
2568
|
+
c.example 'doing add_section Ideas', desc: 'Add a section called Ideas to the doing file'
|
|
2569
|
+
|
|
2570
|
+
c.action do |_global_options, _options, args|
|
|
2571
|
+
raise InvalidArgument, "Section #{args[0]} already exists" if wwid.sections.include?(args[0])
|
|
2572
|
+
|
|
2573
|
+
wwid.content.add_section(args.join(' ').cap_first, log: true)
|
|
2574
|
+
wwid.write(wwid.doing_file)
|
|
2575
|
+
end
|
|
2576
|
+
end
|
|
2577
|
+
|
|
2578
|
+
# @@config
|
|
2579
|
+
desc 'Edit the configuration file or output a value from it'
|
|
2580
|
+
long_desc %(Run without arguments, `doing config` opens your `config.yml` in an editor.
|
|
2581
|
+
If local configurations are found in the path between the current directory
|
|
2582
|
+
and the root (/), a menu will allow you to select which to open in the editor.
|
|
2583
|
+
|
|
2584
|
+
It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
|
|
2585
|
+
|
|
2586
|
+
Use `doing config get` to output the configuration to the terminal, and
|
|
2587
|
+
provide a dot-separated key path to get a specific value. Shows the current value
|
|
2588
|
+
including keys/overrides set by local configs.)
|
|
2589
|
+
command :config do |c|
|
|
2590
|
+
c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
|
|
2591
|
+
c.example 'doing config get doing_file', desc: 'Output the value of a config key as YAML'
|
|
2592
|
+
c.example 'doing config get plugins.plugin_path -o json', desc: 'Output the value of a key path as JSON'
|
|
2593
|
+
c.example 'doing config set plugins.say.say_voice Alex', desc: 'Set the value of a key path and update config file'
|
|
2594
|
+
c.example 'doing config set plug.say.voice Zarvox', desc: 'Key paths for get and set are fuzzy matched'
|
|
2595
|
+
|
|
2596
|
+
c.default_command :edit
|
|
2597
|
+
|
|
2598
|
+
c.desc 'DEPRECATED'
|
|
2599
|
+
c.switch %i[d dump]
|
|
2600
|
+
|
|
2601
|
+
c.desc 'DEPRECATED'
|
|
2602
|
+
c.switch %i[u update]
|
|
2603
|
+
|
|
2604
|
+
# @@config.list
|
|
2605
|
+
c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
|
|
2606
|
+
c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
|
|
2607
|
+
Values defined in the top item in the list will override values in configutations below it.'
|
|
2608
|
+
c.command :list do |list|
|
|
2609
|
+
list.action do |global, options, args|
|
|
2610
|
+
puts config.additional_configs.join("\n")
|
|
2611
|
+
puts config.config_file
|
|
2612
|
+
end
|
|
2613
|
+
end
|
|
2614
|
+
|
|
2615
|
+
# @@config.edit
|
|
2616
|
+
c.desc 'Open config file in editor'
|
|
2617
|
+
c.command :edit do |edit|
|
|
2618
|
+
edit.example 'doing config edit', desc: 'Open a config file in the default editor'
|
|
2619
|
+
edit.example 'doing config edit --editor vim', desc: 'Open config in specific editor'
|
|
2620
|
+
|
|
2621
|
+
edit.desc 'Editor to use'
|
|
2622
|
+
edit.arg_name 'EDITOR'
|
|
2623
|
+
edit.flag %i[e editor], default_value: nil
|
|
2624
|
+
|
|
2625
|
+
if `uname` =~ /Darwin/
|
|
2626
|
+
edit.desc 'Application to use'
|
|
2627
|
+
edit.arg_name 'APP_NAME'
|
|
2628
|
+
edit.flag %i[a app]
|
|
2629
|
+
|
|
2630
|
+
edit.desc 'Application bundle id to use'
|
|
2631
|
+
edit.arg_name 'BUNDLE_ID'
|
|
2632
|
+
edit.flag %i[b bundle_id]
|
|
2633
|
+
|
|
2634
|
+
edit.desc "Use the config_editor_app defined in ~/.config/doing/config.yml (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
|
|
2635
|
+
edit.switch %i[x default]
|
|
2636
|
+
end
|
|
2637
|
+
|
|
2638
|
+
edit.action do |global, options, args|
|
|
2639
|
+
if options[:update] || options[:dump]
|
|
2640
|
+
cmd = commands[:config]
|
|
2641
|
+
if options[:update]
|
|
2642
|
+
cmd = cmd.commands[:update]
|
|
2643
|
+
elsif options[:dump]
|
|
2644
|
+
cmd = cmd.commands[:get]
|
|
2645
|
+
end
|
|
2646
|
+
action = cmd.send(:get_action, nil)
|
|
2647
|
+
action.call(global, options, args)
|
|
2648
|
+
Doing.logger.warn('Deprecated:', '--dump and --update are deprecated,
|
|
2649
|
+
use `doing config get` and `doing config update`')
|
|
2650
|
+
Doing.logger.output_results
|
|
2651
|
+
return
|
|
2652
|
+
end
|
|
2653
|
+
|
|
2654
|
+
config_file = config.choose_config
|
|
2655
|
+
|
|
2656
|
+
if `uname` =~ /Darwin/
|
|
2657
|
+
if options[:default]
|
|
2658
|
+
editor = Doing::Util.find_default_editor('config')
|
|
2659
|
+
if editor
|
|
2660
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2661
|
+
system %(#{editor} "#{config_file}")
|
|
2662
|
+
else
|
|
2663
|
+
`open -a "#{editor}" "#{config_file}"`
|
|
2664
|
+
end
|
|
2665
|
+
else
|
|
2666
|
+
raise InvalidArgument, 'No viable editor found in config or environment.'
|
|
2667
|
+
end
|
|
2668
|
+
elsif options[:app] || options[:bundle_id]
|
|
2669
|
+
if options[:app]
|
|
2670
|
+
`open -a "#{options[:app]}" "#{config_file}"`
|
|
2671
|
+
elsif options[:bundle_id]
|
|
2672
|
+
`open -b #{options[:bundle_id]} "#{config_file}"`
|
|
2673
|
+
end
|
|
2674
|
+
else
|
|
2675
|
+
editor = options[:editor] || Doing::Util.find_default_editor('config')
|
|
2676
|
+
|
|
2677
|
+
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
|
2678
|
+
|
|
2679
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2680
|
+
system %(#{editor} "#{config_file}")
|
|
2681
|
+
else
|
|
2682
|
+
`open -a "#{editor}" "#{config_file}"`
|
|
2683
|
+
end
|
|
2684
|
+
end
|
|
2685
|
+
else
|
|
2686
|
+
editor = options[:editor] || Doing::Util.default_editor
|
|
2687
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
|
|
2688
|
+
|
|
2689
|
+
system %(#{editor} "#{config_file}")
|
|
2690
|
+
end
|
|
2691
|
+
end
|
|
2692
|
+
end
|
|
2693
|
+
|
|
2694
|
+
# @@config.update @@config.refresh
|
|
2695
|
+
c.desc 'Update default config file, adding any missing keys'
|
|
2696
|
+
c.command %i[update refresh] do |update|
|
|
2697
|
+
update.action do |_global, options, args|
|
|
2698
|
+
config.configure({rewrite: true, ignore_local: true})
|
|
2699
|
+
Doing.logger.warn('Config:', 'config refreshed')
|
|
2700
|
+
end
|
|
2701
|
+
end
|
|
2702
|
+
|
|
2703
|
+
# @@config.undo
|
|
2704
|
+
c.desc 'Undo the last change to a config file'
|
|
2705
|
+
c.command :undo do |undo|
|
|
2706
|
+
undo.action do |_global, options, args|
|
|
2707
|
+
config_file = config.choose_config
|
|
2708
|
+
Doing::Util::Backup.restore_last_backup(config_file, count: 1)
|
|
2709
|
+
end
|
|
2710
|
+
end
|
|
2711
|
+
|
|
2712
|
+
# @@config.get @@config.dump
|
|
2713
|
+
c.desc 'Output a key\'s value'
|
|
2714
|
+
c.arg 'KEY_PATH'
|
|
2715
|
+
c.command %i[get dump] do |dump|
|
|
2716
|
+
dump.example 'doing config get', desc: 'Output the entire configuration'
|
|
2717
|
+
dump.example 'doing config get timer_format --output raw', desc: 'Output the value of timer_format as a plain string'
|
|
2718
|
+
dump.example 'doing config get doing_file', desc: 'Output the value of the doing_file setting, respecting local configurations'
|
|
2719
|
+
dump.example 'doing config get -o json plug.plugpath', desc: 'Key path is fuzzy matched: output the value of plugins->plugin_path as JSON'
|
|
2720
|
+
|
|
2721
|
+
dump.desc 'Format for output (json|yaml|raw)'
|
|
2722
|
+
dump.arg_name 'FORMAT'
|
|
2723
|
+
dump.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
|
|
2724
|
+
|
|
2725
|
+
dump.action do |_global, options, args|
|
|
2726
|
+
|
|
2727
|
+
keypath = args.join('.')
|
|
2728
|
+
cfg = config.value_for_key(keypath)
|
|
2729
|
+
real_path = config.resolve_key_path(keypath)
|
|
2730
|
+
|
|
2731
|
+
if cfg
|
|
2732
|
+
val = cfg.map {|k, v| v }[0]
|
|
2733
|
+
if real_path.count.positive?
|
|
2734
|
+
nested_cfg = {}
|
|
2735
|
+
nested_cfg.deep_set(real_path, val)
|
|
2736
|
+
else
|
|
2737
|
+
nested_cfg = val
|
|
2738
|
+
end
|
|
2739
|
+
|
|
2740
|
+
if options[:output] =~ /^r/
|
|
2741
|
+
if val.is_a?(Hash)
|
|
2742
|
+
$stdout.puts YAML.dump(val)
|
|
2743
|
+
elsif val.is_a?(Array)
|
|
2744
|
+
$stdout.puts val.join(', ')
|
|
2745
|
+
else
|
|
2746
|
+
$stdout.puts val.to_s
|
|
2747
|
+
end
|
|
2748
|
+
else
|
|
2749
|
+
$stdout.puts case options[:output]
|
|
2750
|
+
when /^j/
|
|
2751
|
+
JSON.pretty_generate(val)
|
|
2752
|
+
else
|
|
2753
|
+
YAML.dump(nested_cfg)
|
|
2754
|
+
end
|
|
2755
|
+
end
|
|
2756
|
+
else
|
|
2757
|
+
Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
|
|
2758
|
+
end
|
|
2759
|
+
Doing.logger.output_results
|
|
2760
|
+
return
|
|
2761
|
+
end
|
|
2762
|
+
end
|
|
2763
|
+
|
|
2764
|
+
# @@config.set
|
|
2765
|
+
c.desc 'Set a key\'s value in the config file'
|
|
2766
|
+
c.arg 'KEY VALUE'
|
|
2767
|
+
c.command :set do |set|
|
|
2768
|
+
set.example 'doing config set timer_format human', desc: 'Set the value of timer_format to "human"'
|
|
2769
|
+
set.example 'doing config set plug.plugpath ~/my_plugins', desc: 'Key path is fuzzy matched: set the value of plugins->plugin_path'
|
|
2770
|
+
|
|
2771
|
+
set.desc 'Delete specified key'
|
|
2772
|
+
set.switch %i[r remove], default_value: false, negatable: false
|
|
2773
|
+
|
|
2774
|
+
set.action do |_global, options, args|
|
|
2775
|
+
if args.count < 2 && !options[:remove]
|
|
2776
|
+
raise InvalidArgument, 'config set requires at least two arguments, key path and value'
|
|
2573
2777
|
|
|
2574
|
-
|
|
2778
|
+
end
|
|
2575
2779
|
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
view.key?('section') ? view['section'] : settings['current_section']
|
|
2580
|
-
end
|
|
2581
|
-
order = view.key?('order') ? view['order'].normalize_order : 'asc'
|
|
2780
|
+
value = options[:remove] ? nil : args.pop
|
|
2781
|
+
keypath = args.join('.')
|
|
2782
|
+
real_path = config.resolve_key_path(keypath, create: true)
|
|
2582
2783
|
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
else
|
|
2586
|
-
view.key?('totals') ? view['totals'] : false
|
|
2587
|
-
end
|
|
2588
|
-
tag_order = if options[:tag_order]
|
|
2589
|
-
options[:tag_order].normalize_order
|
|
2590
|
-
else
|
|
2591
|
-
view.key?('tag_order') ? view['tag_order'].normalize_order : 'asc'
|
|
2592
|
-
end
|
|
2784
|
+
old_value = settings.dig(*real_path) || nil
|
|
2785
|
+
old_type = old_value&.class.to_s || nil
|
|
2593
2786
|
|
|
2594
|
-
options[:
|
|
2595
|
-
|
|
2787
|
+
if old_value.is_a?(Hash) && !options[:remove]
|
|
2788
|
+
Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
|
|
2789
|
+
didyou = 'Did you mean:'
|
|
2790
|
+
old_value.keys.each do |k|
|
|
2791
|
+
Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
|
|
2792
|
+
didyou = '..........or:'
|
|
2793
|
+
end
|
|
2794
|
+
raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
|
|
2596
2795
|
|
|
2597
|
-
|
|
2598
|
-
options[:tag_sort] =~ /^n/i ? true : false
|
|
2599
|
-
elsif view.key?('tag_sort')
|
|
2600
|
-
view['tag_sort'] =~ /^n/i ? true : false
|
|
2601
|
-
else
|
|
2602
|
-
false
|
|
2603
|
-
end
|
|
2796
|
+
end
|
|
2604
2797
|
|
|
2605
|
-
|
|
2798
|
+
config_file = config.choose_config(create: true)
|
|
2606
2799
|
|
|
2607
|
-
|
|
2800
|
+
cfg = YAML.safe_load_file(config_file) || {}
|
|
2608
2801
|
|
|
2609
|
-
|
|
2802
|
+
$stderr.puts "Updating #{config_file}".yellow
|
|
2610
2803
|
|
|
2611
|
-
if options[:
|
|
2612
|
-
|
|
2613
|
-
|
|
2804
|
+
if options[:remove]
|
|
2805
|
+
cfg.deep_set(real_path, nil)
|
|
2806
|
+
$stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
|
|
2807
|
+
else
|
|
2808
|
+
current_value = cfg.dig(*real_path)
|
|
2809
|
+
cfg.deep_set(real_path, value.set_type(old_type))
|
|
2810
|
+
|
|
2811
|
+
$stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
|
|
2812
|
+
$stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
|
|
2813
|
+
$stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
|
|
2814
|
+
$stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
|
|
2614
2815
|
end
|
|
2615
2816
|
|
|
2616
|
-
|
|
2617
|
-
opts[:view_template] = title
|
|
2618
|
-
opts[:count] = count
|
|
2619
|
-
opts[:format] = date_format
|
|
2620
|
-
opts[:highlight] = options[:color]
|
|
2621
|
-
opts[:only_timed] = only_timed
|
|
2622
|
-
opts[:order] = order
|
|
2623
|
-
opts[:output] = options[:interactive] ? nil : options[:output]
|
|
2624
|
-
opts[:output] = output_format
|
|
2625
|
-
opts[:page_title] = page_title
|
|
2626
|
-
opts[:search] = search
|
|
2627
|
-
opts[:section] = section
|
|
2628
|
-
opts[:tag_filter] = tag_filter
|
|
2629
|
-
opts[:tag_order] = tag_order
|
|
2630
|
-
opts[:tags_color] = tags_color
|
|
2631
|
-
opts[:template] = template
|
|
2632
|
-
opts[:totals] = totals
|
|
2817
|
+
res = Doing::Prompt.yn('Update selected config', default_response: true)
|
|
2633
2818
|
|
|
2634
|
-
Doing::Pager.page wwid.list_section(opts)
|
|
2635
|
-
elsif title.instance_of?(FalseClass)
|
|
2636
2819
|
raise UserCancelled, 'Cancelled' unless res
|
|
2637
|
-
|
|
2638
|
-
|
|
2820
|
+
|
|
2821
|
+
Doing::Util.write_to_file(config_file, YAML.dump(cfg), backup: true)
|
|
2822
|
+
Doing.logger.warn('Config:', "#{config_file} updated")
|
|
2639
2823
|
end
|
|
2640
2824
|
end
|
|
2641
2825
|
end
|
|
2642
2826
|
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2827
|
+
# @@open
|
|
2828
|
+
desc 'Open the "doing" file in an editor'
|
|
2829
|
+
long_desc "`doing open` defaults to using the editors->doing_file setting
|
|
2830
|
+
in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
|
|
2831
|
+
command :open do |c|
|
|
2832
|
+
c.example 'doing open', desc: 'Open the doing file in the default editor'
|
|
2833
|
+
c.desc 'Open with editor command (e.g. vim, mate)'
|
|
2834
|
+
c.arg_name 'COMMAND'
|
|
2835
|
+
c.flag %i[e editor]
|
|
2836
|
+
|
|
2837
|
+
if `uname` =~ /Darwin/
|
|
2838
|
+
c.desc 'Open with app name'
|
|
2839
|
+
c.arg_name 'APP_NAME'
|
|
2840
|
+
c.flag %i[a app]
|
|
2841
|
+
|
|
2842
|
+
c.desc 'Open with app bundle id'
|
|
2843
|
+
c.arg_name 'BUNDLE_ID'
|
|
2844
|
+
c.flag %i[b bundle_id]
|
|
2845
|
+
end
|
|
2647
2846
|
|
|
2648
2847
|
c.action do |_global_options, options, _args|
|
|
2649
|
-
|
|
2650
|
-
|
|
2848
|
+
params = options.dup
|
|
2849
|
+
params.delete_if do |k, v|
|
|
2850
|
+
k.instance_of?(String) || v.nil? || v == false
|
|
2851
|
+
end
|
|
2852
|
+
|
|
2853
|
+
if options[:editor]
|
|
2854
|
+
raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
|
|
2855
|
+
|
|
2856
|
+
editor = TTY::Which.which(options[:editor])
|
|
2857
|
+
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2858
|
+
elsif `uname` =~ /Darwin/
|
|
2859
|
+
if options[:app]
|
|
2860
|
+
system %(open -a "#{options[:app]}" "#{File.expand_path(wwid.doing_file)}")
|
|
2861
|
+
elsif options[:bundle_id]
|
|
2862
|
+
system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
|
|
2863
|
+
elsif Doing::Util.find_default_editor('doing_file')
|
|
2864
|
+
editor = Doing::Util.find_default_editor('doing_file')
|
|
2865
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2866
|
+
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2867
|
+
else
|
|
2868
|
+
system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
|
|
2869
|
+
end
|
|
2870
|
+
else
|
|
2871
|
+
system %(open "#{File.expand_path(wwid.doing_file)}")
|
|
2872
|
+
end
|
|
2873
|
+
else
|
|
2874
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
2875
|
+
|
|
2876
|
+
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2877
|
+
end
|
|
2878
|
+
end
|
|
2879
|
+
end
|
|
2880
|
+
|
|
2881
|
+
# @@tag_dir
|
|
2882
|
+
desc 'Set the default tags for the current directory'
|
|
2883
|
+
long_desc 'Adds default_tags to a .doingrc file in the current directory. Any entry created in this directory or its
|
|
2884
|
+
subdirectories will be tagged with the default tags. You can modify these any time using the `config set` commnand or
|
|
2885
|
+
manually editing the .doingrc file.'
|
|
2886
|
+
arg_name 'TAG [TAG..]'
|
|
2887
|
+
command :tag_dir do |c|
|
|
2888
|
+
c.example 'doing tag_dir project1 project2', desc: 'Add @project1 and @project to to any entries in the current directory'
|
|
2889
|
+
c.example 'doing tag_dir --remove', desc: 'Clear the default tags for the directory'
|
|
2890
|
+
|
|
2891
|
+
c.desc 'Remove all default_tags from the local .doingrc'
|
|
2892
|
+
c.switch %i[r remove], negatable: false
|
|
2893
|
+
|
|
2894
|
+
c.action do |global, options, args|
|
|
2895
|
+
tags = args.join(' ').gsub(/ *, */, ' ').split(' ')
|
|
2896
|
+
|
|
2897
|
+
cfg_cmd = commands[:config]
|
|
2898
|
+
set_cmd = cfg_cmd.commands[:set]
|
|
2899
|
+
set_options = {}
|
|
2900
|
+
if options[:remove]
|
|
2901
|
+
set_args = ['default_tags']
|
|
2902
|
+
set_options[:remove] = true
|
|
2903
|
+
else
|
|
2904
|
+
set_args = ['default_tags', tags.join(',')]
|
|
2905
|
+
end
|
|
2906
|
+
action = set_cmd.send(:get_action, nil)
|
|
2907
|
+
return action.call(global, set_options, set_args)
|
|
2651
2908
|
end
|
|
2652
2909
|
end
|
|
2653
2910
|
|
|
2911
|
+
## File handling/batch modification commands
|
|
2912
|
+
|
|
2913
|
+
# @@archive @@move
|
|
2654
2914
|
desc 'Move entries between sections'
|
|
2655
2915
|
long_desc %(Argument can be a section name to move all entries from a section,
|
|
2656
2916
|
or start with an "@" to move entries matching a tag.
|
|
@@ -2720,29 +2980,131 @@ command %i[archive move] do |c|
|
|
|
2720
2980
|
tags = args.length > 1 ? args[1..].map { |t| t.sub(/^@/, '').strip } : []
|
|
2721
2981
|
end
|
|
2722
2982
|
|
|
2723
|
-
raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
|
2724
|
-
|
|
2725
|
-
tags.concat(options[:tag].to_tags) if options[:tag]
|
|
2726
|
-
|
|
2727
|
-
search = nil
|
|
2728
|
-
|
|
2983
|
+
raise InvalidArgument, '--keep and --count can not be used together' if options[:keep] && options[:count]
|
|
2984
|
+
|
|
2985
|
+
tags.concat(options[:tag].to_tags) if options[:tag]
|
|
2986
|
+
|
|
2987
|
+
search = nil
|
|
2988
|
+
|
|
2989
|
+
options[:case] = options[:case].normalize_case
|
|
2990
|
+
|
|
2991
|
+
if options[:search]
|
|
2992
|
+
search = options[:search]
|
|
2993
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
2994
|
+
end
|
|
2995
|
+
|
|
2996
|
+
opts = options.dup
|
|
2997
|
+
opts[:search] = search
|
|
2998
|
+
opts[:bool] = options[:bool].normalize_bool
|
|
2999
|
+
opts[:destination] = options[:to]
|
|
3000
|
+
opts[:tags] = tags
|
|
3001
|
+
|
|
3002
|
+
wwid.archive(section, opts)
|
|
3003
|
+
end
|
|
3004
|
+
end
|
|
3005
|
+
|
|
3006
|
+
# @@import
|
|
3007
|
+
desc 'Import entries from an external source'
|
|
3008
|
+
long_desc "Imports entries from other sources. Available plugins: #{Doing::Plugins.plugin_names(type: :import, separator: ', ')}"
|
|
3009
|
+
arg_name 'PATH'
|
|
3010
|
+
command :import do |c|
|
|
3011
|
+
c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
|
|
3012
|
+
c.example 'doing import --type doing --tag imported --no-autotag ~/doing_backup.md', desc: 'Import an Doing archive, tag all entries with @imported, skip autotagging'
|
|
3013
|
+
c.example 'doing import --type doing --from "10/1 to 10/15" ~/doing_backup.md', desc: 'Import a Doing archive, only importing entries between two dates'
|
|
3014
|
+
|
|
3015
|
+
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
|
3016
|
+
c.arg_name 'TYPE'
|
|
3017
|
+
c.flag :type, default_value: 'doing'
|
|
3018
|
+
|
|
3019
|
+
c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
|
3020
|
+
c.arg_name 'QUERY'
|
|
3021
|
+
c.flag [:search]
|
|
3022
|
+
|
|
3023
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
3024
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
3025
|
+
|
|
3026
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
3027
|
+
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
3028
|
+
|
|
3029
|
+
c.desc 'Import items that *don\'t* match search/tag/date filters'
|
|
3030
|
+
c.switch [:not], default_value: false, negatable: false
|
|
3031
|
+
|
|
3032
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
3033
|
+
c.arg_name 'TYPE'
|
|
3034
|
+
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
3035
|
+
|
|
3036
|
+
c.desc 'Only import items with recorded time intervals'
|
|
3037
|
+
c.switch [:only_timed], default_value: false, negatable: false
|
|
3038
|
+
|
|
3039
|
+
c.desc 'Target section'
|
|
3040
|
+
c.arg_name 'NAME'
|
|
3041
|
+
c.flag %i[s section]
|
|
3042
|
+
|
|
3043
|
+
c.desc 'Tag all imported entries'
|
|
3044
|
+
c.arg_name 'TAGS'
|
|
3045
|
+
c.flag :tag
|
|
3046
|
+
|
|
3047
|
+
c.desc 'Autotag entries'
|
|
3048
|
+
c.switch :autotag, negatable: true, default_value: true
|
|
3049
|
+
|
|
3050
|
+
c.desc 'Prefix entries with'
|
|
3051
|
+
c.arg_name 'PREFIX'
|
|
3052
|
+
c.flag :prefix
|
|
3053
|
+
|
|
3054
|
+
# TODO: Allow time range filtering
|
|
3055
|
+
c.desc 'Import entries older than date'
|
|
3056
|
+
c.arg_name 'DATE_STRING'
|
|
3057
|
+
c.flag [:before]
|
|
3058
|
+
|
|
3059
|
+
c.desc 'Import entries newer than date'
|
|
3060
|
+
c.arg_name 'DATE_STRING'
|
|
3061
|
+
c.flag [:after]
|
|
3062
|
+
|
|
3063
|
+
c.desc %(
|
|
3064
|
+
Date range to import. Date range argument should be quoted. Date specifications can be natural language.
|
|
3065
|
+
To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
|
|
3066
|
+
Has no effect unless the import plugin has implemented date range filtering.
|
|
3067
|
+
)
|
|
3068
|
+
c.arg_name 'DATE_OR_RANGE'
|
|
3069
|
+
c.flag %i[f from]
|
|
3070
|
+
|
|
3071
|
+
c.desc 'Allow entries that overlap existing times'
|
|
3072
|
+
c.switch [:overlap], negatable: true
|
|
3073
|
+
|
|
3074
|
+
c.action do |_global_options, options, args|
|
|
3075
|
+
options[:fuzzy] = false
|
|
3076
|
+
if options[:section]
|
|
3077
|
+
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
3078
|
+
end
|
|
3079
|
+
|
|
3080
|
+
if options[:from]
|
|
3081
|
+
date_string = options[:from]
|
|
3082
|
+
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
|
3083
|
+
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
|
3084
|
+
start = dates[0].chronify(guess: :begin)
|
|
3085
|
+
finish = dates[2].chronify(guess: :end)
|
|
3086
|
+
else
|
|
3087
|
+
start = date_string.chronify(guess: :begin)
|
|
3088
|
+
finish = false
|
|
3089
|
+
end
|
|
3090
|
+
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
3091
|
+
dates = [start, finish]
|
|
3092
|
+
end
|
|
3093
|
+
|
|
2729
3094
|
options[:case] = options[:case].normalize_case
|
|
2730
3095
|
|
|
2731
|
-
if options[:
|
|
2732
|
-
|
|
2733
|
-
|
|
3096
|
+
if options[:type] =~ Doing::Plugins.plugin_regex(type: :import)
|
|
3097
|
+
options[:no_overlap] = !options[:overlap]
|
|
3098
|
+
options[:date_filter] = dates
|
|
3099
|
+
wwid.import(args, options)
|
|
3100
|
+
wwid.write(wwid.doing_file)
|
|
3101
|
+
else
|
|
3102
|
+
raise InvalidPluginType, "Invalid import type: #{options[:type]}"
|
|
2734
3103
|
end
|
|
2735
|
-
|
|
2736
|
-
opts = options.dup
|
|
2737
|
-
opts[:search] = search
|
|
2738
|
-
opts[:bool] = options[:bool].normalize_bool
|
|
2739
|
-
opts[:destination] = options[:to]
|
|
2740
|
-
opts[:tags] = tags
|
|
2741
|
-
|
|
2742
|
-
wwid.archive(section, opts)
|
|
2743
3104
|
end
|
|
2744
3105
|
end
|
|
2745
3106
|
|
|
3107
|
+
# @@rotate
|
|
2746
3108
|
desc 'Move entries to archive file'
|
|
2747
3109
|
long_desc 'As your doing file grows, commands can get slow. Given that your historical data (and your archive section)
|
|
2748
3110
|
probably aren\'t providing any useful insights a year later, use this command to "rotate" old entries out to an archive
|
|
@@ -2812,298 +3174,166 @@ command :rotate do |c|
|
|
|
2812
3174
|
end
|
|
2813
3175
|
end
|
|
2814
3176
|
|
|
2815
|
-
|
|
2816
|
-
long_desc "`doing open` defaults to using the editors->doing_file setting
|
|
2817
|
-
in #{config.config_file} (#{Doing::Util.find_default_editor('doing_file')})."
|
|
2818
|
-
command :open do |c|
|
|
2819
|
-
c.example 'doing open', desc: 'Open the doing file in the default editor'
|
|
2820
|
-
c.desc 'Open with editor command (e.g. vim, mate)'
|
|
2821
|
-
c.arg_name 'COMMAND'
|
|
2822
|
-
c.flag %i[e editor]
|
|
2823
|
-
|
|
2824
|
-
if `uname` =~ /Darwin/
|
|
2825
|
-
c.desc 'Open with app name'
|
|
2826
|
-
c.arg_name 'APP_NAME'
|
|
2827
|
-
c.flag %i[a app]
|
|
2828
|
-
|
|
2829
|
-
c.desc 'Open with app bundle id'
|
|
2830
|
-
c.arg_name 'BUNDLE_ID'
|
|
2831
|
-
c.flag %i[b bundle_id]
|
|
2832
|
-
end
|
|
2833
|
-
|
|
2834
|
-
c.action do |_global_options, options, _args|
|
|
2835
|
-
params = options.dup
|
|
2836
|
-
params.delete_if do |k, v|
|
|
2837
|
-
k.instance_of?(String) || v.nil? || v == false
|
|
2838
|
-
end
|
|
2839
|
-
|
|
2840
|
-
if options[:editor]
|
|
2841
|
-
raise MissingEditor, "Editor #{options[:editor]} not found" unless Doing::Util.exec_available(options[:editor].split(/ /).first)
|
|
2842
|
-
|
|
2843
|
-
editor = TTY::Which.which(options[:editor])
|
|
2844
|
-
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2845
|
-
elsif `uname` =~ /Darwin/
|
|
2846
|
-
if options[:app]
|
|
2847
|
-
system %(open -a "#{options[:app]}" "#{File.expand_path(wwid.doing_file)}")
|
|
2848
|
-
elsif options[:bundle_id]
|
|
2849
|
-
system %(open -b "#{options[:bundle_id]}" "#{File.expand_path(wwid.doing_file)}")
|
|
2850
|
-
elsif Doing::Util.find_default_editor('doing_file')
|
|
2851
|
-
editor = Doing::Util.find_default_editor('doing_file')
|
|
2852
|
-
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2853
|
-
system %(#{editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2854
|
-
else
|
|
2855
|
-
system %(open -a "#{editor}" "#{File.expand_path(wwid.doing_file)}")
|
|
2856
|
-
end
|
|
2857
|
-
else
|
|
2858
|
-
system %(open "#{File.expand_path(wwid.doing_file)}")
|
|
2859
|
-
end
|
|
2860
|
-
else
|
|
2861
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
2862
|
-
|
|
2863
|
-
system %(#{Doing::Util.default_editor} "#{File.expand_path(wwid.doing_file)}")
|
|
2864
|
-
end
|
|
2865
|
-
end
|
|
2866
|
-
end
|
|
2867
|
-
|
|
2868
|
-
desc 'Edit the configuration file or output a value from it'
|
|
2869
|
-
long_desc %(Run without arguments, `doing config` opens your `config.yml` in an editor.
|
|
2870
|
-
If local configurations are found in the path between the current directory
|
|
2871
|
-
and the root (/), a menu will allow you to select which to open in the editor.
|
|
2872
|
-
|
|
2873
|
-
It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
|
|
2874
|
-
|
|
2875
|
-
Use `doing config get` to output the configuration to the terminal, and
|
|
2876
|
-
provide a dot-separated key path to get a specific value. Shows the current value
|
|
2877
|
-
including keys/overrides set by local configs.)
|
|
2878
|
-
command :config do |c|
|
|
2879
|
-
c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
|
|
2880
|
-
c.example 'doing config get doing_file', desc: 'Output the value of a config key as YAML'
|
|
2881
|
-
c.example 'doing config get plugins.plugin_path -o json', desc: 'Output the value of a key path as JSON'
|
|
2882
|
-
c.example 'doing config set plugins.say.say_voice Alex', desc: 'Set the value of a key path and update config file'
|
|
2883
|
-
c.example 'doing config set plug.say.voice Zarvox', desc: 'Key paths for get and set are fuzzy matched'
|
|
2884
|
-
|
|
2885
|
-
c.default_command :edit
|
|
2886
|
-
|
|
2887
|
-
c.desc 'DEPRECATED'
|
|
2888
|
-
c.switch %i[d dump]
|
|
2889
|
-
|
|
2890
|
-
c.desc 'DEPRECATED'
|
|
2891
|
-
c.switch %i[u update]
|
|
2892
|
-
|
|
2893
|
-
c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
|
|
2894
|
-
c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
|
|
2895
|
-
Values defined in the top item in the list will override values in configutations below it.'
|
|
2896
|
-
c.command :list do |list|
|
|
2897
|
-
list.action do |global, options, args|
|
|
2898
|
-
puts config.additional_configs.join("\n")
|
|
2899
|
-
puts config.config_file
|
|
2900
|
-
end
|
|
2901
|
-
end
|
|
2902
|
-
|
|
2903
|
-
c.desc 'Open config file in editor'
|
|
2904
|
-
c.command :edit do |edit|
|
|
2905
|
-
edit.example 'doing config edit', desc: 'Open a config file in the default editor'
|
|
2906
|
-
edit.example 'doing config edit --editor vim', desc: 'Open config in specific editor'
|
|
2907
|
-
|
|
2908
|
-
edit.desc 'Editor to use'
|
|
2909
|
-
edit.arg_name 'EDITOR'
|
|
2910
|
-
edit.flag %i[e editor], default_value: nil
|
|
2911
|
-
|
|
2912
|
-
if `uname` =~ /Darwin/
|
|
2913
|
-
edit.desc 'Application to use'
|
|
2914
|
-
edit.arg_name 'APP_NAME'
|
|
2915
|
-
edit.flag %i[a app]
|
|
2916
|
-
|
|
2917
|
-
edit.desc 'Application bundle id to use'
|
|
2918
|
-
edit.arg_name 'BUNDLE_ID'
|
|
2919
|
-
edit.flag %i[b bundle_id]
|
|
2920
|
-
|
|
2921
|
-
edit.desc "Use the config_editor_app defined in ~/.config/doing/config.yml (#{settings.key?('config_editor_app') ? settings['config_editor_app'] : 'config_editor_app not set'})"
|
|
2922
|
-
edit.switch %i[x default]
|
|
2923
|
-
end
|
|
2924
|
-
|
|
2925
|
-
edit.action do |global, options, args|
|
|
2926
|
-
if options[:update] || options[:dump]
|
|
2927
|
-
cmd = commands[:config]
|
|
2928
|
-
if options[:update]
|
|
2929
|
-
cmd = cmd.commands[:update]
|
|
2930
|
-
elsif options[:dump]
|
|
2931
|
-
cmd = cmd.commands[:get]
|
|
2932
|
-
end
|
|
2933
|
-
action = cmd.send(:get_action, nil)
|
|
2934
|
-
action.call(global, options, args)
|
|
2935
|
-
Doing.logger.warn('Deprecated:', '--dump and --update are deprecated,
|
|
2936
|
-
use `doing config get` and `doing config update`')
|
|
2937
|
-
Doing.logger.output_results
|
|
2938
|
-
return
|
|
2939
|
-
end
|
|
2940
|
-
|
|
2941
|
-
config_file = config.choose_config
|
|
2942
|
-
|
|
2943
|
-
if `uname` =~ /Darwin/
|
|
2944
|
-
if options[:default]
|
|
2945
|
-
editor = Doing::Util.find_default_editor('config')
|
|
2946
|
-
if editor
|
|
2947
|
-
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2948
|
-
system %(#{editor} "#{config_file}")
|
|
2949
|
-
else
|
|
2950
|
-
`open -a "#{editor}" "#{config_file}"`
|
|
2951
|
-
end
|
|
2952
|
-
else
|
|
2953
|
-
raise InvalidArgument, 'No viable editor found in config or environment.'
|
|
2954
|
-
end
|
|
2955
|
-
elsif options[:app] || options[:bundle_id]
|
|
2956
|
-
if options[:app]
|
|
2957
|
-
`open -a "#{options[:app]}" "#{config_file}"`
|
|
2958
|
-
elsif options[:bundle_id]
|
|
2959
|
-
`open -b #{options[:bundle_id]} "#{config_file}"`
|
|
2960
|
-
end
|
|
2961
|
-
else
|
|
2962
|
-
editor = options[:editor] || Doing::Util.find_default_editor('config')
|
|
2963
|
-
|
|
2964
|
-
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
|
2965
|
-
|
|
2966
|
-
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
2967
|
-
system %(#{editor} "#{config_file}")
|
|
2968
|
-
else
|
|
2969
|
-
`open -a "#{editor}" "#{config_file}"`
|
|
2970
|
-
end
|
|
2971
|
-
end
|
|
2972
|
-
else
|
|
2973
|
-
editor = options[:editor] || Doing::Util.default_editor
|
|
2974
|
-
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
|
|
2975
|
-
|
|
2976
|
-
system %(#{editor} "#{config_file}")
|
|
2977
|
-
end
|
|
2978
|
-
end
|
|
2979
|
-
end
|
|
2980
|
-
|
|
2981
|
-
c.desc 'Update default config file, adding any missing keys'
|
|
2982
|
-
c.command %i[update refresh] do |update|
|
|
2983
|
-
update.action do |_global, options, args|
|
|
2984
|
-
config.configure({rewrite: true, ignore_local: true})
|
|
2985
|
-
Doing.logger.warn('Config:', 'config refreshed')
|
|
2986
|
-
end
|
|
2987
|
-
end
|
|
2988
|
-
|
|
2989
|
-
c.desc 'Undo the last change to a config file'
|
|
2990
|
-
c.command :undo do |undo|
|
|
2991
|
-
undo.action do |_global, options, args|
|
|
2992
|
-
config_file = config.choose_config
|
|
2993
|
-
Doing::Util::Backup.restore_last_backup(config_file, count: 1)
|
|
2994
|
-
end
|
|
2995
|
-
end
|
|
2996
|
-
|
|
2997
|
-
c.desc 'Output a key\'s value'
|
|
2998
|
-
c.arg 'KEY_PATH'
|
|
2999
|
-
c.command %i[get dump] do |dump|
|
|
3000
|
-
dump.example 'doing config get', desc: 'Output the entire configuration'
|
|
3001
|
-
dump.example 'doing config get timer_format --output raw', desc: 'Output the value of timer_format as a plain string'
|
|
3002
|
-
dump.example 'doing config get doing_file', desc: 'Output the value of the doing_file setting, respecting local configurations'
|
|
3003
|
-
dump.example 'doing config get -o json plug.plugpath', desc: 'Key path is fuzzy matched: output the value of plugins->plugin_path as JSON'
|
|
3004
|
-
|
|
3005
|
-
dump.desc 'Format for output (json|yaml|raw)'
|
|
3006
|
-
dump.arg_name 'FORMAT'
|
|
3007
|
-
dump.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
|
|
3008
|
-
|
|
3009
|
-
dump.action do |_global, options, args|
|
|
3010
|
-
|
|
3011
|
-
keypath = args.join('.')
|
|
3012
|
-
cfg = config.value_for_key(keypath)
|
|
3013
|
-
real_path = config.resolve_key_path(keypath)
|
|
3014
|
-
|
|
3015
|
-
if cfg
|
|
3016
|
-
val = cfg.map {|k, v| v }[0]
|
|
3017
|
-
if real_path.count.positive?
|
|
3018
|
-
nested_cfg = {}
|
|
3019
|
-
nested_cfg.deep_set(real_path, val)
|
|
3020
|
-
else
|
|
3021
|
-
nested_cfg = val
|
|
3022
|
-
end
|
|
3177
|
+
## Utility commands
|
|
3023
3178
|
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
$stdout.puts case options[:output]
|
|
3034
|
-
when /^j/
|
|
3035
|
-
JSON.pretty_generate(val)
|
|
3036
|
-
else
|
|
3037
|
-
YAML.dump(nested_cfg)
|
|
3038
|
-
end
|
|
3039
|
-
end
|
|
3179
|
+
# @@colors
|
|
3180
|
+
desc 'List available color variables for configuration templates and views'
|
|
3181
|
+
command :colors do |c|
|
|
3182
|
+
c.action do |_global_options, _options, _args|
|
|
3183
|
+
bgs = []
|
|
3184
|
+
fgs = []
|
|
3185
|
+
colors::attributes.each do |color|
|
|
3186
|
+
if color.to_s =~ /bg/
|
|
3187
|
+
bgs.push("#{colors.send(color, " ")}#{colors.default} <-- #{color.to_s}")
|
|
3040
3188
|
else
|
|
3041
|
-
|
|
3189
|
+
fgs.push("#{colors.send(color, "XXXX")}#{colors.default} <-- #{color.to_s}")
|
|
3042
3190
|
end
|
|
3043
|
-
Doing.logger.output_results
|
|
3044
|
-
return
|
|
3045
3191
|
end
|
|
3192
|
+
out = []
|
|
3193
|
+
out << fgs.join("\n")
|
|
3194
|
+
out << bgs.join("\n")
|
|
3195
|
+
Doing::Pager.page out.join("\n")
|
|
3046
3196
|
end
|
|
3197
|
+
end
|
|
3047
3198
|
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3199
|
+
# @@completion
|
|
3200
|
+
desc 'Generate shell completion scripts'
|
|
3201
|
+
long_desc 'Generates the necessary scripts to add command line completion to various shells, so typing \'doing\' and hitting
|
|
3202
|
+
tab will offer completions of subcommands and their options.'
|
|
3203
|
+
command :completion do |c|
|
|
3204
|
+
c.example 'doing completion', desc: 'Output zsh (default) to STDOUT'
|
|
3205
|
+
c.example 'doing completion --type zsh --file ~/.zsh-completions/_doing.zsh', desc: 'Output zsh completions to file'
|
|
3206
|
+
c.example 'doing completion --type fish --file ~/.config/fish/completions/doing.fish', desc: 'Output fish completions to file'
|
|
3207
|
+
c.example 'doing completion --type bash --file ~/.bash_it/completion/enabled/doing.bash', desc: 'Output bash completions to file'
|
|
3053
3208
|
|
|
3054
|
-
|
|
3055
|
-
|
|
3209
|
+
c.desc 'Shell to generate for (bash, zsh, fish)'
|
|
3210
|
+
c.arg_name 'SHELL'
|
|
3211
|
+
c.flag %i[t type], must_match: /^(?:[bzf](?:[ai]?sh)?|all)$/i, default_value: 'zsh'
|
|
3056
3212
|
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3213
|
+
c.desc 'File to write output to'
|
|
3214
|
+
c.arg_name 'PATH'
|
|
3215
|
+
c.flag %i[f file], default_value: 'STDOUT'
|
|
3060
3216
|
|
|
3061
|
-
|
|
3217
|
+
c.action do |_global_options, options, _args|
|
|
3218
|
+
script_dir = File.join(File.dirname(__FILE__), '..', 'scripts')
|
|
3062
3219
|
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3220
|
+
Doing::Completion.generate_completion(type: options[:type], file: options[:file])
|
|
3221
|
+
end
|
|
3222
|
+
end
|
|
3066
3223
|
|
|
3067
|
-
|
|
3068
|
-
|
|
3224
|
+
# @@plugins
|
|
3225
|
+
desc 'List installed plugins'
|
|
3226
|
+
long_desc %(Lists available plugins, including user-installed plugins.
|
|
3069
3227
|
|
|
3070
|
-
|
|
3071
|
-
Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
|
|
3072
|
-
didyou = 'Did you mean:'
|
|
3073
|
-
old_value.keys.each do |k|
|
|
3074
|
-
Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
|
|
3075
|
-
didyou = '..........or:'
|
|
3076
|
-
end
|
|
3077
|
-
raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
|
|
3228
|
+
Export plugins are available with the `--output` flag on commands that support it.
|
|
3078
3229
|
|
|
3079
|
-
|
|
3230
|
+
Import plugins are available using `doing import --type PLUGIN`.
|
|
3231
|
+
)
|
|
3232
|
+
command :plugins do |c|
|
|
3233
|
+
c.example 'doing plugins', desc: 'List all plugins'
|
|
3234
|
+
c.example 'doing plugins -t import', desc: 'List all import plugins'
|
|
3080
3235
|
|
|
3081
|
-
|
|
3082
|
-
|
|
3236
|
+
c.desc 'List plugins of type (import, export)'
|
|
3237
|
+
c.arg_name 'TYPE'
|
|
3238
|
+
c.flag %i[t type], must_match: /^(?:[iea].*)$/i, default_value: 'all'
|
|
3083
3239
|
|
|
3084
|
-
|
|
3240
|
+
c.desc 'List in single column for completion'
|
|
3241
|
+
c.switch %i[c column], negatable: false, default_value: false
|
|
3085
3242
|
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3243
|
+
c.action do |_global_options, options, _args|
|
|
3244
|
+
Doing::Plugins.list_plugins(options)
|
|
3245
|
+
end
|
|
3246
|
+
end
|
|
3247
|
+
|
|
3248
|
+
# @@sections
|
|
3249
|
+
desc 'List sections'
|
|
3250
|
+
command :sections do |c|
|
|
3251
|
+
c.desc 'List in single column'
|
|
3252
|
+
c.switch %i[c column], negatable: false, default_value: false
|
|
3253
|
+
|
|
3254
|
+
c.action do |_global_options, options, _args|
|
|
3255
|
+
joiner = options[:column] ? "\n" : "\t"
|
|
3256
|
+
print wwid.content.section_titles.join(joiner)
|
|
3257
|
+
end
|
|
3258
|
+
end
|
|
3259
|
+
|
|
3260
|
+
# @@template
|
|
3261
|
+
desc 'Output HTML, CSS, and Markdown (ERB) templates for customization'
|
|
3262
|
+
long_desc %(
|
|
3263
|
+
Templates are printed to STDOUT for piping to a file.
|
|
3264
|
+
Save them and use them in the configuration file under export_templates.
|
|
3265
|
+
)
|
|
3266
|
+
arg_name 'TYPE', must_match: Doing::Plugins.template_regex
|
|
3267
|
+
command :template do |c|
|
|
3268
|
+
c.example 'doing template haml > ~/styles/my_doing.haml', desc: 'Output the haml template and save it to a file'
|
|
3269
|
+
|
|
3270
|
+
c.desc 'List all available templates'
|
|
3271
|
+
c.switch %i[l list], negatable: false
|
|
3272
|
+
|
|
3273
|
+
c.desc 'List in single column for completion'
|
|
3274
|
+
c.switch %i[c column]
|
|
3275
|
+
|
|
3276
|
+
c.desc 'Save template to file instead of STDOUT'
|
|
3277
|
+
c.switch %i[s save], default_value: false, negatable: false
|
|
3278
|
+
|
|
3279
|
+
c.desc 'Save template to alternate location'
|
|
3280
|
+
c.arg_name 'DIRECTORY'
|
|
3281
|
+
c.flag %i[p path], default_value: File.join(Doing::Util.user_home, '.config', 'doing', 'templates')
|
|
3091
3282
|
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3283
|
+
c.action do |_global_options, options, args|
|
|
3284
|
+
if options[:list] || options[:column]
|
|
3285
|
+
if options[:column]
|
|
3286
|
+
$stdout.print Doing::Plugins.plugin_templates.join("\n")
|
|
3287
|
+
else
|
|
3288
|
+
$stdout.puts "Available templates: #{Doing::Plugins.plugin_templates.join(', ')}"
|
|
3095
3289
|
end
|
|
3290
|
+
return
|
|
3291
|
+
end
|
|
3096
3292
|
|
|
3097
|
-
|
|
3293
|
+
if args.empty?
|
|
3294
|
+
type = Doing::Prompt.choose_from(Doing::Plugins.plugin_templates, sorted: false, prompt: 'Select template type > ')
|
|
3295
|
+
type.sub!(/ \(.*?\)$/, '').strip!
|
|
3296
|
+
options[:save] = Doing::Prompt.yn("Save to #{options[:path]}? (No outputs to STDOUT)", default_response: false)
|
|
3297
|
+
else
|
|
3298
|
+
type = args[0]
|
|
3299
|
+
end
|
|
3098
3300
|
|
|
3099
|
-
|
|
3301
|
+
raise InvalidPluginType, "No type specified, use `doing template [#{Doing::Plugins.plugin_templates.join('|')}]`" unless type
|
|
3100
3302
|
|
|
3101
|
-
|
|
3102
|
-
Doing.
|
|
3303
|
+
if options[:save]
|
|
3304
|
+
Doing::Plugins.template_for_trigger(type, save_to: options[:path])
|
|
3305
|
+
else
|
|
3306
|
+
$stdout.puts Doing::Plugins.template_for_trigger(type, save_to: nil)
|
|
3103
3307
|
end
|
|
3308
|
+
|
|
3309
|
+
# case args[0]
|
|
3310
|
+
# when /html|haml/i
|
|
3311
|
+
# $stdout.puts wwid.haml_template
|
|
3312
|
+
# when /css/i
|
|
3313
|
+
# $stdout.puts wwid.css_template
|
|
3314
|
+
# when /markdown|md|erb/i
|
|
3315
|
+
# $stdout.puts wwid.markdown_template
|
|
3316
|
+
# else
|
|
3317
|
+
# exit_now! 'Invalid type specified, must be HAML or CSS'
|
|
3318
|
+
# end
|
|
3319
|
+
end
|
|
3320
|
+
end
|
|
3321
|
+
|
|
3322
|
+
# @@views
|
|
3323
|
+
desc 'List available custom views'
|
|
3324
|
+
command :views do |c|
|
|
3325
|
+
c.desc 'List in single column'
|
|
3326
|
+
c.switch %i[c column], default_value: false
|
|
3327
|
+
|
|
3328
|
+
c.action do |_global_options, options, _args|
|
|
3329
|
+
joiner = options[:column] ? "\n" : "\t"
|
|
3330
|
+
print wwid.views.join(joiner)
|
|
3104
3331
|
end
|
|
3105
3332
|
end
|
|
3106
3333
|
|
|
3334
|
+
## History commands
|
|
3335
|
+
|
|
3336
|
+
# @@undo
|
|
3107
3337
|
desc 'Undo the last X changes to the Doing file'
|
|
3108
3338
|
long_desc 'Reverts the last X commands that altered the doing file.
|
|
3109
3339
|
All changes performed by a single command are undone at once.
|
|
@@ -3153,6 +3383,7 @@ command :undo do |c|
|
|
|
3153
3383
|
end
|
|
3154
3384
|
end
|
|
3155
3385
|
|
|
3386
|
+
# @@redo
|
|
3156
3387
|
long_desc 'Shortcut for `doing undo -r`, reverses the last undo command. You cannot undo a redo.'
|
|
3157
3388
|
arg_name 'COUNT'
|
|
3158
3389
|
command :redo do |c|
|
|
@@ -3175,6 +3406,7 @@ command :redo do |c|
|
|
|
3175
3406
|
end
|
|
3176
3407
|
end
|
|
3177
3408
|
|
|
3409
|
+
# @@changelog @@changes
|
|
3178
3410
|
desc 'List recent changes in Doing'
|
|
3179
3411
|
long_desc 'Display a formatted list of changes in recent versions, latest at the top'
|
|
3180
3412
|
command %i[changelog changes] do |c|
|
|
@@ -3190,6 +3422,9 @@ command %i[changelog changes] do |c|
|
|
|
3190
3422
|
end
|
|
3191
3423
|
end
|
|
3192
3424
|
|
|
3425
|
+
## Hidden commands
|
|
3426
|
+
|
|
3427
|
+
# @@commands_accepting
|
|
3193
3428
|
arg_name 'OPTION'
|
|
3194
3429
|
command :commands_accepting do |c|
|
|
3195
3430
|
c.desc 'Output in single column for completion'
|
|
@@ -3215,107 +3450,26 @@ command :commands_accepting do |c|
|
|
|
3215
3450
|
end
|
|
3216
3451
|
end
|
|
3217
3452
|
|
|
3453
|
+
# @@install_fzf
|
|
3454
|
+
command :install_fzf do |c|
|
|
3455
|
+
c.desc 'Force reinstall'
|
|
3456
|
+
c.switch %i[r reinstall], default_value: false
|
|
3218
3457
|
|
|
3219
|
-
desc '
|
|
3220
|
-
|
|
3221
|
-
arg_name 'PATH'
|
|
3222
|
-
command :import do |c|
|
|
3223
|
-
c.example 'doing import --type timing "~/Desktop/All Activities.json"', desc: 'Import a Timing.app JSON report'
|
|
3224
|
-
c.example 'doing import --type doing --tag imported --no-autotag ~/doing_backup.md', desc: 'Import an Doing archive, tag all entries with @imported, skip autotagging'
|
|
3225
|
-
c.example 'doing import --type doing --from "10/1 to 10/15" ~/doing_backup.md', desc: 'Import a Doing archive, only importing entries between two dates'
|
|
3226
|
-
|
|
3227
|
-
c.desc "Import type (#{Doing::Plugins.plugin_names(type: :import)})"
|
|
3228
|
-
c.arg_name 'TYPE'
|
|
3229
|
-
c.flag :type, default_value: 'doing'
|
|
3230
|
-
|
|
3231
|
-
c.desc 'Only import items matching search. Surround with slashes for regex (/query/), start with single quote for exact match ("\'query")'
|
|
3232
|
-
c.arg_name 'QUERY'
|
|
3233
|
-
c.flag [:search]
|
|
3234
|
-
|
|
3235
|
-
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
3236
|
-
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
3237
|
-
|
|
3238
|
-
c.desc 'Force exact search string matching (case sensitive)'
|
|
3239
|
-
c.switch %i[x exact], default_value: config.exact_match?, negatable: config.exact_match?
|
|
3240
|
-
|
|
3241
|
-
c.desc 'Import items that *don\'t* match search/tag/date filters'
|
|
3242
|
-
c.switch [:not], default_value: false, negatable: false
|
|
3243
|
-
|
|
3244
|
-
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
3245
|
-
c.arg_name 'TYPE'
|
|
3246
|
-
c.flag [:case], must_match: /^[csi]/, default_value: settings.dig('search', 'case')
|
|
3247
|
-
|
|
3248
|
-
c.desc 'Only import items with recorded time intervals'
|
|
3249
|
-
c.switch [:only_timed], default_value: false, negatable: false
|
|
3250
|
-
|
|
3251
|
-
c.desc 'Target section'
|
|
3252
|
-
c.arg_name 'NAME'
|
|
3253
|
-
c.flag %i[s section]
|
|
3254
|
-
|
|
3255
|
-
c.desc 'Tag all imported entries'
|
|
3256
|
-
c.arg_name 'TAGS'
|
|
3257
|
-
c.flag :tag
|
|
3258
|
-
|
|
3259
|
-
c.desc 'Autotag entries'
|
|
3260
|
-
c.switch :autotag, negatable: true, default_value: true
|
|
3261
|
-
|
|
3262
|
-
c.desc 'Prefix entries with'
|
|
3263
|
-
c.arg_name 'PREFIX'
|
|
3264
|
-
c.flag :prefix
|
|
3265
|
-
|
|
3266
|
-
# TODO: Allow time range filtering
|
|
3267
|
-
c.desc 'Import entries older than date'
|
|
3268
|
-
c.arg_name 'DATE_STRING'
|
|
3269
|
-
c.flag [:before]
|
|
3270
|
-
|
|
3271
|
-
c.desc 'Import entries newer than date'
|
|
3272
|
-
c.arg_name 'DATE_STRING'
|
|
3273
|
-
c.flag [:after]
|
|
3274
|
-
|
|
3275
|
-
c.desc %(
|
|
3276
|
-
Date range to import. Date range argument should be quoted. Date specifications can be natural language.
|
|
3277
|
-
To specify a range, use "to" or "through": `--from "monday to friday"` or `--from 10/1 to 10/31`.
|
|
3278
|
-
Has no effect unless the import plugin has implemented date range filtering.
|
|
3279
|
-
)
|
|
3280
|
-
c.arg_name 'DATE_OR_RANGE'
|
|
3281
|
-
c.flag %i[f from]
|
|
3282
|
-
|
|
3283
|
-
c.desc 'Allow entries that overlap existing times'
|
|
3284
|
-
c.switch [:overlap], negatable: true
|
|
3285
|
-
|
|
3286
|
-
c.action do |_global_options, options, args|
|
|
3287
|
-
options[:fuzzy] = false
|
|
3288
|
-
if options[:section]
|
|
3289
|
-
options[:section] = wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
3290
|
-
end
|
|
3291
|
-
|
|
3292
|
-
if options[:from]
|
|
3293
|
-
date_string = options[:from]
|
|
3294
|
-
if date_string =~ / (to|through|thru|(un)?til|-+) /
|
|
3295
|
-
dates = date_string.split(/ (to|through|thru|(un)?til|-+) /)
|
|
3296
|
-
start = dates[0].chronify(guess: :begin)
|
|
3297
|
-
finish = dates[2].chronify(guess: :end)
|
|
3298
|
-
else
|
|
3299
|
-
start = date_string.chronify(guess: :begin)
|
|
3300
|
-
finish = false
|
|
3301
|
-
end
|
|
3302
|
-
raise InvalidTimeExpression, 'Unrecognized date string' unless start
|
|
3303
|
-
dates = [start, finish]
|
|
3304
|
-
end
|
|
3305
|
-
|
|
3306
|
-
options[:case] = options[:case].normalize_case
|
|
3458
|
+
c.desc 'Uninstall'
|
|
3459
|
+
c.switch %i[u uninstall], default_value: false, negatable: false
|
|
3307
3460
|
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
wwid.import(args, options)
|
|
3312
|
-
wwid.write(wwid.doing_file)
|
|
3461
|
+
c.action do |g, o, a|
|
|
3462
|
+
if o[:uninstall]
|
|
3463
|
+
Doing::Prompt.uninstall_fzf
|
|
3313
3464
|
else
|
|
3314
|
-
|
|
3465
|
+
Doing.logger.warn('fzf:', 'force reinstall') if o[:reinstall]
|
|
3466
|
+
res = Doing::Prompt.install_fzf(force: o[:reinstall])
|
|
3315
3467
|
end
|
|
3316
3468
|
end
|
|
3317
3469
|
end
|
|
3318
3470
|
|
|
3471
|
+
## Doing::Hooks
|
|
3472
|
+
|
|
3319
3473
|
pre do |global, _command, _options, _args|
|
|
3320
3474
|
# global[:pager] ||= settings['paginate']
|
|
3321
3475
|
Doing::Pager.paginate = global[:pager]
|