doing 2.1.3 → 2.1.6

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +19 -15
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/.yardopts +1 -1
  6. data/CHANGELOG.md +48 -0
  7. data/Gemfile.lock +25 -1
  8. data/README.md +5 -1
  9. data/bin/doing +429 -142
  10. data/docs/_config.yml +1 -0
  11. data/{doc → docs/doc}/Array.html +63 -1
  12. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  13. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  14. data/docs/doc/BooleanTermParser/Query.html +417 -0
  15. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  16. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  17. data/docs/doc/BooleanTermParser.html +115 -0
  18. data/docs/doc/Doing/CLIFormat.html +131 -0
  19. data/{doc → docs/doc}/Doing/Color.html +2 -2
  20. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  21. data/{doc → docs/doc}/Doing/Configuration.html +157 -11
  22. data/{doc → docs/doc}/Doing/Content.html +0 -0
  23. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  24. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  32. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  33. data/{doc → docs/doc}/Doing/Item.html +134 -73
  34. data/{doc → docs/doc}/Doing/Items.html +36 -2
  35. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  36. data/{doc → docs/doc}/Doing/Note.html +5 -134
  37. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  38. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  39. data/{doc → docs/doc}/Doing/Prompt.html +35 -1
  40. data/{doc → docs/doc}/Doing/Section.html +1 -1
  41. data/docs/doc/Doing/TemplateString.html +713 -0
  42. data/docs/doc/Doing/Util/Backup.html +686 -0
  43. data/{doc → docs/doc}/Doing/Util.html +16 -4
  44. data/{doc → docs/doc}/Doing/WWID.html +133 -73
  45. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  46. data/{doc → docs/doc}/Doing.html +4 -4
  47. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  48. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  49. data/{doc → docs/doc}/GLI.html +1 -1
  50. data/{doc → docs/doc}/Hash.html +1 -1
  51. data/docs/doc/PhraseParser/Operator.html +172 -0
  52. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  53. data/docs/doc/PhraseParser/Query.html +495 -0
  54. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  55. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  56. data/docs/doc/PhraseParser/TermClause.html +293 -0
  57. data/docs/doc/PhraseParser.html +115 -0
  58. data/{doc → docs/doc}/Status.html +1 -1
  59. data/{doc → docs/doc}/String.html +285 -13
  60. data/{doc → docs/doc}/Symbol.html +35 -1
  61. data/{doc → docs/doc}/Time.html +70 -2
  62. data/{doc → docs/doc}/_index.html +132 -4
  63. data/docs/doc/class_list.html +51 -0
  64. data/{doc → docs/doc}/css/common.css +0 -0
  65. data/{doc → docs/doc}/css/full_list.css +0 -0
  66. data/{doc → docs/doc}/css/style.css +0 -0
  67. data/{doc → docs/doc}/file.README.html +6 -2
  68. data/{doc → docs/doc}/file_list.html +0 -0
  69. data/{doc → docs/doc}/frames.html +0 -0
  70. data/{doc → docs/doc}/index.html +6 -2
  71. data/{doc → docs/doc}/js/app.js +0 -0
  72. data/{doc → docs/doc}/js/full_list.js +0 -0
  73. data/{doc → docs/doc}/js/jquery.js +0 -0
  74. data/{doc → docs/doc}/method_list.html +624 -136
  75. data/{doc → docs/doc}/top-level-namespace.html +2 -2
  76. data/docs/index.md +60 -0
  77. data/doing.gemspec +3 -0
  78. data/doing.rdoc +222 -74
  79. data/example_plugin.rb +3 -1
  80. data/lib/completion/_doing.zsh +53 -41
  81. data/lib/completion/doing.bash +17 -6
  82. data/lib/completion/doing.fish +321 -2
  83. data/lib/doing/array.rb +9 -0
  84. data/lib/doing/boolean_term_parser.rb +86 -0
  85. data/lib/doing/completion/fish_completion.rb +46 -3
  86. data/lib/doing/completion/zsh_completion.rb +1 -1
  87. data/lib/doing/configuration.rb +45 -14
  88. data/lib/doing/item.rb +104 -9
  89. data/lib/doing/items.rb +6 -0
  90. data/lib/doing/log_adapter.rb +28 -0
  91. data/lib/doing/note.rb +31 -30
  92. data/lib/doing/phrase_parser.rb +124 -0
  93. data/lib/doing/plugin_manager.rb +84 -21
  94. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  95. data/lib/doing/plugins/export/html_export.rb +2 -2
  96. data/lib/doing/plugins/export/json_export.rb +1 -0
  97. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  98. data/lib/doing/plugins/export/template_export.rb +90 -85
  99. data/lib/doing/prompt.rb +17 -6
  100. data/lib/doing/string.rb +84 -29
  101. data/lib/doing/string_chronify.rb +5 -1
  102. data/lib/doing/symbol.rb +4 -0
  103. data/lib/doing/template_string.rb +197 -0
  104. data/lib/doing/time.rb +32 -0
  105. data/lib/doing/util.rb +6 -7
  106. data/lib/doing/util_backup.rb +287 -0
  107. data/lib/doing/version.rb +1 -1
  108. data/lib/doing/wwid.rb +105 -41
  109. data/lib/doing.rb +9 -0
  110. data/lib/examples/plugins/say_export.rb +1 -1
  111. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  112. data/lib/templates/doing-dayone-entry.erb +6 -0
  113. data/lib/templates/doing-dayone.erb +5 -0
  114. metadata +136 -51
  115. data/doc/class_list.html +0 -51
