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,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'section'
4
+ require_relative 'mixins'
5
+
6
+ module NattyUI
7
+ module Features
8
+ # Creates task section implementing additional {ProgressAttributes}.
9
+ #
10
+ # A task section has additional states and can be closed with {#completed}
11
+ # or {#failed}.
12
+ #
13
+ # @param (see #information)
14
+ # @yieldparam [Wrapper::Task] section the created section
15
+ # @return [Object] the result of the code block
16
+ # @return [Wrapper::Task] itself, when no code block is given
17
+ def task(title, *args, &block)
18
+ _section(:Task, args, title: title, &block)
19
+ end
20
+ end
21
+
22
+ module TaskMethods
23
+ protected
24
+
25
+ def initialize(parent, title:, **opts)
26
+ @parent = parent
27
+ @temp = wrapper.temporary
28
+ @final_text = [title]
29
+ super(parent, title: title, symbol: :task, **opts)
30
+ end
31
+
32
+ def finish
33
+ unless failed?
34
+ @status = :completed if @status == :closed
35
+ @temp.call
36
+ end
37
+ __section(
38
+ @parent,
39
+ :Message,
40
+ @final_text,
41
+ title: @final_text.shift,
42
+ symbol: @status
43
+ )
44
+ end
45
+ end
46
+ private_constant :TaskMethods
47
+
48
+ class Wrapper
49
+ #
50
+ # A {Message} container to visualize the progression of a task.
51
+ #
52
+ # @see Features.task
53
+ class Task < Message
54
+ include ProgressAttributes
55
+ include TaskMethods
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require_relative 'wrapper/ask'
5
+ require_relative 'wrapper/framed'
6
+ require_relative 'wrapper/heading'
7
+ require_relative 'wrapper/message'
8
+ require_relative 'wrapper/progress'
9
+ require_relative 'wrapper/query'
10
+ require_relative 'wrapper/section'
11
+ require_relative 'wrapper/task'
12
+
13
+ module NattyUI
14
+ #
15
+ # Helper class to wrap an output stream and implement all {Features}.
16
+ #
17
+ class Wrapper
18
+ include Features
19
+
20
+ # @return [IO] IO stream used for output
21
+ attr_reader :stream
22
+
23
+ # @attribute [r] ansi?
24
+ # @return [Boolean] whether ANSI is supported
25
+ def ansi? = false
26
+
27
+ # @attribute [r] screen_size
28
+ # @return [[Integer, Integer]] screen size as rows and columns
29
+ def screen_size
30
+ return @stream.winsize if @ws
31
+ [ENV['LINES'].to_i.nonzero? || 25, ENV['COLUMNS'].to_i.nonzero? || 80]
32
+ end
33
+
34
+ # @attribute [r] screen_rows
35
+ # @return [Integer] number of screen rows
36
+ def screen_rows
37
+ @ws ? @stream.winsize[0] : (ENV['LINES'].to_i.nonzero? || 25)
38
+ end
39
+
40
+ # @attribute [r] screen_columns
41
+ # @return [Integer] number of screen columns
42
+ def screen_columns
43
+ @ws ? @stream.winsize[-1] : (ENV['COLUMNS'].to_i.nonzero? || 80)
44
+ end
45
+
46
+ # @!group Tool functions
47
+
48
+ # Print given arguments as lines to the output stream.
49
+ #
50
+ # @overload puts(...)
51
+ # @param [#to_s] ... objects to print
52
+ # @comment @param [#to_s, nil] prefix line prefix
53
+ # @comment @param [#to_s, nil] suffix line suffix
54
+ # @return [Wrapper] itself
55
+ def puts(*args, prefix: nil, suffix: nil)
56
+ if args.empty?
57
+ @stream.puts(embellish("#{prefix}#{suffix}"))
58
+ @lines_written += 1
59
+ else
60
+ StringIO.open do |io|
61
+ io.puts(*args)
62
+ io.rewind
63
+ io.each(chomp: true) do |line|
64
+ @stream.puts(embellish("#{prefix}#{line}#{suffix}"))
65
+ @lines_written += 1
66
+ end
67
+ end
68
+ end
69
+ @stream.flush
70
+ self
71
+ end
72
+ alias add puts
73
+
74
+ # Add at least one empty line
75
+ #
76
+ # @param [#to_i] lines count of lines
77
+ # @return [Wrapper] itself
78
+ def space(lines = 1)
79
+ lines = [lines.to_i, 1].max
80
+ @stream.puts(*Array.new(lines))
81
+ @lines_written += lines
82
+ @stream.flush
83
+ self
84
+ end
85
+
86
+ # @note The screen manipulation is only available in ANSI mode see {#ansi?}
87
+ #
88
+ # Saves current screen, deletes all screen content and moves the cursor
89
+ # to the top left screen corner. It restores the screen after the block.
90
+ #
91
+ # @example
92
+ # UI.page do |page|
93
+ # page.info('This message will disappear in 5 seconds!')
94
+ # sleep 5
95
+ # end
96
+ #
97
+ # @yield [Wrapper] itself
98
+ # @return [Object] block result
99
+ def page
100
+ block_given? ? yield(self) : self
101
+ ensure
102
+ @stream.flush
103
+ end
104
+
105
+ # @note The screen manipulation is only available in ANSI mode see {#ansi?}
106
+ #
107
+ # Resets the part of the screen written below the current output line when
108
+ # the given block ended.
109
+ #
110
+ # @example
111
+ # UI.temporary do |temp|
112
+ # temp.info('This message will disappear in 5 seconds!')
113
+ # sleep 5
114
+ # end
115
+ #
116
+ # @overload temporary
117
+ # @return [Proc] a function to reset the screen
118
+ #
119
+ # @overload temporary
120
+ # @yield [Wrapper] itself
121
+ # @return [Object] block result
122
+ def temporary
123
+ func = temp_func
124
+ return func unless block_given?
125
+ begin
126
+ yield(self)
127
+ ensure
128
+ func.call
129
+ end
130
+ end
131
+
132
+ # @!endgroup
133
+
134
+ # @!visibility private
135
+ attr_reader :lines_written
136
+
137
+ # @!visibility private
138
+ alias inspect to_s
139
+
140
+ protected
141
+
142
+ def embellish(obj)
143
+ obj = NattyUI.plain(obj)
144
+ obj.empty? ? nil : obj
145
+ end
146
+
147
+ def temp_func
148
+ lambda do
149
+ @stream.flush
150
+ self
151
+ end
152
+ end
153
+
154
+ def initialize(stream)
155
+ @stream = stream
156
+ @lines_written = 0
157
+ @ws = stream.respond_to?(:winsize) && stream.winsize&.size == 2
158
+ rescue Errno::ENOTTY
159
+ @ws = false
160
+ end
161
+
162
+ def wrapper = self
163
+ def prefix = nil
164
+ alias suffix prefix
165
+
166
+ private_class_method :new
167
+ end
168
+ end
data/lib/natty-ui.rb ADDED
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'unicode/display_width'
4
+ require_relative 'natty-ui/wrapper'
5
+ require_relative 'natty-ui/ansi_wrapper'
6
+
7
+ #
8
+ # Module to create beautiful, nice, nifty, fancy, neat, pretty, cool, lovely,
9
+ # natty user interfaces for your CLI.
10
+ #
11
+ # It creates {Wrapper} instances which can optionally support ANSI. The UI
12
+ # consists of {Wrapper::Element}s and {Wrapper::Section}s for different
13
+ # {Features}.
14
+ #
15
+ module NattyUI
16
+ class << self
17
+ # @see .valid_in?
18
+ # @return [IO] IO stream used to read input
19
+ # @raise TypeError when a non-readable stream will be assigned
20
+ attr_reader :in_stream
21
+
22
+ # @param [IO] stream to read input
23
+ def in_stream=(stream)
24
+ unless valid_in?(stream)
25
+ raise(TypeError, "readable IO required - #{stream.inspect}")
26
+ end
27
+ @in_stream = stream
28
+ end
29
+
30
+ # Create a wrapper for given `stream`.
31
+ #
32
+ # @see .valid_out?
33
+ #
34
+ # @param [IO] stream valid out stream
35
+ # @param [Boolean, :auto] ansi whether ANSI should be supported
36
+ # or automatically selected
37
+ # @return [Wrapper] wrapper for the given `stream`
38
+ # @raise TypeError when `stream` is not a writable stream
39
+ def new(stream, ansi: :auto)
40
+ unless valid_out?(stream)
41
+ raise(TypeError, "writable IO required - #{stream.inspect}")
42
+ end
43
+ wrapper_class(stream, ansi).__send__(:new, stream)
44
+ end
45
+
46
+ # Test if the given `stream` can be used for output
47
+ #
48
+ # @param [IO] stream IO instance to test
49
+ # @return [Boolean] whether if the given stream is usable
50
+ def valid_out?(stream)
51
+ (stream.is_a?(IO) && !stream.closed? && stream.stat.writable?) ||
52
+ (stream.is_a?(StringIO) && !stream.closed_write?)
53
+ rescue StandardError
54
+ false
55
+ end
56
+
57
+ # Test if the given `stream` can be used for input
58
+ #
59
+ # @param [IO] stream IO instance to test
60
+ # @return [Boolean] whether if the given stream is usable
61
+ def valid_in?(stream)
62
+ (stream.is_a?(IO) && !stream.closed? && stream.stat.readable?) ||
63
+ (stream.is_a?(StringIO) && !stream.closed_read?)
64
+ rescue StandardError
65
+ false
66
+ end
67
+
68
+ # Translate embedded attribute descriptions into ANSI control codes.
69
+ #
70
+ # @param [#to_s] str string to edit
71
+ # @return ]String] edited string
72
+ def embellish(str)
73
+ str = str.to_s
74
+ return '' if str.empty?
75
+ reset = false
76
+ ret =
77
+ str.gsub(/(\[\[((?~\]\]))\]\])/) do
78
+ match = Regexp.last_match[2]
79
+ unless match.delete_prefix!('/')
80
+ ansi = Ansi.try_convert(match)
81
+ next ansi ? reset = ansi : "[[#{match}]]"
82
+ end
83
+ match.empty? or next "[[#{match}]]"
84
+ reset = false
85
+ Ansi.reset
86
+ end
87
+ reset ? "#{ret}#{Ansi.reset}" : ret
88
+ end
89
+
90
+ # Remove embedded attribute descriptions from given string.
91
+ #
92
+ # @param [#to_s] str string to edit
93
+ # @return ]String] edited string
94
+ def plain(str)
95
+ str
96
+ .to_s
97
+ .gsub(/(\[\[((?~\]\]))\]\])/) do
98
+ match = Regexp.last_match[2]
99
+ unless match.delete_prefix!('/')
100
+ ansi = Ansi.try_convert(match)
101
+ next ansi ? nil : "[[#{match}]]"
102
+ end
103
+ match.empty? ? nil : "[[#{match}]]"
104
+ end
105
+ end
106
+
107
+ # Calculate monospace (display) width of given String.
108
+ # It respects Unicode character sizes inclusive emoji.
109
+ #
110
+ # @param [#to_s] str string to calculate
111
+ # @return [Integer] the display size
112
+ def display_width(str)
113
+ str = str.to_s
114
+ return 0 if str.empty?
115
+ ret = Unicode::DisplayWidth.of(str, 1)
116
+ ret -= emoji_extra_width_of(str) if defined?(Unicode::Emoji)
117
+ [ret, 0].max
118
+ end
119
+
120
+ private
121
+
122
+ def wrapper_class(stream, ansi)
123
+ return AnsiWrapper if ansi == true
124
+ return Wrapper if ansi == false || ENV.key?('NO_COLOR')
125
+ stream.tty? ? AnsiWrapper : Wrapper
126
+ end
127
+
128
+ def emoji_extra_width_of(string)
129
+ ret = 0
130
+ string.scan(Unicode::Emoji::REGEX) do |emoji|
131
+ ret += 2 * emoji.scan(EMOJI_MODIFIER_REGEX).size
132
+ emoji.scan(EMOKI_ZWJ_REGEX) do |zwj_succ|
133
+ ret += Unicode::DisplayWidth.of(zwj_succ, 1, {})
134
+ end
135
+ end
136
+ ret
137
+ end
138
+
139
+ def stderr_is_stdout?
140
+ STDOUT.tty? && STDERR.tty? && STDOUT.pos == STDERR.pos
141
+ rescue IOError, SystemCallError
142
+ false
143
+ end
144
+ end
145
+
146
+ if defined?(Unicode::Emoji)
147
+ EMOJI_MODIFIER_REGEX = /[#{Unicode::Emoji::EMOJI_MODIFIERS.pack('U*')}]/
148
+ EMOKI_ZWJ_REGEX = /(?<=#{[Unicode::Emoji::ZWJ].pack('U')})./
149
+ private_constant :EMOJI_MODIFIER_REGEX, :EMOKI_ZWJ_REGEX
150
+ end
151
+
152
+ # Instance for standard output.
153
+ StdOut = new(STDOUT)
154
+
155
+ # Instance for standard error output.
156
+ StdErr = stderr_is_stdout? ? StdOut : new(STDERR)
157
+
158
+ self.in_stream = STDIN
159
+ end
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: natty-ui
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Mike Blumtritt
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-11-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: unicode-display_width
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '2.5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '2.5'
27
+ description: |
28
+ This is the beautiful, nice, nifty, fancy, neat, pretty, cool, lovely,
29
+ natty user interface you like to have for your command line interfaces
30
+ (CLI).
31
+ Here you find elegant, simple and beautiful tools that enhance your
32
+ command line application functionally and aesthetically.
33
+ email:
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files:
37
+ - README.md
38
+ - LICENSE
39
+ files:
40
+ - LICENSE
41
+ - README.md
42
+ - examples/basic.rb
43
+ - examples/colors.rb
44
+ - examples/illustration.svg
45
+ - examples/progress.rb
46
+ - examples/query.rb
47
+ - lib/natty-ui.rb
48
+ - lib/natty-ui/ansi.rb
49
+ - lib/natty-ui/ansi_wrapper.rb
50
+ - lib/natty-ui/version.rb
51
+ - lib/natty-ui/wrapper.rb
52
+ - lib/natty-ui/wrapper/ask.rb
53
+ - lib/natty-ui/wrapper/element.rb
54
+ - lib/natty-ui/wrapper/features.rb
55
+ - lib/natty-ui/wrapper/framed.rb
56
+ - lib/natty-ui/wrapper/heading.rb
57
+ - lib/natty-ui/wrapper/message.rb
58
+ - lib/natty-ui/wrapper/mixins.rb
59
+ - lib/natty-ui/wrapper/progress.rb
60
+ - lib/natty-ui/wrapper/query.rb
61
+ - lib/natty-ui/wrapper/section.rb
62
+ - lib/natty-ui/wrapper/task.rb
63
+ homepage: https://github.com/mblumtritt/natty-ui
64
+ licenses:
65
+ - BSD-3-Clause
66
+ metadata:
67
+ source_code_uri: https://github.com/mblumtritt/natty-ui
68
+ bug_tracker_uri: https://github.com/mblumtritt/natty-ui/issues
69
+ documentation_uri: https://rubydoc.info/gems/natty-ui
70
+ rubygems_mfa_required: 'true'
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '3.0'
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ requirements: []
86
+ rubygems_version: 3.4.21
87
+ signing_key:
88
+ specification_version: 4
89
+ summary: This is the beautiful, nice, nifty, fancy, neat, pretty, cool, lovely, natty
90
+ user interface you like to have for your CLI.
91
+ test_files: []