doing 2.1.14 → 2.1.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. checksums.yaml +4 -4
  2. data/.irbrc +1 -0
  3. data/.yardoc/checksums +14 -12
  4. data/.yardoc/object_types +0 -0
  5. data/.yardoc/objects/root.dat +0 -0
  6. data/CHANGELOG.md +66 -0
  7. data/Gemfile.lock +3 -2
  8. data/README.md +56 -19
  9. data/bin/doing +134 -47
  10. data/docs/doc/Array.html +117 -3
  11. data/docs/doc/BooleanTermParser/Clause.html +1 -1
  12. data/docs/doc/BooleanTermParser/Operator.html +1 -1
  13. data/docs/doc/BooleanTermParser/Query.html +1 -1
  14. data/docs/doc/BooleanTermParser/QueryParser.html +1 -1
  15. data/docs/doc/BooleanTermParser/QueryTransformer.html +1 -1
  16. data/docs/doc/BooleanTermParser.html +1 -1
  17. data/docs/doc/Doing/Color.html +6 -2
  18. data/docs/doc/Doing/Completion.html +1 -1
  19. data/docs/doc/Doing/Configuration.html +8 -4
  20. data/docs/doc/Doing/Errors/DoingNoTraceError.html +1 -1
  21. data/docs/doc/Doing/Errors/DoingRuntimeError.html +1 -1
  22. data/docs/doc/Doing/Errors/DoingStandardError.html +1 -1
  23. data/docs/doc/Doing/Errors/EmptyInput.html +1 -1
  24. data/docs/doc/Doing/Errors/NoResults.html +1 -1
  25. data/docs/doc/Doing/Errors/PluginException.html +1 -1
  26. data/docs/doc/Doing/Errors/UserCancelled.html +1 -1
  27. data/docs/doc/Doing/Errors/WrongCommand.html +1 -1
  28. data/docs/doc/Doing/Errors.html +1 -1
  29. data/docs/doc/Doing/Hooks.html +1 -1
  30. data/docs/doc/Doing/Item.html +224 -2
  31. data/docs/doc/Doing/Items.html +2 -2
  32. data/docs/doc/Doing/LogAdapter.html +1 -1
  33. data/docs/doc/Doing/Note.html +2 -2
  34. data/docs/doc/Doing/Pager.html +1 -1
  35. data/docs/doc/Doing/Plugins.html +1 -1
  36. data/docs/doc/Doing/Prompt.html +69 -1
  37. data/docs/doc/Doing/Section.html +1 -1
  38. data/docs/doc/Doing/TemplateString.html +2 -2
  39. data/docs/doc/Doing/Util/Backup.html +1 -1
  40. data/docs/doc/Doing/Util.html +1 -1
  41. data/docs/doc/Doing/WWID.html +71 -67
  42. data/docs/doc/Doing.html +3 -3
  43. data/docs/doc/GLI/Commands/MarkdownDocumentListener.html +1 -1
  44. data/docs/doc/GLI/Commands.html +1 -1
  45. data/docs/doc/GLI.html +1 -1
  46. data/docs/doc/Hash.html +1 -1
  47. data/docs/doc/Numeric.html +279 -0
  48. data/docs/doc/PhraseParser/Operator.html +1 -1
  49. data/docs/doc/PhraseParser/PhraseClause.html +1 -1
  50. data/docs/doc/PhraseParser/Query.html +1 -1
  51. data/docs/doc/PhraseParser/QueryParser.html +1 -1
  52. data/docs/doc/PhraseParser/QueryTransformer.html +1 -1
  53. data/docs/doc/PhraseParser/TermClause.html +1 -1
  54. data/docs/doc/PhraseParser.html +1 -1
  55. data/docs/doc/Status.html +1 -1
  56. data/docs/doc/String.html +997 -118
  57. data/docs/doc/Symbol.html +1 -1
  58. data/docs/doc/Time.html +1 -1
  59. data/docs/doc/_index.html +14 -9
  60. data/docs/doc/class_list.html +1 -1
  61. data/docs/doc/file.README.html +41 -15
  62. data/docs/doc/index.html +41 -15
  63. data/docs/doc/method_list.html +448 -312
  64. data/docs/doc/top-level-namespace.html +2 -2
  65. data/docs/index.md +56 -19
  66. data/doing.gemspec +1 -0
  67. data/doing.rdoc +36 -6
  68. data/example_plugin.rb +2 -4
  69. data/lib/completion/_doing.zsh +8 -8
  70. data/lib/completion/doing.bash +12 -12
  71. data/lib/completion/doing.fish +8 -3
  72. data/lib/doing/array_chronify.rb +57 -0
  73. data/lib/doing/colors.rb +4 -0
  74. data/lib/doing/configuration.rb +6 -2
  75. data/lib/doing/item.rb +83 -0
  76. data/lib/doing/log_adapter.rb +3 -3
  77. data/lib/doing/numeric_chronify.rb +40 -0
  78. data/lib/doing/plugins/export/dayone_export.rb +1 -1
  79. data/lib/doing/plugins/export/json_export.rb +2 -2
  80. data/lib/doing/plugins/export/template_export.rb +49 -90
  81. data/lib/doing/prompt.rb +52 -0
  82. data/lib/doing/string.rb +137 -33
  83. data/lib/doing/string_chronify.rb +112 -14
  84. data/lib/doing/template_string.rb +1 -1
  85. data/lib/doing/time.rb +4 -4
  86. data/lib/doing/util_backup.rb +1 -1
  87. data/lib/doing/version.rb +1 -1
  88. data/lib/doing/wwid.rb +107 -101
  89. data/lib/doing.rb +35 -31
  90. data/lib/examples/plugins/say_export.rb +1 -4
  91. metadata +26 -2
