doing 2.1.25 → 2.1.26

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