doing 2.0.6.pre → 2.0.10

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +20 -0
  3. data/.yardoc/complete +0 -0
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/.yardoc/proxy_types +0 -0
  7. data/.yardopts +1 -0
  8. data/CHANGELOG.md +7 -7
  9. data/Gemfile.lock +30 -10
  10. data/README.md +1 -1
  11. data/Rakefile +8 -1
  12. data/bin/doing +367 -21
  13. data/doc/Array.html +135 -0
  14. data/doc/Doing/Color.html +506 -0
  15. data/doc/Doing/Configuration.html +680 -0
  16. data/doc/Doing/Errors/DoingNoTraceError.html +186 -0
  17. data/doc/Doing/Errors/DoingRuntimeError.html +186 -0
  18. data/doc/Doing/Errors/DoingStandardError.html +186 -0
  19. data/doc/Doing/Errors/EmptyInput.html +186 -0
  20. data/doc/Doing/Errors/NoResults.html +186 -0
  21. data/doc/Doing/Errors/PluginException.html +248 -0
  22. data/doc/Doing/Errors/UserCancelled.html +186 -0
  23. data/doc/Doing/Errors/WrongCommand.html +186 -0
  24. data/doc/Doing/Errors.html +191 -0
  25. data/doc/Doing/Hooks.html +364 -0
  26. data/doc/Doing/Item.html +1385 -0
  27. data/doc/Doing/Items.html +393 -0
  28. data/doc/Doing/LogAdapter.html +1650 -0
  29. data/doc/Doing/Note.html +535 -0
  30. data/doc/Doing/Pager.html +268 -0
  31. data/doc/Doing/Plugins.html +849 -0
  32. data/doc/Doing/Util.html +870 -0
  33. data/doc/Doing/WWID.html +4827 -0
  34. data/doc/Doing.html +145 -0
  35. data/doc/GLI/Commands/MarkdownDocumentListener.html +763 -0
  36. data/doc/GLI/Commands.html +115 -0
  37. data/doc/GLI.html +115 -0
  38. data/doc/Hash.html +332 -0
  39. data/doc/Status.html +292 -0
  40. data/doc/String.html +1714 -0
  41. data/doc/Symbol.html +250 -0
  42. data/doc/Time.html +182 -0
  43. data/doc/_index.html +411 -0
  44. data/doc/class_list.html +51 -0
  45. data/doc/css/common.css +1 -0
  46. data/doc/css/full_list.css +58 -0
  47. data/doc/css/style.css +497 -0
  48. data/doc/file.README.html +123 -0
  49. data/doc/file_list.html +56 -0
  50. data/doc/frames.html +17 -0
  51. data/doc/index.html +123 -0
  52. data/doc/js/app.js +314 -0
  53. data/doc/js/full_list.js +216 -0
  54. data/doc/js/jquery.js +4 -0
  55. data/doc/method_list.html +1867 -0
  56. data/doc/top-level-namespace.html +112 -0
  57. data/doing.gemspec +5 -1
  58. data/doing.rdoc +354 -6
  59. data/example_plugin.rb +6 -6
  60. data/lib/doing/array.rb +1 -1
  61. data/lib/doing/configuration.rb +14 -12
  62. data/lib/doing/errors.rb +1 -1
  63. data/lib/doing/hash.rb +1 -1
  64. data/lib/doing/item.rb +113 -23
  65. data/lib/doing/log_adapter.rb +123 -113
  66. data/lib/doing/note.rb +1 -1
  67. data/lib/doing/plugin_manager.rb +5 -5
  68. data/lib/doing/plugins/export/csv_export.rb +1 -1
  69. data/lib/doing/plugins/export/template_export.rb +5 -7
  70. data/lib/doing/plugins/import/calendar_import.rb +8 -2
  71. data/lib/doing/plugins/import/doing_import.rb +10 -10
  72. data/lib/doing/plugins/import/timing_import.rb +12 -4
  73. data/lib/doing/string.rb +94 -19
  74. data/lib/doing/symbol.rb +9 -5
  75. data/lib/doing/time.rb +1 -1
  76. data/lib/doing/util.rb +18 -11
  77. data/lib/doing/version.rb +1 -1
  78. data/lib/doing/wwid.rb +455 -335
  79. data/lib/doing/wwidfile.rb +5 -5
  80. data/lib/doing.rb +2 -1
  81. data/lib/examples/plugins/say_export.rb +6 -6
  82. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.css +0 -0
  83. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki.haml +0 -0
  84. data/lib/examples/plugins/{templates → wiki_export/templates}/wiki_index.haml +0 -0
  85. data/lib/examples/plugins/{wiki_export.rb → wiki_export/wiki_export.rb} +0 -0
  86. data/rdocfixer.rb +1 -1
  87. data/scripts/generate_zsh_completions.rb +3 -2
  88. data/yard_templates/default/method_details/setup.rb +3 -0
  89. metadata +121 -8
