doing 2.1.25 → 2.1.26
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 +4 -4
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +283 -108
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/bin/commands/add_section.rb +13 -0
- data/bin/commands/again.rb +99 -0
- data/bin/commands/archive.rb +96 -0
- data/bin/commands/cancel.rb +102 -0
- data/bin/commands/changes.rb +42 -0
- data/bin/commands/choose.rb +9 -0
- data/bin/commands/colors.rb +19 -0
- data/bin/commands/commands.rb +87 -0
- data/bin/commands/commands_accepting.rb +25 -0
- data/bin/commands/completion.rb +24 -0
- data/bin/commands/config.rb +245 -0
- data/bin/commands/done.rb +249 -0
- data/bin/commands/finish.rb +149 -0
- data/bin/commands/flag.rb +126 -0
- data/bin/commands/grep.rb +124 -0
- data/bin/commands/import.rb +101 -0
- data/bin/commands/install_fzf.rb +17 -0
- data/bin/commands/last.rb +114 -0
- data/bin/commands/meanwhile.rb +86 -0
- data/bin/commands/note.rb +130 -0
- data/bin/commands/now.rb +151 -0
- data/bin/commands/on.rb +66 -0
- data/bin/commands/open.rb +53 -0
- data/bin/commands/plugins.rb +23 -0
- data/bin/commands/recent.rb +78 -0
- data/bin/commands/redo.rb +22 -0
- data/bin/commands/reset.rb +106 -0
- data/bin/commands/rotate.rb +73 -0
- data/bin/commands/sections.rb +11 -0
- data/bin/commands/select.rb +123 -0
- data/bin/commands/show.rb +231 -0
- data/bin/commands/since.rb +64 -0
- data/bin/commands/tag.rb +179 -0
- data/bin/commands/tag_dir.rb +29 -0
- data/bin/commands/tags.rb +93 -0
- data/bin/commands/template.rb +61 -0
- data/bin/commands/today.rb +65 -0
- data/bin/commands/undo.rb +49 -0
- data/bin/commands/view.rb +238 -0
- data/bin/commands/views.rb +11 -0
- data/bin/commands/yesterday.rb +73 -0
- data/bin/doing +39 -3641
- data/docs/doc/Array.html +1 -1
- data/docs/doc/BooleanTermParser/Clause.html +1 -1
- data/docs/doc/BooleanTermParser/Operator.html +1 -1
- data/docs/doc/BooleanTermParser/Query.html +1 -1
- data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
- data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
- data/docs/doc/BooleanTermParser.html +1 -1
- data/docs/doc/Doing/Color.html +1 -1
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +2 -1
- data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
- data/docs/doc/Doing/Errors/NoResults.html +1 -1
- data/docs/doc/Doing/Errors/PluginException.html +1 -1
- data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
- data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
- data/docs/doc/Doing/Errors.html +1 -1
- data/docs/doc/Doing/Hooks.html +1 -1
- data/docs/doc/Doing/Item.html +1 -1
- data/docs/doc/Doing/Items.html +1 -1
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +1 -1
- data/docs/doc/Doing/Pager.html +1 -1
- data/docs/doc/Doing/Plugins.html +1 -1
- data/docs/doc/Doing/Prompt.html +46 -1
- data/docs/doc/Doing/Section.html +1 -1
- data/docs/doc/Doing/TemplateString.html +1 -1
- data/docs/doc/Doing/Types.html +1 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +1 -1
- data/docs/doc/Doing.html +2 -2
- data/docs/doc/FalseClass.html +201 -0
- data/docs/doc/GLI/Commands/Help.html +1 -1
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/docs/doc/GLI/Commands.html +1 -1
- data/docs/doc/GLI.html +1 -1
- data/docs/doc/Hash.html +1 -1
- data/docs/doc/Numeric.html +1 -1
- data/docs/doc/Object.html +203 -0
- data/docs/doc/PhraseParser/Operator.html +1 -1
- data/docs/doc/PhraseParser/PhraseClause.html +1 -1
- data/docs/doc/PhraseParser/Query.html +1 -1
- data/docs/doc/PhraseParser/QueryParser.html +1 -1
- data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
- data/docs/doc/PhraseParser/TermClause.html +1 -1
- data/docs/doc/PhraseParser.html +1 -1
- data/docs/doc/Status.html +1 -1
- data/docs/doc/String.html +1 -1
- data/docs/doc/Symbol.html +1 -1
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +201 -0
- data/docs/doc/_index.html +1 -1
- data/docs/doc/file.README.html +2 -2
- data/docs/doc/index.html +2 -2
- data/docs/doc/method_list.html +374 -366
- data/docs/doc/top-level-namespace.html +1 -1
- data/doing.rdoc +15 -5
- data/lib/completion/_doing.zsh +3 -3
- data/lib/completion/doing.fish +1 -1
- data/lib/doing/changelog/changes.rb +1 -1
- data/lib/doing/configuration.rb +1 -0
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/prompt.rb +8 -0
- data/lib/doing/version.rb +1 -1
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/helpers/threaded_tests.rb +25 -19
- metadata +45 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# @@config
|
|
2
|
+
desc 'Edit the configuration file or output a value from it'
|
|
3
|
+
long_desc %(Run without arguments, `doing config` opens your `config.yml` in an editor.
|
|
4
|
+
If local configurations are found in the path between the current directory
|
|
5
|
+
and the root (/), a menu will allow you to select which to open in the editor.
|
|
6
|
+
|
|
7
|
+
It will use the editor defined in `config_editor_app`, or one specified with `--editor`.
|
|
8
|
+
|
|
9
|
+
Use `doing config get` to output the configuration to the terminal, and
|
|
10
|
+
provide a dot-separated key path to get a specific value. Shows the current value
|
|
11
|
+
including keys/overrides set by local configs.)
|
|
12
|
+
command :config do |c|
|
|
13
|
+
c.example 'doing config', desc: "Open an active configuration in #{Doing::Util.find_default_editor('config')}"
|
|
14
|
+
c.example 'doing config get doing_file', desc: 'Output the value of a config key as YAML'
|
|
15
|
+
c.example 'doing config get plugins.plugin_path -o json', desc: 'Output the value of a key path as JSON'
|
|
16
|
+
c.example 'doing config set plugins.say.say_voice Alex', desc: 'Set the value of a key path and update config file'
|
|
17
|
+
c.example 'doing config set plug.say.voice Zarvox', desc: 'Key paths for get and set are fuzzy matched'
|
|
18
|
+
|
|
19
|
+
c.default_command :edit
|
|
20
|
+
|
|
21
|
+
c.desc 'DEPRECATED'
|
|
22
|
+
c.switch %i[d dump]
|
|
23
|
+
|
|
24
|
+
c.desc 'DEPRECATED'
|
|
25
|
+
c.switch %i[u update]
|
|
26
|
+
|
|
27
|
+
# @@config.list
|
|
28
|
+
c.desc 'List configuration paths, including .doingrc files in the current and parent directories'
|
|
29
|
+
c.long_desc 'Config files are listed in order of precedence (if there are multiple configs detected).
|
|
30
|
+
Values defined in the top item in the list will override values in configutations below it.'
|
|
31
|
+
c.command :list do |list|
|
|
32
|
+
list.action do |global, options, args|
|
|
33
|
+
puts @config.additional_configs.join("\n")
|
|
34
|
+
puts @config.config_file
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @@config.edit
|
|
39
|
+
c.desc 'Open config file in editor'
|
|
40
|
+
c.command :edit do |edit|
|
|
41
|
+
edit.example 'doing config edit', desc: 'Open a config file in the default editor'
|
|
42
|
+
edit.example 'doing config edit --editor vim', desc: 'Open config in specific editor'
|
|
43
|
+
|
|
44
|
+
edit.desc 'Editor to use'
|
|
45
|
+
edit.arg_name 'EDITOR'
|
|
46
|
+
edit.flag %i[e editor], default_value: nil
|
|
47
|
+
|
|
48
|
+
if `uname` =~ /Darwin/
|
|
49
|
+
edit.desc 'Application to use'
|
|
50
|
+
edit.arg_name 'APP_NAME'
|
|
51
|
+
edit.flag %i[a app]
|
|
52
|
+
|
|
53
|
+
edit.desc 'Application bundle id to use'
|
|
54
|
+
edit.arg_name 'BUNDLE_ID'
|
|
55
|
+
edit.flag %i[b bundle_id]
|
|
56
|
+
|
|
57
|
+
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'})"
|
|
58
|
+
edit.switch %i[x default]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
edit.action do |global, options, args|
|
|
62
|
+
if options[:update] || options[:dump]
|
|
63
|
+
cmd = commands[:config]
|
|
64
|
+
if options[:update]
|
|
65
|
+
cmd = cmd.commands[:update]
|
|
66
|
+
elsif options[:dump]
|
|
67
|
+
cmd = cmd.commands[:get]
|
|
68
|
+
end
|
|
69
|
+
action = cmd.send(:get_action, nil)
|
|
70
|
+
action.call(global, options, args)
|
|
71
|
+
Doing.logger.warn('Deprecated:', '--dump and --update are deprecated,
|
|
72
|
+
use `doing config get` and `doing config update`')
|
|
73
|
+
Doing.logger.output_results
|
|
74
|
+
return
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
config_file = @config.choose_config
|
|
78
|
+
|
|
79
|
+
if `uname` =~ /Darwin/
|
|
80
|
+
if options[:default]
|
|
81
|
+
editor = Doing::Util.find_default_editor('config')
|
|
82
|
+
if editor
|
|
83
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
84
|
+
system %(#{editor} "#{config_file}")
|
|
85
|
+
else
|
|
86
|
+
`open -a "#{editor}" "#{config_file}"`
|
|
87
|
+
end
|
|
88
|
+
else
|
|
89
|
+
raise InvalidArgument, 'No viable editor found in config or environment.'
|
|
90
|
+
end
|
|
91
|
+
elsif options[:app] || options[:bundle_id]
|
|
92
|
+
if options[:app]
|
|
93
|
+
`open -a "#{options[:app]}" "#{config_file}"`
|
|
94
|
+
elsif options[:bundle_id]
|
|
95
|
+
`open -b #{options[:bundle_id]} "#{config_file}"`
|
|
96
|
+
end
|
|
97
|
+
else
|
|
98
|
+
editor = options[:editor] || Doing::Util.find_default_editor('config')
|
|
99
|
+
|
|
100
|
+
raise MissingEditor, 'No viable editor defined in config or environment' unless editor
|
|
101
|
+
|
|
102
|
+
if Doing::Util.exec_available(editor.split(/ /).first)
|
|
103
|
+
system %(#{editor} "#{config_file}")
|
|
104
|
+
else
|
|
105
|
+
`open -a "#{editor}" "#{config_file}"`
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
else
|
|
109
|
+
editor = options[:editor] || Doing::Util.default_editor
|
|
110
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' unless editor && Doing::Util.exec_available(editor.split(/ /).first)
|
|
111
|
+
|
|
112
|
+
system %(#{editor} "#{config_file}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# @@config.update @@config.refresh
|
|
118
|
+
c.desc 'Update default config file, adding any missing keys'
|
|
119
|
+
c.command %i[update refresh] do |update|
|
|
120
|
+
update.action do |_global, options, args|
|
|
121
|
+
@config.configure({rewrite: true, ignore_local: true})
|
|
122
|
+
Doing.logger.warn('Config:', 'config refreshed')
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# @@config.undo
|
|
127
|
+
c.desc 'Undo the last change to a config file'
|
|
128
|
+
c.command :undo do |undo|
|
|
129
|
+
undo.action do |_global, options, args|
|
|
130
|
+
config_file = @config.choose_config
|
|
131
|
+
Doing::Util::Backup.restore_last_backup(config_file, count: 1)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# @@config.get @@config.dump
|
|
136
|
+
c.desc 'Output a key\'s value'
|
|
137
|
+
c.arg 'KEY_PATH'
|
|
138
|
+
c.command %i[get dump] do |dump|
|
|
139
|
+
dump.example 'doing config get', desc: 'Output the entire configuration'
|
|
140
|
+
dump.example 'doing config get timer_format --output raw', desc: 'Output the value of timer_format as a plain string'
|
|
141
|
+
dump.example 'doing config get doing_file', desc: 'Output the value of the doing_file setting, respecting local configurations'
|
|
142
|
+
dump.example 'doing config get -o json plug.plugpath', desc: 'Key path is fuzzy matched: output the value of plugins->plugin_path as JSON'
|
|
143
|
+
|
|
144
|
+
dump.desc 'Format for output (json|yaml|raw)'
|
|
145
|
+
dump.arg_name 'FORMAT'
|
|
146
|
+
dump.flag %i[o output], default_value: 'yaml', must_match: /^(?:y(?:aml)?|j(?:son)?|r(?:aw)?)$/
|
|
147
|
+
|
|
148
|
+
dump.action do |_global, options, args|
|
|
149
|
+
|
|
150
|
+
keypath = args.join('.')
|
|
151
|
+
cfg = @config.value_for_key(keypath)
|
|
152
|
+
real_path = @config.resolve_key_path(keypath)
|
|
153
|
+
|
|
154
|
+
if cfg
|
|
155
|
+
val = cfg.map {|k, v| v }[0]
|
|
156
|
+
if real_path.count.positive?
|
|
157
|
+
nested_cfg = {}
|
|
158
|
+
nested_cfg.deep_set(real_path, val)
|
|
159
|
+
else
|
|
160
|
+
nested_cfg = val
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if options[:output] =~ /^r/
|
|
164
|
+
if val.is_a?(Hash)
|
|
165
|
+
$stdout.puts YAML.dump(val)
|
|
166
|
+
elsif val.is_a?(Array)
|
|
167
|
+
$stdout.puts val.join(', ')
|
|
168
|
+
else
|
|
169
|
+
$stdout.puts val.to_s
|
|
170
|
+
end
|
|
171
|
+
else
|
|
172
|
+
$stdout.puts case options[:output]
|
|
173
|
+
when /^j/
|
|
174
|
+
JSON.pretty_generate(val)
|
|
175
|
+
else
|
|
176
|
+
YAML.dump(nested_cfg)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
else
|
|
180
|
+
Doing.logger.log_now(:error, 'Config:', "Key #{keypath} not found")
|
|
181
|
+
end
|
|
182
|
+
Doing.logger.output_results
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @@config.set
|
|
187
|
+
c.desc 'Set a key\'s value in the config file'
|
|
188
|
+
c.arg 'KEY VALUE'
|
|
189
|
+
c.command :set do |set|
|
|
190
|
+
set.example 'doing config set timer_format human', desc: 'Set the value of timer_format to "human"'
|
|
191
|
+
set.example 'doing config set plug.plugpath ~/my_plugins', desc: 'Key path is fuzzy matched: set the value of plugins->plugin_path'
|
|
192
|
+
|
|
193
|
+
set.desc 'Delete specified key'
|
|
194
|
+
set.switch %i[r remove], default_value: false, negatable: false
|
|
195
|
+
|
|
196
|
+
set.action do |_global, options, args|
|
|
197
|
+
if args.count < 2 && !options[:remove]
|
|
198
|
+
raise InvalidArgument, 'config set requires at least two arguments, key path and value'
|
|
199
|
+
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
value = options[:remove] ? nil : args.pop
|
|
203
|
+
keypath = args.join('.')
|
|
204
|
+
real_path = @config.resolve_key_path(keypath, create: true)
|
|
205
|
+
old_value = @settings.dig(*real_path)
|
|
206
|
+
old_type = old_value&.class.to_s || nil
|
|
207
|
+
|
|
208
|
+
if old_value.is_a?(Hash) && !options[:remove]
|
|
209
|
+
Doing.logger.log_now(:warn, 'Config:', "Config key must point to a single value, #{real_path.join('->').boldwhite} is a mapping")
|
|
210
|
+
didyou = 'Did you mean:'
|
|
211
|
+
old_value.keys.each do |k|
|
|
212
|
+
Doing.logger.log_now(:warn, "#{didyou}", "#{keypath}.#{k}?")
|
|
213
|
+
didyou = '..........or:'
|
|
214
|
+
end
|
|
215
|
+
raise InvalidArgument, 'Config value is a mapping, can not be set to a single value'
|
|
216
|
+
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
config_file = @config.choose_config(create: true)
|
|
220
|
+
|
|
221
|
+
cfg = YAML.safe_load_file(config_file) || {}
|
|
222
|
+
|
|
223
|
+
$stderr.puts "Updating #{config_file}".yellow
|
|
224
|
+
|
|
225
|
+
if options[:remove]
|
|
226
|
+
cfg.deep_set(real_path, nil)
|
|
227
|
+
$stderr.puts "#{'Deleting key:'.yellow} #{real_path.join('->').boldwhite}"
|
|
228
|
+
else
|
|
229
|
+
current_value = cfg.dig(*real_path)
|
|
230
|
+
cfg.deep_set(real_path, value.set_type(old_type))
|
|
231
|
+
$stderr.puts "#{' Key path:'.yellow} #{real_path.join('->').boldwhite}"
|
|
232
|
+
$stderr.puts "#{'Inherited:'.yellow} #{(old_value ? old_value.to_s : 'empty').boldwhite}"
|
|
233
|
+
$stderr.puts "#{' Current:'.yellow} #{ (current_value ? current_value.to_s : 'empty').boldwhite }"
|
|
234
|
+
$stderr.puts "#{' New:'.yellow} #{value.set_type(old_type).to_s.boldwhite}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
res = Doing::Prompt.yn('Update selected config', default_response: true)
|
|
238
|
+
|
|
239
|
+
raise UserCancelled, 'Cancelled' unless res
|
|
240
|
+
|
|
241
|
+
Doing::Util.write_to_file(config_file, YAML.dump(cfg), backup: true)
|
|
242
|
+
Doing.logger.warn('Config:', "#{config_file} updated")
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# @@done @@did
|
|
2
|
+
desc 'Add a completed item with @done(date). No argument finishes last entry'
|
|
3
|
+
long_desc 'Use this command to add an entry after you\'ve already finished it. It will be immediately marked as @done.
|
|
4
|
+
You can modify the start and end times of the entry using the --back, --took, and --at flags, making it an easy
|
|
5
|
+
way to add entries in post and maintain accurate (albeit manual) time tracking.'
|
|
6
|
+
arg_name 'ENTRY', optional: true
|
|
7
|
+
command %i[done did] do |c|
|
|
8
|
+
c.example 'doing done', desc: 'Tag the last entry @done'
|
|
9
|
+
c.example 'doing done I already finished this', desc: 'Add a new entry and immediately mark it @done'
|
|
10
|
+
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'
|
|
11
|
+
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)'
|
|
12
|
+
|
|
13
|
+
c.desc 'Remove @done tag'
|
|
14
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
15
|
+
|
|
16
|
+
c.desc 'Include date'
|
|
17
|
+
c.switch [:date], negatable: true, default_value: true
|
|
18
|
+
|
|
19
|
+
c.desc 'Immediately archive the entry'
|
|
20
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
21
|
+
|
|
22
|
+
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm).
|
|
23
|
+
Used with --took, backdates start date)
|
|
24
|
+
c.arg_name 'DATE_STRING'
|
|
25
|
+
c.flag %i[at finished], type: DateEndString
|
|
26
|
+
|
|
27
|
+
c.desc 'Backdate start date by interval or set to time [4pm|20m|2h|"yesterday noon"]'
|
|
28
|
+
c.arg_name 'DATE_STRING'
|
|
29
|
+
c.flag %i[b back started], type: DateBeginString
|
|
30
|
+
|
|
31
|
+
c.desc %(
|
|
32
|
+
Start and end times as a date/time range `doing done --from "1am to 8am"`.
|
|
33
|
+
Overrides other date flags.
|
|
34
|
+
)
|
|
35
|
+
c.arg_name 'TIME_RANGE'
|
|
36
|
+
c.flag [:from], must_match: REGEX_RANGE
|
|
37
|
+
|
|
38
|
+
c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
|
|
39
|
+
If used without the --back option, the start date will be moved back to allow
|
|
40
|
+
the completion date to be the current time.)
|
|
41
|
+
c.arg_name 'INTERVAL'
|
|
42
|
+
c.flag %i[t took for], type: DateIntervalString
|
|
43
|
+
|
|
44
|
+
c.desc 'Section'
|
|
45
|
+
c.arg_name 'NAME'
|
|
46
|
+
c.flag %i[s section]
|
|
47
|
+
|
|
48
|
+
c.desc "Edit entry with #{Doing::Util.default_editor} (with no arguments, edits the last entry)"
|
|
49
|
+
c.switch %i[e editor], negatable: false, default_value: false
|
|
50
|
+
|
|
51
|
+
c.desc 'Include a note'
|
|
52
|
+
c.arg_name 'TEXT'
|
|
53
|
+
c.flag %i[n note]
|
|
54
|
+
|
|
55
|
+
c.desc 'Prompt for note via multi-line input'
|
|
56
|
+
c.switch %i[ask], negatable: false, default_value: false
|
|
57
|
+
|
|
58
|
+
c.desc 'Finish last entry not already marked @done'
|
|
59
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
60
|
+
|
|
61
|
+
# c.desc "Edit entry with specified app"
|
|
62
|
+
# c.arg_name 'editor_app'
|
|
63
|
+
# # c.flag [:a, :app]
|
|
64
|
+
|
|
65
|
+
c.action do |_global_options, options, args|
|
|
66
|
+
took = 0
|
|
67
|
+
donedate = nil
|
|
68
|
+
|
|
69
|
+
if options[:from]
|
|
70
|
+
options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
|
|
71
|
+
time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
|
|
72
|
+
end.join(' to ').split_date_range
|
|
73
|
+
date, finish_date = options[:from]
|
|
74
|
+
finish_date ||= Time.now
|
|
75
|
+
else
|
|
76
|
+
if options[:took]
|
|
77
|
+
took = options[:took]
|
|
78
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
if options[:back]
|
|
82
|
+
date = options[:back]
|
|
83
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
|
84
|
+
else
|
|
85
|
+
date = options[:took] ? Time.now - took : Time.now
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if options[:at]
|
|
89
|
+
finish_date = options[:at]
|
|
90
|
+
finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
|
|
91
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
92
|
+
|
|
93
|
+
if options[:took]
|
|
94
|
+
date = finish_date - took
|
|
95
|
+
else
|
|
96
|
+
date ||= finish_date
|
|
97
|
+
end
|
|
98
|
+
elsif options[:took]
|
|
99
|
+
finish_date = date + took
|
|
100
|
+
else
|
|
101
|
+
finish_date = Time.now
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
if options[:date]
|
|
106
|
+
date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
|
|
107
|
+
finish_date = @wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
|
|
108
|
+
|
|
109
|
+
donedate = finish_date.strftime('%F %R')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if options[:section]
|
|
113
|
+
section = @wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
114
|
+
else
|
|
115
|
+
section = @settings['current_section']
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
note = Doing::Note.new
|
|
120
|
+
note.add(options[:note]) if options[:note]
|
|
121
|
+
|
|
122
|
+
if options[:ask] && !options[:editor]
|
|
123
|
+
note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
if options[:editor]
|
|
127
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
128
|
+
is_new = false
|
|
129
|
+
|
|
130
|
+
if args.empty?
|
|
131
|
+
last_entry = @wwid.filter_items([], opt: { unfinished: options[:unfinished], section: section, count: 1, age: :newest }).max_by { |item| item.date }
|
|
132
|
+
|
|
133
|
+
unless last_entry
|
|
134
|
+
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
|
135
|
+
raise NoResults, 'No results'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
old_entry = last_entry.clone
|
|
139
|
+
last_entry.note.add(note)
|
|
140
|
+
input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
|
|
141
|
+
else
|
|
142
|
+
is_new = true
|
|
143
|
+
input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
input = @wwid.fork_editor(input).strip
|
|
147
|
+
raise EmptyInput, 'No content' unless input.good?
|
|
148
|
+
|
|
149
|
+
d, title, note = @wwid.format_input(input)
|
|
150
|
+
|
|
151
|
+
if options[:ask]
|
|
152
|
+
ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
|
|
153
|
+
note.add(ask_note) if ask_note.good?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
date = d.nil? ? date : d
|
|
157
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
158
|
+
if new_entry.should_finish?
|
|
159
|
+
if new_entry.should_time?
|
|
160
|
+
new_entry.tag('done', value: donedate)
|
|
161
|
+
else
|
|
162
|
+
new_entry.tag('done')
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
if (is_new)
|
|
167
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
168
|
+
@wwid.content.push(new_entry)
|
|
169
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
170
|
+
else
|
|
171
|
+
old = old_entry.clone
|
|
172
|
+
@wwid.content.update_item(old_entry, new_entry)
|
|
173
|
+
Doing::Hooks.trigger :post_entry_updated, @wwid, new_entry, old unless options[:archive]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
if options[:archive]
|
|
177
|
+
@wwid.move_item(new_entry, 'Archive', label: true)
|
|
178
|
+
Doing::Hooks.trigger :post_entry_updated, @wwid, new_entry, old_entry
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
@wwid.write(@wwid.doing_file)
|
|
182
|
+
elsif args.empty? && $stdin.stat.size.zero?
|
|
183
|
+
if options[:remove]
|
|
184
|
+
@wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
|
185
|
+
else
|
|
186
|
+
opt = {
|
|
187
|
+
archive: options[:archive],
|
|
188
|
+
back: finish_date,
|
|
189
|
+
count: 1,
|
|
190
|
+
date: options[:date],
|
|
191
|
+
note: note,
|
|
192
|
+
section: section,
|
|
193
|
+
tags: ['done'],
|
|
194
|
+
took: took == 0 ? nil : took,
|
|
195
|
+
unfinished: options[:unfinished]
|
|
196
|
+
}
|
|
197
|
+
@wwid.tag_last(opt)
|
|
198
|
+
end
|
|
199
|
+
elsif !args.empty?
|
|
200
|
+
d, title, new_note = @wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
|
|
201
|
+
date = d.nil? ? date : d
|
|
202
|
+
new_note.add(options[:note])
|
|
203
|
+
title.chomp!
|
|
204
|
+
section = 'Archive' if options[:archive]
|
|
205
|
+
new_entry = Doing::Item.new(date, title, section, new_note)
|
|
206
|
+
|
|
207
|
+
if new_entry.should_finish?
|
|
208
|
+
if new_entry.should_time?
|
|
209
|
+
new_entry.tag('done', value: donedate)
|
|
210
|
+
else
|
|
211
|
+
new_entry.tag('done')
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
216
|
+
@wwid.content.push(new_entry)
|
|
217
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
218
|
+
@wwid.write(@wwid.doing_file)
|
|
219
|
+
Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
|
|
220
|
+
elsif $stdin.stat.size.positive?
|
|
221
|
+
note = Doing::Note.new(options[:note])
|
|
222
|
+
d, title, note = @wwid.format_input($stdin.read.strip)
|
|
223
|
+
unless d.nil?
|
|
224
|
+
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
225
|
+
date = d
|
|
226
|
+
end
|
|
227
|
+
note.add(options[:note]) if options[:note]
|
|
228
|
+
section = options[:archive] ? 'Archive' : section
|
|
229
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
230
|
+
|
|
231
|
+
if new_entry.should_finish?
|
|
232
|
+
if new_entry.should_time?
|
|
233
|
+
new_entry.tag('done', value: donedate)
|
|
234
|
+
else
|
|
235
|
+
new_entry.tag('done')
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
240
|
+
@wwid.content.push(new_entry)
|
|
241
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
242
|
+
|
|
243
|
+
@wwid.write(@wwid.doing_file)
|
|
244
|
+
Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
|
|
245
|
+
else
|
|
246
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @@finish
|
|
2
|
+
desc 'Mark last X entries as @done'
|
|
3
|
+
long_desc 'Marks the last X entries with a @done tag and current date. Does not alter already completed entries.'
|
|
4
|
+
arg_name 'COUNT', optional: true
|
|
5
|
+
command :finish do |c|
|
|
6
|
+
c.example 'doing finish', desc: 'Mark the last entry @done'
|
|
7
|
+
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'
|
|
8
|
+
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'
|
|
9
|
+
|
|
10
|
+
c.desc 'Include date'
|
|
11
|
+
c.switch [:date], negatable: true, default_value: true
|
|
12
|
+
|
|
13
|
+
c.desc 'Backdate completed date to date string [4pm|20m|2h|yesterday noon]'
|
|
14
|
+
c.arg_name 'DATE_STRING'
|
|
15
|
+
c.flag %i[b back started], type: DateBeginString
|
|
16
|
+
|
|
17
|
+
c.desc 'Set the completed date to the start date plus XX[hmd]'
|
|
18
|
+
c.arg_name 'INTERVAL'
|
|
19
|
+
c.flag %i[t took for], type: DateIntervalString
|
|
20
|
+
|
|
21
|
+
c.desc %(Set finish date to specific date/time (natural langauge parsed, e.g. --at=1:30pm). If used, ignores --back.)
|
|
22
|
+
c.arg_name 'DATE_STRING'
|
|
23
|
+
c.flag %i[at finished], type: DateEndString
|
|
24
|
+
|
|
25
|
+
c.desc 'Finish the last X entries containing TAG.
|
|
26
|
+
Separate multiple tags with comma (--tag=tag1,tag2), combine with --bool. Wildcards allowed (*, ?).'
|
|
27
|
+
c.arg_name 'TAG'
|
|
28
|
+
c.flag [:tag], type: TagArray
|
|
29
|
+
|
|
30
|
+
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")'
|
|
31
|
+
c.arg_name 'QUERY'
|
|
32
|
+
c.flag [:search]
|
|
33
|
+
|
|
34
|
+
c.desc 'Perform a tag value query ("@done > two hours ago" or "@progress < 50"). May be used multiple times, combined with --bool'
|
|
35
|
+
c.arg_name 'QUERY'
|
|
36
|
+
c.flag [:val], multiple: true, must_match: REGEX_VALUE_QUERY
|
|
37
|
+
|
|
38
|
+
# c.desc '[DEPRECATED] Use alternative fuzzy matching for search string'
|
|
39
|
+
# c.switch [:fuzzy], default_value: false, negatable: false
|
|
40
|
+
|
|
41
|
+
c.desc 'Force exact search string matching (case sensitive)'
|
|
42
|
+
c.switch %i[x exact], default_value: @config.exact_match?, negatable: @config.exact_match?
|
|
43
|
+
|
|
44
|
+
c.desc 'Finish items that *don\'t* match search/tag filters'
|
|
45
|
+
c.switch [:not], default_value: false, negatable: false
|
|
46
|
+
|
|
47
|
+
c.desc 'Case sensitivity for search string matching [(c)ase-sensitive, (i)gnore, (s)mart]'
|
|
48
|
+
c.arg_name 'TYPE'
|
|
49
|
+
c.flag [:case], must_match: /^[csi]/, default_value: @settings.dig('search', 'case')
|
|
50
|
+
|
|
51
|
+
c.desc 'Boolean (AND|OR|NOT) with which to combine multiple tag filters. Use PATTERN to parse + and - as booleans'
|
|
52
|
+
c.arg_name 'BOOLEAN'
|
|
53
|
+
c.flag [:bool], must_match: REGEX_BOOL, default_value: 'PATTERN'
|
|
54
|
+
|
|
55
|
+
c.desc 'Remove done tag'
|
|
56
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
57
|
+
|
|
58
|
+
c.desc 'Finish last entry (or entries) not already marked @done'
|
|
59
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
60
|
+
|
|
61
|
+
c.desc %(Auto-generate finish dates from next entry's start time.
|
|
62
|
+
Automatically generate completion dates 1 minute before next item (in any section) began.
|
|
63
|
+
--auto overrides the --date and --back parameters.)
|
|
64
|
+
c.switch [:auto], negatable: false, default_value: false
|
|
65
|
+
|
|
66
|
+
c.desc 'Archive entries'
|
|
67
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
68
|
+
|
|
69
|
+
c.desc 'Section'
|
|
70
|
+
c.arg_name 'NAME'
|
|
71
|
+
c.flag %i[s section]
|
|
72
|
+
|
|
73
|
+
c.desc 'Select item(s) to finish from a menu of matching entries'
|
|
74
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
75
|
+
|
|
76
|
+
c.action do |_global_options, options, args|
|
|
77
|
+
options[:fuzzy] = false
|
|
78
|
+
unless options[:auto]
|
|
79
|
+
if options[:took]
|
|
80
|
+
took = options[:took]
|
|
81
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
|
85
|
+
|
|
86
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
87
|
+
|
|
88
|
+
if options[:at]
|
|
89
|
+
finish_date = options[:at]
|
|
90
|
+
finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
|
|
91
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
92
|
+
|
|
93
|
+
date = options[:took] ? finish_date - took : finish_date
|
|
94
|
+
elsif options[:back]
|
|
95
|
+
date = options[:back]
|
|
96
|
+
|
|
97
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
|
98
|
+
else
|
|
99
|
+
date = Time.now
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if options[:tag].nil?
|
|
104
|
+
tags = []
|
|
105
|
+
else
|
|
106
|
+
tags = options[:tag]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
|
110
|
+
|
|
111
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
|
112
|
+
|
|
113
|
+
if options[:interactive]
|
|
114
|
+
count = 0
|
|
115
|
+
else
|
|
116
|
+
count = args[0] ? args[0].to_i : 1
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
search = nil
|
|
120
|
+
|
|
121
|
+
if options[:search]
|
|
122
|
+
search = options[:search]
|
|
123
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
opts = {
|
|
127
|
+
archive: options[:archive],
|
|
128
|
+
back: date,
|
|
129
|
+
case: options[:case].normalize_case,
|
|
130
|
+
count: count,
|
|
131
|
+
date: options[:date],
|
|
132
|
+
fuzzy: options[:fuzzy],
|
|
133
|
+
interactive: options[:interactive],
|
|
134
|
+
not: options[:not],
|
|
135
|
+
remove: options[:remove],
|
|
136
|
+
search: search,
|
|
137
|
+
section: options[:section],
|
|
138
|
+
sequential: options[:auto],
|
|
139
|
+
tag: tags,
|
|
140
|
+
tag_bool: options[:bool].normalize_bool,
|
|
141
|
+
tags: ['done'],
|
|
142
|
+
took: options[:took],
|
|
143
|
+
unfinished: options[:unfinished],
|
|
144
|
+
val: options[:val]
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
@wwid.tag_last(opts)
|
|
148
|
+
end
|
|
149
|
+
end
|