natty-ui 0.8.0 → 0.9.1

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -2
  3. data/examples/24bit-colors.rb +11 -7
  4. data/examples/3bit-colors.rb +3 -2
  5. data/examples/8bit-colors.rb +2 -2
  6. data/examples/animate.rb +24 -0
  7. data/examples/attributes.rb +20 -65
  8. data/examples/demo.rb +39 -203
  9. data/examples/illustration.rb +27 -23
  10. data/examples/{list_in_columns.rb → ls.rb} +11 -11
  11. data/examples/message.rb +30 -0
  12. data/examples/progress.rb +2 -2
  13. data/examples/query.rb +6 -2
  14. data/examples/read_key.rb +13 -0
  15. data/examples/table.rb +26 -8
  16. data/lib/natty-ui/ansi.rb +364 -328
  17. data/lib/natty-ui/ansi_constants.rb +73 -0
  18. data/lib/natty-ui/ansi_wrapper.rb +68 -23
  19. data/lib/natty-ui/key_map.rb +119 -0
  20. data/lib/natty-ui/line_animation/default.rb +35 -0
  21. data/lib/natty-ui/line_animation/matrix.rb +28 -0
  22. data/lib/natty-ui/line_animation/rainbow.rb +30 -0
  23. data/lib/natty-ui/line_animation/test.rb +29 -0
  24. data/lib/natty-ui/line_animation/type_writer.rb +64 -0
  25. data/lib/natty-ui/line_animation.rb +54 -0
  26. data/lib/natty-ui/version.rb +1 -1
  27. data/lib/natty-ui/wrapper/animate.rb +17 -0
  28. data/lib/natty-ui/wrapper/ask.rb +2 -8
  29. data/lib/natty-ui/wrapper/framed.rb +26 -21
  30. data/lib/natty-ui/wrapper/heading.rb +1 -1
  31. data/lib/natty-ui/wrapper/horizontal_rule.rb +3 -3
  32. data/lib/natty-ui/wrapper/list_in_columns.rb +7 -4
  33. data/lib/natty-ui/wrapper/message.rb +4 -4
  34. data/lib/natty-ui/wrapper/progress.rb +33 -8
  35. data/lib/natty-ui/wrapper/query.rb +9 -13
  36. data/lib/natty-ui/wrapper/request.rb +2 -2
  37. data/lib/natty-ui/wrapper/section.rb +25 -11
  38. data/lib/natty-ui/wrapper/table.rb +11 -11
  39. data/lib/natty-ui/wrapper.rb +19 -14
  40. data/lib/natty-ui.rb +64 -32
  41. metadata +16 -5
  42. data/examples/illustration.png +0 -0
@@ -11,8 +11,7 @@ module NattyUI
11
11
  # {Wrapper::Element#close}.
12
12
  #
13
13
  # @param [Array<#to_s>] args more objects to print
14
- # @param [Symbol] type frame type;
15
- # valid types are `:rounded`, `:simple`, `:heavy`, `:semi`, `:double`
14
+ # @param [:block, :double, :heavy, :rounded, :semi, :simple] type frame type
16
15
  # @yieldparam [Wrapper::Framed] framed the created section
17
16
  # @return [Object] the result of the code block
18
17
  # @return [Wrapper::Framed] itself, when no code block is given
@@ -30,30 +29,36 @@ module NattyUI
30
29
  protected
31
30
 
32
31
  def initialize(parent, type:)
33
- @type = FRAME_PARTS[type] or
34
- raise(ArgumentError, "invalid frame type - #{type.inspect}")
35
- parent.puts(
36
- color("#{@type[0]}#{@type[1] * (parent.available_width - 2)}")
37
- )
38
- super(parent, prefix: "#{color(@type[4])} ", prefix_width: 2)
39
- @suffix = ' '
40
- @suffix_width = 1
32
+ deco = as_deco(type)
33
+ super(parent, prefix: "#{deco[0]} ", prefix_width: 2, suffix_width: 2)
34
+ init(deco)
41
35
  end
42
36
 
43
- def finish
44
- @parent.puts(
45
- color("#{@type[3]}#{@type[1] * (@parent.available_width - 2)}")
46
- )
37
+ def as_deco(type)
38
+ if type.is_a?(Symbol)
39
+ ret = DECO[type] and return ret
40
+ elsif type.is_a?(String)
41
+ return type if type.size == 8
42
+ return type * 8 if type.size == 1
43
+ end
44
+ raise(ArgumentError, "invalid frame type - #{type.inspect}")
47
45
  end
