tui-td 0.1.0 → 0.1.2

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: 45c5b163ff48383823bccd9c0b9efe73f5a37b009c2e0968fb2619dea4c87d51
4
- data.tar.gz: '01885208a25d500718f786fb66bc2b0753b2b0ff720cf6ed39c9380c9f1d13bf'
3
+ metadata.gz: b759a458f409fc1596611c306a59a14e04be1842b6f44fff4e45eebd61f6b673
4
+ data.tar.gz: deb8ebbd1e4804f7874f3af677969e333cda8321fb935a287789fd166ab20a29
5
5
  SHA512:
6
- metadata.gz: d4887b9af284ee25b16236b8e4a0aad6eadbb1c8583d14119630250749d3d9fce45a138321f50eb2b59e7de0ea107b34d33461da62113d6b1a76e4f8d789b698
7
- data.tar.gz: c435e9556f67254b627c2b7ff21199489b9991bd37e06eb5f1e609a4b99414917a6d731776434a0c42af53e7d362103caf203020200feb79328e310077e53903
6
+ metadata.gz: ecb87f37f0af8ec853a37465aa822b4c547b389ef064579243522429c48e885b56d7fdf9d701cc7e5e08bed1adeb5d0a7a035301351531fe68806b24d15f9923
7
+ data.tar.gz: 53fe8549f0a04d5bf403dda60ab3016e7be26f817453693f6fda68288a926bf89bdd6a86dc858e624a5f4e7ed618d1c8adc1b1e545dedf9fdf2d0926bcfa2543
data/README.md CHANGED
@@ -1,13 +1,18 @@
1
1
  # TUI Test Drive
2
2
 
3
+ *Like Playwright or Puppeteer, but built specifically for Terminal UIs and optimized for AI Coding Agents.*
4
+
3
5
  Testing framework for Terminal User Interfaces (TUIs) with MCP support.
4
6
 
7
+ A Ruby library, but language-agnostic through its JSON test format and MCP server — use it from Python, JavaScript, Go, or any other programming language on Linux and macOS.
8
+
5
9
  **tui-td** lets you:
6
10
  1. Start a TUI application in a virtual terminal (PTY)
7
11
  2. See the output — as structured JSON, plain text, PNG screenshots, or HTML renders
8
12
  3. Send input — keystrokes, text, control sequences
9
13
  4. Analyze output — find text, check colors, detect cursor position
10
14
  5. Loop — adjust and retest without manual intervention
15
+ 6. Integrate — works with any language via JSON test files or MCP
11
16
 
12
17
  ## Installation
13
18
 
@@ -43,8 +48,7 @@ brew install ruby
43
48
  ### 2. Install tui-td
44
49
 
45
50
  ```bash
46
- curl -sL https://github.com/vurte/tui-td/releases/download/v0.1.0/tui-td-0.1.0.gem -o tui-td.gem
47
- gem install tui-td.gem && rm tui-td.gem
51
+ gem install tui-td
48
52
  ```
49
53
 
50
54
  ### 3. Test
@@ -92,6 +92,7 @@ module TUITD
92
92
  attrs = { fg: "default", bg: "default", bold: false, italic: false, underline: false }
93
93
  saved_cursor = nil
94
94
  scroll_region = nil
95
+ pending_dsr = false
95
96
 
96
97
  # Strip everything before the last full clear (if any)
97
98
  # to avoid accumulated garbage
@@ -102,10 +103,11 @@ module TUITD
102
103
  if processed[i] == "\e" && processed[i + 1] == "["
103
104
  # Find end of CSI sequence
104
105
  j = i + 2
105
- j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`fm]/)
106
+ j += 1 while j < processed.length && !processed[j].match?(/[A-HJ-KP-SX@`fmnR]/)
106
107
  seq = processed[i..j]
107
108
 
108
- _apply_csi(seq, cursor, attrs, grid, rows, cols)
109
+ dsr = _apply_csi(seq, cursor, attrs, grid, rows, cols)
110
+ pending_dsr ||= dsr
109
111
 
110
112
  i = j + 1
111
113
  elsif processed[i] == "\n" || processed[i] == "\r\n"
@@ -164,6 +166,7 @@ module TUITD
164
166
  size: { rows: rows, cols: cols },
165
167
  cursor: cursor,
166
168
  rows: grid,
169
+ pending_dsr: pending_dsr,
167
170
  }
168
171
  end
169
172
 
@@ -211,7 +214,7 @@ module TUITD
211
214
  def self._apply_csi(seq, cursor, attrs, grid, rows, cols)
212
215
  # Strip leading escape char if present
213
216
  cleaned = seq.sub(/^\e/, "")
214
- match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`fhm])$/)
217
+ match = cleaned.match(/^\[([\d;]*)([A-HJ-KP-SX@`fhmnR])$/)
215
218
  return unless match
216
219
 
217
220
  params = match[1].split(";").map(&:to_i)
@@ -267,6 +270,11 @@ module TUITD
267
270
  next unless cursor[:row] < rows && cursor[:col] + i < cols
268
271
  grid[cursor[:row]][cursor[:col] + i][:char] = " "
269
272
  end
273
+ when "n" # DSR — Device Status Report request
274
+ # \e[6n = request cursor position → caller must respond with \e[row;colR
275
+ return params[0] == 6
276
+ when "R" # DSR response (from terminal side) or CPR — ignore
277
+ nil
270
278
  end
