rcurses 4.9.4 → 5.0.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/README.md +237 -565
- data/examples/basic_panes.rb +43 -0
- data/examples/focus_panes.rb +42 -0
- data/lib/rcurses/cursor.rb +48 -0
- data/lib/rcurses/general.rb +99 -0
- data/lib/rcurses/input.rb +127 -0
- data/lib/rcurses/pane.rb +715 -0
- data/lib/rcurses.rb +61 -0
- data/lib/string_extensions.rb +160 -0
- metadata +13 -4
data/lib/rcurses/pane.rb
ADDED
@@ -0,0 +1,715 @@
|
|
1
|
+
module Rcurses
|
2
|
+
class Pane
|
3
|
+
require 'clipboard' # Ensure the 'clipboard' gem is installed
|
4
|
+
include Cursor
|
5
|
+
include Input
|
6
|
+
attr_accessor :x, :y, :w, :h, :fg, :bg
|
7
|
+
attr_accessor :border, :scroll, :text, :ix, :index, :align, :prompt
|
8
|
+
attr_accessor :moreup, :moredown
|
9
|
+
attr_accessor :record, :history
|
10
|
+
|
11
|
+
def initialize(x = 1, y = 1, w = 1, h = 1, fg = nil, bg = nil)
|
12
|
+
@max_h, @max_w = IO.console.winsize
|
13
|
+
@x = x
|
14
|
+
@y = y
|
15
|
+
@w = w
|
16
|
+
@h = h
|
17
|
+
@fg, @bg = fg, bg
|
18
|
+
@text = "" # Initialize text variable
|
19
|
+
@align = "l" # Default alignment
|
20
|
+
@scroll = true # Enable scroll indicators
|
21
|
+
@prompt = "" # Prompt for editline
|
22
|
+
@ix = 0 # Starting text line index
|
23
|
+
@prev_frame = nil # Holds the previously rendered frame (array of lines)
|
24
|
+
@line = 0 # For cursor tracking during editing:
|
25
|
+
@pos = 0 # For cursor tracking during editing:
|
26
|
+
@record = false # Don't record history unless explicitly set to true
|
27
|
+
@history = [] # History array
|
28
|
+
@max_history_size = 100 # Limit history to prevent memory leaks
|
29
|
+
|
30
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer_proc)
|
31
|
+
end
|
32
|
+
|
33
|
+
def text=(new_text)
|
34
|
+
if @record && @text
|
35
|
+
@history << @text
|
36
|
+
@history.shift while @history.size > @max_history_size
|
37
|
+
end
|
38
|
+
@text = new_text
|
39
|
+
end
|
40
|
+
|
41
|
+
def ask(prompt, text)
|
42
|
+
@prompt = prompt
|
43
|
+
@text = text
|
44
|
+
editline
|
45
|
+
if @record && !@text.empty?
|
46
|
+
@history << @text
|
47
|
+
@history.shift while @history.size > @max_history_size
|
48
|
+
end
|
49
|
+
@text
|
50
|
+
end
|
51
|
+
|
52
|
+
def say(text)
|
53
|
+
if @record && !text.empty?
|
54
|
+
@history << text
|
55
|
+
@history.shift while @history.size > @max_history_size
|
56
|
+
end
|
57
|
+
@text = text
|
58
|
+
@ix = 0
|
59
|
+
refresh
|
60
|
+
end
|
61
|
+
|
62
|
+
def clear
|
63
|
+
@text = ""
|
64
|
+
@ix = 0
|
65
|
+
full_refresh
|
66
|
+
end
|
67
|
+
|
68
|
+
def cleanup
|
69
|
+
@prev_frame = nil
|
70
|
+
@lazy_txt = nil
|
71
|
+
@raw_txt = nil
|
72
|
+
@cached_text = nil
|
73
|
+
@txt = nil
|
74
|
+
@history.clear if @history
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.finalizer_proc
|
78
|
+
proc do
|
79
|
+
# Cleanup code that doesn't reference instance variables
|
80
|
+
# since the object is already being finalized
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def move(dx, dy)
|
85
|
+
@x += dx
|
86
|
+
@y += dy
|
87
|
+
refresh
|
88
|
+
end
|
89
|
+
|
90
|
+
def linedown
|
91
|
+
@ix += 1
|
92
|
+
@ix = @text.split("\n").length if @ix > @text.split("\n").length - 1
|
93
|
+
refresh
|
94
|
+
end
|
95
|
+
|
96
|
+
def lineup
|
97
|
+
@ix -= 1
|
98
|
+
@ix = 0 if @ix < 0
|
99
|
+
refresh
|
100
|
+
end
|
101
|
+
|
102
|
+
def pagedown
|
103
|
+
@ix = @ix + @h - 1
|
104
|
+
@ix = @text.split("\n").length - @h if @ix > @text.split("\n").length - @h
|
105
|
+
refresh
|
106
|
+
end
|
107
|
+
|
108
|
+
def pageup
|
109
|
+
@ix = @ix - @h + 1
|
110
|
+
@ix = 0 if @ix < 0
|
111
|
+
refresh
|
112
|
+
end
|
113
|
+
|
114
|
+
def bottom
|
115
|
+
@ix = @text.split("\n").length - @h
|
116
|
+
refresh
|
117
|
+
end
|
118
|
+
|
119
|
+
def top
|
120
|
+
@ix = 0
|
121
|
+
refresh
|
122
|
+
end
|
123
|
+
|
124
|
+
# full_refresh forces a complete repaint.
|
125
|
+
def full_refresh(cont = @text)
|
126
|
+
@prev_frame = nil
|
127
|
+
refresh(cont)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Refresh only the border
|
131
|
+
def border_refresh
|
132
|
+
left_col = @x - 1
|
133
|
+
right_col = @x + @w
|
134
|
+
top_row = @y - 1
|
135
|
+
bottom_row = @y + @h
|
136
|
+
|
137
|
+
if @border
|
138
|
+
fmt = [@fg.to_s, @bg.to_s].join(',')
|
139
|
+
top = ("┌" + "─" * @w + "┐").c(fmt)
|
140
|
+
STDOUT.print "\e[#{top_row};#{left_col}H" + top
|
141
|
+
(0...@h).each do |i|
|
142
|
+
row = @y + i
|
143
|
+
STDOUT.print "\e[#{row};#{left_col}H" + "│".c(fmt)
|
144
|
+
STDOUT.print "\e[#{row};#{right_col}H" + "│".c(fmt)
|
145
|
+
end
|
146
|
+
bottom = ("└" + "─" * @w + "┘").c(fmt)
|
147
|
+
STDOUT.print "\e[#{bottom_row};#{left_col}H" + bottom
|
148
|
+
else
|
149
|
+
STDOUT.print "\e[#{top_row};#{left_col}H" + " " * (@w + 2)
|
150
|
+
(0...@h).each do |i|
|
151
|
+
row = @y + i
|
152
|
+
STDOUT.print "\e[#{row};#{left_col}H" + " "
|
153
|
+
STDOUT.print "\e[#{row};#{right_col}H" + " "
|
154
|
+
end
|
155
|
+
STDOUT.print "\e[#{bottom_row};#{left_col}H" + " " * (@w + 2)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Diff-based refresh that minimizes flicker.
|
160
|
+
# In this updated version we lazily process only the raw lines required to fill the pane.
|
161
|
+
def refresh(cont = @text)
|
162
|
+
begin
|
163
|
+
@max_h, @max_w = IO.console.winsize
|
164
|
+
rescue => e
|
165
|
+
# Fallback to reasonable defaults if terminal size can't be determined
|
166
|
+
@max_h, @max_w = 24, 80
|
167
|
+
end
|
168
|
+
|
169
|
+
# Ensure minimum viable dimensions
|
170
|
+
@max_h = [[@max_h, 3].max, 1000].min # Between 3 and 1000 rows
|
171
|
+
@max_w = [[@max_w, 10].max, 1000].min # Between 10 and 1000 columns
|
172
|
+
|
173
|
+
# Ensure pane dimensions are reasonable
|
174
|
+
@w = [[@w, 1].max, @max_w].min
|
175
|
+
@h = [[@h, 1].max, @max_h].min
|
176
|
+
|
177
|
+
if @border
|
178
|
+
@w = @max_w - 2 if @w > @max_w - 2
|
179
|
+
@h = @max_h - 2 if @h > @max_h - 2
|
180
|
+
@x = [[2, @x].max, @max_w - @w].min
|
181
|
+
@y = [[2, @y].max, @max_h - @h].min
|
182
|
+
else
|
183
|
+
@w = @max_w if @w > @max_w
|
184
|
+
@h = @max_h if @h > @max_h
|
185
|
+
@x = [[1, @x].max, @max_w - @w + 1].min
|
186
|
+
@y = [[1, @y].max, @max_h - @h + 1].min
|
187
|
+
end
|
188
|
+
|
189
|
+
begin
|
190
|
+
o_row, o_col = pos
|
191
|
+
rescue => e
|
192
|
+
# Fallback cursor position
|
193
|
+
o_row, o_col = 1, 1
|
194
|
+
end
|
195
|
+
|
196
|
+
# Hide cursor, disable auto-wrap, reset all SGR and scroll margins
|
197
|
+
# (so stray underline, scroll regions, etc. can’t leak out)
|
198
|
+
STDOUT.print "\e[?25l\e[?7l\e[0m\e[r"
|
199
|
+
|
200
|
+
fmt = [@fg.to_s, @bg.to_s].join(',')
|
201
|
+
|
202
|
+
# Lazy evaluation: If the content or pane width has changed, reinitialize the lazy cache.
|
203
|
+
if !defined?(@cached_text) || @cached_text != cont || @cached_w != @w
|
204
|
+
begin
|
205
|
+
@raw_txt = (cont || "").split("\n").map { |line| line.chomp("\r") }
|
206
|
+
@lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
|
207
|
+
@lazy_index = 0 # Pointer to the next raw line to process.
|
208
|
+
@cached_text = (cont || "").dup
|
209
|
+
@cached_w = @w
|
210
|
+
rescue => e
|
211
|
+
# Fallback if content processing fails
|
212
|
+
@raw_txt = [""]
|
213
|
+
@lazy_txt = []
|
214
|
+
@lazy_index = 0
|
215
|
+
@cached_text = ""
|
216
|
+
@cached_w = @w
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
content_rows = @h
|
221
|
+
# Ensure we have processed enough lines for the current scroll position + visible area.
|
222
|
+
required_lines = @ix + content_rows + 50 # Buffer a bit for smoother scrolling
|
223
|
+
max_cache_size = 1000 # Prevent excessive memory usage
|
224
|
+
|
225
|
+
while @lazy_txt.size < required_lines && @lazy_index < @raw_txt.size && @lazy_txt.size < max_cache_size
|
226
|
+
raw_line = @raw_txt[@lazy_index]
|
227
|
+
# If the raw line is short, no wrapping is needed.
|
228
|
+
if raw_line.respond_to?(:pure) && Rcurses.display_width(raw_line.pure) < @w
|
229
|
+
processed = [raw_line]
|
230
|
+
else
|
231
|
+
processed = split_line_with_ansi(raw_line, @w)
|
232
|
+
end
|
233
|
+
@lazy_txt.concat(processed)
|
234
|
+
@lazy_index += 1
|
235
|
+
end
|
236
|
+
|
237
|
+
# Simplified: just limit max processing, don't trim existing cache
|
238
|
+
# This avoids expensive array operations during scrolling
|
239
|
+
|
240
|
+
@txt = @lazy_txt
|
241
|
+
|
242
|
+
@ix = @txt.length - 1 if @ix > @txt.length - 1
|
243
|
+
@ix = 0 if @ix < 0
|
244
|
+
|
245
|
+
new_frame = []
|
246
|
+
|
247
|
+
content_rows.times do |i|
|
248
|
+
line_str = ""
|
249
|
+
l = @ix + i
|
250
|
+
if @txt[l].to_s != ""
|
251
|
+
pl = @w - Rcurses.display_width(@txt[l].pure)
|
252
|
+
pl = 0 if pl < 0
|
253
|
+
hl = pl / 2
|
254
|
+
case @align
|
255
|
+
when "l"
|
256
|
+
line_str = @txt[l].c(fmt) + " ".c(fmt) * pl
|
257
|
+
when "r"
|
258
|
+
line_str = " ".c(fmt) * pl + @txt[l].c(fmt)
|
259
|
+
when "c"
|
260
|
+
line_str = " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
|
261
|
+
end
|
262
|
+
else
|
263
|
+
line_str = " ".c(fmt) * @w
|
264
|
+
end
|
265
|
+
|
266
|
+
new_frame << line_str
|
267
|
+
end
|
268
|
+
|
269
|
+
diff_buf = ""
|
270
|
+
new_frame.each_with_index do |line, i|
|
271
|
+
row_num = @y + i
|
272
|
+
col_num = @x
|
273
|
+
if @prev_frame.nil? || @prev_frame[i] != line
|
274
|
+
diff_buf << "\e[#{row_num};#{col_num}H" << line
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# restore wrap, then also reset SGR and scroll-region one more time
|
279
|
+
diff_buf << "\e[#{o_row};#{o_col}H\e[?7h\e[0m\e[r"
|
280
|
+
begin
|
281
|
+
print diff_buf
|
282
|
+
rescue => e
|
283
|
+
# If printing fails, at least try to restore terminal state
|
284
|
+
begin
|
285
|
+
print "\e[0m\e[?25h\e[?7h"
|
286
|
+
rescue
|
287
|
+
end
|
288
|
+
end
|
289
|
+
@prev_frame = new_frame
|
290
|
+
|
291
|
+
# Draw scroll markers after printing the frame.
|
292
|
+
if @scroll
|
293
|
+
marker_col = @x + @w - 1
|
294
|
+
if @ix > 0
|
295
|
+
print "\e[#{@y};#{marker_col}H" + "∆".c(fmt)
|
296
|
+
end
|
297
|
+
# If there are more processed lines than fit in the pane
|
298
|
+
# OR there remain raw lines to process, show the down marker.
|
299
|
+
if (@txt.length - @ix) > @h || (@lazy_index < @raw_txt.size)
|
300
|
+
print "\e[#{@y + @h - 1};#{marker_col}H" + "∇".c(fmt)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
if @border
|
305
|
+
# top
|
306
|
+
print "\e[#{@y - 1};#{@x - 1}H" + ("┌" + "─" * @w + "┐").c(fmt)
|
307
|
+
# sides
|
308
|
+
(0...@h).each do |i|
|
309
|
+
print "\e[#{@y + i};#{@x - 1}H" + "│".c(fmt)
|
310
|
+
print "\e[#{@y + i};#{@x + @w}H" + "│".c(fmt)
|
311
|
+
end
|
312
|
+
# bottom
|
313
|
+
print "\e[#{@y + @h};#{@x - 1}H" + ("└" + "─" * @w + "┘").c(fmt)
|
314
|
+
end
|
315
|
+
|
316
|
+
new_frame.join("\n")
|
317
|
+
end
|
318
|
+
|
319
|
+
def textformat(cont)
|
320
|
+
# This method is no longer used in refresh since we process lazily,
|
321
|
+
# but is kept here if needed elsewhere.
|
322
|
+
lines = cont.split("\n")
|
323
|
+
result = []
|
324
|
+
lines.each do |line|
|
325
|
+
split_lines = split_line_with_ansi(line, @w)
|
326
|
+
result.concat(split_lines)
|
327
|
+
end
|
328
|
+
result
|
329
|
+
end
|
330
|
+
|
331
|
+
def right
|
332
|
+
if @pos < @txt[@ix + @line].length
|
333
|
+
@pos += 1
|
334
|
+
if @pos == @w
|
335
|
+
@pos = 0
|
336
|
+
if @line == @h - 1
|
337
|
+
@ix += 1
|
338
|
+
else
|
339
|
+
@line += 1
|
340
|
+
end
|
341
|
+
end
|
342
|
+
else
|
343
|
+
if @line == @h - 1
|
344
|
+
@ix += 1 unless @ix >= @txt.length - @h
|
345
|
+
@pos = 0
|
346
|
+
elsif @line + @ix + 1 < @txt.length
|
347
|
+
@line += 1
|
348
|
+
@pos = 0
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
def left
|
354
|
+
if @pos == 0
|
355
|
+
if @line == 0
|
356
|
+
unless @ix == 0
|
357
|
+
@ix -= 1
|
358
|
+
@pos = @txt[@ix + @line].length
|
359
|
+
end
|
360
|
+
else
|
361
|
+
@line -= 1
|
362
|
+
@pos = @txt[@ix + @line].length
|
363
|
+
end
|
364
|
+
else
|
365
|
+
@pos -= 1
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def up
|
370
|
+
if @line == 0
|
371
|
+
@ix -= 1 unless @ix == 0
|
372
|
+
else
|
373
|
+
@line -= 1
|
374
|
+
end
|
375
|
+
begin
|
376
|
+
@pos = [@pos, @txt[@ix + @line].length].min
|
377
|
+
rescue
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
def down
|
382
|
+
if @line == @h - 1
|
383
|
+
@ix += 1 unless @ix + @line >= @txt.length - 1
|
384
|
+
elsif @line + @ix + 1 < @txt.length
|
385
|
+
@line += 1
|
386
|
+
end
|
387
|
+
begin
|
388
|
+
@pos = [@pos, @txt[@ix + @line].length].min
|
389
|
+
rescue
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
def parse(cont)
|
394
|
+
cont.gsub!(/\*(.+?)\*/, '\1'.b)
|
395
|
+
cont.gsub!(/\/(.+?)\//, '\1'.i)
|
396
|
+
cont.gsub!(/_(.+?)_/, '\1'.u)
|
397
|
+
cont.gsub!(/#(.+?)#/, '\1'.r)
|
398
|
+
cont.gsub!(/<([^|]+)\|([^>]+)>/) do
|
399
|
+
text = $2; codes = $1
|
400
|
+
text.c(codes)
|
401
|
+
end
|
402
|
+
cont
|
403
|
+
end
|
404
|
+
|
405
|
+
def edit
|
406
|
+
begin
|
407
|
+
STDIN.cooked! rescue nil
|
408
|
+
STDIN.echo = true rescue nil
|
409
|
+
# Prepare content with visible newline markers
|
410
|
+
content = @text.pure.gsub("\n", "¬\n")
|
411
|
+
# Reset editing cursor state
|
412
|
+
@ix = 0
|
413
|
+
@line = 0
|
414
|
+
@pos = 0
|
415
|
+
# Initial render sets @txt internally for display and cursor math
|
416
|
+
refresh(content)
|
417
|
+
Rcurses::Cursor.show
|
418
|
+
input_char = ''
|
419
|
+
|
420
|
+
while input_char != 'ESC'
|
421
|
+
# Move the terminal cursor to the logical text cursor
|
422
|
+
row(@y + @line)
|
423
|
+
col(@x + @pos)
|
424
|
+
input_char = getchr(flush: false)
|
425
|
+
case input_char
|
426
|
+
when 'C-L'
|
427
|
+
@align = 'l'
|
428
|
+
when 'C-R'
|
429
|
+
@align = 'r'
|
430
|
+
when 'C-C'
|
431
|
+
@align = 'c'
|
432
|
+
when 'C-Y'
|
433
|
+
Clipboard.copy(@text.pure)
|
434
|
+
when 'C-S'
|
435
|
+
content = content.gsub('¬', "\n")
|
436
|
+
content = parse(content)
|
437
|
+
@text = content
|
438
|
+
input_char = 'ESC'
|
439
|
+
when 'DEL'
|
440
|
+
posx = calculate_posx
|
441
|
+
content.slice!(posx)
|
442
|
+
when 'BACK'
|
443
|
+
if @pos > 0
|
444
|
+
left
|
445
|
+
posx = calculate_posx
|
446
|
+
content.slice!(posx)
|
447
|
+
end
|
448
|
+
when 'WBACK'
|
449
|
+
while @pos > 0 && content[calculate_posx - 1] != ' '
|
450
|
+
left
|
451
|
+
posx = calculate_posx
|
452
|
+
content.slice!(posx)
|
453
|
+
end
|
454
|
+
when 'C-K'
|
455
|
+
line_start_pos = calculate_line_start_pos
|
456
|
+
line_length = @txt[@ix + @line]&.length || 0
|
457
|
+
content.slice!(line_start_pos + @pos, line_length - @pos)
|
458
|
+
when 'UP'
|
459
|
+
up
|
460
|
+
when 'DOWN'
|
461
|
+
down
|
462
|
+
when 'RIGHT'
|
463
|
+
right
|
464
|
+
when 'LEFT'
|
465
|
+
left
|
466
|
+
when 'HOME'
|
467
|
+
@pos = 0
|
468
|
+
when 'END'
|
469
|
+
current_line_length = @txt[@ix + @line]&.length || 0
|
470
|
+
@pos = current_line_length
|
471
|
+
when 'C-HOME'
|
472
|
+
@ix = 0; @line = 0; @pos = 0
|
473
|
+
when 'C-END'
|
474
|
+
total_lines = @txt.length
|
475
|
+
@ix = [total_lines - @h, 0].max
|
476
|
+
@line = [@h - 1, total_lines - @ix - 1].min
|
477
|
+
current_line_length = @txt[@ix + @line]&.length || 0
|
478
|
+
@pos = current_line_length
|
479
|
+
when 'ENTER'
|
480
|
+
posx = calculate_posx
|
481
|
+
content.insert(posx, "¬\n")
|
482
|
+
right
|
483
|
+
when /^.$/
|
484
|
+
posx = calculate_posx
|
485
|
+
content.insert(posx, input_char)
|
486
|
+
right
|
487
|
+
end
|
488
|
+
|
489
|
+
# Handle any buffered input
|
490
|
+
while IO.select([$stdin], nil, nil, 0)
|
491
|
+
input_char = $stdin.read_nonblock(1) rescue nil
|
492
|
+
break unless input_char
|
493
|
+
posx = calculate_posx
|
494
|
+
content.insert(posx, input_char)
|
495
|
+
right
|
496
|
+
end
|
497
|
+
|
498
|
+
# Re-render without overwriting the internal @txt
|
499
|
+
refresh(content)
|
500
|
+
Rcurses::Cursor.show
|
501
|
+
end
|
502
|
+
ensure
|
503
|
+
STDIN.raw! rescue nil
|
504
|
+
STDIN.echo = false rescue nil
|
505
|
+
while IO.select([$stdin], nil, nil, 0)
|
506
|
+
$stdin.read_nonblock(4096) rescue break
|
507
|
+
end
|
508
|
+
end
|
509
|
+
Rcurses::Cursor.hide
|
510
|
+
end
|
511
|
+
|
512
|
+
def editline
|
513
|
+
begin
|
514
|
+
STDIN.cooked! rescue nil
|
515
|
+
STDIN.echo = true rescue nil
|
516
|
+
Rcurses::Cursor.show
|
517
|
+
@x = [[@x, 1].max, @max_w - @w + 1].min
|
518
|
+
@y = [[@y, 1].max, @max_h - @h + 1].min
|
519
|
+
@scroll = false
|
520
|
+
@ix = 0
|
521
|
+
row(@y)
|
522
|
+
fmt = [@fg.to_s, @bg.to_s].join(',')
|
523
|
+
col(@x)
|
524
|
+
print @prompt.c(fmt)
|
525
|
+
prompt_len = @prompt.pure.length
|
526
|
+
content_len = @w - prompt_len
|
527
|
+
cont = @text.pure.slice(0, content_len)
|
528
|
+
@pos = cont.length
|
529
|
+
chr = ''
|
530
|
+
history_index = @history.size
|
531
|
+
|
532
|
+
while chr != 'ESC'
|
533
|
+
col(@x + prompt_len)
|
534
|
+
cont = cont.slice(0, content_len)
|
535
|
+
print cont.ljust(content_len).c(fmt)
|
536
|
+
col(@x + prompt_len + @pos)
|
537
|
+
chr = getchr(flush: false)
|
538
|
+
case chr
|
539
|
+
when 'LEFT'
|
540
|
+
@pos -= 1 if @pos > 0
|
541
|
+
when 'RIGHT'
|
542
|
+
@pos += 1 if @pos < cont.length
|
543
|
+
when 'HOME'
|
544
|
+
@pos = 0
|
545
|
+
when 'END'
|
546
|
+
@pos = cont.length
|
547
|
+
when 'DEL'
|
548
|
+
cont[@pos] = '' if @pos < cont.length
|
549
|
+
when 'BACK'
|
550
|
+
if @pos > 0
|
551
|
+
@pos -= 1
|
552
|
+
cont[@pos] = ''
|
553
|
+
end
|
554
|
+
when 'WBACK'
|
555
|
+
while @pos > 0 && cont[@pos - 1] != ' '
|
556
|
+
@pos -= 1
|
557
|
+
cont[@pos] = ''
|
558
|
+
end
|
559
|
+
when 'C-K'
|
560
|
+
cont = ''
|
561
|
+
@pos = 0
|
562
|
+
when 'ENTER'
|
563
|
+
@text = cont
|
564
|
+
chr = 'ESC'
|
565
|
+
when 'UP'
|
566
|
+
if @history.any? && history_index > 0
|
567
|
+
history_index -= 1
|
568
|
+
cont = @history[history_index].pure.slice(0, content_len)
|
569
|
+
@pos = cont.length
|
570
|
+
end
|
571
|
+
when 'DOWN'
|
572
|
+
if history_index < @history.size - 1
|
573
|
+
history_index += 1
|
574
|
+
cont = @history[history_index].pure.slice(0, content_len)
|
575
|
+
@pos = cont.length
|
576
|
+
elsif history_index == @history.size - 1
|
577
|
+
history_index += 1
|
578
|
+
cont = ""
|
579
|
+
@pos = 0
|
580
|
+
end
|
581
|
+
when /^.$/
|
582
|
+
if @pos < content_len
|
583
|
+
cont.insert(@pos, chr)
|
584
|
+
@pos += 1
|
585
|
+
end
|
586
|
+
end
|
587
|
+
|
588
|
+
while IO.select([$stdin], nil, nil, 0)
|
589
|
+
chr = $stdin.read_nonblock(1) rescue nil
|
590
|
+
break unless chr
|
591
|
+
if @pos < content_len
|
592
|
+
cont.insert(@pos, chr)
|
593
|
+
@pos += 1
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
597
|
+
ensure
|
598
|
+
STDIN.raw! rescue nil
|
599
|
+
STDIN.echo = false rescue nil
|
600
|
+
while IO.select([$stdin], nil, nil, 0)
|
601
|
+
$stdin.read_nonblock(4096) rescue break
|
602
|
+
end
|
603
|
+
end
|
604
|
+
prompt_len = @prompt.pure.length
|
605
|
+
new_col = @x + prompt_len + (@pos > 0 ? @pos - 1 : 0)
|
606
|
+
col(new_col)
|
607
|
+
Rcurses::Cursor.hide
|
608
|
+
end
|
609
|
+
|
610
|
+
private
|
611
|
+
|
612
|
+
def flush_stdin
|
613
|
+
while IO.select([$stdin], nil, nil, 0.005)
|
614
|
+
begin
|
615
|
+
$stdin.read_nonblock(1024)
|
616
|
+
rescue IO::WaitReadable, EOFError
|
617
|
+
break
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
|
+
|
622
|
+
def calculate_posx
|
623
|
+
total_length = 0
|
624
|
+
(@ix + @line).times do |i|
|
625
|
+
total_length += Rcurses.display_width(@txt[i].pure) + 1 # +1 for newline
|
626
|
+
end
|
627
|
+
total_length += @pos
|
628
|
+
total_length
|
629
|
+
end
|
630
|
+
|
631
|
+
def calculate_line_start_pos
|
632
|
+
total_length = 0
|
633
|
+
(@ix + @line).times do |i|
|
634
|
+
total_length += Rcurses.display_width(@txt[i].pure) + 1
|
635
|
+
end
|
636
|
+
total_length
|
637
|
+
end
|
638
|
+
|
639
|
+
def split_line_with_ansi(line, w)
|
640
|
+
begin
|
641
|
+
return [""] if line.nil? || w <= 0
|
642
|
+
|
643
|
+
open_sequences = {
|
644
|
+
"\e[1m" => "\e[22m",
|
645
|
+
"\e[3m" => "\e[23m",
|
646
|
+
"\e[4m" => "\e[24m",
|
647
|
+
"\e[5m" => "\e[25m",
|
648
|
+
"\e[7m" => "\e[27m"
|
649
|
+
}
|
650
|
+
close_sequences = open_sequences.values + ["\e[0m"]
|
651
|
+
ansi_regex = /\e\[[0-9;]*m/
|
652
|
+
result = []
|
653
|
+
tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
|
654
|
+
current_line = ''
|
655
|
+
current_line_length = 0
|
656
|
+
active_sequences = []
|
657
|
+
|
658
|
+
tokens.each do |token|
|
659
|
+
if token.match?(ansi_regex)
|
660
|
+
current_line << token
|
661
|
+
if close_sequences.include?(token)
|
662
|
+
if token == "\e[0m"
|
663
|
+
active_sequences.clear
|
664
|
+
else
|
665
|
+
corresponding_open = open_sequences.key(token)
|
666
|
+
active_sequences.delete(corresponding_open)
|
667
|
+
end
|
668
|
+
else
|
669
|
+
active_sequences << token
|
670
|
+
end
|
671
|
+
else
|
672
|
+
words = token.scan(/\s+|\S+/)
|
673
|
+
words.each do |word|
|
674
|
+
begin
|
675
|
+
word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
|
676
|
+
if current_line_length + word_length <= w
|
677
|
+
current_line << word
|
678
|
+
current_line_length += word_length
|
679
|
+
else
|
680
|
+
if current_line_length > 0
|
681
|
+
result << current_line
|
682
|
+
current_line = active_sequences.join
|
683
|
+
current_line_length = 0
|
684
|
+
end
|
685
|
+
while word_length > w
|
686
|
+
part = word[0, [w, word.length].min]
|
687
|
+
current_line << part
|
688
|
+
result << current_line
|
689
|
+
word = word[[w, word.length].min..-1] || ""
|
690
|
+
word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
|
691
|
+
current_line = active_sequences.join
|
692
|
+
current_line_length = 0
|
693
|
+
end
|
694
|
+
if word_length > 0
|
695
|
+
current_line << word
|
696
|
+
current_line_length += word_length
|
697
|
+
end
|
698
|
+
end
|
699
|
+
rescue => e
|
700
|
+
# Skip problematic word but continue
|
701
|
+
next
|
702
|
+
end
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|
706
|
+
result << current_line unless current_line.empty?
|
707
|
+
result.empty? ? [""] : result
|
708
|
+
rescue => e
|
709
|
+
# Complete fallback
|
710
|
+
return [""]
|
711
|
+
end
|
712
|
+
end
|
713
|
+
end
|
714
|
+
end
|
715
|
+
|