48
46
 
49
- def color(str) = str
47
+ def init(deco)
48
+ aw = @parent.available_width - 1
49
+ parent.puts("#{deco[1]}#{deco[2] * aw}")
50
+ @finish = "#{deco[5]}#{deco[6] * aw}"
51
+ end
52
+
53
+ def finish = @parent.puts(@finish)
50
54
 
51
- FRAME_PARTS = {
52
- rounded: '╭─╮╰│╯',
53
- simple: '┌─┐└│┘',
54
- heavy: '┏━┓┗┃┛',
55
- double: '╔═╗╚║╝',
56
- semi: '╒═╕╘│╛'
55
+ DECO = {
56
+ rounded: '│╭─╮│╰─╯',
57
+ simple: '│┌─┐│└─┘',
58
+ heavy: '┃┏━┓┃┗━┛',
59
+ double: '║╔═╗║╚═╝',
60
+ semi: '│╒═╕│╘═╛',
61
+ block: '▌▛▀▜▐▙▄▟'
57
62
  }.compare_by_identity.freeze
58
63
  end
59
64
  end
@@ -51,7 +51,7 @@ module NattyUI
51
51
  @parent.puts(
52
52
  title,
53
53
  prefix: "#{Ansi[39]}#{enclose} #{Ansi[:bold, 255]}",
54
- suffix: " #{Ansi[:normal, 39]}#{enclose}#{Ansi::RESET}",
54
+ suffix: " #{Ansi[:bold_off, 39]}#{enclose}#{Ansi::RESET}",
55
55
  max_width: available_width - 2 - (enclose.size * 2)
56
56
  )
57
57
  end
@@ -8,7 +8,7 @@ module NattyUI
8
8
  #
9
9
  # @param [#to_s] symbol string to build the horizontal rule
10
10
  # @return [Wrapper::Section, Wrapper] it's parent object
11
- def hr(symbol = '=') = _element(:HorizontalRule, symbol)
11
+ def hr(symbol = '') = _element(:HorizontalRule, symbol)
12
12
  end
13
13
 
14
14
  class Wrapper
@@ -23,8 +23,8 @@ module NattyUI
23
23
  size = NattyUI.display_width(symbol = symbol.to_s)
24
24
  return @parent.puts if size == 0
25
25
  max_width = available_width
26
- @parent.puts(
27
- symbol * ((max_width / size) - 1),
26
+ @parent.print(
27
+ symbol * ((max_width / size)),
28
28
  max_width: max_width,
29
29
  prefix: Ansi[39],
30
30
  prefix_width: 0,
@@ -25,7 +25,7 @@ module NattyUI
25
25
  #
26
26
  # @example ordered list, start at 100
27
27
  # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: 100)
28
- # # => 1 apple 2 banana 3 blueberry 4 pineapple 5 strawberry
28
+ # # => 100 apple 101 banana 102 blueberry 103 pineapple 104 strawberry
29
29
  #
30
30
  # @example ordered list using, uppercase characters
31
31
  # ui.ls('apple', 'banana', 'blueberry', 'pineapple', 'strawberry', glyph: :A)
@@ -84,7 +84,11 @@ module NattyUI
84
84
  ->(s) { "#{(glyph += 1).to_s.rjust(pad)} #{NattyUI.embellish(s)}" }
85
85
  when Symbol
86
86
  lambda do |s|
87
- "#{t = glyph; glyph = glyph.succ; t} #{NattyUI.embellish(s)}"
87
+ "#{
88
+ t = glyph
89
+ glyph = glyph.succ
90
+ t
91
+ } #{NattyUI.embellish(s)}"
88
92
  end
89
93
  else
90
94
  ->(s) { "#{glyph} #{NattyUI.embellish(s)}" }
@@ -122,8 +126,7 @@ module NattyUI
122
126
  end
123
127
 
124
128
  def fill(ary, size)
125
- diff = size - ary.size
126
- ary.fill(nil, ary.size, diff) if diff.positive?
129
+ (diff = size - ary.size).positive? && ary.fill(nil, ary.size, diff)
127
130
  end
128
131
 
129
132
  Item =
@@ -96,13 +96,13 @@ module NattyUI
96
96
  def initialize(parent, title:, glyph:)
97
97
  glyph = parent.wrapper.glyph(glyph) || glyph
98
98
  prefix_width = NattyUI.display_width(glyph) + 1
99
- parent.puts(
100
- title,
101
- prefix: "#{glyph} ",
99
+ super(
100
+ parent,
101
+ prefix: ' ' * prefix_width,
102
102
  prefix_width: prefix_width,
103
103
  suffix_width: 0
104
104
  )
105
- super(parent, prefix: ' ' * prefix_width, prefix_width: prefix_width)
105
+ parent.puts(title, prefix: "#{glyph} ", prefix_width: prefix_width)
106
106
  end
107
107
  end
108
108
  end
@@ -7,14 +7,23 @@ module NattyUI
7
7
  module Features
8
8
  # Creates progress element implementing additional {ProgressAttributes}.
9
9
  #
10
- # A progress element has additional states and can be closed with {#completed}
11
- # or {#failed}.
10
+ # When a `max_value` is given, the progress will by displayed as a bar.
11
+ # Otherwise the `spinner` is used for a little animation.
12
+ #
13
+ # When no pre-defined spinner is specified then spinner will be
14
+ # used char-wise as a string for the progress animation.
15
+ #
16
+ # A progress element has additional states and can be closed with
17
+ # {#completed} or {#failed}.
12
18
  #
13
19
  # @param [#to_s] title object to print as progress title
14
- # @param [##to_f] max_value maximum value of the progress
20
+ # @param [#to_f] max_value maximum value of the progress
21
+ # @param [:bar, :blink, :blocks, :braile, :circle, :colors, :pulse,
22
+ # :snake, :swap, :triangles, :vintage, #to_s] spinner type of spinner or
23
+ # spinner elements
15
24
  # @return [Wrapper::Progress] the created progress element
16
- def progress(title, max_value: nil)
17
- _element(:Progress, title, max_value)
25
+ def progress(title, max_value: nil, spinner: :pulse)
26
+ _element(:Progress, title, max_value, spinner)
18
27
  end
19
28
  end
20
29
 
@@ -29,15 +38,31 @@ module NattyUI
29
38
 
30
39
  protected
31
40
 
32
- def call(title, max_value)
41
+ def call(title, max_value, spinner)
33
42
  @final_text = [title]
34
43
  @max_value = [0, max_value.to_f].max if max_value
35
44
  @value = @progress = 0
36
- draw(title)
45
+ draw(title, SPINNER[spinner] || spinner.to_s)
37
46
  self
38
47
  end
39
48
 
40
- def draw(title)
49
+ SPINNER = {
50
+ bar: '▁▂▃▄▅▆▇█▇▆▅▄▃▂',
51
+ blink: '■□▪▫',
52
+ blocks: '▖▘▝▗',
53
+ braile: '⣷⣯⣟⡿⢿⣻⣽⣾',
54
+ braile_reverse: '⡿⣟⣯⣷⣾⣽⣻⢿',
55
+ circle: '◐◓◑◒',
56
+ colors: '🟨🟧🟥🟦🟪🟩',
57
+ pulse: '•✺◉●◉✺',
58
+ snake: '⠁⠉⠙⠸⢰⣠⣄⡆⠇⠃',
59
+ swap: '㊂㊀㊁',
60
+ triangles: '◢◣◤◥',
61
+ vintage: '-\\|/'
62
+ }.compare_by_identity.freeze
63
+ private_constant :SPINNER
64
+
65
+ def draw(title, _spinner)
41
66
  (wrapper.stream << @parent.prefix << "➔ #{title} ").flush
42
67
  end
43
68
 
@@ -63,28 +63,24 @@ module NattyUI
63
63
  end
64
64
 
65
65
  def as_choices(choices, kw_choices)
66
- ret = {}
67
- choices.each_with_index do |title, i|
68
- (i += 1) == 10 ? break : ret[i.to_s] = title.to_s.tr("\r\n\t", ' ')
69
- end
70
- ret.merge!(
71
- kw_choices
72
- .transform_keys! { [' ', _1.to_s[0]].max }
73
- .transform_values! { _1.to_s.tr("\r\n\t", ' ') }
74
- )
66
+ choices
67
+ .take(9)
68
+ .each_with_index
69
+ .to_h { |str, i| [i + 1, str] }
70
+ .merge!(kw_choices)
71
+ .transform_keys!(&:to_s)
72
+ .transform_values! { _1.to_s.gsub(/[[:space:]]/, ' ') }
75
73
  end
76
74
 
77
75
  def read(choices, result)
78
76
  while true
79
- char = NattyUI.in_stream.getch
80
- return if "\3\4".include?(char)
77
+ char = NattyUI.read_key
78
+ return if char == 'Ctrl+C'
81
79
  next unless choices.key?(char)
82
80
  return char if result == :char
83
81
  return choices[char] if result == :title
84
82
  return char, choices[char]
85
83
  end
86
- rescue Interrupt, SystemCallError
87
- nil
88
84
  end
89
85
 
90
86
  CHOICE_MARK = Ansi[:bold, 34]
@@ -46,8 +46,8 @@ module NattyUI
46
46
  (wrapper.stream << ANSI_PREFIX).flush if wrapper.ansi?
47
47
  end
48
48
 
49
- ANSI_PREFIX = Ansi::RESET + Ansi[:italic]
50
- ANSI_FINISH = Ansi::RESET + Ansi::CURSOR_UP + Ansi::LINE_ERASE
49
+ ANSI_PREFIX = Ansi::RESET + Ansi::ITALIC
50
+ ANSI_FINISH = Ansi::RESET + Ansi::LINE_PREVIOUS + Ansi::LINE_ERASE
51
51
  private_constant :ANSI_PREFIX, :ANSI_FINISH
52
52
  end
53
53
  end
@@ -8,10 +8,14 @@ module NattyUI
8
8
  # into the section.
9
9
  #
10
10
  # @param [Array<#to_s>] args optional objects to print
11
+ # @param [String] prefix used for each printed line
12
+ # @param [String] suffix used for each printed line
11
13
  # @yieldparam [Wrapper::Section] section the created section
12
14
  # @return [Object] the result of the code block
13
15
  # @return [Wrapper::Section] itself, when no code block is given
14
- def section(*args, &block) = _section(:Section, args, prefix: ' ', &block)
16
+ def section(*args, prefix: ' ', suffix: ' ', &block)
17
+ _section(:Section, args, prefix: prefix, suffix: suffix, &block)
18
+ end
15
19
  alias sec section
16
20
  end
17
21
 
@@ -38,10 +42,12 @@ module NattyUI
38
42
  return self if @status
39
43
  @parent.puts(
40
44
  *args,
41
- prefix: "#{@prefix}#{kwargs[:prefix]}",
42
- prefix_width: @prefix_width + kwargs[:prefix_width].to_i,
43
- suffix: "#{kwargs[:suffix]}#{@suffix}",
44
- suffix_width: @suffix_width + kwargs[:suffix_width].to_i
45
+ **kwargs.merge!(
46
+ prefix: "#{@prefix}#{kwargs[:prefix]}",
47
+ prefix_width: @prefix_width + kwargs[:prefix_width].to_i,
48
+ suffix: "#{kwargs[:suffix]}#{@suffix}",
49
+ suffix_width: @suffix_width + kwargs[:suffix_width].to_i
50
+ )
45
51
  )
46
52
  self
47
53
  end
@@ -55,10 +61,12 @@ module NattyUI
55
61
  return self if @status
56
62
  @parent.print(
57
63
  *args,
58
- prefix: "#{@prefix}#{kwargs[:prefix]}",
59
- prefix_width: @prefix_width + kwargs[:prefix_width].to_i,
60
- suffix: "#{kwargs[:suffix]}#{@suffix}",
61
- suffix_width: @suffix_width + kwargs[:suffix_width].to_i
64
+ **kwargs.merge!(
65
+ prefix: "#{@prefix}#{kwargs[:prefix]}",
66
+ prefix_width: @prefix_width + kwargs[:prefix_width].to_i,
67
+ suffix: "#{kwargs[:suffix]}#{@suffix}",
68
+ suffix_width: @suffix_width + kwargs[:suffix_width].to_i
69
+ )
62
70
  )
63
71
  self
64
72
  end
@@ -87,17 +95,23 @@ module NattyUI
87
95
  # @!visibility private
88
96
  def inspect = @status ? "#{_to_s[..-2]} status=#{@status}>" : _to_s
89
97
 
98
+ # @!visibility private
99
+ def rcol = @parent.rcol - @suffix_width
100
+
90
101
  protected
91
102
 
92
103
  def initialize(
93
104
  parent,
94
105
  prefix:,
95
- prefix_width: NattyUI.display_width(prefix)
106
+ prefix_width: NattyUI.display_width(prefix),
107
+ suffix: nil,
108
+ suffix_width: NattyUI.display_width(suffix)
96
109
  )
97
110
  super(parent)
98
111
  @prefix = prefix
112
+ @suffix = suffix
99
113
  @prefix_width = prefix_width
100
- @suffix_width = 0
114
+ @suffix_width = suffix_width
101
115
  end
102
116
  end
103
117
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'element'
3
4
 
4
5
  module NattyUI
@@ -10,11 +11,13 @@ module NattyUI
10
11
  # construction. This means table features are not complete defined and
11
12
  # may change in near future.
12
13
  #
14
+ # Defined values for `type` are
15
+ # :double, :heavy, :semi, :simple
16
+ #
13
17
  # @overload table(type: simple)
14
18
  # Construct and display a table.
15
19
  #
16
- # @param [Symbol] type frame type;
17
- # valid types are `:simple`, `:heavy`, `:semi`, `:double`
20
+ # @param [Symbol] type frame type
18
21
  # @yieldparam [Table] table construction helper
19
22
  # @return [Wrapper::Section, Wrapper] it's parent object
20
23
  #
@@ -39,8 +42,7 @@ module NattyUI
39
42
  # Display the given arrays as rows of a table.
40
43
  #
41
44
  # @param [Array<#to_s>] args one or more arrays representing rows of the table
42
- # @param [Symbol] type frame type;
43
- # valid types are `:simple`, `:heavy`, `:semi`, `:double`
45
+ # @param [Symbol] type frame type
44
46
  # @return [Wrapper::Section, Wrapper] it's parent object
45
47
  #
46
48
  # @example
@@ -115,7 +117,7 @@ module NattyUI
115
117
  raise(ArgumentError, "invalid table type - #{type.inspect}"),
116
118
  Ansi[39],
117
119
  Ansi::RESET
118
- ) { |line| @parent.puts(line) }
120
+ ) { @parent.puts(_1) }
119
121
  @parent
120
122
  end
121
123
 
@@ -142,7 +144,7 @@ module NattyUI
142
144
  @parent.available_width - 1,
143
145
  seperator,
144
146
  NattyUI.plain(seperator, ansi: false)[-1] == ' '
145
- ) { |line| @parent.puts(line) }
147
+ ) { @parent.puts(_1) }
146
148
  @parent
