natty-ui 0.29.0 → 0.31.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.
@@ -37,52 +37,61 @@ module NattyUI
37
37
  # @return [Features]
38
38
  # itself
39
39
  def puts(*text, **options)
40
- bbcode = true if (bbcode = options[:bbcode]).nil?
41
-
42
- max_width = options[:max_width] || Terminal.columns
43
- return self if max_width == 0
44
- if max_width < 1
45
- if max_width > 0
46
- max_width *= Terminal.columns
47
- elsif max_width < 0
48
- max_width += Terminal.columns
49
- end
50
- end
40
+ if options.empty?
41
+ bbcode = true
42
+ max_width = Terminal.columns
43
+ else
44
+ bbcode = true if (bbcode = options[:bbcode]).nil?
45
+ ignore_newline = options[:eol] == false || options[:ignore_newline]
51
46
 
52
- prefix_width =
53
- if (prefix = options[:prefix])
54
- prefix = Ansi.bbcode(prefix) if bbcode
55
- options[:prefix_width] || Text.width(prefix, bbcode: false)
56
- else
57
- 0
47
+ if (max_width = options[:max_width]).nil?
48
+ return self if (max_width = Terminal.columns).zero?
49
+ elsif max_width < 1
50
+ if max_width > 0
51
+ max_width *= Terminal.columns
52
+ elsif max_width < 0
53
+ max_width += Terminal.columns
54
+ else
55
+ return self
56
+ end
58
57
  end
59
58
 
60
- if (first_line = options[:first_line_prefix])
61
- first_line = Ansi.bbcode(first_line) if bbcode
62
- first_line_width =
63
- options[:first_line_prefix_width] ||
64
- Text.width(first_line, bbcode: false)
59
+ prefix_width =
60
+ if (prefix = options[:prefix])
61
+ prefix = Ansi.bbcode(prefix) if bbcode
62
+ options[:prefix_width] || Text.width(prefix, bbcode: false)
63
+ else
64
+ 0
65
+ end
65
66
 
66
- if prefix_width < first_line_width
67
- prefix_next = "#{prefix}#{' ' * (first_line_width - prefix_width)}"
68
- prefix = first_line
69
- prefix_width = first_line_width
70
- else
71
- prefix_next = prefix
72
- prefix =
73
- if first_line_width < prefix_width
74
- first_line + (' ' * (prefix_width - first_line_width))
75
- else
76
- first_line
77
- end
67
+ if (first_line = options[:first_line_prefix])
68
+ first_line = Ansi.bbcode(first_line) if bbcode
69
+ first_line_width =
70
+ options[:first_line_prefix_width] ||
71
+ Text.width(first_line, bbcode: false)
72
+
73
+ if prefix_width < first_line_width
74
+ prefix_next = "#{prefix}#{' ' * (first_line_width - prefix_width)}"
75
+ prefix = first_line
76
+ prefix_width = first_line_width
77
+ else
78
+ prefix_next = prefix
79
+ prefix =
80
+ if first_line_width < prefix_width
81
+ first_line + (' ' * (prefix_width - first_line_width))
82
+ else
83
+ first_line
84
+ end
85
+ end
78
86
  end
79
- end
80
87
 
81
- max_width -= prefix_width
88
+ max_width -= prefix_width
82
89
 
83
- if (suffix = options[:suffix])
84
- suffix = Ansi.bbcode(suffix) if bbcode
85
- max_width -= options[:suffix_width] || Text.width(suffix, bbcode: false)
90
+ if (suffix = options[:suffix])
91
+ suffix = Ansi.bbcode(suffix) if bbcode
92
+ max_width -=
93
+ options[:suffix_width] || Text.width(suffix, bbcode: false)
94
+ end
86
95
  end
87
96
 
88
97
  return self if max_width <= 0
@@ -93,12 +102,14 @@ module NattyUI
93
102
  limit: max_width,
94
103
  bbcode: bbcode,
95
104
  ansi: Terminal.ansi?,
96
- ignore_newline: options[:eol] == false || options[:ignore_newline]
105
+ ignore_newline: ignore_newline
97
106
  )
98
107
 
108
+ @__eol ||= Terminal.ansi? ? "\e[m\n" : "\n"
109
+
99
110
  if (align = options[:align]).nil?