data/example_plugin.rb CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  module Doing
28
28
  ##
29
- ## @brief Plugin class
29
+ ## Plugin class
30
30
  ##
31
31
  class SayExport
32
32
  include Doing::Util
@@ -65,7 +65,7 @@ module Doing
65
65
  ##
66
66
  ## wwid.config['plugins'][PLUGIN_NAME][KEY]
67
67
  ##
68
- ## @brief Method to return plugin settings (required)
68
+ ## Method to return plugin settings (required)
69
69
  ##
70
70
  ## @return Hash of settings for this plugin
71
71
  ##
@@ -87,7 +87,7 @@ module Doing
87
87
  ## included in settings. The method should return a
88
88
  ## string (not output it to the STDOUT).
89
89
  ##
90
- ## @brief Method to return template (optional)
90
+ ## Method to return template (optional)
91
91
  ##
92
92
  ## @param trigger The trigger passed to the
93
93
  ## template function. When this
@@ -96,7 +96,7 @@ module Doing
96
96
  ## used to determine which one is
97
97
  ## output.
98
98
  ##
99
- ## @return (String) template contents
99
+ ## @return [String] template contents
100
100
  ##
101
101
  def self.template(trigger)
102
102
  return unless trigger =~ /^say(it)?$/
@@ -106,7 +106,7 @@ module Doing
106
106
 
107
107
 
108
108
  ##
109
- ## @brief Render data received from an output
109
+ ## Render data received from an output
110
110
  ## command
111
111
  ##
112
112
  ## @param wwid The wwid object with config
@@ -118,7 +118,7 @@ module Doing
118
118
  ## flags passed to command
119
119
  ## (variables[:options])
120
120
  ##
121
- ## @return (String) Rendered output
121
+ ## @return [String] Rendered output
122
122
  ##
123
123
  def self.render(wwid, items, variables: {})
124
124
  return if items.nil? || items.empty?
data/lib/doing/array.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  ##
5
- ## @brief Array helpers
5
+ ## Array helpers
6
6
  ##
7
7
  class ::Array end
8
8
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  ##
5
- ## @brief Configuration object
5
+ ## Configuration object
6
6
  ##
7
7
  class Configuration
8
8
  attr_reader :settings
@@ -106,14 +106,15 @@ module Doing
106
106
 
107
107
  def value_for_key(keypath = '')
108
108
  cfg = @settings
109
+ real_path = []
109
110
  unless keypath =~ /^[.*]?$/
110
111
  paths = keypath.split(/[:.]/)
111
112
  while paths.length.positive? && !cfg.nil?
112
113
  path = paths.shift
113
114
  new_cfg = nil
114
115
  cfg.each do |key, val|
115
- next unless key =~ /#{path.to_rx(2)}/
116
-
116
+ next unless key =~ path.to_rx(distance: 2)
117
+ real_path << key
117
118
  new_cfg = val
118
119
  break
119
120
  end
@@ -122,12 +123,13 @@ module Doing
122
123
  Doing.logger.error("Key match not found: #{path}")
123
124
  break
124
125
  end
125
-
126
126
  cfg = new_cfg
127
127
  end
128
128
  end
129
-
130
- cfg
129
+ Doing.logger.debug('Config:', "translated key path #{keypath} to #{real_path.join('.')}")
130
+ result = {}
131
+ result[real_path[-1]] = cfg
132
+ result
131
133
  end