data/lib/doing/item.rb CHANGED
@@ -9,6 +9,8 @@ module Doing
9
9
 
10
10
  # attr_reader :id
11
11
 
12
+ include Color
13
+
12
14
  ##
13
15
  ## Initialize an item with date, title, section, and
14
16
  ## optional note
@@ -57,6 +59,25 @@ module Doing
57
59
  @end_date ||= Time.parse(Regexp.last_match(1)) if @title =~ /@done\((\d{4}-\d\d-\d\d \d\d:\d\d.*?)\)/
58
60
  end
59
61
 
62
+ def calculate_end_date(opt)
63
+ if opt[:took]
64
+ if @date + opt[:took] > Time.now
65
+ @date = Time.now - opt[:took]
66
+ Time.now
67
+ else
68
+ @date + opt[:took]
69
+ end
70
+ elsif opt[:back]
71
+ if opt[:back].is_a? Integer
72
+ @date + opt[:back]
73
+ else
74
+ @date + (opt[:back] - @date)
75
+ end
76
+ else
77
+ Time.now
78
+ end
79
+ end
80
+
60
81
  # Generate a hash that represents the entry
61
82
  #
62
83
  # @return [String] entry hash
@@ -112,6 +133,19 @@ module Doing
112
133
  (start_a >= start_b && start_a <= end_b) || (end_a >= start_b && end_a <= end_b) || (start_a < start_b && end_a > end_b)
113
134
  end
114
135
 
136
+ ##
137
+ ## Updates the title of the Item by expanding natural
138
+ ## language dates within configured date tags (tags
139
+ ## whose value is expected to be a date)
140
+ ##
141
+ ## @param additional_tags An array of additional
142
+ ## tag names to consider
143
+ ## dates
144
+ ##
145
+ def expand_date_tags(additional_tags = nil)
146
+ @title.expand_date_tags(additional_tags)
147
+ end
148
+
115
149
  ##
116
150
  ## Add (or remove) tags from the title of the item
117
151
  ##
@@ -237,6 +271,34 @@ module Doing
237
271
  (case_type == :smart && search !~ /[A-Z]/) || case_type == :ignore
238
272
  end
239
273
 
274
+ def highlight_search(search, distance: nil, negate: false, case_type: nil)
275
+ prefs = Doing.config.settings['search'] || {}
276
+ matching = prefs.fetch('matching', 'pattern').normalize_matching
277
+ distance ||= prefs.fetch('distance', 3).to_i
278
+ case_type ||= prefs.fetch('case', 'smart').normalize_case
279
+ new_note = Note.new
280
+
281
+ if search.is_rx? || matching == :fuzzy
282
+ rx = search.to_rx(distance: distance, case_type: case_type)
283
+ new_title = @title.gsub(rx) { |m| yellow(m) }
284
+ new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
285
+ else
286
+ query = to_phrase_query(search.strip)
287
+
288
+ if query[:must].nil? && query[:must_not].nil?
289
+ query[:must] = query[:should]
290
+ query[:should] = []
291
+ end
292
+ query[:must].concat(query[:should]).each do |s|
293
+ rx = Regexp.new(s.wildcard_to_rx, ignore_case(s, case_type))
294
+ new_title = @title.gsub(rx) { |m| yellow(m) }
295
+ new_note.add(@note.to_s.gsub(rx) { |m| yellow(m) })
296
+ end
297
+ end
298
+
299
+ Item.new(@date, new_title, @section, new_note)
300
+ end
301
+
240
302
  ##