100
111
  lines.each do |line|
101
- Terminal.print(prefix, line, suffix, __eol, bbcode: false)
112
+ Terminal.print(prefix, line, suffix, @__eol, bbcode: false)
102
113
  @lines_written += 1
103
114
  prefix, prefix_next = prefix_next, nil if prefix_next
104
115
  end
@@ -118,7 +129,7 @@ module NattyUI
118
129
  ' ' * (max_width - width),
119
130
  line,
120
131
  suffix,
121
- __eol,
132
+ @__eol,
122
133
  bbcode: false
123
134
  )
124
135
  @lines_written += 1
@@ -133,7 +144,7 @@ module NattyUI
133
144
  line,
134
145
  ' ' * (space - lw),
135
146
  suffix,
136
- __eol,
147
+ @__eol,
137
148
  bbcode: false
138
149
  )
139
150
  @lines_written += 1
@@ -146,7 +157,7 @@ module NattyUI
146
157
  line,
147
158
  ' ' * (max_width - width),
148
159
  suffix,
149
- __eol,
160
+ @__eol,
150
161
  bbcode: false
151
162
  )
152
163
  @lines_written += 1
@@ -301,7 +312,7 @@ module NattyUI
301
312
  #
302
313
  # @return (see puts)
303
314
  def space(count = 1)
304
- puts("\n" * count) if (count = count.to_i).positive?
315
+ (count = count.to_i).positive? ? puts("\n" * count) : self
305
316
  end
306
317
 
307
318
  # Print given items as list (like 'ls' command).
@@ -474,10 +485,11 @@ module NattyUI
474
485
  # ui.hbars 1..10, style: :blue, width: 0.5
475
486
  #
476
487
  # @param values [#to_a, Array<Numeric>] values to print
477
- # @param with_values [true, false] whether the values should be printed too
488
+ # @param min [#to_f] start value
489
+ # @param max [#to_f] end value
478
490
  # @param normalize [true, false] whether the values should be normalized
479
- # @param height [Integer] output height
480
- # @param bar_width [:auto, :min, Integer] with of each bar
491
+ # @param text [true, false] whether the values should be printed too
492
+ # @param width [:auto, :min, Integer] with of each bar
481
493
  # @param style [Symbol, Array<Symbol>, nil] bar drawing style
482
494
  # @param text_style [Symbol, Array<Symbol>, nil] text style
483
495
  #
@@ -486,8 +498,10 @@ module NattyUI
486
498
  # @return (see puts)
