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 +4 -4
- data/lib/rcurses/general.rb +93 -0
- data/lib/rcurses/pane.rb +149 -96
- metadata +6 -5
- /data/{rcurses-README.md → README.md} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff839fa4bfa8b545649ea90177a931c9fbb445a42b44627579772dc853461106
|
4
|
+
data.tar.gz: b4182b2b2e8829959de75e85c551440cf43e534eae34dc719947c7f0315b793b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 72f4617b7fdb711a536721626605e996b068dcfcebb02ba8f4244fc333d177fe8130fa37a55d6887d343bd6b8948b1b385f26c4e1de6bd7edc76e910f209e6e9
|
7
|
+
data.tar.gz: f5921342d7c0699c230e0ee7cfd3f3448e436e816bf9b7a5d8cad85efeaa81d0609af818a5cc8cd916ae5899e97416d155feb4eeaf24f567e0fa86b83a704769
|
data/lib/rcurses/general.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
169
|
-
@y = 2
|
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
|
174
|
-
@y = 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
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
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
|
-
|
622
|
-
active_sequences.delete(corresponding_open)
|
669
|
+
active_sequences << token
|
623
670
|
end
|
624
671
|
else
|
625
|
-
|
626
|
-
|
627
|
-
|
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
|
-
|
647
|
-
|
648
|
-
|
649
|
-
|
650
|
-
|
651
|
-
|
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
|
+
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-
|
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.
|
33
|
-
|
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
|