natty-ui 0.5.3 → 0.6.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.
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'io/console'
4
- require_relative 'wrapper'
5
3
  require_relative 'ansi'
4
+ require_relative 'wrapper'
6
5
 
7
6
  module NattyUI
8
7
  class AnsiWrapper < Wrapper
@@ -110,18 +109,48 @@ module NattyUI
110
109
  MSG = Ansi[:bold, 231].freeze
111
110
  end
112
111
 
113
- class Framed < Framed
112
+ class Framed < Section
114
113
  protected
115
114
 
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
121
+ 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
135
+ )
136
+ end
137
+
138
+ def suffix = "#{super} "
139
+ def finish = parent.puts(@bottom)
140
+
116
141
  def components(type)
117
- top_start, top_suffix, left, bottom = super
118
- [
119
- "#{Ansi[39]}#{top_start}#{Ansi[:bold, 231]}",
120
- "#{Ansi[:reset, 39]}#{top_suffix}#{Ansi.reset}",
121
- Ansi.embellish(left, 39),
122
- Ansi.embellish(bottom, 39)
123
- ]
142
+ COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
124
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[╔ ╗ ╚ ╝ ═ ║]
153
+ }.compare_by_identity.freeze
125
154
  end
126
155
 
127
156
  class Ask < Ask
@@ -136,6 +165,14 @@ module NattyUI
136
165
  PREFIX = "#{Ansi[:bold, :italic, 220]}▶︎#{Ansi[:reset, 220]}".freeze
137
166
  end
138
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
+
139
176
  class Query < Query
140
177
  protected
141
178
 
@@ -155,9 +192,9 @@ module NattyUI
155
192
  class Progress < Progress
156
193
  protected
157
194
 
158
- def draw_title(title)
195
+ def draw(title)
159
196
  @prefix = "#{prefix}#{TITLE_PREFIX}#{title}#{Ansi.reset} "
160
- (wrapper.stream << @prefix).flush
197
+ (wrapper.stream << @prefix << Ansi.cursor_hide).flush
161
198
  @prefix = "#{Ansi.line_clear}#{@prefix}"
162
199
  if @max_value
163
200
  @prefix << BAR_COLOR
@@ -167,26 +204,21 @@ module NattyUI
167
204
  end
168
205
  end
169
206
 
170
- def draw_final = (wrapper.stream << Ansi.line_clear).flush
171
-
172
207
  def redraw
173
208
  (wrapper.stream << @prefix << (@max_value ? fullbar : indicator)).flush
174
209
  end
175
210
 
211
+ def end_draw = (wrapper.stream << ERASE).flush
176
212
  def indicator = '─╲│╱'[(@indicator += 1) % 4]
213
+ # def indicator = '⣷⣯⣟⡿⢿⣻⣽⣾'[(@indicator += 1) % 8]
177
214
 
178
215
  def fullbar
179
216
  percent = @value / @max_value
180
217
  count = (30 * percent).to_i
218
+ mv = max_value.to_i.to_s
181
219
  "#{'█' * count}#{BAR_BACK}#{'▁' * (30 - count)}" \
182
- "#{BAR_INK} #{
183
- format(
184
- '%<value>.0f/%<max_value>.0f (%<percent>.2f%%)',
185
- value: @value,
186
- max_value: @max_value,
187
- percent: percent * 100
188
- )
189
- }"
220
+ "#{BAR_INK} #{value.to_i.to_s.rjust(mv.size)}/#{mv} " \
221
+ "(#{(percent * 100).round(2).to_s.rjust(6)})"
190
222
  end
191
223
 
192
224
  TITLE_PREFIX = "#{Ansi[:bold, :italic, 117]}➔#{Ansi[:reset, 117]} ".freeze
@@ -194,6 +226,7 @@ module NattyUI
194
226
  BAR_COLOR = Ansi[39, 295].freeze
195
227
  BAR_BACK = Ansi[236, 492].freeze
196
228
  BAR_INK = Ansi[:bold, 255, :on_default].freeze
229
+ ERASE = (Ansi.line_clear + Ansi.cursor_show).freeze
197
230
  end
198
231
 
199
232
  PAGE_BEGIN =
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ #
5
+ # Features of {NattyUI} - methods to display natty elements.
6
+ #
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)
19
+ end
20
+
21
+ protected
22
+
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)
28
+ end
29
+
30
+ def _section(owner, type, args, **opts, &block)
31
+ sec = wrapper.class.const_get(type).__send__(:new, owner, **opts)
32
+ sec.puts(*args) if args && !args.empty?
33
+ block ? sec.__send__(:_call, &block) : sec
34
+ end
35
+ end
36
+ end
@@ -12,7 +12,7 @@ module NattyUI
12
12
  #