487
499
  def hbars(
488
500
  values,
489
- with_values: true,
501
+ min: nil,
502
+ max: nil,
490
503
  normalize: false,
504
+ text: true,
491
505
  width: :auto,
492
506
  style: nil,
493
507
  text_style: nil
@@ -497,12 +511,9 @@ module NattyUI
497
511
  raise(ArgumentError, 'values can not be negative')
498
512
  end
499
513
  style = text_style = nil unless Terminal.ansi?
500
- size = Utils.as_size(3..columns, width)
501
- if with_values
502
- puts(*HBarsRenderer.lines(values, size, normalize, style, text_style))
503
- else
504
- puts(*HBarsRenderer.lines_bars_only(values, size, normalize, style))
505
- end
514
+ renderer = HBarsRenderer.new(values, min, max)
515
+ renderer.with_text(text_style) if text
516
+ puts(*renderer.lines(Utils.as_size(3..columns, width), style, normalize))
506
517
  end
507
518
 
508
519
  # Dynamically display a task progress.
@@ -567,6 +578,31 @@ module NattyUI
567
578
  block ? __with(progress, &block) : progress
568
579
  end
569
580
 
581
+ # Run a shell program
582
+ #
583
+ # @return [Process::Status] when command was executed
584
+ # @return [nil] in error case (like command not found)
585
+ def sh(*cmd, shell: false, input: nil)
586
+ m = (theme = Theme.current).mark(:sh_out)
587
+ opts_out = {
588
+ bbcode: false,
589
+ prefix: "#{m}#{theme.sh_out_style}",
590
+ prefix_width: m.width
591
+ }
592
+ m = theme.mark(:sh_err)
593
+ opts_err = {
594
+ bbcode: false,
595
+ prefix: "#{m}#{theme.sh_err_style}",
596
+ prefix_width: m.width
597
+ }
598
+ ShellCommand.call(*cmd, shell: shell, input: input) do |line, kind|
599
+ puts(
600
+ line.gsub("\t", '    ').gsub(/[[:space:]]/, ' '),
601
+ **(kind == :error ? opts_err : opts_out)
602
+ )
603
+ end
604
+ end
605
+
570
606
  #
571
607
  # @!endgroup
572
608
  #
@@ -945,43 +981,45 @@ module NattyUI
945
981
  end
946
982
  end
947
983
  end
948
-
949
- def __eol
950
- @__eol ||= Terminal.ansi? ? "\e[m\n" : "\n"
951
- end
952
984
  end
953
985
 
954
986
  dir = __dir__
955
- autoload :Choice, "#{dir}/choice.rb"
956
- autoload :DumbChoice, "#{dir}/dumb_choice.rb"
957
- autoload :DumbOptions, "#{dir}/dumb_options.rb"
958
- autoload :CompactLSRenderer, "#{dir}/ls_renderer.rb"
959
987
  autoload :Framed, "#{dir}/framed.rb"
960
- autoload :HBarsRenderer, "#{dir}/hbars_renderer.rb"
961
- autoload :LSRenderer, "#{dir}/ls_renderer.rb"
962
- autoload :Options, "#{dir}/options.rb"
963
- autoload :Progress, "#{dir}/progress.rb"
964
- autoload :DumbProgress, "#{dir}/progress.rb"
965
988
  autoload :Section, "#{dir}/section.rb"
989
+ autoload :ShellCommand, "#{dir}/shell_command.rb"
966
990
  autoload :Table, "#{dir}/table.rb"
967
991
  autoload :Task, "#{dir}/task.rb"
968
992
  autoload :Temporary, "#{dir}/temporary.rb"
969
993
  autoload :Theme, "#{dir}/theme.rb"
970
994
  autoload :Utils, "#{dir}/utils.rb"
995
+
996
+ autoload :Choice, "#{dir}/choice.rb"
997
+ autoload :DumbChoice, "#{dir}/dumb_choice.rb"
998
+ autoload :Options, "#{dir}/options.rb"
999
+ autoload :DumbOptions, "#{dir}/dumb_options.rb"
1000
+ autoload :Progress, "#{dir}/progress.rb"
1001
+ autoload :DumbProgress, "#{dir}/progress.rb"
1002
+
1003
+ autoload :CompactLSRenderer, "#{dir}/ls_renderer.rb"
1004
+ autoload :HBarsRenderer, "#{dir}/hbars_renderer.rb"
1005
+ autoload :LSRenderer, "#{dir}/ls_renderer.rb"
971
1006
  autoload :VBarsRenderer, "#{dir}/vbars_renderer.rb"
972
1007
 
973
1008
  private_constant(
1009
+ :Framed,
1010
+ :ShellCommand,
1011
+ :Utils,
1012
+ # -
974
1013
  :Choice,
975
1014
  :DumbChoice,
1015
+ :Options,
976
1016
  :DumbOptions,
1017
+ :Progress,
1018
+ :DumbProgress,
1019
+ # -
977
1020
  :CompactLSRenderer,
978
- :Framed,
979
1021
  :HBarsRenderer,
980
1022
  :LSRenderer,
981
- :Options,
982
- :Progress,
983
- :DumbProgress,
984
- :Utils,
985
1023
  :VBarsRenderer
986
1024
  )
987
1025
  end
@@ -3,54 +3,62 @@
3
3
  require_relative 'utils'
4
4
 
5
5
  module NattyUI
6
- module HBarsRenderer
7
- class << self
8
- def lines(vals, width, normalize, style, text_style)
9
- if text_style
10
- bots = Ansi[*text_style]
11
- eots = Ansi::RESET
12
- end
13
- texts = vals.map { Str.new(_1) }
14
- tw = texts.max_by(&:width).width
15
- size = width - tw - 1
16
- if size < 2 # text only
17
- return texts.map { "#{bots}#{' ' * (tw - _1.width)}#{_1}#{eots}" }
18
- end
19
- if style
20
- bos = Ansi[*style]
21
- eos = Ansi::RESET
22
- end
23
- vals = normalize ? normalize!(vals, size) : adjust!(vals, size)
24
- texts.map.with_index do |str, idx|
25
- "#{bots}#{' ' * (tw - str.width)}#{str}#{eots}#{bos}▕#{
26
- '▆' * vals[idx]
27
- }#{eos}"
28
- end
29
- end
6
+ class HBarsRenderer
7
+ def initialize(values, min, max)
8
+ @values = values
9
+ @min = min
10
+ @max = max
11
+ end
12
+
13
+ def with_text(style)
14
+ @texts = @values.map { Str.new(_1) }
15
+ @texts_width = @texts.max_by(&:width).width
16
+ @texts.map!(&format_text(style, @texts_width))
17
+ self
18
+ end
30
19
 
