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.
Files changed (163) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +15 -20
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +322 -108
  6. data/Dockerfile +5 -5
  7. data/Dockerfile-2.6 +5 -5
  8. data/Dockerfile-2.7 +5 -4
  9. data/Dockerfile-3.0 +5 -4
  10. data/Gemfile.lock +1 -1
  11. data/README.md +1 -1
  12. data/Rakefile +2 -3
  13. data/bin/commands/add_section.rb +15 -0
  14. data/bin/commands/again.rb +57 -0
  15. data/bin/commands/archive.rb +55 -0
  16. data/bin/commands/cancel.rb +60 -0
  17. data/bin/commands/changes.rb +83 -0
  18. data/bin/commands/choose.rb +9 -0
  19. data/bin/commands/colors.rb +21 -0
  20. data/bin/commands/commands.rb +89 -0
  21. data/bin/commands/commands_accepting.rb +76 -0
  22. data/bin/commands/completion.rb +27 -0
  23. data/bin/commands/config.rb +245 -0
  24. data/bin/commands/done.rb +235 -0
  25. data/bin/commands/finish.rb +126 -0
  26. data/bin/commands/flag.rb +90 -0
  27. data/bin/commands/grep.rb +108 -0
  28. data/bin/commands/import.rb +71 -0
  29. data/bin/commands/install_fzf.rb +17 -0
  30. data/bin/commands/last.rb +81 -0
  31. data/bin/commands/meanwhile.rb +76 -0
  32. data/bin/commands/note.rb +91 -0
  33. data/bin/commands/now.rb +145 -0
  34. data/bin/commands/on.rb +65 -0
  35. data/bin/commands/open.rb +53 -0
  36. data/bin/commands/plugins.rb +23 -0
  37. data/bin/commands/recent.rb +77 -0
  38. data/bin/commands/redo.rb +26 -0
  39. data/bin/commands/reset.rb +73 -0
  40. data/bin/commands/rotate.rb +42 -0
  41. data/bin/commands/sections.rb +11 -0
  42. data/bin/commands/select.rb +105 -0
  43. data/bin/commands/show.rb +185 -0
  44. data/bin/commands/since.rb +63 -0
  45. data/bin/commands/tag.rb +149 -0
  46. data/bin/commands/tag_dir.rb +29 -0
  47. data/bin/commands/tags.rb +66 -0
  48. data/bin/commands/template.rb +61 -0
  49. data/bin/commands/today.rb +64 -0
  50. data/bin/commands/undo.rb +49 -0
  51. data/bin/commands/view.rb +201 -0
  52. data/bin/commands/views.rb +11 -0
  53. data/bin/commands/yesterday.rb +72 -0
  54. data/bin/doing +241 -3706
  55. data/docs/doc/Array.html +3 -502
  56. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  57. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  58. data/docs/doc/BooleanTermParser/Query.html +1 -1
  59. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  60. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  61. data/docs/doc/BooleanTermParser.html +1 -1
  62. data/docs/doc/Doing/Color.html +62 -56
  63. data/docs/doc/Doing/Completion.html +1 -1
  64. data/docs/doc/Doing/Configuration.html +36 -1
  65. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  66. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  67. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  68. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  69. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  70. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  71. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  72. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  73. data/docs/doc/Doing/Errors.html +1 -1
  74. data/docs/doc/Doing/Hooks.html +1 -1
  75. data/docs/doc/Doing/Item.html +1 -1
  76. data/docs/doc/Doing/Items.html +2 -2
  77. data/docs/doc/Doing/LogAdapter.html +1 -1
  78. data/docs/doc/Doing/Note.html +2 -2
  79. data/docs/doc/Doing/Pager.html +1 -1
  80. data/docs/doc/Doing/Plugins.html +1 -1
  81. data/docs/doc/Doing/Prompt.html +46 -1
  82. data/docs/doc/Doing/Section.html +1 -1
  83. data/docs/doc/Doing/TemplateString.html +2 -2
  84. data/docs/doc/Doing/Types.html +41 -1
  85. data/docs/doc/Doing/Util/Backup.html +1 -1
  86. data/docs/doc/Doing/Util.html +1 -1
  87. data/docs/doc/Doing/WWID.html +10 -10
  88. data/docs/doc/Doing.html +3 -3
  89. data/docs/doc/FalseClass.html +235 -0
  90. data/docs/doc/GLI/Commands/Help.html +1 -1
  91. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  92. data/docs/doc/GLI/Commands.html +1 -1
  93. data/docs/doc/GLI.html +1 -1
  94. data/docs/doc/Hash.html +1 -1
  95. data/docs/doc/Numeric.html +1 -1
  96. data/docs/doc/Object.html +203 -0
  97. data/docs/doc/PhraseParser/Operator.html +1 -1
  98. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  99. data/docs/doc/PhraseParser/Query.html +1 -1
  100. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  101. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  102. data/docs/doc/PhraseParser/TermClause.html +1 -1
  103. data/docs/doc/PhraseParser.html +1 -1
  104. data/docs/doc/Status.html +1 -1
  105. data/docs/doc/String.html +287 -3155
  106. data/docs/doc/Symbol.html +40 -6
  107. data/docs/doc/Time.html +1 -1
  108. data/docs/doc/TrueClass.html +235 -0
  109. data/docs/doc/_index.html +5 -10
  110. data/docs/doc/class_list.html +1 -1
  111. data/docs/doc/file.README.html +2 -2
  112. data/docs/doc/index.html +2 -2
  113. data/docs/doc/method_list.html +289 -681
  114. data/docs/doc/top-level-namespace.html +2 -2
  115. data/doing.rdoc +306 -205
  116. data/lib/completion/_doing.zsh +35 -35
  117. data/lib/completion/doing.bash +30 -30
  118. data/lib/completion/doing.fish +88 -78
  119. data/lib/doing/array/array.rb +4 -0
  120. data/lib/doing/array/nested_hash.rb +17 -0
  121. data/lib/doing/{array.rb → array/tags.rb} +7 -25
  122. data/lib/doing/changelog/change.rb +26 -11
  123. data/lib/doing/changelog/changes.rb +31 -4
  124. data/lib/doing/{array_chronify.rb → chronify/array.rb} +0 -0
  125. data/lib/doing/chronify/chronify.rb +5 -0
  126. data/lib/doing/{numeric_chronify.rb → chronify/numeric.rb} +0 -0
  127. data/lib/doing/{string_chronify.rb → chronify/string.rb} +0 -0
  128. data/lib/doing/colors.rb +115 -54
  129. data/lib/doing/configuration.rb +5 -0
  130. data/lib/doing/good.rb +8 -0
  131. data/lib/doing/help_monkey_patch.rb +6 -5
  132. data/lib/doing/item.rb +5 -5
  133. data/lib/doing/items.rb +2 -2
  134. data/lib/doing/log_adapter.rb +35 -2
  135. data/lib/doing/normalize.rb +188 -0
  136. data/lib/doing/pager.rb +1 -0
  137. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  138. data/lib/doing/plugins/export/html_export.rb +1 -1
  139. data/lib/doing/plugins/export/json_export.rb +1 -1
  140. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  141. data/lib/doing/plugins/export/template_export.rb +3 -1
  142. data/lib/doing/prompt.rb +9 -3
  143. data/lib/doing/string/highlight.rb +95 -0
  144. data/lib/doing/string/query.rb +129 -0
  145. data/lib/doing/string/string.rb +12 -0
  146. data/lib/doing/string/tags.rb +164 -0
  147. data/lib/doing/string/transform.rb +168 -0
  148. data/lib/doing/string/truncate.rb +75 -0
  149. data/lib/doing/string/url.rb +82 -0
  150. data/lib/doing/template_string.rb +0 -22
  151. data/lib/doing/types.rb +8 -0
  152. data/lib/doing/util.rb +13 -9
  153. data/lib/doing/version.rb +1 -1
  154. data/lib/doing/wwid.rb +53 -35
  155. data/lib/doing.rb +4 -6
  156. data/lib/examples/commands/wiki.rb +6 -7
  157. data/lib/examples/plugins/wiki_export/wiki_export.rb +1 -1
  158. data/lib/helpers/threaded_tests.rb +39 -20
  159. data/scripts/deploy.rb +107 -0
  160. data/scripts/runtests.sh +4 -0
  161. metadata +63 -8
  162. data/lib/doing/string.rb +0 -765
  163. 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