rcurses 2.0 → 2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,494 @@
1
+ module Rcurses
2
+ class Pane
3
+ require 'clipboard' # Ensure the 'clipboard' gem is installed
4
+ include Cursor
5
+ include Input
6
+ attr_accessor :startx, :starty, :width, :height, :fg, :bg
7
+ attr_accessor :x, :y, :w, :h
8
+ attr_accessor :border, :scroll, :text, :ix, :align, :prompt
9
+
10
+ def initialize(startx = 1, starty = 1, width = 1, height = 1, fg = nil, bg = nil)
11
+ # Using Procs or Lambdas instead of eval
12
+ @startx = startx.is_a?(Proc) ? startx : -> { startx }
13
+ @starty = starty.is_a?(Proc) ? starty : -> { starty }
14
+ @width = width.is_a?(Proc) ? width : -> { width }
15
+ @height = height.is_a?(Proc) ? height : -> { height }
16
+ @fg, @bg = fg, bg
17
+ @text = "" # Initialize text variable
18
+ @align = "l" # Default alignment
19
+ @scroll = true # Initialize scroll indicators to true
20
+ @prompt = "" # Initialize prompt for editline
21
+ @ix = 0 # Text index (starting text line in pane)
22
+ @max_h, @max_w = IO.console.winsize
23
+ end
24
+
25
+ def move(x, y)
26
+ @startx = -> { @x + x }
27
+ @starty = -> { @y + y }
28
+ refresh
29
+ end
30
+
31
+ def refresh(cont = @text)
32
+ @max_h, @max_w = IO.console.winsize
33
+
34
+ # Define the core of the ANSI escape sequence handling
35
+ def split_line_with_ansi(line, w)
36
+ # Define opening and closing sequences
37
+ open_sequences = {
38
+ "\e[1m" => "\e[22m",
39
+ "\e[3m" => "\e[23m",
40
+ "\e[4m" => "\e[24m",
41
+ "\e[5m" => "\e[25m",
42
+ "\e[7m" => "\e[27m" }
43
+ # All known closing sequences
44
+ close_sequences = open_sequences.values + ["\e[0m"]
45
+ # Regex to match ANSI escape sequences
46
+ ansi_regex = /\e\[[0-9;]*m/
47
+ result = []
48
+ # Tokenize the line into ANSI sequences and plain text
49
+ tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
50
+ current_line = ''
51
+ current_line_length = 0
52
+ active_sequences = []
53
+ tokens.each do |token|
54
+ if token.match?(ansi_regex)
55
+ # It's an ANSI sequence
56
+ current_line << token
57
+ if close_sequences.include?(token)
58
+ # It's a closing sequence
59
+ if token == "\e[0m"
60
+ # Reset all sequences
61
+ active_sequences.clear
62
+ else
63
+ # Remove the corresponding opening sequence
64
+ corresponding_open = open_sequences.key(token)
65
+ active_sequences.delete(corresponding_open)
66
+ end
67
+ else
68
+ # It's an opening sequence (or any other ANSI sequence)
69
+ active_sequences << token
70
+ end
71
+ else
72
+ # It's plain text, split into words and spaces
73
+ words = token.scan(/\S+\s*/)
74
+ words.each do |word|
75
+ word_length = word.gsub(ansi_regex, '').length
76
+ if current_line_length + word_length <= w
77
+ # Append word to current line
78
+ current_line << word
79
+ current_line_length += word_length
80
+ else
81
+ # Word doesn't fit in the current line
82
+ if current_line_length > 0
83
+ # Finish the current line and start a new one
84
+ result << current_line
85
+ # Start new line with active ANSI sequences
86
+ current_line = active_sequences.join
87
+ current_line_length = 0
88
+ end
89
+ # Handle long words that might need splitting
90
+ while word_length > w
91
+ # Split the word
92
+ part = word[0, w]
93
+ current_line << part
94
+ result << current_line
95
+ # Update word and lengths
96
+ word = word[w..-1]
97
+ word_length = word.gsub(ansi_regex, '').length
98
+ # Start new line
99
+ current_line = active_sequences.join
100
+ current_line_length = 0
101
+ end
102
+ # Append any remaining part of the word
103
+ if word_length > 0
104
+ current_line << word
105
+ current_line_length += word_length
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ # Append any remaining text in the current line
112
+ result << current_line unless current_line.empty?
113
+ result
114
+ end
115
+
116
+ # Define the main textformat function
117
+ def textformat(cont)
118
+ # Split the content by '\n'
119
+ lines = cont.split("\n")
120
+ result = []
121
+ lines.each do |line|
122
+ split_lines = split_line_with_ansi(line, @w)
123
+ result.concat(split_lines)
124
+ end
125
+ result
126
+ end
127
+
128
+ # Start the actual refresh
129
+ o_row, o_col = pos
130
+ @x = @startx.call
131
+ @y = @starty.call
132
+ @w = @width.call
133
+ @h = @height.call
134
+
135
+ # Adjust pane dimensions and positions
136
+ if @border # Keep panes inside screen
137
+ @w = @max_w - 2 if @w > @max_w - 2
138
+ @h = @max_h - 2 if @h > @max_h - 2
139
+ @x = 2 if @x < 2; @x = @max_w - @w if @x + @w > @max_w
140
+ @y = 2 if @y < 2; @y = @max_h - @h if @y + @h > @max_h
141
+ else
142
+ @w = @max_w if @w > @max_w
143
+ @h = @max_h if @h > @max_h
144
+ @x = 1 if @x < 1; @x = @max_w - @w + 1 if @x + @w > @max_w + 1
145
+ @y = 1 if @y < 1; @y = @max_h - @h + 1 if @y + @h > @max_h + 1
146
+ end
147
+
148
+ col(@x); row(@y) # Cursor to start of pane
149
+ fmt = [@fg, @bg].compact.join(',') # Format for printing in pane (fg,bg)
150
+ @txt = cont.split("\n") # Split content into array
151
+ @txt = textformat(cont) if @txt.any? { |line| line.pure.length >= @w } # Reformat lines if necessary
152
+ @ix = @txt.length - 1 if @ix > @txt.length - 1; @ix = 0 if @ix < 0 # Ensure no out-of-bounds
153
+
154
+ @h.times do |i| # Print pane content
155
+ l = @ix + i # The current line to be printed
156
+ if @txt[l].to_s != "" # Print the text line
157
+ # Get padding width and half width
158
+ pl = @w - @txt[l].pure.length
159
+ pl = 0 if pl < 0
160
+ hl = pl / 2
161
+ case @align
162
+ when "l"
163
+ print @txt[l].c(fmt) + " ".c(fmt) * pl
164
+ when "r"
165
+ print " ".c(fmt) * pl + @txt[l].c(fmt)
166
+ when "c"
167
+ print " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
168
+ end
169
+ else
170
+ print " ".c(fmt) * @w
171
+ end
172
+ col(@x) # Cursor to start of pane
173
+ row(@y + i + 1)
174
+ end
175
+
176
+ if @ix > 0 and @scroll # Print "more" marker at top
177
+ col(@x + @w - 1); row(@y)
178
+ print "▲".c(fmt)
179
+ end
180
+
181
+ if @txt.length - @ix > @h and @scroll # Print bottom "more" marker
182
+ col(@x + @w - 1); row(@y + @h - 1)
183
+ print "▼".c(fmt)
184
+ end
185
+
186
+ if @border # Print border if @border is set to true
187
+ row(@y - 1); col(@x - 1)
188
+ print ("┌" + "─" * @w + "┐").c(fmt)
189
+ @h.times do |i|
190
+ row(@y + i); col(@x - 1)
191
+ print "│".c(fmt)
192
+ col(@x + @w)
193
+ print "│".c(fmt)
194
+ end
195
+ row(@y + @h); col(@x - 1)
196
+ print ("└" + "─" * @w + "┘").c(fmt)
197
+ end
198
+
199
+ row(o_row)
200
+ col(o_col)
201
+ @txt
202
+ end
203
+
204
+ def textformat(cont)
205
+ # Split the content by '\n'
206
+ lines = cont.split("\n")
207
+ result = []
208
+ lines.each do |line|
209
+ split_lines = split_line_with_ansi(line, @w)
210
+ result.concat(split_lines)
211
+ end
212
+ result
213
+ end
214
+
215
+ def right
216
+ if @pos < @txt[@ix + @line].length
217
+ @pos += 1
218
+ if @pos == @w
219
+ @pos = 0
220
+ if @line == @h - 1
221
+ @ix += 1
222
+ else
223
+ @line += 1
224
+ end
225
+ end
226
+ else
227
+ if @line == @h - 1
228
+ @ix += 1 unless @ix >= @txt.length - @h
229
+ @pos = 0
230
+ elsif @line + @ix + 1 < @txt.length
231
+ @line += 1
232
+ @pos = 0
233
+ end
234
+ end
235
+ end
236
+ def left
237
+ if @pos == 0
238
+ if @line == 0
239
+ unless @ix == 0
240
+ @ix -= 1
241
+ @pos = @txt[@ix + @line].length
242
+ end
243
+ else
244
+ @line -= 1
245
+ @pos = @txt[@ix + @line].length
246
+ end
247
+ else
248
+ @pos -= 1
249
+ end
250
+ end
251
+ def up
252
+ if @line == 0
253
+ @ix -= 1 unless @ix == 0
254
+ else
255
+ @line -= 1
256
+ end
257
+ begin
258
+ @pos = [@pos, @txt[@ix + @line].length].min
259
+ rescue
260
+ end
261
+ end
262
+ def down
263
+ if @line == @h - 1
264
+ @ix += 1 unless @ix + @line >= @txt.length - 1
265
+ elsif @line + @ix + 1 < @txt.length
266
+ @line += 1
267
+ end
268
+ begin
269
+ @pos = [@pos, @txt[@ix + @line].length].min
270
+ rescue
271
+ end
272
+ end
273
+
274
+ def parse(cont)
275
+ cont.gsub!(/\*(.+?)\*/, '\1'.b)
276
+ cont.gsub!(/\/(.+?)\//, '\1'.i)
277
+ cont.gsub!(/_(.+?)_/, '\1'.u)
278
+ cont.gsub!(/#(.+?)#/, '\1'.r)
279
+ cont.gsub!(/<([^|]+)\|([^>]+)>/) do
280
+ text = $2; codes = $1
281
+ text.c(codes)
282
+ end
283
+ cont
284
+ end
285
+
286
+ def edit
287
+ begin
288
+ # Switch to raw mode without echoing input
289
+ STDIN.raw!
290
+ # Prepare content for editing, replacing newlines with a placeholder
291
+ content = @text.pure.gsub("\n", "¬\n")
292
+ # Initialize cursor position and indices
293
+ @ix = 0 # Starting index of text lines displayed in the pane
294
+ @line = 0 # Current line number relative to the pane's visible area
295
+ @pos = 0 # Position within the current line (character index)
296
+ @txt = refresh(content)
297
+ input_char = ''
298
+
299
+ while input_char != 'ESC' # Continue until ESC is pressed
300
+ row(@y + @line) # Move cursor to the correct row
301
+ col(@x + @pos) # Move cursor to the correct column
302
+
303
+ input_char = getchr # Read user input
304
+ case input_char
305
+ when 'C-L' # Left justify
306
+ @align = 'l'
307
+ when 'C-R' # Right justify
308
+ @align = 'r'
309
+ when 'C-C' # Center justify
310
+ @align = 'c'
311
+ when 'C-Y' # Copy pane content to clipboard
312
+ Clipboard.copy(@text.pure)
313
+ when 'C-S' # Save edited text back to @text and exit
314
+ content = content.gsub('¬', "\n")
315
+ content = parse(content)
316
+ @text = content
317
+ input_char = 'ESC'
318
+ when 'DEL' # Delete character at current position
319
+ posx = calculate_posx
320
+ content.slice!(posx)
321
+ when 'BACK' # Backspace (delete character before current position)
322
+ if @pos > 0
323
+ left
324
+ posx = calculate_posx
325
+ content.slice!(posx)
326
+ end
327
+ when 'WBACK' # Word backspace
328
+ while @pos > 0 && content[calculate_posx - 1] != ' '
329
+ left
330
+ posx = calculate_posx
331
+ content.slice!(posx)
332
+ end
333
+ when 'C-K' # Kill line (delete from cursor to end of line)
334
+ line_start_pos = calculate_line_start_pos
335
+ line_length = @txt[@ix + @line]&.length || 0
336
+ content.slice!(line_start_pos + @pos, line_length - @pos)
337
+ when 'UP' # Move cursor up one line
338
+ up
339
+ when 'DOWN' # Move cursor down one line
340
+ down
341
+ when 'RIGHT' # Move cursor right one character
342
+ right
343
+ when 'LEFT' # Move cursor left one character
344
+ left
345
+ when 'HOME' # Move to start of line
346
+ @pos = 0
347
+ when 'END' # Move to end of line
348
+ current_line_length = @txt[@ix + @line]&.length || 0
349
+ @pos = current_line_length
350
+ when 'C-HOME' # Move to start of pane
351
+ @ix = 0
352
+ @line = 0
353
+ @pos = 0
354
+ when 'C-END' # Move to end of pane
355
+ total_lines = @txt.length
356
+ @ix = [total_lines - @h, 0].max
357
+ @line = [@h - 1, total_lines - @ix - 1].min
358
+ current_line_length = @txt[@ix + @line]&.length || 0
359
+ @pos = current_line_length
360
+ when 'ENTER' # Insert newline at current position
361
+ posx = calculate_posx
362
+ content.insert(posx, "¬\n")
363
+ right
364
+ when /^.$/ # Insert character at current position
365
+ posx = calculate_posx
366
+ content.insert(posx, input_char)
367
+ right
368
+ else
369
+ # Handle unrecognized input if necessary
370
+ end
371
+
372
+ # Handle pasted input (additional characters in the buffer)
373
+ while IO.select([$stdin], nil, nil, 0)
374
+ input_char = $stdin.read_nonblock(1) rescue nil
375
+ break unless input_char
376
+ posx = calculate_posx
377
+ content.insert(posx, input_char)
378
+ right
379
+ end
380
+
381
+ @txt = refresh(content) # Refresh the pane with the current content
382
+ end
383
+ ensure
384
+ # Restore terminal mode
385
+ STDIN.cooked!
386
+ end
387
+ end
388
+
389
+ def editline
390
+ begin
391
+ # Switch to raw mode without echo
392
+ STDIN.raw!
393
+
394
+ # Initialize position and dimensions
395
+ @x = @startx.call
396
+ @y = @starty.call
397
+ @w = @width.call
398
+ @h = @height.call
399
+ # Ensure pane is within screen bounds
400
+ @x = [[@x, 1].max, @max_w - @w + 1].min
401
+ @y = [[@y, 1].max, @max_h - @h + 1].min
402
+
403
+ @scroll = false
404
+ row(@y)
405
+
406
+ fmt = [@fg, @bg].compact.join(',')
407
+ col(@x)
408
+ print @prompt.c(fmt) # Print prompt at the pane's starting position
409
+
410
+ prompt_len = @prompt.pure.length
411
+ content_len = @w - prompt_len
412
+ cont = @text.pure.slice(0, content_len)
413
+ @pos = cont.length # Set initial cursor position at the end of content
414
+ chr = ''
415
+
416
+ while chr != 'ESC' # Continue until ESC is pressed
417
+ col(@x + prompt_len) # Set cursor at start of content
418
+ cont = cont.slice(0, content_len) # Trim content to max length
419
+ print cont.ljust(content_len).c(fmt) # Print content, left-justified
420
+ col(@x + prompt_len + @pos) # Set cursor to current position
421
+
422
+ chr = getchr # Read user input
423
+ case chr
424
+ when 'LEFT'
425
+ @pos -= 1 if @pos > 0
426
+ when 'RIGHT'
427
+ @pos += 1 if @pos < cont.length
428
+ when 'HOME'
429
+ @pos = 0
430
+ when 'END'
431
+ @pos = cont.length
432
+ when 'DEL'
433
+ cont[@pos] = '' if @pos < cont.length
434
+ when 'BACK'
435
+ if @pos > 0
436
+ @pos -= 1
437
+ cont[@pos] = ''
438
+ end
439
+ when 'WBACK'
440
+ while @pos > 0 && cont[@pos - 1] != ' '
441
+ @pos -= 1
442
+ cont[@pos] = ''
443
+ end
444
+ when 'C-K'
445
+ cont = ''
446
+ @pos = 0
447
+ when 'ENTER'
448
+ @text = parse(cont)
449
+ chr = 'ESC'
450
+ when /^.$/
451
+ if @pos < content_len
452
+ cont.insert(@pos, chr)
453
+ @pos += 1
454
+ end
455
+ end
456
+
457
+ # Handle pasted input
458
+ while IO.select([$stdin], nil, nil, 0)
459
+ chr = $stdin.read_nonblock(1) rescue nil
460
+ break unless chr
461
+ if @pos < content_len
462
+ cont.insert(@pos, chr)
463
+ @pos += 1
464
+ end
465
+ end
466
+ end
467
+ ensure
468
+ # Restore terminal mode
469
+ STDIN.cooked!
470
+ end
471
+ end
472
+
473
+ private
474
+
475
+ # Calculates the position in the content string corresponding to the current cursor position
476
+ def calculate_posx
477
+ total_length = 0
478
+ (@ix + @line).times do |i|
479
+ total_length += @txt[i].pure.length + 1 # +1 for the newline character
480
+ end
481
+ total_length += @pos
482
+ total_length
483
+ end
484
+ # Calculates the starting position of the current line in the content string
485
+ def calculate_line_start_pos
486
+ total_length = 0
487
+ (@ix + @line).times do |i|
488
+ total_length += @txt[i].pure.length + 1 # +1 for the newline character
489
+ end
490
+ total_length
491
+ end
492
+ end
493
+ end
494
+