31
- def lines_bars_only(vals, width, normalize, style)
32
- if style
33
- bos = Ansi[*style]
34
- eos = Ansi::RESET
35
- end
36
- width -= 1
37
- vals = normalize ? normalize!(vals, width) : adjust!(vals, width)
38
- vals.map { "#{bos}▕#{'▆' * _1}#{eos}" }
20
+ def lines(width, style, normalize)
21
+ if @texts
22
+ width -= @texts_width
23
+ return @texts if width < 3
39
24
  end
25
+ width -= 1
26
+ fmt = format(style)
27
+ vals = normalize ? normalized(width, &fmt) : adjusted(width, &fmt)
28
+ vals = @texts.zip(vals).map!(&:join) if @texts
29
+ vals
30
+ end
40
31
 
41
- private
32
+ private
42
33
 
43
- def adjust!(vals, size)
44
- max = vals.max.to_f
45
- vals.map { ((_1 / max) * size).round }
46
- end
34
+ def format_text(style, width)
35
+ return ->(l) { "#{' ' * (width - l.width)}#{l}" } unless style
36
+ bot = Ansi[*style]
37
+ eot = Ansi::RESET
38
+ ->(l) { "#{bot}#{' ' * (width - l.width)}#{l}#{eot}" }
39
+ end
47
40
 
48
- def normalize!(vals, size)
49
- min, max = vals.minmax
50
- return Array.new(vals.size, size) if min == max
51
- max = (max - min).to_f
52
- vals.map { (((_1 - min) / max) * size).round }
53
- end
41
+ def format(style)
42
+ return ->(l) { "▕#{'▆' * l}" } unless style
43
+ bos = Ansi[*style]
44
+ eos = Ansi::RESET
45
+ ->(l) { "#{bos}▕#{'▆' * l}#{eos}" }
46
+ end
47
+
48
+ def adjusted(size)
49
+ vals = @values.map(&:to_f)
50
+ max = vals.max
51
+ max = @max.to_f if @max&.>(max)
52
+ vals.map! { yield(((_1 / max) * size).round) }
53
+ end
54
+
55
+ def normalized(size)
56
+ vals = @values.map(&:to_f)
57
+ min, max = vals.minmax
58
+ min = @min.to_f if @min&.<(min)
59
+ max = @max.to_f if @max&.>(max)
60
+ max -= min
61
+ vals.map! { yield((((_1 - min) / max) * size).round) }
54
62
  end
55
63
  end
56
64
 
@@ -100,8 +100,10 @@ module NattyUI
100
100
  @pin_line = NattyUI.lines_written
101
101
  @style = Theme.current.task_style
102
102
  cm = Theme.current.mark(:current)
103
- @flp = "#{cm} #{@style}"
104
- @flpw = cm.width + 1
103
+ @redraw_opts = {
104
+ first_line_prefix: "#{cm} #{@style}",
105
+ first_line_prefix_width: cm.width + 1
106
+ }
105
107
  max ? self.max = max : redraw
106
108
  end
107
109
 
@@ -111,11 +113,7 @@ module NattyUI
111
113
  curr = bar ? [@title, bar] : [@title]
112
114
  return if @last == curr
113
115
  @pin_line = NattyUI.back_to_line(@pin_line) if @last
114
- @parent.puts(
115
- *curr,
116
- first_line_prefix: @flp,
117
- first_line_prefix_width: @flpw
118
- )
116
+ @parent.puts(*curr, **@redraw_opts)
119
117
  @last = curr
