legion-tty 0.5.0 → 0.5.2
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 +17 -0
- data/lib/legion/tty/app.rb +70 -5
- data/lib/legion/tty/components/input_bar.rb +22 -2
- data/lib/legion/tty/components/status_bar.rb +2 -1
- data/lib/legion/tty/screens/chat/ui_commands.rb +2 -1
- data/lib/legion/tty/screens/chat.rb +21 -8
- data/lib/legion/tty/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7e484905e47a82bedca53b308ff66880c8b64bcb320d9af6a1130fe4ad50a056
|
|
4
|
+
data.tar.gz: f5c49491e1227c5984e99f161662eef56a5d5924903666aa2ea9dcaecccf098f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f2eb92406a0f61f109213eec5858122038e270052cdca8c9c4f3bfcfba7d64d0ead54a1f40b53dd356aa08dc172f920a09f47c46dd108c15a708e2a680ffe474
|
|
7
|
+
data.tar.gz: e107d25bbb2ce75ce886329f8ef02f2fb5991278c5a4d140cc809d940adf34aba9f14a9c4269309be847ae78c78d48ee4d33540d03bfc71712d4b4261ae7a080
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.2] - 2026-04-20
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Bracketed paste support** — `App#run_loop` now enables bracketed paste mode (`\e[?2004h`) on startup and disables (`\e[?2004l`) on shutdown; `read_csi_sequence` detects `\e[200~` / `\e[201~` paste markers and buffers the full pasted text into a `{ paste: text }` event (closes #22)
|
|
7
|
+
- **InputBar paste handling** — `handle_key` recognizes paste Hash events and inserts the full pasted text at the cursor position, replacing newlines with spaces, without triggering submit (closes #22)
|
|
8
|
+
- **Non-symbol key guard** — `dispatch_key` checks `key.is_a?(Symbol)` before the scroll-event include check, preventing `NoMethodError` on Hash paste events (closes #22)
|
|
9
|
+
- **`normalize_key` paste passthrough** — Hash events (paste) pass through without KEY_MAP lookup (closes #22)
|
|
10
|
+
|
|
11
|
+
## [0.5.1] - 2026-04-18
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **Alternate screen buffer** — `App#run_loop` now enables `\e[?1049h` on startup and `\e[?1049l` on shutdown so the TUI no longer pollutes shell scrollback and the prior shell session is restored on exit (closes #20)
|
|
15
|
+
- **Mouse wheel scroll** — enabled SGR mouse tracking (`\e[?1000h` + `\e[?1006h`) and routed wheel-up/wheel-down events to `MessageStream#scroll_up` / `#scroll_down` with 3-line increments (closes #20)
|
|
16
|
+
- **Vim-style scroll bindings** — added `Ctrl+B` (half-page up) and `Ctrl+F` (half-page down) on the Chat screen; wired `Home` / `End` to jump-to-top / jump-to-bottom when the input bar is empty (closes #20)
|
|
17
|
+
- **Scroll hint in status bar** — `scroll_segment` now shows directional arrows (`↑↓ scroll` or `↑ scroll`) alongside the position counter when content overflows the viewport (closes #20)
|
|
18
|
+
- **Help text updated** — added scroll binding reference line to the help overlay
|
|
19
|
+
|
|
3
20
|
## [0.5.0] - 2026-04-17
|
|
4
21
|
|
|
5
22
|
### Added
|
data/lib/legion/tty/app.rb
CHANGED
|
@@ -30,11 +30,20 @@ module Legion
|
|
|
30
30
|
"\e[1~" => :home, "\e[4~" => :end,
|
|
31
31
|
"\x7f" => :backspace, "\b" => :backspace, "\t" => :tab,
|
|
32
32
|
"\x03" => :ctrl_c, "\x04" => :ctrl_d,
|
|
33
|
-
"\x01" => :ctrl_a, "\
|
|
33
|
+
"\x01" => :ctrl_a, "\x02" => :ctrl_b,
|
|
34
|
+
"\x05" => :ctrl_e, "\x06" => :ctrl_f,
|
|
34
35
|
"\x0B" => :ctrl_k, "\x0C" => :ctrl_l, "\x13" => :ctrl_s,
|
|
35
36
|
"\x15" => :ctrl_u
|
|
36
37
|
}.freeze
|
|
37
38
|
|
|
39
|
+
ENABLE_ALT_SCREEN = "\e[?1049h"
|
|
40
|
+
DISABLE_ALT_SCREEN = "\e[?1049l"
|
|
41
|
+
ENABLE_MOUSE = "\e[?1000h\e[?1006h"
|
|
42
|
+
DISABLE_MOUSE = "\e[?1000h\e[?1006l"
|
|
43
|
+
ENABLE_BRACKETED_PASTE = "\e[?2004h"
|
|
44
|
+
DISABLE_BRACKETED_PASTE = "\e[?2004l"
|
|
45
|
+
SGR_MOUSE_RE = /\A\e\[<(\d+);(\d+);(\d+)([Mm])\z/
|
|
46
|
+
|
|
38
47
|
attr_reader :config, :credentials, :screen_manager, :hotkeys, :llm_chat, :input_bar
|
|
39
48
|
|
|
40
49
|
def self.run(argv = [])
|
|
@@ -151,12 +160,15 @@ module Legion
|
|
|
151
160
|
|
|
152
161
|
# --- Event Loop ---
|
|
153
162
|
|
|
154
|
-
# rubocop:disable Metrics/AbcSize
|
|
163
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
155
164
|
def run_loop
|
|
156
165
|
require 'io/console'
|
|
157
166
|
|
|
158
167
|
@running = true
|
|
159
168
|
@raw_mode = true
|
|
169
|
+
$stdout.print ENABLE_ALT_SCREEN
|
|
170
|
+
$stdout.print ENABLE_MOUSE
|
|
171
|
+
$stdout.print ENABLE_BRACKETED_PASTE
|
|
160
172
|
$stdout.print cursor.hide
|
|
161
173
|
$stdout.print cursor.clear_screen
|
|
162
174
|
|
|
@@ -169,30 +181,47 @@ module Legion
|
|
|
169
181
|
|
|
170
182
|
key = normalize_key(raw_key)
|
|
171
183
|
dispatch_key(key)
|
|
184
|
+
drain_burst_keys(raw_in)
|
|
172
185
|
end
|
|
173
186
|
end
|
|
174
187
|
rescue Interrupt
|
|
175
188
|
nil
|
|
176
189
|
ensure
|
|
177
190
|
@raw_mode = false
|
|
191
|
+
$stdout.print DISABLE_BRACKETED_PASTE
|
|
192
|
+
$stdout.print DISABLE_MOUSE
|
|
178
193
|
$stdout.print cursor.show
|
|
179
|
-
$stdout.print
|
|
180
|
-
$stdout.puts
|
|
194
|
+
$stdout.print DISABLE_ALT_SCREEN
|
|
181
195
|
shutdown
|
|
182
196
|
end
|
|
183
|
-
# rubocop:enable Metrics/AbcSize
|
|
197
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
184
198
|
|
|
185
199
|
def needs_refresh?
|
|
186
200
|
active = @screen_manager.active_screen
|
|
187
201
|
active.respond_to?(:streaming?) && active.streaming?
|
|
188
202
|
end
|
|
189
203
|
|
|
204
|
+
def drain_burst_keys(raw_in)
|
|
205
|
+
while @running && raw_in.wait_readable(0)
|
|
206
|
+
burst_key = read_raw_key(raw_in, timeout: 0)
|
|
207
|
+
break unless burst_key
|
|
208
|
+
|
|
209
|
+
dispatch_key(normalize_key(burst_key))
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
190
213
|
def normalize_key(raw)
|
|
214
|
+
return raw if raw.is_a?(Hash)
|
|
215
|
+
|
|
216
|
+
mouse = parse_sgr_mouse(raw)
|
|
217
|
+
return mouse if mouse
|
|
218
|
+
|
|
191
219
|
KEY_MAP[raw] || raw
|
|
192
220
|
end
|
|
193
221
|
|
|
194
222
|
# --- Key Dispatch ---
|
|
195
223
|
|
|
224
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
196
225
|
def dispatch_key(key)
|
|
197
226
|
if key == :ctrl_c
|
|
198
227
|
@running = false
|
|
@@ -213,12 +242,18 @@ module Legion
|
|
|
213
242
|
active = @screen_manager.active_screen
|
|
214
243
|
return unless active
|
|
215
244
|
|
|
245
|
+
if key.is_a?(Symbol) && %i[scroll_up scroll_down].include?(key)
|
|
246
|
+
dispatch_to_screen(active, key)
|
|
247
|
+
return
|
|
248
|
+
end
|
|
249
|
+
|
|
216
250
|
if active.respond_to?(:needs_input_bar?) && active.needs_input_bar? && @input_bar
|
|
217
251
|
dispatch_to_input_screen(active, key)
|
|
218
252
|
else
|
|
219
253
|
dispatch_to_screen(active, key)
|
|
220
254
|
end
|
|
221
255
|
end
|
|
256
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
222
257
|
|
|
223
258
|
def dispatch_to_input_screen(screen, key)
|
|
224
259
|
result = @input_bar.handle_key(key)
|
|
@@ -294,9 +329,39 @@ module Legion
|
|
|
294
329
|
seq << c
|
|
295
330
|
break if c.ord.between?(0x40, 0x7E)
|
|
296
331
|
end
|
|
332
|
+
|
|
333
|
+
return read_paste_content(io) if seq == "\e[200~"
|
|
334
|
+
|
|
297
335
|
seq
|
|
298
336
|
end
|
|
299
337
|
|
|
338
|
+
def read_paste_content(io)
|
|
339
|
+
buf = +''
|
|
340
|
+
end_marker = "\e[201~"
|
|
341
|
+
loop do
|
|
342
|
+
break unless io.wait_readable(1)
|
|
343
|
+
|
|
344
|
+
c = io.getc
|
|
345
|
+
break unless c
|
|
346
|
+
|
|
347
|
+
buf << c
|
|
348
|
+
break if buf.end_with?(end_marker)
|
|
349
|
+
end
|
|
350
|
+
buf.delete_suffix!(end_marker)
|
|
351
|
+
{ paste: buf }
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def parse_sgr_mouse(raw)
|
|
355
|
+
match = SGR_MOUSE_RE.match(raw)
|
|
356
|
+
return nil unless match
|
|
357
|
+
|
|
358
|
+
button = match[1].to_i
|
|
359
|
+
return :scroll_up if button == 64
|
|
360
|
+
return :scroll_down if button == 65
|
|
361
|
+
|
|
362
|
+
nil
|
|
363
|
+
end
|
|
364
|
+
|
|
300
365
|
# --- Rendering ---
|
|
301
366
|
|
|
302
367
|
# rubocop:disable Metrics/AbcSize
|
|
@@ -32,6 +32,8 @@ module Legion
|
|
|
32
32
|
|
|
33
33
|
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
34
34
|
def handle_key(key)
|
|
35
|
+
return handle_paste(key[:paste]) if key.is_a?(Hash) && key.key?(:paste)
|
|
36
|
+
|
|
35
37
|
case key
|
|
36
38
|
when :enter
|
|
37
39
|
submit_line
|
|
@@ -49,10 +51,20 @@ module Legion
|
|
|
49
51
|
when :left
|
|
50
52
|
@cursor_pos = [@cursor_pos - 1, 0].max
|
|
51
53
|
:handled
|
|
52
|
-
when :home
|
|
54
|
+
when :home
|
|
55
|
+
return :pass if @buffer.empty?
|
|
56
|
+
|
|
57
|
+
@cursor_pos = 0
|
|
58
|
+
:handled
|
|
59
|
+
when :ctrl_a
|
|
53
60
|
@cursor_pos = 0
|
|
54
61
|
:handled
|
|
55
|
-
when :end
|
|
62
|
+
when :end
|
|
63
|
+
return :pass if @buffer.empty?
|
|
64
|
+
|
|
65
|
+
@cursor_pos = @buffer.length
|
|
66
|
+
:handled
|
|
67
|
+
when :ctrl_e
|
|
56
68
|
@cursor_pos = @buffer.length
|
|
57
69
|
:handled
|
|
58
70
|
when :ctrl_u
|
|
@@ -192,6 +204,14 @@ module Legion
|
|
|
192
204
|
:handled
|
|
193
205
|
end
|
|
194
206
|
|
|
207
|
+
def handle_paste(text)
|
|
208
|
+
sanitized = text.to_s.gsub(/\r\n?/, "\n").gsub("\n", ' ')
|
|
209
|
+
@buffer.insert(@cursor_pos, sanitized)
|
|
210
|
+
@cursor_pos += sanitized.length
|
|
211
|
+
@tab_matches = []
|
|
212
|
+
:handled
|
|
213
|
+
end
|
|
214
|
+
|
|
195
215
|
def insert_char(key)
|
|
196
216
|
return :pass unless key.is_a?(String) && key.length == 1 && key.ord >= 32
|
|
197
217
|
|
|
@@ -122,7 +122,8 @@ module Legion
|
|
|
122
122
|
scroll = @state[:scroll]
|
|
123
123
|
return nil unless scroll.is_a?(Hash) && scroll[:total].to_i > scroll[:visible].to_i
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
hint = scroll[:current].to_i.positive? ? "\u2191\u2193 scroll" : "\u2191 scroll"
|
|
126
|
+
Theme.c(:muted, "#{scroll[:current]}/#{scroll[:total]} #{hint}")
|
|
126
127
|
end
|
|
127
128
|
|
|
128
129
|
def level_to_priority(level)
|
|
@@ -36,7 +36,8 @@ module Legion
|
|
|
36
36
|
'TOOLS : /tools /export /bookmark /pin /pins /alias /snippet /history',
|
|
37
37
|
'UTILS : /calc /rand',
|
|
38
38
|
'',
|
|
39
|
-
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back'
|
|
39
|
+
'Hotkeys: Ctrl+D=dashboard Ctrl+K=palette Ctrl+S=sessions Esc=back',
|
|
40
|
+
'Scroll : Mouse wheel PgUp/PgDn Ctrl+B/F (half-page) Home/End (top/bottom)'
|
|
40
41
|
].freeze
|
|
41
42
|
|
|
42
43
|
CALC_SAFE_PATTERN = %r{\A[\d\s+\-*/.()%]*\z}
|
|
@@ -200,20 +200,33 @@ module Legion
|
|
|
200
200
|
end
|
|
201
201
|
|
|
202
202
|
def handle_input(key)
|
|
203
|
+
scroll = handle_scroll_key(key)
|
|
204
|
+
return scroll if scroll
|
|
205
|
+
|
|
206
|
+
:pass
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def handle_scroll_key(key)
|
|
203
210
|
case key
|
|
204
|
-
when :page_up
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
when :
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
211
|
+
when :page_up then @message_stream.scroll_up(10)
|
|
212
|
+
when :page_down then @message_stream.scroll_down(10)
|
|
213
|
+
when :scroll_up then @message_stream.scroll_up(3)
|
|
214
|
+
when :scroll_down then @message_stream.scroll_down(3)
|
|
215
|
+
when :ctrl_b then @message_stream.scroll_up(half_page_lines)
|
|
216
|
+
when :ctrl_f then @message_stream.scroll_down(half_page_lines)
|
|
217
|
+
when :home then @message_stream.scroll_up(@message_stream.messages.size * 5)
|
|
218
|
+
when :end then @message_stream.scroll_down(@message_stream.scroll_offset)
|
|
219
|
+
else return nil
|
|
212
220
|
end
|
|
221
|
+
:handled
|
|
213
222
|
end
|
|
214
223
|
|
|
215
224
|
private
|
|
216
225
|
|
|
226
|
+
def half_page_lines
|
|
227
|
+
[(terminal_height - 3) / 2, 1].max
|
|
228
|
+
end
|
|
229
|
+
|
|
217
230
|
def render_focus(width, height)
|
|
218
231
|
stream_lines = @message_stream.render(width: width, height: [height, 1].max)
|
|
219
232
|
@status_bar.update(scroll: @message_stream.scroll_position)
|
data/lib/legion/tty/version.rb
CHANGED