241
303
  ## Test if item matches search string
242
304
  ##
@@ -340,6 +402,27 @@ module Doing
340
402
  "\t- #{@date.strftime('%Y-%m-%d %H:%M')} | #{@title}#{@note.empty? ? '' : "\n#{@note}"}"
341
403
  end
342
404
 
405
+ ##
406
+ ## outputs a colored string with relative date and highlighted tags
407
+ ##
408
+ ## @return Pretty representation of the object.
409
+ ##
410
+ def to_pretty(elements: %i[date title section])
411
+ output = []
412
+ elements.each do |e|
413
+ case e
414
+ when :date
415
+ output << format('%13s |', @date.relative_date).cyan
416
+ when :section
417
+ output << "#{magenta}(#{white(@section)}#{magenta})"
418
+ when :title
419
+ output << @title.white.highlight_tags('cyan')
420
+ end
421
+ end
422
+
423
+ output.join(' ')
424
+ end
425
+
343
426
  # @private
344
427
  def inspect
345
428
  # %(<Doing::Item @date=#{@date} @title="#{@title}" @section:"#{@section}" @note:#{@note.to_s}>)
@@ -52,7 +52,7 @@ module Doing
52
52
  COUNT_KEYS.each { |key| @counters[key] = { tag: [], count: 0 } }
53
53
  @results = []
54
54
  @logdev = $stderr
55
- @max_length = `tput cols`.strip.to_i - 5 || 85
55
+ @max_length = TTY::Screen.columns - 5 || 85
56
56
  self.log_level = level
57
57
  @prev_level = level
58
58
  end
@@ -398,9 +398,9 @@ module Doing
398
398
  return topic.ljust(TOPIC_WIDTH) if topic && message.strip.empty?
399
399
 
400
400
  topic = formatted_topic(topic, colon: block_given?)
401
- message.truncmiddle!(@max_length - TOPIC_WIDTH - 5)
401
+ # message.truncmiddle!(@max_length - TOPIC_WIDTH - 5)
402
402
  out = topic + message
403
- out.truncate!(@max_length) if @max_length.positive?
403
+ # out.truncate!(@max_length) if @max_length.positive?
404
404
  messages << out
405
405
  out
406
406
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Doing
4
+ ##
5
+ ## Number helpers
6
+ ##
7
+ class ::Numeric
8
+ ##
9
+ ## Format human readable time from seconds
10
+ ##
11
+ ## @param seconds [Integer] Seconds
12
+ ##
13
+ def format_time(human: false)
14
+ return [0, 0, 0] if nil?
15
+
16
+ seconds = dup.to_i
17
+ minutes = (seconds / 60).to_i
18
+ hours = (minutes / 60).to_i
19
+ if human
20
+ minutes = (minutes % 60).to_i
21
+ [0, hours, minutes]
22
+ else
23
+ days = (hours / 24).to_i
24
+ hours = (hours % 24).to_i
25
+ minutes = (minutes % 60).to_i
26
+ [days, hours, minutes]
27
+ end
28
+ end
29
+
30
+ ##
31
+ ## Format seconds as natural language time string
32
+ ##
33
+ ## @param format [Symbol] The format to output
34
+ ## (:dhm, :hm, :m, :clock, :natural)
35
+ ##
36
+ def time_string(format: :dhm)
37
+ format_time(human: true).time_string(format: format)
38
+ end
39
+ end
40
+ end
@@ -84,7 +84,7 @@ module Doing
84
84
  interval ||= false
85
85
  human_time = false
86
86
  if interval
87
- d, h, m = wwid.format_time(wwid.get_interval(i, formatted: false))
87
+ d, h, m = wwid.get_interval(i, formatted: false).format_time
88
88
  human_times = []
89
89
  human_times << format('%<d>d day%<p>s', d: d, p: d == 1 ? '' : 's') if d > 0
90
90
  human_times << format('%<h>d hour%<p>s', h: h, p: h == 1 ? '' : 's') if h > 0
@@ -56,7 +56,7 @@ module Doing
56
56
  end_date: end_date,
57
57
  title: title.strip, #+ " #{note}"
58
58
  note: note.instance_of?(Array) ? note.to_s : note,
