doing 2.1.4pre → 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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/.yardoc/checksums +14 -13
  3. data/.yardoc/object_types +0 -0
  4. data/.yardoc/objects/root.dat +0 -0
  5. data/CHANGELOG.md +21 -0
  6. data/Gemfile.lock +3 -1
  7. data/README.md +1 -1
  8. data/bin/doing +149 -91
  9. data/doc/Array.html +63 -1
  10. data/doc/BooleanTermParser/Clause.html +293 -0
  11. data/doc/BooleanTermParser/Operator.html +172 -0
  12. data/doc/BooleanTermParser/Query.html +417 -0
  13. data/doc/BooleanTermParser/QueryParser.html +135 -0
  14. data/doc/BooleanTermParser/QueryTransformer.html +124 -0
  15. data/doc/BooleanTermParser.html +115 -0
  16. data/doc/Doing/CLIFormat.html +131 -0
  17. data/doc/Doing/Color.html +2 -2
  18. data/doc/Doing/Completion.html +1 -1
  19. data/doc/Doing/Configuration.html +116 -12
  20. data/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  21. data/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  22. data/doc/Doing/Errors/DoingStandardError.html +1 -1
  23. data/doc/Doing/Errors/EmptyInput.html +1 -1
  24. data/doc/Doing/Errors/NoResults.html +1 -1
  25. data/doc/Doing/Errors/PluginException.html +1 -1
  26. data/doc/Doing/Errors/UserCancelled.html +1 -1
  27. data/doc/Doing/Errors/WrongCommand.html +1 -1
  28. data/doc/Doing/Errors.html +1 -1
  29. data/doc/Doing/Hooks.html +1 -1
  30. data/doc/Doing/Item.html +100 -73
  31. data/doc/Doing/Items.html +2 -2
  32. data/doc/Doing/LogAdapter.html +70 -1
  33. data/doc/Doing/Note.html +5 -134
  34. data/doc/Doing/Pager.html +1 -1
  35. data/doc/Doing/Plugins.html +380 -40
  36. data/doc/Doing/Prompt.html +1 -1
  37. data/doc/Doing/Section.html +1 -1
  38. data/doc/Doing/TemplateString.html +713 -0
  39. data/doc/Doing/Util/Backup.html +686 -0
  40. data/doc/Doing/Util.html +1 -1
  41. data/doc/Doing/WWID.html +5 -5
  42. data/doc/Doing.html +4 -4
  43. data/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  44. data/doc/GLI/Commands.html +1 -1
  45. data/doc/GLI.html +1 -1
  46. data/doc/Hash.html +1 -1
  47. data/doc/PhraseParser/Operator.html +172 -0
  48. data/doc/PhraseParser/PhraseClause.html +303 -0
  49. data/doc/PhraseParser/Query.html +495 -0
  50. data/doc/PhraseParser/QueryParser.html +136 -0
  51. data/doc/PhraseParser/QueryTransformer.html +124 -0
  52. data/doc/PhraseParser/TermClause.html +293 -0
  53. data/doc/PhraseParser.html +115 -0
  54. data/doc/Status.html +1 -1
  55. data/doc/String.html +182 -12
  56. data/doc/Symbol.html +35 -1
  57. data/doc/Time.html +1 -1
  58. data/doc/_index.html +21 -14
  59. data/doc/class_list.html +1 -1
  60. data/doc/file.README.html +2 -2
  61. data/doc/index.html +2 -2
  62. data/doc/method_list.html +319 -175
  63. data/doc/top-level-namespace.html +1 -1
  64. data/doing.gemspec +1 -0
  65. data/doing.rdoc +56 -9
  66. data/lib/doing/array.rb +9 -0
  67. data/lib/doing/configuration.rb +30 -8
  68. data/lib/doing/item.rb +12 -3
  69. data/lib/doing/log_adapter.rb +28 -0
  70. data/lib/doing/note.rb +31 -30
  71. data/lib/doing/plugin_manager.rb +57 -13
  72. data/lib/doing/plugins/export/dayone_export.rb +209 -0
  73. data/lib/doing/plugins/export/template_export.rb +90 -85
  74. data/lib/doing/prompt.rb +9 -6
  75. data/lib/doing/string.rb +68 -27
  76. data/lib/doing/symbol.rb +4 -0
  77. data/lib/doing/template_string.rb +197 -0
  78. data/lib/doing/util.rb +4 -2
  79. data/lib/doing/util_backup.rb +55 -3
  80. data/lib/doing/version.rb +1 -1
  81. data/lib/doing/wwid.rb +27 -18
  82. data/lib/doing.rb +3 -0
  83. data/lib/templates/doing-dayone-entry.erb +6 -0
  84. data/lib/templates/doing-dayone.erb +5 -0
  85. metadata +42 -2