147
149
  end
148
150
  end
@@ -217,7 +219,7 @@ module NattyUI
217
219
 
218
220
  def align(str, width, alignment)
219
221
  return str unless (width -= NattyUI.display_width(str)).positive?
220
- return str << (' ' * width) if alignment == :left
222
+ return str + (' ' * width) if alignment == :left
221
223
  (' ' * width) << str
222
224
  end
223
225
 
@@ -268,10 +270,8 @@ module NattyUI
268
270
 
269
271
  def adjusted_widths(col_widths)
270
272
  ret = col_widths.dup
271
- if ret.sum <=
272
- (left = @max_width - (@col_div_size * (col_widths.size - 1)))
273
- return ret
274
- end
273
+ left = @max_width - (@col_div_size * (col_widths.size - 1))
274
+ return ret if ret.sum <= left
275
275
  indexed = ret.each_with_index.to_a
276
276
  # TODO: optimize this!
277
277
  until ret.sum <= left
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'io/console'
4
4
  require_relative 'ansi'
5
+ require_relative 'wrapper/animate'
5
6
  require_relative 'wrapper/ask'
6
7
  require_relative 'wrapper/framed'
7
8
  require_relative 'wrapper/heading'
@@ -65,7 +66,7 @@ module NattyUI
65
66
  def print(*args, **kwargs)
