chamomile-flourish 0.1.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/lib/flourish/align.rb +33 -0
- data/lib/flourish/ansi.rb +103 -0
- data/lib/flourish/border.rb +67 -0
- data/lib/flourish/color.rb +105 -0
- data/lib/flourish/color_profile.rb +136 -0
- data/lib/flourish/join.rb +61 -0
- data/lib/flourish/place.rb +34 -0
- data/lib/flourish/style.rb +617 -0
- data/lib/flourish/version.rb +5 -0
- data/lib/flourish/wrap.rb +111 -0
- data/lib/flourish.rb +55 -0
- metadata +86 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5d26d2a333d6f0a962ffb7bf18af8b80424fdeac18262392e534fe4a2d65336f
|
|
4
|
+
data.tar.gz: 617a4eca7cbf2034c8921a293d44707295553400a368e306bfad3de228394635
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 83b89f48310d72d22c5a22681f3c261af4213ca1fc6709bf6abc6ec6c03e3a64cc9cb0f8e7311002634e14968d2b72bc9133fd7fb390f7b73df10cfcfcd8ade7
|
|
7
|
+
data.tar.gz: f42c0446e12058d4565ff09efef8094ea45b8c15882bf1e90d610ae5a260c084814e725c3e3bbdc98c0bf284de8d2abf0417f004f9beb9b8abd81ef607093974
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module Align
|
|
5
|
+
class << self
|
|
6
|
+
def horizontal(lines, width, position)
|
|
7
|
+
lines.map do |line|
|
|
8
|
+
line_width = ANSI.printable_width(line)
|
|
9
|
+
gap = width - line_width
|
|
10
|
+
next line if gap <= 0
|
|
11
|
+
|
|
12
|
+
left_pad = (gap * position).round
|
|
13
|
+
right_pad = gap - left_pad
|
|
14
|
+
"#{" " * left_pad}#{line}#{" " * right_pad}"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def vertical(lines, height, position)
|
|
19
|
+
gap = height - lines.length
|
|
20
|
+
return lines if gap <= 0
|
|
21
|
+
|
|
22
|
+
top_pad = (gap * position).round
|
|
23
|
+
bottom_pad = gap - top_pad
|
|
24
|
+
|
|
25
|
+
result = []
|
|
26
|
+
top_pad.times { result << "" }
|
|
27
|
+
result.concat(lines)
|
|
28
|
+
bottom_pad.times { result << "" }
|
|
29
|
+
result
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module ANSI
|
|
5
|
+
# Matches CSI sequences, OSC sequences, ESC charset/other sequences
|
|
6
|
+
ESCAPE_RE = /\e\[[0-9;]*[A-Za-z]|\e\][^\a\e]*(?:\a|\e\\)|\e[()][AB012]|\e./
|
|
7
|
+
|
|
8
|
+
class << self
|
|
9
|
+
def strip(str)
|
|
10
|
+
str.gsub(ESCAPE_RE, "")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def printable_width(str)
|
|
14
|
+
stripped = strip(str)
|
|
15
|
+
width = 0
|
|
16
|
+
stripped.each_char do |ch|
|
|
17
|
+
width += char_width(ch)
|
|
18
|
+
end
|
|
19
|
+
width
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def height(str)
|
|
23
|
+
str.count("\n") + 1
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def size(str)
|
|
27
|
+
return [0, 1] if str.empty?
|
|
28
|
+
|
|
29
|
+
lines = str.split("\n", -1)
|
|
30
|
+
w = lines.map { |line| printable_width(line) }.max || 0
|
|
31
|
+
[w, lines.length]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def extract_escape(chars, start)
|
|
35
|
+
return nil unless chars[start] == "\e"
|
|
36
|
+
|
|
37
|
+
i = start + 1
|
|
38
|
+
return nil if i >= chars.length
|
|
39
|
+
|
|
40
|
+
if chars[i] == "["
|
|
41
|
+
seq = +"\e["
|
|
42
|
+
i += 1
|
|
43
|
+
while i < chars.length && chars[i].match?(/[0-9;]/)
|
|
44
|
+
seq << chars[i]
|
|
45
|
+
i += 1
|
|
46
|
+
end
|
|
47
|
+
if i < chars.length && chars[i].match?(/[A-Za-z]/)
|
|
48
|
+
seq << chars[i]
|
|
49
|
+
return seq
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def track_sgr(active_sgr, seq)
|
|
57
|
+
if ["\e[0m", "\e[m"].include?(seq)
|
|
58
|
+
active_sgr.clear
|
|
59
|
+
elsif seq.match?(/\A\e\[\d/)
|
|
60
|
+
if active_sgr.empty?
|
|
61
|
+
active_sgr.replace(seq)
|
|
62
|
+
else
|
|
63
|
+
active_sgr.replace("#{active_sgr.delete_suffix("\e[0m")}#{seq}")
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def sgr_open_after?(was_open, seq)
|
|
69
|
+
return false if ["\e[0m", "\e[m"].include?(seq)
|
|
70
|
+
return true if seq.match?(/\A\e\[\d/)
|
|
71
|
+
|
|
72
|
+
was_open
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def char_width(ch)
|
|
78
|
+
cp = ch.ord
|
|
79
|
+
return 2 if cjk?(cp)
|
|
80
|
+
return 0 if cp < 32 || (cp >= 0x7F && cp < 0xA0)
|
|
81
|
+
|
|
82
|
+
1
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def cjk?(cp)
|
|
86
|
+
cp.between?(0x1100, 0x115F) || # Hangul Jamo
|
|
87
|
+
cp == 0x2329 || cp == 0x232A || # Angle brackets
|
|
88
|
+
cp.between?(0x2E80, 0x303E) || # CJK Radicals..CJK Symbols
|
|
89
|
+
cp.between?(0x3040, 0x33BF) || # Hiragana..CJK Compatibility
|
|
90
|
+
cp.between?(0x3400, 0x4DBF) || # CJK Unified Ext A
|
|
91
|
+
cp.between?(0x4E00, 0xA4CF) || # CJK Unified..Yi Radicals
|
|
92
|
+
cp.between?(0xAC00, 0xD7A3) || # Hangul Syllables
|
|
93
|
+
cp.between?(0xF900, 0xFAFF) || # CJK Compatibility Ideographs
|
|
94
|
+
cp.between?(0xFE10, 0xFE6F) || # Vertical forms..Small forms
|
|
95
|
+
cp.between?(0xFF01, 0xFF60) || # Fullwidth forms
|
|
96
|
+
cp.between?(0xFFE0, 0xFFE6) || # Fullwidth signs
|
|
97
|
+
cp.between?(0x1F300, 0x1F9FF) || # Misc Symbols/Emoji
|
|
98
|
+
cp.between?(0x20000, 0x2FFFD) || # CJK Ext B..
|
|
99
|
+
cp.between?(0x30000, 0x3FFFD) # CJK Ext G..
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
BorderDef = Data.define(
|
|
5
|
+
:top, :bottom, :left, :right,
|
|
6
|
+
:top_left, :top_right, :bottom_left, :bottom_right,
|
|
7
|
+
:middle_left, :middle_right, :middle, :middle_top, :middle_bottom
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
module Border
|
|
11
|
+
NORMAL = BorderDef.new(
|
|
12
|
+
top: "─", bottom: "─", left: "│", right: "│",
|
|
13
|
+
top_left: "┌", top_right: "┐", bottom_left: "└", bottom_right: "┘",
|
|
14
|
+
middle_left: "├", middle_right: "┤", middle: "┼", middle_top: "┬", middle_bottom: "┴"
|
|
15
|
+
).freeze
|
|
16
|
+
|
|
17
|
+
ROUNDED = BorderDef.new(
|
|
18
|
+
top: "─", bottom: "─", left: "│", right: "│",
|
|
19
|
+
top_left: "╭", top_right: "╮", bottom_left: "╰", bottom_right: "╯",
|
|
20
|
+
middle_left: "├", middle_right: "┤", middle: "┼", middle_top: "┬", middle_bottom: "┴"
|
|
21
|
+
).freeze
|
|
22
|
+
|
|
23
|
+
THICK = BorderDef.new(
|
|
24
|
+
top: "━", bottom: "━", left: "┃", right: "┃",
|
|
25
|
+
top_left: "┏", top_right: "┓", bottom_left: "┗", bottom_right: "┛",
|
|
26
|
+
middle_left: "┣", middle_right: "┫", middle: "╋", middle_top: "┳", middle_bottom: "┻"
|
|
27
|
+
).freeze
|
|
28
|
+
|
|
29
|
+
DOUBLE = BorderDef.new(
|
|
30
|
+
top: "═", bottom: "═", left: "║", right: "║",
|
|
31
|
+
top_left: "╔", top_right: "╗", bottom_left: "╚", bottom_right: "╝",
|
|
32
|
+
middle_left: "╠", middle_right: "╣", middle: "╬", middle_top: "╦", middle_bottom: "╩"
|
|
33
|
+
).freeze
|
|
34
|
+
|
|
35
|
+
BLOCK = BorderDef.new(
|
|
36
|
+
top: "█", bottom: "█", left: "█", right: "█",
|
|
37
|
+
top_left: "█", top_right: "█", bottom_left: "█", bottom_right: "█",
|
|
38
|
+
middle_left: "█", middle_right: "█", middle: "█", middle_top: "█", middle_bottom: "█"
|
|
39
|
+
).freeze
|
|
40
|
+
|
|
41
|
+
OUTER_HALF_BLOCK = BorderDef.new(
|
|
42
|
+
top: "▀", bottom: "▄", left: "▌", right: "▐",
|
|
43
|
+
top_left: "▛", top_right: "▜", bottom_left: "▙", bottom_right: "▟",
|
|
44
|
+
middle_left: "▌", middle_right: "▐", middle: "┼", middle_top: "▀", middle_bottom: "▄"
|
|
45
|
+
).freeze
|
|
46
|
+
|
|
47
|
+
INNER_HALF_BLOCK = BorderDef.new(
|
|
48
|
+
top: "▄", bottom: "▀", left: "▐", right: "▌",
|
|
49
|
+
top_left: "▗", top_right: "▖", bottom_left: "▝", bottom_right: "▘",
|
|
50
|
+
middle_left: "▐", middle_right: "▌", middle: "┼", middle_top: "▄", middle_bottom: "▀"
|
|
51
|
+
).freeze
|
|
52
|
+
|
|
53
|
+
HIDDEN = BorderDef.new(
|
|
54
|
+
top: " ", bottom: " ", left: " ", right: " ",
|
|
55
|
+
top_left: " ", top_right: " ", bottom_left: " ", bottom_right: " ",
|
|
56
|
+
middle_left: " ", middle_right: " ", middle: " ", middle_top: " ", middle_bottom: " "
|
|
57
|
+
).freeze
|
|
58
|
+
|
|
59
|
+
ASCII = BorderDef.new(
|
|
60
|
+
top: "-", bottom: "-", left: "|", right: "|",
|
|
61
|
+
top_left: "+", top_right: "+", bottom_left: "+", bottom_right: "+",
|
|
62
|
+
middle_left: "+", middle_right: "+", middle: "+", middle_top: "+", middle_bottom: "+"
|
|
63
|
+
).freeze
|
|
64
|
+
|
|
65
|
+
MARKDOWN = ASCII
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module Color
|
|
5
|
+
def self.parse(value)
|
|
6
|
+
return NoColor.new if value.nil? || value == ""
|
|
7
|
+
|
|
8
|
+
return parse_hex(value) if value.is_a?(String) && value.start_with?("#")
|
|
9
|
+
|
|
10
|
+
code = value.to_i
|
|
11
|
+
return ANSIColor.new(code: code) if code.between?(0, 15)
|
|
12
|
+
return ANSI256Color.new(code: code) if code.between?(16, 255)
|
|
13
|
+
|
|
14
|
+
NoColor.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.parse_hex(hex)
|
|
18
|
+
hex = hex.delete_prefix("#")
|
|
19
|
+
case hex.length
|
|
20
|
+
when 3
|
|
21
|
+
r = (hex[0] * 2).to_i(16)
|
|
22
|
+
g = (hex[1] * 2).to_i(16)
|
|
23
|
+
b = (hex[2] * 2).to_i(16)
|
|
24
|
+
TrueColor.new(r: r, g: g, b: b)
|
|
25
|
+
when 6
|
|
26
|
+
r = hex[0..1].to_i(16)
|
|
27
|
+
g = hex[2..3].to_i(16)
|
|
28
|
+
b = hex[4..5].to_i(16)
|
|
29
|
+
TrueColor.new(r: r, g: g, b: b)
|
|
30
|
+
else
|
|
31
|
+
NoColor.new
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private_class_method :parse_hex
|
|
36
|
+
|
|
37
|
+
NoColor = Data.define do
|
|
38
|
+
def fg_sequence = nil
|
|
39
|
+
def bg_sequence = nil
|
|
40
|
+
def no_color? = true
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
ANSIColor = Data.define(:code) do
|
|
44
|
+
def fg_sequence
|
|
45
|
+
if code < 8
|
|
46
|
+
(30 + code).to_s
|
|
47
|
+
else
|
|
48
|
+
(90 + code - 8).to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bg_sequence
|
|
53
|
+
if code < 8
|
|
54
|
+
(40 + code).to_s
|
|
55
|
+
else
|
|
56
|
+
(100 + code - 8).to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def no_color? = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
ANSI256Color = Data.define(:code) do
|
|
64
|
+
def fg_sequence
|
|
65
|
+
"38;5;#{code}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def bg_sequence
|
|
69
|
+
"48;5;#{code}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def no_color? = false
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
TrueColor = Data.define(:r, :g, :b) do
|
|
76
|
+
def fg_sequence
|
|
77
|
+
"38;2;#{r};#{g};#{b}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bg_sequence
|
|
81
|
+
"48;2;#{r};#{g};#{b}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def no_color? = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Named ANSI color constants (0-15)
|
|
88
|
+
BLACK = ANSIColor.new(code: 0)
|
|
89
|
+
RED = ANSIColor.new(code: 1)
|
|
90
|
+
GREEN = ANSIColor.new(code: 2)
|
|
91
|
+
YELLOW = ANSIColor.new(code: 3)
|
|
92
|
+
BLUE = ANSIColor.new(code: 4)
|
|
93
|
+
MAGENTA = ANSIColor.new(code: 5)
|
|
94
|
+
CYAN = ANSIColor.new(code: 6)
|
|
95
|
+
WHITE = ANSIColor.new(code: 7)
|
|
96
|
+
BRIGHT_BLACK = ANSIColor.new(code: 8)
|
|
97
|
+
BRIGHT_RED = ANSIColor.new(code: 9)
|
|
98
|
+
BRIGHT_GREEN = ANSIColor.new(code: 10)
|
|
99
|
+
BRIGHT_YELLOW = ANSIColor.new(code: 11)
|
|
100
|
+
BRIGHT_BLUE = ANSIColor.new(code: 12)
|
|
101
|
+
BRIGHT_MAGENTA = ANSIColor.new(code: 13)
|
|
102
|
+
BRIGHT_CYAN = ANSIColor.new(code: 14)
|
|
103
|
+
BRIGHT_WHITE = ANSIColor.new(code: 15)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module ColorProfile
|
|
5
|
+
TRUE_COLOR = :true_color
|
|
6
|
+
ANSI256 = :ansi256
|
|
7
|
+
ANSI = :ansi
|
|
8
|
+
NO_COLOR = :no_color
|
|
9
|
+
|
|
10
|
+
# ANSI256 to ANSI16 lookup table
|
|
11
|
+
ANSI256_TO_ANSI = [
|
|
12
|
+
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, # 0-15: identity
|
|
13
|
+
0, 4, 4, 4, 12, 12, # 16-21
|
|
14
|
+
2, 6, 4, 4, 12, 12, # 22-27
|
|
15
|
+
2, 2, 6, 4, 12, 12, # 28-33
|
|
16
|
+
2, 2, 2, 6, 12, 12, # 34-39
|
|
17
|
+
10, 10, 10, 10, 14, 12, # 40-45
|
|
18
|
+
10, 10, 10, 10, 10, 14, # 46-51
|
|
19
|
+
1, 5, 4, 4, 12, 12, # 52-57
|
|
20
|
+
3, 8, 4, 4, 12, 12, # 58-63
|
|
21
|
+
2, 2, 6, 4, 12, 12, # 64-69
|
|
22
|
+
2, 2, 2, 6, 12, 12, # 70-75
|
|
23
|
+
10, 10, 10, 10, 14, 12, # 76-81
|
|
24
|
+
10, 10, 10, 10, 10, 14, # 82-87
|
|
25
|
+
1, 5, 4, 4, 12, 12, # 88-93
|
|
26
|
+
3, 3, 8, 4, 12, 12, # 94-99
|
|
27
|
+
2, 2, 2, 6, 12, 12, # 100-105
|
|
28
|
+
2, 2, 2, 2, 6, 12, # 106-111
|
|
29
|
+
10, 10, 10, 10, 14, 12, # 112-117
|
|
30
|
+
10, 10, 10, 10, 10, 14, # 118-123
|
|
31
|
+
1, 5, 5, 4, 12, 12, # 124-129
|
|
32
|
+
3, 3, 8, 4, 12, 12, # 130-135
|
|
33
|
+
3, 3, 3, 8, 12, 12, # 136-141
|
|
34
|
+
2, 2, 2, 2, 6, 12, # 142-147
|
|
35
|
+
10, 10, 10, 10, 14, 12, # 148-153
|
|
36
|
+
10, 10, 10, 10, 10, 14, # 154-159
|
|
37
|
+
9, 5, 5, 5, 13, 12, # 160-165
|
|
38
|
+
3, 3, 8, 8, 12, 12, # 166-171
|
|
39
|
+
3, 3, 3, 8, 12, 12, # 172-177
|
|
40
|
+
3, 3, 3, 3, 8, 12, # 178-183
|
|
41
|
+
11, 11, 10, 10, 14, 12, # 184-189
|
|
42
|
+
10, 10, 10, 10, 10, 14, # 190-195
|
|
43
|
+
9, 9, 5, 5, 13, 12, # 196-201
|
|
44
|
+
9, 9, 9, 13, 13, 12, # 202-207
|
|
45
|
+
3, 3, 3, 8, 8, 12, # 208-213
|
|
46
|
+
3, 3, 3, 3, 8, 14, # 214-219
|
|
47
|
+
11, 11, 11, 11, 7, 12, # 220-225
|
|
48
|
+
11, 11, 11, 11, 11, 15, # 226-231
|
|
49
|
+
0, 0, 0, 0, 0, 0, # 232-237 (grayscale dark)
|
|
50
|
+
8, 8, 8, 8, 8, 8, # 238-243
|
|
51
|
+
7, 7, 7, 7, 7, 7, # 244-249
|
|
52
|
+
15, 15, 15, 15, 15, 15, # 250-255 (grayscale light)
|
|
53
|
+
].freeze
|
|
54
|
+
|
|
55
|
+
class << self
|
|
56
|
+
def detect
|
|
57
|
+
return NO_COLOR if ENV.key?("NO_COLOR")
|
|
58
|
+
|
|
59
|
+
colorterm = ENV.fetch("COLORTERM", "")
|
|
60
|
+
return TRUE_COLOR if %w[truecolor 24bit].include?(colorterm)
|
|
61
|
+
|
|
62
|
+
term = ENV.fetch("TERM", "")
|
|
63
|
+
return ANSI256 if term.include?("256color")
|
|
64
|
+
return ANSI if term.include?("color") || term.include?("ansi")
|
|
65
|
+
|
|
66
|
+
ANSI
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def downsample(color, target_profile)
|
|
70
|
+
return Color::NoColor.new if target_profile == NO_COLOR
|
|
71
|
+
|
|
72
|
+
case color
|
|
73
|
+
when Color::TrueColor
|
|
74
|
+
case target_profile
|
|
75
|
+
when TRUE_COLOR then color
|
|
76
|
+
when ANSI256 then truecolor_to_256(color)
|
|
77
|
+
when ANSI then ansi256_to_ansi(truecolor_to_256(color))
|
|
78
|
+
end
|
|
79
|
+
when Color::ANSI256Color
|
|
80
|
+
case target_profile
|
|
81
|
+
when TRUE_COLOR, ANSI256 then color
|
|
82
|
+
when ANSI then ansi256_to_ansi(color)
|
|
83
|
+
end
|
|
84
|
+
else # ANSIColor, NoColor, etc.
|
|
85
|
+
color
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def truecolor_to_256(color)
|
|
92
|
+
# Check grayscale ramp first (232-255)
|
|
93
|
+
if color.r == color.g && color.g == color.b
|
|
94
|
+
return Color::ANSI256Color.new(code: 16) if color.r < 8
|
|
95
|
+
return Color::ANSI256Color.new(code: 231) if color.r > 248
|
|
96
|
+
|
|
97
|
+
gray_idx = ((color.r.to_f - 8) / 247 * 24).round
|
|
98
|
+
return Color::ANSI256Color.new(code: 232 + gray_idx)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Map to 6x6x6 color cube (indices 16-231)
|
|
102
|
+
r_idx = (color.r.to_f / 255 * 5).round
|
|
103
|
+
g_idx = (color.g.to_f / 255 * 5).round
|
|
104
|
+
b_idx = (color.b.to_f / 255 * 5).round
|
|
105
|
+
|
|
106
|
+
cube_idx = 16 + (36 * r_idx) + (6 * g_idx) + b_idx
|
|
107
|
+
|
|
108
|
+
# Compare cube color distance vs nearest grayscale
|
|
109
|
+
cube_r = r_idx.positive? ? 55 + (r_idx * 40) : 0
|
|
110
|
+
cube_g = g_idx.positive? ? 55 + (g_idx * 40) : 0
|
|
111
|
+
cube_b = b_idx.positive? ? 55 + (b_idx * 40) : 0
|
|
112
|
+
cube_dist = color_distance(color.r, color.g, color.b, cube_r, cube_g, cube_b)
|
|
113
|
+
|
|
114
|
+
gray_avg = (color.r + color.g + color.b) / 3
|
|
115
|
+
gray_idx = ((gray_avg.to_f - 8) / 247 * 24).round.clamp(0, 23)
|
|
116
|
+
gray_val = 8 + (10 * gray_idx)
|
|
117
|
+
gray_dist = color_distance(color.r, color.g, color.b, gray_val, gray_val, gray_val)
|
|
118
|
+
|
|
119
|
+
if gray_dist < cube_dist
|
|
120
|
+
Color::ANSI256Color.new(code: 232 + gray_idx)
|
|
121
|
+
else
|
|
122
|
+
Color::ANSI256Color.new(code: cube_idx)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ansi256_to_ansi(color)
|
|
127
|
+
idx = color.code.clamp(0, 255)
|
|
128
|
+
Color::ANSIColor.new(code: ANSI256_TO_ANSI[idx])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def color_distance(r1, g1, b1, r2, g2, b2)
|
|
132
|
+
((r1 - r2)**2) + ((g1 - g2)**2) + ((b1 - b2)**2)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module Join
|
|
5
|
+
class << self
|
|
6
|
+
def horizontal(position, *strs)
|
|
7
|
+
strs = strs.flatten
|
|
8
|
+
return "" if strs.empty?
|
|
9
|
+
|
|
10
|
+
blocks = strs.map { |s| s.split("\n", -1) }
|
|
11
|
+
max_height = blocks.map(&:length).max
|
|
12
|
+
|
|
13
|
+
# Equalize heights using position for vertical alignment
|
|
14
|
+
blocks = blocks.map do |lines|
|
|
15
|
+
if lines.length < max_height
|
|
16
|
+
Align.vertical(lines, max_height, position)
|
|
17
|
+
else
|
|
18
|
+
lines
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Find max width of each block
|
|
23
|
+
widths = blocks.map do |lines|
|
|
24
|
+
lines.map { |l| ANSI.printable_width(l) }.max || 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Join line by line
|
|
28
|
+
(0...max_height).map do |row|
|
|
29
|
+
blocks.each_with_index.map do |lines, idx|
|
|
30
|
+
line = lines[row] || ""
|
|
31
|
+
# Pad all blocks except the last to their max width
|
|
32
|
+
if idx < blocks.length - 1
|
|
33
|
+
line_width = ANSI.printable_width(line)
|
|
34
|
+
pad = widths[idx] - line_width
|
|
35
|
+
pad.positive? ? "#{line}#{" " * pad}" : line
|
|
36
|
+
else
|
|
37
|
+
line
|
|
38
|
+
end
|
|
39
|
+
end.join
|
|
40
|
+
end.join("\n")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def vertical(position, *strs)
|
|
44
|
+
strs = strs.flatten
|
|
45
|
+
return "" if strs.empty?
|
|
46
|
+
|
|
47
|
+
blocks = strs.map { |s| s.split("\n", -1) }
|
|
48
|
+
|
|
49
|
+
# Find max width across all blocks
|
|
50
|
+
max_width = blocks.flat_map { |lines| lines.map { |l| ANSI.printable_width(l) } }.max || 0
|
|
51
|
+
|
|
52
|
+
# Align each block's lines horizontally
|
|
53
|
+
all_lines = blocks.flat_map do |lines|
|
|
54
|
+
Align.horizontal(lines, max_width, position)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
all_lines.join("\n")
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Flourish
|
|
4
|
+
module Place
|
|
5
|
+
class << self
|
|
6
|
+
def place(width, height, h_pos, v_pos, str)
|
|
7
|
+
lines = str.split("\n", -1)
|
|
8
|
+
|
|
9
|
+
# Horizontal placement
|
|
10
|
+
lines = Align.horizontal(lines, width, h_pos)
|
|
11
|
+
|
|
12
|
+
# Vertical placement
|
|
13
|
+
lines = Align.vertical(lines, height, v_pos)
|
|
14
|
+
|
|
15
|
+
# Ensure all lines are padded to full width
|
|
16
|
+
lines.map do |line|
|
|
17
|
+
line_width = ANSI.printable_width(line)
|
|
18
|
+
pad = width - line_width
|
|
19
|
+
pad.positive? ? "#{line}#{" " * pad}" : line
|
|
20
|
+
end.join("\n")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def place_horizontal(width, pos, str)
|
|
24
|
+
lines = str.split("\n", -1)
|
|
25
|
+
Align.horizontal(lines, width, pos).join("\n")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def place_vertical(height, pos, str)
|
|
29
|
+
lines = str.split("\n", -1)
|
|
30
|
+
Align.vertical(lines, height, pos).join("\n")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|