data/lib/doing/string.rb CHANGED
@@ -28,7 +28,9 @@ module Doing
28
28
  ##
29
29
  ## @return [Regexp] Regex pattern
30
30
  ##
31
- def to_rx(distance: 3, case_type: :smart)
31
+ def to_rx(distance: nil, case_type: nil)
32
+ distance ||= Doing.config.settings.dig('search', 'distance').to_i || 3
33
+ case_type ||= Doing.config.settings.dig('search', 'case')&.normalize_case || :smart
32
34
  case_sensitive = case case_type
33
35
  when :smart
34
36
  self =~ /[A-Z]/ ? true : false
@@ -44,7 +46,9 @@ module Doing
44
46
  when /^'/
45
47
  sub(/^'(.*?)'?$/, '\1')
46
48
  else
47
- split(/ +/).map { |w| w.split('').join(".{0,#{distance}}") }.join('.*?')
49
+ split(/ +/).map do |w|
50
+ w.split('').join(".{0,#{distance}}").gsub(/\+/, '\+').wildcard_to_rx
51
+ end.join('.*?')
48
52
  end
49
53
  Regexp.new(pattern, !case_sensitive)
50
54
  end
@@ -72,7 +76,7 @@ module Doing
72
76
  end
73
77
 
74
78
  ## @param (see #highlight_tags)
75
- def highlight_tags!(color = 'yellow')
79
+ def highlight_tags!(color = 'yellow', last_color: nil)
76
80
  replace highlight_tags(color)
77
81
  end
78
82
 
@@ -83,17 +87,18 @@ module Doing
83
87
  ##
84
88
  ## @return [String] string with @tags highlighted
85
89
  ##
