rcurses 5.0.0 → 5.1.1

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: ff839fa4bfa8b545649ea90177a931c9fbb445a42b44627579772dc853461106
4
- data.tar.gz: b4182b2b2e8829959de75e85c551440cf43e534eae34dc719947c7f0315b793b
3
+ metadata.gz: 8be17e6f76652103c068e4299b2c0bb1818ae74512aaff66e09362cdf10fecbf
4
+ data.tar.gz: 13ebe397820e760f3059bd6df1acfea80c1765e8c7ac66bc00caeb09786119f4
5
5
  SHA512:
6
- metadata.gz: 72f4617b7fdb711a536721626605e996b068dcfcebb02ba8f4244fc333d177fe8130fa37a55d6887d343bd6b8948b1b385f26c4e1de6bd7edc76e910f209e6e9
7
- data.tar.gz: f5921342d7c0699c230e0ee7cfd3f3448e436e816bf9b7a5d8cad85efeaa81d0609af818a5cc8cd916ae5899e97416d155feb4eeaf24f567e0fa86b83a704769
6
+ metadata.gz: dfb744f2464d2e4529180c15851e71ce5f033a7495f96e1f6f9bdaf1aaf7e4c6b788bd1164cdc7536a4ab740c3ac3e6348721712787ede4f09e021c958f5fffe
7
+ data.tar.gz: b54c0bf4e06484eed793bf07ed779427357b6635a7008e8ac12fe8318715631697cf7ddc8ca18f3aa71a73b8d327df369e5b670aed0582ede49a5ebef36e6dfe
data/README.md CHANGED
@@ -10,8 +10,15 @@ Here's a somewhat simple example of a TUI program using rcurses: The [T-REX](htt
10
10
 
11
11
  And here's a much more involved example: The [RTFM](https://github.com/isene/RTFM) terminal file manager.
12
12
 
13
- # NOTE: Version 4.5 gives full RGB support in addition to 256-colors
14
- Just write a color as a string - e.g. `"d533e0"` for a hexadecimal RGB color (or use the terminal 256 colors by supplying an integer in the range 0-255)
13
+ # NOTE: Version 5.0.0 brings major improvements!
14
+ - **Memory leak fixes** - Better memory management and history limits
15
+ - **Terminal state protection** - Proper terminal restoration on crashes
16
+ - **Enhanced Unicode support** - Better handling of CJK characters and emojis
17
+ - **Error handling improvements** - More robust operation in edge cases
18
+ - **Performance optimizations** - Maintains the speed of version 4.8.3
19
+ - **Full backward compatibility** - All existing applications work unchanged
20
+
21
+ Version 4.5 gave full RGB support in addition to 256-colors. Just write a color as a string - e.g. `"d533e0"` for a hexadecimal RGB color (or use the terminal 256 colors by supplying an integer in the range 0-255)
15
22
 
16
23
  # Why?
17
24
  Having struggled with the venerable curses library and the ruby interface to it for many years, I finally got around to write an alternative - in pure Ruby.