66
67
  args = prepare_print(args, kwargs).to_a
67
68
  @lines_written += args.size - 1
68
- @stream.print(args.join("\n"))
69
+ @stream.print(*args)
69
70
  @stream.flush
70
71
  self
71
72
  end
@@ -148,7 +149,10 @@ module NattyUI
148
149
  alias available_width screen_columns
149
150
 
150
151
  # @!visibility private
151
- def prefix = ''
152
+ alias rcol screen_columns
153
+
154
+ # @!visibility private
155
+ def prefix = nil
152
156
 
153
157
  # @return [Array<Symbol>] available glyph names
154
158
  def glyph_names = GLYPHS.keys
@@ -164,25 +168,26 @@ module NattyUI
164
168
  protected
165
169
 
166
170
  def prepare_print(args, kwargs)
167
- prefix = kwargs[:prefix] and prefix = NattyUI.plain(prefix, ansi: false)
168
- suffix = kwargs[:suffix] and suffix = NattyUI.plain(suffix, ansi: false)
171
+ _prepare_print(args, kwargs) { NattyUI.plain(_1, ansi: false) }
172
+ end
173
+
174
+ def _prepare_print(args, kwargs, &cvt)
175
+ prefix = kwargs[:prefix] and prefix = prefix.empty? ? '' : cvt[prefix]
176
+ suffix = kwargs[:suffix] and suffix = suffix.empty? ? '' : cvt[suffix]
169
177
  return ["#{prefix}#{suffix}"] if args.empty?
