chamomile-flourish 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 73bccda63b64cd67d97ef4b8ff7c098e4098cce08426894b8a717df63d640dfb
4
- data.tar.gz: d26ed006acffbf7a6863b055ed69543811c83450fbf2ac5b27a92b7f06ed1d31
3
+ metadata.gz: 9e547c3138b8f6538023e1d92e96e0f28eed52e913acbb54257ee69364bc9163
4
+ data.tar.gz: f9b5f2b82fb6ac380c147c1aa030c6f12c9702f0f596d4e6d7dfdddbf50fec16
5
5
  SHA512:
6
- metadata.gz: 93ba8923e8bf8303cf5ec49a909917a308df7cb1352597c847bb1e305aab0bb406c8db8a4fdd53758eda8b674b082eef028cc65ce4acba1b7be50832cccfb819
7
- data.tar.gz: eb13351362d89be36d4876c81bf4983f0f26a72d8d06bb2314fb7dc7bf92cf2129cd0aaaa594f996e8ed0109befb1e74bab75d89a11d284ed82d1c3eb819d2b3
6
+ metadata.gz: 3f94a17ac8aaf144cafa925c998edaf7e6385f3636653d1c837af422f0196027a65b98d0c068f759ee7dc45928ef5c3cf67e6a26eaf08d114f3a7a12b8423d12
7
+ data.tar.gz: 9a9767c8adb1fc23bf59d35ec430cad99e6025d2577904ce4e0ff9b85318fd8c528d3361ff2ab55a417458bee83f0daaffe1d87434f00ccede0a1262e1ddcf88
data/lib/flourish.rb CHANGED
@@ -1,104 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "flourish/version"
4
- require_relative "flourish/ansi"
5
- require_relative "flourish/color"
6
- require_relative "flourish/color_profile"
7
- require_relative "flourish/wrap"
8
- require_relative "flourish/border"
9
- require_relative "flourish/align"
10
- require_relative "flourish/join"
11
- require_relative "flourish/place"
12
- require_relative "flourish/style"
3
+ require "chamomile"
13
4
 
14
- module Flourish
15
- # Position constants (kept for backward compat)
16
- TOP = 0.0
17
- LEFT = 0.0
18
- CENTER = 0.5
19
- BOTTOM = 1.0
20
- RIGHT = 1.0
5
+ warn "[DEPRECATION] The `chamomile-flourish` gem is deprecated. " \
6
+ "All styling is now part of `chamomile` (v1.0+). " \
7
+ "Replace `require \"flourish\"` with `require \"chamomile\"` " \
8
+ "and change `Flourish::` to `Chamomile::`."
21
9
 
22
- # Symbol-to-float position map
23
- POSITION_MAP = {
24
- top: 0.0,
25
- left: 0.0,
26
- center: 0.5,
27
- bottom: 1.0,
28
- right: 1.0,
29
- }.freeze
30
-
31
- class << self
32
- def width(str)
33
- ANSI.printable_width(str)
34
- end
35
-
36
- def height(str)
37
- ANSI.height(str)
38
- end
39
-
40
- def size(str)
41
- ANSI.size(str)
42
- end
43
-
44
- # New primary API — accepts array or block, keyword align
45
- def horizontal(strs = nil, align: :top, &block)
46
- strs = block.call if block && strs.nil?
47
- strs = Array(strs)
48
- Join.horizontal(resolve_position(align), *strs)
49
- end
50
-
51
- # New primary API — accepts array or block, keyword align
52
- def vertical(strs = nil, align: :left, &block)
53
- strs = block.call if block && strs.nil?
54
- strs = Array(strs)
55
- Join.vertical(resolve_position(align), *strs)
56
- end
57
-
58
- # New primary API — content first, keyword args
59
- # Also supports old positional form for backward compat
60
- def place(first, second = nil, third = nil, fourth = nil, fifth = nil,
61
- width: nil, height: nil, align: :left, valign: :top, content: nil)
62
- if fifth
63
- # Old 5-arg form: place(width, height, h_pos, v_pos, str)
64
- Place.place(first, second, resolve_position(third), resolve_position(fourth), fifth.to_s)
65
- else
66
- # New keyword form: place(content, width:, height:, align:, valign:)
67
- content = (content || first).to_s
68
- Place.place(width || 80, height || 24,
69
- resolve_position(align), resolve_position(valign), content)
70
- end
71
- end
72
-
73
- # Old API — kept for backward compat
74
- def join_horizontal(position, *strs)
75
- Join.horizontal(resolve_position(position), *strs)
76
- end
77
-
78
- # Old API — kept for backward compat
79
- def join_vertical(position, *strs)
80
- Join.vertical(resolve_position(position), *strs)
81
- end
82
-
83
- def place_horizontal(width, pos, str)
84
- Place.place_horizontal(width, resolve_position(pos), str)
85
- end
86
-
87
- def place_vertical(height, pos, str)
88
- Place.place_vertical(height, resolve_position(pos), str)
89
- end
90
-
91
- # Convert symbol positions to float values.
92
- # Accepts symbols (:top, :left, :center, :bottom, :right) or floats.
93
- def resolve_position(val)
94
- case val
95
- when Symbol
96
- POSITION_MAP.fetch(val) { raise ArgumentError, "Unknown position: #{val.inspect}" }
97
- when Numeric
98
- val.to_f
99
- else
100
- raise ArgumentError, "Expected a Symbol or Numeric position, got #{val.inspect}"
101
- end
102
- end
103
- end
104
- end
10
+ Flourish = Chamomile unless defined?(Flourish)
metadata CHANGED
@@ -1,70 +1,50 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chamomile-flourish
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jack Killilea
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-03-12 00:00:00.000000000 Z
11
+ date: 2026-03-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: rspec
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '3.12'
20
- type: :development
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: '3.12'
27
- - !ruby/object:Gem::Dependency
28
- name: rubocop
14
+ name: chamomile
29
15
  requirement: !ruby/object:Gem::Requirement