59
- time: '%02d:%02d:%02d' % wwid.format_time(interval),
59
+ time: interval.time_string(format: :clock),
60
60
  tags: tags
61
61
  }
62
62
 
@@ -68,7 +68,7 @@ module Doing
68
68
  new_item = {
69
69
  'id' => index + 1,
70
70
  'content' => title.strip, #+ " #{note}"
71
- 'title' => title.strip + " (#{'%02d:%02d:%02d' % wwid.format_time(interval)})",
71
+ 'title' => title.strip + " (#{interval.time_string(format: :clock)})",
72
72
  'start' => i.date.strftime('%F %T'),
73
73
  'type' => 'box',
74
74
  'style' => 'color:#4c566b;background-color:#d8dee9;'
@@ -5,6 +5,7 @@
5
5
  # author: Brett Terpstra
6
6
  # url: https://brettterpstra.com
7
7
  module Doing
8
+ # Template Export
8
9
  class TemplateExport
9
10
  include Doing::Color
10
11
  include Doing::Util
@@ -32,7 +33,7 @@ module Doing
32
33
 
33
34
  placeholders = {}
34
35
 
35
- if (!item.note.empty?) && wwid.config['include_notes']
36
+ if !item.note.empty? && wwid.config['include_notes']
36
37
  note = item.note.map(&:strip).delete_if(&:empty?)
37
38
  note.map! { |line| "#{line.sub(/^\t*/, '')} " }
38
39
 
@@ -42,122 +43,74 @@ module Doing
42
43
  line.simple_wrap(width)
43
44
  # line.chomp.gsub(/(.{1,#{width}})(\s+|\Z)/, "\\1\n")
44
45
  end
45
- note = note.delete_if(&:empty?)
46
+ note.delete_if(&:empty?)
46
47
  end
47
48
  else
48
49
  note = []
49
50
  end
50
51
 
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
52
  placeholders['date'] = item.date.strftime(opt[:format])
56
53
 
57
54
  interval = wwid.get_interval(item, record: true, formatted: false) if opt[:times]
58
55
  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)
63
- else
64
- d, h, m = wwid.format_time(interval)
65
- interval = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
66
- end
56
+ interval = case opt[:interval_format].to_sym
57
+ when :human
58
+ interval.time_string(format: :hm)
59
+ else
60
+ interval.time_string(format: :clock)
61
+ end
67
62
  end
68
63
 
69
64
  interval ||= ''
70
- # output.sub!(/%interval/, interval)
71
65
  placeholders['interval'] = interval
72
66
 
73
67
  duration = item.duration if opt[:duration]
74
68
  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)
79
- else
80
- d, h, m = wwid.format_time(duration)
81
- duration = format('%<d>02d:%<h>02d:%<m>02d', d: d, h: h, m: m)
82
- end
69
+ duration = case opt[:interval_format].to_sym
70
+ when :human
71
+ duration.time_string(format: :hm)
72
+ else
73
+ duration.time_string(format: :clock)
74
+ end
83
75
  end
84
76
  duration ||= ''
85
- # output.sub!(/%duration/, duration)
86
77
  placeholders['duration'] = duration
87
78
 
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
79
+ placeholders['shortdate'] = format('%13s', item.date.relative_date)
94
80
  placeholders['section'] = item.section || ''
95
81
  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
82
  placeholders['note'] = note
125
83
  placeholders['idnote'] = note.empty? ? '' : "\n#{note.map { |l| "\t\t#{l.strip} " }.join("\n")}"
126
84
  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
85
 
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
86
+ chompnote = []
87
+ unless note.empty?
88
+ chompnote = note.map do |l|
89
+ l.gsub(/\n+/, ' ').gsub(/(^\s*|\s*$)/, '').gsub(/\s+/, ' ')
90
+ end
91
+ end
92
+ placeholders['chompnote'] = chompnote.join(' ')
156
93
 
157
- output.gsub!(/(?<!\\)%hr(_under)?/) do
94
+ template = opt[:template].dup
95
+ note_rx = /(?i-m)(?x:^([\s\S]*?)
96
+ (%(?:[io]d|(?:\^[\s\S])?
97
+ (?:(?:[ _t]|[^a-z0-9])?\d+)?
98
+ (?:[\s\S][ _t]?)?)?note)
99
+ ([\s\S]*?)$)/
100
+ template.sub!(note_rx, '\1\3\2')
101
+ output = Doing::TemplateString.new(template,
102
+ color: flag,
103
+ placeholders: placeholders,
104
+ reset: reset,
105
+ tags_color: opt[:tags_color],
106
+ wrap_width: opt[:wrap_width]).colored
107
+
108
+ output.gsub!(/(?<!\\)%(\S)?hr(_under)?/) do
158
109
  o = ''
