natty-ui 0.7.0 → 0.8.0

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.
@@ -12,24 +12,39 @@ module NattyUI
12
12
  @stream.flush
13
13
  return self
14
14
  end
15
- (@stream << PAGE_BEGIN).flush
15
+ (@stream << Ansi::BLANK_SLATE).flush
16
16
  begin
17
17
  yield(self)
18
18
  ensure
19
- (@stream << PAGE_END).flush
19
+ (@stream << Ansi::UNBLANK_SLATE).flush
20
20
  end
21
21
  end
22
22
 
23
+ def cls
24
+ (@stream << Ansi::CURSOR_HOME << Ansi::SCREEN_ERASE).flush
25
+ self
26
+ end
27
+
23
28
  protected
24
29
 
25
- def embellish(obj) = (obj = NattyUI.embellish(obj)).empty? ? nil : obj
30
+ def prepare_print(args, kwargs)
31
+ prefix = kwargs[:prefix] and prefix = NattyUI.embellish(prefix)
32
+ suffix = kwargs[:suffix] and suffix = NattyUI.embellish(suffix)
33
+ return ["#{prefix}#{suffix}"] if args.empty?
34
+ NattyUI
35
+ .each_line(
36
+ *args.map! { NattyUI.embellish(_1) },
37
+ max_width: max_with(prefix, suffix, kwargs)
38
+ )
39
+ .map { "#{prefix}#{_1}#{suffix}" }
40
+ end
26
41
 
27
42
  def temp_func
28
43
  count = @lines_written
29
44
  lambda do
30
45
  count = @lines_written - count
31
46
  if count.nonzero?
32
- @stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
47
+ @stream << Ansi.cursor_line_up(count) << Ansi::SCREEN_ERASE_BELOW
33
48
  @lines_written -= count
34
49
  end
35
50
  @stream.flush
@@ -37,41 +52,47 @@ module NattyUI
37
52
  end
38
53
  end
39
54
 
40
- class Message < Message
41
- protected
55
+ class Progress < Progress
56
+ def draw(title)
57
+ @msg =
58
+ "#{@parent.prefix}#{Ansi[:bold, 39]}➔#{Ansi[:reset, 39]} " \
59
+ "#{title}#{Ansi::RESET} "
60
+ (wrapper.stream << @msg << Ansi::CURSOR_HIDE).flush
61
+ @msg = "#{Ansi::LINE_CLEAR}#{@msg}"
62
+ return @msg << BAR_COLOR if @max_value
63
+ @msg << Ansi[:bold, 220]
64
+ @indicator = 0
65
+ end
42
66
 
43
- def title_attr(str, symbol)
44
- color = COLORS[symbol]
45
- {
46
- prefix:
47
- if color
48
- "#{Ansi[:bold, :italic, color]}#{str}" \
49
- "#{Ansi[:reset, :bold, color]} "
50
- else
51
- "#{Ansi[:bold, 231]}#{str} "
52
- end,
53
- suffix: Ansi.reset
54
- }
67
+ def redraw
68
+ (wrapper.stream << @msg << (@max_value ? fullbar : indicator)).flush
55
69
  end
56
70
 
57
- COLORS = {
58
- default: 231,
59
- information: 117,
60
- warning: 220,
61
- error: 196,
62
- completed: 46,
63
- failed: 198,
64
- query: 220,
65
- task: 117
66
- }.compare_by_identity.freeze
71
+ def end_draw
72
+ (wrapper.stream << Ansi::LINE_CLEAR << Ansi::CURSOR_SHOW).flush
73
+ end
74
+
75
+ def indicator = '◐◓◑◒'[(@indicator += 1) % 4]
76
+
77
+ def fullbar
78
+ percent = @value / @max_value
79
+ count = (30 * percent).to_i
80
+ mv = max_value.to_i.to_s
81
+ "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
82
+ "#{BAR_INK} #{value.to_i.to_s.rjust(mv.size)}/#{mv} " \
83
+ "(#{(percent * 100).round(2).to_s.rjust(6)})"
84
+ end
85
+
86
+ BAR_COLOR = Ansi[39, 295].freeze
87
+ BAR_BACK = Ansi[236, 492].freeze
88
+ BAR_INK = Ansi[:bold, 255, :on_default].freeze
67
89
  end
