doing 2.1.2pre → 2.1.6pre

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 (116) 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 +62 -14
  7. data/Gemfile.lock +25 -1
  8. data/README.md +5 -1
  9. data/Rakefile +2 -0
  10. data/bin/doing +429 -142
  11. data/docs/_config.yml +1 -0
  12. data/{doc → docs/doc}/Array.html +63 -1
  13. data/docs/doc/BooleanTermParser/Clause.html +293 -0
  14. data/docs/doc/BooleanTermParser/Operator.html +172 -0
  15. data/docs/doc/BooleanTermParser/Query.html +417 -0
  16. data/docs/doc/BooleanTermParser/QueryParser.html +135 -0
  17. data/docs/doc/BooleanTermParser/QueryTransformer.html +124 -0
  18. data/docs/doc/BooleanTermParser.html +115 -0
  19. data/docs/doc/Doing/CLIFormat.html +131 -0
  20. data/{doc → docs/doc}/Doing/Color.html +2 -2
  21. data/{doc → docs/doc}/Doing/Completion.html +1 -1
  22. data/{doc → docs/doc}/Doing/Configuration.html +163 -69
  23. data/{doc → docs/doc}/Doing/Content.html +0 -0
  24. data/{doc → docs/doc}/Doing/Errors/DoingNoTraceError.html +1 -1
  25. data/{doc → docs/doc}/Doing/Errors/DoingRuntimeError.html +1 -1
  26. data/{doc → docs/doc}/Doing/Errors/DoingStandardError.html +1 -1
  27. data/{doc → docs/doc}/Doing/Errors/EmptyInput.html +1 -1
  28. data/{doc → docs/doc}/Doing/Errors/NoResults.html +1 -1
  29. data/{doc → docs/doc}/Doing/Errors/PluginException.html +1 -1
  30. data/{doc → docs/doc}/Doing/Errors/UserCancelled.html +1 -1
  31. data/{doc → docs/doc}/Doing/Errors/WrongCommand.html +1 -1
  32. data/{doc → docs/doc}/Doing/Errors.html +1 -1
  33. data/{doc → docs/doc}/Doing/Hooks.html +1 -1
  34. data/{doc → docs/doc}/Doing/Item.html +135 -89
  35. data/{doc → docs/doc}/Doing/Items.html +36 -2
  36. data/{doc → docs/doc}/Doing/LogAdapter.html +70 -1
  37. data/{doc → docs/doc}/Doing/Note.html +5 -134
  38. data/{doc → docs/doc}/Doing/Pager.html +1 -1
  39. data/{doc → docs/doc}/Doing/Plugins.html +431 -35
  40. data/{doc → docs/doc}/Doing/Prompt.html +70 -18
  41. data/{doc → docs/doc}/Doing/Section.html +1 -1
  42. data/docs/doc/Doing/TemplateString.html +713 -0
  43. data/docs/doc/Doing/Util/Backup.html +686 -0
  44. data/{doc → docs/doc}/Doing/Util.html +16 -4
  45. data/{doc → docs/doc}/Doing/WWID.html +133 -73
  46. data/{doc → docs/doc}/Doing/WWIDFile.html +0 -0
  47. data/{doc → docs/doc}/Doing.html +4 -4
  48. data/{doc → docs/doc}/GLI/Commands/MarkdownDocumentListener.html +1 -1
  49. data/{doc → docs/doc}/GLI/Commands.html +1 -1
  50. data/{doc → docs/doc}/GLI.html +1 -1
  51. data/{doc → docs/doc}/Hash.html +1 -1
  52. data/docs/doc/PhraseParser/Operator.html +172 -0
  53. data/docs/doc/PhraseParser/PhraseClause.html +303 -0
  54. data/docs/doc/PhraseParser/Query.html +495 -0
  55. data/docs/doc/PhraseParser/QueryParser.html +136 -0
  56. data/docs/doc/PhraseParser/QueryTransformer.html +124 -0
  57. data/docs/doc/PhraseParser/TermClause.html +293 -0
  58. data/docs/doc/PhraseParser.html +115 -0
  59. data/{doc → docs/doc}/Status.html +1 -1
  60. data/{doc → docs/doc}/String.html +319 -13
  61. data/{doc → docs/doc}/Symbol.html +35 -1
  62. data/{doc → docs/doc}/Time.html +70 -2
  63. data/{doc → docs/doc}/_index.html +132 -4
  64. data/docs/doc/class_list.html +51 -0
  65. data/{doc → docs/doc}/css/common.css +0 -0
  66. data/{doc → docs/doc}/css/full_list.css +0 -0
  67. data/{doc → docs/doc}/css/style.css +0 -0
  68. data/{doc → docs/doc}/file.README.html +6 -2
  69. data/{doc → docs/doc}/file_list.html +0 -0
  70. data/{doc → docs/doc}/frames.html +0 -0
  71. data/{doc → docs/doc}/index.html +6 -2
  72. data/{doc → docs/doc}/js/app.js +0 -0
  73. data/{doc → docs/doc}/js/full_list.js +0 -0
  74. data/{doc → docs/doc}/js/jquery.js +0 -0
  75. data/{doc → docs/doc}/method_list.html +684 -196
  76. data/{doc → docs/doc}/top-level-namespace.html +2 -2
  77. data/docs/index.md +60 -0
  78. data/doing.gemspec +3 -0
  79. data/doing.rdoc +222 -74
  80. data/example_plugin.rb +3 -1
  81. data/lib/completion/_doing.zsh +53 -41
  82. data/lib/completion/doing.bash +17 -6
  83. data/lib/completion/doing.fish +321 -2
  84. data/lib/doing/array.rb +9 -0
  85. data/lib/doing/boolean_term_parser.rb +86 -0
  86. data/lib/doing/completion/fish_completion.rb +46 -3
  87. data/lib/doing/completion/zsh_completion.rb +1 -1
  88. data/lib/doing/configuration.rb +48 -21
  89. data/lib/doing/item.rb +105 -10
  90. data/lib/doing/items.rb +6 -0
  91. data/lib/doing/log_adapter.rb +28 -0
  92. data/lib/doing/note.rb +31 -30
  93. data/lib/doing/phrase_parser.rb +124 -0
  94. data/lib/doing/plugin_manager.rb +84 -21
  95. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  96. data/lib/doing/plugins/export/html_export.rb +2 -2
  97. data/lib/doing/plugins/export/json_export.rb +1 -0
  98. data/lib/doing/plugins/export/markdown_export.rb +1 -1
  99. data/lib/doing/plugins/export/template_export.rb +94 -86
  100. data/lib/doing/prompt.rb +26 -15
  101. data/lib/doing/string.rb +114 -29
  102. data/lib/doing/string_chronify.rb +5 -1
  103. data/lib/doing/symbol.rb +4 -0
  104. data/lib/doing/template_string.rb +197 -0
  105. data/lib/doing/time.rb +32 -0
  106. data/lib/doing/util.rb +6 -7
  107. data/lib/doing/util_backup.rb +287 -0
  108. data/lib/doing/version.rb +1 -1
  109. data/lib/doing/wwid.rb +105 -41
  110. data/lib/doing.rb +9 -0
  111. data/lib/examples/plugins/say_export.rb +1 -1
  112. data/lib/examples/plugins/wiki_export/wiki_export.rb +3 -3
  113. data/lib/templates/doing-dayone-entry.erb +6 -0
  114. data/lib/templates/doing-dayone.erb +5 -0
  115. metadata +136 -51
  116. data/doc/class_list.html +0 -51