170
178
  NattyUI
171
179
  .each_line(
172
- *args.map! { NattyUI.plain(_1, ansi: false) },
173
- max_width: max_with(prefix, suffix, kwargs)
180
+ *args.map!(&cvt),
181
+ max_width:
182
+ kwargs.fetch(:max_width) do
183
+ screen_columns -
184
+ kwargs.fetch(:prefix_width) { NattyUI.display_width(prefix) } -
185
+ kwargs.fetch(:suffix_width) { NattyUI.display_width(suffix) }
186
+ end
174
187
  )
175
188
  .map { "#{prefix}#{_1}#{suffix}" }
176
189
  end
177
190
 
178
- def max_with(prefix, suffix, kwargs)
179
- mw = kwargs[:max_width] and return mw
180
- mw = screen_columns
181
- mw -= kwargs[:prefix_width] || NattyUI.display_width(prefix) if prefix
182
- mw -= kwargs[:suffix_width] || NattyUI.display_width(suffix) if suffix
183
- mw
184
- end
185
-
186
191
  def temp_func
187
192
  lambda do
188
193
  @stream.flush
data/lib/natty-ui.rb CHANGED
@@ -83,18 +83,19 @@ module NattyUI
83
83
  def embellish(str)
84
84
  return +'' if (str = str.to_s).empty?
