natty-ui 0.5.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.
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'features'
4
+
5
+ module NattyUI
6
+ class Wrapper
7
+ #
8
+ # Basic visual element implementing all {Features}.
9
+ #
10
+ class Element
11
+ include Features
12
+
13
+ # @return [Section] when embedded in a section
14
+ # @return [Wrapper] when not embedded in a section
15
+ attr_reader :parent
16
+
17
+ # @return [Symbol] close status when closed
18
+ # @return [nil] when not closed
19
+ attr_reader :status
20
+
21
+ # @attribute [r] closed?
22
+ # @return [Boolean] whether its closed or not
23
+ def closed? = (@status != nil)
24
+
25
+ # Close the element.
26
+ #
27
+ # @return [Element] itself when used without a code block
28
+ # @return [nil] when used with a code block
29
+ def close = _close(:closed)
30
+
31
+ alias _to_s to_s
32
+ private :_to_s
33
+
34
+ # @!visibility private
35
+ def inspect
36
+ "#{_to_s[..-2]} status=#{@status}}}>"
37
+ end
38
+
39
+ protected
40
+
41
+ def prefix = "#{@parent.__send__(:prefix)}#{@prefix}"
42
+ def suffix = "#{@parent.__send__(:suffix)}#{@suffix}"
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)
53
+ @parent = parent
54
+ end
55
+
56
+ def _close(state)
57
+ return self if @status
58
+ @status = state
59
+ finish
60
+ @raise ? raise(BREAK) : self
61
+ end
62
+
63
+ def _call
64
+ @raise = true
65
+ yield(self)
66
+ close unless closed?
67
+ rescue BREAK
68
+ nil
69
+ end
70
+
71
+ BREAK = Class.new(StandardError)
72
+ private_constant :BREAK
73
+
74
+ private_class_method :new
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ #
5
+ # Mix-in for output functionality.
6
+ #
7
+ module Features
8
+ protected
9
+
10
+ def _element(type, *args)
11
+ wrapper.class.const_get(type).__send__(:new, self).__send__(:_call, *args)
12
+ end
13
+
14
+ def _section(type, args, **opts, &block)
15
+ __section(self, type, args, **opts, &block)
16
+ end
17
+
18
+ def __section(owner, type, args, **opts, &block)
19
+ sec = wrapper.class.const_get(type).__send__(:new, owner, **opts)
20
+ sec.puts(*args) if args && !args.empty?
21
+ block ? sec.__send__(:_call, &block) : sec
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'section'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Creates frame-enclosed section with a highlighted `title` and
8
+ # prints given additional arguments as lines into the section.
9
+ #
10
+ # When no block is given, the section must be closed, see {Section#close}.
11
+ #
12
+ # @param [#to_s] title object to print as section title
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`
16
+ # @yieldparam [Wrapper::Framed] section the created section
17
+ # @return [Object] the result of the code block
18
+ # @return [Wrapper::Framed] itself, when no code block is given
19
+ def framed(title, *args, type: :rounded, &block)
20
+ _section(:Framed, args, title: title, type: type, &block)
21
+ end
22
+ end
23
+
24
+ class Wrapper
25
+ #
26
+ # A frame-enclosed {Section} with a highlighted title.
27
+ #
28
+ # @see Features#framed
29
+ class Framed < Section
30
+ protected
31
+
32
+ def initialize(parent, title:, type:, **opts)
33
+ top_start, top_suffix, left, bottom = components(type)
34
+ parent.puts(" #{title} ", prefix: top_start, suffix: top_suffix)
35
+ @bottom = bottom
36
+ super(parent, prefix: "#{left} ", **opts)
37
+ end
38
+
39
+ def finish = parent.puts(@bottom)
40
+
41
+ def components(type)
42
+ COMPONENTS[type] || raise(ArgumentError, "invalid frame type - #{type}")
43
+ end
44
+
45
+ COMPONENTS = {
46
+ rounded: %w[╭── ───── │ ╰──────────],
47
+ simple: %w[┌── ───── │ └──────────],
48
+ heavy: %w[┏━━ ━━━━━ ┃ ┗━━━━━━━━━━],
49
+ semi: %w[┍━━ ━━━━━ │ ┕━━━━━━━━━━],
50
+ double: %w[╔══ ═════ ║ ╚══════════]
51
+ }.compare_by_identity.freeze
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'section'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Creates section with a H1 title.
8
+ #
9
+ # @param (see #information)
10
+ # @yieldparam [Wrapper::Heading] section the created section
11
+ # @return [Object] the result of the code block
12
+ # @return [Wrapper::Heading] itself, when no code block is given
13
+ def h1(title, *args, &block)
14
+ _section(:Heading, args, title: title, weight: 1, &block)
15
+ end
16
+
17
+ # Creates section with a H2 title.
18
+ #
19
+ # @param (see #information)
20
+ # @yieldparam (see #h1)
21
+ # @return (see #h1)
22
+ def h2(title, *args, &block)
23
+ _section(:Heading, args, title: title, weight: 2, &block)
24
+ end
25
+
26
+ # Creates section with a H3 title.
27
+ #
28
+ # @param (see #information)
29
+ # @yieldparam (see #h1)
30
+ # @return (see #h1)
31
+ def h3(title, *args, &block)
32
+ _section(:Heading, args, title: title, weight: 3, &block)
33
+ end
34
+
35
+ # Creates section with a H4 title.
36
+ #
37
+ # @param (see #information)
38
+ # @yieldparam (see #h1)
39
+ # @return (see #h1)
40
+ def h4(title, *args, &block)
41
+ _section(:Heading, args, title: title, weight: 4, &block)
42
+ end
43
+
44
+ # Creates section with a H5 title.
45
+ #
46
+ # @param (see #information)
47
+ # @yieldparam (see #h1)
48
+ # @return (see #h1)
49
+ def h5(title, *args, &block)
50
+ _section(:Heading, args, title: title, weight: 5, &block)
51
+ end
52
+ end
53
+
54
+ class Wrapper
55
+ #
56
+ # A {Section} with a highlighted title.
57
+ #
58
+ # @see Features#h1
59
+ # @see Features#h2
60
+ # @see Features#h3
61
+ # @see Features#h4
62
+ # @see Features#h5
63
+ class Heading < Section
64
+ protected
65
+
66
+ def initialize(parent, title:, weight:, **opts)
67
+ prefix, suffix = enclose(weight)
68
+ parent.puts(title, prefix: prefix, suffix: suffix)
69
+ super(parent, **opts)
70
+ end
71
+
72
+ def enclose(weight)
73
+ enclose = ENCLOSE[weight]
74
+ return "#{enclose} ", " #{enclose}" if enclose
75
+ raise(ArgumentError, "invalid heading weight - #{weight}")
76
+ end
77
+
78
+ ENCLOSE = {
79
+ 1 => '═══════',
80
+ 2 => '━━━━━',
81
+ 3 => '━━━',
82
+ 4 => '───',
83
+ 5 => '──'
84
+ }.compare_by_identity.freeze
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'section'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Creates a simple message section with a highlighted `title` and
8
+ # prints given additional arguments as lines into the section.
9
+ #
10
+ # @param [#to_s] title object to print as section title
11
+ # @param [Array<#to_s>] args more objects to print
12
+ # @param [#to_s] symbol symbol/prefix used for the title
13
+ # @yieldparam [Wrapper::Message] section the created section
14
+ # @return [Object] the result of the code block
15
+ # @return [Wrapper::Message] itself, when no code block is given
16
+ def message(title, *args, symbol: :default, &block)
17
+ _section(:Message, args, title: title, symbol: symbol, &block)
18
+ end
19
+ alias msg message
20
+
21
+ # Creates a informational message section with a highlighted `title` and
22
+ # prints given additional arguments as lines into the section.
23
+ #
24
+ # @param [#to_s] title object to print as section title
25
+ # @param [Array<#to_s>] args more objects to print
26
+ # @yieldparam (see #message)
27
+ # @return (see #message)
28
+ def information(title, *args, &block)
29
+ _section(:Message, args, title: title, symbol: :information, &block)
30
+ end
31
+ alias info information
32
+
33
+ # Creates a warning message section with a highlighted `title` and
34
+ # prints given additional arguments as lines into the section.
35
+ #
36
+ # @param (see #information)
37
+ # @yieldparam (see #message)
38
+ # @return (see #message)
39
+ def warning(title, *args, &block)
40
+ _section(:Message, args, title: title, symbol: :warning, &block)
41
+ end
42
+ alias warn warning
43
+
44
+ # Creates a error message section with a highlighted `title` and
45
+ # prints given additional arguments as lines into the section.
46
+ #
47
+ # @param (see #information)
48
+ # @yieldparam (see #message)
49
+ # @return (see #message)
50
+ def error(title, *args, &block)
51
+ _section(:Message, args, title: title, symbol: :error, &block)
52
+ end
53
+ alias err error
54
+
55
+ # Creates a completion message section with a highlighted `title` and
56
+ # prints given additional arguments as lines into the section.
57
+ #
58
+ # When used for a {#task} section it closes this section with status `:ok`.
59
+ #
60
+ # @param (see #information)
61
+ # @yieldparam (see #message)
62
+ # @return (see #message)
63
+ def completed(title, *args, &block)
64
+ _section(:Message, args, title: title, symbol: :completed, &block)
65
+ end
66
+ alias done completed
67
+ alias ok completed
68
+
69
+ # Creates a failure message section with a highlighted `title` and
70
+ # prints given additional arguments as lines into the section.
71
+ #
72
+ # When used for a {#task} section it closes this section with status
73
+ # `:failed`.
74
+ #
75
+ # @param (see #information)
76
+ # @yieldparam (see #message)
77
+ # @return (see #message)
78
+ def failed(title, *args, &block)
79
+ _section(:Message, args, title: title, symbol: :failed, &block)
80
+ end
81
+ end
82
+
83
+ class Wrapper
84
+ #
85
+ # A {Section} with a highlighted title.
86
+ #
87
+ # @see Features#message
88
+ # @see Features#information
89
+ # @see Features#warning
90
+ # @see Features#error
91
+ # @see Features#completed
92
+ # @see Features#failed
93
+ class Message < Section
94
+ protected
95
+
96
+ def initialize(parent, title:, symbol:, **opts)
97
+ parent.puts(title, **title_attr(str = as_symbol_str(symbol), symbol))
98
+ super(parent, prefix: ' ' * (NattyUI.display_width(str) + 1), **opts)
99
+ end
100
+
101
+ def title_attr(str, _symbol) = { prefix: "#{str} " }
102
+ def as_symbol_str(symbol) = (SYMBOL[symbol] || symbol)
103
+
104
+ SYMBOL = {
105
+ default: '•',
106
+ information: 'i',
107
+ warning: '!',
108
+ error: 'X',
109
+ completed: '✓',
110
+ failed: 'F',
111
+ query: '▶︎',
112
+ task: '➔'
113
+ }.compare_by_identity.freeze
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NattyUI
4
+ #
5
+ # Additional attributes for progression elements.
6
+ #
7
+ # Progression elements have additional states ({#completed?}, {#failed?}) and
8
+ # can be closed with calling {Features#completed} or {Features#failed}.
9
+ #
10
+ # @see Wrapper::Progress
11
+ # @see Wrapper::Task
12
+ #
13
+ module ProgressAttributes
14
+ # @attribute [r] completed?
15
+ # @return [Boolean] whether the task completed sucessfully
16
+ def completed? = (@status == :completed)
17
+
18
+ # @attribute [r] failed?
19
+ # @return [Boolean] whether the task failed
20
+ def failed? = (@status == :failed)
21
+
22
+ # @!visibility private
23
+ def completed(*args)
24
+ @final_text = args unless args.empty?
25
+ _close(:completed)
26
+ end
27
+ alias done completed
28
+ alias ok completed
29
+
30
+ # @!visibility private
31
+ def failed(*args)
32
+ @final_text = args unless args.empty?
33
+ _close(:failed)
34
+ end
35
+ end
36
+
37
+ #
38
+ # Additional attributes for progression elements.
39
+ #
40
+ # @see Wrapper::Progress
41
+ #
42
+ module ValueAttributes
43
+ # @return [Float] current value
44
+ attr_reader :value
45
+
46
+ def value=(val)
47
+ @value = [0, val.to_f].max
48
+ @max_value = @value if @max_value&.< 0
49
+ redraw
50
+ end
51
+
52
+ # Maximal value.
53
+ #
54
+ # @return [Float] maximal value
55
+ # @return [nil] when no max_value was configured
56
+ attr_reader :max_value
57
+
58
+ # Increase the value by given amount.
59
+ #
60
+ # @param increment [#to_f] value increment
61
+ # @return [Wrapper::Element] itself
62
+ def step(increment = 1)
63
+ self.value = @value + increment.to_f
64
+ self
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+ require_relative 'mixins'
5
+
6
+ module NattyUI
7
+ module Features
8
+ # Creates progress element implementing additional {ProgressAttributes}.
9
+ #
10
+ # A progress element has additional states and can be closed with {#completed}
11
+ # or {#failed}.
12
+ #
13
+ # @param [#to_s] title object to print as progress title
14
+ # @param [##to_f] max_value maximum value of the progress
15
+ # @return [Wrapper::Progress] the created progress element
16
+ def progress(title, max_value: nil)
17
+ _section(:Progress, nil, title: title, max_value: max_value)
18
+ end
19
+ end
20
+
21
+ class Wrapper
22
+ #
23
+ # An {Element} displaying a progression.
24
+ #
25
+ # @see Features#progress
26
+ class Progress < Element
27
+ include ProgressAttributes
28
+ include ValueAttributes
29
+
30
+ protected
31
+
32
+ def initialize(parent, title:, max_value:, **_)
33
+ super(parent)
34
+ @final_text = [title]
35
+ @max_value = [0, max_value.to_f].max if max_value
36
+ @value = 0
37
+ @progress = 0
38
+ draw_title(title)
39
+ end
40
+
41
+ def draw_title(title) = (wrapper.stream << prefix << "➔ #{title} ").flush
42
+ def draw_final = (wrapper.stream << "\n")
43
+
44
+ def redraw
45
+ return (wrapper.stream << '.').flush unless @max_value
46
+ cn = (20 * @value / @max_value).to_i
47
+ return if @progress == cn
48
+ (wrapper.stream << ('.' * (cn - @progress))).flush
49
+ @progress = cn
50
+ end
51
+
52
+ def finish
53
+ draw_final
54
+ return @parent.failed(*@final_text) if failed?
55
+ @status = :ok if @status == :closed
56
+ @parent.completed(*@final_text)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Request a choice from user.
8
+ #
9
+ # @example Select by Index
10
+ # choice = sec.query(
11
+ # 'Which fruits do you prefer?',
12
+ # 'Apples',
13
+ # 'Bananas',
14
+ # 'Cherries'
15
+ # )
16
+ # # => '1' or '2' or '3' or nil if user aborted
17
+ #
18
+ # @example Select by given char
19
+ # choice = sec.query(
20
+ # 'Which fruits do you prefer?',
21
+ # a: 'Apples',
22
+ # b: 'Bananas',
23
+ # c: 'Cherries'
24
+ # )
25
+ # # => 'a' or 'b' or 'c' or nil if user aborted
26
+ #
27
+ # @param question [#to_s] Question to display
28
+ # @param choices [#to_s] choices selectable via index (0..9)
29
+ # @param result [Symbol] defines how the result ist returned
30
+ # @param kw_choices [{Char => #to_s}] choices selectable with given char
31
+ # @return [Char] when `result` is configured as `:char`
32
+ # @return [#to_s] when `result` is configured as `:choice`
33
+ # @return [[Char, #to_s]] when `result` is configured as `:both`
34
+ # @return [nil] when input was aborted with `ESC`, `^C` or `^D`
35
+ def query(question, *choices, result: :char, **kw_choices)
36
+ _element(:Query, question, choices, kw_choices, result)
37
+ end
38
+ end
39
+
40
+ class Wrapper
41
+ #
42
+ # An {Element} to request a user choice.
43
+ #
44
+ # @see Features#query
45
+ class Query < Element
46
+ protected
47
+
48
+ def _call(question, choices, kw_choices, result_typye)
49
+ choices = grab(choices, kw_choices)
50
+ return if choices.empty?
51
+ wrapper.temporary do
52
+ __section(
53
+ @parent,
54
+ :Message,
55
+ choices.map { |k, v| "#{k} #{v}" },
56
+ title: question,
57
+ symbol: :query
58
+ )
59
+ read(choices, result_typye)
60
+ end
61
+ end
62
+
63
+ def read(choices, result_typye)
64
+ while true
65
+ char = NattyUI.in_stream.getch
66
+ return if "\3\4\e".include?(char)
67
+ next unless choices.key?(char)
68
+ return char if result_typye == :char
69
+ return choices[char] if result_typye == :choice
70
+ return char, choices[char]
71
+ end
72
+ end
73
+
74
+ def grab(choices, kw_choices)
75
+ Array
76
+ .new(choices.size) { |i| i + 1 }
77
+ .zip(choices)
78
+ .to_h
79
+ .merge!(kw_choices)
80
+ .transform_keys! { |k| [k.to_s[0], ' '].max }
81
+ .transform_values! { |v| v.to_s.tr("\r\n", ' ') }
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'element'
4
+
5
+ module NattyUI
6
+ module Features
7
+ # Creates a default section and prints given arguments as lines
8
+ # into the section.
9
+ #
10
+ # @param [Array<#to_s>] args objects to print
11
+ # @yieldparam [Wrapper::Section] section the created section
12
+ # @return [Object] the result of the code block
13
+ # @return [Wrapper::Section] itself, when no code block is given
14
+ def section(*args, &block)
15
+ _section(:Section, args, prefix: ' ', suffix: ' ', &block)
16
+ end
17
+ alias sec section
18
+
19
+ # Creates a quotation section and prints given arguments as lines
20
+ # into the section.
21
+ #
22
+ # @param (see #section)
23
+ # @yieldparam (see #section)
24
+ # @return (see #section)
25
+ def quote(*args, &block)
26
+ _section(:Section, args, prefix: '▍ ', prefix_attr: 39, &block)
27
+ end
28
+ end
29
+
30
+ class Wrapper
31
+ #
32
+ # Visual element to keep text lines together.
33
+ #
34
+ # A section can contain other elements and sections.
35
+ #
36
+ # @see Features#section
37
+ # @see Features#quote
38
+ class Section < Element
39
+ # Close the section.
40
+ #
41
+ # @return [Section] itself when used without a code block
42
+ # @return [nil] when used with a code block
43
+ def close = _close(:closed)
44
+
45
+ # Print given arguments as lines into the section.
46
+ #
47
+ # @overload puts(...)
48
+ # @param [#to_s] ... objects to print
49
+ # @comment @param [#to_s, nil] prefix line prefix
50
+ # @comment @param [#to_s, nil] suffix line suffix
51
+ # @return [Section] itself
52
+ def puts(*args, prefix: nil, suffix: nil)
53
+ return self if @status
54
+ @parent.puts(
55
+ *args,
56
+ prefix: prefix ? "#{@prefix}#{prefix}" : @prefix,
57
+ suffix: suffix ? "#{@suffix}#{suffix}" : @suffix
58
+ )
59
+ self
60
+ end
61
+ alias add puts
62
+
63
+ # Add at least one empty line
64
+ #
65
+ # @param [#to_i] lines count of lines
66
+ # @return [Section] itself
67
+ def space(lines = 1)
68
+ @parent.puts(
69
+ *Array.new([lines.to_i, 1].max),
70
+ prefix: @prefix,
71
+ suffix: @suffix
72
+ )
73
+ self
74
+ end
75
+
76
+ # @note The screen manipulation is only available in ANSI mode see {#ansi?}
77
+ #
78
+ # Resets the part of the screen written below the current output line when
79
+ # the given block ended.
80
+ #
81
+ # @example
82
+ # section.temporary do |temp|
83
+ # temp.info('This message will disappear in 5 seconds!')
84
+ # sleep 5
85
+ # end
86
+ #
87
+ # @yield [Section] itself
88
+ # @return [Object] block result
89
+ def temporary
90
+ block_given? ? yield(self) : self
91
+ end
92
+
93
+ protected
94
+
95
+ def initialize(parent, prefix: nil, suffix: nil, **_)
96
+ super(parent)
97
+ @prefix = prefix
98
+ @suffix = suffix
99
+ end
100
+ end
101
+ end
102
+ end