data/lib/doing/prompt.rb CHANGED
@@ -16,6 +16,14 @@ module Doing
16
16
  @default_answer ||= false
17
17
  end
18
18
 
19
+ def enter_text(prompt, default_response: '')
20
+ return default_response if @default_answer
21
+
22
+ print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
23
+ $stdin.gets.strip
24
+ end
25
+
26
+
19
27
  ##
20
28
  ## Ask a yes or no question in the terminal
21
29
  ##
@@ -110,14 +118,17 @@ module Doing
110
118
  return nil unless $stdout.isatty
111
119
 
112
120
  # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
113
- fzf_args << %(--prompt="#{prompt}")
114
- fzf_args << "--height=#{options.count + 2}"
115
- fzf_args << '--info=inline'
116
- fzf_args << '--multi' if multiple
121
+ default_args = []
122
+ default_args << %(--prompt="#{prompt}")
123
+ default_args << "--height=#{options.count + 2}"
124
+ default_args << '--info=inline'
125
+ default_args << '--multi' if multiple
117
126
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
118
- fzf_args << %(--header="#{header}")
127
+ default_args << %(--header="#{header}")
128
+ default_args.concat(fzf_args)
119
129
  options.sort! if sorted
120
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
130
+
131
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
121
132
  return false if res.strip.size.zero?
122
133
 
123
134
  res
@@ -127,16 +138,16 @@ module Doing
127
138
  ## Create an interactive menu to select from a set of Items
128
139
  ##
129
140
  ## @param items [Array] list of items
130
- ## @param opt [Hash] options
131
- ## @param include_section [Boolean] include section
141
+ ## @param opt Additional options
132
142
  ##
133
- ## @option opt [String] :header
134
- ## @option opt [String] :prompt
135
- ## @option opt [String] :query
136
- ## @option opt [Boolean] :show_if_single
137
- ## @option opt [Boolean] :menu
138
- ## @option opt [Boolean] :sort
139
- ## @option opt [Boolean] :multiple
143
+ ## @option opt [Boolean] :include_section Include section name for each item in menu
144
+ ## @option opt [String] :header A custom header string
145
+ ## @option opt [String] :prompt A custom prompt string
146
+ ## @option opt [String] :query Initial query
147
+ ## @option opt [Boolean] :show_if_single Show menu even if there's only one option
148
+ ## @option opt [Boolean] :menu Show menu
149
+ ## @option opt [Boolean] :sort Sort options
150
+ ## @option opt [Boolean] :multiple Allow multiple selections
140
151
  ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
