rcurses 4.9.0 → 4.9.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.
data/lib/rcurses.rb DELETED
@@ -1,106 +0,0 @@
1
- # INFORMATION
2
- # Name: rcurses - Ruby CURSES
3
- # Language: Pure Ruby
4
- # Author: Geir Isene <g@isene.com>
5
- # Web_site: http://isene.com/
6
- # Github: https://github.com/isene/rcurses
7
- # License: Public domain
8
- # Version: 4.9.0: Major performance improvements - memory leak fixes, terminal dimension caching, batch updates, better Unicode support, enhanced error handling
9
-
10
- require 'io/console' # Basic gem for rcurses
11
- require 'io/wait' # stdin handling
12
- require 'timeout'
13
-
14
- require_relative 'string_extensions'
15
- require_relative 'rcurses/general'
16
- require_relative 'rcurses/cursor'
17
- require_relative 'rcurses/input'
18
- require_relative 'rcurses/pane'
19
-
20
- module Rcurses
21
- class << self
22
- # Public: Initialize Rcurses. Switches terminal into raw/no-echo
23
- # and registers cleanup handlers. Idempotent.
24
- def init!
25
- return if @initialized
26
- return unless $stdin.tty?
27
-
28
- # enter raw mode, disable echo
29
- $stdin.raw!
30
- $stdin.echo = false
31
-
32
- # ensure cleanup on normal exit
33
- at_exit { cleanup! }
34
-
35
- # ensure cleanup on signals
36
- %w[INT TERM].each do |sig|
37
- trap(sig) { cleanup!; exit }
38
- end
39
-
40
- @initialized = true
41
- end
42
-
43
- # Public: Restore terminal to normal mode, clear screen, show cursor.
44
- # Idempotent: subsequent calls do nothing.
45
- def cleanup!
46
- return if @cleaned_up
47
-
48
- $stdin.cooked!
49
- $stdin.echo = true
50
- Rcurses.clear_screen
51
- Cursor.show
52
-
53
- @cleaned_up = true
54
- end
55
-
56
- # Public: Batch multiple pane updates to prevent flickering
57
- def batch_updates
58
- @batched_panes = []
59
- yield
60
- @batched_panes.each(&:resume_updates)
61
- @batched_panes = nil
62
- end
63
-
64
- # Internal: Track panes that need updating in batch mode
65
- def add_to_batch(pane)
66
- if @batched_panes
67
- pane.suspend_updates unless @batched_panes.include?(pane)
68
- @batched_panes << pane unless @batched_panes.include?(pane)
69
- end
70
- end
71
-
72
- # Public: Check if we're in batch mode
73
- def batch_mode?
74
- !@batched_panes.nil?
75
- end
76
-
77
- # Content caching system for improved performance
78
- def self.cache_get(key)
79
- @content_cache ||= {}
80
- @content_cache[key]
81
- end
82
-
83
- def self.cache_set(key, value)
84
- @content_cache ||= {}
85
- @content_cache_limit ||= 100
86
-
87
- # Simple LRU eviction when cache gets too large
88
- if @content_cache.size >= @content_cache_limit
89
- # Remove oldest entries (simplified LRU)
90
- keys_to_remove = @content_cache.keys.first(@content_cache.size - @content_cache_limit + 10)
91
- keys_to_remove.each { |k| @content_cache.delete(k) }
92
- end
93
-
94
- @content_cache[key] = value
95
- end
96
-
97
- def self.cache_clear
98
- @content_cache = {}
99
- end
100
- end
101
-
102
- # Kick off initialization as soon as the library is required.
103
- init!
104
- end
105
-
106
- # vim: set sw=2 sts=2 et filetype=ruby fdn=2 fcs=fold\:\ :
@@ -1,171 +0,0 @@
1
- # string_extensions.rb
2
-
3
- class String
4
- # Compiled regex patterns for performance
5
- ANSI_SGR_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
6
- ANSI_SEQUENCE_REGEX = /\e\[\d+(?:;\d+)*m/.freeze
7
- # 256-color or truecolor RGB foregroundbreset only the fg (SGR 39)
8
- def fg(color)
9
- sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
10
- r, g, b = color.scan(/../).map { |c| c.to_i(16) }
11
- ["\e[38;2;#{r};#{g};#{b}m", "\e[39m"]
12
- else
13
- ["\e[38;5;#{color}m", "\e[39m"]
14
- end
15
- color(self, sp, ep)
16
- end
17
-
18
- # 256-color or truecolor RGB backgroundbreset only the bg (SGR 49)
19
- def bg(color)
20
- sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
21
- r, g, b = color.scan(/../).map { |c| c.to_i(16) }
22
- ["\e[48;2;#{r};#{g};#{b}m", "\e[49m"]
23
- else
24
- ["\e[48;5;#{color}m", "\e[49m"]
25
- end
26
- color(self, sp, ep)
27
- end
28
-
29
- # Both fg and bg in one go
30
- def fb(fg_color, bg_color)
31
- parts = []
32
- if fg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
33
- r, g, b = fg_color.scan(/../).map { |c| c.to_i(16) }
34
- parts << "38;2;#{r};#{g};#{b}"
35
- else
36
- parts << "38;5;#{fg_color}"
37
- end
38
-
39
- if bg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
40
- r, g, b = bg_color.scan(/../).map { |c| c.to_i(16) }
41
- parts << "48;2;#{r};#{g};#{b}"
42
- else
43
- parts << "48;5;#{bg_color}"
44
- end
45
-
46
- sp = "\e[#{parts.join(';')}m"
47
- color(self, sp, "\e[39;49m")
48
- end
49
-
50
- # bold, italic, underline, blink, reverse
51
- def b; color(self, "\e[1m", "\e[22m"); end
52
- def i; color(self, "\e[3m", "\e[23m"); end
53
- def u; color(self, "\e[4m", "\e[24m"); end
54
- def l; color(self, "\e[5m", "\e[25m"); end
55
- def r; color(self, "\e[7m", "\e[27m"); end
56
-
57
- # Internal helper - wraps +text+ in start/end sequences,
58
- # and re-applies start on every newline.
59
- def color(text, sp, ep = "\e[0m")
60
- t = text.gsub("\n", "#{ep}\n#{sp}")
61
- "#{sp}#{t}#{ep}"
62
- end
63
-
64
- # Combined code: "foo".c("FF0000,00FF00,bui")
65
- # — 6-hex or decimal for fg, then for bg, then letters b/i/u/l/r
66
- def c(code)
67
- parts = code.split(',')
68
- seq = []
69
-
70
- fg = parts.shift
71
- if fg =~ /\A[0-9A-Fa-f]{6}\z/
72
- r,g,b = fg.scan(/../).map{|c|c.to_i(16)}
73
- seq << "38;2;#{r};#{g};#{b}"
74
- elsif fg =~ /\A\d+\z/
75
- seq << "38;5;#{fg}"
76
- end
77
-
78
- if parts.any?
79
- bg = parts.shift
80
- if bg =~ /\A[0-9A-Fa-f]{6}\z/
81
- r,g,b = bg.scan(/../).map{|c|c.to_i(16)}
82
- seq << "48;2;#{r};#{g};#{b}"
83
- elsif bg =~ /\A\d+\z/
84
- seq << "48;5;#{bg}"
85
- end
86
- end
87
-
88
- seq << '1' if code.include?('b')
89
- seq << '3' if code.include?('i')
90
- seq << '4' if code.include?('u')
91
- seq << '5' if code.include?('l')
92
- seq << '7' if code.include?('r')
93
-
94
- "\e[#{seq.join(';')}m#{self}\e[0m"
95
- end
96
-
97
- # Strip all ANSI SGR sequences
98
- def pure
99
- gsub(ANSI_SGR_REGEX, '')
100
- end
101
-
102
- # Remove stray leading/trailing reset if the string has no other styling
103
- def clean_ansi
104
- # If we have opening ANSI codes without proper closing, just use pure
105
- # to avoid unbalanced sequences that can corrupt terminal display
106
- temp = gsub(/\A(?:\e\[0m)+/, '').gsub(/\e\[0m\z/, '')
107
- # Check if we have unbalanced ANSI sequences (opening codes without closing)
108
- if temp =~ /\e\[[\d;]+m/ && temp !~ /\e\[0m\z/
109
- pure
110
- else
111
- temp
112
- end
113
- end
114
-
115
- # Truncate the *visible* length to n, but preserve embedded ANSI
116
- def shorten(n)
117
- count = 0
118
- out = ''
119
- i = 0
120
-
121
- while i < length && count < n
122
- if self[i] == "\e" && (m = self[i..-1].match(/\A(#{ANSI_SEQUENCE_REGEX.source})/))
123
- out << m[1]
124
- i += m[1].length
125
- else
126
- out << self[i]
127
- i += 1
128
- count += 1
129
- end
130
- end
131
-
132
- out
133
- end
134
-
135
- # Insert +insertion+ at visible position +pos+ (negative → end),
136
- # respecting and re-inserting existing ANSI sequences.
137
- def inject(insertion, pos)
138
- pure_txt = pure
139
- visible_len = pure_txt.length
140
- pos = visible_len if pos < 0
141
-
142
- count, out, i, injected = 0, '', 0, false
143
-
144
- while i < length
145
- if self[i] == "\e" && (m = self[i..-1].match(/\A(#{ANSI_SEQUENCE_REGEX.source})/))
146
- out << m[1]
147
- i += m[1].length
148
- else
149
- if count == pos && !injected
150
- out << insertion
151
- injected = true
152
- end
153
- out << self[i]
154
- count += 1
155
- i += 1
156
- end
157
- end
158
-
159
- unless injected
160
- if out =~ /(#{ANSI_SEQUENCE_REGEX.source})\z/
161
- trailing = $1
162
- out = out[0...-trailing.length] + insertion + trailing
163
- else
164
- out << insertion
165
- end
166
- end
167
-
168
- out
169
- end
170
- end
171
-