120
118
  end
121
119
 
@@ -0,0 +1,132 @@
1
+ module NattyUI
2
+ module ShellCommand
3
+ class << self
4
+ def call(*cmd, shell: false, input: nil, **options)
5
+ options = options.except(:in, :out, :err)
6
+ env = (cmd[0].is_a?(Hash) ? cmd.shift.dup : {}).freeze
7
+ if shell
8
+ cmd = cmd.map! { _escape(it) }.join(' ')
9
+ elsif cmd.size == 1 && cmd[0].include?(' ')
10
+ cmd = cmd[0]
11
+ end
12
+ cmd.freeze
13
+ input = Input.for(input)
14
+ ret = nil
15
+ with_io(options, input) do |cio, out_r, err_r, in_w|
16
+ thread = Process.detach(Process.spawn(env, *cmd, options))
17
+ begin
18
+ cio.each(&:close)
19
+ read = [out_r, err_r]
20
+ write = [in_w] if in_w
21
+ while !read.empty? || write
22
+ rr, wr, = IO.select(read, write)
23
+ if rr.include?(out_r)
24
+ begin
25
+ yield(out_r.readline(chomp: true), :output)
26
+ rescue SystemCallError, IOError
27
+ read.delete(out_r)
28
+ end
29
+ end
30
+ if rr.include?(err_r)
31
+ begin
32
+ yield(err_r.readline(chomp: true), :error)
33
+ rescue SystemCallError, IOError
34
+ read.delete(err_r)
35
+ end
36
+ end
37
+ next if wr.empty?
38
+ begin
39
+ next if input.call(in_w)
40
+ rescue SystemCallError, IOError
41
+ # nop
42
+ end
43
+ in_w.close
44
+ write = nil
45
+ end
46
+ ensure
47
+ ret = thread.join.value
48
+ end
49
+ end
50
+ ret
51
+ rescue SystemCallError, IOError
52
+ nil
53
+ end
54
+
55
+ private
56
+
57
+ def with_io(options, input)
58
+ IO.pipe do |out_r, out_w|
59
+ IO.pipe do |err_r, err_w|
60
+ cio = [options[:out] = out_w, options[:err] = err_w]
61
+ return yield(cio, out_r, err_r) unless input
62
+ IO.pipe do |in_r, in_w|
63
+ cio << (options[:in] = in_r)
64
+ in_w.sync = true
65
+ yield(cio, out_r, err_r, in_w)
66
+ end
67
+ end
68
+ end
69
+ end
70
+
71
+ def _escape(str)
72
+ return +"''" if str.empty?
73
+ str = str.dup
74
+ str.gsub!(%r{[^A-Za-z0-9_\-.,:+/@\n]}, "\\\\\\&")
75
+ str.gsub!("\n", "'\n'")
76
+ str
77
+ end
78
+ end
79
+
80
+ module Input
81
+ def self.for(obj)
82
+ return unless obj
83
+ return CopyWriter.new(obj) if obj.respond_to?(:readpartial)
84
+ return CopyWriter.new(obj.to_io) if obj.respond_to?(:to_io)
85
+ return ArrayWriter.new(obj) if obj.is_a?(Array)
86
+ return EnumerableWriter.new(obj) if obj.respond_to?(:each)
87
+ return ArrayWriter.new(obj.to_a) if obj.respond_to?(:to_a)
88
+ Writer.new(obj)
89
+ end
90
+
91
+ class Writer
92
+ def call(io) = (io.write(_next) if @obj)
93
+ def initialize(obj) = (@obj = obj)
94
+ def _next = (_, @obj = @obj, nil).first
95
+ end
96
+
97
+ class CopyWriter < Writer
98
+ def call(io) = (IO.copy_stream(_next, io) if @obj)
99
+ end
100
+
101
+ class ArrayWriter
102
+ def call(io) = io.write(@ry[@idx += 1] || return)
103
+
104
+ def initialize(ary)
105
+ @ry = ary
106
+ @idx = -1
107
+ end
108
+ end
109
+
110
+ class EnumerableWriter
111
+ def call(io)
112
+ io.write(@enum.next)
113
+ rescue StopIteration
114
+ false
115
+ end
116
+
117
+ def initialize(enum)
118
+ @enum =
119
+ if enum.respond_to?(:enum_for)
120
+ enum.enum_for(:each)
121
+ else
122
+ Enumerator.new { |y| enum.each { y << it } }
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ private_constant :Input
129
+ end
130
+
131
+ private_constant :ShellCommand
132
+ end
@@ -9,8 +9,8 @@ module NattyUI
9
9
  # Collection of rows and columns used by {Features.table}.