68
- private_constant :Message
90
+ private_constant :Progress
69
91
 
70
- class Section < Section
92
+ module Temporary
71
93
  def temporary
72
- stream = wrapper.stream
73
94
  unless block_given?
74
- stream.flush
95
+ wrapper.stream.flush
75
96
  return self
76
97
  end
77
98
  count = wrapper.lines_written
@@ -80,163 +101,71 @@ module NattyUI
80
101
  ensure
81
102
  count = wrapper.lines_written - count
82
103
  if count.nonzero?
83
- stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
104
+ wrapper.stream << Ansi.cursor_line_up(count) <<
105
+ Ansi::SCREEN_ERASE_BELOW
84
106
  end
85
- stream.flush
107
+ wrapper.stream.flush
86
108
  end
87
109
  end
110
+ end
111
+ private_constant :Temporary
88
112
 
89
- protected
90
-
91
- def initialize(parent, prefix_attr: nil, **opts)
92
- super
93
- return unless @prefix && prefix_attr
94
- @prefix = Ansi.embellish(@prefix, *prefix_attr)
95
- end
113
+ class Section < Section
114
+ include Temporary
96
115
  end
97
116
  private_constant :Section
98
117
 
99
- class Heading < Heading
100
- protected
118
+ class Quote < Quote
119
+ include Temporary
101
120
 
102
- def enclose(weight)
103
- prefix, suffix = super
104
- ["#{PREFIX}#{prefix}#{MSG}", "#{PREFIX}#{suffix}#{Ansi.reset}"]
121
+ def initialize(...)
122
+ super
123
+ @prefix = "#{Ansi[39]}#{@prefix}#{Ansi::RESET}"
105
124
  end
125
+ end
126
+ private_constant :Quote
106
127
 
107
- PREFIX = Ansi[39].freeze
108
- MSG = Ansi[:bold, 231].freeze
128
+ class Framed < Framed
129
+ def color(str) = "#{Ansi[39]}#{str}#{Ansi::RESET}"
109
130
  end
110
- private_constant :Heading
131
+ private_constant :Framed
111
132
 
112
- class Framed < Section
133
+ class Message < Section
113
134
  protected
114
135
 
115
- def initialize(parent, title:, type:, **opts)
116
- @parent = parent
117
- title = title.to_s.tr("\r\n\t", '')
118
- topl, topr, botl, botr, hor, vert = *components(type)
119
- width = available_width
120
- rcount = [width - _plain_width(title) - 6, 0].max
136
+ def initialize(parent, title:, glyph:)
137
+ wrapper = parent.wrapper
138
+ color = COLORS[glyph] || COLORS[:default]
139
+ glyph = wrapper.glyph(glyph) || glyph
140
+ prefix_width = NattyUI.display_width(glyph) + 1
121
141
  parent.puts(
122
- "#{COLOR}#{topl}#{hor}#{hor}#{Ansi.reset} " \
123
- "#{TITLE_ATTR}#{title}#{Ansi.reset} " \
124
- "#{COLOR}#{hor * rcount}#{topr}#{Ansi.reset}"
125
- )
126
- @bottom = "#{COLOR}#{botl}#{hor * (width - 2)}#{botr}#{Ansi.reset}"
127
- vert = "#{COLOR}#{vert}#{Ansi.reset}"
128
- super(
129
- parent,
130
- prefix: "#{vert} ",
131
- suffix:
132
- "#{Ansi.cursor_right_aligned}" \
133
- "#{Ansi.cursor_left(suffix_width)}#{vert}",
134
- **opts
142
+ title,
143
+ prefix: "#{glyph} #{color}",
144
+ prefix_width: prefix_width,
145
+ suffix: Ansi::RESET,
146
+ suffix_width: 0
135
147
  )