85
85
  reset = false
86
- ret =
86
+ str =
87
87
  str.gsub(/(\[\[((?~\]\]))\]\])/) do
88
88
  match = Regexp.last_match[2]
89
- unless match.delete_prefix!('/')
89
+ if match[0] == '/'
90
+ next "[[#{match[1..]}]]" if match.size > 1
91
+ reset = false
92
+ Ansi::RESET
93
+ else
90
94
  ansi = Ansi.try_convert(match)
91
- next ansi ? reset = ansi : "[[#{match}]]"
95
+ ansi ? reset = ansi : "[[#{match}]]"
92
96
  end
93
- match.empty? or next "[[#{match}]]"
94
- reset = false
95
- Ansi::RESET
96
97
  end
97
- reset ? "#{ret}#{Ansi::RESET}" : ret
98
+ reset ? "#{str}#{Ansi::RESET}" : str
98
99
  end
99
100
 
100
101
  # Remove embedded attribute descriptions from given string.
@@ -103,16 +104,13 @@ module NattyUI
103
104
  # @param [:keep,:remove] ansi keep or remove ANSI codes too
104
105
  # @return [String] edited string
105
106
  def plain(str, ansi: :keep)
107
+ return +'' if (str = str.to_s).empty?
106
108
  str =
107
- str
108
- .to_s
109
- .gsub(/(\[\[((?~\]\]))\]\])/) do
110
- match = Regexp.last_match[2]
111
- if match.delete_prefix!('/')
112
- next match.empty? ? nil : "[[#{match}]]"
113
- end
114
- Ansi.try_convert(match) ? nil : "[[#{match}]]"
115
- end
109
+ str.gsub(/(\[\[((?~\]\]))\]\])/) do
110
+ match = Regexp.last_match[2]
111
+ next match.size == 1 ? nil : "[[#{match[1..]}]]" if match[0] == '/'
112
+ Ansi.try_convert(match) ? nil : "[[#{match}]]"
113
+ end
116
114
  ansi == :keep ? str : Ansi.blemish(str)
117
115
  end
118
116
 
@@ -122,8 +120,21 @@ module NattyUI
122
120
  # @param [#to_s] str string to calculate