86
- def highlight_tags(color = 'yellow')
87
- escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
88
- color = color.split(' ') unless color.is_a?(Array)
89
- tag_color = ''
90
- color.each { |c| tag_color += Doing::Color.send(c) }
91
- last_color = if !escapes.empty?
92
- escapes[-1][0]
93
- else
94
- Doing::Color.default
95
- end
96
- gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{Doing::Color.reset}#{last_color}")
90
+ def highlight_tags(color = 'yellow', last_color: nil)
91
+ unless last_color
92
+ escapes = scan(/(\e\[[\d;]+m)[^\e]+@/)
93
+ color = color.split(' ') unless color.is_a?(Array)
94
+ tag_color = color.each_with_object([]) { |c, arr| arr << Doing::Color.send(c) }.join('')
95
+ last_color = if !escapes.empty?
96
+ (escapes.count > 1 ? escapes[-2..-1] : [escapes[-1]]).map { |v| v[0] }.join('')
97
+ else
98
+ Doing::Color.default
99
+ end
100
+ end
101
+ gsub(/(\s|m)(@[^ ("']+)/, "\\1#{tag_color}\\2#{last_color}")
97
102
  end
98
103
 
99
104
  ##
@@ -192,11 +197,25 @@ module Doing
192
197
  ## @param offset [Integer] (Optional) The width to pad each subsequent line
193
198
  ## @param prefix [String] (Optional) A prefix to add to each line
194
199
  ##
195
- def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '')
200
+ def wrap(len, pad: 0, indent: ' ', offset: 0, prefix: '', color: '', after: '', reset: '', pad_first: false)
196
201
  last_color = color.empty? ? '' : after.last_color
197
- note_rx = /(?i-m)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)/
202
+ note_rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?note/
203
+ note = ''
204
+ after = after.dup if after.frozen?
205
+ after.sub!(note_rx) do
206
+ note = Regexp.last_match(0)
207
+ ''
208
+ end
209
+
210
+ left_pad = ' ' * offset
211
+ left_pad += indent
212
+
213
+
214
+ # return "#{left_pad}#{prefix}#{color}#{self}#{last_color} #{note}" unless len.positive?
215
+
198
216
  # Don't break inside of tag values
199
- str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
217
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
218
+
200
219
  words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
201
220
  out = []
202
221
  line = []
@@ -215,18 +234,18 @@ module Doing
215
234
  line << word.uncolor
216
235
  end
217
236
  out.push(line.join(' '))
218
- note = ''
219
- after = after.dup if after.frozen?
220
- after.sub!(note_rx) do
221
- note = Regexp.last_match(0)
222
- ''
223
- end
224
237
 
238
+ last_color = ''
225
239
  out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
226
240
 
227
- left_pad = ' ' * offset
228
- left_pad += indent
229
- out.map { |l| "#{left_pad}#{color}#{l}#{last_color}" }.join("\n").strip + last_color + " #{note}".chomp
241
+ out.map.with_index { |l, idx|
242
+ if !pad_first && idx == 0
243
+ "#{color}#{prefix}#{l}#{last_color}"
244
+ else
245
+ "#{left_pad}#{color}#{prefix}#{l}#{last_color}"
246
+ end
247
+ }.join("\n") + " #{note}".chomp
248
+ # res.join("\n").strip + last_color + " #{note}".chomp
230
249
  end
231
250
 
232
251
  ##
@@ -240,6 +259,10 @@ module Doing
240
259
  end
241
260
  end
242
261
 
262
+ def pluralize(number)
263
+ number == 1 ? self : "#{self}s"
264
+ end
265
+
243
266
  ##
244
267
  ## Convert a sort order string to a qualified type
245
268
  ##
@@ -271,7 +294,7 @@ module Doing
271
294
 
272
295
  def normalize_case(default = :smart)
273
296
  case self
274
- when /^c/i
297
+ when /^(c|sens)/i
275
298
  :sensitive
276
299
  when /^i/i
277
300
  :ignore
@@ -299,11 +322,35 @@ module Doing
299
322
  :or
300
323
  when /(not|none)/i
301
324
  :not
325
+ when /^p/i
326
+ :pattern
302
327
  else
303
328
  default.is_a?(Symbol) ? default : default.normalize_bool
304
329
  end
305
330
  end
306
331
 
332
+ ##
333
+ ## Convert a matching configuration string to a symbol
334
+ ##
335
+ ## @return Symbol :fuzzy, :pattern, :exact
336
+ ##
337
+ def normalize_matching!(default = :pattern)
338
+ replace normalize_bool(default)
339
+ end
340
+
341
+ def normalize_matching(default = :pattern)
342
+ case self
343
+ when /^f/i
344
+ :fuzzy
345
+ when /^p/i
346
+ :pattern
347
+ when /^e/i
348
+ :exact
349
+ else
350
+ default.is_a?(Symbol) ? default : default.normalize_matching
351
+ end
352
+ end
353
+
307
354
  def normalize_trigger!
308
355
  replace normalize_trigger
309
356
  end
@@ -312,8 +359,16 @@ module Doing
312
359
  gsub(/\((?!\?:)/, '(?:').downcase
313
360
  end
314
361
 
362
+ def wildcard_to_rx
363
+ gsub(/\?/, '\S').gsub(/\*/, '\S*?')
364
+ end
365
+
366
+ def add_at
367
+ strip.sub(/^([+-]*)@/, '\1')
368
+ end
369
+
315
370
  def to_tags
316
- gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map { |t| t.strip.sub(/^@/, '') }
371
+ gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map(&:add_at)
317
372
  end
318
373
 
319
374
  def add_tags!(tags, remove: false)
@@ -533,7 +588,7 @@ module Doing
533
588
  end
534
589
  else
535
590
  case self
536
- when / *, */
591
+ when /(^\[.*?\]$| *, *)/
537
592
  gsub(/^\[ *| *\]$/, '').split(/ *, */)
538
593
  when /^[0-9]+$/
539
594
  to_i
@@ -41,7 +41,11 @@ module Doing
41
41
  if secs_ago
42
42
  now - secs_ago
43
43
  else
44
- Chronic.parse(self, { guess: options.fetch(:guess, :begin), context: options.fetch(:future, false) ? :future : :past, ambiguous_time_range: 8 })
44
+ Chronic.parse(self, {
45
+ guess: options.fetch(:guess, :begin),
46
+ context: options.fetch(:future, false) ? :future : :past,
47
+ ambiguous_time_range: 8
48
+ })
45
49
  end
46
50
  end
47
51
 
data/lib/doing/symbol.rb CHANGED
@@ -16,5 +16,9 @@ module Doing
16
16
  def normalize_case
17
17
  self
18
18
  end
19
+
20
+ def normalize_matching(default = :pattern)
21
+ to_s.normalize_matching(default)
22
+ end
19
23
  end
20
24
  end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## Template string formatting
6
+ ##
7
+ class TemplateString < String
8
+ class ::String
9
+ ##
10
+ ## Extract the longest valid color from a string.
11
+ ##
12
+ ## Allows %colors to bleed into other text and still
13
+ ## be recognized, e.g. %greensomething still finds
14
+ ## %green.
15
+ ##
16
+ ## @return [String] a valid color name
17
+ ## @api private
18
+ def validate_color
19
+ valid_color = nil
20
+ compiled = ''
21
+ split('').each do |char|
22
+ compiled += char
23
+ valid_color = compiled if Color.attributes.include?(compiled.to_sym)
24
+ end
25
+
26
+ valid_color
27
+ end
28
+ end
29
+
30
+ attr_reader :original
31
+
32
+ include Color
33
+ def initialize(string, placeholders: {}, force_color: false, wrap_width: 0, color: '', tags_color: '', reset: '')
34
+ Color.coloring = true if force_color
35
+ @colors = nil
36
+ @original = string
37
+ super(Color.reset + string)
38
+
39
+ placeholders.each { |k, v| fill(k, v, wrap_width: wrap_width, color: color, tags_color: tags_color) }
40
+ end
41
+
42
+ ##
43
+ ## Test if string contains any valid %colors
44
+ ##
45
+ ## @return [Boolean] True if colors, False otherwise.
46
+ ##
47
+ def colors?
48
+ scan(/%([a-z]+)/).each do
49
+ return true if Regexp.last_match(1).validate_color
50
+ end
51
+ false
52
+ end
53
+
54
+ def reparse
55
+ @parsed_colors = nil
56
+ end
57
+
58
+ ##
59
+ ## Return string with %colors replaced with escape codes
60
+ ##
61
+ ## @return [String] colorized string
62
+ ##
63
+ def colored
64
+ reparse
65
+ parsed_colors[:string].apply_colors(parsed_colors[:colors])
66
+ end
67
+
68
+ ##
69
+ ## Remove all valid %colors from string
70
+ ##
71
+ ## @return [String] cleaned string
72
+ ##
73
+ def raw
74
+ parsed_colors[:string].uncolor
75
+ end
76
+
77
+ def parsed_colors
78
+ @parsed_colors ||= parse_colors
79
+ end
80
+
81
+ ##
82
+ ## Parse a template string for %colors and return a hash
83
+ ## of colors and string locations
84
+ ##
85
+ ## @return [Hash] Uncolored string and array of colors and locations
86
+ def parse_colors
87
+ working = dup
88
+ color_array = []
89
+
90
+ scan(/(?<!\\)(%([a-z]+))/).each do |color|
91
+ valid_color = color[1].validate_color
92
+ next unless valid_color
93
+
94
+ idx = working.match(/(?<!\\)%#{valid_color}/).begin(0)
95
+ color_array.push({ name: valid_color, color: Color.send(valid_color), index: idx })
96
+ working.sub!(/(?<!\\)%#{valid_color}/, '')
97
+ end
98
+
99
+ { string: working, colors: color_array }
100
+ end
101
+
102
+ ##
103
+ ## Apply a color array to a string
104
+ ##
105
+ ## @param color_array [Array] Array of hashes
106
+ ## containing :name, :color,
107
+ ## :index
108
+ ##
109
+ def apply_colors(color_array)
110
+ str = dup
111
+ color_array.reverse.each do |color|
112
+ c = color[:color].empty? ? Color.send(color[:name]) : color[:color]
113
+ str.insert(color[:index], c)
114
+ end
115
+ str
116
+ end
117
+
118
+ def fill(placeholder, value, wrap_width: 0, color: '', tags_color: '', reset: '')
119
+ reparse
120
+ rx = /(?mi)(?<!\\)%(?<width>-?\d+)?(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])(?<icount>\d+))?(?<prefix>.[ _t]?)?#{placeholder.sub(/^%/, '')}(?<after>.*?)$/
121
+ ph = raw.match(rx)
122
+
123
+ return unless ph
124
+ placeholder_offset = ph.begin(0)
125
+ last_colors = parsed_colors[:colors].select { |v| v[:index] <= placeholder_offset + 4 }
126
+
127
+ last_color = last_colors.map { |v| v[:color] }.pop(3).join('')
128
+
129
+ sub!(rx) do
130
+ m = Regexp.last_match
131
+
132
+ after = m['after']
133
+
134
+ if value.nil? || value.empty?
135
+ after
136
+ else
137
+ pad = m['width'].to_i
138
+ mark = m['mchar'] || ''
139
+ if placeholder == 'shortdate' && m['width'].nil?
140
+ pad = 13
141
+ end
142
+ indent = nil
143
+ if m['ichar']
144
+ char = m['ichar'] =~ /t/ ? "\t" : ' '
145
+ indent = char * m['icount'].to_i
146
+ end
147
+ indent ||= placeholder =~ /^title/ ? '' : "\t"
148
+ prefix = m['prefix']
149
+ if placeholder =~ /^title/
150
+ color = last_color + color
151
+
152
+ if wrap_width.positive? || pad.positive?
153
+ width = pad.positive? ? pad : wrap_width
154
+
155
+ out = value.gsub(/%/, '\%').strip.wrap(width,
156
+ pad: pad,
157
+ indent: indent,
158
+ offset: placeholder_offset,
159
+ prefix: prefix,
160
+ color: color,
161
+ after: after,
162
+ reset: reset,
163
+ pad_first: false)
164
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
165
+ out
166
+ else
167
+ out = format("%s%s%#{pad}s%s", prefix, color, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
168
+ out.highlight_tags!(tags_color, last_color: color) if tags_color && !tags_color.empty?
169
+ out
170
+ end
171
+ elsif placeholder =~ /^note/
172
+ if wrap_width.positive? || pad.positive?
173
+ width = pad.positive? ? pad : wrap_width
174
+ outstring = value.map do |l|
175
+ if l.empty?
176
+ ' '
177
+ else
178
+ line = l.gsub(/%/, '\%').strip.wrap(width, pad: pad, indent: indent, offset: 0, prefix: prefix, color: last_color, after: after, reset: reset, pad_first: true)
179
+ line.highlight_tags!(tags_color, last_color: last_color) unless tags_color.nil? || tags_color.empty?
180
+ "#{line} "
181
+ end
182
+ end.join("\n")
183
+ "\n#{last_color}#{mark}#{outstring} "
184
+ else
185
+ out = format("\n%s%s%s%#{pad}s%s", indent, prefix, last_color, value.join("\n#{indent}#{prefix}").gsub(/%/, '\%').sub(/\s*$/, ''), after)
186
+ out.highlight_tags!(tags_color, last_color: last_color) if tags_color && !tags_color.empty?
187
+ out
188
+ end
189
+ else
190
+ format("%s%#{pad}s%s", prefix, value.gsub(/%/, '\%').sub(/\s*$/, ''), after)
191
+ end
192
+ end
193
+ end
194
+ @parsed_colors = parse_colors
195
+ end
196
+ end
197
+ end
data/lib/doing/time.rb CHANGED
@@ -14,5 +14,37 @@ module Doing
14
14
  strftime('%m/%d/%Y %_I:%M%P')
15
15
  end
16
16
  end
17
+
18
+ def humanize(seconds)
19
+ s = seconds
20
+ m = (s / 60).floor
21
+ s = (s % 60).floor
22
+ h = (m / 60).floor
23
+ m = (m % 60).floor
24
+ d = (h / 24).floor
25
+ h = h % 24
26
+
27
+ output = []
28
+ output.push("#{d} #{'day'.pluralize(d)}") if d.positive?
29
+ output.push("#{h} #{'hour'.pluralize(h)}") if h.positive?
30
+ output.push("#{m} #{'minute'.pluralize(m)}") if m.positive?
31
+ output.push("#{s} #{'second'.pluralize(s)}") if s.positive?
32
+ output.join(', ')
33
+ end
34
+
35
+ def time_ago
36
+ if self > Date.today.to_time
37
+ output = humanize(Time.now - self)
38
+ "#{output} ago"
39
+ elsif self > (Date.today - 1).to_time
40
+ "Yesterday at #{strftime('%_I:%M:%S%P')}"
41
+ elsif self > (Date.today - 6).to_time
42
+ strftime('%a %I:%M:%S%P')
43
+ elsif self.year == Date.today.year
44
+ strftime('%m/%d %I:%M:%S%P')
45
+ else
46
+ strftime('%m/%d/%Y %I:%M:%S%P')
47
+ end
48
+ end
17
49
  end
18
50
  end
data/lib/doing/util.rb CHANGED
@@ -112,20 +112,19 @@ module Doing
112
112
  puts content
113
113
  return
114
114
  end
115
-
115
+ Doing.logger.benchmark(:write_file, :start)
116
116
  file = File.expand_path(file)
117
117
 
118
- if File.exist?(file) && backup
119
- # Create a backup copy for the undo command
120
- FileUtils.cp(file, "#{file}~")
121
- end
118
+ Backup.write_backup(file) if backup
122
119
 
123
120
  File.open(file, 'w+') do |f|
124
121
  f.puts content
125
122
  Doing.logger.debug('Write:', "File written: #{file}")
126
123
  end
127
-
124
+ Doing.logger.benchmark(:_post_write_hook, :start)
128
125
  Hooks.trigger :post_write, file
126
+ Doing.logger.benchmark(:_post_write_hook, :finish)
127
+ Doing.logger.benchmark(:write_file, :finish)
129
128
  end
130
129
 
131
130
  def safe_load_file(filename)
@@ -133,7 +132,7 @@ module Doing
133
132
  end
134
133
 
135
134
  def default_editor
136
- @default_editor = find_default_editor
135
+ @default_editor ||= find_default_editor
137
136
  end
138
137
 
139
138
  def editor_with_args