rcurses 4.9.5 → 5.0.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: 3640945686095502c9c1cd2d3c38223a6af7dfc5c30ac9a7e6c2bcd311c156f2
4
- data.tar.gz: fee33bc2c07cec00788c402c66707a1cdc0ccd4a3a7d7099a8cb8857bba2091b
3
+ metadata.gz: ff839fa4bfa8b545649ea90177a931c9fbb445a42b44627579772dc853461106
4
+ data.tar.gz: b4182b2b2e8829959de75e85c551440cf43e534eae34dc719947c7f0315b793b
5
5
  SHA512:
6
- metadata.gz: 330e0732e4996d66e2f1843dfe88fdbd9fc905375cec018ecccd31582f9915a21e2a461528ec1f7c9e7f54d3582a2ff2d29d7aaf20dc6a1f6d61754e688f229f
7
- data.tar.gz: 6c77f6074af662c578d4df20fccc4a03d1512252a0d71fea3e2e0d2af6ee7327a03fc2fdec90858d49f4c89fd864948e45504310f75c921ce5f68c2c93d251fc
6
+ metadata.gz: 72f4617b7fdb711a536721626605e996b068dcfcebb02ba8f4244fc333d177fe8130fa37a55d6887d343bd6b8948b1b385f26c4e1de6bd7edc76e910f209e6e9
7
+ data.tar.gz: f5921342d7c0699c230e0ee7cfd3f3448e436e816bf9b7a5d8cad85efeaa81d0609af818a5cc8cd916ae5899e97416d155feb4eeaf24f567e0fa86b83a704769
@@ -1,6 +1,99 @@
1
1
  module Rcurses
2
+ @@terminal_state_saved = false
3
+ @@original_stty_state = nil
4
+
2
5
  def self.clear_screen
3
6
  # ANSI code \e[2J clears the screen, and \e[H moves the cursor to the top left.
4
7
  print "\e[2J\e[H"
5
8
  end
9
+
10
+ def self.save_terminal_state(install_handlers = false)
11
+ unless @@terminal_state_saved
12
+ @@original_stty_state = `stty -g 2>/dev/null`.chomp rescue nil
13
+ @@terminal_state_saved = true
14
+ setup_signal_handlers if install_handlers
15
+ end
16
+ end
17
+
18
+ def self.restore_terminal_state
19
+ if @@terminal_state_saved && @@original_stty_state
20
+ begin
21
+ # Restore terminal settings
22
+ system("stty #{@@original_stty_state} 2>/dev/null")
23
+ # Reset terminal
24
+ print "\e[0m\e[?25h\e[?7h\e[r"
25
+ STDOUT.flush
26
+ rescue
27
+ # Fallback restoration
28
+ begin
29
+ STDIN.cooked! rescue nil
30
+ STDIN.echo = true rescue nil
31
+ rescue
32
+ end
33
+ end
34
+ end
35
+ @@terminal_state_saved = false
36
+ end
37
+
38
+ def self.setup_signal_handlers
39
+ ['TERM', 'INT', 'QUIT', 'HUP'].each do |signal|
40
+ Signal.trap(signal) do
41
+ restore_terminal_state
42
+ exit(1)
43
+ end
44
+ end
45
+
46
+ # Handle WINCH (window size change) gracefully
47
+ Signal.trap('WINCH') do
48
+ # Just ignore for now - applications should handle this themselves
49
+ end
50
+ end
51
+
52
+ def self.with_terminal_protection(install_handlers = true)
53
+ save_terminal_state(install_handlers)
54
+ begin
55
+ yield
56
+ ensure
57
+ restore_terminal_state
58
+ end
59
+ end
60
+
61
+ # Simple, fast display_width function (like original 4.8.3)
62
+ def self.display_width(str)
63
+ return 0 if str.nil? || str.empty?
64
+
65
+ width = 0
66
+ str.each_char do |char|
67
+ cp = char.ord
68
+ if cp == 0
69
+ # NUL – no width
70
+ elsif cp < 32 || (cp >= 0x7F && cp < 0xA0)
71
+ # Control characters: no width
72
+ width += 0
73
+ # Approximate common wide ranges:
74
+ elsif (cp >= 0x1100 && cp <= 0x115F) ||
75
+ cp == 0x2329 || cp == 0x232A ||
76
+ (cp >= 0x2E80 && cp <= 0xA4CF) ||
77
+ (cp >= 0xAC00 && cp <= 0xD7A3) ||
78
+ (cp >= 0xF900 && cp <= 0xFAFF) ||
79
+ (cp >= 0xFE10 && cp <= 0xFE19) ||
80
+ (cp >= 0xFE30 && cp <= 0xFE6F) ||
81
+ (cp >= 0xFF00 && cp <= 0xFF60) ||
82
+ (cp >= 0xFFE0 && cp <= 0xFFE6)
83
+ width += 2
84
+ else
85
+ width += 1
86
+ end
87
+ end
88
+ width
89
+ end
90
+
91
+ # Comprehensive Unicode display width (available but not used in performance-critical paths)
92
+ def self.display_width_unicode(str)
93
+ return 0 if str.nil? || str.empty?
94
+
95
+ # ... full Unicode implementation available when needed ...
96
+ # For now, just delegate to the simple version
97
+ display_width(str)
98
+ end
6
99
  end