13
13
  module ProgressAttributes
14
14
  # @attribute [r] completed?
15
- # @return [Boolean] whether the task completed sucessfully
15
+ # @return [Boolean] whether the task completed successfully
16
16
  def completed? = (@status == :completed)
17
17
 
18
18
  # @attribute [r] failed?
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NattyUI
4
4
  # @return [String] the version number of the gem
5
- VERSION = '0.5.3'
5
+ VERSION = '0.6.0'
6
6
  end
@@ -17,7 +17,7 @@ module NattyUI
17
17
  # when true
18
18
  # sec.info('Yeah!!')
19
19
  # when false
20
- # sec.write("That's pitty!")
20
+ # sec.write("That's pity!")
21
21
  # else
22
22
  # sec.failed('You should have an opinion!')
23
23
  # end
@@ -67,9 +67,9 @@ module NattyUI
67
67
 
68
68
  def grab(yes, no)
69
69
  yes = yes.to_s.chars.uniq
70
+ raise(ArgumentError, ':yes can not be empty') if yes.empty?
70
71
  no = no.to_s.chars.uniq
71
- raise(ArgumentError, ':yes can not be emoty') if yes.empty?
72
- raise(ArgumentError, ':no can not be emoty') if no.empty?
72
+ raise(ArgumentError, ':no can not be empty') if no.empty?
73
73
  return yes, no if (yes & no).empty?
74
74
  raise(ArgumentError, 'chars in :yes and :no can not be intersect')
75
75
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'features'
3
+ require_relative '../features'
4
4
 
5
5
  module NattyUI
6
6
  class Wrapper
@@ -36,7 +36,10 @@ module NattyUI
36
36
  protected
37
37
 
38
38
  def prefix = "#{@parent.__send__(:prefix)}#{@prefix}"
39
- def suffix = "#{@parent.__send__(:suffix)}#{@suffix}"
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
40
43
  def finish = nil
41
44
 
42
45
  def wrapper
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ module Features
7
+ #
8
+ # Print items of a given list as columns.
9
+ # In the default compact format columns may have diffrent widths and the
10
+ # list items are ordered column-wise.
11
+ # The non-compact format prints all columns in same width and order the list
12
+ # items row-wise.
13
+ #
14
+ # @param [Array<#to_s>] args items to print
15
+ # @param [Boolean] compact whether to use compact format
16
+ # @return [Wrapper, Wrapper::Element] itself
17
+ def ls(*args, compact: true) = _element(:ListInColumns, args, compact)
18
+ end
19
+
20
+ class Wrapper
21
+ #
22
+ # An {Element} to print items of a given list as columns.
23
+ #
24
+ # @see Features#ls
25
+ class ListInColumns < Element
26
+ protected
27
+
28
+ def _call(list, compact)
29
+ list.flatten!
30
+ return parent if list.empty?
31
+ list.map! { |item| Item.new(item = item.to_s, _plain_width(item)) }
32
+ if compact
33
+ each_compacted(list, available_width) { |line| parent.puts(line) }
34
+ else
35
+ each(list, available_width) { |line| parent.puts(line) }
36
+ end
37
+ parent
38
+ end
39
+
40
+ def each(list, max_width)
41
+ width = list.max_by(&:width).width + 3
42
+ list.each_slice(max_width / width) do |slice|
43
+ yield(slice.map { |item| item.to_s(width) }.join)
44
+ end
45
+ end
46
+
47
+ def each_compacted(list, max_width)
48
+ found, widths = find_columns(list, max_width)
49
+ fill(found[-1], found[0].size)
50
+ found.transpose.each do |row|
51
+ row = row.each_with_index.map { |item, col| item&.to_s(widths[col]) }
52
+ yield(row.join)
53
+ end
54
+ end
55
+
56
+ def find_columns(list, max_width)
57
+ found = [list]
58
+ widths = [list.max_by(&:width).width]
59
+ 1.upto(list.size - 1) do |slice_size|
60
+ candidate = list.each_slice(list.size / slice_size).to_a
61
+ cwidths = candidate.map { |ary| ary.max_by(&:width).width + 3 }
62
+ cwidths[-1] -= 3
63
+ break if cwidths.sum > max_width
64
+ found = candidate
65
+ widths = cwidths
66
+ end
67
+ [found, widths]
68
+ end
69
+
70
+ def fill(ary, size)
71
+ diff = size - ary.size
72
+ ary.fill(nil, ary.size, diff) if diff.positive?
73
+ end
74
+
75
+ Item =
76
+ Data.define(:str, :width) do
77
+ def to_s(in_width) = "#{str}#{' ' * (in_width - width)}"
78
+ end
79
+ end
80
+ end
81
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'element'
4
- require_relative 'mixins'
4
+ require_relative '../mixins'
5
5
 
