doing 2.1.22 → 2.1.26
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.yardoc/checksums +17 -14
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +323 -111
- data/Gemfile.lock +1 -1
- data/README.md +1 -1
- data/Rakefile +2 -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 +54 -3505
- data/docs/doc/Array.html +79 -11
- 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 +4 -4
- data/docs/doc/Doing/Completion.html +2 -2
- data/docs/doc/Doing/Configuration.html +17 -18
- 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 +206 -0
- data/docs/doc/Doing/Util/Backup.html +10 -10
- data/docs/doc/Doing/Util.html +16 -19
- data/docs/doc/Doing/WWID.html +65 -53
- data/docs/doc/Doing.html +3 -3
- data/docs/doc/FalseClass.html +201 -0
- data/docs/doc/GLI/Commands/Help.html +185 -0
- data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +17 -17
- data/docs/doc/GLI/Commands.html +5 -3
- data/docs/doc/GLI.html +4 -2
- data/docs/doc/Hash.html +47 -21
- 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 +144 -51
- data/docs/doc/Symbol.html +8 -8
- data/docs/doc/Time.html +6 -6
- data/docs/doc/TrueClass.html +201 -0
- data/docs/doc/_index.html +46 -16
- 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 +292 -212
- data/docs/doc/top-level-namespace.html +2 -2
- data/docs/index.md +1 -1
- data/doing.rdoc +178 -16
- data/example_plugin.rb +2 -2
- data/lib/completion/_doing.zsh +27 -27
- data/lib/completion/doing.bash +31 -20
- data/lib/completion/doing.fish +33 -11
- data/lib/doing/array.rb +2 -2
- data/lib/doing/changelog/change.rb +115 -0
- data/lib/doing/changelog/changes.rb +73 -0
- data/lib/doing/changelog/entry.rb +21 -0
- data/lib/doing/changelog/version.rb +97 -0
- data/lib/doing/changelog.rb +6 -0
- data/lib/doing/completion/fish_completion.rb +2 -1
- data/lib/doing/configuration.rb +20 -13
- data/lib/doing/good.rb +64 -0
- data/lib/doing/hash.rb +7 -2
- data/lib/doing/help_monkey_patch.rb +31 -0
- data/lib/doing/hooks.rb +8 -4
- data/lib/doing/item.rb +24 -35
- data/lib/doing/pager.rb +1 -0
- data/lib/doing/plugins/export/template_export.rb +1 -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 +8 -0
- data/lib/doing/string.rb +20 -11
- data/lib/doing/string_chronify.rb +1 -1
- data/lib/doing/template_string.rb +2 -2
- data/lib/doing/types.rb +3 -0
- data/lib/doing/util.rb +12 -11
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +62 -37
- data/lib/doing.rb +2 -0
- data/lib/examples/commands/wiki.rb +6 -7
- data/lib/helpers/threaded_tests.rb +61 -71
- data/lib/helpers/threaded_tests_string.rb +50 -0
- metadata +56 -2
@@ -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
|