tans-parser 0.1.0 → 0.1.1

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: 7f88d3f3ba4bc87f81c46fb3168b660728f980e5fa97daf608ba9f68e35f2bdf
4
- data.tar.gz: 5b535136d317e598098cca288e7f8f157f0d0d45b6974f437572860d5554cb14
3
+ metadata.gz: 4c7b3233b668eeef9d7240a8cddd5f8389cc62fdf3c641103472696daab956f4
4
+ data.tar.gz: 39391122997e84cde9b4313542a1f2071cc752f66d9577e1c008133359064223
5
5
  SHA512:
6
- metadata.gz: 32f9a060a089b2d8771b249e5a548b1725498d6b1b1a101cc8cbd547a41939fc8683d93853c734a8eb3f7edcbce6e5cb0fc3af162207727b02ac5f8850e5a5b5
7
- data.tar.gz: 34481b9990055c0ba0fdd4d972703cbcab3208360ad6e4877cc0953e3f9de91838095aa5e9f7c54b4e3e563820d5e1b752a3925883021fdda9b45dd0f882862d
6
+ metadata.gz: 6489e45f59dbb4f7e7375108bc4f6f881fab6989415b5d44d09337bbaa166edb43974c1f6ebf8f1af068637dc7ee8bd604e0e6f55c8ffc0268f5fe610a23ec3e
7
+ data.tar.gz: e485259b38989cc3f1953691113bc6f92ee6da125c9a0502f38f5550ca75703e0ec3919043f9297b9ee0611044701086b937cb88d2e32a8c5d7047ae6267c461
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 0.1.1
4
+
5
+ - `TansParser::Element` — value object for recognized UI elements (role, text, position, size, colors)
6
+ - `TansParser::Selector` — scans terminal state for recognized UI elements:
7
+ - Buttons (`[ OK ]`, `(Cancel)`, `<Submit>`)
8
+ - Checkboxes (`[x]`, `[*]`, `[ ]` at line starts)
9
+ - Dialogs (box-drawing character regions)
10
+ - Statusbars (bottom row with non-default background)
11
+ - Progress bars (`[#### ]`, `[====> ]` patterns)
12
+ - Query API: `get_by_text`, `get_by_role`, convenience accessors (`buttons`, `checkboxes`, `dialogs`, `statusbars`, `progress_bars`)
13
+ - `Element#to_h` with nil-value exclusion via `.compact`
14
+ - 36 new tests for selector and element, 100% line and branch coverage maintained
15
+
3
16
  ## 0.1.0
4
17
 
5
18
  - Initial release: ANSI escape sequence parser extracted from tui-td
data/README.md CHANGED
@@ -1,9 +1,7 @@
1
- # tans-parser — Terminal ANSI State Utils
1
+ # tans-parser
2
2
 
3
3
  Parse raw terminal output with ANSI escape sequences into structured, queryable data.
4
4
 
5
- Zero runtime dependencies. Ruby stdlib only.
6
-
7
5
  ## Installation
8
6
 
9
7
  Ruby 3.0+ required.
@@ -47,7 +45,7 @@ state.foreground_at(0, 0) # => "red"
47
45
  state.background_at(0, 0) # => "default"
48
46
  state.style_at(0, 0) # => {bold: false, italic: false, underline: false}
49
47
 
50
- # AI-friendly JSON with highlights
48
+ # JSON with highlights
51
49
  state.to_ai_json
52
50
  # => {size:, cursor:, text:, highlights:, summary:}
53
51
  ```
@@ -70,6 +68,40 @@ resolve_color("color82", nil) # => [95, 255, 0]
70
68
  xterm_256(16) # => [0x00, 0x00, 0x00]
71
69
  ```
72
70
 
