doing 2.1.1pre → 2.1.5pre

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 (92) 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/CHANGELOG.md +58 -8
  6. data/Gemfile.lock +25 -1
  7. data/README.md +1 -1
  8. data/Rakefile +2 -0
  9. data/bin/doing +447 -149
  10. data/doc/Array.html +63 -1
  11. data/doc/BooleanTermParser/Clause.html +293 -0
  12. data/doc/BooleanTermParser/Operator.html +172 -0
  13. data/doc/BooleanTermParser/Query.html +417 -0
  14. data/doc/BooleanTermParser/QueryParser.html +135 -0
  15. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  16. data/doc/BooleanTermParser.html +115 -0
  17. data/doc/Doing/CLIFormat.html +131 -0
  18. data/doc/Doing/Color.html +2 -2
  19. data/doc/Doing/Completion.html +1 -1
  20. data/doc/Doing/Configuration.html +168 -73
  21. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  22. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  23. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  24. data/doc/Doing/Errors/EmptyInput.html +1 -1
  25. data/doc/Doing/Errors/NoResults.html +1 -1
  26. data/doc/Doing/Errors/PluginException.html +1 -1
  27. data/doc/Doing/Errors/UserCancelled.html +1 -1
  28. data/doc/Doing/Errors/WrongCommand.html +1 -1
  29. data/doc/Doing/Errors.html +1 -1
  30. data/doc/Doing/Hooks.html +1 -1
  31. data/doc/Doing/Item.html +177 -86
  32. data/doc/Doing/Items.html +36 -2
  33. data/doc/Doing/LogAdapter.html +70 -1
  34. data/doc/Doing/Note.html +5 -134
  35. data/doc/Doing/Pager.html +1 -1
  36. data/doc/Doing/Plugins.html +380 -40
  37. data/doc/Doing/Prompt.html +70 -18
  38. data/doc/Doing/Section.html +1 -1
  39. data/doc/Doing/TemplateString.html +713 -0
  40. data/doc/Doing/Util/Backup.html +686 -0
  41. data/doc/Doing/Util.html +16 -4
  42. data/doc/Doing/WWID.html +133 -73
  43. data/doc/Doing.html +4 -4
  44. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  45. data/doc/GLI/Commands.html +1 -1
  46. data/doc/GLI.html +1 -1
  47. data/doc/Hash.html +1 -1
  48. data/doc/PhraseParser/Operator.html +172 -0
  49. data/doc/PhraseParser/PhraseClause.html +303 -0
  50. data/doc/PhraseParser/Query.html +495 -0
  51. data/doc/PhraseParser/QueryParser.html +136 -0
  52. data/doc/PhraseParser/QueryTransformer.html +124 -0
  53. data/doc/PhraseParser/TermClause.html +293 -0
  54. data/doc/PhraseParser.html +115 -0
  55. data/doc/Status.html +1 -1
  56. data/doc/String.html +319 -13
  57. data/doc/Symbol.html +35 -1
  58. data/doc/Time.html +70 -2
  59. data/doc/_index.html +132 -4
  60. data/doc/class_list.html +1 -1
  61. data/doc/file.README.html +2 -2
  62. data/doc/index.html +2 -2
  63. data/doc/method_list.html +648 -160
  64. data/doc/top-level-namespace.html +2 -2
  65. data/doing.gemspec +3 -0
  66. data/doing.rdoc +263 -82
  67. data/lib/completion/doing.bash +18 -18
  68. data/lib/doing/array.rb +9 -0
  69. data/lib/doing/boolean_term_parser.rb +86 -0
  70. data/lib/doing/configuration.rb +63 -24
  71. data/lib/doing/item.rb +112 -10
  72. data/lib/doing/items.rb +6 -0
  73. data/lib/doing/log_adapter.rb +28 -0
  74. data/lib/doing/note.rb +31 -30
  75. data/lib/doing/phrase_parser.rb +124 -0
  76. data/lib/doing/plugin_manager.rb +57 -13
  77. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  78. data/lib/doing/plugins/export/template_export.rb +113 -81
  79. data/lib/doing/prompt.rb +26 -13
  80. data/lib/doing/string.rb +114 -29
  81. data/lib/doing/string_chronify.rb +5 -1
  82. data/lib/doing/symbol.rb +4 -0
  83. data/lib/doing/template_string.rb +197 -0
  84. data/lib/doing/time.rb +32 -0
  85. data/lib/doing/util.rb +6 -7
  86. data/lib/doing/util_backup.rb +287 -0
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +152 -55
  89. data/lib/doing.rb +9 -0
  90. data/lib/templates/doing-dayone-entry.erb +6 -0
  91. data/lib/templates/doing-dayone.erb +5 -0
  92. metadata +85 -2
