ruby_rich 0.3.1 → 0.4.1

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,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRich
4
+ class Sidebar
5
+ attr_accessor :width, :height
6
+ attr_reader :plan, :tasks
7
+
8
+ def initialize(plan: "", tasks: [])
9
+ @plan = plan
10
+ @tasks = tasks
11
+ @width = 0
12
+ @height = 0
13
+ @focused = false
14
+ end
15
+
16
+ def focus
17
+ @focused = true
18
+ self
19
+ end
20
+
21
+ def blur
22
+ @focused = false
23
+ self
24
+ end
25
+
26
+ def update_plan(text)
27
+ @plan = text.to_s
28
+ self
29
+ end
30
+
31
+ def set_tasks(tasks)
32
+ @tasks = tasks
33
+ self
34
+ end
35
+
36
+ def add_task(label, status: :pending)
37
+ @tasks << { label: label, status: status }
38
+ self
39
+ end
40
+
41
+ def render
42
+ plan_height = [(@height * 0.48).floor, 3].max
43
+ tasks_height = [@height - plan_height, 3].max
44
+ [
45
+ *panel_lines("Plan", @plan, plan_height),
46
+ *panel_lines("Tasks", render_tasks, tasks_height)
47
+ ].first(@height)
48
+ end
49
+
50
+ private
51
+
52
+ def panel_lines(title, content, height)
53
+ panel = Panel.new(content, title: title, border_style: @focused ? :green : :blue, title_align: :left)
54
+ panel.width = @width
55
+ panel.height = height
56
+ panel.render
57
+ end
58
+
59
+ def render_tasks
60
+ return "No active tasks" if @tasks.empty?
61
+
62
+ @tasks.map do |task|
63
+ case task
64
+ when Hash
65
+ "#{status_marker(task[:status])} #{task[:label]} #{AnsiCode.color(:black, true)}#{task[:status]}#{AnsiCode.reset}"
66
+ else
67
+ "• #{task}"
68
+ end
69
+ end.join("\n")
70
+ end
71
+
72
+ def status_marker(status)
73
+ case status
74
+ when :done, :completed
75
+ "#{AnsiCode.color(:green, true)}✓#{AnsiCode.reset}"
76
+ when :running, :in_progress
77
+ "#{AnsiCode.color(:blue, true)}●#{AnsiCode.reset}"
78
+ when :failed, :error
79
+ "#{AnsiCode.color(:red, true)}!#{AnsiCode.reset}"
80
+ else
81
+ "#{AnsiCode.color(:black, true)}○#{AnsiCode.reset}"
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyRich
4
+ class SlashInput
5
+ attr_reader :value, :selected_index
6
+
7
+ DEFAULT_MENU_LIMIT = 8
8
+
9
+ def initialize(prompt: "> ", items: [], width: nil, menu_limit: DEFAULT_MENU_LIMIT, on_select: nil, on_submit: nil)
10
+ @prompt = prompt
11
+ @items = normalize_items(items)
12
+ @width = width
13
+ @menu_limit = menu_limit
14
+ @on_select = on_select
15
+ @on_submit = on_submit
16
+ @value = ""
17
+ @selected_index = 0
18
+ @menu_open = false
19
+ end
20
+
21
+ def attach(layout, priority: 100)
22
+ [:string, :backspace, :up, :down, :enter, :escape].each do |event_name|
23
+ layout.key(event_name, priority) do |event_data, live|
24
+ handle_key(event_data, live)
25
+ false
26
+ end
27
+ end
28
+ self
29
+ end
30
+
31
+ def handle_key(event_data, live = nil)
32
+ case event_data[:name]
33
+ when :string
34
+ append(event_data[:value].to_s)
35
+ when :backspace
36
+ backspace
37
+ when :up
38
+ move_selection(-1)
39
+ when :down
40
+ move_selection(1)
41
+ when :enter
42
+ enter(live)
43
+ when :escape
44
+ close_menu
45
+ end
46
+ end
47
+
48
+ def render
49
+ lines = [input_line]
50
+ return fit_lines(lines) unless menu_open?
51
+
52
+ matches = filtered_items.first(@menu_limit)
53
+ if matches.empty?
54
+ lines << "#{AnsiCode.color(:yellow)} No matches#{AnsiCode.reset}"
55
+ else
56
+ matches.each_with_index do |item, index|
57
+ lines << render_item(item, index == @selected_index)
58
+ end
59
+ end
60
+
61
+ fit_lines(lines)
62
+ end
63
+
64
+ def menu_open?
65
+ @menu_open
66
+ end
67
+
68
+ private
69
+
70
+ def append(text)
71
+ @value += text
72
+ @menu_open = true if text == "/" || @menu_open
73
+ clamp_selection
74
+ end
75
+
76
+ def backspace
77
+ @value = @value[0...-1].to_s
78
+ @menu_open = false unless @value.include?("/")
79
+ clamp_selection
80
+ end
81
+
82
+ def enter(live)
83
+ if @menu_open
84
+ select_current(live)
85
+ else
86
+ submitted_value = @value
87
+ @on_submit&.call(submitted_value, live)
88
+ reset_input
89
+ end
90
+ end
91
+
92
+ def select_current(live)
93
+ item = filtered_items[@selected_index]
94
+ return unless item
95
+
96
+ @value = replace_query_with(item[:value])
97
+ @menu_open = false
98
+ @selected_index = 0
99
+ @on_select&.call(item, live)
100
+ end
101
+
102
+ def move_selection(delta)
103
+ return unless @menu_open
104
+
105
+ count = [filtered_items.size, @menu_limit].min
106
+ return if count.zero?
107
+
108
+ @selected_index = (@selected_index + delta) % count
109
+ end
110
+
111
+ def close_menu
112
+ @menu_open = false
113
+ @selected_index = 0
114
+ end
115
+
116
+ def reset_input
117
+ @value = ""
118
+ close_menu
119
+ end
120
+
121
+ def filtered_items
122
+ query = current_query.downcase
123
+ return @items if query.empty?
124
+
125
+ @items.select do |item|
126
+ item[:label].downcase.include?(query) || item[:value].downcase.include?(query)
127
+ end
128
+ end
129
+
130
+ def current_query
131
+ slash_index = @value.rindex("/")
132
+ return "" unless slash_index
133
+
134
+ @value[(slash_index + 1)..].to_s
135
+ end
136
+
137
+ def replace_query_with(replacement)
138
+ slash_index = @value.rindex("/")
139
+ return @value unless slash_index
140
+
141
+ @value[0...slash_index].to_s + replacement
142
+ end
143
+
144
+ def clamp_selection
145
+ count = [filtered_items.size, @menu_limit].min
146
+ @selected_index = 0 if count.zero? || @selected_index >= count
147
+ end
148
+
149
+ def input_line
150
+ "#{@prompt}#{@value}"
151
+ end
152
+
153
+ def render_item(item, selected)
154
+ marker = selected ? ">" : " "
155
+ color = selected ? AnsiCode.inverse : AnsiCode.color(:white)
156
+ description = item[:description].to_s
157
+ suffix = description.empty? ? "" : " #{AnsiCode.color(:black, true)}#{description}#{AnsiCode.reset}"
158
+ " #{color}#{marker} #{item[:label]}#{AnsiCode.reset}#{suffix}"
159
+ end
160
+
161
+ def fit_lines(lines)
162
+ return lines unless @width
163
+
164
+ lines.map { |line| truncate_display(line, @width) }
165
+ end
166
+
167
+ def truncate_display(line, max_width)
168
+ return line if line.display_width <= max_width
169
+
170
+ result = ""
171
+ width = 0
172
+ line.each_char do |char|
173
+ char_width = char.display_width
174
+ break if width + char_width > max_width
175
+
176
+ result += char
177
+ width += char_width
178
+ end
179
+ result
180
+ end
181
+
182
+ def normalize_items(items)
183
+ items.map do |item|
184
+ case item
185
+ when Hash
186
+ {
187
+ label: item.fetch(:label, item.fetch("label", item[:value] || item["value"])).to_s,
188
+ value: item.fetch(:value, item.fetch("value", item[:label] || item["label"])).to_s,
189
+ description: item.fetch(:description, item.fetch("description", "")).to_s
190
+ }
191
+ else
192
+ { label: item.to_s, value: item.to_s, description: "" }
193
+ end
194
+ end
195
+ end
196
+ end
197
+ end
@@ -0,0 +1,246 @@
1
+ module RubyRich
2
+ class Status
3
+ # 状态指示器类型
4
+ INDICATORS = {
5
+ # 简单状态
6
+ success: { symbol: '✅', color: "\e[92m", text: 'Success' },
7
+ error: { symbol: '❌', color: "\e[91m", text: 'Error' },
8
+ warning: { symbol: '⚠️', color: "\e[93m", text: 'Warning' },
9
+ info: { symbol: 'ℹ️', color: "\e[94m", text: 'Info' },
10
+
11
+ # 进度状态
12
+ pending: { symbol: '⏳', color: "\e[93m", text: 'Pending' },
13
+ running: { symbol: '🏃', color: "\e[94m", text: 'Running' },
14
+ completed: { symbol: '✅', color: "\e[92m", text: 'Completed' },
15
+ failed: { symbol: '💥', color: "\e[91m", text: 'Failed' },
16
+
17
+ # 系统状态
18
+ online: { symbol: '🟢', color: "\e[92m", text: 'Online' },
19
+ offline: { symbol: '🔴', color: "\e[91m", text: 'Offline' },
20
+ maintenance: { symbol: '🔧', color: "\e[93m", text: 'Maintenance' },
21
+
22
+ # 安全状态
23
+ secure: { symbol: '🔒', color: "\e[92m", text: 'Secure' },
24
+ insecure: { symbol: '🔓', color: "\e[91m", text: 'Insecure' },
25
+
26
+ # 等级状态
27
+ low: { symbol: '🔵', color: "\e[94m", text: 'Low' },
28
+ medium: { symbol: '🟡', color: "\e[93m", text: 'Medium' },
29
+ high: { symbol: '🔴', color: "\e[91m", text: 'High' },
30
+ critical: { symbol: '💀', color: "\e[95m", text: 'Critical' }
31
+ }.freeze
32
+
33
+ # 加载动画帧
34
+ SPINNER_FRAMES = {
35
+ dots: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'],
36
+ line: ['|', '/', '-', '\\'],
37
+ arrow: ['←', '↖', '↑', '↗', '→', '↘', '↓', '↙'],
38
+ bounce: ['⠁', '⠂', '⠄', '⠂'],
39
+ pulse: ['●', '◐', '◑', '◒', '◓', '◔', '◕', '○'],
40
+ clock: ['🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛']
41
+ }.freeze
42
+
43
+ def self.indicator(type, text: nil, show_text: true, colorize: true)
44
+ config = INDICATORS[type.to_sym]
45
+ return "Unknown status: #{type}" unless config
46
+
47
+ symbol = config[:symbol]
48
+ color = colorize ? config[:color] : ""
49
+ reset = colorize ? "\e[0m" : ""
50
+ status_text = text || config[:text]
51
+
52
+ if show_text
53
+ "#{color}#{symbol} #{status_text}#{reset}"
54
+ else
55
+ "#{color}#{symbol}#{reset}"
56
+ end
57
+ end
58
+
59
+ def self.spinner(type: :dots, text: 'Loading...', delay: 0.1)
60
+ frames = SPINNER_FRAMES[type.to_sym] || SPINNER_FRAMES[:dots]
61
+
62
+ Thread.new do
63
+ frame_index = 0
64
+ loop do
65
+ print "\r\e[K\e[94m#{frames[frame_index]}\e[0m #{text}"
66
+ $stdout.flush
67
+ sleep delay
68
+ frame_index = (frame_index + 1) % frames.length
69
+ end
70
+ end
71
+ end
72
+
73
+ def self.stop_spinner(final_message: nil)
74
+ if final_message
75
+ print "\r\e[K#{final_message}\n"
76
+ else
77
+ print "\r\e[K"
78
+ end
79
+ $stdout.flush
80
+ end
81
+
82
+ # 静态进度条
83
+ def self.progress_bar(current, total, width: 30, style: :filled)
84
+ percentage = (current.to_f / total * 100).round(1)
85
+ filled_width = (current.to_f / total * width).round
86
+
87
+ case style
88
+ when :filled
89
+ filled = '█' * filled_width
90
+ empty = '░' * (width - filled_width)
91
+ bar = "#{filled}#{empty}"
92
+ when :blocks
93
+ filled = '■' * filled_width
94
+ empty = '□' * (width - filled_width)
95
+ bar = "#{filled}#{empty}"
96
+ when :dots
97
+ filled = '●' * filled_width
98
+ empty = '○' * (width - filled_width)
99
+ bar = "#{filled}#{empty}"
100
+ else
101
+ filled = '=' * filled_width
102
+ empty = '-' * (width - filled_width)
103
+ bar = "#{filled}#{empty}"
104
+ end
105
+
106
+ "\e[92m[#{bar}]\e[0m #{percentage}% (#{current}/#{total})"
107
+ end
108
+
109
+ # 状态板
110
+ class StatusBoard
111
+ def initialize(width: 60)
112
+ @width = width
113
+ @items = []
114
+ end
115
+
116
+ def add_item(label, status, description: nil)
117
+ @items << {
118
+ label: label,
119
+ status: status,
120
+ description: description
121
+ }
122
+ self
123
+ end
124
+
125
+ def render(show_descriptions: true, align_status: :right)
126
+ lines = []
127
+ lines << "┌#{'─' * (@width - 2)}┐"
128
+
129
+ @items.each do |item|
130
+ label = item[:label]
131
+ status_text = RubyRich::Status.indicator(item[:status])
132
+ description = item[:description]
133
+
134
+ # 计算实际显示宽度(排除 ANSI 代码)
135
+ status_display_width = status_text.gsub(/\e\[[0-9;]*m/, '').length
136
+
137
+ case align_status
138
+ when :right
139
+ padding = @width - 4 - label.length - status_display_width
140
+ padding = [padding, 1].max
141
+ main_line = "│ #{label}#{' ' * padding}#{status_text} │"
142
+ when :left
143
+ padding = @width - 4 - label.length - status_display_width
144
+ padding = [padding, 1].max
145
+ main_line = "│ #{status_text} #{label}#{' ' * padding}│"
146
+ else # center
147
+ total_content = label.length + status_display_width + 1
148
+ left_padding = [(@width - 2 - total_content) / 2, 1].max
149
+ right_padding = @width - 2 - total_content - left_padding
150
+ main_line = "│#{' ' * left_padding}#{label} #{status_text}#{' ' * right_padding}│"
151
+ end
152
+
153
+ lines << main_line
154
+
155
+ if show_descriptions && description
156
+ desc_lines = wrap_text(description, @width - 4)
157
+ desc_lines.each do |desc_line|
158
+ padding = @width - 4 - desc_line.length
159
+ lines << "│ \e[90m#{desc_line}#{' ' * padding}\e[0m │"
160
+ end
161
+ end
162
+ end
163
+
164
+ lines << "└#{'─' * (@width - 2)}┘"
165
+ lines.join("\n")
166
+ end
167
+
168
+ private
169
+
170
+ def wrap_text(text, max_width)
171
+ words = text.split(' ')
172
+ lines = []
173
+ current_line = ''
174
+
175
+ words.each do |word|
176
+ if (current_line + ' ' + word).length <= max_width
177
+ current_line += current_line.empty? ? word : ' ' + word
178
+ else
179
+ lines << current_line unless current_line.empty?
180
+ current_line = word
181
+ end
182
+ end
183
+
184
+ lines << current_line unless current_line.empty?
185
+ lines
186
+ end
187
+ end
188
+
189
+ # 实时状态监控
190
+ class Monitor
191
+ def initialize(refresh_rate: 1.0)
192
+ @refresh_rate = refresh_rate
193
+ @items = {}
194
+ @running = false
195
+ end
196
+
197
+ def add_item(key, label, &block)
198
+ @items[key] = {
199
+ label: label,
200
+ block: block
201
+ }
202
+ self
203
+ end
204
+
205
+ def start
206
+ @running = true
207
+
208
+ Thread.new do
209
+ while @running
210
+ system('clear')
211
+ puts render_status
212
+ sleep @refresh_rate
213
+ end
214
+ end
215
+ end
216
+
217
+ def stop
218
+ @running = false
219
+ end
220
+
221
+ private
222
+
223
+ def render_status
224
+ lines = []
225
+ lines << "\e[1m\e[96mSystem Status Monitor\e[0m"
226
+ lines << "─" * 40
227
+ lines << ""
228
+
229
+ @items.each do |key, item|
230
+ begin
231
+ status = item[:block].call
232
+ status_indicator = RubyRich::Status.indicator(status)
233
+ lines << "#{item[:label]}: #{status_indicator}"
234
+ rescue => e
235
+ error_indicator = RubyRich::Status.indicator(:error, text: "Error: #{e.message}")
236
+ lines << "#{item[:label]}: #{error_indicator}"
237
+ end
238
+ end
239
+
240
+ lines << ""
241
+ lines << "\e[90mPress Ctrl+C to stop monitoring\e[0m"
242
+ lines.join("\n")
243
+ end
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,171 @@
1
+ require 'rouge'
2
+
3
+ module RubyRich
4
+ class Syntax
5
+ # 支持的语言别名映射
6
+ LANGUAGE_ALIASES = {
7
+ 'rb' => 'ruby',
8
+ 'py' => 'python',
9
+ 'js' => 'javascript',
10
+ 'ts' => 'typescript',
11
+ 'sh' => 'shell',
12
+ 'bash' => 'shell',
13
+ 'zsh' => 'shell',
14
+ 'yml' => 'yaml',
15
+ 'md' => 'markdown'
16
+ }.freeze
17
+
18
+ # 语法高亮主题颜色映射
19
+ THEME_COLORS = {
20
+ # Rouge token types to ANSI colors
21
+ 'Comment' => "\e[90m", # Bright black (gray)
22
+ 'Comment.Single' => "\e[90m",
23
+ 'Comment.Multiline' => "\e[90m",
24
+ 'Comment.Preproc' => "\e[95m", # Bright magenta
25
+
26
+ 'Keyword' => "\e[94m", # Bright blue
27
+ 'Keyword.Constant' => "\e[94m",
28
+ 'Keyword.Declaration' => "\e[94m",
29
+ 'Keyword.Namespace' => "\e[94m",
30
+ 'Keyword.Pseudo' => "\e[94m",
31
+ 'Keyword.Reserved' => "\e[94m",
32
+ 'Keyword.Type' => "\e[94m",
33
+
34
+ 'Literal' => "\e[96m", # Bright cyan
35
+ 'Literal.Date' => "\e[96m",
36
+ 'Literal.Number' => "\e[93m", # Bright yellow
37
+ 'Literal.Number.Bin' => "\e[93m",
38
+ 'Literal.Number.Float' => "\e[93m",
39
+ 'Literal.Number.Hex' => "\e[93m",
40
+ 'Literal.Number.Integer' => "\e[93m",
41
+ 'Literal.Number.Oct' => "\e[93m",
42
+
43
+ 'Literal.String' => "\e[92m", # Bright green
44
+ 'Literal.String.Affix' => "\e[92m",
45
+ 'Literal.String.Backtick' => "\e[92m",
46
+ 'Literal.String.Char' => "\e[92m",
47
+ 'Literal.String.Doc' => "\e[92m",
48
+ 'Literal.String.Double' => "\e[92m",
49
+ 'Literal.String.Escape' => "\e[96m",
50
+ 'Literal.String.Heredoc' => "\e[92m",
51
+ 'Literal.String.Interpol' => "\e[96m",
52
+ 'Literal.String.Other' => "\e[92m",
53
+ 'Literal.String.Regex' => "\e[91m", # Bright red
54
+ 'Literal.String.Single' => "\e[92m",
55
+ 'Literal.String.Symbol' => "\e[95m", # Bright magenta
56
+
57
+ 'Name' => "\e[97m", # Bright white
58
+ 'Name.Attribute' => "\e[93m", # Bright yellow
59
+ 'Name.Builtin' => "\e[96m", # Bright cyan
60
+ 'Name.Builtin.Pseudo' => "\e[96m",
61
+ 'Name.Class' => "\e[93m", # Bright yellow
62
+ 'Name.Constant' => "\e[93m",
63
+ 'Name.Decorator' => "\e[95m", # Bright magenta
64
+ 'Name.Entity' => "\e[93m",
65
+ 'Name.Exception' => "\e[91m", # Bright red
66
+ 'Name.Function' => "\e[96m", # Bright cyan
67
+ 'Name.Property' => "\e[96m",
68
+ 'Name.Label' => "\e[95m",
69
+ 'Name.Namespace' => "\e[93m",
70
+ 'Name.Other' => "\e[97m",
71
+ 'Name.Tag' => "\e[94m", # Bright blue
72
+ 'Name.Variable' => "\e[97m",
73
+ 'Name.Variable.Class' => "\e[97m",
74
+ 'Name.Variable.Global' => "\e[97m",
75
+ 'Name.Variable.Instance' => "\e[97m",
76
+
77
+ 'Operator' => "\e[91m", # Bright red
78
+ 'Operator.Word' => "\e[94m", # Bright blue
79
+
80
+ 'Punctuation' => "\e[97m", # Bright white
81
+
82
+ 'Error' => "\e[101m\e[97m", # Red background, white text
83
+ 'Generic.Deleted' => "\e[91m", # Bright red
84
+ 'Generic.Emph' => "\e[3m", # Italic
85
+ 'Generic.Error' => "\e[91m", # Bright red
86
+ 'Generic.Heading' => "\e[1m\e[94m", # Bold bright blue
87
+ 'Generic.Inserted' => "\e[92m", # Bright green
88
+ 'Generic.Output' => "\e[90m", # Bright black (gray)
89
+ 'Generic.Prompt' => "\e[1m", # Bold
90
+ 'Generic.Strong' => "\e[1m", # Bold
91
+ 'Generic.Subheading' => "\e[95m", # Bright magenta
92
+ 'Generic.Traceback' => "\e[91m" # Bright red
93
+ }.freeze
94
+
95
+ def self.highlight(code, language = nil, theme: :default)
96
+ new(theme: theme).highlight(code, language)
97
+ end
98
+
99
+ def initialize(theme: :default)
100
+ @theme = theme
101
+ end
102
+
103
+ def highlight(code, language = nil)
104
+ return code if code.nil? || code.empty?
105
+
106
+ # 检测或规范化语言
107
+ language = detect_language(code) if language.nil?
108
+ language = normalize_language(language) if language
109
+
110
+ # 如果无法检测语言,返回原始代码
111
+ return code unless language
112
+
113
+ begin
114
+ lexer = Rouge::Lexer.find(language)
115
+ return code unless lexer
116
+
117
+ # 使用自定义格式化器
118
+ formatter = AnsiFormatter.new
119
+ formatter.format(lexer.lex(code))
120
+ rescue => e
121
+ # 如果高亮失败,返回原始代码
122
+ code
123
+ end
124
+ end
125
+
126
+ def list_languages
127
+ Rouge::Lexer.all.map(&:tag).sort
128
+ end
129
+
130
+ private
131
+
132
+ def detect_language(code)
133
+ # 简单的语言检测启发式
134
+ return 'ruby' if code.include?('def ') && code.include?('end')
135
+ return 'python' if code.include?('def ') && code.include?(':')
136
+ return 'javascript' if code.include?('function') || code.include?('=>')
137
+ return 'html' if code.include?('<html') || code.include?('<!DOCTYPE')
138
+ return 'css' if code.match?(/\w+\s*{[^}]*}/)
139
+ return 'shell' if code.start_with?('#!/bin/') || code.include?('$ ')
140
+ return 'json' if code.strip.start_with?('{') && code.include?(':')
141
+ return 'yaml' if code.include?('---') || code.match?(/^\w+:/)
142
+
143
+ nil
144
+ end
145
+
146
+ def normalize_language(language)
147
+ return nil if language.nil?
148
+ language = language.to_s.downcase.strip
149
+ LANGUAGE_ALIASES[language] || language
150
+ end
151
+
152
+ # 自定义 ANSI 格式化器
153
+ class AnsiFormatter
154
+ def format(tokens)
155
+ output = ''
156
+ tokens.each do |token, value|
157
+ color_code = THEME_COLORS[token.qualname] ||
158
+ THEME_COLORS[token.token_chain.map(&:qualname).find { |t| THEME_COLORS[t] }] ||
159
+ ''
160
+
161
+ if color_code.empty?
162
+ output << value
163
+ else
164
+ output << "#{color_code}#{value}\e[0m"
165
+ end
166
+ end
167
+ output
168
+ end
169
+ end
170
+ end
171
+ end