71
+ ### Element recognition
72
+
73
+ ```ruby
74
+ state = TansParser::State.new(state_data)
75
+ selector = TansParser::Selector.new(state)
76
+
77
+ # Find UI elements by role
78
+ selector.buttons # => [Element, ...] — [ OK ], (Cancel), <Submit>
79
+ selector.checkboxes # => [Element, ...] — [x], [*], [ ] at line starts
80
+ selector.dialogs # => [Element, ...] — box-drawing character regions (┌─┐│└┘)
81
+ selector.statusbars # => [Element, ...] — bottom row with non-default background
82
+ selector.progress_bars # => [Element, ...] — [#### ], [====> ] patterns
83
+
84
+ # Find by text or role
85
+ selector.get_by_text("OK") # partial match
86
+ selector.get_by_role(:button) # also accepts "button" (String)
87
+ ```
88
+
89
+ Each `TansParser::Element` is a Struct:
90
+
91
+ ```ruby
92
+ el = selector.buttons.first
93
+ el.role # => :button
94
+ el.text # => "OK"
95
+ el.row # => 1
96
+ el.col # => 2
97
+ el.width # => 4
98
+ el.height # => 1
99
+ el.checked # => nil (für Checkboxen: true/false)
100
+ el.fg # => "default"
101
+ el.bg # => "default"
102
+ el.to_h # => {role: :button, text: "OK", row: 1, col: 2, ...}
103
+ ```
104
+
73
105
  ## Cell format
74
106
 
75
107
  Each cell is a Hash with these keys:
data/lib/tans-parser.rb CHANGED
@@ -7,4 +7,6 @@ end
7
7
  require_relative "tans_parser/version"
8
8
  require_relative "tans_parser/ansi_parser"
9
9
  require_relative "tans_parser/ansi_utils"