@@ -23,116 +23,148 @@ module Doing
23
23
  out = ''
24
24
  items.each do |item|
25
25
  if opt[:highlight] && item.title =~ /@#{wwid.config['marker_tag']}\b/i
26
- # flag = Doing::Color.send(wwid.config['marker_color'])
27
- reset = Doing::Color.default
26
+ flag = Doing::Color.send(wwid.config['marker_color'])
27
+ reset = Doing::Color.reset + Doing::Color.default
28
28
  else
29
- # flag = ''
29
+ flag = ''
30
30
  reset = ''
31
31
  end
32
32
 
33
+ placeholders = {}
34
+
33
35
  if (!item.note.empty?) && wwid.config['include_notes']
34
36
  note = item.note.map(&:strip).delete_if(&:empty?)
35
37
  note.map! { |line| "#{line.sub(/^\t*/, '')} " }
36
38
 
37
39
  if opt[:wrap_width]&.positive?
38
40
  width = opt[:wrap_width]
39
- note.map! { |line| line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n") }
40
- note = note.join("\n").split(/\n/).delete_if(&:empty?)
41
+ note.map! do |line|
42
+ line.simple_wrap(width)
43
+ # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
44
+ end
45
+ note = note.delete_if(&:empty?)
41
46
  end
42
47
  else
43
48
  note = []
44
49
  end
45
50
 
46
- output = opt[:template].dup
47
-
48
- output.gsub!(/%[a-z]+/) do |m|
49
- if Doing::Color.respond_to?(m.sub(/^%/, ''))
50
- Doing::Color.send(m.sub(/^%/, ''))
51
+ # output.sub!(/%(\d+)?date/) do
52
+ # pad = Regexp.last_match(1).to_i
53
+ # format("%#{pad}s", item.date.strftime(opt[:format]))
54
+ # end
55
+ placeholders['date'] = item.date.strftime(opt[:format])
56
+
57
+ interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
58
+ if interval
59
+ case opt[:interval_format].to_sym
60
+ when :human
61
+ _d, h, m = wwid.format_time(interval, human: true)
62
+ interval = format('%<h> 4dh %<m>02dm', h: h, m: m)
51
63
  else
52
- m
64
+ d, h, m = wwid.format_time(interval)
65
+ interval = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
53
66
  end
54
67
  end
55
68
 
56
- output.sub!(/%(\d+)?date/) do
57
- pad = Regexp.last_match(1).to_i
58
- format("%#{pad}s", item.date.strftime(opt[:format]))
59
- end
60
-
61
- interval = wwid.get_interval(item, record: true) if opt[:times]
62
-
63
69
  interval ||= ''
64
- output.sub!(/%interval/, interval)
65
-
66
- output.sub!(/%(\d+)?shortdate/) do
67
- pad = Regexp.last_match(1) || 13
68
- format("%#{pad}s", item.date.relative_date)
69
- end
70
-
71
- output.sub!(/%section/, item.section) if item.section
72
-
73
- title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
74
- title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
75
-
76
- title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
77
-
78
- output.sub!(title_rx) do
79
- m = Regexp.last_match
80
-
81
- after = m['after']
82
- pad = m['width'].to_i
83
- indent = ''
84
- if m['ichar']
85
- char = m['ichar'] =~ /t/ ? "\t" : ' '
86
- indent = char * m['icount'].to_i
87
- end
88
- prefix = m['prefix']
89
- if opt[:wrap_width]&.positive? || pad.positive?
90
- width = pad.positive? ? pad : opt[:wrap_width]
91
- item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
92
- # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
70
+ # output.sub!(/%interval/, interval)
71
+ placeholders['interval'] = interval
72
+
73
+ duration = item.duration if opt[:duration]
74
+ if duration
75
+ case opt[:interval_format].to_sym
76
+ when :human
77
+ _d, h, m = wwid.format_time(duration, human: true)
78
+ duration = format('%<h> 4dh %<m>02dm', h: h, m: m)
93
79
  else
94
- format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
80
+ d, h, m = wwid.format_time(duration)
81
+ duration = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
95
82
  end
96
83
  end
97
-
98
- # output.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
99
- if opt[:tags_color]
100
- output.highlight_tags!(opt[:tags_color])
101
- end
102
-
103
- if note.empty?
104
- output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
105
- else
106
- output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
107
- output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
108
- output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
109
- output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
110
- m = Regexp.last_match
111
- mark = m['mchar'] || ''
112
- indent = if m['ichar']
113
- char = m['ichar'] =~ /t/ ? "\t" : ' '
114
- char * m['icount'].to_i
115
- else
116
- ''
117
- end
118
- prefix = m['prefix'] || ''
119
- "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
120
- end
121
-
122
- output.sub!(/%chompnote/) do
123
- note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
124
- end
125
- end
126
-
127
- output.gsub!(/%hr(_under)?/) do
84
+ duration ||= ''
85
+ # output.sub!(/%duration/, duration)
86
+ placeholders['duration'] = duration
87
+
88
+ # output.sub!(/%(\d+)?shortdate/) do
89
+ # pad = Regexp.last_match(1) || 13
90
+ # format("%#{pad}s", item.date.relative_date)
91
+ # end
92
+ placeholders['shortdate'] = format("%13s", item.date.relative_date)
93
+ # output.sub!(/%section/, item.section) if item.section
94
+ placeholders['section'] = item.section || ''
95
+ placeholders['title'] = item.title
96
+
97
+ # title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
98
+ # title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
99
+
100
+ # title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
101
+
102
+ # output.sub!(title_rx) do
103
+ # m = Regexp.last_match
104
+
105
+ # after = m['after']
106
+ # pad = m['width'].to_i
107
+ # indent = ''
108
+ # if m['ichar']
109
+ # char = m['ichar'] =~ /t/ ? "\t" : ' '
110
+ # indent = char * m['icount'].to_i
111
+ # end
112
+ # prefix = m['prefix']
113
+ # if opt[:wrap_width]&.positive? || pad.positive?
114
+ # width = pad.positive? ? pad : opt[:wrap_width]
115
+ # item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
116
+ # # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
117
+ # else
118
+ # format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
119
+ # end
120
+ # end
121
+
122
+
123
+
124
+ placeholders['note'] = note
125
+ placeholders['idnote'] = note.empty? ? '' : "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}"
126
+ placeholders['odnote'] = note.empty? ? '' : "\n#{note.map { |l| "#{l.strip} " }.join("\n")}"
127
+ placeholders['chompnote'] = note.empty? ? '' : note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
128
+
129
+ # if note.empty?
130
+ # output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
131
+ # else
132
+ # output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
133
+ # output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
134
+ # output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
135
+ # output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
136
+ # m = Regexp.last_match
137
+ # mark = m['mchar'] || ''
138
+ # indent = if m['ichar']
139
+ # char = m['ichar'] =~ /t/ ? "\t" : ' '
140
+ # char * m['icount'].to_i
141
+ # else
142
+ # ''
143
+ # end
144
+ # prefix = m['prefix'] || ''
145
+ # "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
146
+ # end
147
+
148
+ # output.sub!(/%chompnote/) do
149
+ # note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
150
+ # end
151
+ # end
152
+
153
+ template = opt[:template].dup
154
+ template.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
155
+ output = Doing::TemplateString.new(template, placeholders: placeholders, wrap_width: opt[:wrap_width], color: flag, tags_color: opt[:tags_color], reset: reset).colored
156
+
157
+ output.gsub!(/(?<!\\)%hr(_under)?/) do
128
158
  o = ''
129
159
  `tput cols`.to_i.times do
130
160
  o += Regexp.last_match(1).nil? ? '-' : '_'
131
161
  end
132
162
  o
133
163
  end
134
- output.gsub!(/%n/, "\n")
135
- output.gsub!(/%t/, "\t")
164
+ output.gsub!(/(?<!\\)%n/, "\n")
165
+ output.gsub!(/(?<!\\)%t/, "\t")
166
+
167
+ output.gsub!(/\\%/, '%')
136
168
 
137
169
  out += "#{output}\n"
138
170
  end
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,12 +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 << '--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
115
126
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
116
- fzf_args << %(--header "#{header}")
127
+ default_args << %(--header="#{header}")
128
+ default_args.concat(fzf_args)
117
129
  options.sort! if sorted
118
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
130
+
131
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
119
132
  return false if res.strip.size.zero?
120
133
 
121
134
  res
@@ -125,16 +138,16 @@ module Doing
125
138
  ## Create an interactive menu to select from a set of Items
126
139
  ##
127
140
  ## @param items [Array] list of items
128
- ## @param opt [Hash] options
129
- ## @param include_section [Boolean] include section
141
+ ## @param opt Additional options
130
142
  ##
131
- ## @option opt [String] :header
132
- ## @option opt [String] :prompt
133
- ## @option opt [String] :query
134
- ## @option opt [Boolean] :show_if_single
135
- ## @option opt [Boolean] :menu
136
- ## @option opt [Boolean] :sort
137
- ## @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
138
151
  ## @option opt [Symbol] :case (:sensitive, :ignore, :smart)
139
152
  ##
140
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