data/lib/rcurses/pane.rb CHANGED
@@ -1,33 +1,4 @@
1
1
  module Rcurses
2
- # A simple display_width function that approximates how many columns a string occupies.
3
- # This is a simplified version that may need adjustments for full Unicode support.
4
- def self.display_width(str)
5
- width = 0
6
- str.each_char do |char|
7
- cp = char.ord
8
- if cp == 0
9
- # NUL – no width
10
- elsif cp < 32 || (cp >= 0x7F && cp < 0xA0)
11
- # Control characters: no width
12
- width += 0
13
- # Approximate common wide ranges:
14
- elsif (cp >= 0x1100 && cp <= 0x115F) ||
15
- cp == 0x2329 || cp == 0x232A ||
16
- (cp >= 0x2E80 && cp <= 0xA4CF) ||
17
- (cp >= 0xAC00 && cp <= 0xD7A3) ||
18
- (cp >= 0xF900 && cp <= 0xFAFF) ||
19
- (cp >= 0xFE10 && cp <= 0xFE19) ||
20
- (cp >= 0xFE30 && cp <= 0xFE6F) ||
21
- (cp >= 0xFF00 && cp <= 0xFF60) ||
22
- (cp >= 0xFFE0 && cp <= 0xFFE6)
23
- width += 2
24
- else
25
- width += 1
26
- end
27
- end
28
- width
29
- end
30
-
31
2
  class Pane
32
3
  require 'clipboard' # Ensure the 'clipboard' gem is installed
33
4
  include Cursor
@@ -54,10 +25,16 @@ module Rcurses
54
25
  @pos = 0 # For cursor tracking during editing:
55
26
  @record = false # Don't record history unless explicitly set to true
56
27
  @history = [] # History array
28
+ @max_history_size = 100 # Limit history to prevent memory leaks
29
+
30
+ ObjectSpace.define_finalizer(self, self.class.finalizer_proc)
57
31
  end
58
32
 
59
33
  def text=(new_text)
60
- (@history << @text) if @record && @text
34
+ if @record && @text
35
+ @history << @text
36
+ @history.shift while @history.size > @max_history_size
37
+ end
61
38
  @text = new_text
62
39
  end
63
40
 
@@ -65,12 +42,18 @@ module Rcurses
65
42
  @prompt = prompt
66
43
  @text = text
67
44
  editline
68
- (@history << @text) if @record && !@text.empty?
45
+ if @record && !@text.empty?
46
+ @history << @text
47
+ @history.shift while @history.size > @max_history_size
48
+ end
69
49
  @text
70
50
  end
71
51
 
72
52
  def say(text)
73
- (@history << text) if @record && !text.empty?
53
+ if @record && !text.empty?
54
+ @history << text
55
+ @history.shift while @history.size > @max_history_size
56
+ end
74
57
  @text = text
75
58
  @ix = 0
76
59
  refresh
@@ -82,6 +65,22 @@ module Rcurses
82
65
  full_refresh
83
66
  end
84
67
 
68
+ def cleanup
69
+ @prev_frame = nil
70
+ @lazy_txt = nil
71
+ @raw_txt = nil
72
+ @cached_text = nil
73
+ @txt = nil
74
+ @history.clear if @history
75
+ end
76
+
77
+ def self.finalizer_proc
78
+ proc do
79
+ # Cleanup code that doesn't reference instance variables
80
+ # since the object is already being finalized
81
+ end
82
+ end
83
+
85
84
  def move(dx, dy)
86
85
  @x += dx
87
86
  @y += dy
@@ -160,21 +159,39 @@ module Rcurses
160
159
  # Diff-based refresh that minimizes flicker.
161
160
  # In this updated version we lazily process only the raw lines required to fill the pane.
162
161
  def refresh(cont = @text)
