doing 2.1.25 → 2.1.29

Sign up to get free protection for your applications and to get access to all the features.
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