10
+ require_relative "tans_parser/element"
11
+ require_relative "tans_parser/selector"
10
12
  require_relative "tans_parser/state"
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TansParser
4
+ # Describes a recognized UI element on the terminal screen.
5
+ Element = Struct.new(
6
+ :role,
7
+ :text,
8
+ :row, :col,
9
+ :width, :height,
10
+ :checked,
11
+ :focused,
12
+ :fg, :bg,
13
+ keyword_init: true,
14
+ ) do
15
+ def to_h
16
+ {
17
+ role: role,
18
+ text: text,
19
+ row: row, col: col,
20
+ width: width, height: height,
21
+ checked: checked,
22
+ focused: focused,
23
+ fg: fg, bg: bg,
24
+ }.compact
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TansParser
4
+ # Scans terminal state for recognized UI elements.
5
+ #
6
+ # selector = Selector.new(state)
7
+ # selector.get_by_text("OK") # => [Element, ...]
8
+ # selector.get_by_role(:button) # => [Element, ...]
9
+ # selector.buttons # => [Element, ...]
10
+ # selector.dialogs # => [Element, ...]
11
+ #
12
+ class Selector
13
+ TOP_LEFT_CORNERS = /[┌┏┎┍]/
14
+ BOTTOM_LEFT_CORNERS = %w[└ ┗ ┖ ┕ ╰ ╚].freeze
15
+ BOTTOM_RIGHT_CORNERS = %w[┘ ┛ ┚ ┙ ╯ ╝].freeze
16
+
17
+ attr_reader :state, :elements
18
+
19
+ def initialize(state)
20
+ @state = state.is_a?(State) ? state : State.new(state)
21
+ @elements = scan
22
+ end
23
+
24
+ # Find elements by visible text (partial match).
25
+ def get_by_text(text)
26
+ @elements.select { |e| e.text&.include?(text) }
27
+ end
28
+
29
+ # Find elements by role.
30
+ def get_by_role(role)
31
+ @elements.select { |e| e.role == role.to_sym }
32
+ end
33
+
34
+ # Convenience accessors
35
+ def buttons
36
+ get_by_role(:button)
37
+ end
38
+
39
+ def checkboxes
40
+ get_by_role(:checkbox)
41
+ end
42
+
43
+ def dialogs
44
+ get_by_role(:dialog)
45
+ end
46
+
47
+ def statusbars
48
+ get_by_role(:statusbar)
49
+ end
50
+
51
+ def progress_bars
52
+ get_by_role(:progress)
53
+ end
54
+
55
+ private
56
+
57
+ def grid
58
+ @state.grid
59
+ end
60
+
61
+ def scan
62
+ results = []
63
+ results.concat(detect_buttons)
64
+ results.concat(detect_checkboxes)
65
+ results.concat(detect_dialogs)
66
+ results.concat(detect_statusbars)
67
+ results.concat(detect_progress_bars)
68
+ results
69
+ end
70
+
71
+ # Detects buttons: [ OK ], (Cancel), <Submit>
72
+ # rubocop:disable Metrics/AbcSize
73
+ def detect_buttons
74
+ buttons = []
75
+ grid.each_with_index do |row, r|
76
+ line = row.map { |c| c[:char] }.join
77
+ scan_match = line.enum_for(:scan, /\[([^\]]+)\]|\(([^)]+)\)|<([^>]+)>/)
78
+ scan_match.each do
79
+ text = (::Regexp.last_match(1) || ::Regexp.last_match(2) || ::Regexp.last_match(3)).to_s.strip
80
+ next if text.empty?
81
+
82
+ col = ::Regexp.last_match.begin(0)
83
+ buttons << Element.new(
84
+ role: :button,
85
+ text: text,
86
+ row: r, col: col,
87
+ width: ::Regexp.last_match[0].length, height: 1,
88
+ fg: row[col][:fg],
89
+ bg: row[col][:bg],
90
+ )
91
+ end
92
+ end
93
+ buttons
94
+ end
95
+ # rubocop:enable Metrics/AbcSize
96
+
97
+ # Detects checkboxes: [x], [*], [ ] at start of lines
98
+ def detect_checkboxes
99
+ checkboxes = []
100
+ grid.each_with_index do |row, r|
101
+ line = row.map { |c| c[:char] }.join
102
+ match = line.match(/^(\s*)\[([ xX*])\]\s+(.+)/)
103
+ next unless match
104
+
105
+ checked = match[2] != " "
106
+ label = match[3].strip
107
+ col = match.begin(3)
108
+ checkboxes << Element.new(
109
+ role: :checkbox,
110
+ text: label,
111
+ row: r, col: col,
112
+ width: label.length, height: 1,
113
+ checked: checked,
114
+ )
115
+ end
116
+ checkboxes
117
+ end
118
+
119
+ # Detects dialogs: regions enclosed by box-drawing characters
120
+ def detect_dialogs
121
+ dialogs = []
122
+ grid.each_with_index do |row, r|
123
+ line = row.map { |c| c[:char] }.join
124
+ tl_idx = 0
125
+ while (tl_idx = line.index(TOP_LEFT_CORNERS, tl_idx))
126
+ width = dialog_top_width(line, tl_idx)
127
+ unless width
128
+ tl_idx += 1
129
+ next
130
+ end
131
+
132
+ bottom_r = dialog_bottom_row(tl_idx, width, r + 1)
133
+ if bottom_r
134
+ height = bottom_r - r + 1
135
+ text = extract_dialog_text(r + 1, tl_idx + 1, width - 2, height - 2)
136
+ dialogs << Element.new(
137
+ role: :dialog,
138
+ text: text,
139
+ row: r, col: tl_idx,
140
+ width: width, height: height,
141
+ )
142
+ end
143
+ tl_idx += 1
144
+ end
145
+ end
146
+ dialogs
147
+ end
148
+
149
+ # Returns the width of a dialog top border if valid, nil otherwise
150
+ def dialog_top_width(line, tl_idx)
151
+ top_row = line[tl_idx..]
152
+ tr_match = top_row.match(/^[┌┏┎┍]([─━]*)([┐┓┒┑])/)
153
+ return nil unless tr_match
154
+
155
+ tr_match[0].length
156
+ end
157
+
158
+ # Returns the row index of a matching bottom border, nil if not found
159
+ def dialog_bottom_row(tl_idx, width, start_row)
160
+ r = start_row
161
+ while r < grid.length
162
+ bottom_line = grid[r].map { |c| c[:char] }.join
163
+ bot_left = bottom_line[tl_idx]
164
+ if BOTTOM_LEFT_CORNERS.include?(bot_left) && bottom_line[tl_idx + width - 1]
165
+ bot_right = bottom_line[tl_idx + width - 1]
166
+ return r if BOTTOM_RIGHT_CORNERS.include?(bot_right)
167
+ end
168
+ r += 1
169
+ end
170
+ nil
171
+ end
172
+
173
+ # Extracts visible text from inside a dialog
174
+ def extract_dialog_text(start_row, start_col, inner_width, inner_height)
175
+ lines = []
176
+ (start_row...[start_row + inner_height, grid.length].min).each do |r|
177
+ row = grid[r]
178
+ slice = row[start_col, [inner_width, row.length - start_col].min]
179
+ line = slice.map { |c| c[:char] }.join.rstrip
180
+ lines << line unless line.empty?
181
+ end
182
+ lines.join(" ").strip
183
+ end
184
+
185
+ # Detects statusbar: bottom row with reversed/inverse colors
186
+ def detect_statusbars
187
+ bars = []
188
+ return bars if grid.empty?
189
+
190
+ last_row_idx = grid.length - 1
191
+ last_row = grid[last_row_idx]
192
+
193
+ non_default = last_row.reject { |c| c[:bg] == "default" }
194
+ return bars if non_default.length < 3
195
+
196
+ text = last_row.map { |c| c[:char] }.join.strip
197
+ return bars if text.empty?
198
+
199
+ bars << Element.new(
200
+ role: :statusbar,
201
+ text: text,
202
+ row: last_row_idx, col: 0,
203
+ width: last_row.length, height: 1,
204
+ bg: non_default.first[:bg],
205
+ )
206
+ bars
207
+ end
208
+
209
+ # Detects progress bars: [#### ] or [=====> ] patterns
210
+ def detect_progress_bars
211
+ bars = []
212
+ grid.each_with_index do |row, r|
213
+ line = row.map { |c| c[:char] }.join
214
+ match = line.match(/\[([#>=-]+)\s*\]/)
215
+ next unless match
216
+
217
+ filled = match[1]
218
+ total = match[0].length - 2
219
+ percent = (filled.length.to_f / total * 100).round
220
+ bars << Element.new(
221
+ role: :progress,
222
+ text: "#{percent}%",
223
+ row: r, col: ::Regexp.last_match.begin(0),
224
+ width: match[0].length, height: 1,
225
+ checked: percent == 100,
226
+ )
227
+ end
228
+ bars
229
+ end
230
+ end
231
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TansParser
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tans-parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Haluk Durmus
@@ -124,7 +124,8 @@ dependencies:
124
124
  description: tans-parser parses raw terminal output with ANSI escape sequences into
125
125
  a structured grid representation with per-cell attributes (char, fg, bg, bold, italic,
126
126
  underline, blink). Includes a query API (State) for text search, color inspection,
127
- and AI-friendly JSON output.
127
+ and JSON output, plus heuristic UI element recognition (Selector) for buttons, checkboxes,
128
+ dialogs, statusbars, and progress bars.
128
129
  email:
129
130
  - haluk_durmus@yahoo.de
130
131
  executables: []
@@ -137,6 +138,8 @@ files:
137
138
  - lib/tans-parser.rb
138
139
  - lib/tans_parser/ansi_parser.rb
139
140
  - lib/tans_parser/ansi_utils.rb
141
+ - lib/tans_parser/element.rb
142
+ - lib/tans_parser/selector.rb
140
143
  - lib/tans_parser/state.rb
141
144
  - lib/tans_parser/version.rb
142
145
  homepage: https://github.com/vurte/tans-parser
@@ -162,5 +165,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
162
165
  requirements: []
163
166
  rubygems_version: 4.0.6
164
167
  specification_version: 4
165
- summary: Terminal ANSI State Utils — parse ANSI terminal output into structured data
168
+ summary: Parse ANSI terminal output into structured data with UI element recognition
166
169
  test_files: []