6
6
  module NattyUI
7
7
  module Features
@@ -35,11 +35,11 @@ module NattyUI
35
35
  @max_value = [0, max_value.to_f].max if max_value
36
36
  @value = 0
37
37
  @progress = 0
38
- draw_title(title)
38
+ draw(title)
39
39
  end
40
40
 
41
- def draw_title(title) = (wrapper.stream << prefix << "➔ #{title} ").flush
42
- def draw_final = (wrapper.stream << "\n")
41
+ def draw(title) = (wrapper.stream << prefix << "➔ #{title} ").flush
42
+ def end_draw = (wrapper.stream << "\n")
43
43
 
44
44
  def redraw
45
45
  return (wrapper.stream << '.').flush unless @max_value
@@ -50,10 +50,15 @@ module NattyUI
50
50
  end
51
51
 
52
52
  def finish
53
- draw_final
53
+ end_draw
54
54
  return @parent.failed(*@final_text) if failed?
55
- @status = :ok if @status == :closed
56
- @parent.completed(*@final_text)
55
+ _section(
56
+ @parent,
57
+ :Message,
58
+ @final_text,
59
+ title: @final_text.shift,
60
+ symbol: @status = :completed
61
+ )
57
62
  end
58
63
  end
59
64
  end
@@ -28,7 +28,7 @@ module NattyUI
28
28
  #
29
29
  # @param question [#to_s] Question to display
30
30
  # @param choices [#to_s] choices selectable via index (0..9)
31
- # @param result [Symbol] defines how the result ist returned
31
+ # @param result [Symbol] defines how the result will be returned
32
32
  # @param kw_choices [{Char => #to_s}] choices selectable with given char
33
33
  # @return [Char] when `result` is configured as `:char`
34
34
  # @return [#to_s] when `result` is configured as `:choice`
@@ -80,7 +80,7 @@ module NattyUI
80
80
  .to_h
81
81
  .merge!(kw_choices)
82
82
  .transform_keys! { |k| [k.to_s[0], ' '].max }
83
- .transform_values! { |v| v.to_s.tr("\r\n", ' ') }
83
+ .transform_values! { |v| v.to_s.tr("\r\n\t", ' ') }
84
84
  end
85
85
  end
86
86
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Request user input.
8
+ #
9
+ # @param question [#to_s] Question to display
10
+ # @return [String] the user input
11
+ # @return [nil] when input was aborted with `^C` or `^D`
12
+ def request(question) = _element(:Request, question)
13
+ end
14
+
15
+ class Wrapper
16
+ #
17
+ # An {Element} to request user input.
18
+ #
19
+ # @see Features#request
20
+ class Request < Element
21
+ protected
22
+
23
+ def _call(question)
24
+ NattyUI.readline(prompt(question), stream: wrapper.stream)
25
+ ensure
26
+ finish
27
+ end
28
+
29
+ def prompt(question) = "#{prefix}▶︎ #{question}: "
30
+ end
31
+ end
32
+ end
@@ -56,7 +56,7 @@ module NattyUI
56
56
  *args,
57
57
  max_width: max_width,
58
58
  prefix: prefix ? "#{@prefix}#{prefix}" : @prefix,
59
- suffix: suffix ? "#{@suffix}#{suffix}" : @suffix
59
+ suffix: suffix ? "#{suffix}#{@suffix}" : @suffix
60
60
  )
61
61
  self
62
62
  end
@@ -68,7 +68,7 @@ module NattyUI
68
68
  # @return [Section] itself
69
69
  def space(lines = 1)
70
70
  @parent.puts(
71
- "\n" * ([lines.to_i, 1].max - 1),
71
+ "\n" * [lines.to_i, 1].max,
72
72
  prefix: @prefix,
73
73
  suffix: @suffix
74
74
  )
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'section'
4
- require_relative 'mixins'
4
+ require_relative '../mixins'
5
5
 
6
6
  module NattyUI
7
7
  module Features
@@ -1,11 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'io/console'
3
4
  require_relative 'wrapper/ask'
4
5
  require_relative 'wrapper/framed'
5
6
  require_relative 'wrapper/heading'
7
+ require_relative 'wrapper/list_in_columns'
6
8
  require_relative 'wrapper/message'
7
9
  require_relative 'wrapper/progress'
8
10
  require_relative 'wrapper/query'
11
+ require_relative 'wrapper/request'
9
12
  require_relative 'wrapper/section'
10
13
  require_relative 'wrapper/task'
