doing 2.1.25 → 2.1.29
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 +15 -20
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +322 -108
- 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 +2 -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 +83 -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 -3706
- data/docs/doc/Array.html +3 -502
- 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 +62 -56
- data/docs/doc/Doing/Completion.html +1 -1
- data/docs/doc/Doing/Configuration.html +36 -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 +2 -2
- data/docs/doc/Doing/LogAdapter.html +1 -1
- data/docs/doc/Doing/Note.html +2 -2
- 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 +2 -2
- data/docs/doc/Doing/Types.html +41 -1
- data/docs/doc/Doing/Util/Backup.html +1 -1
- data/docs/doc/Doing/Util.html +1 -1
- data/docs/doc/Doing/WWID.html +10 -10
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +235 -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 +287 -3155
- data/docs/doc/Symbol.html +40 -6
- data/docs/doc/Time.html +1 -1
- data/docs/doc/TrueClass.html +235 -0
- data/docs/doc/_index.html +5 -10
- 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 +289 -681
- data/docs/doc/top-level-namespace.html +2 -2
- data/doing.rdoc +306 -205
- data/lib/completion/_doing.zsh +35 -35
- data/lib/completion/doing.bash +30 -30
- data/lib/completion/doing.fish +88 -78
- 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 +31 -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 +5 -0
- data/lib/doing/good.rb +8 -0
- data/lib/doing/help_monkey_patch.rb +6 -5
- data/lib/doing/item.rb +5 -5
- 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/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 +0 -22
- data/lib/doing/types.rb +8 -0
- data/lib/doing/util.rb +13 -9
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +53 -35
- data/lib/doing.rb +4 -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 +39 -20
- data/scripts/deploy.rb +107 -0
- data/scripts/runtests.sh +4 -0
- metadata +63 -8
- data/lib/doing/string.rb +0 -765
- data/lib/doing/symbol.rb +0 -28
|
@@ -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 = Doing::Util.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,235 @@
|
|
|
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 %(
|
|
28
|
+
Start and end times as a date/time range `doing done --from "1am to 8am"`.
|
|
29
|
+
Overrides other date flags.
|
|
30
|
+
)
|
|
31
|
+
c.arg_name 'TIME_RANGE'
|
|
32
|
+
c.flag [:from], must_match: REGEX_RANGE
|
|
33
|
+
|
|
34
|
+
c.desc %(Set completion date to start date plus interval (XX[mhd] or HH:MM).
|
|
35
|
+
If used without the --back option, the start date will be moved back to allow
|
|
36
|
+
the completion date to be the current time.)
|
|
37
|
+
c.arg_name 'INTERVAL'
|
|
38
|
+
c.flag %i[t took for], type: DateIntervalString
|
|
39
|
+
|
|
40
|
+
c.desc 'Section'
|
|
41
|
+
c.arg_name 'NAME'
|
|
42
|
+
c.flag %i[s section]
|
|
43
|
+
|
|
44
|
+
c.desc 'Finish last entry not already marked @done'
|
|
45
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
46
|
+
|
|
47
|
+
add_options(:add_entry, c)
|
|
48
|
+
|
|
49
|
+
c.action do |_global_options, options, args|
|
|
50
|
+
@wwid.auto_tag = !options[:noauto]
|
|
51
|
+
|
|
52
|
+
took = 0
|
|
53
|
+
donedate = nil
|
|
54
|
+
|
|
55
|
+
if options[:from]
|
|
56
|
+
options[:from] = options[:from].split(/#{REGEX_RANGE_INDICATOR}/).map do |time|
|
|
57
|
+
time =~ REGEX_TIME ? "today #{time.sub(/(?mi)(^.*?(?=\d+)|(?<=[ap]m).*?$)/, '')}" : time
|
|
58
|
+
end.join(' to ').split_date_range
|
|
59
|
+
date, finish_date = options[:from]
|
|
60
|
+
finish_date ||= Time.now
|
|
61
|
+
else
|
|
62
|
+
if options[:took]
|
|
63
|
+
took = options[:took]
|
|
64
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if options[:back]
|
|
68
|
+
date = options[:back]
|
|
69
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --back' if date.nil?
|
|
70
|
+
else
|
|
71
|
+
date = options[:took] ? Time.now - took : Time.now
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
if options[:at]
|
|
75
|
+
finish_date = options[:at]
|
|
76
|
+
finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
|
|
77
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
78
|
+
|
|
79
|
+
if options[:took]
|
|
80
|
+
date = finish_date - took
|
|
81
|
+
else
|
|
82
|
+
date ||= finish_date
|
|
83
|
+
end
|
|
84
|
+
elsif options[:took]
|
|
85
|
+
finish_date = date + took
|
|
86
|
+
else
|
|
87
|
+
finish_date = Time.now
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if options[:date]
|
|
92
|
+
date = date.chronify(guess: :begin, context: :today) if date =~ REGEX_TIME
|
|
93
|
+
finish_date = @wwid.verify_duration(date, finish_date) unless options[:took] || options[:from]
|
|
94
|
+
|
|
95
|
+
donedate = finish_date.strftime('%F %R')
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
if options[:section]
|
|
99
|
+
section = @wwid.guess_section(options[:section]) || options[:section].cap_first
|
|
100
|
+
else
|
|
101
|
+
section = @settings['current_section']
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
note = Doing::Note.new
|
|
106
|
+
note.add(options[:note]) if options[:note]
|
|
107
|
+
|
|
108
|
+
if options[:ask] && !options[:editor]
|
|
109
|
+
note.add(Doing::Prompt.read_lines(prompt: 'Add a note'))
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
if options[:editor]
|
|
113
|
+
raise MissingEditor, 'No EDITOR variable defined in environment' if Doing::Util.default_editor.nil?
|
|
114
|
+
is_new = false
|
|
115
|
+
|
|
116
|
+
if args.empty?
|
|
117
|
+
last_entry = @wwid.filter_items([], opt: { unfinished: options[:unfinished], section: section, count: 1, age: :newest }).max_by { |item| item.date }
|
|
118
|
+
|
|
119
|
+
unless last_entry
|
|
120
|
+
Doing.logger.debug('Skipped:', options[:unfinished] ? 'No unfinished entry' : 'Last entry already @done')
|
|
121
|
+
raise NoResults, 'No results'
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
old_entry = last_entry.clone
|
|
125
|
+
last_entry.note.add(note)
|
|
126
|
+
input = ["#{last_entry.date.strftime('%F %R | ')}#{last_entry.title}", last_entry.note.strip_lines.join("\n")].join("\n")
|
|
127
|
+
else
|
|
128
|
+
is_new = true
|
|
129
|
+
input = ["#{date.strftime('%F %R | ')}#{args.join(' ')}", note.strip_lines.join("\n")].join("\n")
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
input = @wwid.fork_editor(input).strip
|
|
133
|
+
raise EmptyInput, 'No content' unless input.good?
|
|
134
|
+
|
|
135
|
+
d, title, note = @wwid.format_input(input)
|
|
136
|
+
|
|
137
|
+
if options[:ask]
|
|
138
|
+
ask_note = Doing::Prompt.read_lines(prompt: 'Add a note')
|
|
139
|
+
note.add(ask_note) if ask_note.good?
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
date = d.nil? ? date : d
|
|
143
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
144
|
+
if new_entry.should_finish?
|
|
145
|
+
if new_entry.should_time?
|
|
146
|
+
new_entry.tag('done', value: donedate)
|
|
147
|
+
else
|
|
148
|
+
new_entry.tag('done')
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
if (is_new)
|
|
153
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
154
|
+
@wwid.content.push(new_entry)
|
|
155
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
156
|
+
else
|
|
157
|
+
old = old_entry.clone
|
|
158
|
+
@wwid.content.update_item(old_entry, new_entry)
|
|
159
|
+
Doing::Hooks.trigger :post_entry_updated, @wwid, new_entry, old unless options[:archive]
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
if options[:archive]
|
|
163
|
+
@wwid.move_item(new_entry, 'Archive', label: true)
|
|
164
|
+
Doing::Hooks.trigger :post_entry_updated, @wwid, new_entry, old_entry
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
@wwid.write(@wwid.doing_file)
|
|
168
|
+
elsif args.empty? && $stdin.stat.size.zero?
|
|
169
|
+
if options[:remove]
|
|
170
|
+
@wwid.tag_last({ tags: ['done'], count: 1, section: section, remove: true })
|
|
171
|
+
else
|
|
172
|
+
opt = {
|
|
173
|
+
archive: options[:archive],
|
|
174
|
+
back: finish_date,
|
|
175
|
+
count: 1,
|
|
176
|
+
date: options[:date],
|
|
177
|
+
note: note,
|
|
178
|
+
section: section,
|
|
179
|
+
tags: ['done'],
|
|
180
|
+
took: took == 0 ? nil : took,
|
|
181
|
+
unfinished: options[:unfinished]
|
|
182
|
+
}
|
|
183
|
+
@wwid.tag_last(opt)
|
|
184
|
+
end
|
|
185
|
+
elsif !args.empty?
|
|
186
|
+
d, title, new_note = @wwid.format_input([args.join(' '), note.strip_lines.join("\n")].join("\n"))
|
|
187
|
+
date = d.nil? ? date : d
|
|
188
|
+
new_note.add(options[:note])
|
|
189
|
+
title.chomp!
|
|
190
|
+
section = 'Archive' if options[:archive]
|
|
191
|
+
new_entry = Doing::Item.new(date, title, section, new_note)
|
|
192
|
+
|
|
193
|
+
if new_entry.should_finish?
|
|
194
|
+
if new_entry.should_time?
|
|
195
|
+
new_entry.tag('done', value: donedate)
|
|
196
|
+
else
|
|
197
|
+
new_entry.tag('done')
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
202
|
+
@wwid.content.push(new_entry)
|
|
203
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
204
|
+
@wwid.write(@wwid.doing_file)
|
|
205
|
+
Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
|
|
206
|
+
elsif $stdin.stat.size.positive?
|
|
207
|
+
note = Doing::Note.new(options[:note])
|
|
208
|
+
d, title, note = @wwid.format_input($stdin.read.strip)
|
|
209
|
+
unless d.nil?
|
|
210
|
+
Doing.logger.debug('Parser:', 'Date detected in input, overriding command line values')
|
|
211
|
+
date = d
|
|
212
|
+
end
|
|
213
|
+
note.add(options[:note]) if options[:note]
|
|
214
|
+
section = options[:archive] ? 'Archive' : section
|
|
215
|
+
new_entry = Doing::Item.new(date, title, section, note)
|
|
216
|
+
|
|
217
|
+
if new_entry.should_finish?
|
|
218
|
+
if new_entry.should_time?
|
|
219
|
+
new_entry.tag('done', value: donedate)
|
|
220
|
+
else
|
|
221
|
+
new_entry.tag('done')
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
Doing::Hooks.trigger :pre_entry_add, @wwid, new_entry
|
|
226
|
+
@wwid.content.push(new_entry)
|
|
227
|
+
Doing::Hooks.trigger :post_entry_added, @wwid, new_entry
|
|
228
|
+
|
|
229
|
+
@wwid.write(@wwid.doing_file)
|
|
230
|
+
Doing.logger.info('New entry:', %(added "#{new_entry.date.relative_date}: #{new_entry.title}" to #{section}))
|
|
231
|
+
else
|
|
232
|
+
raise EmptyInput, 'You must provide content when creating a new entry'
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
end
|
|
@@ -0,0 +1,126 @@
|
|
|
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 'Overwrite existing @done tag with new date'
|
|
26
|
+
c.switch %i[update], negatable: false, default_value: false
|
|
27
|
+
|
|
28
|
+
c.desc 'Remove @done tag'
|
|
29
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
30
|
+
|
|
31
|
+
c.desc 'Finish last entry (or entries) not already marked @done'
|
|
32
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
33
|
+
|
|
34
|
+
c.desc %(Auto-generate finish dates from next entry's start time.
|
|
35
|
+
Automatically generate completion dates 1 minute before next item (in any section) began.
|
|
36
|
+
--auto overrides the --date and --back parameters.)
|
|
37
|
+
c.switch [:auto], negatable: false, default_value: false
|
|
38
|
+
|
|
39
|
+
c.desc 'Archive entries'
|
|
40
|
+
c.switch %i[a archive], negatable: false, default_value: false
|
|
41
|
+
|
|
42
|
+
c.desc 'Section'
|
|
43
|
+
c.arg_name 'NAME'
|
|
44
|
+
c.flag %i[s section]
|
|
45
|
+
|
|
46
|
+
c.desc 'Select item(s) to finish from a menu of matching entries'
|
|
47
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
48
|
+
|
|
49
|
+
add_options(:search, c)
|
|
50
|
+
add_options(:tag_filter, c)
|
|
51
|
+
|
|
52
|
+
c.action do |_global_options, options, args|
|
|
53
|
+
options[:fuzzy] = false
|
|
54
|
+
unless options[:auto]
|
|
55
|
+
if options[:took]
|
|
56
|
+
took = options[:took]
|
|
57
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --took' if took.nil?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
raise InvalidArgument, '--back and --took can not be used together' if options[:back] && options[:took]
|
|
61
|
+
|
|
62
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
63
|
+
|
|
64
|
+
if options[:at]
|
|
65
|
+
finish_date = options[:at]
|
|
66
|
+
finish_date = finish_date.chronify(guess: :begin) if finish_date.is_a? String
|
|
67
|
+
raise InvalidTimeExpression, 'Unable to parse date string for --at' if finish_date.nil?
|
|
68
|
+
|
|
69
|
+
date = options[:took] ? finish_date - took : finish_date
|
|
70
|
+
elsif options[:back]
|
|
71
|
+
date = options[:back]
|
|
72
|
+
|
|
73
|
+
raise InvalidTimeExpression, 'Unable to parse date string' if date.nil?
|
|
74
|
+
else
|
|
75
|
+
date = Time.now
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
if options[:tag].nil?
|
|
80
|
+
tags = []
|
|
81
|
+
else
|
|
82
|
+
tags = options[:tag]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
raise InvalidArgument, 'Only one argument allowed' if args.length > 1
|
|
86
|
+
|
|
87
|
+
raise InvalidArgument, 'Invalid argument (specify number of recent items to mark @done)' unless args.length == 0 || args[0] =~ /\d+/
|
|
88
|
+
|
|
89
|
+
if options[:interactive]
|
|
90
|
+
count = 0
|
|
91
|
+
else
|
|
92
|
+
count = args[0] ? args[0].to_i : 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
search = nil
|
|
96
|
+
|
|
97
|
+
if options[:search]
|
|
98
|
+
search = options[:search]
|
|
99
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
opts = {
|
|
103
|
+
archive: options[:archive],
|
|
104
|
+
back: date,
|
|
105
|
+
case: options[:case].normalize_case,
|
|
106
|
+
count: count,
|
|
107
|
+
date: options[:date],
|
|
108
|
+
fuzzy: options[:fuzzy],
|
|
109
|
+
interactive: options[:interactive],
|
|
110
|
+
not: options[:not],
|
|
111
|
+
remove: options[:remove],
|
|
112
|
+
search: search,
|
|
113
|
+
section: options[:section],
|
|
114
|
+
sequential: options[:auto],
|
|
115
|
+
tag: tags,
|
|
116
|
+
tag_bool: options[:bool].normalize_bool,
|
|
117
|
+
tags: ['done'],
|
|
118
|
+
took: options[:took],
|
|
119
|
+
unfinished: options[:unfinished],
|
|
120
|
+
update: options[:update],
|
|
121
|
+
val: options[:val]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@wwid.tag_last(opts)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @@mark @@flag
|
|
2
|
+
desc 'Mark last entry as flagged'
|
|
3
|
+
command %i[mark flag] do |c|
|
|
4
|
+
c.example 'doing flag', desc: 'Add @flagged to the last entry created'
|
|
5
|
+
c.example 'doing mark', desc: 'mark is an alias for flag'
|
|
6
|
+
c.example 'doing flag --tag project1 --count 2', desc: 'Add @flagged to the last 2 entries tagged @project1'
|
|
7
|
+
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'
|
|
8
|
+
|
|
9
|
+
c.desc 'Section'
|
|
10
|
+
c.arg_name 'SECTION_NAME'
|
|
11
|
+
c.flag %i[s section], default_value: 'All'
|
|
12
|
+
|
|
13
|
+
c.desc 'How many recent entries to tag (0 for all)'
|
|
14
|
+
c.arg_name 'COUNT'
|
|
15
|
+
c.flag %i[c count], default_value: 1, must_match: /^\d+$/, type: Integer
|
|
16
|
+
|
|
17
|
+
c.desc 'Don\'t ask permission to flag all entries when count is 0'
|
|
18
|
+
c.switch %i[force], negatable: false, default_value: false
|
|
19
|
+
|
|
20
|
+
c.desc 'Include current date/time with tag'
|
|
21
|
+
c.switch %i[d date], negatable: false, default_value: false
|
|
22
|
+
|
|
23
|
+
c.desc 'Remove flag'
|
|
24
|
+
c.switch %i[r remove], negatable: false, default_value: false
|
|
25
|
+
|
|
26
|
+
c.desc 'Flag last entry (or entries) not marked @done'
|
|
27
|
+
c.switch %i[u unfinished], negatable: false, default_value: false
|
|
28
|
+
|
|
29
|
+
c.desc 'Select item(s) to flag from a menu of matching entries'
|
|
30
|
+
c.switch %i[i interactive], negatable: false, default_value: false
|
|
31
|
+
|
|
32
|
+
add_options(:search, c)
|
|
33
|
+
add_options(:tag_filter, c)
|
|
34
|
+
|
|
35
|
+
c.action do |_global_options, options, _args|
|
|
36
|
+
options[:fuzzy] = false
|
|
37
|
+
mark = @settings['marker_tag'] || 'flagged'
|
|
38
|
+
|
|
39
|
+
raise InvalidArgument, '--search and --tag can not be used together' if options[:search] && options[:tag]
|
|
40
|
+
|
|
41
|
+
section = 'All'
|
|
42
|
+
|
|
43
|
+
section = @wwid.guess_section(options[:section]) || options[:section].cap_first if options[:section]
|
|
44
|
+
|
|
45
|
+
search_tags = options[:tag].nil? ? [] : options[:tag]
|
|
46
|
+
|
|
47
|
+
if options[:interactive]
|
|
48
|
+
count = 0
|
|
49
|
+
options[:force] = true
|
|
50
|
+
else
|
|
51
|
+
count = options[:count].to_i
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if options[:search]
|
|
55
|
+
search = options[:search]
|
|
56
|
+
search.sub!(/^'?/, "'") if options[:exact]
|
|
57
|
+
options[:search] = search
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
if count.zero? && !options[:force]
|
|
61
|
+
section_q = if options[:search]
|
|
62
|
+
' matching your search terms'
|
|
63
|
+
elsif options[:tag]
|
|
64
|
+
' matching your tag search'
|
|
65
|
+
elsif section == 'All'
|
|
66
|
+
''
|
|
67
|
+
else
|
|
68
|
+
" in section #{section}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
question = if options[:remove]
|
|
72
|
+
"Are you sure you want to unflag all entries#{section_q}"
|
|
73
|
+
else
|
|
74
|
+
"Are you sure you want to flag all records#{section_q}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
res = Doing::Prompt.yn(question, default_response: false)
|
|
78
|
+
|
|
79
|
+
exit_now! 'Cancelled' unless res
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
options[:count] = count
|
|
83
|
+
options[:section] = section
|
|
84
|
+
options[:tag] = search_tags
|
|
85
|
+
options[:tags] = [mark]
|
|
86
|
+
options[:tag_bool] = options[:bool]
|
|
87
|
+
|
|
88
|
+
@wwid.tag_last(options)
|
|
89
|
+
end
|
|
90
|
+
end
|