132
134
 
133
135
  # It takes the input, fills in the defaults where values do not exist.
@@ -148,9 +150,9 @@ module Doing
148
150
  end
149
151
 
150
152
  ##
151
- ## @brief Read user configuration and merge with defaults
153
+ ## Read user configuration and merge with defaults
152
154
  ##
153
- ## @param opt (Hash) Additional Options
155
+ ## @param opt [Hash] Additional Options
154
156
  ##
155
157
  def configure(opt = {})
156
158
  @ignore_local = opt[:ignore_local] if opt[:ignore_local]
@@ -219,7 +221,7 @@ module Doing
219
221
  end
220
222
 
221
223
  ##
222
- ## @brief Read local configurations
224
+ ## Read local configurations
223
225
  ##
224
226
  ## @return Hash of config options
225
227
  ##
@@ -251,7 +253,7 @@ module Doing
251
253
  end
252
254
 
253
255
  ##
254
- ## @brief Reads a configuration.
256
+ ## Reads a configuration.
255
257
  ##
256
258
  def read_config
257
259
  unless File.exist?(config_file)
@@ -279,9 +281,9 @@ module Doing
279
281
  end
280
282
 
281
283
  ##
282
- ## @brief Finds a project-specific configuration file
284
+ ## Finds a project-specific configuration file
283
285
  ##
284
- ## @return (String) A file path
286
+ ## @return [String] A file path
285
287
  ##
286
288
  def find_local_config
287
289
  dir = Dir.pwd
data/lib/doing/errors.rb CHANGED
@@ -27,7 +27,7 @@ module Doing
27
27
  end
28
28
 
29
29
  class WrongCommand < ::StandardError
30
- def initialize(msg = 'wrong command', topic = 'Error:')
30
+ def initialize(msg = 'wrong command', topic: 'Error:')
31
31
  Doing.logger.warn(topic, msg)
32
32
 
33
33
  super(msg)
data/lib/doing/hash.rb CHANGED
@@ -4,7 +4,7 @@ module Doing
4
4
  # Hash helpers
5
5
  class ::Hash
6
6
  ##
7
- ## @brief Freeze all values in a hash
7
+ ## Freeze all values in a hash
8
8
  ##
9
9
  ## @return { description_of_the_return_value }
10
10
  ##
data/lib/doing/item.rb CHANGED
@@ -2,11 +2,24 @@
2
2
 
3
3
  module Doing
4
4
  ##
5
- ## @brief This class describes a single WWID item
5
+ ## This class describes a single WWID item
6
6
  ##
7
7
  class Item
8
+ include Amatch
9
+
8
10
  attr_accessor :date, :title, :section, :note
9
11
 
12
+ ##
13
+ ## Initialize an item with date, title, section, and
14
+ ## optional note
15
+ ##
16
+ ## @param date [Time] The item's start date
17
+ ## @param title [String] The title
18
+ ## @param section [String] The section to which
19
+ ## the item belongs
20
+ ## @param note [Array or String] The note
21
+ ## (optional)
22
+ ##
10
23
  def initialize(date, title, section, note = nil)
11
24
  @date = date.is_a?(Time) ? date : Time.parse(date)
12
25
  @title = title
@@ -18,14 +31,36 @@ module Doing
18
31
  # @date = new_date.is_a?(Time) ? new_date : Time.parse(new_date)
19
32
  # end
20
33
 
34
+ ##
35
+ ## Get the difference between the item's start date and
36
+ ## the value of its @done tag (if present)
37
+ ##
38
+ ## @return Interval in seconds
39
+ ##
21
40
  def interval
22
41
  @interval ||= calc_interval
23
42
  end
24
43
 
44
+ ##
45
+ ## Get the value of the item's @done tag
46
+ ##
47
+ ## @return [Time] @done value
48
+ ##
25
49
  def end_date
26
50
  @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
27
51
  end
28
52
 
53
+ def hash_id
54
+ (@date.to_s + @title + @section + @note.join(' ')).hash
55
+ end
56
+
57
+ ##
58
+ ## Test for equality between items
59
+ ##
60
+ ## @param other [Item] The other item
61
+ ##
62
+ ## @return [Boolean] is equal?
63
+ ##
29
64
  def equal?(other)
