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.
- checksums.yaml +7 -0
- data/LICENSE +28 -0
- data/README.md +122 -0
- data/examples/basic.rb +61 -0
- data/examples/colors.rb +5 -0
- data/examples/illustration.svg +1 -0
- data/examples/progress.rb +84 -0
- data/examples/query.rb +32 -0
- data/lib/natty-ui/ansi.rb +430 -0
- data/lib/natty-ui/ansi_wrapper.rb +207 -0
- data/lib/natty-ui/version.rb +6 -0
- data/lib/natty-ui/wrapper/ask.rb +76 -0
- data/lib/natty-ui/wrapper/element.rb +77 -0
- data/lib/natty-ui/wrapper/features.rb +24 -0
- data/lib/natty-ui/wrapper/framed.rb +54 -0
- data/lib/natty-ui/wrapper/heading.rb +87 -0
- data/lib/natty-ui/wrapper/message.rb +116 -0
- data/lib/natty-ui/wrapper/mixins.rb +67 -0
- data/lib/natty-ui/wrapper/progress.rb +60 -0
- data/lib/natty-ui/wrapper/query.rb +85 -0
- data/lib/natty-ui/wrapper/section.rb +102 -0
- data/lib/natty-ui/wrapper/task.rb +58 -0
- data/lib/natty-ui/wrapper.rb +168 -0
- data/lib/natty-ui.rb +159 -0
- metadata +91 -0
@@ -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: []
|