tuile 0.13.0 → 0.15.0
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 +150 -37
- data/COMPARISON.md +101 -0
- data/DECISIONS.md +4266 -226
- data/README.md +44 -24
- data/TERMINOLOGY.md +22 -7
- data/book/03-layout.md +17 -10
- data/book/05-focus.md +67 -3
- data/book/06-theming.md +153 -7
- data/book/07-components.md +643 -67
- data/book/08-testing.md +94 -0
- data/book/09-styled-text.md +3 -3
- data/book/10-locale.md +216 -0
- data/book/README.md +14 -5
- data/examples/file_commander.rb +1 -1
- data/examples/sampler.rb +402 -62
- data/ideas/arrow-key-navigation.md +2 -2
- data/ideas/binder.md +177 -0
- data/ideas/composite-field.md +77 -0
- data/ideas/focus-accent.md +116 -0
- data/ideas/form-layout.md +151 -0
- data/ideas/hover/probe.rb +241 -0
- data/ideas/hover/probe_spec.rb +82 -0
- data/ideas/hover.md +909 -0
- data/ideas/modal-backdrop.md +24 -0
- data/ideas/new-components.md +49 -29
- data/lib/tuile/buffer.rb +51 -3
- data/lib/tuile/color.rb +143 -0
- data/lib/tuile/color_depth.rb +80 -0
- data/lib/tuile/component/abstract_string_field.rb +106 -58
- data/lib/tuile/component/abstract_wrapping_field.rb +243 -0
- data/lib/tuile/component/big_decimal_field.rb +52 -79
- data/lib/tuile/component/button.rb +3 -3
- data/lib/tuile/component/checkbox.rb +3 -3
- data/lib/tuile/component/checkbox_group.rb +36 -20
- data/lib/tuile/component/combo_box.rb +68 -33
- data/lib/tuile/component/confirm_window.rb +442 -0
- data/lib/tuile/component/date_field.rb +322 -0
- data/lib/tuile/component/float_field.rb +57 -82
- data/lib/tuile/component/has_bad_input.rb +88 -0
- data/lib/tuile/component/has_caption.rb +8 -0
- data/lib/tuile/component/has_content.rb +43 -11
- data/lib/tuile/component/has_placeholder.rb +62 -0
- data/lib/tuile/component/has_validation.rb +115 -0
- data/lib/tuile/component/has_value.rb +28 -1
- data/lib/tuile/component/info_window.rb +64 -16
- data/lib/tuile/component/integer_field.rb +51 -78
- data/lib/tuile/component/label.rb +6 -38
- data/lib/tuile/component/layout/box.rb +87 -19
- data/lib/tuile/component/layout.rb +13 -13
- data/lib/tuile/component/list.rb +11 -6
- data/lib/tuile/component/list_dropdown.rb +22 -10
- data/lib/tuile/component/log_text_view.rb +71 -0
- data/lib/tuile/component/log_window.rb +13 -48
- data/lib/tuile/component/menu_bar/cascade.rb +3 -3
- data/lib/tuile/component/menu_bar.rb +5 -5
- data/lib/tuile/component/notification.rb +16 -34
- data/lib/tuile/component/overlay.rb +209 -0
- data/lib/tuile/component/popup.rb +59 -187
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +39 -22
- data/lib/tuile/component/select.rb +26 -10
- data/lib/tuile/component/slot.rb +54 -0
- data/lib/tuile/component/tab_sheet.rb +0 -11
- data/lib/tuile/component/tabs.rb +5 -5
- data/lib/tuile/component/text_area.rb +14 -8
- data/lib/tuile/component/text_field.rb +42 -15
- data/lib/tuile/component/text_view.rb +25 -8
- data/lib/tuile/component/time_field.rb +454 -0
- data/lib/tuile/component/window.rb +48 -59
- data/lib/tuile/component.rb +580 -54
- data/lib/tuile/event_queue.rb +21 -1
- data/lib/tuile/fake_screen.rb +37 -3
- data/lib/tuile/final.rb +75 -0
- data/lib/tuile/keys.rb +7 -0
- data/lib/tuile/locale.rb +851 -0
- data/lib/tuile/screen.rb +251 -55
- data/lib/tuile/screen_pane.rb +50 -44
- data/lib/tuile/styled_string.rb +40 -7
- data/lib/tuile/terminal_background.rb +74 -16
- data/lib/tuile/testing.rb +198 -0
- data/lib/tuile/theme.rb +100 -10
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +88 -12
- data/lib/tuile.rb +1 -0
- data/sig/tuile.rbs +4545 -770
- metadata +25 -1
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Terminal mouse-mode probe — fills one row of `ideas/hover.md`'s matrix.
|
|
5
|
+
# Research tooling for that idea; it dies with the note at graduation.
|
|
6
|
+
#
|
|
7
|
+
# Deliberately depends on nothing but stdlib: this measures the *terminal*, so
|
|
8
|
+
# Tuile's own parsing (and its fixed 5-byte escape gulp) must stay out of the
|
|
9
|
+
# way. Bytes are read greedily and parsed incrementally, so the log shows what
|
|
10
|
+
# actually arrived rather than what a fixed-width reader would have made of it.
|
|
11
|
+
#
|
|
12
|
+
# Run it in its OWN terminal / tmux window — never in one where another TUI
|
|
13
|
+
# owns stdin, and never in the terminal a coding agent is drawing on: mode 1003
|
|
14
|
+
# would pour motion reports into *its* stdin.
|
|
15
|
+
#
|
|
16
|
+
# ruby ideas/hover/probe.rb [logfile]
|
|
17
|
+
#
|
|
18
|
+
# Rows recorded so far live in `ideas/hover.md` under "Measured". Still wanted:
|
|
19
|
+
# Alacritty bare, Alacritty over ssh, tmux local. The logs themselves are not
|
|
20
|
+
# committed — a re-run regenerates them, and the findings are what graduate.
|
|
21
|
+
#
|
|
22
|
+
# The parser is covered by ideas/hover/probe_spec.rb — run that before trusting
|
|
23
|
+
# an interactive session, since a decode slip wastes the whole run (it already
|
|
24
|
+
# caught an X10 coordinate off-by-one: the button code is byte-32, but the
|
|
25
|
+
# coordinates are 1-based *and* offset, so they decode as byte-33).
|
|
26
|
+
|
|
27
|
+
require "io/console"
|
|
28
|
+
|
|
29
|
+
LOG = if __FILE__ == $PROGRAM_NAME
|
|
30
|
+
File.open(ARGV[0] || "mouse_probe.log", "w").tap { _1.sync = true }
|
|
31
|
+
else
|
|
32
|
+
$stderr # loaded for testing: never open a log, never touch the terminal
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def say(msg) = ($stdout.print("#{msg}\r\n"); $stdout.flush)
|
|
36
|
+
def log(msg) = LOG.puts(msg)
|
|
37
|
+
def esc(seq) = ($stdout.print(seq); $stdout.flush)
|
|
38
|
+
|
|
39
|
+
def both(msg)
|
|
40
|
+
say msg
|
|
41
|
+
log msg
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# --- incremental parser -----------------------------------------------------
|
|
45
|
+
# Returns [events, leftover]. Each event is [kind, detail].
|
|
46
|
+
def parse(buf)
|
|
47
|
+
events = []
|
|
48
|
+
until buf.empty?
|
|
49
|
+
if buf.start_with?("\e[M")
|
|
50
|
+
break if buf.bytesize < 6
|
|
51
|
+
|
|
52
|
+
# Button code is offset by 32; coordinates are 1-based *and* offset by 32,
|
|
53
|
+
# so they come back with -33. Mixing these up is the classic X10 slip.
|
|
54
|
+
cb, cx, cy = buf[3].ord - 32, buf[4].ord - 33, buf[5].ord - 33
|
|
55
|
+
events << [:x10, decode_cb(cb) + " at #{cx},#{cy}"]
|
|
56
|
+
buf = buf[6..]
|
|
57
|
+
elsif (m = buf.match(/\A\e\[<(\d+);(\d+);(\d+)([Mm])/))
|
|
58
|
+
cb = m[1].to_i
|
|
59
|
+
kind = m[4] == "M" ? "press" : "release"
|
|
60
|
+
events << [:sgr, "#{decode_cb(cb)} [#{kind}] at #{m[2].to_i - 1},#{m[3].to_i - 1}"]
|
|
61
|
+
buf = buf[m[0].bytesize..]
|
|
62
|
+
elsif buf.start_with?("\e[I")
|
|
63
|
+
events << [:focus, "FocusIn"]
|
|
64
|
+
buf = buf[3..]
|
|
65
|
+
elsif buf.start_with?("\e[O")
|
|
66
|
+
events << [:focus, "FocusOut"]
|
|
67
|
+
buf = buf[3..]
|
|
68
|
+
elsif (m = buf.match(/\A\e\[\?(\d+);(\d+)\$y/))
|
|
69
|
+
events << [:decrpm, "mode #{m[1]} => #{decrpm_meaning(m[2].to_i)}"]
|
|
70
|
+
buf = buf[m[0].bytesize..]
|
|
71
|
+
elsif buf.start_with?("\e[<") || buf.start_with?("\e[?")
|
|
72
|
+
break # incomplete, wait for more bytes
|
|
73
|
+
else
|
|
74
|
+
events << [:other, buf[0].inspect]
|
|
75
|
+
buf = buf[1..]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
[events, buf]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def decode_cb(code)
|
|
82
|
+
base = code & 0b11
|
|
83
|
+
mods = []
|
|
84
|
+
mods << "shift" if code & 4 != 0
|
|
85
|
+
mods << "meta" if code & 8 != 0
|
|
86
|
+
mods << "ctrl" if code & 16 != 0
|
|
87
|
+
motion = code & 32 != 0
|
|
88
|
+
wheel = code & 64 != 0
|
|
89
|
+
name =
|
|
90
|
+
if wheel
|
|
91
|
+
%w[wheel_up wheel_down wheel_left wheel_right][base] || "wheel?#{base}"
|
|
92
|
+
else
|
|
93
|
+
%w[left middle right release][base]
|
|
94
|
+
end
|
|
95
|
+
"code=#{code} #{motion ? 'MOTION ' : ''}#{name}#{mods.empty? ? '' : " +#{mods.join('+')}"}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def decrpm_meaning(ps)
|
|
99
|
+
{ 0 => "NOT RECOGNIZED", 1 => "set", 2 => "reset (supported)",
|
|
100
|
+
3 => "permanently set", 4 => "permanently reset" }[ps] || "unknown(#{ps})"
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# --- drain ------------------------------------------------------------------
|
|
104
|
+
# Reads for `seconds`, returning every parsed event. `stop_on` ends it early.
|
|
105
|
+
def drain(seconds, stop_on: nil)
|
|
106
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + seconds
|
|
107
|
+
buf = +""
|
|
108
|
+
out = []
|
|
109
|
+
loop do
|
|
110
|
+
left = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
111
|
+
break if left <= 0
|
|
112
|
+
next unless IO.select([$stdin], nil, nil, [left, 0.2].min)
|
|
113
|
+
|
|
114
|
+
chunk = $stdin.read_nonblock(4096)
|
|
115
|
+
# Timestamp the *read*, and record the chunk size: inter-arrival gaps and
|
|
116
|
+
# multi-event chunks are how we tell whether ssh/tmux coalesced anything.
|
|
117
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
118
|
+
buf << chunk
|
|
119
|
+
events, buf = parse(buf)
|
|
120
|
+
log format(" [read %+7.3fs %3dB -> %d ev]", now - deadline + seconds, chunk.bytesize, events.size) if events.any?
|
|
121
|
+
events.each do |kind, detail|
|
|
122
|
+
out << [kind, detail]
|
|
123
|
+
log " #{kind}: #{detail}"
|
|
124
|
+
end
|
|
125
|
+
break if stop_on && buf.empty? && out.any? { |(_, d)| d.include?(stop_on) }
|
|
126
|
+
end
|
|
127
|
+
out
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def wait_for_enter
|
|
131
|
+
say " [press ENTER to start]"
|
|
132
|
+
loop do
|
|
133
|
+
IO.select([$stdin], nil, nil, 60) or break
|
|
134
|
+
break if $stdin.read_nonblock(4096).include?("\r") || false
|
|
135
|
+
rescue IO::WaitReadable, EOFError
|
|
136
|
+
break
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ALL_OFF = "\e[?1000l\e[?1002l\e[?1003l\e[?1004l\e[?1006l"
|
|
141
|
+
|
|
142
|
+
return unless __FILE__ == $PROGRAM_NAME
|
|
143
|
+
|
|
144
|
+
begin
|
|
145
|
+
$stdin.raw do
|
|
146
|
+
log "=== mouse probe #{Time.now.iso8601 rescue Time.now} ==="
|
|
147
|
+
log "TERM=#{ENV['TERM']} COLORTERM=#{ENV['COLORTERM']} TMUX=#{ENV['TMUX']}"
|
|
148
|
+
log "size=#{IO.console.winsize.reverse.join('x')}"
|
|
149
|
+
log ""
|
|
150
|
+
|
|
151
|
+
# -- Phase 0: DECRQM support ---------------------------------------------
|
|
152
|
+
both "PHASE 0 DECRQM mode-support probe (no mouse needed)"
|
|
153
|
+
[9, 1000, 1002, 1003, 1004, 1005, 1006, 1015, 1016].each do |mode|
|
|
154
|
+
esc "\e[?#{mode}$p"
|
|
155
|
+
got = drain(0.3)
|
|
156
|
+
both " #{mode}: #{got.empty? ? 'NO REPLY' : got.map(&:last).join(', ')}"
|
|
157
|
+
end
|
|
158
|
+
log ""
|
|
159
|
+
|
|
160
|
+
# -- Phase 1: 1000 + X10 -------------------------------------------------
|
|
161
|
+
both ""
|
|
162
|
+
both "PHASE 1 mode 1000, X10 encoding — click a few times (5s)"
|
|
163
|
+
wait_for_enter
|
|
164
|
+
esc "\e[?1000h"
|
|
165
|
+
got = drain(5)
|
|
166
|
+
esc "\e[?1000l"
|
|
167
|
+
both " => #{got.size} events; releases seen: #{got.count { |(_, d)| d.include?('release') }}"
|
|
168
|
+
log ""
|
|
169
|
+
|
|
170
|
+
# -- Phase 2: 1000 + SGR 1006 -------------------------------------------
|
|
171
|
+
both ""
|
|
172
|
+
both "PHASE 2 mode 1000 + SGR 1006 — click a few times, incl. right-click (5s)"
|
|
173
|
+
wait_for_enter
|
|
174
|
+
esc "\e[?1006h\e[?1000h"
|
|
175
|
+
got = drain(5)
|
|
176
|
+
esc "\e[?1000l"
|
|
177
|
+
sgr = got.count { |(k, _)| k == :sgr }
|
|
178
|
+
both " => #{got.size} events, #{sgr} in SGR form (0 means 1006 was ignored)"
|
|
179
|
+
log ""
|
|
180
|
+
|
|
181
|
+
# -- Phase 3: 1002 -------------------------------------------------------
|
|
182
|
+
both ""
|
|
183
|
+
both "PHASE 3 mode 1002 — FIRST move with no button (expect nothing),"
|
|
184
|
+
both " THEN drag with the left button held (expect motion) (8s)"
|
|
185
|
+
wait_for_enter
|
|
186
|
+
esc "\e[?1002h"
|
|
187
|
+
got = drain(8)
|
|
188
|
+
esc "\e[?1002l"
|
|
189
|
+
motion = got.count { |(_, d)| d.include?("MOTION") }
|
|
190
|
+
both " => #{got.size} events, #{motion} motion"
|
|
191
|
+
log ""
|
|
192
|
+
|
|
193
|
+
# -- Phase 4: 1003 rate --------------------------------------------------
|
|
194
|
+
both ""
|
|
195
|
+
both "PHASE 4 mode 1003 — move the mouse around normally, NON-STOP (10s)"
|
|
196
|
+
wait_for_enter
|
|
197
|
+
esc "\e[?1003h"
|
|
198
|
+
t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
199
|
+
got = drain(10)
|
|
200
|
+
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
|
|
201
|
+
esc "\e[?1003l"
|
|
202
|
+
motion = got.count { |(_, d)| d.include?("MOTION") }
|
|
203
|
+
both format(" => %d events (%d motion) in %.1fs = %.1f/s <-- THE NUMBER",
|
|
204
|
+
got.size, motion, elapsed, got.size / elapsed)
|
|
205
|
+
log ""
|
|
206
|
+
|
|
207
|
+
# -- Phase 5: pointer leaves the window ---------------------------------
|
|
208
|
+
both ""
|
|
209
|
+
both "PHASE 5 mode 1003 + 1004 — move the pointer OUT of this window,"
|
|
210
|
+
both " click another app, then come back (10s)"
|
|
211
|
+
wait_for_enter
|
|
212
|
+
esc "\e[?1003h\e[?1004h"
|
|
213
|
+
got = drain(10)
|
|
214
|
+
esc "\e[?1003l\e[?1004l"
|
|
215
|
+
focus = got.select { |(k, _)| k == :focus }
|
|
216
|
+
both " => #{got.size} events; focus events: #{focus.empty? ? 'NONE' : focus.map(&:last).join(', ')}"
|
|
217
|
+
both " (NONE means nothing tells us the pointer left — hover would strand)"
|
|
218
|
+
log ""
|
|
219
|
+
|
|
220
|
+
# -- Phase 6: wide coordinates ------------------------------------------
|
|
221
|
+
cols = IO.console.winsize[1]
|
|
222
|
+
both ""
|
|
223
|
+
if cols > 223
|
|
224
|
+
both "PHASE 6 mode 1003 + 1006 — click PAST column 223 (width=#{cols}) (8s)"
|
|
225
|
+
wait_for_enter
|
|
226
|
+
esc "\e[?1006h\e[?1003h"
|
|
227
|
+
got = drain(8)
|
|
228
|
+
esc "\e[?1003l\e[?1006l"
|
|
229
|
+
both " => #{got.size} events; max x seen: #{got.map { |(_, d)| d[/at (\d+),/, 1].to_i }.max}"
|
|
230
|
+
else
|
|
231
|
+
both "PHASE 6 SKIPPED — terminal is #{cols} cols, need >223 to test the X10 cap"
|
|
232
|
+
log "PHASE 6 skipped: width #{cols}"
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
both ""
|
|
236
|
+
both "done — log written to #{LOG.path}"
|
|
237
|
+
end
|
|
238
|
+
ensure
|
|
239
|
+
esc ALL_OFF
|
|
240
|
+
LOG.close
|
|
241
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Covers probe.rb's byte decoder. Deliberately NOT under spec/ — this is
|
|
5
|
+
# research tooling for ideas/hover.md, not gem code, so it must stay out of
|
|
6
|
+
# `rake spec` and out of CI. Plain assertions, no rspec:
|
|
7
|
+
#
|
|
8
|
+
# ruby ideas/hover/probe_spec.rb
|
|
9
|
+
#
|
|
10
|
+
# Run it before an interactive probe session: a decode slip wastes the whole
|
|
11
|
+
# run, and one already happened (X10 coordinates are byte-33, not byte-32 —
|
|
12
|
+
# the offset differs from the button code's because coordinates are 1-based).
|
|
13
|
+
|
|
14
|
+
require_relative "probe"
|
|
15
|
+
|
|
16
|
+
FAILURES = []
|
|
17
|
+
|
|
18
|
+
def check(input, want)
|
|
19
|
+
events, leftover = parse(input.dup)
|
|
20
|
+
got = events.map { |kind, detail| "#{kind}: #{detail}" }
|
|
21
|
+
if got == want && leftover.empty?
|
|
22
|
+
puts " ok #{input.inspect}"
|
|
23
|
+
else
|
|
24
|
+
FAILURES << input
|
|
25
|
+
puts " FAIL #{input.inspect}\n got #{got.inspect} leftover=#{leftover.inspect}\n want #{want.inspect}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# A fragment must be held for the next read, never half-consumed: real reads
|
|
30
|
+
# do not align to event boundaries (measured: 4 reads in one 10s sample came
|
|
31
|
+
# back at 11.5 bytes/event, i.e. one event split across two reads).
|
|
32
|
+
def check_held(fragment)
|
|
33
|
+
events, leftover = parse(fragment.dup)
|
|
34
|
+
if events.empty? && leftover == fragment
|
|
35
|
+
puts " ok held #{fragment.inspect}"
|
|
36
|
+
else
|
|
37
|
+
FAILURES << fragment
|
|
38
|
+
puts " FAIL #{fragment.inspect} -> #{events.inspect} leftover=#{leftover.inspect}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
puts "X10 encoding (button code is byte-32; coordinates are byte-33):"
|
|
43
|
+
check "\e[M !\"", ["x10: code=0 left at 0,1"]
|
|
44
|
+
check "\e[M#!!", ["x10: code=3 release at 0,0"]
|
|
45
|
+
check "\e[M\"!!", ["x10: code=2 right at 0,0"]
|
|
46
|
+
check "\e[M!!!", ["x10: code=1 middle at 0,0"]
|
|
47
|
+
check "\e[M`!!", ["x10: code=64 wheel_up at 0,0"]
|
|
48
|
+
check "\e[Ma!!", ["x10: code=65 wheel_down at 0,0"]
|
|
49
|
+
check "\e[M0!!", ["x10: code=16 left +ctrl at 0,0"]
|
|
50
|
+
puts "X10 motion codes — the set that arrives under 1002/1003:"
|
|
51
|
+
check "\e[M@!!", ["x10: code=32 MOTION left at 0,0"] # drag; all of 1002's traffic
|
|
52
|
+
check "\e[MC!!", ["x10: code=35 MOTION release at 0,0"] # hover; 1003 only
|
|
53
|
+
puts "X10 burst — two reports back to back must split cleanly (a fixed 6-byte"
|
|
54
|
+
puts "read is safe here, which is why a PTY spec may burst X10 but not SGR):"
|
|
55
|
+
check "\e[M !\"\e[M#!\"", ["x10: code=0 left at 0,1", "x10: code=3 release at 0,1"]
|
|
56
|
+
|
|
57
|
+
puts "SGR 1006 — press/release told apart by the final byte, and the release"
|
|
58
|
+
puts "carries its button (X10's release code 3 is anonymous):"
|
|
59
|
+
check "\e[<0;1;1M", ["sgr: code=0 left [press] at 0,0"]
|
|
60
|
+
check "\e[<0;1;1m", ["sgr: code=0 left [release] at 0,0"]
|
|
61
|
+
check "\e[<2;300;40m", ["sgr: code=2 right [release] at 299,39"]
|
|
62
|
+
check "\e[<35;11;6M", ["sgr: code=35 MOTION release [press] at 10,5"]
|
|
63
|
+
check "\e[<64;5;5M", ["sgr: code=64 wheel_up [press] at 4,4"]
|
|
64
|
+
puts "SGR past the X10 cap — the whole reason to request 1006:"
|
|
65
|
+
check "\e[<0;500;9M", ["sgr: code=0 left [press] at 499,8"]
|
|
66
|
+
|
|
67
|
+
puts "focus tracking (mode 1004) and DECRQM replies:"
|
|
68
|
+
check "\e[I", ["focus: FocusIn"]
|
|
69
|
+
check "\e[O", ["focus: FocusOut"]
|
|
70
|
+
check "\e[?1003;2$y", ["decrpm: mode 1003 => reset (supported)"]
|
|
71
|
+
check "\e[?1005;0$y", ["decrpm: mode 1005 => NOT RECOGNIZED"]
|
|
72
|
+
|
|
73
|
+
puts "partial input is held, not consumed:"
|
|
74
|
+
["\e[M", "\e[M!", "\e[<0;1", "\e[?100"].each { check_held(_1) }
|
|
75
|
+
|
|
76
|
+
puts
|
|
77
|
+
if FAILURES.empty?
|
|
78
|
+
puts "ALL PARSER TESTS PASS"
|
|
79
|
+
else
|
|
80
|
+
puts "#{FAILURES.size} FAILED"
|
|
81
|
+
end
|
|
82
|
+
exit(FAILURES.empty? ? 0 : 1)
|