natty-ui 0.6.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,19 +12,31 @@ 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)
26
- obj = NattyUI.embellish(obj)
27
- 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}" }
28
40
  end
29
41
 
30
42
  def temp_func
@@ -32,7 +44,7 @@ module NattyUI
32
44
  lambda do
33
45
  count = @lines_written - count
34
46
  if count.nonzero?
35
- @stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
47
+ @stream << Ansi.cursor_line_up(count) << Ansi::SCREEN_ERASE_BELOW
36
48
  @lines_written -= count
37
49
  end
38
50
  @stream.flush
@@ -40,40 +52,47 @@ module NattyUI
40
52
  end
41
53
  end
42
54
 
43
- class Message < Message
44
- 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
45
66
 
46
- def title_attr(str, symbol)
47
- color = COLORS[symbol]
48
- {
49
- prefix:
50
- if color
51
- "#{Ansi[:bold, :italic, color]}#{str}" \
52
- "#{Ansi[:reset, :bold, color]} "
53
- else
54
- "#{Ansi[:bold, 231]}#{str} "
55
- end,
56
- suffix: Ansi.reset
57
- }
67
+ def redraw
68
+ (wrapper.stream << @msg << (@max_value ? fullbar : indicator)).flush
58
69
  end
59
70
 
60
- COLORS = {
61
- default: 231,
62
- information: 117,
63
- warning: 220,
64
- error: 196,
65
- completed: 46,
66
- failed: 198,
67
- query: 220,
68
- task: 117
69
- }.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
70
89
  end
90
+ private_constant :Progress
71
91
 
72
- class Section < Section
92
+ module Temporary
73
93
  def temporary
74
- stream = wrapper.stream
75
94
  unless block_given?
76
- stream.flush
95
+ wrapper.stream.flush
77
96
  return self
78
97
  end
79
98
  count = wrapper.lines_written
@@ -82,158 +101,71 @@ module NattyUI
82
101
  ensure
83
102
  count = wrapper.lines_written - count
84
103
  if count.nonzero?
85
- stream << Ansi.cursor_line_up(count) << Ansi.screen_erase_below
104
+ wrapper.stream << Ansi.cursor_line_up(count) <<
105
+ Ansi::SCREEN_ERASE_BELOW
86
106
  end
87
- stream.flush
107
+ wrapper.stream.flush
88
108
  end
89
109
  end
110
+ end
111
+ private_constant :Temporary
90
112
 
91
- protected
92
-
93
- def initialize(parent, prefix_attr: nil, **opts)
94
- super
95
- return unless @prefix && prefix_attr
96
- @prefix = Ansi.embellish(@prefix, *prefix_attr)
97
- end
113
+ class Section < Section
114
+ include Temporary
98
115
  end
116
+ private_constant :Section
99
117
 
100
- class Heading < Heading
101
- protected
118
+ class Quote < Quote
119
+ include Temporary
102
120
 
103
- def enclose(weight)
104
- prefix, suffix = super
105
- ["#{PREFIX}#{prefix}#{MSG}", "#{PREFIX}#{suffix}#{Ansi.reset}"]
121
+ def initialize(...)
122
+ super
123
+ @prefix = "#{Ansi[39]}#{@prefix}#{Ansi::RESET}"
106
124
  end
125
+ end
126
+ private_constant :Quote
107
127
 
108
- PREFIX = Ansi[39].freeze
109
- MSG = Ansi[:bold, 231].freeze
128
+ class Framed < Framed
129
+ def color(str) = "#{Ansi[39]}#{str}#{Ansi::RESET}"
110
130
  end
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", '')
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}"
142
+ title,
143
+ prefix: "#{glyph} #{color}",
144
+ prefix_width: prefix_width,
145
+ suffix: Ansi::RESET,
146
+ suffix_width: 0
125
147
  )
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
135
- )
136
- end
137
-
138
- def suffix = "#{super} "
139
- def finish = parent.puts(@bottom)
140
-
141
- def components(type)
142
- COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
148
+ super(parent, prefix: ' ' * prefix_width, prefix_width: prefix_width)
143
149
  end