141
152
  ##
142
153
  def choose_from_items(items, **opt)
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
  ##
@@ -162,6 +167,29 @@ module Doing
162
167
  replace uncolor
163
168
  end
164
169
 
170
+ def simple_wrap(width)
171
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
172
+ words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
173
+ out = []
174
+ line = []
175
+
176
+ words.each do |word|
177
+ if word.uncolor.length >= width
178
+ chars = word.uncolor.split('')
179
+ out << chars.slice!(0, width - 1).join('') while chars.count >= width
180
+ line << chars.join('')
181
+ next
182
+ elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > width
183
+ out.push(line.join(' '))
184
+ line.clear
185
+ end
186
+
187
+ line << word.uncolor
188
+ end
189
+ out.push(line.join(' '))
190
+ out.join("\n")
191
+ end
192
+
165
193
  ##
166
194
  ## Wrap string at word breaks, respecting tags
167
195
  ##
@@ -169,16 +197,36 @@ module Doing
169
197
  ## @param offset [Integer] (Optional) The width to pad each subsequent line
170
198
  ## @param prefix [String] (Optional) A prefix to add to each line
171
199
  ##
172
- 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)
173
201
  last_color = color.empty? ? '' : after.last_color
174
- 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
+
175
216
  # Don't break inside of tag values
176
- str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }
217
+ str = gsub(/@\S+\(.*?\)/) { |tag| tag.gsub(/\s/, '%%%%') }.gsub(/\n/, ' ')
218
+
177
219
  words = str.split(/ /).map { |word| word.gsub(/%%%%/, ' ') }
178
220
  out = []
179
221
  line = []
222
+
180
223
  words.each do |word|
181
- if line.join(' ').uncolor.length + word.uncolor.length + 1 > len
224
+ if word.uncolor.length >= len
225
+ chars = word.uncolor.split('')
226
+ out << chars.slice!(0, len - 1).join('') while chars.count >= len
227
+ line << chars.join('')
228
+ next
229
+ elsif line.join(' ').uncolor.length + word.uncolor.length + 1 > len
182
230
  out.push(line.join(' '))
183
231
  line.clear
184
232
  end
@@ -186,17 +234,18 @@ module Doing
186
234
  line << word.uncolor
187
235
  end
188
236
  out.push(line.join(' '))
189
- note = ''
190
- after.sub!(note_rx) do
191
- note = Regexp.last_match(0)
192
- ''
193
- end
194
237
 
238
+ last_color = ''
195
239
  out[0] = format("%-#{pad}s%s%s", out[0], last_color, after)
196
240
 
197
- left_pad = ' ' * offset
198
- left_pad += indent
199
- 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
200
249
  end
201
250
 
202
251
  ##
@@ -210,6 +259,10 @@ module Doing
210
259
  end
211
260
  end
212
261
 
262
+ def pluralize(number)
263
+ number == 1 ? self : "#{self}s"
264
+ end
265
+
213
266
  ##
214
267
  ## Convert a sort order string to a qualified type
215
268
  ##
@@ -241,7 +294,7 @@ module Doing
241
294
 
242
295
  def normalize_case(default = :smart)
243
296
  case self
244
- when /^c/i
297
+ when /^(c|sens)/i
245
298
  :sensitive
246
299
  when /^i/i
247
300
  :ignore
@@ -269,11 +322,35 @@ module Doing
269
322
  :or
270
323
  when /(not|none)/i
271
324
  :not
325
+ when /^p/i
326
+ :pattern
272
327
  else
273
328
  default.is_a?(Symbol) ? default : default.normalize_bool
274
329
  end
275
330
  end
276
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
+
277
354
  def normalize_trigger!
278
355
  replace normalize_trigger
279
356
  end
@@ -282,8 +359,16 @@ module Doing
282
359
  gsub(/\((?!\?:)/, '(?:').downcase
283
360
  end
284
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
+
285
370
  def to_tags
286
- gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map { |t| t.strip.sub(/^@/, '') }
371
+ gsub(/ *, */, ' ').gsub(/ +/, ' ').split(/ /).sort.uniq.map(&:add_at)
287
372
  end
288
373
 
289
374
  def add_tags!(tags, remove: false)
@@ -503,7 +588,7 @@ module Doing
503
588
  end
504
589
  else
505
590
  case self
506
- when / *, */
591
+ when /(^\[.*?\]$| *, *)/
507
592
  gsub(/^\[ *| *\]$/, '').split(/ *, */)
508
593
  when /^[0-9]+$/
509
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