30
65
  return false if @title.strip != other.title.strip
31
66
 
@@ -36,10 +71,25 @@ module Doing
36
71
  true
37
72
  end
38
73
 
74
+ ##
75
+ ## Test if two items occur at the same time (same start date and equal duration)
76
+ ##
77
+ ## @param item_b [Item] The item to compare
78
+ ##
79
+ ## @return [Boolean] is equal?
80
+ ##
39
81
  def same_time?(item_b)
40
82
  date == item_b.date ? interval == item_b.interval : false
41
83
  end
42
84
 
85
+ ##
86
+ ## Test if the interval between start date and @done
87
+ ## value overlaps with another item's
88
+ ##
89
+ ## @param item_b [Item] The item to compare
90
+ ##
91
+ ## @return [Boolean] overlaps?
92
+ ##
43
93
  def overlapping_time?(item_b)
44
94
  return true if same_time?(item_b)
45
95
 
@@ -52,43 +102,83 @@ module Doing
52
102
  (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
53
103
  end
54
104
 
105
+ ##
106
+ ## Add (or remove) tags from the title of the item
107
+ ##
108
+ ## @param tag [String] The tag to add
109
+ ## @param value [String] A value to include as @tag(value)
110
+ ## @param remove [Boolean] if true remove instead of adding
111
+ ## @param rename_to [String] if not nil, rename target tag to this tag name
112
+ ## @param regex [Boolean] treat target tag string as regex pattern
113
+ ##
55
114
  def tag(tag, value: nil, remove: false, rename_to: nil, regex: false)
56
115
  @title.tag!(tag, value: value, remove: remove, rename_to: rename_to, regex: regex).strip!
57
116
  end
58
117
 
118
+ ##
119
+ ## Get a list of tags on the item
120
+ ##
121
+ ## @return [Array] array of tags (no values)
122
+ ##
59
123
  def tags
60
124
  @title.scan(/(?<= |\A)@([^\s(]+)/).map {|tag| tag[0]}.sort.uniq
61
125
  end
62
126
 
63
- def tags?(tags, bool = :and)
127
+ ##
128
+ ## Test if item contains tag(s)
129
+ ##
130
+ ## @param tags (Array or String) The tags to test. Can be an array or a comma-separated string.
131
+ ## @param bool (Symbol) The boolean to use for multiple tags (:and, :or, :not)
132
+ ## @param negate [Boolean] negate the result?
133
+ ##
134
+ ## @return [Boolean] true if tag/bool combination passes
135
+ ##
136
+ def tags?(tags, bool = :and, negate: false)
64
137
  tags = split_tags(tags)
65
138
  bool = bool.normalize_bool
66
139
 
67
- case bool
68
- when :and
69
- all_tags?(tags)
70
- when :not
71
- no_tags?(tags)
72
- else
73
- any_tags?(tags)
74
- end
75
- end
76
-
77
- def search(search)
140
+ matches = case bool
141
+ when :and
142
+ all_tags?(tags)
143
+ when :not
144
+ no_tags?(tags)
145
+ else
146
+ any_tags?(tags)
147
+ end
148
+ negate ? !matches : matches
149
+ end
150
+
151
+ ##
152
+ ## Test if item matches search string
153
+ ##
154
+ ## @param search [String] The search string
155
+ ## @param negate [Boolean] negate results
156
+ ## @param case_type (Symbol) The case-sensitivity
157
+ ## type (:sensitive,
158
+ ## :ignore, :smart)
159
+ ##
160
+ ## @return [Boolean] matches search criteria
161
+ ##
162
+ def search(search, distance: 3, negate: false, case_type: :smart, fuzzy: false)
78
163
  text = @title + @note.to_s
79
- pattern = case search.strip
80
- when %r{^/.*?/$}
81
- search.sub(%r{/(.*?)/}, '\1')
82
- when /^'/
83
- case_sensitive = true
84
- search.sub(/^'(.*?)'?$/, '\1')
164
+
165
+ if search.is_rx? || !fuzzy
166
+ matches = text =~ search.to_rx(distance: distance, case_type: case_type)
167
+ else
168
+ distance = 0.25 if distance > 1
169
+ score = if (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
170
+ text.downcase.pair_distance_similar(search.downcase)
85
171
  else
86
- case_sensitive = true if search =~ /[A-Z]/
87
- search.split('').join('.{0,3}')
172
+ score = text.pair_distance_similar(search)
88
173
  end
89
- rx = Regexp.new(pattern, !case_sensitive)
90
174
 
91
- text =~ rx
175
+ if score >= distance
176
+ matches = true
177
+ Doing.logger.debug('Fuzzy Match:', %(#{@title}, "#{search}" #{score}))
178
+ end
179
+ end
180
+
181
+ negate ? !matches : matches
92
182
  end
93
183
 
94
184
  def should_finish?
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Doing
4
4
  ##
5
- ## @brief Log adapter
5
+ ## Log adapter
6
6
  ##
7
7
  class LogAdapter
8
8
  attr_writer :logdev, :max_length
@@ -32,7 +32,7 @@ module Doing
32
32
  ].freeze
33
33
 
34
34
  #
35
- # @brief Create a new instance of a log writer
35
+ # Create a new instance of a log writer
36
36
  #
37
37
  # @param level (optional, symbol) the log level
38
38
  #
@@ -47,29 +47,25 @@ module Doing
47
47
  end
48
48
 
49
49
  #
50
- # @brief Set the log level on the writer
50
+ # Set the log level on the writer
51
51
  #
52
52
  # @param level (symbol) the log level
53
53
  #
54
54
  # @return nothing
55
55
  #
56
- def log_level=(level)
57
- level ||= 'info'
56
+ def log_level=(level = 'info')
58
57
  level = level.to_s
59
- if level.is_a?(String) && level =~ /^([ewid]\w+|[0123])$/
60
- level = case level
61
- when /^[e0]/
62
- :error
63
- when /^[w1]/
64
- :warn
65
- when /^[i2]/
66
- :info
67
- when /^[d3]/
68
- :debug
69
- end
70
- else
71
- level = level.downcase.to_sym
72
- end
58
+
59
+ level = case level
60
+ when /^[e0]/i
61
+ :error
62
+ when /^[w1]/i
63
+ :warn
64
+ when /^[d3]/i
65
+ :debug
66
+ else
67
+ :info
68
+ end
73
69
 
74
70
  @level = level
75
71
  end
@@ -84,46 +80,6 @@ module Doing
84
80
  # log_now :debug, 'Doing Version:', Doing::VERSION
85
81
  end
86
82
 
87
- def format_counter(key, data)
88
- case key
89
- when :added_tags
90
- ['Tagged:', data[:message] || 'added %tags to %count %items']
91
- when :removed_tags
92
- ['Untagged:', data[:message] || 'removed %tags from %count %items']
93
- when :added
94
- ['Added:', data[:message] || 'added %count new %items']
95
- when :updated
96
- ['Updated:', data[:message] || 'updated %count %items']
97
- when :deleted
98
- ['Deleted:', data[:message] || 'deleted %count %items']
99
- when :moved
100
- ['Moved:', data[:message] || 'moved %count %items']
101
- when :completed
102
- ['Completed:', data[:message] || 'completed %count %items']
103
- when :archived
104
- ['Archived:', data[:message] || 'archived %count %items']
105
- when :completed_archived
106
- ['Archived:', data[:message] || 'completed and archived %count %items']
107
- when :skipped
108
- ['Skipped:', data[:message] || '%count %items were unchanged']
109
- end
110
- end
111
-
112
- def total_counters
113
- @counters.each do |key, data|
114
- next if data[:count].zero?
115
-
116
- count = data[:count]
117
- tags = data[:tag] ? data[:tag].uniq.map { |t| "@#{t}".cyan }.join(', ') : 'tags'
118
- topic, m = format_counter(key, data)
119
- message = m.dup
120
- message.sub!(/%count/, count.to_s)
121
- message.sub!(/%items/, count == 1 ? 'item' : 'items')
122
- message.sub!(/%tags/, tags)
123
- write(data[:level], topic, message)
124
- end
125
- end
126
-
127
83
  def count(key, level: :info, count: 1, tag: nil, message: nil)
128
84
  raise ArgumentError, 'invalid counter key' unless COUNT_KEYS.include?(key)
129
85
 
@@ -134,7 +90,7 @@ module Doing
134
90
  end
135
91
 
136
92
  #
137
- # @brief Print a debug message
93
+ # Print a debug message
138
94
  #
139
95
  # @param topic the topic of the message
140
96
  # @param message the message detail
@@ -146,7 +102,7 @@ module Doing
146
102
  end
147
103
 
148
104
  #
149
- # @brief Print a message
105
+ # Print a message
150
106
  #
151
107
  # @param topic the topic of the message, e.g.
152
108
  # "Configuration file",
@@ -160,7 +116,7 @@ module Doing
160
116
  end
161
117
 
162
118
  #
163
- # @brief Print a message
119
+ # Print a message
164
120
  #
165
121
  # @param topic the topic of the message, e.g.
166
122
  # "Configuration file",
@@ -174,7 +130,7 @@ module Doing
174
130
  end
175
131
 
176
132
  #
177
- # @brief Print an error message
133
+ # Print an error message
178
134
  #
179
135
  # @param topic the topic of the message, e.g.
180
136
  # "Configuration file",
@@ -188,7 +144,7 @@ module Doing
188
144
  end
189
145
 
190
146
  #
191
- # @brief Print an error message and immediately
147
+ # Print an error message and immediately
192
148
  # abort the process
193
149
  #
194
150
  # @param topic the topic of the message, e.g.
@@ -204,33 +160,8 @@ module Doing
204
160
  abort
205
161
  end
206
162
 
207
- # Internal: Build a topic method
208
163
  #
209
- # @param topic the topic of the message, e.g.
210
- # "Configuration file",
211
- # "Deprecation", etc.
212
- # @param message the message detail
213
- #
214
- # @return the formatted message
215
- #
216
- def message(topic, message = nil)
217
- raise ArgumentError, 'block or message, not both' if block_given? && message
218
-
219
- message = yield if block_given?
220
- message = message.to_s.gsub(/\s+/, ' ')
221
-
222
- return topic.ljust(TOPIC_WIDTH) if topic && message.strip.empty?
223
-
224
- topic = formatted_topic(topic, colon: block_given?)
225
- message.truncmiddle!(@max_length - TOPIC_WIDTH - 5)
226
- out = topic + message
227
- out.truncate!(@max_length) if @max_length.positive?
228
- messages << out
229
- out
230
- end
231
-
232
- #
233
- # @brief Format the topic
164
+ # Format the topic
234
165
  #
235
166
  # @param topic the topic of the message, e.g.
236
167
  # "Configuration file",
@@ -250,39 +181,34 @@ module Doing
250
181
  end
251
182
 
252
183
  #
253
- # @brief Check if the message should be written
254
- # given the log level.
255
- #
256
- # @param level_of_message the Symbol level of
257
- # message, one of :debug,
258
- # :info, :warn, :error
259
- #
260
- # @return whether the message should be written.
261
- #
262
- def write_message?(level_of_message)
263
- LOG_LEVELS.fetch(@level) <= LOG_LEVELS.fetch(level_of_message)
264
- end
265
-
266
- #
267
- # @brief Log a message.
184
+ # Log a message.
268
185
  #
269
- # @param level_of_message the Symbol level of
270
- # message, one of :debug,
271
- # :info, :warn, :error
272
- # @param topic the String topic or full
273
- # message
274
- # @param message the String message
275
- # (optional)
186
+ # @param level_of_message [Symbol] the Symbol
187
+ # level of message, one of
188
+ # :debug, :info, :warn,
189
+ # :error
190
+ # @param topic [String] the String
191
+ # topic or full message
192
+ # @param message [String] the String
193
+ # message (optional)
276
194
  # @param block a block containing the
277
195
  # message (optional)
278
196
  #
279
- # @return false if the message was not written
197
+ # @return [Boolean] false if the message was not written
280
198
  #
281
199
  def write(level_of_message, topic, message = nil, &block)
282
200
  @results << { level: level_of_message, message: message(topic, message, &block) }
283
201
  true
284
202
  end
285
203
 
204
+ ##
205
+ ## Log to console immediately instead of writing messages on exit
206
+ ##
207
+ ## @param level [Symbol] The level
208
+ ## @param topic [String] The topic or full message
209
+ ## @param message [String] The message (optional)
210
+ ## @param block a block containing the message (optional)
211
+ ##
286
212
  def log_now(level, topic, message = nil, &block)
287
213
  return false unless write_message?(level)
288
214
 
@@ -293,6 +219,11 @@ module Doing
293
219
  end
294
220
  end
295
221
 
222
+ ##
223
+ ## Output registers based on log level
224
+ ##
225
+ ## @return nothing
226
+ ##
296
227
  def output_results
297
228
  total_counters
298
229
 
@@ -309,6 +240,85 @@ module Doing
309
240
 
310
241
  private
311
242
 
243
+ def format_counter(key, data)
244
+ case key
245
+ when :added_tags
246
+ ['Tagged:', data[:message] || 'added %tags to %count %items']
247
+ when :removed_tags
248
+ ['Untagged:', data[:message] || 'removed %tags from %count %items']
249
+ when :added
250
+ ['Added:', data[:message] || 'added %count new %items']
251
+ when :updated
252
+ ['Updated:', data[:message] || 'updated %count %items']
253
+ when :deleted
254
+ ['Deleted:', data[:message] || 'deleted %count %items']
255
+ when :moved
256
+ ['Moved:', data[:message] || 'moved %count %items']
257
+ when :completed
258
+ ['Completed:', data[:message] || 'completed %count %items']
259
+ when :archived
260
+ ['Archived:', data[:message] || 'archived %count %items']
261
+ when :completed_archived
262
+ ['Archived:', data[:message] || 'completed and archived %count %items']
263
+ when :skipped
264
+ ['Skipped:', data[:message] || '%count %items were unchanged']
265
+ end
266
+ end
267
+
268
+ def total_counters
269
+ @counters.each do |key, data|
270
+ next if data[:count].zero?
271
+
272
+ count = data[:count]
273
+ tags = data[:tag] ? data[:tag].uniq.map { |t| "@#{t}".cyan }.join(', ') : 'tags'
274
+ topic, m = format_counter(key, data)
275
+ message = m.dup
276
+ message.sub!(/%count/, count.to_s)
277
+ message.sub!(/%items/, count == 1 ? 'item' : 'items')
278
+ message.sub!(/%tags/, tags)
279
+ write(data[:level], topic, message)
280
+ end
281
+ end
282
+
283
+ #
284
+ # Check if the message should be written
285
+ # given the log level.
286
+ #
287
+ # @param level_of_message the Symbol level of
288
+ # message, one of :debug,
289
+ # :info, :warn, :error
290
+ #
291
+ # @return whether the message should be written.
292
+ #
293
+ def write_message?(level_of_message)
294
+ LOG_LEVELS.fetch(@level) <= LOG_LEVELS.fetch(level_of_message)
295
+ end
296
+
297
+ # Internal: Build a topic method
298
+ #
299
+ # @param topic the topic of the message, e.g.
300
+ # "Configuration file",
301
+ # "Deprecation", etc.
302
+ # @param message the message detail
303
+ #
304
+ # @return the formatted message
305
+ #
306
+ def message(topic, message = nil)
307
+ raise ArgumentError, 'block or message, not both' if block_given? && message
308
+
309
+ message = yield if block_given?
310
+ message = message.to_s.gsub(/\s+/, ' ')
311
+
312
+ return topic.ljust(TOPIC_WIDTH) if topic && message.strip.empty?
313
+
314
+ topic = formatted_topic(topic, colon: block_given?)
315
+ message.truncmiddle!(@max_length - TOPIC_WIDTH - 5)
316
+ out = topic + message
317
+ out.truncate!(@max_length) if @max_length.positive?
318
+ messages << out
319
+ out
320
+ end
321
+
312
322
  def color_message(level, topic, message = nil, &block)
313
323
  colors = Doing::Color
314
324
  message = message(topic, message, &block)