Class: TUITD::Driver
- Inherits:
-
Object
- Object
- TUITD::Driver
- Defined in:
- lib/tui_td/driver.rb
Overview
Drives a TUI application in a pseudo-terminal (PTY).
Usage:
driver = Driver.new("my_tui_app")
driver.start
driver.wait_for_text("> ")
driver.send("Write hello\n")
state = driver.state_json # => structured JSON for AI
driver.screenshot("out.png")
driver.close
Instance Attribute Summary collapse
-
#command ⇒ Object
readonly
Returns the value of attribute command.
-
#state ⇒ Object
readonly
Returns the value of attribute state.
Instance Method Summary collapse
-
#close ⇒ Object
Close the driver and clean up.
-
#exitstatus ⇒ Object
Get the process exit status (nil if still running).
-
#initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {}) ⇒ Driver
constructor
A new instance of Driver.
-
#raw_output ⇒ Object
Get the terminal output (raw ANSI + text).
-
#refresh ⇒ Object
Refresh the terminal state by re-parsing the output buffer.
-
#screenshot(output_path) ⇒ Object
Capture a PNG screenshot of the current terminal state.
-
#send(text) ⇒ Object
Send text to the TUI.
-
#send_keys(keys) ⇒ Object
Send keys (escape sequences, control characters).
-
#start ⇒ Object
Start the TUI application in a PTY.
-
#state_data ⇒ Object
Get structured terminal state as a Hash.
-
#state_json(pretty: false) ⇒ Object
Get structured terminal state as JSON string.
-
#wait_for_exit ⇒ Object
Wait until the process finishes.
-
#wait_for_stable(stable_ms: 300) ⇒ Object
Wait for output to stabilize (grid content unchanged for N milliseconds).
-
#wait_for_text(text) ⇒ Object
Wait until output contains the given text.
Constructor Details
#initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {}) ⇒ Driver
Returns a new instance of Driver.
25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
# File 'lib/tui_td/driver.rb', line 25 def initialize(command, rows: 40, cols: 120, timeout: 30, chdir: nil, env: {}) @command = command @rows = rows @cols = cols @timeout = timeout @chdir = chdir @env = env @state = nil @stdin = nil @stdout = nil @wait_thr = nil @output_buffer = +"" @output_mutex = Mutex.new @reader_thread = nil @reader_running = false end |
Instance Attribute Details
#command ⇒ Object (readonly)
Returns the value of attribute command.
23 24 25 |
# File 'lib/tui_td/driver.rb', line 23 def command @command end |
#state ⇒ Object (readonly)
Returns the value of attribute state.
23 24 25 |
# File 'lib/tui_td/driver.rb', line 23 def state @state end |
Instance Method Details
#close ⇒ Object
Close the driver and clean up
182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 |
# File 'lib/tui_td/driver.rb', line 182 def close _stop_reader_thread # Kill the process if still running if @pid begin if Process.waitpid(@pid, Process::WNOHANG).nil? begin Process.kill("TERM", @pid) rescue StandardError nil end sleep 0.05 begin Process.kill("KILL", @pid) rescue StandardError nil end end rescue Errno::ECHILD # Already reaped by Process.detach end end begin @stdin&.close rescue StandardError nil end begin @stdout&.close rescue StandardError nil end @stdin = @stdout = @pid = nil end |
#exitstatus ⇒ Object
Get the process exit status (nil if still running)
141 142 143 144 145 146 147 148 |
# File 'lib/tui_td/driver.rb', line 141 def exitstatus return nil unless @wait_thr status = @wait_thr.value status&.exitstatus rescue NoMethodError nil end |
#raw_output ⇒ Object
Get the terminal output (raw ANSI + text)
151 152 153 154 |
# File 'lib/tui_td/driver.rb', line 151 def raw_output read_available! @output_mutex.synchronize { @output_buffer.dup } end |
#refresh ⇒ Object
Refresh the terminal state by re-parsing the output buffer. Call this if the terminal content has changed and you need an up-to-date state.
158 159 160 161 |
# File 'lib/tui_td/driver.rb', line 158 def refresh refresh_state! @state end |
#screenshot(output_path) ⇒ Object
Capture a PNG screenshot of the current terminal state
176 177 178 179 |
# File 'lib/tui_td/driver.rb', line 176 def screenshot(output_path) state_data Screenshot.new(@state).render(output_path) end |
#send(text) ⇒ Object
Send text to the TUI
63 64 65 66 67 68 |
# File 'lib/tui_td/driver.rb', line 63 def send(text) ensure_running! @stdin&.print(text) @stdin&.flush true end |
#send_keys(keys) ⇒ Object
Send keys (escape sequences, control characters)
71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'lib/tui_td/driver.rb', line 71 def send_keys(keys) ensure_running! case keys when :enter then send("\r") when :tab then send("\t") when :escape then send("\e") when :up then send("\e[A") when :down then send("\e[B") when :left then send("\e[D") when :right then send("\e[C") when :backspace then send("\u007f") when :ctrl_c then send("\u0003") when :ctrl_d then send("\u0004") when :page_up then send("\e[5~") when :page_down then send("\e[6~") else send(keys.to_s) end end |
#start ⇒ Object
Start the TUI application in a PTY
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/tui_td/driver.rb', line 43 def start env = { "TERM" => "xterm-256color", "COLUMNS" => @cols.to_s, "LINES" => @rows.to_s, }.merge(@env.transform_keys(&:to_s)) spawn_opts = {} spawn_opts[:chdir] = @chdir if @chdir @stdout, @stdin, @pid = PTY.spawn(env, @command, spawn_opts) @stdout.winsize = [@rows, @cols] # Set PTY window size for TUIs that check winsize @wait_thr = Process.detach(@pid) # Read until initial output stabilizes wait_for_stable refresh_state! _start_reader_thread true end |
#state_data ⇒ Object
Get structured terminal state as a Hash
164 165 166 167 |
# File 'lib/tui_td/driver.rb', line 164 def state_data refresh_state! @state end |
#state_json(pretty: false) ⇒ Object
Get structured terminal state as JSON string
170 171 172 173 |
# File 'lib/tui_td/driver.rb', line 170 def state_json(pretty: false) state_data pretty ? JSON.pretty_generate(@state) : JSON.generate(@state) end |
#wait_for_exit ⇒ Object
Wait until the process finishes
136 137 138 |
# File 'lib/tui_td/driver.rb', line 136 def wait_for_exit @wait_thr&.value end |
#wait_for_stable(stable_ms: 300) ⇒ Object
Wait for output to stabilize (grid content unchanged for N milliseconds)
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# File 'lib/tui_td/driver.rb', line 106 def wait_for_stable(stable_ms: 300) deadline = monotonic + @timeout last_change = monotonic last_grid = nil loop do raise TimeoutError, "Timeout waiting for stable output" if monotonic > deadline had_data = read_available! process_alive = process_alive? if had_data current_grid = parse_grid_snapshot if current_grid != last_grid last_grid = current_grid last_change = monotonic end elsif !process_alive # Process exited and no more data — final state reached break elsif last_grid && (monotonic - last_change) * 1000 >= stable_ms # rubocop:disable Lint/DuplicateBranch break end sleep 0.05 end refresh_state! end |
#wait_for_text(text) ⇒ Object
Wait until output contains the given text
91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/tui_td/driver.rb', line 91 def wait_for_text(text) deadline = monotonic + @timeout loop do raise TimeoutError, "Timeout waiting for: #{text.inspect}" if monotonic > deadline read_available! found = @output_mutex.synchronize { @output_buffer.include?(text) } break if found sleep 0.05 end refresh_state! end |