rcurses 2.0 → 2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +24 -0
- data/{rcurses_example.rb → examples/basic_panes.rb} +1 -1
- data/lib/rcurses/cursor.rb +79 -0
- data/lib/rcurses/input.rb +83 -0
- data/lib/rcurses/pane.rb +494 -0
- data/lib/rcurses.rb +5 -699
- data/lib/string_extensions.rb +34 -0
- metadata +11 -8
- data/extconf.rb +0 -33
data/lib/rcurses.rb
CHANGED
@@ -5,708 +5,14 @@
|
|
5
5
|
# Web_site: http://isene.com/
|
6
6
|
# Github: https://github.com/isene/rcurses
|
7
7
|
# License: Public domain
|
8
|
-
# Version: 2.
|
8
|
+
# Version: 2.1: New gem structure
|
9
9
|
|
10
10
|
require 'io/console' # Basic gem for rcurses
|
11
11
|
require 'io/wait' # stdin handling
|
12
|
-
begin
|
13
|
-
require 'clipboard' # Ensure the 'clipboard' gem is installed
|
14
|
-
rescue LoadError
|
15
|
-
# Define a fallback Clipboard module
|
16
|
-
module Clipboard
|
17
|
-
def self.copy(_text)
|
18
|
-
# Clipboard functionality is not available
|
19
|
-
puts "Clipboard functionality is not available. Please install the 'clipboard' gem to enable it."
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
12
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
def fb(fg, bg); color(self, "\e[38;5;#{fg};48;5;#{bg}m"); end
|
29
|
-
def b ; color(self, "\e[1m", "\e[22m") ; end
|
30
|
-
def i ; color(self, "\e[3m", "\e[23m") ; end
|
31
|
-
def u ; color(self, "\e[4m", "\e[24m") ; end
|
32
|
-
def l ; color(self, "\e[5m", "\e[25m") ; end
|
33
|
-
def r ; color(self, "\e[7m", "\e[27m") ; end
|
34
|
-
# Internal function
|
35
|
-
def color(text, sp, ep = "\e[0m"); "#{sp}#{text}#{ep}"; end
|
36
|
-
|
37
|
-
# Use format "TEST".c("204,45,bui") to print "TEST" in bold, underline italic, fg=204 and bg=45
|
38
|
-
def c(code)
|
39
|
-
prop = "\e["
|
40
|
-
prop += "38;5;#{code.match(/^\d+/).to_s};" if code.match(/^\d+/)
|
41
|
-
prop += "48;5;#{code.match(/(?<=,)\d+/).to_s};" if code.match(/(?<=,)\d+/)
|
42
|
-
prop += "1;" if code.include?('b')
|
43
|
-
prop += "3;" if code.include?('i')
|
44
|
-
prop += "4;" if code.include?('u')
|
45
|
-
prop += "5;" if code.include?('l')
|
46
|
-
prop += "7;" if code.include?('r')
|
47
|
-
prop.chop! if prop[-1] == ";"
|
48
|
-
prop += "m"
|
49
|
-
prop += self
|
50
|
-
prop += "\e[0m"
|
51
|
-
prop
|
52
|
-
end
|
53
|
-
|
54
|
-
def pure
|
55
|
-
self.gsub(/\e\[\d+(?:;\d+)*m/, '')
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
module Rcurses
|
60
|
-
module Cursor
|
61
|
-
# Terminal cursor movement ANSI codes (inspired by https://github.com/piotrmurach/tty-cursor)
|
62
|
-
module_function
|
63
|
-
ESC = "\e".freeze
|
64
|
-
CSI = "\e[".freeze
|
65
|
-
def save # Save current position
|
66
|
-
print(Gem.win_platform? ? CSI + 's' : ESC + '7')
|
67
|
-
end
|
68
|
-
def restore # Restore cursor position
|
69
|
-
print(Gem.win_platform? ? CSI + 'u' : ESC + '8')
|
70
|
-
end
|
71
|
-
def pos # Query cursor current position
|
72
|
-
res = ''
|
73
|
-
$stdin.raw do |stdin|
|
74
|
-
$stdout << CSI + '6n' # The actual ANSI get-position
|
75
|
-
$stdout.flush
|
76
|
-
while (c = stdin.getc) != 'R'
|
77
|
-
res << c if c
|
78
|
-
end
|
79
|
-
end
|
80
|
-
m = res.match(/(?<row>\d+);(?<col>\d+)/)
|
81
|
-
return m[:row].to_i, m[:col].to_i
|
82
|
-
end
|
83
|
-
def rowget
|
84
|
-
row, _col = pos
|
85
|
-
row
|
86
|
-
end
|
87
|
-
def colget
|
88
|
-
_row, col = pos
|
89
|
-
col
|
90
|
-
end
|
91
|
-
def up(n = 1) # Move cursor up by n
|
92
|
-
print(CSI + "#{(n || 1)}A")
|
93
|
-
end
|
94
|
-
def down(n = 1) # Move the cursor down by n
|
95
|
-
print(CSI + "#{(n || 1)}B")
|
96
|
-
end
|
97
|
-
def left(n = 1) # Move the cursor backward by n
|
98
|
-
print(CSI + "#{n || 1}D")
|
99
|
-
end
|
100
|
-
def right(n = 1) # Move the cursor forward by n
|
101
|
-
print(CSI + "#{n || 1}C")
|
102
|
-
end
|
103
|
-
def col(n = 1) # Cursor moves to nth position horizontally in the current line
|
104
|
-
print(CSI + "#{n || 1}G")
|
105
|
-
end
|
106
|
-
def row(n = 1) # Cursor moves to the nth position vertically in the current column
|
107
|
-
print(CSI + "#{n || 1}d")
|
108
|
-
end
|
109
|
-
def next_line # Move cursor down to beginning of next line
|
110
|
-
print(CSI + 'E' + CSI + "1G")
|
111
|
-
end
|
112
|
-
def prev_line # Move cursor up to beginning of previous line
|
113
|
-
print(CSI + 'A' + CSI + "1G")
|
114
|
-
end
|
115
|
-
def clear_char(n = 1) # Erase n characters from the current cursor position
|
116
|
-
print(CSI + "#{n}X")
|
117
|
-
end
|
118
|
-
def clear_line # Erase the entire current line and return to beginning of the line
|
119
|
-
print(CSI + '2K' + CSI + "1G")
|
120
|
-
end
|
121
|
-
def clear_line_before # Erase from the beginning of the line up to and including the current cursor position.
|
122
|
-
print(CSI + '1K')
|
123
|
-
end
|
124
|
-
def clear_line_after # Erase from the current position (inclusive) to the end of the line
|
125
|
-
print(CSI + '0K')
|
126
|
-
end
|
127
|
-
def clear_screen_down # Clear screen down from current row
|
128
|
-
print(CSI + 'J')
|
129
|
-
end
|
130
|
-
def scroll_up # Scroll display up one line
|
131
|
-
print(ESC + 'M')
|
132
|
-
end
|
133
|
-
def scroll_down # Scroll display down one line
|
134
|
-
print(ESC + 'D')
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
module Rinput
|
139
|
-
def getchr
|
140
|
-
# Function to process key presses
|
141
|
-
c = $stdin.getch
|
142
|
-
case c
|
143
|
-
when "\e" # ANSI escape sequences
|
144
|
-
return "ESC" if !$stdin.ready?
|
145
|
-
second_char = $stdin.getc
|
146
|
-
case second_char
|
147
|
-
when '[' # CSI
|
148
|
-
third_char = $stdin.getc
|
149
|
-
case third_char
|
150
|
-
when 'A' then chr = "UP"
|
151
|
-
when 'B' then chr = "DOWN"
|
152
|
-
when 'C' then chr = "RIGHT"
|
153
|
-
when 'D' then chr = "LEFT"
|
154
|
-
when 'Z' then chr = "S-TAB"
|
155
|
-
when '2'
|
156
|
-
fourth_char = $stdin.getc
|
157
|
-
chr = fourth_char == '~' ? "INS" : ""
|
158
|
-
when '3'
|
159
|
-
fourth_char = $stdin.getc
|
160
|
-
chr = fourth_char == '~' ? "DEL" : ""
|
161
|
-
when '5'
|
162
|
-
fourth_char = $stdin.getc
|
163
|
-
chr = fourth_char == '~' ? "PgUP" : ""
|
164
|
-
when '6'
|
165
|
-
fourth_char = $stdin.getc
|
166
|
-
chr = fourth_char == '~' ? "PgDOWN" : ""
|
167
|
-
when '1', '7'
|
168
|
-
fourth_char = $stdin.getc
|
169
|
-
chr = fourth_char == '~' ? "HOME" : ""
|
170
|
-
when '4', '8'
|
171
|
-
fourth_char = $stdin.getc
|
172
|
-
chr = fourth_char == '~' ? "END" : ""
|
173
|
-
else chr = ""
|
174
|
-
end
|
175
|
-
when 'O' # Function keys
|
176
|
-
third_char = $stdin.getc
|
177
|
-
case third_char
|
178
|
-
when 'a' then chr = "C-UP"
|
179
|
-
when 'b' then chr = "C-DOWN"
|
180
|
-
when 'c' then chr = "C-RIGHT"
|
181
|
-
when 'd' then chr = "C-LEFT"
|
182
|
-
else chr = ""
|
183
|
-
end
|
184
|
-
else
|
185
|
-
chr = ""
|
186
|
-
end
|
187
|
-
when "\r" then chr = "ENTER"
|
188
|
-
when "\t" then chr = "TAB"
|
189
|
-
when "\u007F", "\b" then chr = "BACK"
|
190
|
-
when "\u0001" then chr = "C-A"
|
191
|
-
when "\u0002" then chr = "C-B"
|
192
|
-
when "\u0003" then chr = "C-C"
|
193
|
-
when "\u0004" then chr = "C-D"
|
194
|
-
when "\u0005" then chr = "C-E"
|
195
|
-
when "\u0006" then chr = "C-F"
|
196
|
-
when "\u0007" then chr = "C-G"
|
197
|
-
when "\u000B" then chr = "C-K"
|
198
|
-
when "\u000C" then chr = "C-L"
|
199
|
-
when "\u000D" then chr = "C-M"
|
200
|
-
when "\u000E" then chr = "C-N"
|
201
|
-
when "\u000F" then chr = "C-O"
|
202
|
-
when "\u0010" then chr = "C-P"
|
203
|
-
when "\u0011" then chr = "C-Q"
|
204
|
-
when "\u0012" then chr = "C-R"
|
205
|
-
when "\u0014" then chr = "C-T"
|
206
|
-
when "\u0015" then chr = "C-U"
|
207
|
-
when "\u0016" then chr = "C-V"
|
208
|
-
when "\u0018" then chr = "C-X"
|
209
|
-
when "\u0019" then chr = "C-Y"
|
210
|
-
when "\u001A" then chr = "C-Z"
|
211
|
-
when "\u0017" then chr = "WBACK"
|
212
|
-
when "\u0013" then chr = "C-S"
|
213
|
-
when /[[:print:]]/ then chr = c
|
214
|
-
else chr = ""
|
215
|
-
end
|
216
|
-
chr
|
217
|
-
end
|
218
|
-
end
|
219
|
-
|
220
|
-
class Pane
|
221
|
-
include Rcurses::Cursor
|
222
|
-
include Rcurses::Rinput
|
223
|
-
attr_accessor :startx, :starty, :width, :height, :fg, :bg
|
224
|
-
attr_accessor :x, :y, :w, :h
|
225
|
-
attr_accessor :border, :scroll, :text, :ix, :align, :prompt
|
226
|
-
|
227
|
-
def initialize(startx = 1, starty = 1, width = 1, height = 1, fg = nil, bg = nil)
|
228
|
-
# Using Procs or Lambdas instead of eval
|
229
|
-
@startx = startx.is_a?(Proc) ? startx : -> { startx }
|
230
|
-
@starty = starty.is_a?(Proc) ? starty : -> { starty }
|
231
|
-
@width = width.is_a?(Proc) ? width : -> { width }
|
232
|
-
@height = height.is_a?(Proc) ? height : -> { height }
|
233
|
-
@fg, @bg = fg, bg
|
234
|
-
@text = "" # Initialize text variable
|
235
|
-
@align = "l" # Default alignment
|
236
|
-
@scroll = true # Initialize scroll indicators to true
|
237
|
-
@prompt = "" # Initialize prompt for editline
|
238
|
-
@ix = 0 # Text index (starting text line in pane)
|
239
|
-
@max_h, @max_w = IO.console.winsize
|
240
|
-
end
|
241
|
-
|
242
|
-
def move(x, y)
|
243
|
-
@startx = -> { @x + x }
|
244
|
-
@starty = -> { @y + y }
|
245
|
-
refresh
|
246
|
-
end
|
247
|
-
|
248
|
-
def refresh(cont = @text)
|
249
|
-
@max_h, @max_w = IO.console.winsize
|
250
|
-
|
251
|
-
# Define the core of the ANSI escape sequence handling
|
252
|
-
def split_line_with_ansi(line, w)
|
253
|
-
# Define opening and closing sequences
|
254
|
-
open_sequences = {
|
255
|
-
"\e[1m" => "\e[22m",
|
256
|
-
"\e[3m" => "\e[23m",
|
257
|
-
"\e[4m" => "\e[24m",
|
258
|
-
"\e[5m" => "\e[25m",
|
259
|
-
"\e[7m" => "\e[27m" }
|
260
|
-
# All known closing sequences
|
261
|
-
close_sequences = open_sequences.values + ["\e[0m"]
|
262
|
-
# Regex to match ANSI escape sequences
|
263
|
-
ansi_regex = /\e\[[0-9;]*m/
|
264
|
-
result = []
|
265
|
-
# Tokenize the line into ANSI sequences and plain text
|
266
|
-
tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
|
267
|
-
current_line = ''
|
268
|
-
current_line_length = 0
|
269
|
-
active_sequences = []
|
270
|
-
tokens.each do |token|
|
271
|
-
if token.match?(ansi_regex)
|
272
|
-
# It's an ANSI sequence
|
273
|
-
current_line << token
|
274
|
-
if close_sequences.include?(token)
|
275
|
-
# It's a closing sequence
|
276
|
-
if token == "\e[0m"
|
277
|
-
# Reset all sequences
|
278
|
-
active_sequences.clear
|
279
|
-
else
|
280
|
-
# Remove the corresponding opening sequence
|
281
|
-
corresponding_open = open_sequences.key(token)
|
282
|
-
active_sequences.delete(corresponding_open)
|
283
|
-
end
|
284
|
-
else
|
285
|
-
# It's an opening sequence (or any other ANSI sequence)
|
286
|
-
active_sequences << token
|
287
|
-
end
|
288
|
-
else
|
289
|
-
# It's plain text, split into words and spaces
|
290
|
-
words = token.scan(/\S+\s*/)
|
291
|
-
words.each do |word|
|
292
|
-
word_length = word.gsub(ansi_regex, '').length
|
293
|
-
if current_line_length + word_length <= w
|
294
|
-
# Append word to current line
|
295
|
-
current_line << word
|
296
|
-
current_line_length += word_length
|
297
|
-
else
|
298
|
-
# Word doesn't fit in the current line
|
299
|
-
if current_line_length > 0
|
300
|
-
# Finish the current line and start a new one
|
301
|
-
result << current_line
|
302
|
-
# Start new line with active ANSI sequences
|
303
|
-
current_line = active_sequences.join
|
304
|
-
current_line_length = 0
|
305
|
-
end
|
306
|
-
# Handle long words that might need splitting
|
307
|
-
while word_length > w
|
308
|
-
# Split the word
|
309
|
-
part = word[0, w]
|
310
|
-
current_line << part
|
311
|
-
result << current_line
|
312
|
-
# Update word and lengths
|
313
|
-
word = word[w..-1]
|
314
|
-
word_length = word.gsub(ansi_regex, '').length
|
315
|
-
# Start new line
|
316
|
-
current_line = active_sequences.join
|
317
|
-
current_line_length = 0
|
318
|
-
end
|
319
|
-
# Append any remaining part of the word
|
320
|
-
if word_length > 0
|
321
|
-
current_line << word
|
322
|
-
current_line_length += word_length
|
323
|
-
end
|
324
|
-
end
|
325
|
-
end
|
326
|
-
end
|
327
|
-
end
|
328
|
-
# Append any remaining text in the current line
|
329
|
-
result << current_line unless current_line.empty?
|
330
|
-
result
|
331
|
-
end
|
332
|
-
|
333
|
-
# Define the main textformat function
|
334
|
-
def textformat(cont)
|
335
|
-
# Split the content by '\n'
|
336
|
-
lines = cont.split("\n")
|
337
|
-
result = []
|
338
|
-
lines.each do |line|
|
339
|
-
split_lines = split_line_with_ansi(line, @w)
|
340
|
-
result.concat(split_lines)
|
341
|
-
end
|
342
|
-
result
|
343
|
-
end
|
344
|
-
|
345
|
-
# Start the actual refresh
|
346
|
-
o_row, o_col = pos
|
347
|
-
@x = @startx.call
|
348
|
-
@y = @starty.call
|
349
|
-
@w = @width.call
|
350
|
-
@h = @height.call
|
351
|
-
|
352
|
-
# Adjust pane dimensions and positions
|
353
|
-
if @border # Keep panes inside screen
|
354
|
-
@w = @max_w - 2 if @w > @max_w - 2
|
355
|
-
@h = @max_h - 2 if @h > @max_h - 2
|
356
|
-
@x = 2 if @x < 2; @x = @max_w - @w if @x + @w > @max_w
|
357
|
-
@y = 2 if @y < 2; @y = @max_h - @h if @y + @h > @max_h
|
358
|
-
else
|
359
|
-
@w = @max_w if @w > @max_w
|
360
|
-
@h = @max_h if @h > @max_h
|
361
|
-
@x = 1 if @x < 1; @x = @max_w - @w + 1 if @x + @w > @max_w + 1
|
362
|
-
@y = 1 if @y < 1; @y = @max_h - @h + 1 if @y + @h > @max_h + 1
|
363
|
-
end
|
364
|
-
|
365
|
-
col(@x); row(@y) # Cursor to start of pane
|
366
|
-
fmt = [@fg, @bg].compact.join(',') # Format for printing in pane (fg,bg)
|
367
|
-
@txt = cont.split("\n") # Split content into array
|
368
|
-
@txt = textformat(cont) if @txt.any? { |line| line.pure.length >= @w } # Reformat lines if necessary
|
369
|
-
@ix = @txt.length - 1 if @ix > @txt.length - 1; @ix = 0 if @ix < 0 # Ensure no out-of-bounds
|
370
|
-
|
371
|
-
@h.times do |i| # Print pane content
|
372
|
-
l = @ix + i # The current line to be printed
|
373
|
-
if @txt[l].to_s != "" # Print the text line
|
374
|
-
# Get padding width and half width
|
375
|
-
pl = @w - @txt[l].pure.length
|
376
|
-
pl = 0 if pl < 0
|
377
|
-
hl = pl / 2
|
378
|
-
case @align
|
379
|
-
when "l"
|
380
|
-
print @txt[l].c(fmt) + " ".c(fmt) * pl
|
381
|
-
when "r"
|
382
|
-
print " ".c(fmt) * pl + @txt[l].c(fmt)
|
383
|
-
when "c"
|
384
|
-
print " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
|
385
|
-
end
|
386
|
-
else
|
387
|
-
print " ".c(fmt) * @w
|
388
|
-
end
|
389
|
-
col(@x) # Cursor to start of pane
|
390
|
-
row(@y + i + 1)
|
391
|
-
end
|
392
|
-
|
393
|
-
if @ix > 0 and @scroll # Print "more" marker at top
|
394
|
-
col(@x + @w - 1); row(@y)
|
395
|
-
print "▲".c(fmt)
|
396
|
-
end
|
397
|
-
|
398
|
-
if @txt.length - @ix > @h and @scroll # Print bottom "more" marker
|
399
|
-
col(@x + @w - 1); row(@y + @h - 1)
|
400
|
-
print "▼".c(fmt)
|
401
|
-
end
|
402
|
-
|
403
|
-
if @border # Print border if @border is set to true
|
404
|
-
row(@y - 1); col(@x - 1)
|
405
|
-
print ("┌" + "─" * @w + "┐").c(fmt)
|
406
|
-
@h.times do |i|
|
407
|
-
row(@y + i); col(@x - 1)
|
408
|
-
print "│".c(fmt)
|
409
|
-
col(@x + @w)
|
410
|
-
print "│".c(fmt)
|
411
|
-
end
|
412
|
-
row(@y + @h); col(@x - 1)
|
413
|
-
print ("└" + "─" * @w + "┘").c(fmt)
|
414
|
-
end
|
415
|
-
|
416
|
-
row(o_row)
|
417
|
-
col(o_col)
|
418
|
-
@txt
|
419
|
-
end
|
420
|
-
|
421
|
-
def textformat(cont)
|
422
|
-
# Split the content by '\n'
|
423
|
-
lines = cont.split("\n")
|
424
|
-
result = []
|
425
|
-
lines.each do |line|
|
426
|
-
split_lines = split_line_with_ansi(line, @w)
|
427
|
-
result.concat(split_lines)
|
428
|
-
end
|
429
|
-
result
|
430
|
-
end
|
431
|
-
|
432
|
-
def right
|
433
|
-
if @pos < @txt[@ix + @line].length
|
434
|
-
@pos += 1
|
435
|
-
if @pos == @w
|
436
|
-
@pos = 0
|
437
|
-
if @line == @h - 1
|
438
|
-
@ix += 1
|
439
|
-
else
|
440
|
-
@line += 1
|
441
|
-
end
|
442
|
-
end
|
443
|
-
else
|
444
|
-
if @line == @h - 1
|
445
|
-
@ix += 1 unless @ix >= @txt.length - @h
|
446
|
-
@pos = 0
|
447
|
-
elsif @line + @ix + 1 < @txt.length
|
448
|
-
@line += 1
|
449
|
-
@pos = 0
|
450
|
-
end
|
451
|
-
end
|
452
|
-
end
|
453
|
-
def left
|
454
|
-
if @pos == 0
|
455
|
-
if @line == 0
|
456
|
-
unless @ix == 0
|
457
|
-
@ix -= 1
|
458
|
-
@pos = @txt[@ix + @line].length
|
459
|
-
end
|
460
|
-
else
|
461
|
-
@line -= 1
|
462
|
-
@pos = @txt[@ix + @line].length
|
463
|
-
end
|
464
|
-
else
|
465
|
-
@pos -= 1
|
466
|
-
end
|
467
|
-
end
|
468
|
-
def up
|
469
|
-
if @line == 0
|
470
|
-
@ix -= 1 unless @ix == 0
|
471
|
-
else
|
472
|
-
@line -= 1
|
473
|
-
end
|
474
|
-
begin
|
475
|
-
@pos = [@pos, @txt[@ix + @line].length].min
|
476
|
-
rescue
|
477
|
-
end
|
478
|
-
end
|
479
|
-
def down
|
480
|
-
if @line == @h - 1
|
481
|
-
@ix += 1 unless @ix + @line >= @txt.length - 1
|
482
|
-
elsif @line + @ix + 1 < @txt.length
|
483
|
-
@line += 1
|
484
|
-
end
|
485
|
-
begin
|
486
|
-
@pos = [@pos, @txt[@ix + @line].length].min
|
487
|
-
rescue
|
488
|
-
end
|
489
|
-
end
|
490
|
-
|
491
|
-
def parse(cont)
|
492
|
-
cont.gsub!(/\*(.+?)\*/, '\1'.b)
|
493
|
-
cont.gsub!(/\/(.+?)\//, '\1'.i)
|
494
|
-
cont.gsub!(/_(.+?)_/, '\1'.u)
|
495
|
-
cont.gsub!(/#(.+?)#/, '\1'.r)
|
496
|
-
cont.gsub!(/<([^|]+)\|([^>]+)>/) do
|
497
|
-
text = $2; codes = $1
|
498
|
-
text.c(codes)
|
499
|
-
end
|
500
|
-
cont
|
501
|
-
end
|
502
|
-
|
503
|
-
def edit
|
504
|
-
begin
|
505
|
-
# Switch to raw mode without echoing input
|
506
|
-
STDIN.raw!
|
507
|
-
# Prepare content for editing, replacing newlines with a placeholder
|
508
|
-
content = @text.pure.gsub("\n", "¬\n")
|
509
|
-
# Initialize cursor position and indices
|
510
|
-
@ix = 0 # Starting index of text lines displayed in the pane
|
511
|
-
@line = 0 # Current line number relative to the pane's visible area
|
512
|
-
@pos = 0 # Position within the current line (character index)
|
513
|
-
@txt = refresh(content)
|
514
|
-
input_char = ''
|
515
|
-
|
516
|
-
while input_char != 'ESC' # Continue until ESC is pressed
|
517
|
-
row(@y + @line) # Move cursor to the correct row
|
518
|
-
col(@x + @pos) # Move cursor to the correct column
|
519
|
-
|
520
|
-
input_char = getchr # Read user input
|
521
|
-
case input_char
|
522
|
-
when 'C-L' # Left justify
|
523
|
-
@align = 'l'
|
524
|
-
when 'C-R' # Right justify
|
525
|
-
@align = 'r'
|
526
|
-
when 'C-C' # Center justify
|
527
|
-
@align = 'c'
|
528
|
-
when 'C-Y' # Copy pane content to clipboard
|
529
|
-
Clipboard.copy(@text.pure)
|
530
|
-
when 'C-S' # Save edited text back to @text and exit
|
531
|
-
content = content.gsub('¬', "\n")
|
532
|
-
content = parse(content)
|
533
|
-
@text = content
|
534
|
-
input_char = 'ESC'
|
535
|
-
when 'DEL' # Delete character at current position
|
536
|
-
posx = calculate_posx
|
537
|
-
content.slice!(posx)
|
538
|
-
when 'BACK' # Backspace (delete character before current position)
|
539
|
-
if @pos > 0
|
540
|
-
left
|
541
|
-
posx = calculate_posx
|
542
|
-
content.slice!(posx)
|
543
|
-
end
|
544
|
-
when 'WBACK' # Word backspace
|
545
|
-
while @pos > 0 && content[calculate_posx - 1] != ' '
|
546
|
-
left
|
547
|
-
posx = calculate_posx
|
548
|
-
content.slice!(posx)
|
549
|
-
end
|
550
|
-
when 'C-K' # Kill line (delete from cursor to end of line)
|
551
|
-
line_start_pos = calculate_line_start_pos
|
552
|
-
line_length = @txt[@ix + @line]&.length || 0
|
553
|
-
content.slice!(line_start_pos + @pos, line_length - @pos)
|
554
|
-
when 'UP' # Move cursor up one line
|
555
|
-
up
|
556
|
-
when 'DOWN' # Move cursor down one line
|
557
|
-
down
|
558
|
-
when 'RIGHT' # Move cursor right one character
|
559
|
-
right
|
560
|
-
when 'LEFT' # Move cursor left one character
|
561
|
-
left
|
562
|
-
when 'HOME' # Move to start of line
|
563
|
-
@pos = 0
|
564
|
-
when 'END' # Move to end of line
|
565
|
-
current_line_length = @txt[@ix + @line]&.length || 0
|
566
|
-
@pos = current_line_length
|
567
|
-
when 'C-HOME' # Move to start of pane
|
568
|
-
@ix = 0
|
569
|
-
@line = 0
|
570
|
-
@pos = 0
|
571
|
-
when 'C-END' # Move to end of pane
|
572
|
-
total_lines = @txt.length
|
573
|
-
@ix = [total_lines - @h, 0].max
|
574
|
-
@line = [@h - 1, total_lines - @ix - 1].min
|
575
|
-
current_line_length = @txt[@ix + @line]&.length || 0
|
576
|
-
@pos = current_line_length
|
577
|
-
when 'ENTER' # Insert newline at current position
|
578
|
-
posx = calculate_posx
|
579
|
-
content.insert(posx, "¬\n")
|
580
|
-
right
|
581
|
-
when /^.$/ # Insert character at current position
|
582
|
-
posx = calculate_posx
|
583
|
-
content.insert(posx, input_char)
|
584
|
-
right
|
585
|
-
else
|
586
|
-
# Handle unrecognized input if necessary
|
587
|
-
end
|
588
|
-
|
589
|
-
# Handle pasted input (additional characters in the buffer)
|
590
|
-
while IO.select([$stdin], nil, nil, 0)
|
591
|
-
input_char = $stdin.read_nonblock(1) rescue nil
|
592
|
-
break unless input_char
|
593
|
-
posx = calculate_posx
|
594
|
-
content.insert(posx, input_char)
|
595
|
-
right
|
596
|
-
end
|
597
|
-
|
598
|
-
@txt = refresh(content) # Refresh the pane with the current content
|
599
|
-
end
|
600
|
-
ensure
|
601
|
-
# Restore terminal mode
|
602
|
-
STDIN.cooked!
|
603
|
-
end
|
604
|
-
end
|
605
|
-
|
606
|
-
def editline
|
607
|
-
begin
|
608
|
-
# Switch to raw mode without echo
|
609
|
-
STDIN.raw!
|
610
|
-
|
611
|
-
# Initialize position and dimensions
|
612
|
-
@x = @startx.call
|
613
|
-
@y = @starty.call
|
614
|
-
@w = @width.call
|
615
|
-
@h = @height.call
|
616
|
-
# Ensure pane is within screen bounds
|
617
|
-
@x = [[@x, 1].max, @max_w - @w + 1].min
|
618
|
-
@y = [[@y, 1].max, @max_h - @h + 1].min
|
619
|
-
|
620
|
-
@scroll = false
|
621
|
-
row(@y)
|
622
|
-
|
623
|
-
fmt = [@fg, @bg].compact.join(',')
|
624
|
-
col(@x)
|
625
|
-
print @prompt.c(fmt) # Print prompt at the pane's starting position
|
626
|
-
|
627
|
-
prompt_len = @prompt.pure.length
|
628
|
-
content_len = @w - prompt_len
|
629
|
-
cont = @text.pure.slice(0, content_len)
|
630
|
-
@pos = cont.length # Set initial cursor position at the end of content
|
631
|
-
chr = ''
|
632
|
-
|
633
|
-
while chr != 'ESC' # Continue until ESC is pressed
|
634
|
-
col(@x + prompt_len) # Set cursor at start of content
|
635
|
-
cont = cont.slice(0, content_len) # Trim content to max length
|
636
|
-
print cont.ljust(content_len).c(fmt) # Print content, left-justified
|
637
|
-
col(@x + prompt_len + @pos) # Set cursor to current position
|
638
|
-
|
639
|
-
chr = getchr # Read user input
|
640
|
-
case chr
|
641
|
-
when 'LEFT'
|
642
|
-
@pos -= 1 if @pos > 0
|
643
|
-
when 'RIGHT'
|
644
|
-
@pos += 1 if @pos < cont.length
|
645
|
-
when 'HOME'
|
646
|
-
@pos = 0
|
647
|
-
when 'END'
|
648
|
-
@pos = cont.length
|
649
|
-
when 'DEL'
|
650
|
-
cont[@pos] = '' if @pos < cont.length
|
651
|
-
when 'BACK'
|
652
|
-
if @pos > 0
|
653
|
-
@pos -= 1
|
654
|
-
cont[@pos] = ''
|
655
|
-
end
|
656
|
-
when 'WBACK'
|
657
|
-
while @pos > 0 && cont[@pos - 1] != ' '
|
658
|
-
@pos -= 1
|
659
|
-
cont[@pos] = ''
|
660
|
-
end
|
661
|
-
when 'C-K'
|
662
|
-
cont = ''
|
663
|
-
@pos = 0
|
664
|
-
when 'ENTER'
|
665
|
-
@text = parse(cont)
|
666
|
-
chr = 'ESC'
|
667
|
-
when /^.$/
|
668
|
-
if @pos < content_len
|
669
|
-
cont.insert(@pos, chr)
|
670
|
-
@pos += 1
|
671
|
-
end
|
672
|
-
end
|
673
|
-
|
674
|
-
# Handle pasted input
|
675
|
-
while IO.select([$stdin], nil, nil, 0)
|
676
|
-
chr = $stdin.read_nonblock(1) rescue nil
|
677
|
-
break unless chr
|
678
|
-
if @pos < content_len
|
679
|
-
cont.insert(@pos, chr)
|
680
|
-
@pos += 1
|
681
|
-
end
|
682
|
-
end
|
683
|
-
end
|
684
|
-
ensure
|
685
|
-
# Restore terminal mode
|
686
|
-
STDIN.cooked!
|
687
|
-
end
|
688
|
-
end
|
689
|
-
|
690
|
-
private
|
691
|
-
|
692
|
-
# Calculates the position in the content string corresponding to the current cursor position
|
693
|
-
def calculate_posx
|
694
|
-
total_length = 0
|
695
|
-
(@ix + @line).times do |i|
|
696
|
-
total_length += @txt[i].pure.length + 1 # +1 for the newline character
|
697
|
-
end
|
698
|
-
total_length += @pos
|
699
|
-
total_length
|
700
|
-
end
|
701
|
-
# Calculates the starting position of the current line in the content string
|
702
|
-
def calculate_line_start_pos
|
703
|
-
total_length = 0
|
704
|
-
(@ix + @line).times do |i|
|
705
|
-
total_length += @txt[i].pure.length + 1 # +1 for the newline character
|
706
|
-
end
|
707
|
-
total_length
|
708
|
-
end
|
709
|
-
end
|
710
|
-
end
|
13
|
+
require_relative 'string_extensions'
|
14
|
+
require_relative 'rcurses/cursor'
|
15
|
+
require_relative 'rcurses/input'
|
16
|
+
require_relative 'rcurses/pane'
|
711
17
|
|
712
18
|
# vim: set sw=2 sts=2 et filetype=ruby fdm=syntax fdn=2 fcs=fold\:\ :
|