159
- `tput cols`.to_i.times do
160
- o += Regexp.last_match(1).nil? ? '-' : '_'
110
+ TTY::Screen.columns.to_i.times do
111
+ char = Regexp.last_match(2).nil? ? '-' : '_'
112
+ char = Regexp.last_match(1).nil? ? char : Regexp.last_match(1)
113
+ o += char
161
114
  end
162
115
  o
163
116
  end
@@ -166,11 +119,17 @@ module Doing
166
119
 
167
120
  output.gsub!(/\\%/, '%')
168
121
 
122
+ output.highlight_search!(opt[:search]) if opt[:search] && !opt[:not] && opt[:hilite]
123
+
169
124
  out += "#{output}\n"
170
125
  end
171
126
 
172
127
  # Doing.logger.debug('Template Export:', "#{items.count} items output to template #{opt[:template]}")
173
- out += wwid.tag_times(format: wwid.config['timer_format'].to_sym, sort_by_name: opt[:sort_tags], sort_order: opt[:tag_order]) if opt[:totals]
128
+ if opt[:totals]
129
+ out += wwid.tag_times(format: wwid.config['timer_format'].to_sym,
130
+ sort_by_name: opt[:sort_tags],
131
+ sort_order: opt[:tag_order])
132
+ end
174
133
  out
175
134
  end
176
135
 
data/lib/doing/prompt.rb CHANGED
@@ -17,13 +17,63 @@ module Doing
17
17
  end
18
18
 
19
19
  def enter_text(prompt, default_response: '')
20
+ $stdin.reopen('/dev/tty')
20
21
  return default_response if @default_answer
21
22
 
22
23
  print "#{yellow(prompt).sub(/:?$/, ':')} #{reset}"
23
24
  $stdin.gets.strip
24
25
  end
25
26
 
27
+ def read_line(prompt: 'Enter text', completions: [], default_response: '')
28
+ $stdin.reopen('/dev/tty')
29
+ return default_response if @default_answer
30
+
31
+ unless completions.empty?
32
+ completions.sort!
33
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
34
+ Readline.completion_append_character = ' '
35
+ Readline.completion_proc = comp
36
+ end
37
+
38
+ begin
39
+ Readline.readline("#{yellow(prompt).sub(/:?$/, ':')} #{reset}", true).strip
40
+ rescue Interrupt
41
+ raise UserCancelled
42
+ end
43
+ end
44
+
45
+ def read_lines(prompt: 'Enter text', completions: [])
46
+ $stdin.reopen('/dev/tty')
47
+ return default_response if @default_answer
48
+
49
+ completions.sort!
50
+ comp = proc { |s| completions.grep(/^#{Regexp.escape(s)}/) }
51
+ Readline.completion_append_character = ' '
52
+ Readline.completion_proc = comp
53
+ prompt_text = []
54
+ prompt_text << boldgreen(prompt.sub(/:?$/, ':'))
55
+ prompt_text << yellow(' Enter a blank line (')
56
+ prompt_text << boldwhite('return twice')
57
+ prompt_text << yellow(') to end editing')
58
+ puts prompt_text.join('')
59
+
60
+ res = []
61
+
62
+ begin
63
+ while (line = Readline.readline('> ', true))
64
+ break if line.strip.empty?
65
+
66
+ res << line.chomp
67
+ end
68
+ rescue Interrupt
69
+ raise UserCancelled
70
+ end
71
+
72
+ res.join("\n").strip
73
+ end
74
+
26
75
  def request_lines(prompt: 'Enter text')
76
+ $stdin.reopen('/dev/tty')
27
77
  ask_note = []
28
78
  reader = TTY::Reader.new(interrupt: -> { raise Errors::UserCancelled }, track_history: false)
29
79
  puts "#{boldgreen(prompt.sub(/:?$/, ':'))} #{yellow('Hit return for a new line, ')}#{boldwhite('enter a blank line (')}#{boldyellow('return twice')}#{boldwhite(') to end editing')}"
@@ -51,6 +101,8 @@ module Doing
51
101
  return @force_answer
52
102
  end
53
103
 
104
+ $stdin.reopen('/dev/tty')
105
+
54
106
  default = if default_response.is_a?(String)
55
107
  default_response =~ /y/i ? true : false
56
108
  else