rcurses 2.9 → 3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66ea9b80bc6e110518ce62e95aa6f021fdbde64a8ca639c7bb43b33856496372
4
- data.tar.gz: 59a2371b4bad8b29ce338385d7d506426c1d02b4d84f56b4421d1ee4b56dc32d
3
+ metadata.gz: 706550c12ee9965c33b958c6ce4ec5cb0aa12fb37196b4dd623cea43b982528f
4
+ data.tar.gz: 17e20e12a50a34f2d36069041b3f1dfbedbf27ab7dddf8f3681fd8b289e6fe00
5
5
  SHA512:
6
- metadata.gz: b04179f02c444db27c64ff66f090eda1a2d0176177a32f85c94e867903e1148af47972b50601d6e70e56d2830c5ba8470fc97ce3385963fd685c5c07fa00b155
7
- data.tar.gz: 144a1941e04608a19def0450bc93e334d1577388eb798de4423e9720bf3435834786cce1045f3f25969490d5aec6b43010f8da1dca4c69aa1341ec32c827141f
6
+ metadata.gz: 68fe3d1c80b12fae7ad09b49efdec40a889bf36f6d269e68ab09971bf9f057874ac122cff4d1c08c9b0673e13e65857c98e9e888977138a1c51a385544520901
7
+ data.tar.gz: aa6d784ef29afbb0b5e09ada56b2caadf2b474e0bf909eaa92dc161f88d6d1e6c33424d3c1ed2c407148d9ce502bd5563bf1eb1118e3b2a7a15444df18494a89
data/README.md CHANGED
@@ -69,6 +69,8 @@ text | The text/content of the Pane
69
69
  ix | "Index" - the line number at the top of the Pane, starts at 0, the first line of text in the Pane
70
70
  align | Text alignment in the Pane: "l" = lefts aligned, "c" = center, "r" = right, with the default "l"
71
71
  prompt | The prompt to print at the beginning of a one-liner Pane used as an input box
72
+ moreup | Set to true when there is more text above what is shown (top scroll bar i showing)
73
+ moredown | Set to true when there is more text below what is shown (bottom scroll bar i showing)
72
74
 
73
75
  The methods for Pane:
74
76
 
@@ -150,12 +152,16 @@ Key pressed | string returned
150
152
  ----------------|----------------------------------------------------------
151
153
  `esc` | "ESC"
152
154
  `up` | "UP"
155
+ `shift-up` | "S-UP"
153
156
  `ctrl-up` | "C-UP"
154
157
  `down` | "DOWN"
158
+ `shift-down` | "S-DOWN"
155
159
  `ctrl-down` | "C-DOWN"
156
160
  `right` | "RIGHT"
161
+ `shift-right` | "S-RIGHT"
157
162
  `ctrl-right` | "C-RIGHT"
158
163
  `left` | "LEFT"
164
+ `shifth-left` | "S-LEFT"
159
165
  `ctrl-left` | "C-LEFT"
160
166
  `shift-tab` | "S-TAB"
161
167
  `insert` | "INS"
data/lib/rcurses/input.rb CHANGED
@@ -21,9 +21,13 @@ module Rcurses
21
21
  third_char = $stdin.getc
22
22
  case third_char
23
23
  when 'A' then chr = "UP"
24
+ when 'a' then chr = "S-UP"
24
25
  when 'B' then chr = "DOWN"
26
+ when 'b' then chr = "S-DOWN"
25
27
  when 'C' then chr = "RIGHT"
28
+ when 'c' then chr = "S-RIGHT"
26
29
  when 'D' then chr = "LEFT"
30
+ when 'd' then chr = "S-LEFT"
27
31
  when 'Z' then chr = "S-TAB"
28
32
  when '1'
29
33
  fourth_char = $stdin.getc
data/lib/rcurses/pane.rb CHANGED
@@ -5,6 +5,7 @@ module Rcurses
5
5
  include Input
6
6
  attr_accessor :x, :y, :w, :h, :fg, :bg
7
7
  attr_accessor :border, :scroll, :text, :ix, :align, :prompt