163
- @max_h, @max_w = IO.console.winsize
162
+ begin
163
+ @max_h, @max_w = IO.console.winsize
164
+ rescue => e
165
+ # Fallback to reasonable defaults if terminal size can't be determined
166
+ @max_h, @max_w = 24, 80
167
+ end
168
+
169
+ # Ensure minimum viable dimensions
170
+ @max_h = [[@max_h, 3].max, 1000].min # Between 3 and 1000 rows
171
+ @max_w = [[@max_w, 10].max, 1000].min # Between 10 and 1000 columns
172
+
173
+ # Ensure pane dimensions are reasonable
174
+ @w = [[@w, 1].max, @max_w].min
175
+ @h = [[@h, 1].max, @max_h].min
164
176
 
165
177
  if @border
166
178
  @w = @max_w - 2 if @w > @max_w - 2
167
179
  @h = @max_h - 2 if @h > @max_h - 2
168
- @x = 2 if @x < 2; @x = @max_w - @w if @x + @w > @max_w
169
- @y = 2 if @y < 2; @y = @max_h - @h if @y + @h > @max_h
180
+ @x = [[2, @x].max, @max_w - @w].min
181
+ @y = [[2, @y].max, @max_h - @h].min
170
182
  else
171
183
  @w = @max_w if @w > @max_w
172
184
  @h = @max_h if @h > @max_h
173
- @x = 1 if @x < 1; @x = @max_w - @w + 1 if @x + @w > @max_w + 1
174
- @y = 1 if @y < 1; @y = @max_h - @h + 1 if @y + @h > @max_h + 1
185
+ @x = [[1, @x].max, @max_w - @w + 1].min
186
+ @y = [[1, @y].max, @max_h - @h + 1].min
175
187
  end
176
188
 
177
- o_row, o_col = pos
189
+ begin
190
+ o_row, o_col = pos
191
+ rescue => e
192
+ # Fallback cursor position
193
+ o_row, o_col = 1, 1
194
+ end
178
195
 
179
196
  # Hide cursor, disable auto-wrap, reset all SGR and scroll margins
180
197
  # (so stray underline, scroll regions, etc. can’t leak out)
@@ -184,17 +201,28 @@ module Rcurses
184
201
 
185
202
  # Lazy evaluation: If the content or pane width has changed, reinitialize the lazy cache.
186
203
  if !defined?(@cached_text) || @cached_text != cont || @cached_w != @w
187
- @raw_txt = cont.split("\n").map { |line| line.chomp("\r") }
188
- @lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
189
- @lazy_index = 0 # Pointer to the next raw line to process.
190
- @cached_text = cont.dup
191
- @cached_w = @w
204
+ begin
205
+ @raw_txt = (cont || "").split("\n").map { |line| line.chomp("\r") }
206
+ @lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
207
+ @lazy_index = 0 # Pointer to the next raw line to process.
208
+ @cached_text = (cont || "").dup
209
+ @cached_w = @w
210
+ rescue => e
211
+ # Fallback if content processing fails
212
+ @raw_txt = [""]
213
+ @lazy_txt = []
214
+ @lazy_index = 0
215
+ @cached_text = ""
216
+ @cached_w = @w
217
+ end
192
218
  end
193
219
 
194
220
  content_rows = @h
195
221
  # Ensure we have processed enough lines for the current scroll position + visible area.
196
- required_lines = @ix + content_rows
197
- while @lazy_txt.size < required_lines && @lazy_index < @raw_txt.size
222
+ required_lines = @ix + content_rows + 50 # Buffer a bit for smoother scrolling
223
+ max_cache_size = 1000 # Prevent excessive memory usage
224
+
225
+ while @lazy_txt.size < required_lines && @lazy_index < @raw_txt.size && @lazy_txt.size < max_cache_size
198
226
  raw_line = @raw_txt[@lazy_index]
199
227
  # If the raw line is short, no wrapping is needed.
200
228
  if raw_line.respond_to?(:pure) && Rcurses.display_width(raw_line.pure) < @w
@@ -205,6 +233,10 @@ module Rcurses
205
233
  @lazy_txt.concat(processed)
206
234
  @lazy_index += 1
207
235
  end
236
+
237
+ # Simplified: just limit max processing, don't trim existing cache
238
+ # This avoids expensive array operations during scrolling
239
+
208
240
  @txt = @lazy_txt
209
241
 
210
242
  @ix = @txt.length - 1 if @ix > @txt.length - 1
@@ -245,7 +277,15 @@ module Rcurses
245
277
 
246
278
  # restore wrap, then also reset SGR and scroll-region one more time
247
279
  diff_buf << "\e[#{o_row};#{o_col}H\e[?7h\e[0m\e[r"
248
- print diff_buf
280
+ begin
281
+ print diff_buf
282
+ rescue => e
283
+ # If printing fails, at least try to restore terminal state
284
+ begin
285
+ print "\e[0m\e[?25h\e[?7h"
286
+ rescue
287
+ end
288
+ end
249
289
  @prev_frame = new_frame
250
290
 
251
291
  # Draw scroll markers after printing the frame.