30
16
  requirements:
31
17
  - - "~>"
32
18
  - !ruby/object:Gem::Version
33
19
  version: '1.0'
34
- type: :development
20
+ type: :runtime
35
21
  prerelease: false
36
22
  version_requirements: !ruby/object:Gem::Requirement
37
23
  requirements:
38
24
  - - "~>"
39
25
  - !ruby/object:Gem::Version
40
26
  version: '1.0'
41
- description: CSS-like box model styling for terminal output colors, padding, margins,
42
- borders, alignment
27
+ description: This gem is deprecated. All styling is now part of the chamomile gem
28
+ (v1.0+). This shim pulls in chamomile and aliases Flourish to Chamomile for backward
29
+ compatibility.
43
30
  email:
44
31
  executables: []
45
32
  extensions: []
46
33
  extra_rdoc_files: []
47
34
  files:
48
35
  - lib/flourish.rb
49
- - lib/flourish/align.rb
50
- - lib/flourish/ansi.rb
51
- - lib/flourish/border.rb
52
- - lib/flourish/color.rb
53
- - lib/flourish/color_profile.rb
54
- - lib/flourish/join.rb
55
- - lib/flourish/place.rb
56
- - lib/flourish/style.rb
57
- - lib/flourish/version.rb
58
- - lib/flourish/wrap.rb
59
- homepage: https://github.com/chamomile-rb/flourish
36
+ homepage: https://github.com/chamomile-rb/chamomile
60
37
  licenses:
61
38
  - MIT
62
39
  metadata:
63
40
  rubygems_mfa_required: 'true'
64
- homepage_uri: https://github.com/chamomile-rb/flourish
65
- source_code_uri: https://github.com/chamomile-rb/flourish
66
- changelog_uri: https://github.com/chamomile-rb/flourish/blob/master/CHANGELOG.md
67
- post_install_message:
41
+ homepage_uri: https://github.com/chamomile-rb/chamomile
42
+ source_code_uri: https://github.com/chamomile-rb/chamomile
43
+ changelog_uri: https://github.com/chamomile-rb/chamomile/blob/master/CHANGELOG.md
44
+ post_install_message: |
45
+ [DEPRECATION] chamomile-flourish is deprecated.
46
+ All styling is now part of the `chamomile` gem (v1.0+).
47
+ Replace `gem "flourish"` with `gem "chamomile"` in your Gemfile.
68
48
  rdoc_options: []
69
49
  require_paths:
70
50
  - lib
@@ -82,5 +62,5 @@ requirements: []
82
62
  rubygems_version: 3.5.11
83
63
  signing_key:
84
64
  specification_version: 4
85
- summary: Terminal styling library for Ruby
65
+ summary: "[DEPRECATED] Use chamomile instead"
86
66
  test_files: []
@@ -1,33 +0,0 @@
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
data/lib/flourish/ansi.rb DELETED
@@ -1,103 +0,0 @@
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
@@ -1,67 +0,0 @@
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
@@ -1,105 +0,0 @@
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
@@ -1,136 +0,0 @@
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