@@ -0,0 +1,209 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ # title: Day One Export
6
+ # description: Export entries to Day One plist for auto import
7
+ # author: Brett Terpstra
8
+ # url: https://brettterpstra.com
9
+ module Doing
10
+ class DayOneRenderer
11
+ attr_accessor :items, :page_title, :totals
12
+
13
+ def initialize(page_title, items, totals)
14
+ @page_title = page_title
15
+ @items = items
16
+ @totals = totals
17
+ end
18
+
19
+ def get_binding
20
+ binding()
21
+ end
22
+ end
23
+
24
+ class DayoneExport
25
+ include Doing::Util
26
+
27
+ def self.settings
28
+ {
29
+ trigger: 'day(?:one)?(?:-(?:days?|entries))?',
30
+ templates: [
31
+ { name: 'dayone', trigger: 'day(?:one)?$' },
32
+ { name: 'dayone_entry', trigger: 'day(?:one)-entr(?:y|ies)?$'}
33
+ ]
34
+ }
35
+ end
36
+
37
+ def self.template(trigger)
38
+ case trigger
39
+ when /day(?:one)-entr(?:y|ies)?$/
40
+ IO.read(File.join(File.dirname(__FILE__), '../../../templates/doing-dayone-entry.erb'))
41
+ else
42
+ IO.read(File.join(File.dirname(__FILE__), '../../../templates/doing-dayone.erb'))
43
+ end
44
+ end
45
+
46
+ def self.render(wwid, items, variables: {})
47
+
48
+ return if items.nil?
49
+
50
+ opt = variables[:options]
51
+ trigger = opt[:output]
52
+ digest = case trigger
53
+ when /-days?$/
54
+ :day
55
+ when /-entries$/
56
+ :entries
57
+ else
58
+ :digest
59
+ end
60
+
61
+ all_items = []
62
+ days = {}
63
+ flagged = false
64
+ tags = []
65
+
66
+ items.each do |i|
67
+ day_flagged = false
68
+ date_key = i.date.strftime('%Y-%m-%d')
69
+
70
+ if String.method_defined? :force_encoding
71
+ title = i.title.force_encoding('utf-8').link_urls(format: :markdown)
72
+ note = i.note.map { |line| line.force_encoding('utf-8').strip.link_urls(format: :markdown) } if i.note
73
+ else
74
+ title = i.title.link_urls(format: :markdown)
75
+ note = i.note.map { |line| line.strip.link_urls(format: :markdown) } if i.note
76
+ end
77
+
78
+ title = "#{title} @project(#{i.section})" unless variables[:is_single]
79
+
80
+ tags.concat(i.tag_array).sort!.uniq!
81
+ flagged = day_flagged = true if i.tags?(wwid.config['marker_tag'])
82
+
83
+ interval = wwid.get_interval(i, record: true) if i.title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/ && opt[:times]
84
+ interval ||= false
85
+ human_time = false
86
+ if interval
87
+ d, h, m = wwid.format_time(wwid.get_interval(i, formatted: false))
88
+ human_times = []
89
+ human_times << format('%<d>d day%<p>s', d: d, p: d == 1 ? '' : 's') if d > 0
90
+ human_times << format('%<h>d hour%<p>s', h: h, p: h == 1 ? '' : 's') if h > 0
91
+ human_times << format('%<m>d minute%<p>s', m: m, p: m == 1 ? '' : 's') if m > 0
92
+ human_time = human_times.join(', ')
93
+ end
94
+
95
+ done = i.tags?('done') ? ' ' : ' '
96
+
97
+ item = {
98
+ date_object: i.date,
99
+ date: i.date.strftime('%a %-I:%M%p'),
100
+ shortdate: i.date.relative_date,
101
+ done: done,
102
+ note: note,
103
+ section: i.section,
104
+ time: interval,
105
+ human_time: human_time,
106
+ title: title.strip,
107
+ starred: day_flagged,
108
+ tags: i.tag_array
109
+ }
110
+ all_items << item
111
+
112
+
113
+ if days.key?(date_key)
114
+ days[date_key][:starred] = true if day_flagged
115
+ days[date_key][:tags] = days[date_key][:tags].concat(i.tag_array).sort.uniq
116
+ days[date_key][:entries].push(item)
117
+ else
118
+ days[date_key] ||= { tags: [], entries: [], starred: false }
119
+ days[date_key][:starred] = true if day_flagged
120
+ days[date_key][:tags] = days[date_key][:tags].concat(i.tag_array).sort.uniq
121
+ days[date_key][:entries].push(item)
122
+ end
123
+ end
124
+
125
+
126
+ template = if wwid.config['export_templates']['dayone'] && File.exist?(File.expand_path(wwid.config['export_templates']['dayone']))
127
+ IO.read(File.expand_path(wwid.config['export_templates']['dayone']))
128
+ else
129
+ self.template('dayone')
130
+ end
131
+
132
+ totals = opt[:totals] ? wwid.tag_times(format: :markdown, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) : ''
133
+
134
+ case digest
135
+ when :day
136
+ days.each do |k, hsh|
137
+ title = "#{k}: #{variables[:page_title]}"
138
+ to_dayone(template: template,
139
+ title: title,
140
+ items: hsh[:entries],
141
+ totals: '',
142
+ date: Time.parse(k),
143
+ tags: tags,
144
+ starred: hsh[:starred])
145
+ end
146
+ when :entries
147
+ entry_template = if wwid.config['export_templates']['dayone_entry'] && File.exist?(File.expand_path(wwid.config['export_templates']['dayone_entry']))
148
+ IO.read(File.expand_path(wwid.config['export_templates']['dayone_entry']))
149
+ else
150
+ self.template('dayone-entry')
151
+ end
152
+ all_items.each do |item|
153
+ to_dayone(template: entry_template,
154
+ title: '',
155
+ items: [item],
156
+ totals: '',
157
+ date: item[:date_object],
158
+ tags: item[:tags],
159
+ starred: item[:starred])
160
+ end
161
+ else
162
+ to_dayone(template: template,
163
+ title: variables[:page_title],
164
+ items: all_items,
165
+ totals: totals,
166
+ date: Time.now,
167
+ tags: tags,
168
+ starred: flagged)
169
+ end
170
+
171
+ @out = ''
172
+ end
173
+
174
+ def self.to_dayone(template: self.template(nil), title: 'doing', items: [], totals: '', date: Time.now, tags: [], starred: false)
175
+ mdx = DayOneRenderer.new(title, items, totals)
176
+
177
+ engine = ERB.new(template)
178
+ content = engine.result(mdx.get_binding)
179
+
180
+ uuid = SecureRandom.uuid
181
+ # uuid = `uuidgen`.strip
182
+
183
+ plist = {
184
+ 'Creation Date' => date,
185
+ 'Creator' => { 'Software Agent' => 'Doing/2.0.0' },
186
+ 'Entry Text' => content,
187
+ 'Starred' => starred,
188
+ 'Tags' => tags.sort.uniq.delete_if { |t| t =~ /(done|cancell?ed|from)/ },
189
+ 'UUID' => uuid
190
+ }
191
+
192
+ container = File.expand_path('~/Library/Group Containers/')
193
+ dayone_dir = Dir.glob('*.dayoneapp2', base: container).first
194
+ import_dir = File.join(container, dayone_dir, 'Data', 'Auto Import', 'Default Journal.dayone', 'entries')
195
+ FileUtils.mkdir_p(import_dir) unless File.exist?(import_dir)
196
+ entry_file = File.join(import_dir, "#{uuid}.doentry")
197
+ Doing.logger.debug('Day One Export:', "Exporting to #{entry_file}")
198
+ File.open(entry_file, 'w') do |f|
199
+ f.puts plist.to_plist
200
+ end
201
+
202
+ Doing.logger.count(:exported, level: :info, count: items.count, message: '%count %items exported to Day One import folder')
203
+ end
204
+
205
+ Doing::Plugins.register 'dayone', :export, self
206
+ Doing::Plugins.register 'dayone-days', :export, self
207
+ Doing::Plugins.register 'dayone-entries', :export, self
208
+ end
209
+ end
@@ -23,13 +23,15 @@ 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*/, '')} " }
@@ -40,26 +42,17 @@ module Doing
40
42
  line.simple_wrap(width)