123
121
  # @return [Integer] the display size
124
122
  def display_width(str)
125
- return 0 if (str = str.to_s).empty?
126
- Reline::Unicode.calculate_width(plain(str), true)
123
+ str = plain(str).encode(Encoding::UTF_8)
124
+ return 0 if str.empty?
125
+ width = 0
126
+ in_zero_width = false
127
+ str.scan(
128
+ Reline::Unicode::WIDTH_SCANNER
129
+ ) do |non_printing_start, non_printing_end, _csi, _osc, gc|
130
+ if in_zero_width
131
+ in_zero_width = false if non_printing_end
132
+ next
133
+ end
134
+ next in_zero_width = true if non_printing_start
135
+ width += Reline::Unicode.get_mbchar_width(gc) if gc
136
+ end
137
+ width
127
138
  end
128
139
 
129
140
  # Convert given arguments into strings and yield each line.
@@ -149,9 +160,12 @@ module NattyUI
149
160
  str
150
161
  .to_s
151
162
  .each_line(chomp: true) do |line|
152
- Reline::Unicode.split_by_width(line, max_width)[0].each do |part|
153
- yield(part) if part
154
- end
163
+ next yield(line) if line.empty?
164
+ lines, _height = Reline::Unicode.split_by_width(line, max_width)
165
+ lines.compact!
166
+ next if lines.empty?
167
+ lines.pop if lines[-1].empty?
168
+ lines.each(&block)
155
169
  end
156
170
  end
157
171
  nil
@@ -159,26 +173,38 @@ module NattyUI
159
173
 
160
174
  # Read next raw key (keyboard input) from {in_stream}.
161
175
  #
176
+ # The input will be returned as named key codes like "Ctrl+C" by default.
177
+ # This can be changed by the `mode` parameter:
178
+ #
179
+ # - `:named` - name if available (fallback to raw)
180
+ # - `:raw` - key code "as is"
181
+ # - `:both` - key code and name if available
182
+ #
183
+ # @param [:named, :raw, :both] mode modfies the result
162
184
  # @return [String] read key
163
- def read_key
185
+ def read_key(mode: :named)
164
186
  return @in_stream.getch unless defined?(@in_stream.getc)
165
187
  return @in_stream.getc unless defined?(@in_stream.raw)
166
- @in_stream.raw do |raw|
167
- key = raw.getc
168
- while (nc = raw.read_nonblock(1, exception: false))
188
+ @in_stream.raw do |raw_stream|
189
+ key = raw_stream.getc
190
+ while (nc = raw_stream.read_nonblock(1, exception: false))
169
191
  nc.is_a?(String) ? key += nc : break
170
192
  end
171
- key
193
+ return key if mode == :raw
194
+ return key, KEY_MAP[key]&.dup if mode == :both
195
+ KEY_MAP[key]&.dup || key
172
196
  end
197
+ rescue Interrupt, SystemCallError
198
+ nil
173
199
  end
174
200
 
175
201
  private
176
202
 
177
203
  def wrapper_class(stream, ansi)
178
204
  return AnsiWrapper if ansi == true
179
- if ansi == false || ENV.key?('NO_COLOR') || ENV['TERM'] == 'dumb'
180
- return Wrapper
181
- end
205
+ return Wrapper if ansi == false || ENV.key?('NO_COLOR')
206
+ return AnsiWrapper if ENV['ANSI'] == '1'
207
+ return Wrapper if ENV['TERM'] == 'dumb'
182
208
  stream.tty? ? AnsiWrapper : Wrapper
183
209
  end
184
210
 
@@ -197,8 +223,14 @@ module NattyUI
197
223
 
198
224
  @element = StdOut
199
225
  self.in_stream = STDIN
226
+
227
+ autoload(:KEY_MAP, File.join(__dir__, 'natty-ui', 'key_map'))
228
+ private_constant :KEY_MAP
200
229
  end
201
230
 
202
- # @see NattyUI.element
203
- # @return [NattyUI::Wrapper, NattyUI::Wrapper::Element] active UI element
204
- def ui = NattyUI.element unless defined?(ui)
231
+ # @!visibility private
232
+ module Kernel
233
+ # @see NattyUI.element
234
+ # @return [NattyUI::Wrapper, NattyUI::Wrapper::Element] active UI element
235
+ def ui = NattyUI.element unless defined?(ui)
236
+ end