tui-td 0.2.10 → 0.2.11

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.
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
- command = nil
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 globals[:format] == :json || globals[:format] == :pretty_json
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 + "\n")
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" # clear screen, home cursor
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] ? 'PASSED' : 'FAILED'}"
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 OptionParser.new { |o| o.banner = "Usage: tui-td <command> [options]" }
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,5 +1,8 @@
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"
@@ -38,12 +41,13 @@ module TUITD
38
41
 
39
42
  # Start the TUI application in a PTY
40
43
  def start
41
- env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s }.merge(@env.transform_keys(&:to_s))
44
+ env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s,
45
+ "LINES" => @rows.to_s, }.merge(@env.transform_keys(&:to_s))
42
46
  spawn_opts = {}
43
47
  spawn_opts[:chdir] = @chdir if @chdir
44
48
 
45
49
  @stdout, @stdin, @pid = PTY.spawn(env, @command, spawn_opts)
46
- @stdout.winsize = [@rows, @cols] # Set PTY window size for TUIs that check winsize
50
+ @stdout.winsize = [@rows, @cols] # Set PTY window size for TUIs that check winsize
47
51
  @wait_thr = Process.detach(@pid)
48
52
 
49
53
  # Read until initial output stabilizes
@@ -88,9 +92,11 @@ module TUITD
88
92
  deadline = monotonic + @timeout
89
93
  loop do
90
94
  raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline
95
+
91
96
  read_available!
92
97
  found = @output_mutex.synchronize { @output_buffer.include?(text) }
93
98
  break if found
99
+
94
100
  sleep 0.05
95
101
  end
96
102
  refresh_state!
@@ -117,7 +123,7 @@ module TUITD
117
123
  elsif !process_alive
118
124
  # Process exited and no more data — final state reached
119
125
  break
120
- elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms
126
+ elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms # rubocop:disable Lint/DuplicateBranch
121
127
  break
122
128
  end
123
129
 
@@ -134,6 +140,7 @@ module TUITD
134
140
  # Get the process exit status (nil if still running)
135
141
  def exitstatus
136
142
  return nil unless @wait_thr
143
+
137
144
  status = @wait_thr.value
138
145
  status&.exitstatus
139
146
  rescue NoMethodError
@@ -179,16 +186,32 @@ module TUITD
179
186
  if @pid
180
187
  begin
181
188
  if Process.waitpid(@pid, Process::WNOHANG).nil?
182
- Process.kill("TERM", @pid) rescue nil
189
+ begin
190
+ Process.kill("TERM", @pid)
191
+ rescue StandardError
192
+ nil
193
+ end
183
194
  sleep 0.05
184
- Process.kill("KILL", @pid) rescue nil
195
+ begin
196
+ Process.kill("KILL", @pid)
197
+ rescue StandardError
198
+ nil
199
+ end
185
200
  end
186
201
  rescue Errno::ECHILD
187
202
  # Already reaped by Process.detach
188
203
  end
189
204
  end
190
- @stdin&.close rescue nil
191
- @stdout&.close rescue nil
205
+ begin
206
+ @stdin&.close
207
+ rescue StandardError
208
+ nil
209
+ end
210
+ begin
211
+ @stdout&.close
212
+ rescue StandardError
213
+ nil
214
+ end
192
215
  @stdin = @stdout = @pid = nil
193
216
  end
194
217
 
@@ -199,6 +222,7 @@ module TUITD
199
222
  @reader_thread = Thread.new do
200
223
  loop do
201
224
  break unless @reader_running
225
+
202
226
  begin
203
227
  read_available!
204
228
  rescue IOError, Errno::EIO
@@ -211,11 +235,15 @@ module TUITD
211
235
 
212
236
  def _stop_reader_thread
213
237
  @reader_running = false
214
- if @reader_thread
215
- @reader_thread.join(1)
216
- @reader_thread.kill rescue nil
217
- @reader_thread = nil
238
+ return unless @reader_thread
239
+
240
+ @reader_thread.join(1)
241
+ begin
242
+ @reader_thread.kill
243
+ rescue StandardError
244
+ nil
218
245
  end
246
+ @reader_thread = nil
219
247
  end
220
248
 
221
249
  def ensure_running!
@@ -265,6 +293,7 @@ module TUITD
265
293
 
266
294
  def process_alive?
267
295
  return false unless @pid
296
+
268
297
  Process.waitpid(@pid, Process::WNOHANG).nil?
269
298
  rescue Errno::ECHILD
270
299
  false
@@ -273,7 +302,9 @@ module TUITD
273
302
  def monotonic
274
303
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
275
304
  end
276
- end
305
+ end
277
306
 
278
307
  class TimeoutError < Error; end
279
308
  end
309
+ # rubocop:enable Metrics/ClassLength, Metrics/MethodLength, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/ParameterLists
310
+ # rubocop:enable Naming/PredicateMethod
@@ -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
- '<span class="line"></span>'
125
- else
126
- runs = build_runs(row, ri)
127
- spans = runs.map do |run|
128
- render_run(run)
129
- end
130
- %(<span class="line">#{spans.join}</span>)
131
- end
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 = (cell[:char] || cell["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 = is_cursor?(ri, ci)
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 !cursor_vis
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 is_cursor?(ri, ci)
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("#%02x%02x%02x", *rgb)
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
@@ -19,8 +19,8 @@ module TUITD
19
19
  end
20
20
 
21
21
  description { "have text #{expected.inspect}" }
22
- failure_message { |state| "expected terminal to contain #{expected.inspect}" }
23
- failure_message_when_negated { |state| "expected terminal NOT to contain #{expected.inspect}" }
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 { |state| "expected terminal to match #{pattern.inspect}" }
34
- failure_message_when_negated { |state| "expected terminal NOT to match #{pattern.inspect}" }
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) { |row, col| @row, @col = row, col }
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 |state|
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) { |row, col| @row, @col = row, col }
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 |state|
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) { |row, col| @row, @col = row, col }
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 |state|
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 |driver|
100
+ failure_message do |_driver|
92
101
  "expected exit status #{expected}, but was #{@actual}"
93
102
  end
94
- failure_message_when_negated do |driver|
103
+ failure_message_when_negated do |_driver|
95
104
  "expected exit status not to be #{expected}"
96
105
  end
97
106
  end