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 +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +36 -4
- data/lib/tans-parser.rb +2 -0
- data/lib/tans_parser/element.rb +27 -0
- data/lib/tans_parser/selector.rb +231 -0
- data/lib/tans_parser/version.rb +1 -1
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4c7b3233b668eeef9d7240a8cddd5f8389cc62fdf3c641103472696daab956f4
|
|
4
|
+
data.tar.gz: 39391122997e84cde9b4313542a1f2071cc752f66d9577e1c008133359064223
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
#
|
|
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
|
@@ -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
|
data/lib/tans_parser/version.rb
CHANGED
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.
|
|
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
|
|
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:
|
|
168
|
+
summary: Parse ANSI terminal output into structured data with UI element recognition
|
|
166
169
|
test_files: []
|