11
14
 
@@ -160,6 +163,12 @@ module NattyUI
160
163
  def prefix = nil
161
164
  alias suffix prefix
162
165
 
166
+ def prefix_width = 0
167
+ alias suffix_width prefix_width
168
+ alias width prefix_width
169
+
170
+ alias available_width screen_columns
171
+
163
172
  private_class_method :new
164
173
  end
165
174
  end
data/lib/natty-ui.rb CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  require 'readline'
4
4
  unless defined?(Reline)
5
- # only load the Reline::Unicode part
5
+ # load the Reline::Unicode part only
6
6
  # @!visibility private
7
7
  module Reline
8
+ # @!visibility private
8
9
  def self.ambiguous_width = 1
9
10
  end
10
11
  require 'reline/unicode'
@@ -156,19 +157,28 @@ module NattyUI
156
157
 
157
158
  # Read user input line from {.in_stream}.
158
159
  #
160
+ # This method uses Ruby's Readline implementation (default gem). See there
161
+ # for more information.
162
+ #
159
163
  # @see .valid_out?
160
164
  #
161
165
  # @param [#to_s] prompt input prompt
166
+ # @param [false, nil, #call] completion disable autocompletion, use default
167
+ # autocompletion or use given completion proc
162
168
  # @param [IO] stream writeable IO used to display output
163
169
  # @return [String] user input line
164
- # @return [nil] when user interrputed input with `^C` or `^D`
165
- def readline(prompt = nil, stream: StdOut.stream)
170
+ # @return [nil] when user interrupted input with `^C` or `^D`
171
+ def readline(prompt = nil, completion: false, stream: StdOut.stream)
172
+ cp = Readline.completion_proc
173
+ Readline.completion_proc = completion == false ? ->(*_) {} : completion
166
174
  Readline.output = stream
167
175
  Readline.input = @in_stream
168
176
  Readline.readline(prompt.to_s)
169
177
  rescue Interrupt
170
178
  stream.puts
171
179
  nil
180
+ ensure
181
+ Readline.completion_proc = cp
172
182
  end
173
183
 
174
184
  private
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: natty-ui
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Blumtritt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-11-11 00:00:00.000000000 Z
11
+ date: 2023-11-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  This is the beautiful, nice, nifty, fancy, neat, pretty, cool, lovely,
@@ -25,25 +25,28 @@ extra_rdoc_files:
25
25
  files:
26
26
  - LICENSE
27
27
  - README.md
28
+ - examples/attributes.rb
28
29
  - examples/basic.rb
29
- - examples/colors.rb
30
- - examples/illustration.svg
30
+ - examples/illustration.png
31
+ - examples/list_in_columns.rb
31
32
  - examples/progress.rb
32
33
  - examples/query.rb
33
34
  - lib/natty-ui.rb
34
35
  - lib/natty-ui/ansi.rb
35
36
  - lib/natty-ui/ansi_wrapper.rb
37
+ - lib/natty-ui/features.rb
38
+ - lib/natty-ui/mixins.rb
36
39
  - lib/natty-ui/version.rb
37
40
  - lib/natty-ui/wrapper.rb
38
41
  - lib/natty-ui/wrapper/ask.rb
39
42
  - lib/natty-ui/wrapper/element.rb
40
- - lib/natty-ui/wrapper/features.rb
41
43
  - lib/natty-ui/wrapper/framed.rb
42
44
  - lib/natty-ui/wrapper/heading.rb
45
+ - lib/natty-ui/wrapper/list_in_columns.rb
43
46
  - lib/natty-ui/wrapper/message.rb
44
- - lib/natty-ui/wrapper/mixins.rb
45
47
  - lib/natty-ui/wrapper/progress.rb
46
48
  - lib/natty-ui/wrapper/query.rb
49
+ - lib/natty-ui/wrapper/request.rb
47
50
  - lib/natty-ui/wrapper/section.rb
48
51
  - lib/natty-ui/wrapper/task.rb
49
52
  homepage: https://github.com/mblumtritt/natty-ui
@@ -69,7 +72,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
69
72
  - !ruby/object:Gem::Version
70
73
  version: '0'
71
74
  requirements: []
72
- rubygems_version: 3.4.21
75
+ rubygems_version: 3.4.22
73
76
  signing_key:
74
77
  specification_version: 4
75
78
  summary: This is the beautiful, nice, nifty, fancy, neat, pretty, cool, lovely, natty
data/examples/colors.rb DELETED
@@ -1,7 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # require 'natty-ui'
4
- require_relative '../lib/natty-ui'
5
-
6
- 256.times { |i| print NattyUI::Ansi.embellish("color #{i} ", i) }
7
- puts