41
43
  # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
42
44
  end
43
- note = note.join("\n").split(/\n/).delete_if(&:empty?)
45
+ note = note.delete_if(&:empty?)
44
46
  end
45
47
  else
46
48
  note = []
47
49
  end
48
50
 
49
- output = opt[:template].dup
50
-
51
- output.gsub!(/%[a-z]+/) do |m|
52
- if Doing::Color.respond_to?(m.sub(/^%/, ''))
53
- Doing::Color.send(m.sub(/^%/, ''))
54
- else
55
- m
56
- end
57
- end
58
-
59
- output.sub!(/%(\d+)?date/) do
60
- pad = Regexp.last_match(1).to_i
61
- format("%#{pad}s", item.date.strftime(opt[:format]))
62
- end
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])
63
56
 
64
57
  interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
65
58
  if interval
@@ -74,7 +67,8 @@ module Doing
74
67
  end
75
68
 
76
69
  interval ||= ''
77
- output.sub!(/%interval/, interval)
70
+ # output.sub!(/%interval/, interval)
71
+ placeholders['interval'] = interval
78
72
 
79
73
  duration = item.duration if opt[:duration]
80
74
  if duration
@@ -88,78 +82,89 @@ module Doing
88
82
  end
89
83
  end
90
84
  duration ||= ''
