tui-td 0.1.1 → 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 +4 -4
- data/README.md +5 -0
- data/lib/tui_td/ansi_parser.rb +11 -3
- data/lib/tui_td/driver.rb +57 -9
- data/lib/tui_td/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b759a458f409fc1596611c306a59a14e04be1842b6f44fff4e45eebd61f6b673
|
|
4
|
+
data.tar.gz: deb8ebbd1e4804f7874f3af677969e333cda8321fb935a287789fd166ab20a29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
data/lib/tui_td/ansi_parser.rb
CHANGED
|
@@ -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@`
|
|
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@`
|
|
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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
-
@
|
|
179
|
-
|
|
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
|
data/lib/tui_td/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Haluk Durmus
|
|
@@ -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: TUI
|
|
158
|
+
summary: TUI testing framework — language-agnostic via JSON tests and MCP
|
|
159
159
|
test_files: []
|