doing 2.1.25 → 2.1.26

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