91
- output.sub!(/%duration/, duration)
92
-
93
- output.sub!(/%(\d+)?shortdate/) do
94
- pad = Regexp.last_match(1) || 13
95
- format("%#{pad}s", item.date.relative_date)
96
- end
97
-
98
- output.sub!(/%section/, item.section) if item.section
99
-
100
- title_rx = /(?mi)%(?<width>-?\d+)?(?:(?<ichar>[ _t])(?<icount>\d+))?(?<prefix>.[ _t]?)?title(?<after>.*?)$/
101
- title_color = Doing::Color.reset + output.match(/(?mi)^(.*?)(%.*?title)/)[1].last_color
102
-
103
- title_offset = Doing::Color.uncolor(output).match(title_rx).begin(0)
104
-
105
- output.sub!(title_rx) do
106
- m = Regexp.last_match
107
-
108
- after = m['after']
109
- pad = m['width'].to_i
110
- indent = ''
111
- if m['ichar']
112
- char = m['ichar'] =~ /t/ ? "\t" : ' '
113
- indent = char * m['icount'].to_i
114
- end
115
- prefix = m['prefix']
116
- if opt[:wrap_width]&.positive? || pad.positive?
117
- width = pad.positive? ? pad : opt[:wrap_width]
118
- item.title.wrap(width, pad: pad, indent: indent, offset: title_offset, prefix: prefix, color: title_color, after: after, reset: reset)
119
- # flag + item.title.gsub(/(.{#{opt[:wrap_width]}})(?=\s+|\Z)/, "\\1\n ").sub(/\s*$/, '') + reset
120
- else
121
- format("%s%#{pad}s%s", prefix, item.title.sub(/\s*$/, ''), after)
122
- end
123
- end
124
-
125
- # output.sub!(/(?i-m)^([\s\S]*?)(%(?:[io]d|(?:\^[\s\S])?(?:(?:[ _t]|[^a-z0-9])?\d+)?(?:[\s\S][ _t]?)?)?note)([\s\S]*?)$/, '\1\3\2')
126
- if opt[:tags_color]
127
- output.highlight_tags!(opt[:tags_color])
128
- end
129
-
130
- if note.empty?
131
- output.gsub!(/%(chomp|[io]d|(\^.)?(([ _t]|[^a-z0-9])?\d+)?(.[ _t]?)?)?note/, '')
132
- else
133
- output.sub!(/%note/, "\n#{note.map { |l| "\t#{l.strip} " }.join("\n")}")
134
- output.sub!(/%idnote/, "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}")
135
- output.sub!(/%odnote/, "\n#{note.map { |l| "#{l.strip} " }.join("\n")}")
136
- output.sub!(/(?mi)%(?:\^(?<mchar>.))?(?:(?<ichar>[ _t]|[^a-z0-9])?(?<icount>\d+))?(?<prefix>.[ _t]?)?note/) do
137
- m = Regexp.last_match
138
- mark = m['mchar'] || ''
139
- indent = if m['ichar']
140
- char = m['ichar'] =~ /t/ ? "\t" : ' '
141
- char * m['icount'].to_i
142
- else
143
- ''
144
- end
145
- prefix = m['prefix'] || ''
146
- "\n#{note.map { |l| "#{mark}#{indent}#{prefix}#{l.strip} " }.join("\n")}"
147
- end
148
-
149
- output.sub!(/%chompnote/) do
150
- note.map { |l| l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ') }.join(' ')
151
- end
152
- end
153
-
154
- output.gsub!(/%hr(_under)?/) do
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
155
158
  o = ''
156
159
  `tput cols`.to_i.times do
157
160
  o += Regexp.last_match(1).nil? ? '-' : '_'
158
161
  end
159
162
  o
160
163
  end
161
- output.gsub!(/%n/, "\n")
162
- output.gsub!(/%t/, "\t")
164
+ output.gsub!(/(?<!\\)%n/, "\n")
165
+ output.gsub!(/(?<!\\)%t/, "\t")
166
+
167
+ output.gsub!(/\\%/, '%')
163
168
 
164
169
  out += "#{output}\n"
165
170
  end
data/lib/doing/prompt.rb CHANGED
@@ -118,14 +118,17 @@ module Doing
118
118
  return nil unless $stdout.isatty
119
119
 
120
120
  # fzf_args << '-1' # User is expecting a menu, and even if only one it seves as confirmation
121
- fzf_args << %(--prompt="#{prompt}")
122
- fzf_args << "--height=#{options.count + 2}"
123
- fzf_args << '--info=inline'
124
- 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
125
126
  header = "esc: cancel,#{multiple ? ' tab: multi-select, ctrl-a: select all,' : ''} return: confirm"
126
- fzf_args << %(--header="#{header}")
127
+ default_args << %(--header="#{header}")
128
+ default_args.concat(fzf_args)
127
129
  options.sort! if sorted
128
- res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{fzf_args.join(' ')}`
130
+
131
+ res = `echo #{Shellwords.escape(options.join("\n"))}|#{fzf} #{default_args.join(' ')}`
129
132
  return false if res.strip.size.zero?
130
133
 
131
134
  res
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
  ##
@@ -275,7 +294,7 @@ module Doing
275
294
 
276
295
  def normalize_case(default = :smart)
277
296
  case self
278
- when /^c/i
297
+ when /^(c|sens)/i
279
298
  :sensitive
280
299
  when /^i/i
281
300
  :ignore
@@ -310,6 +329,28 @@ module Doing
310
329
  end
311
330
  end
312
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
+
313
354
  def normalize_trigger!
314
355
  replace normalize_trigger
315
356
  end
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