termline 1.1.1 → 1.1.3
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 +4 -4
- data/lib/termline/details.rb +62 -0
- data/lib/termline/list.rb +20 -2
- data/lib/termline/msg.rb +21 -7
- data/lib/termline/space.rb +24 -9
- data/lib/termline/style.rb +21 -27
- data/lib/termline/table.rb +156 -34
- data/lib/termline/terminal.rb +82 -0
- data/lib/termline/text.rb +49 -0
- data/lib/termline/version.rb +11 -10
- data/lib/termline.rb +119 -20
- data/license.txt +674 -0
- data/readme.md +266 -31
- data/todo.md +52 -0
- metadata +12 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b593daa817cc923186c32b2f3170ba26038d3a8dde434ab9b47dd6ebc87511b
|
|
4
|
+
data.tar.gz: '048cced682cad7f691f3e2f37bf0e2ea53fa4ed3a8417a351abd8e7c692747b1'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f50437c2d6fdf48228d60cc086c6ef687ad0e8700aa36b2375fcd040e7cef3b7e6ec16d87f4347c89128cd358ecda03d52ba35b7e528d7a849de0316d926a615
|
|
7
|
+
data.tar.gz: d64e15285ad6cdd6f3266d1afc2b1b8a97a9518a306b560dc44276802671c746e353b47ecac984e3df8fb0876da9a73c2facf1bb991143be3ab1a053b74a1150
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Termline
|
|
4
|
+
module Details
|
|
5
|
+
class << self
|
|
6
|
+
def builder(data=nil, color: :blue, separator: ": ", icon: nil, icons: nil, **additional_data)
|
|
7
|
+
data = additional_data if data.nil?
|
|
8
|
+
data = data.merge(additional_data) if !additional_data.empty? && data.respond_to?(:merge)
|
|
9
|
+
|
|
10
|
+
unless data.respond_to?(:each_pair)
|
|
11
|
+
raise ArgumentError, "Details data must be a hash-like object"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
pairs = data.each_pair.to_a
|
|
15
|
+
return "" if pairs.empty?
|
|
16
|
+
|
|
17
|
+
icon_map = normalize_icon_map(icons)
|
|
18
|
+
show_icon_column = !icon.nil? || !icon_map.empty?
|
|
19
|
+
key_width = pairs.map { |key, _value| key.to_s.length }.max
|
|
20
|
+
|
|
21
|
+
pairs.map do |key, value|
|
|
22
|
+
row_icon = icon_map.fetch(key.to_s, icon)
|
|
23
|
+
icon_prefix = format_icon(row_icon, color, show_icon_column)
|
|
24
|
+
label = Style.colorize(key.to_s.ljust(key_width), color)
|
|
25
|
+
"#{icon_prefix}#{label}#{separator}#{format_value(value)}"
|
|
26
|
+
end.join("\n")
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def format_value(value)
|
|
32
|
+
case value
|
|
33
|
+
when nil
|
|
34
|
+
"nil"
|
|
35
|
+
when String
|
|
36
|
+
value
|
|
37
|
+
else
|
|
38
|
+
value.to_s
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def normalize_icon_map(icons)
|
|
43
|
+
return {} if icons.nil?
|
|
44
|
+
|
|
45
|
+
unless icons.respond_to?(:each_pair)
|
|
46
|
+
raise ArgumentError, "Details icons must be a hash-like object"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
icons.each_pair.each_with_object({}) do |(key, value), normalized|
|
|
50
|
+
normalized[key.to_s] = value
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_icon(icon, color, show_icon_column)
|
|
55
|
+
return "" unless show_icon_column
|
|
56
|
+
return " " if icon.nil?
|
|
57
|
+
|
|
58
|
+
"#{Style.colorize(Style.icon(icon), color)} "
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/termline/list.rb
CHANGED
|
@@ -33,8 +33,26 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module Termline
|
|
34
34
|
module List
|
|
35
35
|
class << self
|
|
36
|
-
def builder(items, icon
|
|
37
|
-
|
|
36
|
+
def builder(items, icon: :debug, color: :default, ordered: false, start: 1)
|
|
37
|
+
first_number = integer_start(start)
|
|
38
|
+
|
|
39
|
+
items.each_with_index.map do |item, index|
|
|
40
|
+
marker = if ordered
|
|
41
|
+
Style.colorize("#{first_number + index}.", color)
|
|
42
|
+
else
|
|
43
|
+
Style.colorize(Style.icon(icon), color)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
" #{marker} #{item}"
|
|
47
|
+
end.join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def integer_start(value)
|
|
53
|
+
Integer(value)
|
|
54
|
+
rescue ArgumentError, TypeError
|
|
55
|
+
raise ArgumentError, "List start must be an integer"
|
|
38
56
|
end
|
|
39
57
|
end
|
|
40
58
|
end
|
data/lib/termline/msg.rb
CHANGED
|
@@ -40,21 +40,31 @@ module Termline
|
|
|
40
40
|
icon: nil,
|
|
41
41
|
color: :default,
|
|
42
42
|
bgcolor: :default,
|
|
43
|
-
timestamp:
|
|
43
|
+
timestamp: nil,
|
|
44
|
+
time:nil,
|
|
45
|
+
data:nil)
|
|
44
46
|
|
|
47
|
+
unless time.nil? || time == true || time == false
|
|
48
|
+
raise ArgumentError, "Message time must be true, false, or nil"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
show_time = time.nil? ? Termline.timestamps? : time
|
|
45
52
|
parts = []
|
|
46
|
-
parts << "[#{timestamp}]" if
|
|
53
|
+
parts << "[#{timestamp || current_timestamp}]" if show_time
|
|
47
54
|
parts << Style.colorize(Style.icon(icon), color) if icon
|
|
48
55
|
parts << Style.colorize(tag, color) if tag
|
|
49
|
-
parts <<
|
|
56
|
+
parts << if bgcolor == :default
|
|
57
|
+
message
|
|
58
|
+
else
|
|
59
|
+
Style.colorize(message, color, bgcolor)
|
|
60
|
+
end
|
|
50
61
|
parts << data_text(data, color) if data
|
|
51
62
|
|
|
52
63
|
parts.compact.map(&:to_s).reject(&:empty?).join(" ").strip
|
|
53
64
|
end
|
|
54
65
|
|
|
55
|
-
def alert
|
|
56
|
-
|
|
57
|
-
puts("\e[5m#{Style.colorize(message,:red)}\e[0m")
|
|
66
|
+
def alert(message)
|
|
67
|
+
Style.colorize(" #{message} ", :white, :red)
|
|
58
68
|
end
|
|
59
69
|
|
|
60
70
|
private
|
|
@@ -70,7 +80,11 @@ module Termline
|
|
|
70
80
|
|
|
71
81
|
def data_text(data, color)
|
|
72
82
|
return nil if data.nil? || data.empty?
|
|
73
|
-
data.map { |key, value| "#{
|
|
83
|
+
data.map { |key, value| "#{Style.colorize(key, color)}=#{format_value(value)}" }.join(" ")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def current_timestamp
|
|
87
|
+
Time.now.strftime(Termline.timestamp_format)
|
|
74
88
|
end
|
|
75
89
|
end
|
|
76
90
|
end
|
data/lib/termline/space.rb
CHANGED
|
@@ -33,19 +33,34 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module Termline
|
|
34
34
|
module Space
|
|
35
35
|
class << self
|
|
36
|
-
def builder(character, count)
|
|
36
|
+
def builder(character, count=8)
|
|
37
37
|
character * count
|
|
38
38
|
end
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
|
|
40
|
+
def line(character, width)
|
|
41
|
+
character = character.to_s
|
|
42
|
+
raise ArgumentError, "Separator character cannot be empty" if character.empty?
|
|
43
|
+
|
|
44
|
+
width = positive_width(width)
|
|
45
|
+
|
|
46
|
+
(character * ((width.to_f / character.length).ceil))[0, width]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def color(color)
|
|
50
|
+
return "" unless Termline.terminal.ansi?
|
|
51
|
+
|
|
52
|
+
Style.pretty("", :black, color)
|
|
45
53
|
end
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def positive_width(value)
|
|
58
|
+
width = Integer(value)
|
|
59
|
+
raise ArgumentError unless width.positive?
|
|
60
|
+
|
|
61
|
+
width
|
|
62
|
+
rescue ArgumentError, TypeError
|
|
63
|
+
raise ArgumentError, "Separator width must be a positive integer"
|
|
49
64
|
end
|
|
50
65
|
end
|
|
51
66
|
end
|
data/lib/termline/style.rb
CHANGED
|
@@ -48,19 +48,19 @@ module Termline
|
|
|
48
48
|
# standard color palette for texts
|
|
49
49
|
COLORS = {
|
|
50
50
|
skyblue: '96',
|
|
51
|
-
default: '
|
|
51
|
+
default: '39',
|
|
52
52
|
yellow: '1;33',
|
|
53
53
|
white: '1;37',
|
|
54
54
|
green: '32',
|
|
55
55
|
black: '30',
|
|
56
56
|
gray: '90',
|
|
57
|
-
blue: '
|
|
57
|
+
blue: '34',
|
|
58
58
|
red: '31'
|
|
59
59
|
}.freeze
|
|
60
60
|
|
|
61
61
|
# standard color palette for backgrounds
|
|
62
62
|
BG_COLORS = {
|
|
63
|
-
default: '
|
|
63
|
+
default: '49',
|
|
64
64
|
yellow: '103',
|
|
65
65
|
white: '107',
|
|
66
66
|
green: '42',
|
|
@@ -68,39 +68,33 @@ module Termline
|
|
|
68
68
|
blue: '44',
|
|
69
69
|
red: '41'
|
|
70
70
|
}.freeze
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
# calculate the console width
|
|
74
|
-
# tputcols is not available on windows
|
|
75
|
-
WIDTH = begin
|
|
76
|
-
term = ENV['TERM']
|
|
77
|
-
term ? `tput cols`.to_i : 1
|
|
78
|
-
rescue
|
|
79
|
-
1
|
|
80
|
-
end
|
|
81
|
-
|
|
82
71
|
class << self
|
|
83
|
-
def icon
|
|
84
|
-
ICONS
|
|
72
|
+
def icon(icon)
|
|
73
|
+
ICONS.fetch(icon) do
|
|
74
|
+
raise ArgumentError, "Unknown icon: #{icon.inspect}. Available icons: #{ICONS.keys.join(', ')}"
|
|
75
|
+
end
|
|
85
76
|
end
|
|
86
77
|
|
|
87
78
|
# Concat color to text
|
|
88
79
|
def colorize(text, colour = :default, bg_colour = :default)
|
|
89
|
-
colour_code = COLORS
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
colour_code = COLORS.fetch(colour) do
|
|
81
|
+
raise ArgumentError, "Unknown color: #{colour.inspect}. Available colors: #{COLORS.keys.join(', ')}"
|
|
82
|
+
end
|
|
83
|
+
bg_colour_code = BG_COLORS.fetch(bg_colour) do
|
|
84
|
+
raise ArgumentError, "Unknown background color: #{bg_colour.inspect}. Available background colors: #{BG_COLORS.keys.join(', ')}"
|
|
85
|
+
end
|
|
86
|
+
return text.to_s unless Termline.terminal.ansi?
|
|
87
|
+
|
|
88
|
+
return "\e[#{colour_code};#{bg_colour_code}m#{text}\e[0m"
|
|
92
89
|
end
|
|
93
90
|
|
|
94
91
|
# Format text
|
|
95
92
|
def pretty(message, colour = :default, bg_colour = :default)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
return colorize("#{ message } #{"\ " * width}", colour, bg_colour)
|
|
93
|
+
text = message.to_s
|
|
94
|
+
return colorize(text, colour, bg_colour) if bg_colour == :default
|
|
95
|
+
|
|
96
|
+
padding = [Termline.terminal.width - text.length, 0].max
|
|
97
|
+
colorize("#{text}#{" " * padding}", colour, bg_colour)
|
|
104
98
|
end
|
|
105
99
|
end
|
|
106
100
|
end
|
data/lib/termline/table.rb
CHANGED
|
@@ -33,58 +33,180 @@ Building a better future, one line of code at a time.
|
|
|
33
33
|
module Termline
|
|
34
34
|
module Table
|
|
35
35
|
class << self
|
|
36
|
-
|
|
36
|
+
ALIGNMENTS = [:auto, :left, :center, :right].freeze
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
def builder(data, header:true, align: :auto)
|
|
39
|
+
# Convert every supported input into a predictable matrix first.
|
|
40
|
+
# Rendering can then stay independent from Active Record and hashes.
|
|
41
|
+
source = normalize_source(data)
|
|
42
|
+
return "" if source.empty?
|
|
40
43
|
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
keys, rows, hash_rows = normalize_rows(source)
|
|
45
|
+
return "" if rows.empty? || rows.first.empty?
|
|
43
46
|
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
headers = hash_rows && header ? keys.map { |key| key.to_s.upcase } : nil
|
|
48
|
+
text_rows = rows.map { |row| row.map { |value| normalize_value(value) } }
|
|
49
|
+
|
|
50
|
+
# Widths are calculated from visible text before padding or borders
|
|
51
|
+
# are added, so the completed line never exceeds the terminal width.
|
|
52
|
+
widths = column_widths(text_rows, headers, Termline.terminal.width)
|
|
53
|
+
return "" unless widths
|
|
54
|
+
|
|
55
|
+
alignments = widths.each_index.map do |index|
|
|
56
|
+
column_alignment(align, index, keys[index], rows.map { |row| row[index] })
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
separator = render_separator(widths)
|
|
60
|
+
lines = []
|
|
61
|
+
|
|
62
|
+
if headers
|
|
63
|
+
lines << render_row(headers, widths, alignments)
|
|
64
|
+
lines << separator
|
|
65
|
+
else
|
|
66
|
+
lines << separator
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
text_rows.each do |row|
|
|
70
|
+
lines << render_row(row, widths, alignments)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
lines << separator unless headers
|
|
74
|
+
lines.join("\n")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def normalize_source(data)
|
|
80
|
+
# A single hash is one row. Active Record support is detected only
|
|
81
|
+
# when Active Record is already loaded, keeping it an optional dependency.
|
|
82
|
+
return [] if data.nil?
|
|
83
|
+
return [data] if data.is_a?(Hash)
|
|
84
|
+
|
|
85
|
+
if active_record_relation?(data)
|
|
86
|
+
return data.to_a.map(&:serializable_hash)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
unless data.respond_to?(:to_a)
|
|
90
|
+
raise ArgumentError, "Table data must be a hash or collection of rows"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
data.to_a
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def active_record_relation?(data)
|
|
97
|
+
defined?(::ActiveRecord::Relation) && data.is_a?(::ActiveRecord::Relation)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def normalize_rows(source)
|
|
101
|
+
if source.all? { |row| row.is_a?(Hash) }
|
|
102
|
+
# Use the union of keys in insertion order. This preserves columns
|
|
103
|
+
# that appear only in later rows and leaves missing values blank.
|
|
104
|
+
keys = source.flat_map(&:keys).uniq
|
|
105
|
+
rows = source.map { |row| keys.map { |key| row[key] } }
|
|
106
|
+
return [keys, rows, true]
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
unless source.all? { |row| row.is_a?(Array) }
|
|
110
|
+
raise ArgumentError, "Table rows must all be hashes or all be arrays"
|
|
46
111
|
end
|
|
47
112
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
113
|
+
# Array rows have no column names. Pad short rows so every rendered
|
|
114
|
+
# row has the same number of cells and borders remain aligned.
|
|
115
|
+
column_count = source.map(&:length).max.to_i
|
|
116
|
+
rows = source.map { |row| row + Array.new(column_count - row.length) }
|
|
117
|
+
[Array.new(column_count) { |index| index }, rows, false]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def normalize_value(value)
|
|
121
|
+
# Tables are intentionally single-line components; embedded whitespace
|
|
122
|
+
# is collapsed instead of allowing one value to break the layout.
|
|
123
|
+
value.nil? ? "" : value.to_s.gsub(/\s+/, " ").strip
|
|
124
|
+
end
|
|
51
125
|
|
|
52
|
-
|
|
53
|
-
|
|
126
|
+
def column_widths(rows, headers, terminal_width)
|
|
127
|
+
column_count = rows.first.length
|
|
54
128
|
|
|
55
|
-
#
|
|
56
|
-
|
|
129
|
+
# Each column contributes two padding spaces and one separator.
|
|
130
|
+
# The final `+ 1` accounts for the closing border.
|
|
131
|
+
available = terminal_width - ((column_count * 3) + 1)
|
|
132
|
+
return nil if available < column_count
|
|
57
133
|
|
|
58
|
-
|
|
59
|
-
|
|
134
|
+
widths = Array.new(column_count, 1)
|
|
135
|
+
column_count.times do |index|
|
|
136
|
+
values = rows.map { |row| row[index].length }
|
|
137
|
+
values << headers[index].length if headers
|
|
138
|
+
widths[index] = values.max
|
|
139
|
+
end
|
|
60
140
|
|
|
61
|
-
#
|
|
62
|
-
|
|
141
|
+
# Repeatedly shrink the widest column. This favors compact columns
|
|
142
|
+
# while sharing scarce space fairly between long text columns.
|
|
143
|
+
while widths.sum > available
|
|
144
|
+
index = widths.each_index.select { |position| widths[position] > 1 }.max_by { |position| widths[position] }
|
|
145
|
+
break unless index
|
|
63
146
|
|
|
64
|
-
|
|
65
|
-
|
|
147
|
+
widths[index] -= 1
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
widths
|
|
151
|
+
end
|
|
66
152
|
|
|
67
|
-
|
|
68
|
-
|
|
153
|
+
def column_alignment(align, index, key, values)
|
|
154
|
+
# Alignment may be global, positional, or keyed by the original hash
|
|
155
|
+
# key. Missing per-column settings fall back to automatic alignment.
|
|
156
|
+
requested = case align
|
|
157
|
+
when Hash
|
|
158
|
+
align.fetch(key) do
|
|
159
|
+
align.fetch(key.to_s) { align.fetch(index, :auto) }
|
|
160
|
+
end
|
|
161
|
+
when Array
|
|
162
|
+
align[index] || :auto
|
|
163
|
+
else
|
|
164
|
+
align
|
|
165
|
+
end
|
|
69
166
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
167
|
+
requested = requested.to_sym if requested.respond_to?(:to_sym)
|
|
168
|
+
unless ALIGNMENTS.include?(requested)
|
|
169
|
+
raise ArgumentError, "Unknown table alignment: #{requested.inspect}. Available alignments: #{ALIGNMENTS.join(', ')}"
|
|
170
|
+
end
|
|
73
171
|
|
|
74
|
-
|
|
75
|
-
puts separator * (cols - 1)
|
|
172
|
+
return requested unless requested == :auto
|
|
76
173
|
|
|
77
|
-
#
|
|
78
|
-
|
|
174
|
+
# A column is numeric only when all of its non-nil source values are
|
|
175
|
+
# numeric. Numeric-looking strings remain left-aligned intentionally.
|
|
176
|
+
printable_values = values.compact
|
|
177
|
+
printable_values.any? && printable_values.all? { |value| value.is_a?(Numeric) } ? :right : :left
|
|
178
|
+
end
|
|
79
179
|
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
def render_separator(widths)
|
|
181
|
+
"|#{widths.map { |width| "-" * (width + 2) }.join("|")}|"
|
|
182
|
+
end
|
|
82
183
|
|
|
83
|
-
|
|
84
|
-
|
|
184
|
+
def render_row(row, widths, alignments)
|
|
185
|
+
cells = row.each_index.map do |index|
|
|
186
|
+
text = truncate(row[index], widths[index])
|
|
187
|
+
align_text(text, widths[index], alignments[index])
|
|
85
188
|
end
|
|
86
189
|
|
|
87
|
-
|
|
190
|
+
"| #{cells.join(' | ')} |"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def truncate(text, width)
|
|
194
|
+
return text if text.length <= width
|
|
195
|
+
return "…" if width == 1
|
|
196
|
+
|
|
197
|
+
# Reserve the final cell for an ellipsis so truncation is visible.
|
|
198
|
+
"#{text[0, width - 1]}…"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def align_text(text, width, alignment)
|
|
202
|
+
case alignment
|
|
203
|
+
when :right
|
|
204
|
+
text.rjust(width)
|
|
205
|
+
when :center
|
|
206
|
+
text.center(width)
|
|
207
|
+
else
|
|
208
|
+
text.ljust(width)
|
|
209
|
+
end
|
|
88
210
|
end
|
|
89
211
|
end
|
|
90
212
|
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "io/console"
|
|
4
|
+
|
|
5
|
+
module Termline
|
|
6
|
+
class Terminal
|
|
7
|
+
DEFAULT_WIDTH = 80
|
|
8
|
+
ANSI_MODES = [true, false, :auto].freeze
|
|
9
|
+
|
|
10
|
+
attr_reader :ansi
|
|
11
|
+
|
|
12
|
+
def initialize(output: nil, ansi: :auto, width: nil)
|
|
13
|
+
self.output = output
|
|
14
|
+
self.ansi = ansi
|
|
15
|
+
self.width = width
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def output
|
|
19
|
+
@output || $stdout
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def output=(io)
|
|
23
|
+
unless io.nil? || io.respond_to?(:puts)
|
|
24
|
+
raise ArgumentError, "Output must respond to #puts"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@output = io
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ansi=(mode)
|
|
31
|
+
unless ANSI_MODES.include?(mode)
|
|
32
|
+
raise ArgumentError, "ANSI mode must be true, false, or :auto"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
@ansi = mode
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def ansi?
|
|
39
|
+
return ansi if ansi == true || ansi == false
|
|
40
|
+
|
|
41
|
+
output.respond_to?(:tty?) && output.tty? && !ENV.key?("NO_COLOR") && ENV["TERM"] != "dumb"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def width
|
|
45
|
+
return @width if @width
|
|
46
|
+
|
|
47
|
+
width_from(output) || environment_width || width_from(IO.console) || DEFAULT_WIDTH
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def width=(value)
|
|
51
|
+
if value.nil?
|
|
52
|
+
@width = nil
|
|
53
|
+
return
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
width = Integer(value)
|
|
57
|
+
raise ArgumentError, "Terminal width must be greater than zero" unless width.positive?
|
|
58
|
+
|
|
59
|
+
@width = width
|
|
60
|
+
rescue ArgumentError, TypeError
|
|
61
|
+
raise ArgumentError, "Terminal width must be a positive integer"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def environment_width
|
|
67
|
+
value = Integer(ENV.fetch("COLUMNS", 0))
|
|
68
|
+
value if value.positive?
|
|
69
|
+
rescue ArgumentError, TypeError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def width_from(io)
|
|
74
|
+
return unless io.respond_to?(:winsize)
|
|
75
|
+
|
|
76
|
+
value = io.winsize.last.to_i
|
|
77
|
+
value if value.positive?
|
|
78
|
+
rescue IOError, SystemCallError
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Termline
|
|
4
|
+
module Text
|
|
5
|
+
class << self
|
|
6
|
+
def title(text, color: :blue, character: "─", width: Termline.terminal.width)
|
|
7
|
+
character = character.to_s
|
|
8
|
+
raise ArgumentError, "Title character cannot be empty" if character.empty?
|
|
9
|
+
|
|
10
|
+
width = positive_width(width)
|
|
11
|
+
prefix = "#{character * 2} #{text}"
|
|
12
|
+
plain = fit_title(prefix, character, width)
|
|
13
|
+
|
|
14
|
+
Style.colorize(plain, color)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def fit_title(prefix, character, width)
|
|
20
|
+
return truncate(prefix, width) if prefix.length >= width
|
|
21
|
+
|
|
22
|
+
remaining = width - prefix.length - 1
|
|
23
|
+
"#{prefix} #{fill(character, remaining)}"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def fill(character, width)
|
|
27
|
+
return "" unless width.positive?
|
|
28
|
+
|
|
29
|
+
(character * ((width.to_f / character.length).ceil))[0, width]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def truncate(text, width)
|
|
33
|
+
return text if text.length <= width
|
|
34
|
+
return "…" if width == 1
|
|
35
|
+
|
|
36
|
+
"#{text[0, width - 1]}…"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def positive_width(value)
|
|
40
|
+
width = Integer(value)
|
|
41
|
+
raise ArgumentError unless width.positive?
|
|
42
|
+
|
|
43
|
+
width
|
|
44
|
+
rescue ArgumentError, TypeError
|
|
45
|
+
raise ArgumentError, "Text width must be a positive integer"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/termline/version.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
=begin
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Lesli
|
|
4
|
+
|
|
5
|
+
Copyright (c) 2026, Lesli Technologies, S. A.
|
|
4
6
|
|
|
5
7
|
This program is free software: you can redistribute it and/or modify
|
|
6
8
|
it under the terms of the GNU General Public License as published by
|
|
@@ -13,23 +15,22 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
13
15
|
GNU General Public License for more details.
|
|
14
16
|
|
|
15
17
|
You should have received a copy of the GNU General Public License
|
|
16
|
-
along with this program. If not, see
|
|
18
|
+
along with this program. If not, see http://www.gnu.org/licenses/.
|
|
17
19
|
|
|
18
|
-
Lesli Ruby
|
|
20
|
+
Lesli · Ruby on Rails SaaS Development Framework.
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
Made with ♥ by LesliTech
|
|
21
23
|
Building a better future, one line of code at a time.
|
|
22
24
|
|
|
23
|
-
@contact
|
|
24
|
-
@website
|
|
25
|
+
@contact hello@lesli.tech
|
|
26
|
+
@website https://www.lesli.tech
|
|
25
27
|
@license GPLv3 http://www.gnu.org/licenses/gpl-3.0.en.html
|
|
26
|
-
@author The Lesli Development Team
|
|
27
28
|
|
|
28
|
-
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
29
|
+
// · ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~ ~·~
|
|
29
30
|
// ·
|
|
30
31
|
=end
|
|
31
32
|
|
|
32
33
|
module Termline
|
|
33
|
-
VERSION = "1.1.
|
|
34
|
-
BUILD = "
|
|
34
|
+
VERSION = "1.1.3"
|
|
35
|
+
BUILD = ""
|
|
35
36
|
end
|