rcurses 4.9.4 → 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.
data/lib/rcurses.rb ADDED
@@ -0,0 +1,61 @@
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.3: Reverted to stable 4.8.3 codebase after 4.9.0-4.9.2 color issues
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
+ end
56
+
57
+ # Kick off initialization as soon as the library is required.
58
+ init!
59
+ end
60
+
61
+ # vim: set sw=2 sts=2 et filetype=ruby fdn=2 fcs=fold\:\ :
@@ -0,0 +1,160 @@
1
+ # string_extensions.rb
2
+
3
+ class String
4
+ # 256-color or truecolor RGB foregroundbreset only the fg (SGR 39)
5
+ def fg(color)
6
+ sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
7
+ r, g, b = color.scan(/../).map { |c| c.to_i(16) }
8
+ ["\e[38;2;#{r};#{g};#{b}m", "\e[39m"]
9
+ else
10
+ ["\e[38;5;#{color}m", "\e[39m"]
11
+ end
12
+ color(self, sp, ep)
13
+ end
14
+
15
+ # 256-color or truecolor RGB backgroundbreset only the bg (SGR 49)
16
+ def bg(color)
17
+ sp, ep = if color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
18
+ r, g, b = color.scan(/../).map { |c| c.to_i(16) }
19
+ ["\e[48;2;#{r};#{g};#{b}m", "\e[49m"]
20
+ else
21
+ ["\e[48;5;#{color}m", "\e[49m"]
22
+ end
23
+ color(self, sp, ep)
24
+ end
25
+
26
+ # Both fg and bg in one go
27
+ def fb(fg_color, bg_color)
28
+ parts = []
29
+ if fg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
30
+ r, g, b = fg_color.scan(/../).map { |c| c.to_i(16) }
31
+ parts << "38;2;#{r};#{g};#{b}"
32
+ else
33
+ parts << "38;5;#{fg_color}"
34
+ end
35
+
36
+ if bg_color.to_s =~ /\A[0-9A-Fa-f]{6}\z/
37
+ r, g, b = bg_color.scan(/../).map { |c| c.to_i(16) }
38
+ parts << "48;2;#{r};#{g};#{b}"
39
+ else
40
+ parts << "48;5;#{bg_color}"
41
+ end
42
+
43
+ sp = "\e[#{parts.join(';')}m"
44
+ color(self, sp, "\e[39;49m")
45
+ end
46
+
47
+ # bold, italic, underline, blink, reverse
48
+ def b; color(self, "\e[1m", "\e[22m"); end
49
+ def i; color(self, "\e[3m", "\e[23m"); end
50
+ def u; color(self, "\e[4m", "\e[24m"); end
51
+ def l; color(self, "\e[5m", "\e[25m"); end
52
+ def r; color(self, "\e[7m", "\e[27m"); end
53
+
54
+ # Internal helper - wraps +text+ in start/end sequences,
55
+ # and re-applies start on every newline.
56
+ def color(text, sp, ep = "\e[0m")
57
+ t = text.gsub("\n", "#{ep}\n#{sp}")
58
+ "#{sp}#{t}#{ep}"
59
+ end
60
+
61
+ # Combined code: "foo".c("FF0000,00FF00,bui")
62
+ # — 6-hex or decimal for fg, then for bg, then letters b/i/u/l/r
63
+ def c(code)
64
+ parts = code.split(',')
65
+ seq = []
66
+
67
+ fg = parts.shift
68
+ if fg =~ /\A[0-9A-Fa-f]{6}\z/
69
+ r,g,b = fg.scan(/../).map{|c|c.to_i(16)}
70
+ seq << "38;2;#{r};#{g};#{b}"
71
+ elsif fg =~ /\A\d+\z/
72
+ seq << "38;5;#{fg}"
73
+ end
74
+
75
+ if parts.any?
76
+ bg = parts.shift
77
+ if bg =~ /\A[0-9A-Fa-f]{6}\z/
78
+ r,g,b = bg.scan(/../).map{|c|c.to_i(16)}
79
+ seq << "48;2;#{r};#{g};#{b}"
80
+ elsif bg =~ /\A\d+\z/
81
+ seq << "48;5;#{bg}"
82
+ end
83
+ end
84
+
85
+ seq << '1' if code.include?('b')
86
+ seq << '3' if code.include?('i')
87
+ seq << '4' if code.include?('u')
88
+ seq << '5' if code.include?('l')
89
+ seq << '7' if code.include?('r')
90
+
91
+ "\e[#{seq.join(';')}m#{self}\e[0m"
92
+ end
93
+
94
+ # Strip all ANSI SGR sequences
95
+ def pure
96
+ gsub(/\e\[\d+(?:;\d+)*m/, '')
97
+ end
98
+
99
+ # Remove stray leading/trailing reset if the string has no other styling
100
+ def clean_ansi
101
+ gsub(/\A(?:\e\[0m)+/, '').gsub(/\e\[0m\z/, '')
102
+ end
103
+
104
+ # Truncate the *visible* length to n, but preserve embedded ANSI
105
+ def shorten(n)
106
+ count = 0
107
+ out = ''
108
+ i = 0
109
+
110
+ while i < length && count < n
111
+ if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
112
+ out << m[1]
113
+ i += m[1].length
114
+ else
115
+ out << self[i]
116
+ i += 1
117
+ count += 1
118
+ end
119
+ end
120
+
121
+ out
122
+ end
123
+
124
+ # Insert +insertion+ at visible position +pos+ (negative → end),
125
+ # respecting and re-inserting existing ANSI sequences.
126
+ def inject(insertion, pos)
127
+ pure_txt = pure
128
+ visible_len = pure_txt.length
129
+ pos = visible_len if pos < 0
130
+
131
+ count, out, i, injected = 0, '', 0, false
132
+
133
+ while i < length
134
+ if self[i] == "\e" && (m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/))
135
+ out << m[1]
136
+ i += m[1].length
137
+ else
138
+ if count == pos && !injected
139
+ out << insertion
140
+ injected = true
141
+ end
142
+ out << self[i]
143
+ count += 1
144
+ i += 1
145
+ end
146
+ end
147
+
148
+ unless injected
149
+ if out =~ /(\e\[\d+(?:;\d+)*m)\z/
150
+ trailing = $1
151
+ out = out[0...-trailing.length] + insertion + trailing
152
+ else
153
+ out << insertion
154
+ end
155
+ end
156
+
157
+ out
158
+ end
159
+ end
160
+
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.4
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,8 +29,9 @@ 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.4: Stable release with proper
33
- 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: []
@@ -38,6 +39,14 @@ extra_rdoc_files: []
38
39
  files:
39
40
  - LICENSE
40
41
  - README.md
42
+ - examples/basic_panes.rb
43
+ - examples/focus_panes.rb
44
+ - lib/rcurses.rb
45
+ - lib/rcurses/cursor.rb
46
+ - lib/rcurses/general.rb
47
+ - lib/rcurses/input.rb
48
+ - lib/rcurses/pane.rb
49
+ - lib/string_extensions.rb
41
50
  homepage: https://isene.com/
42
51
  licenses:
43
52
  - Unlicense