10
10
  #
11
11
  class Table
12
- class Column < NattyUI::Attributes::Base
13
- include NattyUI::Attributes::Width
12
+ class Column < NattyUI::Attributes
13
+ include Width
14
14
 
15
15
  # @return [Integer] column index
16
16
  attr_reader :index
@@ -22,6 +22,12 @@ module NattyUI
22
22
  def to_s = "#{super.chop} @index:#{@index} @width:#{width.inspect}>"
23
23
  alias inspect to_s
24
24
 
25
+ def assign(**attributes)
26
+ return self if attributes.empty?
27
+ @parent.each_cell_of(@index) { _1.attributes.merge!(**attributes) }
28
+ self
29
+ end
30
+
25
31
  private
26
32
 
27
33
  def find_width
@@ -137,6 +143,12 @@ module NattyUI
137
143
  self
138
144
  end
139
145
 
146
+ def assign(**attributes)
147
+ return self if attributes.empty?
148
+ @cells.each { _1.attributes.merge!(**attributes) }
149
+ self
150
+ end
151
+
140
152
  private
141
153
 
142
154
  def respond_to_missing?(name, _)
@@ -169,19 +181,42 @@ module NattyUI
169
181
  class Cell
170
182
  include TextWithAttributes
171
183
 
172
- class Attributes < NattyUI::Attributes::Base
173
- prepend NattyUI::Attributes::Width
174
- prepend NattyUI::Attributes::Padding
175
- prepend NattyUI::Attributes::Align
176
- prepend NattyUI::Attributes::Vertical
177
- prepend NattyUI::Attributes::Style
184
+ class Attributes < NattyUI::Attributes
185
+ prepend Width
186
+ prepend Padding
187
+ prepend Align
188
+ prepend Vertical
189
+ prepend Style
190
+
191
+ # Whether the text's line breaks are processed.
192
+ #
193
+ # @return [true, false]
194
+ attr_reader :eol
195
+
196
+ # @attribute [w] eol
197
+ def eol=(value)
198
+ @eol = value ? true : false
199
+ end
200
+
201
+ protected
202
+
203
+ def _assign(opt)
204
+ @eol = opt[:eol]
205
+ @eol = true if @eol.nil?
206
+ super
207
+ end
208
+
209
+ def _store(opt)
210
+ opt[:eol] = false unless @eol
211
+ super
212
+ end
178
213
  end
179
214
  end
180
215
 
181
- class Attributes < NattyUI::Attributes::Base
182
- prepend NattyUI::Attributes::Border
183
- prepend NattyUI::Attributes::BorderStyle
184
- prepend NattyUI::Attributes::Position
216
+ class Attributes < NattyUI::Attributes
217
+ prepend Border
218
+ prepend BorderStyle
219
+ prepend Position
185
220
 
186
221
  # Whether the table has a border around.
187
222
  #
@@ -115,12 +115,12 @@ module NattyUI
115
115
  @align = att.align
116
116
  @vertical = att.vertical
117
117
  @style = att.style_bbcode
118
- @text = width_corrected(cell.text, width)
118
+ @text = width_corrected(cell.text, width, att.eol == false)
119
119
  end
120
120
 
121
121
  private
122
122
 
123
- def width_corrected(text, width)
123
+ def width_corrected(text, width, ignore_newline)
124
124
  @width, @padding[3], @padding[1] =
125
125
  WidthFinder.adjust(width, @padding[3], @padding[1])
126
126
  @empty = @style ? "#{@style}#{' ' * width}[/]" : ' ' * width
@@ -130,6 +130,7 @@ module NattyUI
130
130
  *text,
131
131
  limit: @width,
132
132
  bbcode: true,
133
+ ignore_newline: ignore_newline,
133
134
  ansi: Terminal.ansi?
134
135
  ).map(&txt_fmt),
135
136
  Array.new(@padding[2], @empty)