@@ -92,6 +99,7 @@ full_refresh | Refreshes/redraws the Pane with content completely (without dif
92
99
  edit | An editor for the Pane. When this is invoked, all existing font dressing is stripped and the user gets to edit the raw text. The user can add font effects similar to Markdown; Use an asterisk before and after text to be drawn in bold, text between forward-slashes become italic, and underline before and after text means the text will be underlined, a hash-sign before and after text makes the text reverse colored. You can also combine a whole set of dressings in this format: `<23,245,biurl\|Hello World!>` - this will make "Hello World!" print in the color 23 with the background color 245 (regardless of the Pane's fg/bg setting) in bold, italic, underlined, reversed colored and blinking. Hitting `ESC` while in edit mode will disregard the edits, while `Ctrl-S` will save the edits
93
100
  editline | Used for one-line Panes. It will print the content of the property `prompt` and then the property `text` that can then be edited by the user. Hitting `ESC` will disregard the edits, while `ENTER` will save the edited text
94
101
  clear | Clears the pane
102
+ cleanup | Cleans up pane memory (history, caches) - useful for memory management
95
103
  say(text) | Short form for setting panel.text, then doing a refresh of that panel
96
104
  ask(prompt,text) | Short form of setting panel.prompt, then panel.text, doing a panel.editline and then returning panel.text
97
105
  pagedown | Scroll down one page height in the text (minus one line), but not longer than the length of the text
data/lib/rcurses/input.rb CHANGED
@@ -8,6 +8,7 @@ module Rcurses
8
8
  rescue Timeout::Error
9
9
  return nil
10
10
  end
11
+
11
12
 
12
13
  # 2) If it's ESC, grab any quick trailing bytes
13
14
  seq = c
@@ -19,6 +20,8 @@ module Rcurses
19
20
  end
20
21
  end
21
22
  end
23
+
24
+
22
25
 
23
26
  # 3) Single ESC alone
24
27
  return "ESC" if seq == "\e"
@@ -38,6 +41,11 @@ module Rcurses
38
41
  if m = seq.match(/\A\e\[\d+;2([ABCD])\z/)
39
42
  return { 'A' => "S-UP", 'B' => "S-DOWN", 'C' => "S-RIGHT", 'D' => "S-LEFT" }[m[1]]
40
43
  end
44
+
45
+ # 6b) CSI style ctrl-arrows (e.g. ESC [1;5A )
46
+ if m = seq.match(/\A\e\[\d+;5([ABCD])\z/)
47
+ return { 'A' => "C-UP", 'B' => "C-DOWN", 'C' => "C-RIGHT", 'D' => "C-LEFT" }[m[1]]
48
+ end
41
49
 
42
50
  # 7) Plain arrows
43
51
  if m = seq.match(/\A\e\[([ABCD])\z/)
@@ -66,13 +74,17 @@ module Rcurses
66
74
  end
67
75
  end
68
76
 
69
- # 9) SS3 function keys F1-F4
77
+ # 9) SS3 function keys F1-F4 and Ctrl+arrows
70
78
  if seq.start_with?("\eO") && seq.length == 3
71
79
  return case seq[2]
72
80
  when 'P' then "F1"
73
81
  when 'Q' then "F2"
74
82
  when 'R' then "F3"
75
83
  when 'S' then "F4"
84
+ when 'a' then "C-UP" # Some terminals send ESC O a for Ctrl+Up
85
+ when 'b' then "C-DOWN" # Some terminals send ESC O b for Ctrl+Down
86
+ when 'c' then "C-RIGHT" # Some terminals send ESC O c for Ctrl+Right
87
+ when 'd' then "C-LEFT" # Some terminals send ESC O d for Ctrl+Left
76
88
  else ""
77
89
  end
78
90
  end
data/lib/rcurses/pane.rb CHANGED
@@ -9,7 +9,7 @@ module Rcurses
9
9
  attr_accessor :record, :history
10
10
 
11
11
  def initialize(x = 1, y = 1, w = 1, h = 1, fg = nil, bg = nil)
12
- @max_h, @max_w = IO.console.winsize
12
+ @max_h, @max_w = IO.console ? IO.console.winsize : [24, 80]
13
13
  @x = x
14
14
  @y = y
15
15
  @w = w
@@ -198,6 +198,10 @@ module Rcurses
198
198
  STDOUT.print "\e[?25l\e[?7l\e[0m\e[r"
199
199
 
200
200
  fmt = [@fg.to_s, @bg.to_s].join(',')
201
+
202
+ # Skip color application if fg and bg are both nil or empty
203
+ @skip_colors = (@fg.nil? && @bg.nil?) || (fmt == ",")
204
+
201
205
 
202
206
  # Lazy evaluation: If the content or pane width has changed, reinitialize the lazy cache.
203
207
  if !defined?(@cached_text) || @cached_text != cont || @cached_w != @w
@@ -251,16 +255,31 @@ module Rcurses
251
255
  pl = @w - Rcurses.display_width(@txt[l].pure)
252
256
  pl = 0 if pl < 0
253
257
  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)
258
+ # Skip color application if pane has no colors set or text has ANY ANSI codes
259
+ if @skip_colors || @txt[l].include?("\e[")
260
+ # Don't apply pane colors - text already has ANSI sequences
261
+ case @align
262
+ when "l"
263
+ line_str = @txt[l] + " " * pl
264
+ when "r"
265
+ line_str = " " * pl + @txt[l]
266
+ when "c"
267
+ line_str = " " * hl + @txt[l] + " " * (pl - hl)
268
+ end
269
+ else
270
+ # Apply pane colors normally
271
+ case @align
272
+ when "l"
273
+ line_str = @txt[l].c(fmt) + " ".c(fmt) * pl
274
+ when "r"
275
+ line_str = " ".c(fmt) * pl + @txt[l].c(fmt)
276
+ when "c"
277
+ line_str = " ".c(fmt) * hl + @txt[l].c(fmt) + " ".c(fmt) * (pl - hl)
278
+ end
261
279
  end
262
280
  else
263
- line_str = " ".c(fmt) * @w
281
+ # Empty line - only apply colors if pane has them
282
+ line_str = @skip_colors ? " " * @w : " ".c(fmt) * @w
264
283
  end
265
284
 
266
285
  new_frame << line_str
@@ -278,6 +297,16 @@ module Rcurses
278
297
  # restore wrap, then also reset SGR and scroll-region one more time
279
298
  diff_buf << "\e[#{o_row};#{o_col}H\e[?7h\e[0m\e[r"
280
299
  begin
300
+ # Debug: check what's actually being printed
301
+ if diff_buf.include?("Purpose") && diff_buf.include?("[38;5;")
302
+ File.open("/tmp/rcurses_debug.log", "a") do |f|
303
+ f.puts "=== PRINT DEBUG ==="
304
+ f.puts "diff_buf sample: #{diff_buf[0..200].inspect}"
305
+ f.puts "Has escape byte 27: #{diff_buf.bytes.include?(27)}"
306
+ f.puts "Escape count: #{diff_buf.bytes.count(27)}"
307
+ end
308
+ end
309
+
281
310
  print diff_buf
282
311
  rescue => e
283
312
  # If printing fails, at least try to restore terminal state
@@ -640,34 +669,94 @@ module Rcurses
640
669
  begin
641
670
  return [""] if line.nil? || w <= 0
642
671
 
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
672
  ansi_regex = /\e\[[0-9;]*m/
652
673
  result = []
653
674
  tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
654
675
  current_line = ''
655
676
  current_line_length = 0
656
- active_sequences = []
677
+
678
+ # Track SGR state properly
679
+ sgr_state = {
680
+ bold: false, # 1/22
681
+ italic: false, # 3/23
682
+ underline: false, # 4/24
683
+ blink: false, # 5/25
684
+ reverse: false, # 7/27
685
+ fg_color: nil, # 38/39 (nil means default)
686
+ bg_color: nil # 48/49 (nil means default)
687
+ }
688
+
689
+ # Helper to parse SGR parameters
690
+ parse_sgr = lambda do |sequence|
691
+ return unless sequence =~ /\e\[([0-9;]*)m/
692
+ param_str = $1
693
+ params = param_str.empty? ? [0] : param_str.split(';').map(&:to_i)
694
+ i = 0
695
+ while i < params.length
696
+ case params[i]
697
+ when 0 # Reset all
698
+ sgr_state[:bold] = false
699
+ sgr_state[:italic] = false
700
+ sgr_state[:underline] = false
701
+ sgr_state[:blink] = false
702
+ sgr_state[:reverse] = false
703
+ sgr_state[:fg_color] = nil
704
+ sgr_state[:bg_color] = nil
705
+ when 1 then sgr_state[:bold] = true
706
+ when 3 then sgr_state[:italic] = true
707
+ when 4 then sgr_state[:underline] = true
708
+ when 5 then sgr_state[:blink] = true
709
+ when 7 then sgr_state[:reverse] = true
710
+ when 22 then sgr_state[:bold] = false
711
+ when 23 then sgr_state[:italic] = false
712
+ when 24 then sgr_state[:underline] = false
713
+ when 25 then sgr_state[:blink] = false
714
+ when 27 then sgr_state[:reverse] = false
715
+ when 38 # Foreground color
716
+ if params[i+1] == 5 && params[i+2] # 256 color
717
+ sgr_state[:fg_color] = "38;5;#{params[i+2]}"
718
+ i += 2
719
+ elsif params[i+1] == 2 && params[i+4] # RGB color
720
+ sgr_state[:fg_color] = "38;2;#{params[i+2]};#{params[i+3]};#{params[i+4]}"
721
+ i += 4
722
+ end
723
+ when 39 then sgr_state[:fg_color] = nil # Default foreground
724
+ when 48 # Background color
725
+ if params[i+1] == 5 && params[i+2] # 256 color
726
+ sgr_state[:bg_color] = "48;5;#{params[i+2]}"
727
+ i += 2
728
+ elsif params[i+1] == 2 && params[i+4] # RGB color
729
+ sgr_state[:bg_color] = "48;2;#{params[i+2]};#{params[i+3]};#{params[i+4]}"
730
+ i += 4
731
+ end
732
+ when 49 then sgr_state[:bg_color] = nil # Default background
733
+ # Handle legacy 8-color codes (30-37, 40-47, 90-97, 100-107)
734
+ when 30..37 then sgr_state[:fg_color] = params[i].to_s
735
+ when 40..47 then sgr_state[:bg_color] = params[i].to_s
736
+ when 90..97 then sgr_state[:fg_color] = params[i].to_s
737
+ when 100..107 then sgr_state[:bg_color] = params[i].to_s
738
+ end
739
+ i += 1
740
+ end
741
+ end
742
+
743
+ # Helper to reconstruct SGR sequence from state
744
+ build_sgr = lambda do
745
+ codes = []
746
+ codes << "1" if sgr_state[:bold]
747
+ codes << "3" if sgr_state[:italic]
748
+ codes << "4" if sgr_state[:underline]
749
+ codes << "5" if sgr_state[:blink]
750
+ codes << "7" if sgr_state[:reverse]
751
+ codes << sgr_state[:fg_color] if sgr_state[:fg_color]
752
+ codes << sgr_state[:bg_color] if sgr_state[:bg_color]
753
+ codes.empty? ? "" : "\e[#{codes.join(';')}m"
754
+ end
657
755
 
658
756
  tokens.each do |token|
659
757
  if token.match?(ansi_regex)
660
758
  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
759
+ parse_sgr.call(token)
671
760
  else
672
761
  words = token.scan(/\s+|\S+/)
673
762
  words.each do |word|
@@ -679,7 +768,8 @@ module Rcurses
679
768
  else
680
769
  if current_line_length > 0
681
770
  result << current_line
682
- current_line = active_sequences.join
771
+ # Start new line with current SGR state
772
+ current_line = build_sgr.call
683
773
  current_line_length = 0
684
774
  end
685
775
  while word_length > w
@@ -688,7 +778,8 @@ module Rcurses
688
778
  result << current_line
689
779
  word = word[[w, word.length].min..-1] || ""
690
780
  word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
691
- current_line = active_sequences.join
781
+ # Start new line with current SGR state
782
+ current_line = build_sgr.call
692
783
  current_line_length = 0
693
784
  end
694
785
  if word_length > 0
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: 5.0.0
4
+ version: 5.1.1
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-07-03 00:00:00.000000000 Z
11
+ date: 2025-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard