doing 2.0.22 → 2.1.0pre
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.
- checksums.yaml +4 -4
- data/.yardoc/checksums +18 -15
- data/.yardoc/object_types +0 -0
- data/.yardoc/objects/root.dat +0 -0
- data/CHANGELOG.md +36 -1
- data/Gemfile.lock +8 -1
- data/README.md +7 -1
- data/Rakefile +23 -4
- data/bin/doing +323 -173
- data/doc/Array.html +354 -1
- data/doc/Doing/Color.html +104 -92
- data/doc/Doing/Completion.html +216 -0
- data/doc/Doing/Configuration.html +340 -5
- data/doc/Doing/Content.html +229 -0
- data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
- data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
- data/doc/Doing/Errors/DoingStandardError.html +1 -1
- data/doc/Doing/Errors/EmptyInput.html +1 -1
- data/doc/Doing/Errors/NoResults.html +1 -1
- data/doc/Doing/Errors/PluginException.html +1 -1
- data/doc/Doing/Errors/UserCancelled.html +1 -1
- data/doc/Doing/Errors/WrongCommand.html +1 -1
- data/doc/Doing/Errors.html +1 -1
- data/doc/Doing/Hooks.html +1 -1
- data/doc/Doing/Item.html +337 -49
- data/doc/Doing/Items.html +444 -35
- data/doc/Doing/LogAdapter.html +139 -51
- data/doc/Doing/Note.html +253 -22
- data/doc/Doing/Pager.html +74 -36
- data/doc/Doing/Plugins.html +1 -1
- data/doc/Doing/Prompt.html +674 -0
- data/doc/Doing/Section.html +354 -0
- data/doc/Doing/Util.html +57 -1
- data/doc/Doing/WWID.html +477 -670
- data/doc/Doing/WWIDFile.html +398 -0
- data/doc/Doing.html +5 -5
- data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
- data/doc/GLI/Commands.html +1 -1
- data/doc/GLI.html +1 -1
- data/doc/Hash.html +97 -1
- data/doc/Status.html +37 -3
- data/doc/String.html +599 -23
- data/doc/Symbol.html +3 -3
- data/doc/Time.html +1 -1
- data/doc/_index.html +22 -1
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +8 -2
- data/doc/index.html +8 -2
- data/doc/method_list.html +453 -173
- data/doc/top-level-namespace.html +1 -1
- data/doing.gemspec +3 -0
- data/doing.rdoc +79 -27
- data/example_plugin.rb +5 -5
- data/lib/completion/_doing.zsh +42 -42
- data/lib/completion/doing.bash +10 -10
- data/lib/completion/doing.fish +1 -280
- data/lib/doing/array.rb +36 -0
- data/lib/doing/colors.rb +70 -66
- data/lib/doing/completion/bash_completion.rb +1 -2
- data/lib/doing/completion/fish_completion.rb +1 -1
- data/lib/doing/completion/zsh_completion.rb +1 -1
- data/lib/doing/completion.rb +6 -0
- data/lib/doing/configuration.rb +134 -23
- data/lib/doing/hash.rb +37 -0
- data/lib/doing/item.rb +77 -12
- data/lib/doing/items.rb +125 -0
- data/lib/doing/log_adapter.rb +58 -4
- data/lib/doing/note.rb +53 -1
- data/lib/doing/pager.rb +49 -38
- data/lib/doing/plugins/export/markdown_export.rb +4 -4
- data/lib/doing/plugins/export/template_export.rb +2 -2
- data/lib/doing/plugins/import/calendar_import.rb +4 -4
- data/lib/doing/plugins/import/doing_import.rb +5 -7
- data/lib/doing/plugins/import/timing_import.rb +3 -3
- data/lib/doing/prompt.rb +206 -0
- data/lib/doing/section.rb +30 -0
- data/lib/doing/string.rb +123 -35
- data/lib/doing/util.rb +14 -6
- data/lib/doing/version.rb +1 -1
- data/lib/doing/wwid.rb +307 -614
- data/lib/doing.rb +6 -2
- data/lib/examples/plugins/capture_thing_import.rb +162 -0
- data/rdoc_to_mmd.rb +14 -8
- data/scripts/generate_bash_completions.rb +1 -1
- data/scripts/generate_fish_completions.rb +1 -1
- data/scripts/generate_zsh_completions.rb +1 -1
- metadata +73 -5
- data/lib/doing/wwidfile.rb +0 -117
data/lib/doing/configuration.rb
CHANGED
@@ -90,21 +90,70 @@ module Doing
|
|
90
90
|
}
|
91
91
|
|
92
92
|
def initialize(file = nil, options: {})
|
93
|
-
|
94
|
-
cf = File.expand_path(file)
|
95
|
-
# raise MissingConfigFile, "Config not found (#{cf})" unless File.exist?(cf)
|
93
|
+
@config_file = file.nil? ? default_config_file : File.expand_path(file)
|
96
94
|
|
97
|
-
|
95
|
+
@settings = configure(options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def config_file
|
99
|
+
@config_file ||= default_config_file
|
100
|
+
end
|
101
|
+
|
102
|
+
def config_file=(file)
|
103
|
+
@config_file = file
|
104
|
+
end
|
105
|
+
|
106
|
+
def config_dir
|
107
|
+
@config_dir ||= File.join(Util.user_home, '.config', 'doing')
|
108
|
+
# @config_dir ||= Util.user_home
|
109
|
+
end
|
110
|
+
|
111
|
+
def default_config_file
|
112
|
+
raise DoingRuntimeError, "#{config_dir} exists but is not a directory" if File.exist?(config_dir) && !File.directory?(config_dir)
|
113
|
+
|
114
|
+
unless File.exist?(config_dir)
|
115
|
+
FileUtils.mkdir_p(config_dir)
|
116
|
+
Doing.logger.log_now(:warn, "Config directory created at #{config_dir}")
|
98
117
|
end
|
99
118
|
|
100
|
-
|
119
|
+
# File.join(config_dir, 'config.yml')
|
120
|
+
File.join(config_dir, 'config.yml')
|
101
121
|
end
|
102
122
|
|
103
123
|
def additional_configs
|
104
124
|
@additional_configs ||= find_local_config
|
105
125
|
end
|
106
126
|
|
107
|
-
|
127
|
+
##
|
128
|
+
## Present a menu if there are multiple configs found
|
129
|
+
##
|
130
|
+
## @return [String] file path
|
131
|
+
##
|
132
|
+
def choose_config
|
133
|
+
if @additional_configs.count.positive?
|
134
|
+
choices = [@config_file]
|
135
|
+
choices.concat(@additional_configs)
|
136
|
+
res = Doing::Prompt.choose_from(choices.uniq.sort.reverse, sorted: false, prompt: 'Local configs found, select which to update > ')
|
137
|
+
|
138
|
+
raise UserCancelled, 'Cancelled' unless res
|
139
|
+
|
140
|
+
res.strip || @config_file
|
141
|
+
else
|
142
|
+
@config_file
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
## Resolve a fuzzy-matched key path
|
148
|
+
##
|
149
|
+
## @param keypath [String] A dot-separated key
|
150
|
+
## path, e.g.
|
151
|
+
## "plugins.plugin_path". Will also
|
152
|
+
## work with "plug.path" (fuzzy
|
153
|
+
## matched, first match wins)
|
154
|
+
## @return [Array] ordered array of resolved keys
|
155
|
+
##
|
156
|
+
def resolve_key_path(keypath)
|
108
157
|
cfg = @settings
|
109
158
|
real_path = []
|
110
159
|
unless keypath =~ /^[.*]?$/
|
@@ -113,7 +162,8 @@ module Doing
|
|
113
162
|
path = paths.shift
|
114
163
|
new_cfg = nil
|
115
164
|
cfg.each do |key, val|
|
116
|
-
next unless key =~ path.to_rx(distance:
|
165
|
+
next unless key =~ path.to_rx(distance: 4)
|
166
|
+
|
117
167
|
real_path << key
|
118
168
|
new_cfg = val
|
119
169
|
break
|
@@ -127,9 +177,30 @@ module Doing
|
|
127
177
|
end
|
128
178
|
end
|
129
179
|
Doing.logger.debug('Config:', "translated key path #{keypath} to #{real_path.join('.')}")
|
130
|
-
|
131
|
-
|
132
|
-
|
180
|
+
real_path
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
## Get the value for a fuzzy-matched key path
|
185
|
+
##
|
186
|
+
## @param keypath [String] A dot-separated key
|
187
|
+
## path, e.g.
|
188
|
+
## "plugins.plugin_path". Will also
|
189
|
+
## work with "plug.path" (fuzzy
|
190
|
+
## matched, first match wins)
|
191
|
+
## @return [Hash] Config value
|
192
|
+
##
|
193
|
+
def value_for_key(keypath = '')
|
194
|
+
cfg = @settings
|
195
|
+
real_path = ['config']
|
196
|
+
unless keypath =~ /^[.*]?$/
|
197
|
+
real_path = resolve_key_path(keypath)
|
198
|
+
return nil unless real_path&.count&.positive?
|
199
|
+
|
200
|
+
cfg = cfg.dig(*real_path)
|
201
|
+
end
|
202
|
+
|
203
|
+
cfg.nil? ? nil : { real_path[-1] => cfg }
|
133
204
|
end
|
134
205
|
|
135
206
|
# It takes the input, fills in the defaults where values do not exist.
|
@@ -141,12 +212,37 @@ module Doing
|
|
141
212
|
Util.deep_merge_hashes(DEFAULTS, Configuration[user_config].stringify_keys)
|
142
213
|
end
|
143
214
|
|
144
|
-
|
145
|
-
|
146
|
-
|
215
|
+
##
|
216
|
+
## Method for transitioning from ~/.doingrc to ~/.config/doing/config.yml
|
217
|
+
##
|
218
|
+
def update_deprecated_config
|
219
|
+
# return # Until further notice
|
220
|
+
return if File.exist?(default_config_file)
|
147
221
|
|
148
|
-
|
149
|
-
|
222
|
+
old_file = File.join(Util.user_home, '.doingrc')
|
223
|
+
return unless File.exist?(old_file)
|
224
|
+
|
225
|
+
wwid = Doing::WWID.new
|
226
|
+
Doing.logger.log_now(:warn, 'Deprecated:', "main config file location has changed to #{config_file}")
|
227
|
+
res = wwid.yn("Move #{old_file} to new location, preserving settings?", default_response: true)
|
228
|
+
|
229
|
+
return unless res
|
230
|
+
|
231
|
+
if File.exist?(default_config_file)
|
232
|
+
res = wwid.yn("#{default_config_file} already exists, overwrite it?", default_response: false)
|
233
|
+
|
234
|
+
unless res
|
235
|
+
@config_file = old_file
|
236
|
+
return
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
FileUtils.mv old_file, default_config_file, force: true
|
241
|
+
Doing.logger.log_now(:warn, 'Config:', "Config file moved to #{default_config_file}")
|
242
|
+
Doing.logger.log_now(:warn, 'Config:', %(If ~/.doingrc exists in the future,
|
243
|
+
it will be considered a local config and its values will override the
|
244
|
+
default configuration.))
|
245
|
+
Process.exit 0
|
150
246
|
end
|
151
247
|
|
152
248
|
##
|
@@ -155,6 +251,8 @@ module Doing
|
|
155
251
|
## @param opt [Hash] Additional Options
|
156
252
|
##
|
157
253
|
def configure(opt = {})
|
254
|
+
update_deprecated_config if config_file == default_config_file
|
255
|
+
|
158
256
|
@ignore_local = opt[:ignore_local] if opt[:ignore_local]
|
159
257
|
|
160
258
|
config = read_config.dup
|
@@ -191,8 +289,23 @@ module Doing
|
|
191
289
|
config
|
192
290
|
end
|
193
291
|
|
292
|
+
# @private
|
293
|
+
def inspect
|
294
|
+
%(<Doing::Configuration #{@settings.hash}>)
|
295
|
+
end
|
296
|
+
|
297
|
+
# @private
|
298
|
+
def to_s
|
299
|
+
YAML.dump(@settings)
|
300
|
+
end
|
301
|
+
|
194
302
|
private
|
195
303
|
|
304
|
+
##
|
305
|
+
## Test for deprecated config keys
|
306
|
+
##
|
307
|
+
## @param config The configuration
|
308
|
+
##
|
196
309
|
def find_deprecations(config)
|
197
310
|
deprecated = false
|
198
311
|
if config.key?('editor')
|
@@ -206,14 +319,16 @@ module Doing
|
|
206
319
|
deprecated = true
|
207
320
|
config['editors']['config'] = config['config_editor_app']
|
208
321
|
config.delete('config_editor_app')
|
209
|
-
Doing.logger.debug('Deprecated:',
|
322
|
+
Doing.logger.debug('Deprecated:',
|
323
|
+
"config key 'config_editor_app' is now 'editors->config', please update your config.")
|
210
324
|
end
|
211
325
|
|
212
326
|
if config.key?('editor_app') && !config['editors']['doing_file']
|
213
327
|
deprecated = true
|
214
328
|
config['editors']['doing_file'] = config['editor_app']
|
215
329
|
config.delete('editor_app')
|
216
|
-
Doing.logger.debug('Deprecated:',
|
330
|
+
Doing.logger.debug('Deprecated:',
|
331
|
+
"config key 'editor_app' is now 'editors->doing_file', please update your config.")
|
217
332
|
end
|
218
333
|
|
219
334
|
Doing.logger.warn('Deprecated:', 'outdated keys found, please run `doing config --update`.') if deprecated
|
@@ -257,7 +372,7 @@ module Doing
|
|
257
372
|
##
|
258
373
|
def read_config
|
259
374
|
unless File.exist?(config_file)
|
260
|
-
Doing.logger.info('Config:', 'Config file doesn\'t exist, using default configuration'
|
375
|
+
Doing.logger.info('Config:', 'Config file doesn\'t exist, using default configuration')
|
261
376
|
return {}.deep_merge(DEFAULTS)
|
262
377
|
end
|
263
378
|
|
@@ -302,11 +417,7 @@ module Doing
|
|
302
417
|
end
|
303
418
|
|
304
419
|
def load_plugins(add_dir = nil)
|
305
|
-
|
306
|
-
FileUtils.mkdir_p(add_dir) if add_dir && !File.exist?(add_dir)
|
307
|
-
rescue
|
308
|
-
nil
|
309
|
-
end
|
420
|
+
FileUtils.mkdir_p(add_dir) if add_dir && !File.exist?(add_dir)
|
310
421
|
|
311
422
|
Plugins.load_plugins(add_dir)
|
312
423
|
end
|
data/lib/doing/hash.rb
CHANGED
@@ -27,5 +27,42 @@ module Doing
|
|
27
27
|
def symbolize_keys
|
28
28
|
each_with_object({}) { |(k, v), hsh| hsh[k.to_sym] = v.is_a?(Hash) ? v.symbolize_keys : v }
|
29
29
|
end
|
30
|
+
|
31
|
+
# Set a nested hash value using an array
|
32
|
+
#
|
33
|
+
# @example `{}.deep_set(['one', 'two'], 'value')`
|
34
|
+
# @example `=> { 'one' => { 'two' => 'value' } }
|
35
|
+
#
|
36
|
+
# @param path [Array] key path
|
37
|
+
# @param value The value
|
38
|
+
#
|
39
|
+
def deep_set(path, value)
|
40
|
+
if path.count == 1
|
41
|
+
if value
|
42
|
+
self[path[0]] = value
|
43
|
+
else
|
44
|
+
delete(path[0])
|
45
|
+
end
|
46
|
+
else
|
47
|
+
if value
|
48
|
+
self.default_proc = ->(h, k) { h[k] = Hash.new(&h.default_proc) }
|
49
|
+
dig(*path[0..-2])[path.fetch(-1)] = value
|
50
|
+
else
|
51
|
+
return self unless dig(*path)
|
52
|
+
|
53
|
+
dig(*path[0..-2]).delete(path.fetch(-1))
|
54
|
+
path.pop
|
55
|
+
cleaned = self
|
56
|
+
path.each do |key|
|
57
|
+
if cleaned[key].empty?
|
58
|
+
cleaned.delete(key)
|
59
|
+
break
|
60
|
+
end
|
61
|
+
cleaned = cleaned[key]
|
62
|
+
end
|
63
|
+
empty? ? nil : self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
30
67
|
end
|
31
68
|
end
|
data/lib/doing/item.rb
CHANGED
@@ -5,10 +5,10 @@ module Doing
|
|
5
5
|
## This class describes a single WWID item
|
6
6
|
##
|
7
7
|
class Item
|
8
|
-
# include Amatch
|
9
|
-
|
10
8
|
attr_accessor :date, :title, :section, :note
|
11
9
|
|
10
|
+
attr_reader :id
|
11
|
+
|
12
12
|
##
|
13
13
|
## Initialize an item with date, title, section, and
|
14
14
|
## optional note
|
@@ -50,8 +50,11 @@ module Doing
|
|
50
50
|
@end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
|
51
51
|
end
|
52
52
|
|
53
|
-
|
54
|
-
|
53
|
+
# Generate a hash that represents the entry
|
54
|
+
#
|
55
|
+
# @return [String] entry hash
|
56
|
+
def id
|
57
|
+
@id ||= (@date.to_s + @title + @section).hash
|
55
58
|
end
|
56
59
|
|
57
60
|
##
|
@@ -105,14 +108,42 @@ module Doing
|
|
105
108
|
##
|
106
109
|
## Add (or remove) tags from the title of the item
|
107
110
|
##
|
108
|
-
## @param
|
109
|
-
## @param
|
110
|
-
##
|
111
|
-
## @
|
112
|
-
## @
|
111
|
+
## @param tags [Array] The tags to apply
|
112
|
+
## @param **options Additional options
|
113
|
+
##
|
114
|
+
## @option options :date [Boolean] Include timestamp?
|
115
|
+
## @option options :single [Boolean] Log as a single change?
|
116
|
+
## @option options :value [String] A value to include as @tag(value)
|
117
|
+
## @option options :remove [Boolean] if true remove instead of adding
|
118
|
+
## @option options :rename_to [String] if not nil, rename target tag to this tag name
|
119
|
+
## @option options :regex [Boolean] treat target tag string as regex pattern
|
120
|
+
## @option options :force [Boolean] with rename_to, add tag if it doesn't exist
|
113
121
|
##
|
114
|
-
def tag(
|
115
|
-
|
122
|
+
def tag(tags, **options)
|
123
|
+
added = []
|
124
|
+
removed = []
|
125
|
+
|
126
|
+
date = options.fetch(:date, false)
|
127
|
+
options[:value] ||= date ? Time.now.strftime('%F %R') : nil
|
128
|
+
options.delete(:date)
|
129
|
+
|
130
|
+
single = options.fetch(:single, false)
|
131
|
+
options.delete(:single)
|
132
|
+
|
133
|
+
tags = tags.to_tags if tags.is_a? ::String
|
134
|
+
|
135
|
+
remove = options.fetch(:remove, false)
|
136
|
+
tags.each do |tag|
|
137
|
+
bool = remove ? :and : :not
|
138
|
+
if tags?(tag, bool)
|
139
|
+
@title.tag!(tag, **options).strip!
|
140
|
+
remove ? removed.push(tag) : added.push(tag)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
Doing.logger.log_change(tags_added: added, tags_removed: removed, count: 1, item: self, single: single)
|
145
|
+
|
146
|
+
self
|
116
147
|
end
|
117
148
|
|
118
149
|
##
|
@@ -121,7 +152,7 @@ module Doing
|
|
121
152
|
## @return [Array] array of tags (no values)
|
122
153
|
##
|
123
154
|
def tags
|
124
|
-
@title.scan(/(?<= |\A)@([^\s(]+)/).map {|tag| tag[0]}.sort.uniq
|
155
|
+
@title.scan(/(?<= |\A)@([^\s(]+)/).map { |tag| tag[0] }.sort.uniq
|
125
156
|
end
|
126
157
|
|
127
158
|
##
|
@@ -190,6 +221,40 @@ module Doing
|
|
190
221
|
should?('never_time')
|
191
222
|
end
|
192
223
|
|
224
|
+
##
|
225
|
+
## Move item from current section to destination section
|
226
|
+
##
|
227
|
+
## @param new_section [String] The destination
|
228
|
+
## section
|
229
|
+
## @param label [Boolean] add @from(original
|
230
|
+
## section) tag
|
231
|
+
## @param log [Boolean] log this action
|
232
|
+
##
|
233
|
+
## @return nothing
|
234
|
+
##
|
235
|
+
def move_to(new_section, label: true, log: true)
|
236
|
+
from = @section
|
237
|
+
|
238
|
+
tag('from', rename_to: 'from', value: from, force: true) if label
|
239
|
+
@section = new_section
|
240
|
+
|
241
|
+
Doing.logger.count(@section == 'Archive' ? :archived : :moved) if log
|
242
|
+
Doing.logger.debug("#{@section == 'Archive' ? 'Archived' : 'Moved'}:",
|
243
|
+
"#{@title.truncate(60)} from #{from} to #{@section}")
|
244
|
+
self
|
245
|
+
end
|
246
|
+
|
247
|
+
# outputs item in Doing file format, including leading tab
|
248
|
+
def to_s
|
249
|
+
"\t- #{@date.strftime('%Y-%m-%d %H:%M')} | #{@title}#{@note.empty? ? '' : "\n#{@note}"}"
|
250
|
+
end
|
251
|
+
|
252
|
+
# @private
|
253
|
+
def inspect
|
254
|
+
# %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
|
255
|
+
%(<Doing::Item @date=#{@date}>)
|
256
|
+
end
|
257
|
+
|
193
258
|
private
|
194
259
|
|
195
260
|
def should?(key)
|
data/lib/doing/items.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Doing
|
4
|
+
# Items Array
|
5
|
+
class Items < Array
|
6
|
+
attr_accessor :sections
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
super
|
10
|
+
@sections = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# List sections, title only
|
14
|
+
#
|
15
|
+
# @return [Array] section titles
|
16
|
+
#
|
17
|
+
def section_titles
|
18
|
+
@sections.map(&:title)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Test if section already exists
|
22
|
+
#
|
23
|
+
# @param section [String] section title
|
24
|
+
#
|
25
|
+
# @return [Boolean] true if section exists
|
26
|
+
#
|
27
|
+
def section?(section)
|
28
|
+
has_section = false
|
29
|
+
section = section.is_a?(Section) ? section.title.downcase : section.downcase
|
30
|
+
@sections.each do |s|
|
31
|
+
if s.title.downcase == section
|
32
|
+
has_section = true
|
33
|
+
break
|
34
|
+
end
|
35
|
+
end
|
36
|
+
has_section
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add a new section to the sections array. Accepts
|
40
|
+
# either a Section object, or a title string that will
|
41
|
+
# be converted into a Section.
|
42
|
+
#
|
43
|
+
# @param section [Section] The section to add. A
|
44
|
+
# String value will be converted to
|
45
|
+
# Section automatically.
|
46
|
+
# @param log [Boolean] Add a log message
|
47
|
+
# notifying the user about the
|
48
|
+
# creation of the section.
|
49
|
+
#
|
50
|
+
# @return nothing
|
51
|
+
#
|
52
|
+
def add_section(section, log: false)
|
53
|
+
section = section.is_a?(Section) ? section : Section.new(section.cap_first)
|
54
|
+
|
55
|
+
return if section?(section)
|
56
|
+
|
57
|
+
@sections.push(section)
|
58
|
+
Doing.logger.info('New section:', %("#{section}" added)) if log
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get a new Items object containing only items in a
|
62
|
+
# specified section
|
63
|
+
#
|
64
|
+
# @param section [String] section title
|
65
|
+
#
|
66
|
+
# @return [Items] Array of items
|
67
|
+
#
|
68
|
+
def in_section(section)
|
69
|
+
if section =~ /^all$/i
|
70
|
+
dup
|
71
|
+
else
|
72
|
+
items = Items.new.concat(select { |item| item.section == section })
|
73
|
+
items.add_section(section, log: false)
|
74
|
+
items
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
## Delete an item from the index
|
80
|
+
##
|
81
|
+
## @param item The item
|
82
|
+
##
|
83
|
+
def delete_item(item, single: false)
|
84
|
+
deleted = delete(item)
|
85
|
+
Doing.logger.count(:deleted)
|
86
|
+
Doing.logger.info('Entry deleted:', deleted.title) if single
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
## Update an item in the index with a modified item
|
91
|
+
##
|
92
|
+
## @param old_item The old item
|
93
|
+
## @param new_item The new item
|
94
|
+
##
|
95
|
+
def update_item(old_item, new_item)
|
96
|
+
s_idx = index { |item| item.equal?(old_item) }
|
97
|
+
|
98
|
+
raise ItemNotFound, 'Unable to find item in index, did it mutate?' unless s_idx
|
99
|
+
|
100
|
+
return if fetch(s_idx).equal?(new_item)
|
101
|
+
|
102
|
+
self[s_idx] = new_item
|
103
|
+
Doing.logger.count(:updated)
|
104
|
+
Doing.logger.info('Entry updated:', self[s_idx].title.truncate(60))
|
105
|
+
new_item
|
106
|
+
end
|
107
|
+
|
108
|
+
# Output sections and items in Doing file format
|
109
|
+
def to_s
|
110
|
+
out = []
|
111
|
+
@sections.each do |section|
|
112
|
+
out.push(section.original)
|
113
|
+
in_section(section.title).each { |item| out.push(item.to_s)}
|
114
|
+
end
|
115
|
+
|
116
|
+
out.join("\n")
|
117
|
+
end
|
118
|
+
|
119
|
+
# @private
|
120
|
+
def inspect
|
121
|
+
"#<Doing::Items #{count} items, #{@sections.count} sections: #{@sections.map { |s| "<Section:#{s.title} #{in_section(s.title).count} items>" }.join(', ')}>"
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
data/lib/doing/log_adapter.rb
CHANGED
@@ -5,9 +5,16 @@ module Doing
|
|
5
5
|
## Log adapter
|
6
6
|
##
|
7
7
|
class LogAdapter
|
8
|
-
|
8
|
+
# Sets the log device
|
9
|
+
attr_writer :logdev
|
9
10
|
|
10
|
-
|
11
|
+
# Max length of log messages (truncate in middle)
|
12
|
+
attr_writer :max_length
|
13
|
+
|
14
|
+
# Returns the current log level (debug, info, warn, error)
|
15
|
+
attr_reader :level
|
16
|
+
|
17
|
+
attr_reader :messages, :results
|
11
18
|
|
12
19
|
TOPIC_WIDTH = 12
|
13
20
|
|
@@ -28,6 +35,7 @@ module Doing
|
|
28
35
|
deleted
|
29
36
|
moved
|
30
37
|
removed_tags
|
38
|
+
rotated
|
31
39
|
skipped
|
32
40
|
updated
|
33
41
|
].freeze
|
@@ -45,6 +53,7 @@ module Doing
|
|
45
53
|
@logdev = $stderr
|
46
54
|
@max_length = `tput cols`.strip.to_i - 5 || 85
|
47
55
|
self.log_level = level
|
56
|
+
@prev_level = level
|
48
57
|
end
|
49
58
|
|
50
59
|
#
|
@@ -71,8 +80,26 @@ module Doing
|
|
71
80
|
@level = level
|
72
81
|
end
|
73
82
|
|
83
|
+
# Set log level temporarily
|
84
|
+
def temp_level(level)
|
85
|
+
return if level.nil? || level.to_sym == @log_level
|
86
|
+
|
87
|
+
@prev_level = log_level.dup
|
88
|
+
@log_level = level.to_sym
|
89
|
+
end
|
90
|
+
|
91
|
+
# Restore temporary level
|
92
|
+
def restore_level
|
93
|
+
return if @prev_level.nil? || @prev_level == @log_level
|
94
|
+
|
95
|
+
self.log_level = @prev_level
|
96
|
+
@prev_level = nil
|
97
|
+
end
|
98
|
+
|
74
99
|
def adjust_verbosity(options = {})
|
75
|
-
if options[:
|
100
|
+
if options[:log_level]
|
101
|
+
self.log_level = options[:log_level].to_sym
|
102
|
+
elsif options[:quiet]
|
76
103
|
self.log_level = :error
|
77
104
|
elsif options[:verbose] || options[:debug]
|
78
105
|
self.log_level = :debug
|
@@ -227,7 +254,6 @@ module Doing
|
|
227
254
|
##
|
228
255
|
def output_results
|
229
256
|
total_counters
|
230
|
-
|
231
257
|
results = @results.select { |msg| write_message?(msg[:level]) }.uniq
|
232
258
|
|
233
259
|
if @logdev == $stdout
|
@@ -239,10 +265,38 @@ module Doing
|
|
239
265
|
end
|
240
266
|
end
|
241
267
|
|
268
|
+
def log_change(tags_added: [], tags_removed: [], count: 1, item: nil, single: false)
|
269
|
+
if tags_added.empty? && tags_removed.empty?
|
270
|
+
count(:skipped, level: :debug, message: '%count %items with no change', count: count)
|
271
|
+
else
|
272
|
+
if tags_added.empty?
|
273
|
+
count(:skipped, level: :debug, message: 'no tags added to %count %items')
|
274
|
+
elsif single && item
|
275
|
+
added = tags_added.log_tags
|
276
|
+
info('Tagged:',
|
277
|
+
%(added #{tags_added.count == 1 ? 'tag' : 'tags'} #{added} to #{item.title}))
|
278
|
+
else
|
279
|
+
count(:added_tags, level: :info, tag: tags_added, message: '%tags added to %count %items')
|
280
|
+
end
|
281
|
+
|
282
|
+
if tags_removed.empty?
|
283
|
+
count(:skipped, level: :debug, message: 'no tags removed from %count %items')
|
284
|
+
elsif single && item
|
285
|
+
added = tags_added.log_tags
|
286
|
+
info('Untagged:',
|
287
|
+
%(removed #{tags_removed.count == 1 ? 'tag' : 'tags'} #{added} from #{item.title}))
|
288
|
+
else
|
289
|
+
count(:removed_tags, level: :info, tag: tags_removed, message: '%tags removed from %count %items')
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
242
294
|
private
|
243
295
|
|
244
296
|
def format_counter(key, data)
|
245
297
|
case key
|
298
|
+
when :rotated
|
299
|
+
['Rotated:', data[:message] || 'rotated %count %items']
|
246
300
|
when :autotag
|
247
301
|
['Autotag:', data[:message] || 'autotagged %count %items']
|
248
302
|
when :added_tags
|