Class: TansParser::State

Inherits:
Object
  • Object
show all
Defined in:
lib/tans_parser/state.rb

Overview

Represents the parsed state of a terminal screen. Provides high-level query methods for AI consumption.

Constant Summary collapse

TEXT_SEARCH_TIMEOUT =

Search for text across the entire terminal. For regex patterns, matching is bounded by a timeout to prevent ReDoS.

state.find_text("hello")                  # partial match (default)
state.find_text("hello", match: :exact)   # exact row match
state.find_text("\\d+", match: :regex)    # regex from string
state.find_text(/\\d{3}/)                 # Regexp object (partial mode)

Returns [{ row:, col:, text:, full_line: }, …]. text is the actual matched substring (for Regexp/:regex mode) or the pattern string (for :partial/:exact with String).

5
DEFAULT_CELL =
{ char: " ", fg: "default", bg: "default",
bold: false, italic: false, underline: false, blink: false, }.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data) ⇒ State

Returns a new instance of State.

Raises:

  • (ArgumentError)


13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/tans_parser/state.rb', line 13

def initialize(data)
  raise ArgumentError, "State data must include :size key" unless data[:size]
  raise ArgumentError, "State data must include :rows key" unless data[:rows]

  @rows = data[:size][:rows]
  @cols = data[:size][:cols]
  @grid = data[:rows]
  @cursor = data[:cursor]

  cursor_info = data[:cursor].is_a?(Hash) ? data[:cursor] : {}
  @cursor_visible = data.key?(:cursor_visible) ? data[:cursor_visible] : (cursor_info[:visible] != false)
  @cursor_style = data.key?(:cursor_style) ? data[:cursor_style] : (cursor_info[:style] || 1)

  @mouse_mode = data[:mouse_mode] || :none
  @mouse_format = data[:mouse_format] || :normal

  @annotations = data[:annotations] || []
end

Instance Attribute Details

#annotationsObject (readonly)

Returns the value of attribute annotations.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def annotations
  @annotations
end

#colsObject (readonly)

Returns the value of attribute cols.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def cols
  @cols
end

#cursorObject (readonly)

Returns the value of attribute cursor.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def cursor
  @cursor
end

#cursor_styleObject (readonly)

Returns the value of attribute cursor_style.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def cursor_style
  @cursor_style
end

#cursor_visibleObject (readonly)

Returns the value of attribute cursor_visible.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def cursor_visible
  @cursor_visible
end

#gridObject (readonly)

Returns the value of attribute grid.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def grid
  @grid
end

#mouse_formatObject (readonly)

Returns the value of attribute mouse_format.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def mouse_format
  @mouse_format
end

#mouse_modeObject (readonly)

Returns the value of attribute mouse_mode.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def mouse_mode
  @mouse_mode
end

#rowsObject (readonly)

Returns the value of attribute rows.



11
12
13
# File 'lib/tans_parser/state.rb', line 11

def rows
  @rows
end

Instance Method Details

#annotate_role(role, row:, col:, width: 1, height: 1, text: nil, **extra) ⇒ Object

Annotate a region of the terminal with a semantic role. These annotations are picked up by Selector during element recognition. rubocop:disable Metrics/ParameterLists



35
36
37
38
# File 'lib/tans_parser/state.rb', line 35

def annotate_role(role, row:, col:, width: 1, height: 1, text: nil, **extra)
  @annotations << { role: role.to_sym, row: row, col: col,
                    width: width, height: height, text: text, }.merge(extra)
end

#background_at(row, col) ⇒ Object



89
90
91
92
93
# File 'lib/tans_parser/state.rb', line 89

def background_at(row, col)
  return nil if row >= @rows || col >= @cols

  @grid[row][col][:bg]
end

#diff(other_state, chars_only: false, ignore_rows: []) ⇒ Object

Compare this state with another State and return cell-level differences. With chars_only: true, only differences in the :char key are reported. Use ignore_rows: to skip specific rows (e.g. cursor/prompt lines).



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/tans_parser/state.rb', line 132

def diff(other_state, chars_only: false, ignore_rows: [])
  other = other_state.is_a?(State) ? other_state : State.new(other_state)
  max_rows = [@rows, other.rows].max
  max_cols = [@cols, other.cols].max
  results = []

  (0...max_rows).each do |r|
    next if ignore_rows.include?(r)

    (0...max_cols).each do |c|
      a = cell_at(r, c)
      b = other.send(:cell_at, r, c)
      next if chars_only ? a[:char] == b[:char] : a == b

      results << { row: r, col: c, before: a, after: b }
    end
  end
  results
end

#find_text(pattern, match: :partial) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/tans_parser/state.rb', line 66

def find_text(pattern, match: :partial)
  unless %i[partial exact regex].include?(match)
    raise ArgumentError, "unknown match mode: #{match.inspect}. Use :partial, :exact, or :regex"
  end

  results = []
  case match
  when :exact
    find_text_exact(pattern, results)
  else
    compiled = compile_pattern(pattern, match)
    find_text_with_regex(compiled, results)
  end
  results
end

#foreground_at(row, col) ⇒ Object

Get the color at a specific cell



83
84
85
86
87
# File 'lib/tans_parser/state.rb', line 83

def foreground_at(row, col)
  return nil if row >= @rows || col >= @cols

  @grid[row][col][:fg]
end

#plain_textObject

Get plain text of the entire terminal (no ANSI)



42
43
44
# File 'lib/tans_parser/state.rb', line 42

def plain_text
  @grid.map { |row| row.map { |c| c[:char] }.join.rstrip }.join("\n")
end

#style_at(row, col) ⇒ Object



95
96
97
98
99
100
# File 'lib/tans_parser/state.rb', line 95

def style_at(row, col)
  return nil if row >= @rows || col >= @cols

  cell = @grid[row][col]
  { bold: cell[:bold], italic: cell[:italic], underline: cell[:underline] }
end

#text_at(row, col, length = @cols - col) ⇒ Object

Get text at a specific position



47
48
49
50
51
# File 'lib/tans_parser/state.rb', line 47

def text_at(row, col, length = @cols - col)
  return "" if row >= @rows || col >= @cols

  @grid[row][col, length].map { |c| c[:char] }.join
end

#to_ai_jsonObject



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/tans_parser/state.rb', line 102

def to_ai_json
  h = extract_highlights
  cursor_info = @cursor.is_a?(Hash) ? @cursor : {}
  r = cursor_info[:row] || cursor_info["row"] || 0
  c = cursor_info[:col] || cursor_info["col"] || 0
  styled_count = h.count { |hl| hl[:bold] || hl[:italic] || hl[:underline] || hl[:fg] || hl[:bg] }

  summary = "Cursor at [#{r},#{c}]. "
  summary << "#{styled_count} styled row#{"s" unless styled_count == 1}"
  fgs = h.flat_map { |hl| hl[:fg] }.compact.uniq
  bgs = h.flat_map { |hl| hl[:bg] }.compact.uniq
  summary << ", colors: fg=#{fgs.sort.join(",")}" unless fgs.empty?
  summary << ", bg=#{bgs.sort.join(",")}" unless bgs.empty?
  summary << "."

  {
    size: { rows: @rows, cols: @cols },
    cursor: cursor_info,
    text: plain_text,
    highlights: h,
    summary: summary,
  }
end