rcurses 4.9.0 → 4.9.3
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 +565 -237
- metadata +3 -12
- data/examples/basic_panes.rb +0 -43
- data/examples/focus_panes.rb +0 -42
- data/lib/rcurses/cursor.rb +0 -53
- data/lib/rcurses/general.rb +0 -6
- data/lib/rcurses/input.rb +0 -132
- data/lib/rcurses/pane.rb +0 -765
- data/lib/rcurses.rb +0 -106
- data/lib/string_extensions.rb +0 -171
data/lib/rcurses/pane.rb
DELETED
@@ -1,765 +0,0 @@
|
|
1
|
-
module Rcurses
|
2
|
-
# Enhanced display_width function with better Unicode support
|
3
|
-
def self.display_width(str)
|
4
|
-
width = 0
|
5
|
-
str.each_char do |char|
|
6
|
-
cp = char.ord
|
7
|
-
|
8
|
-
# Handle NUL and control characters
|
9
|
-
if cp == 0
|
10
|
-
# NUL – no width
|
11
|
-
next
|
12
|
-
elsif cp < 32 || (cp >= 0x7F && cp < 0xA0)
|
13
|
-
# Control characters: no width
|
14
|
-
next
|
15
|
-
end
|
16
|
-
|
17
|
-
# Handle combining characters (zero width)
|
18
|
-
if (cp >= 0x0300 && cp <= 0x036F) || # Combining Diacritical Marks
|
19
|
-
(cp >= 0x1AB0 && cp <= 0x1AFF) || # Combining Diacritical Marks Extended
|
20
|
-
(cp >= 0x1DC0 && cp <= 0x1DFF) || # Combining Diacritical Marks Supplement
|
21
|
-
(cp >= 0x20D0 && cp <= 0x20FF) || # Combining Diacritical Marks for Symbols
|
22
|
-
(cp >= 0xFE20 && cp <= 0xFE2F) # Combining Half Marks
|
23
|
-
next
|
24
|
-
end
|
25
|
-
|
26
|
-
# Handle wide characters (East Asian width)
|
27
|
-
if (cp >= 0x1100 && cp <= 0x115F) || # Hangul Jamo
|
28
|
-
(cp >= 0x2329 && cp <= 0x232A) || # Left/Right-Pointing Angle Bracket
|
29
|
-
(cp >= 0x2E80 && cp <= 0x2EFF) || # CJK Radicals Supplement
|
30
|
-
(cp >= 0x2F00 && cp <= 0x2FDF) || # Kangxi Radicals
|
31
|
-
(cp >= 0x2FF0 && cp <= 0x2FFF) || # Ideographic Description Characters
|
32
|
-
(cp >= 0x3000 && cp <= 0x303E) || # CJK Symbols and Punctuation
|
33
|
-
(cp >= 0x3041 && cp <= 0x3096) || # Hiragana
|
34
|
-
(cp >= 0x30A1 && cp <= 0x30FA) || # Katakana
|
35
|
-
(cp >= 0x3105 && cp <= 0x312D) || # Bopomofo
|
36
|
-
(cp >= 0x3131 && cp <= 0x318E) || # Hangul Compatibility Jamo
|
37
|
-
(cp >= 0x3190 && cp <= 0x31BA) || # Kanbun
|
38
|
-
(cp >= 0x31C0 && cp <= 0x31E3) || # CJK Strokes
|
39
|
-
(cp >= 0x31F0 && cp <= 0x31FF) || # Katakana Phonetic Extensions
|
40
|
-
(cp >= 0x3200 && cp <= 0x32FF) || # Enclosed CJK Letters and Months
|
41
|
-
(cp >= 0x3300 && cp <= 0x33FF) || # CJK Compatibility
|
42
|
-
(cp >= 0x3400 && cp <= 0x4DBF) || # CJK Unified Ideographs Extension A
|
43
|
-
(cp >= 0x4E00 && cp <= 0x9FFF) || # CJK Unified Ideographs
|
44
|
-
(cp >= 0xA960 && cp <= 0xA97F) || # Hangul Jamo Extended-A
|
45
|
-
(cp >= 0xAC00 && cp <= 0xD7A3) || # Hangul Syllables
|
46
|
-
(cp >= 0xD7B0 && cp <= 0xD7FF) || # Hangul Jamo Extended-B
|
47
|
-
(cp >= 0xF900 && cp <= 0xFAFF) || # CJK Compatibility Ideographs
|
48
|
-
(cp >= 0xFE10 && cp <= 0xFE19) || # Vertical Forms
|
49
|
-
(cp >= 0xFE30 && cp <= 0xFE6F) || # CJK Compatibility Forms
|
50
|
-
(cp >= 0xFF00 && cp <= 0xFF60) || # Fullwidth Forms
|
51
|
-
(cp >= 0xFFE0 && cp <= 0xFFE6) || # Fullwidth Forms
|
52
|
-
(cp >= 0x1F000 && cp <= 0x1F02F) || # Mahjong Tiles
|
53
|
-
(cp >= 0x1F030 && cp <= 0x1F09F) || # Domino Tiles
|
54
|
-
(cp >= 0x1F100 && cp <= 0x1F1FF) || # Enclosed Alphanumeric Supplement
|
55
|
-
(cp >= 0x1F200 && cp <= 0x1F2FF) || # Enclosed Ideographic Supplement
|
56
|
-
(cp >= 0x1F300 && cp <= 0x1F5FF) || # Miscellaneous Symbols and Pictographs
|
57
|
-
(cp >= 0x1F600 && cp <= 0x1F64F) || # Emoticons
|
58
|
-
(cp >= 0x1F650 && cp <= 0x1F67F) || # Ornamental Dingbats
|
59
|
-
(cp >= 0x1F680 && cp <= 0x1F6FF) || # Transport and Map Symbols
|
60
|
-
(cp >= 0x1F700 && cp <= 0x1F77F) || # Alchemical Symbols
|
61
|
-
(cp >= 0x1F780 && cp <= 0x1F7FF) || # Geometric Shapes Extended
|
62
|
-
(cp >= 0x1F800 && cp <= 0x1F8FF) || # Supplemental Arrows-C
|
63
|
-
(cp >= 0x1F900 && cp <= 0x1F9FF) || # Supplemental Symbols and Pictographs
|
64
|
-
(cp >= 0x20000 && cp <= 0x2FFFF) || # CJK Unified Ideographs Extension B-F
|
65
|
-
(cp >= 0x30000 && cp <= 0x3FFFF) # CJK Unified Ideographs Extension G
|
66
|
-
width += 2
|
67
|
-
else
|
68
|
-
width += 1
|
69
|
-
end
|
70
|
-
end
|
71
|
-
width
|
72
|
-
end
|
73
|
-
|
74
|
-
class Pane
|
75
|
-
require 'clipboard' # Ensure the 'clipboard' gem is installed
|
76
|
-
include Cursor
|
77
|
-
include Input
|
78
|
-
|
79
|
-
# Compiled regex patterns for performance
|
80
|
-
ANSI_REGEX = /\e\[[0-9;]*m/.freeze
|
81
|
-
SGR_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
|
82
|
-
attr_accessor :x, :y, :w, :h, :fg, :bg
|
83
|
-
attr_accessor :border, :scroll, :text, :ix, :index, :align, :prompt
|
84
|
-
attr_accessor :moreup, :moredown
|
85
|
-
attr_accessor :record, :history
|
86
|
-
attr_accessor :updates_suspended
|
87
|
-
|
88
|
-
def initialize(x = 1, y = 1, w = 1, h = 1, fg = nil, bg = nil)
|
89
|
-
@terminal_size_cache = nil
|
90
|
-
@terminal_size_time = nil
|
91
|
-
@x = x
|
92
|
-
@y = y
|
93
|
-
@w = w
|
94
|
-
@h = h
|
95
|
-
@fg, @bg = fg, bg
|
96
|
-
@text = "" # Initialize text variable
|
97
|
-
@align = "l" # Default alignment
|
98
|
-
@scroll = true # Enable scroll indicators
|
99
|
-
@prompt = "" # Prompt for editline
|
100
|
-
@ix = 0 # Starting text line index
|
101
|
-
@prev_frame = nil # Holds the previously rendered frame (array of lines)
|
102
|
-
@line = 0 # For cursor tracking during editing:
|
103
|
-
@pos = 0 # For cursor tracking during editing:
|
104
|
-
@record = false # Don't record history unless explicitly set to true
|
105
|
-
@history = [] # History array
|
106
|
-
@updates_suspended = false
|
107
|
-
@lazy_cache_limit = 1000 # Limit for lazy text cache
|
108
|
-
end
|
109
|
-
|
110
|
-
def text=(new_text)
|
111
|
-
(@history << @text) if @record && @text
|
112
|
-
@text = new_text
|
113
|
-
end
|
114
|
-
|
115
|
-
def ask(prompt, text)
|
116
|
-
@prompt = prompt
|
117
|
-
@text = text
|
118
|
-
editline
|
119
|
-
(@history << @text) if @record && !@text.empty?
|
120
|
-
@text
|
121
|
-
end
|
122
|
-
|
123
|
-
def say(text)
|
124
|
-
(@history << text) if @record && !text.empty?
|
125
|
-
@text = text
|
126
|
-
@ix = 0
|
127
|
-
refresh
|
128
|
-
end
|
129
|
-
|
130
|
-
def clear
|
131
|
-
@text = ""
|
132
|
-
@ix = 0
|
133
|
-
full_refresh
|
134
|
-
end
|
135
|
-
|
136
|
-
def suspend_updates
|
137
|
-
@updates_suspended = true
|
138
|
-
end
|
139
|
-
|
140
|
-
def resume_updates
|
141
|
-
@updates_suspended = false
|
142
|
-
refresh
|
143
|
-
end
|
144
|
-
|
145
|
-
def get_terminal_size
|
146
|
-
now = Time.now
|
147
|
-
if @terminal_size_cache.nil? || @terminal_size_time.nil? || (now - @terminal_size_time) > 0.5
|
148
|
-
@terminal_size_cache = IO.console.winsize
|
149
|
-
@terminal_size_time = now
|
150
|
-
end
|
151
|
-
@terminal_size_cache
|
152
|
-
end
|
153
|
-
|
154
|
-
def move(dx, dy)
|
155
|
-
@x += dx
|
156
|
-
@y += dy
|
157
|
-
refresh
|
158
|
-
end
|
159
|
-
|
160
|
-
def linedown
|
161
|
-
@ix += 1
|
162
|
-
text_lines = @text.split("\n")
|
163
|
-
@ix = text_lines.length if @ix > text_lines.length - 1
|
164
|
-
refresh
|
165
|
-
end
|
166
|
-
|
167
|
-
def lineup
|
168
|
-
@ix -= 1
|
169
|
-
@ix = 0 if @ix < 0
|
170
|
-
refresh
|
171
|
-
end
|
172
|
-
|
173
|
-
def pagedown
|
174
|
-
@ix = @ix + @h - 1
|
175
|
-
text_lines = @text.split("\n")
|
176
|
-
@ix = text_lines.length - @h if @ix > text_lines.length - @h
|
177
|
-
refresh
|
178
|
-
end
|
179
|
-
|
180
|
-
def pageup
|
181
|
-
@ix = @ix - @h + 1
|
182
|
-
@ix = 0 if @ix < 0
|
183
|
-
refresh
|
184
|
-
end
|
185
|
-
|
186
|
-
def bottom
|
187
|
-
text_lines = @text.split("\n")
|
188
|
-
@ix = text_lines.length - @h
|
189
|
-
refresh
|
190
|
-
end
|
191
|
-
|
192
|
-
def top
|
193
|
-
@ix = 0
|
194
|
-
refresh
|
195
|
-
end
|
196
|
-
|
197
|
-
# full_refresh forces a complete repaint.
|
198
|
-
def full_refresh(cont = @text)
|
199
|
-
@prev_frame = nil
|
200
|
-
refresh(cont)
|
201
|
-
end
|
202
|
-
|
203
|
-
# Refresh only the border
|
204
|
-
def border_refresh
|
205
|
-
left_col = @x - 1
|
206
|
-
right_col = @x + @w
|
207
|
-
top_row = @y - 1
|
208
|
-
bottom_row = @y + @h
|
209
|
-
|
210
|
-
if @border
|
211
|
-
fmt = [@fg.to_s, @bg.to_s].join(',')
|
212
|
-
top = ("┌" + "─" * @w + "┐").c(fmt)
|
213
|
-
STDOUT.print "\e[#{top_row};#{left_col}H" + top
|
214
|
-
(0...@h).each do |i|
|
215
|
-
row = @y + i
|
216
|
-
STDOUT.print "\e[#{row};#{left_col}H" + "│".c(fmt)
|
217
|
-
STDOUT.print "\e[#{row};#{right_col}H" + "│".c(fmt)
|
218
|
-
end
|
219
|
-
bottom = ("└" + "─" * @w + "┘").c(fmt)
|
220
|
-
STDOUT.print "\e[#{bottom_row};#{left_col}H" + bottom
|
221
|
-
else
|
222
|
-
STDOUT.print "\e[#{top_row};#{left_col}H" + " " * (@w + 2)
|
223
|
-
(0...@h).each do |i|
|
224
|
-
row = @y + i
|
225
|
-
STDOUT.print "\e[#{row};#{left_col}H" + " "
|
226
|
-
STDOUT.print "\e[#{row};#{right_col}H" + " "
|
227
|
-
end
|
228
|
-
STDOUT.print "\e[#{bottom_row};#{left_col}H" + " " * (@w + 2)
|
229
|
-
end
|
230
|
-
end
|
231
|
-
|
232
|
-
# Diff-based refresh that minimizes flicker.
|
233
|
-
# In this updated version we lazily process only the raw lines required to fill the pane.
|
234
|
-
def refresh(cont = @text)
|
235
|
-
return if @updates_suspended
|
236
|
-
|
237
|
-
# Check if we're in batch mode and suspend updates accordingly
|
238
|
-
if Rcurses.batch_mode?
|
239
|
-
Rcurses.add_to_batch(self)
|
240
|
-
return
|
241
|
-
end
|
242
|
-
|
243
|
-
@max_h, @max_w = get_terminal_size
|
244
|
-
|
245
|
-
if @border
|
246
|
-
@w = @max_w - 2 if @w > @max_w - 2
|
247
|
-
@h = @max_h - 2 if @h > @max_h - 2
|
248
|
-
@x = 2 if @x < 2; @x = @max_w - @w if @x + @w > @max_w
|
249
|
-
@y = 2 if @y < 2; @y = @max_h - @h if @y + @h > @max_h
|
250
|
-
else
|
251
|
-
@w = @max_w if @w > @max_w
|
252
|
-
@h = @max_h if @h > @max_h
|
253
|
-
@x = 1 if @x < 1; @x = @max_w - @w + 1 if @x + @w > @max_w + 1
|
254
|
-
@y = 1 if @y < 1; @y = @max_h - @h + 1 if @y + @h > @max_h + 1
|
255
|
-
end
|
256
|
-
|
257
|
-
o_row, o_col = pos
|
258
|
-
|
259
|
-
# Hide cursor, disable auto-wrap, reset all SGR and scroll margins
|
260
|
-
# (so stray underline, scroll regions, etc. can’t leak out)
|
261
|
-
STDOUT.print "\e[?25l\e[?7l\e[0m\e[r"
|
262
|
-
|
263
|
-
fmt = [@fg.to_s, @bg.to_s].join(',')
|
264
|
-
|
265
|
-
# Lazy evaluation: If the content or pane width has changed, reinitialize the lazy cache.
|
266
|
-
if !defined?(@cached_text) || @cached_text != cont || @cached_w != @w
|
267
|
-
@raw_txt = cont.split("\n")
|
268
|
-
@lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
|
269
|
-
@lazy_index = 0 # Pointer to the next raw line to process.
|
270
|
-
@cached_text = cont.dup
|
271
|
-
@cached_w = @w
|
272
|
-
|
273
|
-
# Clear cache if it gets too large to prevent memory leaks
|
274
|
-
if @lazy_txt.size > @lazy_cache_limit
|
275
|
-
@lazy_txt.clear
|
276
|
-
@lazy_index = 0
|
277
|
-
end
|
278
|
-
end
|
279
|
-
|
280
|
-
content_rows = @h
|
281
|
-
# Ensure we have processed enough lines for the current scroll position + visible area.
|
282
|
-
required_lines = @ix + content_rows
|
283
|
-
while @lazy_txt.size < required_lines && @lazy_index < @raw_txt.size
|
284
|
-
raw_line = @raw_txt[@lazy_index]
|
285
|
-
# If the raw line is short, no wrapping is needed.
|
286
|
-
if raw_line.respond_to?(:pure) && Rcurses.display_width(raw_line.pure) < @w
|
287
|
-
processed = [raw_line]
|
288
|
-
else
|
289
|
-
processed = split_line_with_ansi(raw_line, @w)
|
290
|
-
end
|
291
|
-
@lazy_txt.concat(processed)
|
292
|
-
@lazy_index += 1
|
293
|
-
end
|
294
|
-
@txt = @lazy_txt
|
295
|
-
|
296
|
-
@ix = @txt.length - 1 if @ix > @txt.length - 1
|
297
|
-
@ix = 0 if @ix < 0
|
298
|
-
|
299
|
-
new_frame = []
|
300
|
-
|
301
|
-
content_rows.times do |i|
|
302
|
-
line_str = ""
|
303
|
-
l = @ix + i
|
304
|
-
if @txt[l].to_s != ""
|
305
|
-
pl = @w - Rcurses.display_width(@txt[l].pure)
|
306
|
-
pl = 0 if pl < 0
|
307
|
-
hl = pl / 2
|
308
|
-
case @align
|
309
|
-
when "l"
|
310
|
-
line_str = @txt[l].pure.c(fmt) + " ".c(fmt) * pl
|
311
|
-
when "r"
|
312
|
-
line_str = " ".c(fmt) * pl + @txt[l].pure.c(fmt)
|
313
|
-
when "c"
|
314
|
-
line_str = " ".c(fmt) * hl + @txt[l].pure.c(fmt) + " ".c(fmt) * (pl - hl)
|
315
|
-
end
|
316
|
-
else
|
317
|
-
line_str = " ".c(fmt) * @w
|
318
|
-
end
|
319
|
-
|
320
|
-
new_frame << line_str
|
321
|
-
end
|
322
|
-
|
323
|
-
diff_buf = ""
|
324
|
-
new_frame.each_with_index do |line, i|
|
325
|
-
row_num = @y + i
|
326
|
-
col_num = @x
|
327
|
-
if @prev_frame.nil? || @prev_frame[i] != line
|
328
|
-
diff_buf << "\e[#{row_num};#{col_num}H" << line
|
329
|
-
end
|
330
|
-
end
|
331
|
-
|
332
|
-
# restore wrap, then also reset SGR and scroll-region one more time
|
333
|
-
diff_buf << "\e[#{o_row};#{o_col}H\e[?7h\e[0m\e[r"
|
334
|
-
print diff_buf
|
335
|
-
@prev_frame = new_frame
|
336
|
-
|
337
|
-
# Draw scroll markers after printing the frame.
|
338
|
-
if @scroll
|
339
|
-
marker_col = @x + @w - 1
|
340
|
-
if @ix > 0
|
341
|
-
print "\e[#{@y};#{marker_col}H" + "∆".c(fmt)
|
342
|
-
end
|
343
|
-
# If there are more processed lines than fit in the pane
|
344
|
-
# OR there remain raw lines to process, show the down marker.
|
345
|
-
if (@txt.length - @ix) > @h || (@lazy_index < @raw_txt.size)
|
346
|
-
print "\e[#{@y + @h - 1};#{marker_col}H" + "∇".c(fmt)
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
if @border
|
351
|
-
# top
|
352
|
-
print "\e[#{@y - 1};#{@x - 1}H" + ("┌" + "─" * @w + "┐").c(fmt)
|
353
|
-
# sides
|
354
|
-
(0...@h).each do |i|
|
355
|
-
print "\e[#{@y + i};#{@x - 1}H" + "│".c(fmt)
|
356
|
-
print "\e[#{@y + i};#{@x + @w}H" + "│".c(fmt)
|
357
|
-
end
|
358
|
-
# bottom
|
359
|
-
print "\e[#{@y + @h};#{@x - 1}H" + ("└" + "─" * @w + "┘").c(fmt)
|
360
|
-
end
|
361
|
-
|
362
|
-
new_frame.join("\n")
|
363
|
-
end
|
364
|
-
|
365
|
-
def textformat(cont)
|
366
|
-
# This method is no longer used in refresh since we process lazily,
|
367
|
-
# but is kept here if needed elsewhere.
|
368
|
-
lines = cont.split("\n")
|
369
|
-
result = []
|
370
|
-
lines.each do |line|
|
371
|
-
split_lines = split_line_with_ansi(line, @w)
|
372
|
-
result.concat(split_lines)
|
373
|
-
end
|
374
|
-
result
|
375
|
-
end
|
376
|
-
|
377
|
-
def right
|
378
|
-
if @pos < @txt[@ix + @line].length
|
379
|
-
@pos += 1
|
380
|
-
if @pos == @w
|
381
|
-
@pos = 0
|
382
|
-
if @line == @h - 1
|
383
|
-
@ix += 1
|
384
|
-
else
|
385
|
-
@line += 1
|
386
|
-
end
|
387
|
-
end
|
388
|
-
else
|
389
|
-
if @line == @h - 1
|
390
|
-
@ix += 1 unless @ix >= @txt.length - @h
|
391
|
-
@pos = 0
|
392
|
-
elsif @line + @ix + 1 < @txt.length
|
393
|
-
@line += 1
|
394
|
-
@pos = 0
|
395
|
-
end
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
def left
|
400
|
-
if @pos == 0
|
401
|
-
if @line == 0
|
402
|
-
unless @ix == 0
|
403
|
-
@ix -= 1
|
404
|
-
@pos = @txt[@ix + @line].length
|
405
|
-
end
|
406
|
-
else
|
407
|
-
@line -= 1
|
408
|
-
@pos = @txt[@ix + @line].length
|
409
|
-
end
|
410
|
-
else
|
411
|
-
@pos -= 1
|
412
|
-
end
|
413
|
-
end
|
414
|
-
|
415
|
-
def up
|
416
|
-
if @line == 0
|
417
|
-
@ix -= 1 unless @ix == 0
|
418
|
-
else
|
419
|
-
@line -= 1
|
420
|
-
end
|
421
|
-
begin
|
422
|
-
@pos = [@pos, @txt[@ix + @line].length].min
|
423
|
-
rescue
|
424
|
-
end
|
425
|
-
end
|
426
|
-
|
427
|
-
def down
|
428
|
-
if @line == @h - 1
|
429
|
-
@ix += 1 unless @ix + @line >= @txt.length - 1
|
430
|
-
elsif @line + @ix + 1 < @txt.length
|
431
|
-
@line += 1
|
432
|
-
end
|
433
|
-
begin
|
434
|
-
@pos = [@pos, @txt[@ix + @line].length].min
|
435
|
-
rescue
|
436
|
-
end
|
437
|
-
end
|
438
|
-
|
439
|
-
def parse(cont)
|
440
|
-
cont.gsub!(/\*(.+?)\*/, '\1'.b)
|
441
|
-
cont.gsub!(/\/(.+?)\//, '\1'.i)
|
442
|
-
cont.gsub!(/_(.+?)_/, '\1'.u)
|
443
|
-
cont.gsub!(/#(.+?)#/, '\1'.r)
|
444
|
-
cont.gsub!(/<([^|]+)\|([^>]+)>/) do
|
445
|
-
text = $2; codes = $1
|
446
|
-
text.c(codes)
|
447
|
-
end
|
448
|
-
cont
|
449
|
-
end
|
450
|
-
|
451
|
-
def edit
|
452
|
-
begin
|
453
|
-
STDIN.cooked! rescue nil
|
454
|
-
STDIN.echo = true rescue nil
|
455
|
-
# Prepare content with visible newline markers
|
456
|
-
content = @text.pure.gsub("\n", "¬\n")
|
457
|
-
# Reset editing cursor state
|
458
|
-
@ix = 0
|
459
|
-
@line = 0
|
460
|
-
@pos = 0
|
461
|
-
# Initial render sets @txt internally for display and cursor math
|
462
|
-
refresh(content)
|
463
|
-
Rcurses::Cursor.show
|
464
|
-
input_char = ''
|
465
|
-
|
466
|
-
while input_char != 'ESC'
|
467
|
-
# Move the terminal cursor to the logical text cursor
|
468
|
-
row(@y + @line)
|
469
|
-
col(@x + @pos)
|
470
|
-
input_char = getchr(flush: false)
|
471
|
-
case input_char
|
472
|
-
when 'C-L'
|
473
|
-
@align = 'l'
|
474
|
-
when 'C-R'
|
475
|
-
@align = 'r'
|
476
|
-
when 'C-C'
|
477
|
-
@align = 'c'
|
478
|
-
when 'C-Y'
|
479
|
-
Clipboard.copy(@text.pure)
|
480
|
-
when 'C-S'
|
481
|
-
content = content.gsub('¬', "\n")
|
482
|
-
content = parse(content)
|
483
|
-
@text = content
|
484
|
-
input_char = 'ESC'
|
485
|
-
when 'DEL'
|
486
|
-
posx = calculate_posx
|
487
|
-
content.slice!(posx)
|
488
|
-
when 'BACK'
|
489
|
-
if @pos > 0
|
490
|
-
left
|
491
|
-
posx = calculate_posx
|
492
|
-
content.slice!(posx)
|
493
|
-
end
|
494
|
-
when 'WBACK'
|
495
|
-
while @pos > 0 && content[calculate_posx - 1] != ' '
|
496
|
-
left
|
497
|
-
posx = calculate_posx
|
498
|
-
content.slice!(posx)
|
499
|
-
end
|
500
|
-
when 'C-K'
|
501
|
-
line_start_pos = calculate_line_start_pos
|
502
|
-
line_length = @txt[@ix + @line]&.length || 0
|
503
|
-
content.slice!(line_start_pos + @pos, line_length - @pos)
|
504
|
-
when 'UP'
|
505
|
-
up
|
506
|
-
when 'DOWN'
|
507
|
-
down
|
508
|
-
when 'RIGHT'
|
509
|
-
right
|
510
|
-
when 'LEFT'
|
511
|
-
left
|
512
|
-
when 'HOME'
|
513
|
-
@pos = 0
|
514
|
-
when 'END'
|
515
|
-
current_line_length = @txt[@ix + @line]&.length || 0
|
516
|
-
@pos = current_line_length
|
517
|
-
when 'C-HOME'
|
518
|
-
@ix = 0; @line = 0; @pos = 0
|
519
|
-
when 'C-END'
|
520
|
-
total_lines = @txt.length
|
521
|
-
@ix = [total_lines - @h, 0].max
|
522
|
-
@line = [@h - 1, total_lines - @ix - 1].min
|
523
|
-
current_line_length = @txt[@ix + @line]&.length || 0
|
524
|
-
@pos = current_line_length
|
525
|
-
when 'ENTER'
|
526
|
-
posx = calculate_posx
|
527
|
-
content.insert(posx, "¬\n")
|
528
|
-
right
|
529
|
-
when /^.$/
|
530
|
-
posx = calculate_posx
|
531
|
-
content.insert(posx, input_char)
|
532
|
-
right
|
533
|
-
end
|
534
|
-
|
535
|
-
# Handle any buffered input
|
536
|
-
while IO.select([$stdin], nil, nil, 0)
|
537
|
-
input_char = $stdin.read_nonblock(1) rescue nil
|
538
|
-
break unless input_char
|
539
|
-
posx = calculate_posx
|
540
|
-
content.insert(posx, input_char)
|
541
|
-
right
|
542
|
-
end
|
543
|
-
|
544
|
-
# Re-render without overwriting the internal @txt
|
545
|
-
refresh(content)
|
546
|
-
Rcurses::Cursor.show
|
547
|
-
end
|
548
|
-
ensure
|
549
|
-
STDIN.raw! rescue nil
|
550
|
-
STDIN.echo = false rescue nil
|
551
|
-
while IO.select([$stdin], nil, nil, 0)
|
552
|
-
$stdin.read_nonblock(4096) rescue break
|
553
|
-
end
|
554
|
-
end
|
555
|
-
Rcurses::Cursor.hide
|
556
|
-
end
|
557
|
-
|
558
|
-
def editline
|
559
|
-
begin
|
560
|
-
STDIN.cooked! rescue nil
|
561
|
-
STDIN.echo = true rescue nil
|
562
|
-
Rcurses::Cursor.show
|
563
|
-
@x = [[@x, 1].max, @max_w - @w + 1].min
|
564
|
-
@y = [[@y, 1].max, @max_h - @h + 1].min
|
565
|
-
@scroll = false
|
566
|
-
@ix = 0
|
567
|
-
row(@y)
|
568
|
-
fmt = [@fg.to_s, @bg.to_s].join(',')
|
569
|
-
col(@x)
|
570
|
-
print @prompt.c(fmt)
|
571
|
-
prompt_len = @prompt.pure.length
|
572
|
-
content_len = @w - prompt_len
|
573
|
-
cont = @text.pure.slice(0, content_len)
|
574
|
-
@pos = cont.length
|
575
|
-
chr = ''
|
576
|
-
history_index = @history.size
|
577
|
-
|
578
|
-
while chr != 'ESC'
|
579
|
-
col(@x + prompt_len)
|
580
|
-
cont = cont.slice(0, content_len)
|
581
|
-
print cont.ljust(content_len).c(fmt)
|
582
|
-
col(@x + prompt_len + @pos)
|
583
|
-
chr = getchr(flush: false)
|
584
|
-
case chr
|
585
|
-
when 'LEFT'
|
586
|
-
@pos -= 1 if @pos > 0
|
587
|
-
when 'RIGHT'
|
588
|
-
@pos += 1 if @pos < cont.length
|
589
|
-
when 'HOME'
|
590
|
-
@pos = 0
|
591
|
-
when 'END'
|
592
|
-
@pos = cont.length
|
593
|
-
when 'DEL'
|
594
|
-
cont[@pos] = '' if @pos < cont.length
|
595
|
-
when 'BACK'
|
596
|
-
if @pos > 0
|
597
|
-
@pos -= 1
|
598
|
-
cont[@pos] = ''
|
599
|
-
end
|
600
|
-
when 'WBACK'
|
601
|
-
while @pos > 0 && cont[@pos - 1] != ' '
|
602
|
-
@pos -= 1
|
603
|
-
cont[@pos] = ''
|
604
|
-
end
|
605
|
-
when 'C-K'
|
606
|
-
cont = ''
|
607
|
-
@pos = 0
|
608
|
-
when 'ENTER'
|
609
|
-
@text = cont
|
610
|
-
chr = 'ESC'
|
611
|
-
when 'UP'
|
612
|
-
if @history.any? && history_index > 0
|
613
|
-
history_index -= 1
|
614
|
-
cont = @history[history_index].pure.slice(0, content_len)
|
615
|
-
@pos = cont.length
|
616
|
-
end
|
617
|
-
when 'DOWN'
|
618
|
-
if history_index < @history.size - 1
|
619
|
-
history_index += 1
|
620
|
-
cont = @history[history_index].pure.slice(0, content_len)
|
621
|
-
@pos = cont.length
|
622
|
-
elsif history_index == @history.size - 1
|
623
|
-
history_index += 1
|
624
|
-
cont = ""
|
625
|
-
@pos = 0
|
626
|
-
end
|
627
|
-
when /^.$/
|
628
|
-
if @pos < content_len
|
629
|
-
cont.insert(@pos, chr)
|
630
|
-
@pos += 1
|
631
|
-
end
|
632
|
-
end
|
633
|
-
|
634
|
-
while IO.select([$stdin], nil, nil, 0)
|
635
|
-
chr = $stdin.read_nonblock(1) rescue nil
|
636
|
-
break unless chr
|
637
|
-
if @pos < content_len
|
638
|
-
cont.insert(@pos, chr)
|
639
|
-
@pos += 1
|
640
|
-
end
|
641
|
-
end
|
642
|
-
end
|
643
|
-
ensure
|
644
|
-
STDIN.raw! rescue nil
|
645
|
-
STDIN.echo = false rescue nil
|
646
|
-
while IO.select([$stdin], nil, nil, 0)
|
647
|
-
$stdin.read_nonblock(4096) rescue break
|
648
|
-
end
|
649
|
-
end
|
650
|
-
prompt_len = @prompt.pure.length
|
651
|
-
new_col = @x + prompt_len + (@pos > 0 ? @pos - 1 : 0)
|
652
|
-
col(new_col)
|
653
|
-
Rcurses::Cursor.hide
|
654
|
-
end
|
655
|
-
|
656
|
-
private
|
657
|
-
|
658
|
-
def flush_stdin
|
659
|
-
while IO.select([$stdin], nil, nil, 0.005)
|
660
|
-
begin
|
661
|
-
$stdin.read_nonblock(1024)
|
662
|
-
rescue IO::WaitReadable, EOFError
|
663
|
-
break
|
664
|
-
end
|
665
|
-
end
|
666
|
-
end
|
667
|
-
|
668
|
-
def calculate_posx
|
669
|
-
total_length = 0
|
670
|
-
(@ix + @line).times do |i|
|
671
|
-
total_length += Rcurses.display_width(@txt[i].pure) + 1 # +1 for newline
|
672
|
-
end
|
673
|
-
total_length += @pos
|
674
|
-
total_length
|
675
|
-
end
|
676
|
-
|
677
|
-
def calculate_line_start_pos
|
678
|
-
total_length = 0
|
679
|
-
(@ix + @line).times do |i|
|
680
|
-
total_length += Rcurses.display_width(@txt[i].pure) + 1
|
681
|
-
end
|
682
|
-
total_length
|
683
|
-
end
|
684
|
-
|
685
|
-
def split_line_with_ansi(line, w)
|
686
|
-
open_sequences = {
|
687
|
-
"\e[1m" => "\e[22m",
|
688
|
-
"\e[3m" => "\e[23m",
|
689
|
-
"\e[4m" => "\e[24m",
|
690
|
-
"\e[5m" => "\e[25m",
|
691
|
-
"\e[7m" => "\e[27m"
|
692
|
-
}
|
693
|
-
close_sequences = open_sequences.values + ["\e[0m"]
|
694
|
-
result = []
|
695
|
-
tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
|
696
|
-
current_line = ''
|
697
|
-
current_line_length = 0
|
698
|
-
active_sequences = []
|
699
|
-
tokens.each do |token|
|
700
|
-
if token.match?(ANSI_REGEX)
|
701
|
-
current_line << token
|
702
|
-
if close_sequences.include?(token)
|
703
|
-
if token == "\e[0m"
|
704
|
-
active_sequences.clear
|
705
|
-
else
|
706
|
-
corresponding_open = open_sequences.key(token)
|
707
|
-
active_sequences.delete(corresponding_open)
|
708
|
-
end
|
709
|
-
else
|
710
|
-
active_sequences << token
|
711
|
-
end
|
712
|
-
else
|
713
|
-
words = token.scan(/\s+|\S+/)
|
714
|
-
words.each do |word|
|
715
|
-
word_length = Rcurses.display_width(word.gsub(ANSI_REGEX, ''))
|
716
|
-
if current_line_length + word_length <= w
|
717
|
-
current_line << word
|
718
|
-
current_line_length += word_length
|
719
|
-
else
|
720
|
-
if current_line_length > 0
|
721
|
-
result << current_line
|
722
|
-
current_line = active_sequences.join
|
723
|
-
current_line_length = 0
|
724
|
-
end
|
725
|
-
while word_length > w
|
726
|
-
# Split safely respecting UTF-8 boundaries and display width
|
727
|
-
part = safe_substring_by_width(word, w)
|
728
|
-
current_line << part
|
729
|
-
result << current_line
|
730
|
-
word = word[part.length..-1]
|
731
|
-
word_length = Rcurses.display_width(word.gsub(ANSI_REGEX, ''))
|
732
|
-
current_line = active_sequences.join
|
733
|
-
current_line_length = 0
|
734
|
-
end
|
735
|
-
if word_length > 0
|
736
|
-
current_line << word
|
737
|
-
current_line_length += word_length
|
738
|
-
end
|
739
|
-
end
|
740
|
-
end
|
741
|
-
end
|
742
|
-
end
|
743
|
-
result << current_line unless current_line.empty?
|
744
|
-
result
|
745
|
-
end
|
746
|
-
|
747
|
-
# Helper method to safely split strings by display width while respecting UTF-8 boundaries
|
748
|
-
def safe_substring_by_width(str, max_width)
|
749
|
-
return str if Rcurses.display_width(str) <= max_width
|
750
|
-
|
751
|
-
result = ''
|
752
|
-
current_width = 0
|
753
|
-
|
754
|
-
str.each_char do |char|
|
755
|
-
char_width = Rcurses.display_width(char)
|
756
|
-
break if current_width + char_width > max_width
|
757
|
-
result += char
|
758
|
-
current_width += char_width
|
759
|
-
end
|
760
|
-
|
761
|
-
result
|
762
|
-
end
|
763
|
-
end
|
764
|
-
end
|
765
|
-
|