tui-td 0.2.10 → 0.2.12
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/CHANGELOG.md +29 -0
- data/lib/tui_td/ansi_parser.rb +3 -719
- data/lib/tui_td/ansi_utils.rb +3 -71
- data/lib/tui_td/cairo_renderer.rb +5 -2
- data/lib/tui_td/cli.rb +12 -10
- data/lib/tui_td/driver.rb +54 -14
- data/lib/tui_td/html_renderer.rb +19 -17
- data/lib/tui_td/matchers.rb +21 -12
- data/lib/tui_td/mcp/server.rb +104 -87
- data/lib/tui_td/screenshot.rb +70 -52
- data/lib/tui_td/state.rb +3 -117
- data/lib/tui_td/test_runner.rb +41 -27
- data/lib/tui_td/unifont_glyphs.rb +2142 -2141
- data/lib/tui_td/version.rb +1 -1
- data/lib/tui_td.rb +7 -3
- metadata +50 -7
data/lib/tui_td/ansi_utils.rb
CHANGED
|
@@ -1,75 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
# Shared ANSI color constants and helpers.
|
|
5
|
-
# Used by Screenshot, HtmlRenderer, and other color-aware renderers.
|
|
6
|
-
module ANSIUtils
|
|
7
|
-
ANSI_RGB = {
|
|
8
|
-
"black" => [0x00, 0x00, 0x00],
|
|
9
|
-
"red" => [0xAA, 0x00, 0x00],
|
|
10
|
-
"green" => [0x00, 0xAA, 0x00],
|
|
11
|
-
"yellow" => [0xAA, 0x55, 0x00],
|
|
12
|
-
"blue" => [0x00, 0x00, 0xAA],
|
|
13
|
-
"magenta" => [0xAA, 0x00, 0xAA],
|
|
14
|
-
"cyan" => [0x00, 0xAA, 0xAA],
|
|
15
|
-
"white" => [0xAA, 0xAA, 0xAA],
|
|
16
|
-
"bright_black" => [0x55, 0x55, 0x55],
|
|
17
|
-
"bright_red" => [0xFF, 0x55, 0x55],
|
|
18
|
-
"bright_green" => [0x55, 0xFF, 0x55],
|
|
19
|
-
"bright_yellow" => [0xFF, 0xFF, 0x55],
|
|
20
|
-
"bright_blue" => [0x55, 0x55, 0xFF],
|
|
21
|
-
"bright_magenta"=> [0xFF, 0x55, 0xFF],
|
|
22
|
-
"bright_cyan" => [0x55, 0xFF, 0xFF],
|
|
23
|
-
"bright_white" => [0xFF, 0xFF, 0xFF],
|
|
24
|
-
}.freeze
|
|
25
|
-
|
|
26
|
-
CUBE = [0x00, 0x5F, 0x87, 0xAF, 0xD7, 0xFF].freeze
|
|
27
|
-
|
|
28
|
-
ANSI_INDEX = %w[
|
|
29
|
-
black red green yellow blue magenta cyan white
|
|
30
|
-
bright_black bright_red bright_green bright_yellow
|
|
31
|
-
bright_blue bright_magenta bright_cyan bright_white
|
|
32
|
-
].freeze
|
|
33
|
-
|
|
34
|
-
DEFAULT_FG = [0xC0, 0xC0, 0xC0].freeze
|
|
35
|
-
DEFAULT_BG = [0x00, 0x00, 0x00].freeze
|
|
3
|
+
require "tans-parser"
|
|
36
4
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
when "default"
|
|
40
|
-
fallback
|
|
41
|
-
when /^#([0-9a-fA-F]{6})$/
|
|
42
|
-
[$1[0..1].to_i(16), $1[2..3].to_i(16), $1[4..5].to_i(16)]
|
|
43
|
-
when /\Acolor(\d+)\z/
|
|
44
|
-
xterm_256($1.to_i)
|
|
45
|
-
when /\Abright_(.+)\z/
|
|
46
|
-
ANSI_RGB[name] || fallback
|
|
47
|
-
else
|
|
48
|
-
ANSI_RGB[name] || fallback
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def xterm_256(index)
|
|
53
|
-
if index < 16
|
|
54
|
-
name = ANSI_INDEX[index]
|
|
55
|
-
ANSI_RGB[name] || DEFAULT_FG
|
|
56
|
-
elsif index < 232
|
|
57
|
-
r = CUBE[((index - 16) / 36) % 6]
|
|
58
|
-
g = CUBE[((index - 16) / 6) % 6]
|
|
59
|
-
b = CUBE[(index - 16) % 6]
|
|
60
|
-
[r, g, b]
|
|
61
|
-
else
|
|
62
|
-
v = 8 + (index - 232) * 10
|
|
63
|
-
[v, v, v]
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def _dig(hash, *keys)
|
|
68
|
-
keys.each do |k|
|
|
69
|
-
return nil unless hash
|
|
70
|
-
hash = hash[k] || hash[k.to_s]
|
|
71
|
-
end
|
|
72
|
-
hash
|
|
73
|
-
end
|
|
74
|
-
end
|
|
5
|
+
module TUITD
|
|
6
|
+
ANSIUtils = TansParser::ANSIUtils
|
|
75
7
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
|
|
4
|
+
|
|
3
5
|
module TUITD
|
|
4
6
|
module CairoRenderer
|
|
5
7
|
CELL_W = 8
|
|
@@ -76,9 +78,9 @@ module TUITD
|
|
|
76
78
|
CELL_W.times do |dx|
|
|
77
79
|
sum = 0
|
|
78
80
|
scale.times do |sy|
|
|
79
|
-
row_off = (dy * scale + sy) * stride
|
|
81
|
+
row_off = ((dy * scale) + sy) * stride
|
|
80
82
|
scale.times do |sx|
|
|
81
|
-
sum += data.getbyte(row_off + (dx * scale + sx) * 4 + 3)
|
|
83
|
+
sum += data.getbyte(row_off + (((dx * scale) + sx) * 4) + 3)
|
|
82
84
|
end
|
|
83
85
|
end
|
|
84
86
|
alpha_grid[dy][dx] = sum / scale_sq
|
|
@@ -107,3 +109,4 @@ module TUITD
|
|
|
107
109
|
end
|
|
108
110
|
end
|
|
109
111
|
end
|
|
112
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/ParameterLists
|
data/lib/tui_td/cli.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
|
4
|
+
|
|
3
5
|
require "optparse"
|
|
4
6
|
|
|
5
7
|
module TUITD
|
|
@@ -11,7 +13,7 @@ module TUITD
|
|
|
11
13
|
|
|
12
14
|
def run(argv)
|
|
13
15
|
global_opts = {}
|
|
14
|
-
|
|
16
|
+
nil
|
|
15
17
|
command_opts = {}
|
|
16
18
|
|
|
17
19
|
OptionParser.new do |opts|
|
|
@@ -131,7 +133,7 @@ module TUITD
|
|
|
131
133
|
server = MCP::Server.new(
|
|
132
134
|
rows: globals[:rows] || 40,
|
|
133
135
|
cols: globals[:cols] || 120,
|
|
134
|
-
timeout: globals[:timeout] || 30
|
|
136
|
+
timeout: globals[:timeout] || 30,
|
|
135
137
|
)
|
|
136
138
|
server.start
|
|
137
139
|
end
|
|
@@ -148,7 +150,7 @@ module TUITD
|
|
|
148
150
|
|
|
149
151
|
driver.wait_for_stable
|
|
150
152
|
|
|
151
|
-
if
|
|
153
|
+
if %i[json pretty_json].include?(globals[:format])
|
|
152
154
|
puts driver.state_json(pretty: globals[:format] == :pretty_json)
|
|
153
155
|
else
|
|
154
156
|
_render_text(driver.state_data)
|
|
@@ -182,6 +184,7 @@ module TUITD
|
|
|
182
184
|
print "> "
|
|
183
185
|
input = $stdin.gets
|
|
184
186
|
break unless input
|
|
187
|
+
|
|
185
188
|
input = input.chomp
|
|
186
189
|
break if input == "exit"
|
|
187
190
|
|
|
@@ -195,7 +198,7 @@ module TUITD
|
|
|
195
198
|
elsif input.start_with?("key ")
|
|
196
199
|
driver.send_keys(input.split(" ", 2).last.to_sym)
|
|
197
200
|
else
|
|
198
|
-
driver.send(input
|
|
201
|
+
driver.send("#{input}\n")
|
|
199
202
|
end
|
|
200
203
|
end
|
|
201
204
|
rescue Interrupt
|
|
@@ -258,16 +261,14 @@ module TUITD
|
|
|
258
261
|
|
|
259
262
|
on_step = if verbose || live || step_mode
|
|
260
263
|
lambda do |info|
|
|
261
|
-
if live && info[:driver]
|
|
262
|
-
info[:driver].wait_for_stable(stable_ms: 200)
|
|
263
|
-
end
|
|
264
|
+
info[:driver].wait_for_stable(stable_ms: 200) if live && info[:driver]
|
|
264
265
|
if verbose
|
|
265
266
|
status = info[:result].passed ? "PASS" : "FAIL"
|
|
266
267
|
puts "[#{info[:index] + 1}/#{info[:total]}] #{info[:action]}: #{info[:result].message}"
|
|
267
268
|
puts " → #{status}"
|
|
268
269
|
end
|
|
269
270
|
if live && info[:driver]
|
|
270
|
-
print "\e[2J\e[H"
|
|
271
|
+
print "\e[2J\e[H" # clear screen, home cursor
|
|
271
272
|
_render_text(info[:driver].state_data)
|
|
272
273
|
end
|
|
273
274
|
if step_mode
|
|
@@ -285,7 +286,7 @@ module TUITD
|
|
|
285
286
|
|
|
286
287
|
puts
|
|
287
288
|
puts "Test: #{result[:name]}"
|
|
288
|
-
puts "Status: #{result[:passed] ?
|
|
289
|
+
puts "Status: #{result[:passed] ? "PASSED" : "FAILED"}"
|
|
289
290
|
puts "-" * 40
|
|
290
291
|
|
|
291
292
|
result[:results].each do |r|
|
|
@@ -312,7 +313,7 @@ module TUITD
|
|
|
312
313
|
end
|
|
313
314
|
|
|
314
315
|
def _help_main
|
|
315
|
-
puts
|
|
316
|
+
puts(OptionParser.new { |o| o.banner = "Usage: tui-td <command> [options]" })
|
|
316
317
|
puts
|
|
317
318
|
puts "For more: tui-td help test (JSON test step types)"
|
|
318
319
|
puts " tui-td help rspec (RSpec matchers)"
|
|
@@ -480,3 +481,4 @@ module TUITD
|
|
|
480
481
|
end
|
|
481
482
|
end
|
|
482
483
|
end
|
|
484
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/BlockLength
|
data/lib/tui_td/driver.rb
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
|
4
|
+
# rubocop:disable Naming/PredicateMethod
|
|
5
|
+
|
|
3
6
|
require "pty"
|
|
4
7
|
require "io/console"
|
|
5
8
|
require "json"
|
|
9
|
+
require "shellwords"
|
|
6
10
|
|
|
7
11
|
module TUITD
|
|
8
12
|
# Drives a TUI application in a pseudo-terminal (PTY).
|
|
@@ -17,6 +21,9 @@ module TUITD
|
|
|
17
21
|
# driver.close
|
|
18
22
|
#
|
|
19
23
|
class Driver
|
|
24
|
+
FORBIDDEN_ENV = %w[PATH LD_PRELOAD LD_LIBRARY_PATH DYLD_INSERT_LIBRARIES
|
|
25
|
+
DYLD_FRAMEWORK_PATH RUBYOPT HOME RUBYLIB GEM_HOME GEM_PATH].freeze
|
|
26
|
+
|
|
20
27
|
attr_reader :command, :state
|
|
21
28
|
|
|
22
29
|
def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {})
|
|
@@ -25,7 +32,7 @@ module TUITD
|
|
|
25
32
|
@cols = cols
|
|
26
33
|
@timeout = timeout
|
|
27
34
|
@chdir = chdir
|
|
28
|
-
@env = env
|
|
35
|
+
@env = sanitize_env(env)
|
|
29
36
|
@state = nil
|
|
30
37
|
@stdin = nil
|
|
31
38
|
@stdout = nil
|
|
@@ -38,12 +45,14 @@ module TUITD
|
|
|
38
45
|
|
|
39
46
|
# Start the TUI application in a PTY
|
|
40
47
|
def start
|
|
41
|
-
env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s,
|
|
48
|
+
env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s,
|
|
49
|
+
"LINES" => @rows.to_s, }.merge(@env.transform_keys(&:to_s))
|
|
42
50
|
spawn_opts = {}
|
|
43
51
|
spawn_opts[:chdir] = @chdir if @chdir
|
|
44
52
|
|
|
45
|
-
|
|
46
|
-
@stdout
|
|
53
|
+
cmd_args = Shellwords.shellsplit(@command)
|
|
54
|
+
@stdout, @stdin, @pid = PTY.spawn(env, *cmd_args, spawn_opts)
|
|
55
|
+
@stdout.winsize = [@rows, @cols] # Set PTY window size for TUIs that check winsize
|
|
47
56
|
@wait_thr = Process.detach(@pid)
|
|
48
57
|
|
|
49
58
|
# Read until initial output stabilizes
|
|
@@ -88,9 +97,11 @@ module TUITD
|
|
|
88
97
|
deadline = monotonic + @timeout
|
|
89
98
|
loop do
|
|
90
99
|
raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline
|
|
100
|
+
|
|
91
101
|
read_available!
|
|
92
102
|
found = @output_mutex.synchronize { @output_buffer.include?(text) }
|
|
93
103
|
break if found
|
|
104
|
+
|
|
94
105
|
sleep 0.05
|
|
95
106
|
end
|
|
96
107
|
refresh_state!
|
|
@@ -117,7 +128,7 @@ module TUITD
|
|
|
117
128
|
elsif !process_alive
|
|
118
129
|
# Process exited and no more data — final state reached
|
|
119
130
|
break
|
|
120
|
-
elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms
|
|
131
|
+
elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms # rubocop:disable Lint/DuplicateBranch
|
|
121
132
|
break
|
|
122
133
|
end
|
|
123
134
|
|
|
@@ -134,6 +145,7 @@ module TUITD
|
|
|
134
145
|
# Get the process exit status (nil if still running)
|
|
135
146
|
def exitstatus
|
|
136
147
|
return nil unless @wait_thr
|
|
148
|
+
|
|
137
149
|
status = @wait_thr.value
|
|
138
150
|
status&.exitstatus
|
|
139
151
|
rescue NoMethodError
|
|
@@ -179,16 +191,32 @@ module TUITD
|
|
|
179
191
|
if @pid
|
|
180
192
|
begin
|
|
181
193
|
if Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
182
|
-
|
|
194
|
+
begin
|
|
195
|
+
Process.kill("TERM", @pid)
|
|
196
|
+
rescue StandardError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
183
199
|
sleep 0.05
|
|
184
|
-
|
|
200
|
+
begin
|
|
201
|
+
Process.kill("KILL", @pid)
|
|
202
|
+
rescue StandardError
|
|
203
|
+
nil
|
|
204
|
+
end
|
|
185
205
|
end
|
|
186
206
|
rescue Errno::ECHILD
|
|
187
207
|
# Already reaped by Process.detach
|
|
188
208
|
end
|
|
189
209
|
end
|
|
190
|
-
|
|
191
|
-
|
|
210
|
+
begin
|
|
211
|
+
@stdin&.close
|
|
212
|
+
rescue StandardError
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
begin
|
|
216
|
+
@stdout&.close
|
|
217
|
+
rescue StandardError
|
|
218
|
+
nil
|
|
219
|
+
end
|
|
192
220
|
@stdin = @stdout = @pid = nil
|
|
193
221
|
end
|
|
194
222
|
|
|
@@ -199,6 +227,7 @@ module TUITD
|
|
|
199
227
|
@reader_thread = Thread.new do
|
|
200
228
|
loop do
|
|
201
229
|
break unless @reader_running
|
|
230
|
+
|
|
202
231
|
begin
|
|
203
232
|
read_available!
|
|
204
233
|
rescue IOError, Errno::EIO
|
|
@@ -211,11 +240,19 @@ module TUITD
|
|
|
211
240
|
|
|
212
241
|
def _stop_reader_thread
|
|
213
242
|
@reader_running = false
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
243
|
+
return unless @reader_thread
|
|
244
|
+
|
|
245
|
+
@reader_thread.join(1)
|
|
246
|
+
begin
|
|
247
|
+
@reader_thread.kill
|
|
248
|
+
rescue StandardError
|
|
249
|
+
nil
|
|
218
250
|
end
|
|
251
|
+
@reader_thread = nil
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def sanitize_env(env)
|
|
255
|
+
env.reject { |k, _| FORBIDDEN_ENV.include?(k.to_s.upcase) }
|
|
219
256
|
end
|
|
220
257
|
|
|
221
258
|
def ensure_running!
|
|
@@ -265,6 +302,7 @@ module TUITD
|
|
|
265
302
|
|
|
266
303
|
def process_alive?
|
|
267
304
|
return false unless @pid
|
|
305
|
+
|
|
268
306
|
Process.waitpid(@pid, Process::WNOHANG).nil?
|
|
269
307
|
rescue Errno::ECHILD
|
|
270
308
|
false
|
|
@@ -273,7 +311,9 @@ module TUITD
|
|
|
273
311
|
def monotonic
|
|
274
312
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
275
313
|
end
|
|
276
|
-
|
|
314
|
+
end
|
|
277
315
|
|
|
278
316
|
class TimeoutError < Error; end
|
|
279
317
|
end
|
|
318
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
|
|
319
|
+
# rubocop:enable Naming/PredicateMethod
|
data/lib/tui_td/html_renderer.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# rubocop:disable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
4
|
+
|
|
3
5
|
require_relative "ansi_utils"
|
|
4
6
|
|
|
5
7
|
module TUITD
|
|
@@ -121,14 +123,14 @@ module TUITD
|
|
|
121
123
|
def render_body
|
|
122
124
|
lines = @grid.map.with_index do |row, ri|
|
|
123
125
|
line_html = if row.nil? || row.empty?
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
'<span class="line"></span>'
|
|
127
|
+
else
|
|
128
|
+
runs = build_runs(row, ri)
|
|
129
|
+
spans = runs.map do |run|
|
|
130
|
+
render_run(run)
|
|
131
|
+
end
|
|
132
|
+
%(<span class="line">#{spans.join}</span>)
|
|
133
|
+
end
|
|
132
134
|
line_html
|
|
133
135
|
end
|
|
134
136
|
|
|
@@ -140,7 +142,7 @@ module TUITD
|
|
|
140
142
|
current_run = nil
|
|
141
143
|
|
|
142
144
|
row.each_with_index do |cell, ci|
|
|
143
|
-
char =
|
|
145
|
+
char = cell[:char] || cell["char"] || " "
|
|
144
146
|
fg = cell[:fg] || cell["fg"] || "default"
|
|
145
147
|
bg = cell[:bg] || cell["bg"] || "default"
|
|
146
148
|
bold = cell[:bold] || cell["bold"] || false
|
|
@@ -149,7 +151,7 @@ module TUITD
|
|
|
149
151
|
blink = cell[:blink] || cell["blink"] || false
|
|
150
152
|
|
|
151
153
|
style_key = [fg, bg, bold, italic, underline, blink]
|
|
152
|
-
is_cur =
|
|
154
|
+
is_cur = cursor_at?(ri, ci)
|
|
153
155
|
|
|
154
156
|
if current_run && current_run[:key] == style_key && !current_run[:has_cursor] && !is_cur
|
|
155
157
|
current_run[:chars] << char
|
|
@@ -159,7 +161,7 @@ module TUITD
|
|
|
159
161
|
chars: [char],
|
|
160
162
|
style: cell_style(fg, bg, bold, italic, underline),
|
|
161
163
|
has_cursor: is_cur,
|
|
162
|
-
blink: blink
|
|
164
|
+
blink: blink,
|
|
163
165
|
}
|
|
164
166
|
runs << current_run
|
|
165
167
|
end
|
|
@@ -186,9 +188,7 @@ module TUITD
|
|
|
186
188
|
if run[:has_cursor]
|
|
187
189
|
classes << "cursor-cell"
|
|
188
190
|
cursor_vis = @cursor[:visible] != false && @cursor["visible"] != false
|
|
189
|
-
if
|
|
190
|
-
classes << "cursor-hidden"
|
|
191
|
-
else
|
|
191
|
+
if cursor_vis
|
|
192
192
|
style_val = @cursor[:style] || @cursor["style"]
|
|
193
193
|
case style_val
|
|
194
194
|
when 0, 1
|
|
@@ -204,6 +204,8 @@ module TUITD
|
|
|
204
204
|
when 6
|
|
205
205
|
classes << "cursor-bar"
|
|
206
206
|
end
|
|
207
|
+
else
|
|
208
|
+
classes << "cursor-hidden"
|
|
207
209
|
end
|
|
208
210
|
end
|
|
209
211
|
classes << "term-blink" if run[:blink]
|
|
@@ -213,12 +215,12 @@ module TUITD
|
|
|
213
215
|
%(<span#{cls}#{style}>#{chars}</span>)
|
|
214
216
|
end
|
|
215
217
|
|
|
216
|
-
def
|
|
218
|
+
def cursor_at?(ri, ci)
|
|
217
219
|
(@cursor[:row] || @cursor["row"]) == ri && (@cursor[:col] || @cursor["col"]) == ci
|
|
218
220
|
end
|
|
219
221
|
|
|
220
222
|
def css_color(rgb)
|
|
221
|
-
format("
|
|
223
|
+
format("#%<r>02x%<g>02x%<b>02x", r: rgb[0], g: rgb[1], b: rgb[2])
|
|
222
224
|
end
|
|
223
225
|
|
|
224
226
|
def escape_html(char)
|
|
@@ -230,6 +232,6 @@ module TUITD
|
|
|
230
232
|
else char
|
|
231
233
|
end
|
|
232
234
|
end
|
|
233
|
-
|
|
234
235
|
end
|
|
235
236
|
end
|
|
237
|
+
# rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
data/lib/tui_td/matchers.rb
CHANGED
|
@@ -19,8 +19,8 @@ module TUITD
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
description { "have text #{expected.inspect}" }
|
|
22
|
-
failure_message { |
|
|
23
|
-
failure_message_when_negated { |
|
|
22
|
+
failure_message { |_state| "expected terminal to contain #{expected.inspect}" }
|
|
23
|
+
failure_message_when_negated { |_state| "expected terminal NOT to contain #{expected.inspect}" }
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
RSpec::Matchers.define :have_regex do |pattern|
|
|
@@ -30,12 +30,15 @@ module TUITD
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
description { "match regex #{pattern.inspect}" }
|
|
33
|
-
failure_message { |
|
|
34
|
-
failure_message_when_negated { |
|
|
33
|
+
failure_message { |_state| "expected terminal to match #{pattern.inspect}" }
|
|
34
|
+
failure_message_when_negated { |_state| "expected terminal NOT to match #{pattern.inspect}" }
|
|
35
35
|
end
|
|
36
36
|
|
|
37
37
|
RSpec::Matchers.define :have_fg do |expected|
|
|
38
|
-
chain(:at)
|
|
38
|
+
chain(:at) do |row, col|
|
|
39
|
+
@row = row
|
|
40
|
+
@col = col
|
|
41
|
+
end
|
|
39
42
|
|
|
40
43
|
match do |state|
|
|
41
44
|
@actual = state.foreground_at(@row, @col)
|
|
@@ -43,13 +46,16 @@ module TUITD
|
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
description { "have foreground #{expected.inspect} at [#{@row},#{@col}]" }
|
|
46
|
-
failure_message do |
|
|
49
|
+
failure_message do |_state|
|
|
47
50
|
"expected FG at [#{@row},#{@col}] to be #{expected.inspect}, but was #{@actual.inspect}"
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
RSpec::Matchers.define :have_bg do |expected|
|
|
52
|
-
chain(:at)
|
|
55
|
+
chain(:at) do |row, col|
|
|
56
|
+
@row = row
|
|
57
|
+
@col = col
|
|
58
|
+
end
|
|
53
59
|
|
|
54
60
|
match do |state|
|
|
55
61
|
@actual = state.background_at(@row, @col)
|
|
@@ -57,13 +63,16 @@ module TUITD
|
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
description { "have background #{expected.inspect} at [#{@row},#{@col}]" }
|
|
60
|
-
failure_message do |
|
|
66
|
+
failure_message do |_state|
|
|
61
67
|
"expected BG at [#{@row},#{@col}] to be #{expected.inspect}, but was #{@actual.inspect}"
|
|
62
68
|
end
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
RSpec::Matchers.define :have_style do
|
|
66
|
-
chain(:at)
|
|
72
|
+
chain(:at) do |row, col|
|
|
73
|
+
@row = row
|
|
74
|
+
@col = col
|
|
75
|
+
end
|
|
67
76
|
chain(:with) { |expected| @expected = expected }
|
|
68
77
|
|
|
69
78
|
match do |state|
|
|
@@ -75,7 +84,7 @@ module TUITD
|
|
|
75
84
|
description do
|
|
76
85
|
"have style #{@expected.inspect} at [#{@row},#{@col}]"
|
|
77
86
|
end
|
|
78
|
-
failure_message do |
|
|
87
|
+
failure_message do |_state|
|
|
79
88
|
"expected style at [#{@row},#{@col}] to be #{@expected.inspect}, but was #{@actual.inspect}"
|
|
80
89
|
end
|
|
81
90
|
end
|
|
@@ -88,10 +97,10 @@ module TUITD
|
|
|
88
97
|
end
|
|
89
98
|
|
|
90
99
|
description { "have exit status #{expected}" }
|
|
91
|
-
failure_message do |
|
|
100
|
+
failure_message do |_driver|
|
|
92
101
|
"expected exit status #{expected}, but was #{@actual}"
|
|
93
102
|
end
|
|
94
|
-
failure_message_when_negated do |
|
|
103
|
+
failure_message_when_negated do |_driver|
|
|
95
104
|
"expected exit status not to be #{expected}"
|
|
96
105
|
end
|
|
97
106
|
end
|