148
+ super(parent, prefix: ' ' * prefix_width, prefix_width: prefix_width)
136
149
  end
137
150
 
138
- def suffix = "#{super} "
139
- def finish = parent.puts(@bottom)
140
-
141
- def components(type)
142
- COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
143
- end
144
-
145
- COLOR = Ansi[39].freeze
146
- TITLE_ATTR = Ansi[:bold, 231].freeze
147
- COMPONENTS = {
148
- rounded: %w[╭ ╮ ╰ ╯ ─ │],
149
- simple: %w[┌ ┐ └ ┘ ─ │],
150
- heavy: %w[┏ ┓ ┗ ┛ ━ ┃],
151
- semi: %w[┍ ┑ ┕ ┙ ━ │],
152
- double: %w[╔ ╗ ╚ ╝ ═ ║]
151
+ COLORS = {
152
+ default: Ansi[255],
153
+ information: Ansi[255],
154
+ warning: Ansi[221],
155
+ error: Ansi[208],
156
+ completed: Ansi[82],
157
+ failed: Ansi[196],
158
+ task: Ansi[39],
159
+ query: Ansi[255]
153
160
  }.compare_by_identity.freeze
154
161
  end
155
- private_constant :Framed
156
-
157
- class Ask < Ask
158
- protected
159
-
160
- def query(question)
161
- (wrapper.stream << "#{prefix}#{PREFIX} #{question}#{Ansi.reset} ").flush
162
- end
163
-
164
- def finish = (wrapper.stream << Ansi.line_clear).flush
165
-
166
- PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
167
- end
168
- private_constant :Ask
169
-
170
- class Request < Request
171
- def prompt(question) = "#{prefix}#{PREFIX} #{question}#{Ansi.reset} "
172
- def finish = (wrapper.stream << FINISH).flush
173
-
174
- PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
175
- FINISH = (Ansi.cursor_line_up + Ansi.line_erase_to_end).freeze
176
- end
177
- private_constant :Request
178
-
179
- class Query < Query
180
- protected
181
-
182
- def read(choices, result_typye)
183
- wrapper.stream << "#{prefix}#{PROMPT} "
184
- super
185
- end
186
-
187
- PROMPT = Ansi.embellish(':', :bold, 220).freeze
188
- end
189
- private_constant :Query
162
+ private_constant :Message
190
163
 
191
164
  class Task < Message
192
165
  include ProgressAttributes
193
166
  include TaskMethods
194
167
  end
195
168
  private_constant :Task
196
-
197
- class Progress < Progress
198
- protected
199
-
200
- def draw(title)
201
- @prefix = "#{prefix}#{TITLE_PREFIX}#{title}#{Ansi.reset} "
202
- (wrapper.stream << @prefix << Ansi.cursor_hide).flush
203
- @prefix = "#{Ansi.line_clear}#{@prefix}"
204
- return @prefix << BAR_COLOR if @max_value
205
- @prefix << INDICATOR_ATTRIBUTE
206
- @indicator = 0
207
- end
208
-
209
- def redraw
210
- (wrapper.stream << @prefix << (@max_value ? fullbar : indicator)).flush
211
- end
212
-
213
- def end_draw = (wrapper.stream << ERASE).flush
214
- def indicator = '─╲│╱'[(@indicator += 1) % 4]
215
- # def indicator = '⣷⣯⣟⡿⢿⣻⣽⣾'[(@indicator += 1) % 8]
216
-
217
- def fullbar
218
- percent = @value / @max_value
219
- count = (30 * percent).to_i
220
- mv = max_value.to_i.to_s
221
- "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
222
- "#{BAR_INK} #{value.to_i.to_s.rjust(mv.size)}/#{mv} " \
223
- "(#{(percent * 100).round(2).to_s.rjust(6)})"
224
- end
225
-
226
- TITLE_PREFIX = "#{Ansi[:bold, :italic, 117]}➔#{Ansi[:reset, 117]} ".freeze
227
- INDICATOR_ATTRIBUTE = Ansi[:bold, 220].freeze
228
- BAR_COLOR = Ansi[39, 295].freeze
229
- BAR_BACK = Ansi[236, 492].freeze
230
- BAR_INK = Ansi[:bold, 255, :on_default].freeze
231
- ERASE = (Ansi.line_clear + Ansi.cursor_show).freeze
232
- end
233
- private_constant :Progress
234
-
235
- PAGE_BEGIN =
236
- "#{Ansi.reset}#{Ansi.cursor_save_pos}#{Ansi.screen_save}" \
237
- "#{Ansi.cursor_home}#{Ansi.screen_erase}".freeze
238
- PAGE_END =
239
- "#{Ansi.screen_restore}#{Ansi.cursor_restore_pos}#{Ansi.reset}".freeze
240
169
  end
241
170
 
242
171
  private_constant :AnsiWrapper
@@ -5,32 +5,25 @@ module NattyUI
5
5
  # Features of {NattyUI} - methods to display natty elements.
6
6
  #
7
7
  module Features
8
- # Print a horizontal rule
9
- #
10
- # @param [#to_s] symbol string to build the horizontal rule
11
- # @return [Wrapper, Wrapper::Element] itself
12
- def hr(symbol = '═')
13
- symbol = symbol.to_s
14
- size = _plain_width(symbol)
15
- return self if size.zero?
16
- msg = symbol * ((available_width - 1) / size)
17
- return puts(msg, prefix: Ansi[39], suffix: Ansi.reset) if wrapper.ansi?
18
- puts(msg)
8
+ # @return [Wrapper] assigned output stream wrapper
9
+ def wrapper
10
+ return @wrapper if @wrapper
11
+ @wrapper = @parent
12
+ @wrapper = @wrapper.parent until @wrapper.is_a?(Wrapper)
13
+ @wrapper
19
14
  end
20
15
 
21
16
  protected
22
17
 
23
- def _plain_width(str) = NattyUI.display_width(NattyUI.plain(str))
24
- def _blemish_width(str) = NattyUI.display_width(Ansi.blemish(str))
25
-
26
18
  def _element(type, ...)
27
- wrapper.class.const_get(type).__send__(:new, self).__send__(:_call, ...)
19
+ wrapper.class.const_get(type).__send__(:new, self).__send__(:call, ...)
28
20
  end
29
21
 
30
- def _section(owner, type, args, **opts, &block)
31
- sec = wrapper.class.const_get(type).__send__(:new, owner, **opts)
22
+ def _section(type, args = nil, owner: nil, **opts, &block)
23
+ sec = wrapper.class.const_get(type).__send__(:new, owner || self, **opts)
32
24
  sec.puts(*args) if args && !args.empty?
33
- block ? sec.__send__(:_call, &block) : sec
25
+ sec.__send__(:call, &block) if block
26
+ sec
34
27
  end
35
28
  end
36
29
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NattyUI
4
- # @return [String] the version number of the gem
5
- VERSION = '0.7.0'
4
+ # The version number of the gem.
5
+ VERSION = '0.8.0'
6
6
  end
@@ -13,13 +13,13 @@ module NattyUI
13
13
  # The default for `yes` includes `ENTER` and `RETURN` key
14
14
  #
15
15
  # @example
16
- # case sec.ask('Do you like the NattyUI gem?')
16
+ # case ui.ask('Do you like the NattyUI gem?')
17
17
  # when true
18
- # sec.info('Yeah!!')
18
+ # ui.info('Yeah!!')
19
19
  # when false
20
- # sec.write("That's pity!")
20
+ # ui.write("That's pity!")
21
21
  # else
22
- # sec.failed('You should have an opinion!')
22
+ # ui.failed('You should have an opinion!')
23
23
  # end
24
24
  #
25
25
  # @see NattyUI.in_stream
@@ -28,7 +28,7 @@ module NattyUI
28
28
  # @param yes [#to_s] chars which will be used to answer 'Yes'
29
29
  # @param no [#to_s] chars which will be used to answer 'No'
30
30
  # @return [Boolean] whether the answer is yes or no
31
- # @return [nil] when input was aborted with `ESC`, `^C` or `^D`
31
+ # @return [nil] when input was aborted with `^C` or `^D`
32
32
  def ask(question, yes: "jotsyd\r\n", no: 'n')
33
33
  _element(:Ask, question, yes, no)
34
34
  end
@@ -42,27 +42,33 @@ module NattyUI
42
42
  class Ask < Element
43
43
  protected
44
44
 
45
- def _call(question, yes, no)
45
+ def call(question, yes, no)
46
46
  yes, no = grab(yes, no)
47
- query(question)
48
- read(yes, no)
49
- ensure
50
- finish
51
- end
52
-
53
- def query(question)
54
- (wrapper.stream << prefix << "▶︎ #{question} ").flush
55
- end
56
-
57
- def finish = (wrapper.stream << "\n").flush
58
-
59
- def read(yes, no)
47
+ draw(question)
60
48
  while true
61
49
  char = NattyUI.in_stream.getch
62
- return if "\3\4\e".include?(char)
50
+ return if "\3\4".include?(char)
63
51
  return true if yes.include?(char)
64
52
  return false if no.include?(char)
65
53
  end
54
+ rescue Interrupt, SystemCallError
55
+ nil
56
+ ensure
57
+ if @parent.ansi?
58
+ (wrapper.stream << Ansi::LINE_CLEAR).flush
59
+ else
60
+ @parent.puts
61
+ end
62
+ end
63
+
64
+ def draw(question)
65
+ glyph = wrapper.glyph(:query)
66
+ @parent.print(
67
+ question,
68
+ prefix: "#{glyph} #{Ansi[255]}",
69
+ prefix_width: NattyUI.display_width(glyph) + 1,
70
+ suffix_width: 0
71
+ )
66
72
  end
67
73
 
68
74
  def grab(yes, no)
@@ -10,7 +10,7 @@ module NattyUI
10
10
  class Element
11
11
  include Features
12
12
 
13
- # @return [Section] when embedded in a section
13
+ # @return [Element] when embedded in a section
14
14
  # @return [Wrapper] when not embedded in a section
15
15
  attr_reader :parent
16
16
 
@@ -22,6 +22,9 @@ module NattyUI
22
22
  # @return [Boolean] whether its closed or not
23
23
  def closed? = (@status != nil)
24
24
 
25
+ # @return [Integer] available columns count within the element
26
+ def available_width = @parent.available_width
27
+
25
28
  # Close the element.
26
29
  #
27
30
  # @return [Element] itself
@@ -29,28 +32,11 @@ module NattyUI
29
32
 
30
33
  alias _to_s to_s
31
34
  private :_to_s
32
-
33
35
  # @!visibility private
34
- def inspect = "#{_to_s[..-2]} status=#{@status}}}>"
36
+ def inspect = _to_s
35
37
 
36
38
  protected
37
39
 
38
- def prefix = "#{@parent.__send__(:prefix)}#{@prefix}"
39
- def suffix = "#{@suffix}#{@parent.__send__(:suffix)}"
40
- def prefix_width = _blemish_width(prefix)
41
- def suffix_width = _blemish_width(suffix)
42
- def available_width = wrapper.screen_columns - prefix_width - suffix_width
43
- def finish = nil
44
-
45
- def wrapper
46
- return @wrapper if @wrapper
47
- @wrapper = self
48
- @wrapper = @wrapper.parent until @wrapper.is_a?(Wrapper)
49
- @wrapper
50
- end
51
-
52
- def initialize(parent, **_) = (@parent = parent)
53
-
54
40
  def _close(state)
55
41
  return self if @status
56
42
  @status = state
@@ -58,18 +44,28 @@ module NattyUI
58
44
  @raise ? raise(BREAK) : self
59
45
  end
60
46
 
61
- def _call
47
+ def call
48
+ NattyUI.instance_variable_set(:@element, self)
62
49
  @raise = true
63
50
  yield(self)
64
- close unless closed?
51
+ closed? ? self : close
65
52
  rescue BREAK
66
53
  nil
54
+ ensure
55
+ NattyUI.instance_variable_set(:@element, @parent)
67
56
  end
68
57
 
69
- BREAK = Class.new(StandardError)
70
- private_constant :BREAK
58
+ def finish = nil
59
+ def prefix = "#{@parent.instance_variable_get(:@prefix)}#{@prefix}"
60
+
61
+ def initialize(parent)
62
+ @parent = parent
63
+ end
71
64
 
72
65
  private_class_method :new
66
+
67
+ BREAK = Class.new(StandardError)
68
+ private_constant :BREAK
73
69
  end
74
70
  end
75
71
  end
@@ -10,15 +10,14 @@ module NattyUI
10
10
  # When no block is given, the section must be closed, see
11
11
  # {Wrapper::Element#close}.
12
12
  #
13
- # @param [#to_s] title object to print as section title
14
13
  # @param [Array<#to_s>] args more objects to print
15
14
  # @param [Symbol] type frame type;
16
15
  # valid types are `:rounded`, `:simple`, `:heavy`, `:semi`, `:double`
17
16
  # @yieldparam [Wrapper::Framed] framed the created section
18
17
  # @return [Object] the result of the code block
19
18
  # @return [Wrapper::Framed] itself, when no code block is given
20
- def framed(title, *args, type: :rounded, &block)
21
- _section(self, :Framed, args, title: title, type: type, &block)
19
+ def framed(*args, type: :rounded, &block)
20
+ _section(:Framed, args, type: type, &block)
22
21
  end
23
22
  end
24
23
 
@@ -30,25 +29,31 @@ module NattyUI
30
29
  class Framed < Section
31
30
  protected
32
31
 
33
- def initialize(parent, title:, type:, **opts)
34
- top_start, top_suffix, left, bottom = components(type)
35
- parent.puts(" #{title} ", prefix: top_start, suffix: top_suffix)
36
- @bottom = bottom
37
- super(parent, prefix: "#{left} ", **opts)
32
+ 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
38
41
  end
39
42
 
40
- def finish = parent.puts(@bottom)
41
-
42
- def components(type)
43
- COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
43
+ def finish
44
+ @parent.puts(
45
+ color("#{@type[3]}#{@type[1] * (@parent.available_width - 2)}")
46
+ )
44
47
  end
45
48
 
46
- COMPONENTS = {
47
- rounded: %w[╭── ───── │ ╰──────────],
48
- simple: %w[┌── ───── │ └──────────],
49
- heavy: %w[┏━━ ━━━━━ ┃ ┗━━━━━━━━━━],
50
- semi: %w[┍━━ ━━━━━ │ ┕━━━━━━━━━━],
51
- double: %w[╔══ ═════ ║ ╚══════════]
49
+ def color(str) = str
50
+
51
+ FRAME_PARTS = {
52
+ rounded: '╭─╮╰│╯',
53
+ simple: '┌─┐└│┘',
54
+ heavy: '┏━┓┗┃┛',
55
+ double: '╔═╗╚║╝',
56
+ semi: '╒═╕╘│╛'
52
57
  }.compare_by_identity.freeze
53
58
  end
54
59
  end