8
+ attr_accessor :moreup, :moredown
8
9
 
9
10
  def initialize(x = 1, y = 1, w = 1, h = 1, fg = nil, bg = nil)
10
11
  @x = x
@@ -14,9 +15,9 @@ module Rcurses
14
15
  @fg, @bg = fg, bg
15
16
  @text = "" # Initialize text variable
16
17
  @align = "l" # Default alignment
17
- @scroll = true # Initialize scroll indicators to true
18
- @prompt = "" # Initialize prompt for editline
19
- @ix = 0 # Text index (starting text line in pane)
18
+ @scroll = true # Enable scroll indicators
19
+ @prompt = "" # Prompt for editline
20
+ @ix = 0 # Starting text line index
20
21
  @max_h, @max_w = IO.console.winsize
21
22
  end
22
23
 
@@ -73,108 +74,12 @@ module Rcurses
73
74
  refresh
74
75
  end
75
76
 
77
+ # Optimized refresh using double buffering to minimize flicker.
76
78
  def refresh(cont = @text)
77
79
  @max_h, @max_w = IO.console.winsize
78
80
 
79
- # Define the core of the ANSI escape sequence handling
80
- def split_line_with_ansi(line, w)
81
- # Define opening and closing sequences
82
- open_sequences = {
83
- "\e[1m" => "\e[22m",
84
- "\e[3m" => "\e[23m",
85
- "\e[4m" => "\e[24m",
86
- "\e[5m" => "\e[25m",
87
- "\e[7m" => "\e[27m" }
88
- # All known closing sequences
89
- close_sequences = open_sequences.values + ["\e[0m"]
90
- # Regex to match ANSI escape sequences
91
- ansi_regex = /\e\[[0-9;]*m/
92
- result = []
93
- # Tokenize the line into ANSI sequences and plain text
94
- tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
95
- current_line = ''
96
- current_line_length = 0
97
- active_sequences = []
98
- tokens.each do |token|
99
- if token.match?(ansi_regex)
100
- # It's an ANSI sequence
101
- current_line << token
102
- if close_sequences.include?(token)
103
- # It's a closing sequence
104
- if token == "\e[0m"
105
- # Reset all sequences
106
- active_sequences.clear
107
- else
108
- # Remove the corresponding opening sequence
109
- corresponding_open = open_sequences.key(token)
110
- active_sequences.delete(corresponding_open)
111
- end
112
- else
113
- # It's an opening sequence (or any other ANSI sequence)
114
- active_sequences << token
115
- end
116
- else
117
- # It's plain text, split into words and spaces
118
- words = token.scan(/\S+\s*/)
119
- words.each do |word|
120
- word_length = word.gsub(ansi_regex, '').length
121
- if current_line_length + word_length <= w
122
- # Append word to current line
123
- current_line << word
124
- current_line_length += word_length
125
- else
126
- # Word doesn't fit in the current line
127
- if current_line_length > 0
128
- # Finish the current line and start a new one
129
- result << current_line
130
- # Start new line with active ANSI sequences
131
- current_line = active_sequences.join
132
- current_line_length = 0
133
- end
134
- # Handle long words that might need splitting
135
- while word_length > w
136
- # Split the word
137
- part = word[0, w]
138
- current_line << part
139
- result << current_line
140
- # Update word and lengths
141
- word = word[w..-1]
142
- word_length = word.gsub(ansi_regex, '').length
143
- # Start new line
144
- current_line = active_sequences.join
145
- current_line_length = 0
146
- end
147
- # Append any remaining part of the word
148
- if word_length > 0
149
- current_line << word
150
- current_line_length += word_length
151
- end
152
- end
153
- end
154
- end
155
- end
156
- # Append any remaining text in the current line
157
- result << current_line unless current_line.empty?
158
- result
159
- end
160
-
161
- # Define the main textformat function
162
- def textformat(cont)
163
- # Split the content by '\n'
164
- lines = cont.split("\n")
165
- result = []
166
- lines.each do |line|
167
- split_lines = split_line_with_ansi(line, @w)
168
- result.concat(split_lines)
169
- end
170
- result
171
- end
172
-
173
- # Start the actual refresh
174
- o_row, o_col = pos
175
-
176
- # Adjust pane dimensions and positions
177
- if @border # Keep panes inside screen
81
+ # Adjust pane dimensions (unchanged logic)
82
+ if @border
178
83
  @w = @max_w - 2 if @w > @max_w - 2
179
84
  @h = @max_h - 2 if @h > @max_h - 2
180
85
  @x = 2 if @x < 2; @x = @max_w - @w if @x + @w > @max_w
@@ -186,69 +91,81 @@ module Rcurses
186
91
  @y = 1 if @y < 1; @y = @max_h - @h + 1 if @y + @h > @max_h + 1
187
92
  end
188
93
 
189
- col(@x); row(@y) # Cursor to start of pane
190
- fmt = [@fg, @bg].compact.join(',') # Format for printing in pane (fg,bg)
191
- @txt = cont.split("\n") # Split content into array
192
- @txt = textformat(cont) if @txt.any? { |line| line.pure.length >= @w } # Reformat lines if necessary
193
- @ix = @txt.length - 1 if @ix > @txt.length - 1; @ix = 0 if @ix < 0 # Ensure no out-of-bounds
94
+ # Save current cursor position.
95
+ o_row, o_col = pos
96
+
97
+ # Hide cursor and switch to alternate screen buffer (if desired). Hide it any case and use Curses.show to bring it back.
98
+ # Note: The alternate screen buffer means your program’s output won’t mix with the normal terminal content.
99
+ STDOUT.print "\e[?25l" # hide cursor
100
+ # Uncomment the next line to use the alternate screen buffer.
101
+ #STDOUT.print "\e[?1049h"
102
+
103
+ buf = ""
104
+ buf << "\e[#{@y};#{@x}H" # Move cursor to start of pane.
105
+ fmt = [@fg, @bg].compact.join(',')
106
+ @txt = cont.split("\n")
107
+ @txt = textformat(cont) if @txt.any? { |line| line.pure.length >= @w }
108
+ @ix = @txt.length - 1 if @ix > @txt.length - 1
109
+ @ix = 0 if @ix < 0
194
110
 
195
- @h.times do |i| # Print pane content
196
- l = @ix + i # The current line to be printed
197
- if @txt[l].to_s != "" # Print the text line
198
- # Get padding width and half width
111
+ @h.times do |i|
112
+ l = @ix + i
113
+ if @txt[l].to_s != ""
199
114
  pl = @w - @txt[l].pure.length
200
115
  pl = 0 if pl < 0
201
116
  hl = pl / 2
202
117
  case @align
203
118
  when "l"
204
- print @txt[l].c(fmt) + " ".c(fmt) * pl
119
+ buf << @txt[l].c(fmt) << " ".c(fmt) * pl
205
120
  when "r"
206
- print " ".c(fmt) * pl + @txt[l].c(fmt)
121
+ buf << " ".c(fmt) * pl << @txt[l].c(fmt)
207
122
  when "c"
208
- print " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
123
+ buf << " ".c(fmt) * hl << @txt[l].c(fmt) << " ".c(fmt) * (pl - hl)
209
124
  end
210
125
  else
211
- print " ".c(fmt) * @w
126
+ buf << " ".c(fmt) * @w
212
127
  end
213
- col(@x) # Cursor to start of pane
214
- row(@y + i + 1)
128
+ buf << "\e[#{@y + i + 1};#{@x}H" # Next line.
215
129
  end
216
130
 
217
- if @ix > 0 and @scroll # Print "more" marker at top
218
- col(@x + @w - 1); row(@y)
219
- print "".c(fmt)
131
+ # Draw scroll markers.
132
+ if @ix > 0 and @scroll
133
+ buf << "\e[#{@y};#{@x + @w - 1}H" << "∆".c(fmt)
134
+ @moreup = true
135
+ else
136
+ @moreup = false
220
137
  end
221
138
 
222
- if @txt.length - @ix > @h and @scroll # Print bottom "more" marker
223
- col(@x + @w - 1); row(@y + @h - 1)
224
- print "▼".c(fmt)
139
+ if @txt.length - @ix > @h and @scroll
140
+ buf << "\e[#{@y + @h - 1};#{@x + @w - 1}H" << "∇".c(fmt)
141
+ @moredown = true
142
+ else
143
+ @moredown = false
225
144
  end
226
145
 
227
- if @border # Print border if @border is set to true
228
- row(@y - 1); col(@x - 1)
229
- print ("┌" + "─" * @w + "┐").c(fmt)
146
+ # Draw border if enabled.
147
+ if @border
148
+ buf << "\e[#{@y - 1};#{@x - 1}H" << ("┌" + "─" * @w + "┐").c(fmt)
230
149
  @h.times do |i|
231
- row(@y + i); col(@x - 1)
232
- print "│".c(fmt)
233
- col(@x + @w)
234
- print "│".c(fmt)
150
+ buf << "\e[#{@y + i};#{@x - 1}H" << "│".c(fmt)
151
+ buf << "\e[#{@y + i};#{@x + @w}H" << "│".c(fmt)
235
152
  end
236
- row(@y + @h); col(@x - 1)
237
- print ("└" + "─" * @w + "┘").c(fmt)
153
+ buf << "\e[#{@y + @h};#{@x - 1}H" << ("└" + "─" * @w + "┘").c(fmt)
238
154
  end
239
155
 
240
- row(o_row)
241
- col(o_col)
156
+ # Restore original cursor position.
157
+ buf << "\e[#{o_row};#{o_col}H"
158
+
159
+ # Print the whole buffer at once.
160
+ print buf
161
+
162
+ # Uncomment the next line to switched to the alternate screen buffer.
163
+ #STDOUT.print "\e[?1049l"
164
+
242
165
  @txt
243
166
  end
244
167
 
245
- def puts(txt)
246
- @text = txt
247
- refresh
248
- end
249
-
250
168
  def textformat(cont)
251
- # Split the content by '\n'
252
169
  lines = cont.split("\n")
253
170
  result = []
254
171
  lines.each do |line|
@@ -258,6 +175,11 @@ module Rcurses
258
175
  result
259
176
  end
260
177
 
178
+ def puts(txt)
179
+ @text = txt
180
+ refresh
181
+ end
182
+
261
183
  def right
262
184
  if @pos < @txt[@ix + @line].length
263
185
  @pos += 1
@@ -279,6 +201,7 @@ module Rcurses
279
201
  end
280
202
  end
281
203
  end
204
+
282
205
  def left
283
206
  if @pos == 0
284
207
  if @line == 0
@@ -294,6 +217,7 @@ module Rcurses
294
217
  @pos -= 1
295
218
  end
296
219
  end
220
+
297
221
  def up
298
222
  if @line == 0
299
223
  @ix -= 1 unless @ix == 0
@@ -305,6 +229,7 @@ module Rcurses
305
229
  rescue
306
230
  end
307
231
  end
232
+
308
233
  def down
309
234
  if @line == @h - 1
310
235
  @ix += 1 unless @ix + @line >= @txt.length - 1
@@ -318,10 +243,10 @@ module Rcurses
318
243
  end
319
244
 
320
245
  def parse(cont)
321
- cont.gsub!(/\*(.+?)\*/, '\1'.b)
322
- cont.gsub!(/\/(.+?)\//, '\1'.i)
323
- cont.gsub!(/_(.+?)_/, '\1'.u)
324
- cont.gsub!(/#(.+?)#/, '\1'.r)
246
+ cont.gsub!(/\*(.+?)\*/, '\1'.b)
247
+ cont.gsub!(/\/(.+?)\//, '\1'.i)
248
+ cont.gsub!(/_(.+?)_/, '\1'.u)
249
+ cont.gsub!(/#(.+?)#/, '\1'.r)
325
250
  cont.gsub!(/<([^|]+)\|([^>]+)>/) do
326
251
  text = $2; codes = $1
327
252
  text.c(codes)
@@ -331,92 +256,85 @@ module Rcurses
331
256
 
332
257
  def edit
333
258
  begin
334
- # Switch to raw mode without echoing input
335
259
  STDIN.raw!
336
260
  Rcurses::Cursor.show
337
- # Prepare content for editing, replacing newlines with a placeholder
338
261
  content = @text.pure.gsub("\n", "¬\n")
339
- # Initialize cursor position and indices
340
- @ix = 0 # Starting index of text lines displayed in the pane
341
- @line = 0 # Current line number relative to the pane's visible area
342
- @pos = 0 # Position within the current line (character index)
343
- @txt = refresh(content)
262
+ @ix = 0
263
+ @line = 0
264
+ @pos = 0
265
+ @txt = refresh(content)
344
266
  input_char = ''
345
267
 
346
- while input_char != 'ESC' # Continue until ESC is pressed
347
- row(@y + @line) # Move cursor to the correct row
348
- col(@x + @pos) # Move cursor to the correct column
349
-
350
- input_char = getchr # Read user input
268
+ while input_char != 'ESC'
269
+ row(@y + @line)
270
+ col(@x + @pos)
271
+ input_char = getchr
351
272
  case input_char
352
- when 'C-L' # Left justify
273
+ when 'C-L'
353
274
  @align = 'l'
354
- when 'C-R' # Right justify
275
+ when 'C-R'
355
276
  @align = 'r'
356
- when 'C-C' # Center justify
277
+ when 'C-C'
357
278
  @align = 'c'
358
- when 'C-Y' # Copy pane content to clipboard
279
+ when 'C-Y'
359
280
  Clipboard.copy(@text.pure)
360
- when 'C-S' # Save edited text back to @text and exit
281
+ when 'C-S'
361
282
  content = content.gsub('¬', "\n")
362
283
  content = parse(content)
363
284
  @text = content
364
285
  input_char = 'ESC'
365
- when 'DEL' # Delete character at current position
286
+ when 'DEL'
366
287
  posx = calculate_posx
367
288
  content.slice!(posx)
368
- when 'BACK' # Backspace (delete character before current position)
289
+ when 'BACK'
369
290
  if @pos > 0
370
291
  left
371
292
  posx = calculate_posx
372
293
  content.slice!(posx)
373
294
  end
374
- when 'WBACK' # Word backspace
295
+ when 'WBACK'
375
296
  while @pos > 0 && content[calculate_posx - 1] != ' '
376
297
  left
377
298
  posx = calculate_posx
378
299
  content.slice!(posx)
379
300
  end
380
- when 'C-K' # Kill line (delete from cursor to end of line)
301
+ when 'C-K'
381
302
  line_start_pos = calculate_line_start_pos
382
303
  line_length = @txt[@ix + @line]&.length || 0
383
304
  content.slice!(line_start_pos + @pos, line_length - @pos)
384
- when 'UP' # Move cursor up one line
305
+ when 'UP'
385
306
  up
386
- when 'DOWN' # Move cursor down one line
307
+ when 'DOWN'
387
308
  down
388
- when 'RIGHT' # Move cursor right one character
309
+ when 'RIGHT'
389
310
  right
390
- when 'LEFT' # Move cursor left one character
311
+ when 'LEFT'
391
312
  left
392
- when 'HOME' # Move to start of line
313
+ when 'HOME'
393
314
  @pos = 0
394
- when 'END' # Move to end of line
315
+ when 'END'
395
316
  current_line_length = @txt[@ix + @line]&.length || 0
396
317
  @pos = current_line_length
397
- when 'C-HOME' # Move to start of pane
318
+ when 'C-HOME'
398
319
  @ix = 0
399
320
  @line = 0
400
321
  @pos = 0
401
- when 'C-END' # Move to end of pane
322
+ when 'C-END'
402
323
  total_lines = @txt.length
403
324
  @ix = [total_lines - @h, 0].max
404
325
  @line = [@h - 1, total_lines - @ix - 1].min
405
326
  current_line_length = @txt[@ix + @line]&.length || 0
406
327
  @pos = current_line_length
407
- when 'ENTER' # Insert newline at current position
328
+ when 'ENTER'
408
329
  posx = calculate_posx
409
330
  content.insert(posx, "¬\n")
410
331
  right
411
- when /^.$/ # Insert character at current position
332
+ when /^.$/
412
333
  posx = calculate_posx
413
334
  content.insert(posx, input_char)
414
335
  right
415
- else
416
- # Handle unrecognized input if necessary
417
336
  end
418
337
 
419
- # Handle pasted input (additional characters in the buffer)
420
338
  while IO.select([$stdin], nil, nil, 0)
421
339
  input_char = $stdin.read_nonblock(1) rescue nil
422
340
  break unless input_char
@@ -425,10 +343,9 @@ module Rcurses
425
343
  right
426
344
  end
427
345
 
428
- @txt = refresh(content) # Refresh the pane with the current content
346
+ @txt = refresh(content)
429
347
  end
430
348
  ensure
431
- # Restore terminal mode
432
349
  STDIN.cooked!
433
350
  end
434
351
  Rcurses::Cursor.hide
@@ -436,36 +353,28 @@ module Rcurses
436
353
 
437
354
  def editline
438
355
  begin
439
- # Switch to raw mode without echo
440
356
  STDIN.raw!
441
357
  Rcurses::Cursor.show
442
-
443
- # Initialize position and dimensions
444
- # Ensure pane is within screen bounds
445
358
  @x = [[@x, 1].max, @max_w - @w + 1].min
446
359
  @y = [[@y, 1].max, @max_h - @h + 1].min
447
-
448
360
  @scroll = false
449
- @ix = 0
361
+ @ix = 0
450
362
  row(@y)
451
-
452
363
  fmt = [@fg, @bg].compact.join(',')
453
364
  col(@x)
454
- print @prompt.c(fmt) # Print prompt at the pane's starting position
455
-
365
+ print @prompt.c(fmt)
456
366
  prompt_len = @prompt.pure.length
457
367
  content_len = @w - prompt_len
458
368
  cont = @text.pure.slice(0, content_len)
459
- @pos = cont.length # Set initial cursor position at the end of content
369
+ @pos = cont.length
460
370
  chr = ''
461
371
 
462
- while chr != 'ESC' # Continue until ESC is pressed
463
- col(@x + prompt_len) # Set cursor at start of content
464
- cont = cont.slice(0, content_len) # Trim content to max length
465
- print cont.ljust(content_len).c(fmt) # Print content, left-justified
466
- col(@x + prompt_len + @pos) # Set cursor to current position
467
-
468
- chr = getchr # Read user input
372
+ while chr != 'ESC'
373
+ col(@x + prompt_len)
374
+ cont = cont.slice(0, content_len)
375
+ print cont.ljust(content_len).c(fmt)
376
+ col(@x + prompt_len + @pos)
377
+ chr = getchr
469
378
  case chr
470
379
  when 'LEFT'
471
380
  @pos -= 1 if @pos > 0
@@ -500,7 +409,6 @@ module Rcurses
500
409
  end
501
410
  end
502
411
 
503
- # Handle pasted input
504
412
  while IO.select([$stdin], nil, nil, 0)
505
413
  chr = $stdin.read_nonblock(1) rescue nil
506
414
  break unless chr
@@ -511,7 +419,6 @@ module Rcurses
511
419
  end
512
420
  end
513
421
  ensure
514
- # Restore terminal mode
515
422
  STDIN.cooked!
516
423
  end
517
424
  Rcurses::Cursor.hide
@@ -519,23 +426,94 @@ module Rcurses
519
426
 
520
427
  private
521
428
 
522
- # Calculates the position in the content string corresponding to the current cursor position
429
+ def flush_stdin
430
+ while IO.select([$stdin], nil, nil, 0.005)
431
+ begin
432
+ $stdin.read_nonblock(1024)
433
+ rescue IO::WaitReadable, EOFError
434
+ break
435
+ end
436
+ end
437
+ end
438
+
523
439
  def calculate_posx
524
440
  total_length = 0
525
441
  (@ix + @line).times do |i|
526
- total_length += @txt[i].pure.length + 1 # +1 for the newline character
442
+ total_length += @txt[i].pure.length + 1 # +1 for the newline
527
443
  end
528
444
  total_length += @pos
529
445
  total_length
530
446
  end
531
- # Calculates the starting position of the current line in the content string
447
+
532
448
  def calculate_line_start_pos
533
449
  total_length = 0
534
450
  (@ix + @line).times do |i|
535
- total_length += @txt[i].pure.length + 1 # +1 for the newline character
451
+ total_length += @txt[i].pure.length + 1
536
452
  end
537
453
  total_length
538
454
  end
455
+
456
+ def split_line_with_ansi(line, w)
457
+ open_sequences = {
458
+ "\e[1m" => "\e[22m",
459
+ "\e[3m" => "\e[23m",
460
+ "\e[4m" => "\e[24m",
461
+ "\e[5m" => "\e[25m",
462
+ "\e[7m" => "\e[27m"
463
+ }
464
+ close_sequences = open_sequences.values + ["\e[0m"]
465
+ ansi_regex = /\e\[[0-9;]*m/
466
+ result = []
467
+ tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
468
+ current_line = ''
469
+ current_line_length = 0
470
+ active_sequences = []
471
+ tokens.each do |token|
472
+ if token.match?(ansi_regex)
473
+ current_line << token
474
+ if close_sequences.include?(token)
475
+ if token == "\e[0m"
476
+ active_sequences.clear
477
+ else
478
+ corresponding_open = open_sequences.key(token)
479
+ active_sequences.delete(corresponding_open)
480
+ end
481
+ else
482
+ active_sequences << token
483
+ end
484
+ else
485
+ words = token.scan(/\s+|\S+/)
486
+ words.each do |word|
487
+ word_length = word.gsub(ansi_regex, '').length
488
+ if current_line_length + word_length <= w
489
+ current_line << word
490
+ current_line_length += word_length
491
+ else
492
+ if current_line_length > 0
493
+ result << current_line
494
+ current_line = active_sequences.join
495
+ current_line_length = 0
496
+ end
497
+ while word_length > w
498
+ part = word[0, w]
499
+ current_line << part
500
+ result << current_line
501
+ word = word[w..-1]
502
+ word_length = word.gsub(ansi_regex, '').length
503
+ current_line = active_sequences.join
504
+ current_line_length = 0
505
+ end
506
+ if word_length > 0
507
+ current_line << word
508
+ current_line_length += word_length
509
+ end
510
+ end
511
+ end
512
+ end
513
+ end
514
+ result << current_line unless current_line.empty?
515
+ result
516
+ end
539
517
  end
540
518
  end
541
519
 
data/lib/rcurses.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # Web_site: http://isene.com/
6
6
  # Github: https://github.com/isene/rcurses
7
7
  # License: Public domain
8
- # Version: 2.9: Added Rcurses.clear_screen
8
+ # Version: 3.0: Flicker reduction by double screen buffer
9
9
 
10
10
  require 'io/console' # Basic gem for rcurses
11
11
  require 'io/wait' # stdin handling
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rcurses
3
3
  version: !ruby/object:Gem::Version
4
- version: '2.9'
4
+ version: '3.0'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Geir Isene
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-03-28 00:00:00.000000000 Z
11
+ date: 2025-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -29,7 +29,8 @@ description: 'Create curses applications for the terminal easier than ever. Crea
29
29
  up text (in panes or anywhere in the terminal) in bold, italic, underline, reverse
30
30
  color, blink and in any 256 terminal colors for foreground and background. Use a
31
31
  simple editor to let users edit text in panes. Left, right or center align text
32
- in panes. Cursor movement around the terminal. New in 2.9: Added Rcurses.clear_screen.'
32
+ in panes. Cursor movement around the terminal. New in 3.0: Flicker reduction by
33
+ double screen buffer.'
33
34
  email: g@isene.com
34
35
  executables: []
35
36
  extensions: []