144
150
 
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
-
156
- class Ask < Ask
157
- protected
158
-
159
- def query(question)
160
- (wrapper.stream << "#{prefix}#{PREFIX} #{question}#{Ansi.reset} ").flush
161
- end
162
-
163
- def finish = (wrapper.stream << Ansi.line_clear).flush
164
-
165
- PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
166
- end
167
-
168
- class Request < Request
169
- def prompt(question) = "#{prefix}#{PREFIX} #{question}#{Ansi.reset} "
170
- def finish = (wrapper.stream << FINISH).flush
171
-
172
- PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
173
- FINISH = (Ansi.cursor_line_up + Ansi.line_erase_to_end).freeze
174
- end
175
-
176
- class Query < Query
177
- protected
178
-
179
- def read(choices, result_typye)
180
- wrapper.stream << "#{prefix}#{PROMPT} "
181
- super
182
- end
183
-
184
- PROMPT = Ansi.embellish(':', :bold, 220).freeze
185
- end
162
+ private_constant :Message
186
163
 
187
164
  class Task < Message
188
165
  include ProgressAttributes
189
166
  include TaskMethods
190
167
  end
191
-
192
- class Progress < Progress
193
- protected
194
-
195
- def draw(title)
196
- @prefix = "#{prefix}#{TITLE_PREFIX}#{title}#{Ansi.reset} "
197
- (wrapper.stream << @prefix << Ansi.cursor_hide).flush
198
- @prefix = "#{Ansi.line_clear}#{@prefix}"
199
- if @max_value
200
- @prefix << BAR_COLOR
201
- else
202
- @prefix << INDICATOR_ATTRIBUTE
203
- @indicator = 0
204
- end
205
- end
206
-
207
- def redraw
208
- (wrapper.stream << @prefix << (@max_value ? fullbar : indicator)).flush
209
- end
210
-
211
- def end_draw = (wrapper.stream << ERASE).flush
212
- def indicator = '─╲│╱'[(@indicator += 1) % 4]
213
- # def indicator = '⣷⣯⣟⡿⢿⣻⣽⣾'[(@indicator += 1) % 8]
214
-
215
- def fullbar
216
- percent = @value / @max_value
217
- count = (30 * percent).to_i
218
- mv = max_value.to_i.to_s
219
- "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
220
- "#{BAR_INK} #{value.to_i.to_s.rjust(mv.size)}/#{mv} " \
221
- "(#{(percent * 100).round(2).to_s.rjust(6)})"
222
- end
223
-
224
- TITLE_PREFIX = "#{Ansi[:bold, :italic, 117]}➔#{Ansi[:reset, 117]} ".freeze
225
- INDICATOR_ATTRIBUTE = Ansi[:bold, 220].freeze
226
- BAR_COLOR = Ansi[39, 295].freeze
227
- BAR_BACK = Ansi[236, 492].freeze
228
- BAR_INK = Ansi[:bold, 255, :on_default].freeze
229
- ERASE = (Ansi.line_clear + Ansi.cursor_show).freeze
230
- end
231
-
232
- PAGE_BEGIN =
233
- "#{Ansi.reset}#{Ansi.cursor_save_pos}#{Ansi.screen_save}" \
234
- "#{Ansi.cursor_home}#{Ansi.screen_erase}".freeze
235
- PAGE_END =
236
- "#{Ansi.screen_restore}#{Ansi.cursor_restore_pos}#{Ansi.reset}".freeze
168
+ private_constant :Task
237
169
  end
238
170
 
239
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
- def _element(type, *args)
27
- wrapper.class.const_get(type).__send__(:new, self).__send__(:_call, *args)
18
+ def _element(type, ...)
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.6.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 "\u0003\u0004\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