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.
- checksums.yaml +4 -4
- data/README.md +565 -237
- metadata +3 -12
- data/examples/basic_panes.rb +0 -43
- data/examples/focus_panes.rb +0 -42
- data/lib/rcurses/cursor.rb +0 -53
- data/lib/rcurses/general.rb +0 -6
- data/lib/rcurses/input.rb +0 -132
- data/lib/rcurses/pane.rb +0 -765
- data/lib/rcurses.rb +0 -106
- data/lib/string_extensions.rb +0 -171
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\:\ :
|
data/lib/string_extensions.rb
DELETED
@@ -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
|
-
|