log_bench 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/.standard.yml +3 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/Rakefile +10 -0
- data/exe/log_bench +87 -0
- data/lib/log_bench/app/filter.rb +60 -0
- data/lib/log_bench/app/input_handler.rb +245 -0
- data/lib/log_bench/app/main.rb +96 -0
- data/lib/log_bench/app/monitor.rb +59 -0
- data/lib/log_bench/app/renderer/ansi.rb +176 -0
- data/lib/log_bench/app/renderer/details.rb +488 -0
- data/lib/log_bench/app/renderer/header.rb +99 -0
- data/lib/log_bench/app/renderer/main.rb +30 -0
- data/lib/log_bench/app/renderer/request_list.rb +211 -0
- data/lib/log_bench/app/renderer/scrollbar.rb +38 -0
- data/lib/log_bench/app/screen.rb +96 -0
- data/lib/log_bench/app/sort.rb +61 -0
- data/lib/log_bench/app/state.rb +175 -0
- data/lib/log_bench/json_formatter.rb +92 -0
- data/lib/log_bench/log/cache_entry.rb +82 -0
- data/lib/log_bench/log/call_line_entry.rb +60 -0
- data/lib/log_bench/log/collection.rb +98 -0
- data/lib/log_bench/log/entry.rb +108 -0
- data/lib/log_bench/log/file.rb +103 -0
- data/lib/log_bench/log/parser.rb +64 -0
- data/lib/log_bench/log/query_entry.rb +132 -0
- data/lib/log_bench/log/request.rb +90 -0
- data/lib/log_bench/railtie.rb +45 -0
- data/lib/log_bench/version.rb +5 -0
- data/lib/log_bench.rb +26 -0
- data/lib/tasks/log_bench.rake +97 -0
- data/logbench-preview.png +0 -0
- metadata +171 -0
@@ -0,0 +1,245 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module App
|
5
|
+
class InputHandler
|
6
|
+
include Curses
|
7
|
+
|
8
|
+
# Key codes
|
9
|
+
TAB = 9
|
10
|
+
CTRL_F = 6 # Page down
|
11
|
+
CTRL_B = 2 # Page up
|
12
|
+
CTRL_D = 4 # Half page down
|
13
|
+
CTRL_U = 21 # Half page up
|
14
|
+
CTRL_C = 3 # Quit
|
15
|
+
ESC = 27 # Escape
|
16
|
+
|
17
|
+
# UI constants
|
18
|
+
DEFAULT_VISIBLE_HEIGHT = 20
|
19
|
+
|
20
|
+
def initialize(state)
|
21
|
+
self.state = state
|
22
|
+
end
|
23
|
+
|
24
|
+
def handle_input
|
25
|
+
ch = getch
|
26
|
+
return if ch == -1 || ch.nil?
|
27
|
+
|
28
|
+
if filter_mode_active?
|
29
|
+
handle_filter_input(ch)
|
30
|
+
else
|
31
|
+
handle_navigation_input(ch)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
attr_accessor :state
|
38
|
+
|
39
|
+
def filter_mode_active?
|
40
|
+
state.filter_mode || state.detail_filter_mode
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle_filter_input(ch)
|
44
|
+
case ch
|
45
|
+
when 10, 13 # Enter - exit filter mode but keep filter text
|
46
|
+
if state.filter_mode
|
47
|
+
state.filter_mode = false
|
48
|
+
elsif state.detail_filter_mode
|
49
|
+
state.detail_filter_mode = false
|
50
|
+
end
|
51
|
+
when 27 # ESC - exit filter mode and clear filter
|
52
|
+
state.exit_filter_mode
|
53
|
+
when Curses::KEY_UP, "k", "K"
|
54
|
+
state.exit_filter_mode
|
55
|
+
state.navigate_up
|
56
|
+
when Curses::KEY_DOWN, "j", "J"
|
57
|
+
state.exit_filter_mode
|
58
|
+
state.navigate_down
|
59
|
+
when 127, 8 # Backspace
|
60
|
+
state.backspace_filter
|
61
|
+
else
|
62
|
+
add_character_to_filter(ch)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def add_character_to_filter(ch)
|
67
|
+
return unless printable_character?(ch)
|
68
|
+
|
69
|
+
char_to_add = if ch.is_a?(String)
|
70
|
+
ch
|
71
|
+
else
|
72
|
+
ch.chr
|
73
|
+
end
|
74
|
+
|
75
|
+
state.add_to_filter(char_to_add)
|
76
|
+
reset_selection_if_main_filter
|
77
|
+
end
|
78
|
+
|
79
|
+
def printable_character?(ch)
|
80
|
+
if ch.is_a?(String)
|
81
|
+
ch.length == 1 && ch.ord >= 32 && ch.ord <= 126
|
82
|
+
elsif ch.is_a?(Integer)
|
83
|
+
ch.between?(32, 126)
|
84
|
+
else
|
85
|
+
false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def reset_selection_if_main_filter
|
90
|
+
return unless state.filter_mode
|
91
|
+
|
92
|
+
state.selected = 0
|
93
|
+
state.scroll_offset = 0
|
94
|
+
end
|
95
|
+
|
96
|
+
def handle_navigation_input(ch)
|
97
|
+
case ch
|
98
|
+
when Curses::KEY_LEFT, "h", "H"
|
99
|
+
state.switch_to_left_pane
|
100
|
+
when Curses::KEY_RIGHT, "l", "L"
|
101
|
+
state.switch_to_right_pane
|
102
|
+
when TAB
|
103
|
+
toggle_pane_focus
|
104
|
+
when Curses::KEY_UP, "k", "K"
|
105
|
+
handle_up_navigation
|
106
|
+
when Curses::KEY_DOWN, "j", "J"
|
107
|
+
handle_down_navigation
|
108
|
+
when CTRL_F
|
109
|
+
handle_page_down
|
110
|
+
when CTRL_B
|
111
|
+
handle_page_up
|
112
|
+
when CTRL_D
|
113
|
+
handle_half_page_down
|
114
|
+
when CTRL_U
|
115
|
+
handle_half_page_up
|
116
|
+
when "g"
|
117
|
+
handle_go_to_top
|
118
|
+
when "G"
|
119
|
+
handle_go_to_bottom
|
120
|
+
when "a", "A"
|
121
|
+
state.toggle_auto_scroll
|
122
|
+
when "f", "F", "/"
|
123
|
+
state.enter_filter_mode
|
124
|
+
when "c", "C"
|
125
|
+
if state.left_pane_focused?
|
126
|
+
state.clear_filter
|
127
|
+
else
|
128
|
+
state.clear_detail_filter
|
129
|
+
end
|
130
|
+
when "s", "S"
|
131
|
+
state.cycle_sort_mode
|
132
|
+
when "q", "Q", CTRL_C
|
133
|
+
state.stop!
|
134
|
+
when ESC
|
135
|
+
handle_escape
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def toggle_pane_focus
|
140
|
+
if state.left_pane_focused?
|
141
|
+
state.switch_to_right_pane
|
142
|
+
else
|
143
|
+
state.switch_to_left_pane
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def handle_up_navigation
|
148
|
+
state.navigate_up
|
149
|
+
state.adjust_scroll_for_selection(visible_height) if state.left_pane_focused?
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_down_navigation
|
153
|
+
if state.left_pane_focused?
|
154
|
+
max_index = state.filtered_requests.size - 1
|
155
|
+
state.selected = [state.selected + 1, max_index].min
|
156
|
+
state.auto_scroll = false
|
157
|
+
state.adjust_scroll_for_selection(visible_height)
|
158
|
+
else
|
159
|
+
state.navigate_down
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def handle_page_down
|
164
|
+
if state.left_pane_focused?
|
165
|
+
page_size = visible_height
|
166
|
+
max_index = state.filtered_requests.size - 1
|
167
|
+
state.selected = [state.selected + page_size, max_index].min
|
168
|
+
state.auto_scroll = false
|
169
|
+
state.adjust_scroll_for_selection(visible_height)
|
170
|
+
else
|
171
|
+
state.detail_scroll_offset += visible_height
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
def handle_page_up
|
176
|
+
if state.left_pane_focused?
|
177
|
+
page_size = visible_height
|
178
|
+
state.selected = [state.selected - page_size, 0].max
|
179
|
+
state.auto_scroll = false
|
180
|
+
state.adjust_scroll_for_selection(visible_height)
|
181
|
+
else
|
182
|
+
state.detail_scroll_offset = [state.detail_scroll_offset - visible_height, 0].max
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle_half_page_down
|
187
|
+
if state.left_pane_focused?
|
188
|
+
half_page = visible_height / 2
|
189
|
+
max_index = state.filtered_requests.size - 1
|
190
|
+
state.selected = [state.selected + half_page, max_index].min
|
191
|
+
state.auto_scroll = false
|
192
|
+
state.adjust_scroll_for_selection(visible_height)
|
193
|
+
else
|
194
|
+
state.detail_scroll_offset += visible_height / 2
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def handle_half_page_up
|
199
|
+
if state.left_pane_focused?
|
200
|
+
half_page = visible_height / 2
|
201
|
+
state.selected = [state.selected - half_page, 0].max
|
202
|
+
state.auto_scroll = false
|
203
|
+
state.adjust_scroll_for_selection(visible_height)
|
204
|
+
else
|
205
|
+
state.detail_scroll_offset = [state.detail_scroll_offset - visible_height / 2, 0].max
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
def handle_go_to_top
|
210
|
+
if state.left_pane_focused?
|
211
|
+
state.selected = 0
|
212
|
+
state.auto_scroll = false
|
213
|
+
state.adjust_scroll_for_selection(visible_height)
|
214
|
+
else
|
215
|
+
state.detail_scroll_offset = 0
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def handle_go_to_bottom
|
220
|
+
if state.left_pane_focused?
|
221
|
+
max_index = state.filtered_requests.size - 1
|
222
|
+
state.selected = [max_index, 0].max
|
223
|
+
state.auto_scroll = false
|
224
|
+
state.adjust_scroll_for_selection(visible_height)
|
225
|
+
else
|
226
|
+
# Calculate max scroll for detail pane
|
227
|
+
state.detail_scroll_offset = 999 # Will be adjusted by renderer
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def handle_escape
|
232
|
+
if state.filter_mode || state.detail_filter_mode
|
233
|
+
state.exit_filter_mode
|
234
|
+
else
|
235
|
+
state.clear_filter
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def visible_height
|
240
|
+
# Approximate visible height for calculations
|
241
|
+
DEFAULT_VISIBLE_HEIGHT
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "curses"
|
4
|
+
|
5
|
+
module LogBench
|
6
|
+
module App
|
7
|
+
class Main
|
8
|
+
include Curses
|
9
|
+
|
10
|
+
# Default log file paths
|
11
|
+
DEFAULT_LOG_PATHS = %w[log/development.log].freeze
|
12
|
+
|
13
|
+
# Timing
|
14
|
+
MAIN_LOOP_SLEEP_INTERVAL = 0.05
|
15
|
+
|
16
|
+
# Error messages
|
17
|
+
LOG_FILE_NOT_FOUND = "Error: No log file found at %s!"
|
18
|
+
RAILS_PROJECT_HINT = "Please run from a Rails project directory or specify a valid log file"
|
19
|
+
|
20
|
+
def initialize(log_file_path = "log/development.log")
|
21
|
+
self.log_file_path = find_log_file(log_file_path)
|
22
|
+
self.state = State.new
|
23
|
+
validate_log_file!
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
setup_screen
|
28
|
+
setup_components
|
29
|
+
load_initial_data
|
30
|
+
initial_draw
|
31
|
+
start_monitoring
|
32
|
+
main_loop
|
33
|
+
ensure
|
34
|
+
cleanup
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def find_log_file(path)
|
40
|
+
candidates = [path] + DEFAULT_LOG_PATHS
|
41
|
+
candidates.find { |candidate| File.exist?(candidate) } || path
|
42
|
+
end
|
43
|
+
|
44
|
+
def validate_log_file!
|
45
|
+
unless File.exist?(log_file_path)
|
46
|
+
puts LOG_FILE_NOT_FOUND % log_file_path
|
47
|
+
puts RAILS_PROJECT_HINT
|
48
|
+
exit 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def setup_screen
|
53
|
+
self.screen = Screen.new
|
54
|
+
screen.setup
|
55
|
+
end
|
56
|
+
|
57
|
+
def setup_components
|
58
|
+
self.input_handler = InputHandler.new(state)
|
59
|
+
self.renderer = Renderer::Main.new(screen, state)
|
60
|
+
end
|
61
|
+
|
62
|
+
def load_initial_data
|
63
|
+
log_file = Log::File.new(log_file_path)
|
64
|
+
state.requests = log_file.requests
|
65
|
+
end
|
66
|
+
|
67
|
+
def initial_draw
|
68
|
+
renderer.draw
|
69
|
+
end
|
70
|
+
|
71
|
+
def start_monitoring
|
72
|
+
self.monitor = Monitor.new(log_file_path, state)
|
73
|
+
monitor.start
|
74
|
+
end
|
75
|
+
|
76
|
+
def main_loop
|
77
|
+
loop do
|
78
|
+
break unless state.running?
|
79
|
+
|
80
|
+
renderer.draw
|
81
|
+
input_handler.handle_input
|
82
|
+
sleep MAIN_LOOP_SLEEP_INTERVAL
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def cleanup
|
87
|
+
monitor&.stop
|
88
|
+
screen&.cleanup
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
attr_accessor :log_file_path, :state, :screen, :monitor, :input_handler, :renderer
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module App
|
5
|
+
class Monitor
|
6
|
+
def initialize(log_file_path, state)
|
7
|
+
self.log_file_path = log_file_path
|
8
|
+
self.state = state
|
9
|
+
self.running = false
|
10
|
+
end
|
11
|
+
|
12
|
+
def start
|
13
|
+
return if running
|
14
|
+
|
15
|
+
self.running = true
|
16
|
+
self.thread = Thread.new { monitor_loop }
|
17
|
+
end
|
18
|
+
|
19
|
+
def stop
|
20
|
+
self.running = false
|
21
|
+
thread&.kill
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_accessor :log_file_path, :state, :thread, :running
|
27
|
+
|
28
|
+
def monitor_loop
|
29
|
+
log_file = Log::File.new(log_file_path)
|
30
|
+
|
31
|
+
loop do
|
32
|
+
break unless running
|
33
|
+
|
34
|
+
begin
|
35
|
+
log_file.watch do |new_collection|
|
36
|
+
add_new_requests(new_collection.requests)
|
37
|
+
end
|
38
|
+
rescue
|
39
|
+
# Log error but continue monitoring
|
40
|
+
# In a real app, you might want to handle this differently
|
41
|
+
sleep 1
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_new_requests(new_requests)
|
47
|
+
return if new_requests.empty?
|
48
|
+
|
49
|
+
state.requests.concat(new_requests)
|
50
|
+
keep_recent_requests
|
51
|
+
end
|
52
|
+
|
53
|
+
def keep_recent_requests
|
54
|
+
# Keep only the last 1000 requests to prevent memory issues
|
55
|
+
state.requests = state.requests.last(1000) if state.requests.size > 1000
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,176 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module LogBench
|
4
|
+
module App
|
5
|
+
module Renderer
|
6
|
+
class Ansi
|
7
|
+
include Curses
|
8
|
+
|
9
|
+
def initialize(screen)
|
10
|
+
self.screen = screen
|
11
|
+
end
|
12
|
+
|
13
|
+
def has_ansi_codes?(text)
|
14
|
+
text.match?(/\e\[[0-9;]*m/)
|
15
|
+
end
|
16
|
+
|
17
|
+
def parse_and_render(text, win)
|
18
|
+
# Parse ANSI escape codes and render with proper colors
|
19
|
+
parts = text.split(/(\e\[[0-9;]*m)/)
|
20
|
+
current_color = nil
|
21
|
+
|
22
|
+
parts.each do |part|
|
23
|
+
if part =~ /\e\[([0-9;]*)m/
|
24
|
+
# ANSI escape code
|
25
|
+
codes = $1.split(";").map(&:to_i)
|
26
|
+
current_color = ansi_to_curses_color(codes)
|
27
|
+
elsif current_color && !part.empty?
|
28
|
+
# Text content
|
29
|
+
win.attron(current_color) { win.addstr(part) }
|
30
|
+
elsif !part.empty?
|
31
|
+
win.addstr(part)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def wrap_ansi_text(text, max_width)
|
37
|
+
# For ANSI text, we need to preserve color state across chunks
|
38
|
+
clean_text = text.gsub(/\e\[[0-9;]*m/, "")
|
39
|
+
|
40
|
+
if clean_text.length <= max_width
|
41
|
+
[text]
|
42
|
+
else
|
43
|
+
chunks = []
|
44
|
+
remaining = text
|
45
|
+
active_colors = []
|
46
|
+
|
47
|
+
# Extract initial color state
|
48
|
+
text.scan(/\e\[[0-9;]*m/) do |ansi_code|
|
49
|
+
if /\e\[0m/.match?(ansi_code)
|
50
|
+
active_colors.clear
|
51
|
+
else
|
52
|
+
active_colors << ansi_code
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
while remaining.length > 0
|
57
|
+
clean_remaining = remaining.gsub(/\e\[[0-9;]*m/, "")
|
58
|
+
|
59
|
+
if clean_remaining.length <= max_width
|
60
|
+
# Last chunk
|
61
|
+
chunks << if active_colors.any? && !remaining.start_with?(*active_colors)
|
62
|
+
active_colors.join("") + remaining
|
63
|
+
else
|
64
|
+
remaining
|
65
|
+
end
|
66
|
+
break
|
67
|
+
else
|
68
|
+
# Find break point and preserve color state
|
69
|
+
break_point = max_width
|
70
|
+
original_pos = 0
|
71
|
+
clean_pos = 0
|
72
|
+
chunk_colors = active_colors.dup
|
73
|
+
|
74
|
+
remaining.each_char.with_index do |char, idx|
|
75
|
+
if /^\e\[[0-9;]*m/.match?(remaining[idx..])
|
76
|
+
# Found ANSI sequence
|
77
|
+
ansi_match = remaining[idx..].match(/^(\e\[[0-9;]*m)/)
|
78
|
+
ansi_code = ansi_match[1]
|
79
|
+
|
80
|
+
if /\e\[0m/.match?(ansi_code)
|
81
|
+
chunk_colors.clear
|
82
|
+
active_colors.clear
|
83
|
+
else
|
84
|
+
chunk_colors << ansi_code unless chunk_colors.include?(ansi_code)
|
85
|
+
active_colors << ansi_code unless active_colors.include?(ansi_code)
|
86
|
+
end
|
87
|
+
|
88
|
+
original_pos += ansi_code.length
|
89
|
+
idx + ansi_code.length - 1
|
90
|
+
else
|
91
|
+
clean_pos += 1
|
92
|
+
original_pos += 1
|
93
|
+
|
94
|
+
if clean_pos >= break_point
|
95
|
+
break
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
chunk_text = remaining[0...original_pos]
|
101
|
+
chunks << if active_colors.any? && !chunk_text.start_with?(*active_colors)
|
102
|
+
active_colors.join("") + chunk_text
|
103
|
+
else
|
104
|
+
chunk_text
|
105
|
+
end
|
106
|
+
|
107
|
+
remaining = remaining[original_pos..]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
chunks
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def wrap_plain_text(text, max_width)
|
116
|
+
# Simple text wrapping for plain text
|
117
|
+
if text.length <= max_width
|
118
|
+
[text]
|
119
|
+
else
|
120
|
+
chunks = []
|
121
|
+
remaining = text
|
122
|
+
|
123
|
+
while remaining.length > 0
|
124
|
+
if remaining.length <= max_width
|
125
|
+
chunks << remaining
|
126
|
+
break
|
127
|
+
else
|
128
|
+
# Find a good break point (try to break on spaces)
|
129
|
+
break_point = max_width
|
130
|
+
if remaining[0...max_width].include?(" ")
|
131
|
+
# Find the last space within the limit
|
132
|
+
break_point = remaining[0...max_width].rindex(" ") || max_width
|
133
|
+
end
|
134
|
+
|
135
|
+
chunks << remaining[0...break_point]
|
136
|
+
remaining = remaining[break_point..].lstrip
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
chunks
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
attr_accessor :screen
|
147
|
+
|
148
|
+
def ansi_to_curses_color(codes)
|
149
|
+
# Convert ANSI color codes to curses color pairs
|
150
|
+
return nil if codes.empty? || codes == [0]
|
151
|
+
|
152
|
+
# Handle common ANSI codes
|
153
|
+
codes.each do |code|
|
154
|
+
case code
|
155
|
+
when 1 then return color_pair(7) | A_BOLD # Bold/bright
|
156
|
+
when 30 then return color_pair(8) # Black
|
157
|
+
when 31 then return color_pair(6) # Red
|
158
|
+
when 32 then return color_pair(3) # Green
|
159
|
+
when 33 then return color_pair(4) # Yellow
|
160
|
+
when 34 then return color_pair(5) # Blue
|
161
|
+
when 35 then return color_pair(9) # Magenta
|
162
|
+
when 36 then return color_pair(1) # Cyan
|
163
|
+
when 37 then return nil # White (default)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
nil
|
168
|
+
end
|
169
|
+
|
170
|
+
def color_pair(n)
|
171
|
+
screen.color_pair(n)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|