271
279
  end
272
280
 
data/lib/tui_td/driver.rb CHANGED
@@ -30,6 +30,9 @@ module TUITD
30
30
  @stdout = nil
31
31
  @wait_thr = nil
32
32
  @output_buffer = +""
33
+ @output_mutex = Mutex.new
34
+ @reader_thread = nil
35
+ @reader_running = false
33
36
  end
34
37
 
35
38
  # Start the TUI application in a PTY
@@ -46,6 +49,8 @@ module TUITD
46
49
  wait_for_stable
47
50
  refresh_state!
48
51
 
52
+ _start_reader_thread
53
+
49
54
  true
50
55
  end
51
56
 
@@ -81,7 +86,8 @@ module TUITD
81
86
  loop do
82
87
  raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline
83
88
  read_available!
84
- break if @output_buffer.include?(text)
89
+ found = @output_mutex.synchronize { @output_buffer.include?(text) }
90
+ break if found
85
91
  sleep 0.05
86
92
  end
87
93
  refresh_state!
@@ -114,7 +120,7 @@ module TUITD
114
120
  # Get the terminal output (raw ANSI + text)
115
121
  def raw_output
116
122
  read_available!
117
- @output_buffer
123
+ @output_mutex.synchronize { @output_buffer.dup }
118
124
  end
119
125
 
120
126
  # Get structured terminal state as a Hash
@@ -137,6 +143,8 @@ module TUITD
137
143
 
138
144
  # Close the driver and clean up
139
145
  def close
146
+ _stop_reader_thread
147
+
140
148
  # Kill the process if still running
141
149
  if @pid
142
150
  begin
@@ -156,6 +164,30 @@ module TUITD
156
164
 
157
165
  private
158
166
 
167
+ def _start_reader_thread
168
+ @reader_running = true
169
+ @reader_thread = Thread.new do
170
+ loop do
171
+ break unless @reader_running
172
+ begin
173
+ read_available!
174
+ rescue IOError, Errno::EIO
175
+ break
176
+ end
177
+ sleep 0.05
178
+ end
179
+ end
180
+ end
181
+
182
+ def _stop_reader_thread
183
+ @reader_running = false
184
+ if @reader_thread
185
+ @reader_thread.join(1)
186
+ @reader_thread.kill rescue nil
187
+ @reader_thread = nil
188
+ end
189
+ end
190
+
159
191
  def ensure_running!
160
192
  raise Error, "Driver not started. Call #start first." if @stdin.nil?
161
193
  raise Error, "Process exited (status: #{@wait_thr&.value&.exitstatus})" unless @wait_thr&.alive?
@@ -164,19 +196,35 @@ module TUITD
164
196
  def read_available!
165
197
  return false unless @stdout
166
198
 
167
- ready, = IO.select([@stdout], nil, nil, 0.01)
168
- return false unless ready
199
+ data = @stdout.read_nonblock(4096)
200
+
201
+ @output_mutex.synchronize { @output_buffer << data }
202
+
203
+ respond_to_dsr if data.include?("\e[6n")
169
204
 
170
- data = @stdout.readpartial(4096)
171
- @output_buffer << data
172
205
  true
173
- rescue EOFError
206
+ rescue IO::WaitReadable, EOFError
174
207
  false
175
208
  end
176
209
 
210
+ def respond_to_dsr
211
+ @output_mutex.synchronize do
212
+ @state = ANSIParser.parse(@output_buffer, @rows, @cols)
213
+ @state[:raw] = @output_buffer.dup
214
+ @output_buffer.gsub!("\e[6n", "")
215
+
216
+ cursor = @state[:cursor]
217
+ response = "\e[#{cursor[:row] + 1};#{cursor[:col] + 1}R"
218
+ @stdin&.print(response)
219
+ @stdin&.flush
220
+ end
221
+ end
222
+
177
223
  def refresh_state!
178
- @state = ANSIParser.parse(@output_buffer, @rows, @cols)
179
- @state[:raw] = @output_buffer.dup
224
+ @output_mutex.synchronize do
225
+ @state = ANSIParser.parse(@output_buffer, @rows, @cols)
226
+ @state[:raw] = @output_buffer.dup
227
+ end
180
228
  end
181
229
 
182
230
  def monotonic
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TUITD
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tui-td
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haluk Durmus
@@ -107,9 +107,9 @@ dependencies:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
109
  version: '1.50'
110
- description: tui-td is a Ruby framework for testing terminal UIs. It drives TUIs in
111
- a PTY, captures ANSI state (colors, layout, cursor), and outputs structured data
112
- that AI models can understand. Supports screenshot rendering for vision model consumption.
110
+ description: tui-td drives terminal applications in a PTY, captures ANSI state as
111
+ structured data, and provides PNG screenshots and HTML renders. Includes an MCP
112
+ server for AI-driven testing, a JSON test runner, and RSpec matchers.
113
113
  email:
114
114
  - haluk_durmus@yahoo.de
115
115
  executables:
@@ -155,5 +155,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
155
155
  requirements: []
156
156
  rubygems_version: 4.0.6
157
157
  specification_version: 4
158
- summary: AI-friendly TUI (Terminal User Interface) testing framework
158
+ summary: TUI testing framework — language-agnostic via JSON tests and MCP
159
159
  test_files: []