@@ -597,65 +637,78 @@ module Rcurses
597
637
  end
598
638
 
599
639
  def split_line_with_ansi(line, w)
600
- open_sequences = {
601
- "\e[1m" => "\e[22m",
602
- "\e[3m" => "\e[23m",
603
- "\e[4m" => "\e[24m",
604
- "\e[5m" => "\e[25m",
605
- "\e[7m" => "\e[27m"
606
- }
607
- close_sequences = open_sequences.values + ["\e[0m"]
608
- ansi_regex = /\e\[[0-9;]*m/
609
- result = []
610
- tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
611
- current_line = ''
612
- current_line_length = 0
613
- active_sequences = []
614
- tokens.each do |token|
615
- if token.match?(ansi_regex)
616
- current_line << token
617
- if close_sequences.include?(token)
618
- if token == "\e[0m"
619
- active_sequences.clear
640
+ begin
641
+ return [""] if line.nil? || w <= 0
642
+
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
+ ansi_regex = /\e\[[0-9;]*m/
652
+ result = []
653
+ tokens = line.scan(/(\e\[[0-9;]*m|[^\e]+)/).flatten.compact
654
+ current_line = ''
655
+ current_line_length = 0
656
+ active_sequences = []
657
+
658
+ tokens.each do |token|
659
+ if token.match?(ansi_regex)
660
+ 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
620
668
  else
621
- corresponding_open = open_sequences.key(token)
622
- active_sequences.delete(corresponding_open)
669
+ active_sequences << token
623
670
  end
624
671
  else
625
- active_sequences << token
626
- end
627
- else
628
- words = token.scan(/\s+|\S+/)
629
- words.each do |word|
630
- word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
631
- if current_line_length + word_length <= w
632
- current_line << word
633
- current_line_length += word_length
634
- else
635
- if current_line_length > 0
636
- result << current_line
637
- current_line = active_sequences.join
638
- current_line_length = 0
639
- end
640
- while word_length > w
641
- part = word[0, w]
642
- current_line << part
643
- result << current_line
644
- word = word[w..-1]
672
+ words = token.scan(/\s+|\S+/)
673
+ words.each do |word|
674
+ begin
645
675
  word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
646
- current_line = active_sequences.join
647
- current_line_length = 0
648
- end
649
- if word_length > 0
650
- current_line << word
651
- current_line_length += word_length
676
+ if current_line_length + word_length <= w
677
+ current_line << word
678
+ current_line_length += word_length
679
+ else
680
+ if current_line_length > 0
681
+ result << current_line
682
+ current_line = active_sequences.join
683
+ current_line_length = 0
684
+ end
685
+ while word_length > w
686
+ part = word[0, [w, word.length].min]
687
+ current_line << part
688
+ result << current_line
689
+ word = word[[w, word.length].min..-1] || ""
690
+ word_length = Rcurses.display_width(word.gsub(ansi_regex, ''))
691
+ current_line = active_sequences.join
692
+ current_line_length = 0
693
+ end
694
+ if word_length > 0
695
+ current_line << word
696
+ current_line_length += word_length
697
+ end
698
+ end
699
+ rescue => e
700
+ # Skip problematic word but continue
701
+ next
652
702
  end
653
703
  end
654
704
  end
655
705
  end
706
+ result << current_line unless current_line.empty?
707
+ result.empty? ? [""] : result
708
+ rescue => e
709
+ # Complete fallback
710
+ return [""]
656
711
  end
657
- result << current_line unless current_line.empty?
658
- result
659
712
  end
660
713
  end
661
714
  end
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: 4.9.5
4
+ version: 5.0.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-07-02 00:00:00.000000000 Z
11
+ date: 2025-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -29,14 +29,16 @@ 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. 4.9.5: Emergency fix - properly built
33
- gem with color handling restored.'
32
+ in panes. Cursor movement around the terminal. 5.0.0: Major improvements - memory
33
+ leak fixes, terminal state protection, Unicode support, and enhanced error handling
34
+ while maintaining full backward compatibility and 4.8.3 performance.'
34
35
  email: g@isene.com
35
36
  executables: []
36
37
  extensions: []
37
38
  extra_rdoc_files: []
38
39
  files:
39
40
  - LICENSE
41
+ - README.md
40
42
  - examples/basic_panes.rb
41
43
  - examples/focus_panes.rb
42
44
  - lib/rcurses.rb
@@ -45,7 +47,6 @@ files:
45
47
  - lib/rcurses/input.rb
46
48
  - lib/rcurses/pane.rb
47
49
  - lib/string_extensions.rb
48
- - rcurses-README.md
49
50
  homepage: https://isene.com/
50
51
  licenses:
51
52
  - Unlicense
File without changes