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