rcurses 4.0 → 4.5

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: 64fede93e0d646b947c3cf6c3217a1aa7bf62f923b724a4dd55b02e3981b8593
4
- data.tar.gz: 3dca4a698dbbc31356e50e322d3e52ba2c0d6a574b7b824e6d6fe7d5d5b49dd2
3
+ metadata.gz: 494b705528b9bb3e5040c9ecf8c1ff31c7cf0db13e04736140f35298d49b48b0
4
+ data.tar.gz: 6f74dc4880397c55461c6ff0f5a6371edec67edfdfb3eb3d65d7dafdc5ac6295
5
5
  SHA512:
6
- metadata.gz: e866db8fa1db394653b90a88c73632d024b7b565db7f984360004b6d4bb4b83c56650e2784669bf827f32585dbbbb9918f98016d5a6612ee39ff352e194b7f84
7
- data.tar.gz: 67ceadf4ccb04fb5708e0faa33253099156cddedd9fe906d5d24a11d9ccfeac836a945ff7990f19df380f64061a986836a6432206e5f7a8cd09cb6f9e112e81b
6
+ metadata.gz: 0cbed1ef8307fd124d035dce74501d58f648668884ae50190d82460d33b3825557ccd1f07b4c0a64d7b69c81a0c1ef240cf1828adc769e96ffd9bfb47d3e7f08
7
+ data.tar.gz: 0f7747ce65c0f8103759b8df5e64bceee0bf7593d137380a08b6002ec6cbaeb3540236f9ebc65b1f28f293ed009b76565fe4e0fd63cdcb2bf597dd32430107cc
data/README.md CHANGED
@@ -10,6 +10,9 @@ Here's a somewhat simple example of a TUI program using rcurses: The [T-REX](htt
10
10
 
11
11
  And here's a much more involved example: The [RTFM](https://github.com/isene/RTFM) terminal file manager.
12
12
 
13
+ # NOTE: Version 4.5 gives full RGB support in addition to 256-colors
14
+ Just write a color as a string - e.g. `"d533e0"` for a hexadecimal RGB color (or use the terminal 256 colors by supplying an integer in the range 0-255)
15
+
13
16
  # Why?
14
17
  Having struggled with the venerable curses library and the ruby interface to it for many years, I finally got around to write an alternative - in pure Ruby.
15
18
 
@@ -83,6 +86,7 @@ Method | Description
83
86
  new/init | Initializes a Pane with optional arguments `x, y, w, h, fg and bg`
84
87
  move(x,y) | Move the pane by `x`and `y` (`mypane.move(-4,5)` will move the pane left four characters and five characters down)
85
88
  refresh | Refreshes/redraws the Pane with content
89
+ border_refresh | Refresh the Pane border only
86
90
  full_refresh | Refreshes/redraws the Pane with content completely (without diff rendering)
87
91
  edit | An editor for the Pane. When this is invoked, all existing font dressing is stripped and the user gets to edit the raw text. The user can add font effects similar to Markdown; Use an asterisk before and after text to be drawn in bold, text between forward-slashes become italic, and underline before and after text means the text will be underlined, a hash-sign before and after text makes the text reverse colored. You can also combine a whole set of dressings in this format: `<23,245,biurl|Hello World!>` - this will make "Hello World!" print in the color 23 with the background color 245 (regardless of the Pane's fg/bg setting) in bold, italic, underlined, reversed colored and blinking. Hitting `ESC` while in edit mode will disregard the edits, while `Ctrl-S` will save the edits
88
92
  editline | Used for one-line Panes. It will print the content of the property `prompt` and then the property `text` that can then be edited by the user. Hitting `ESC` will disregard the edits, while `ENTER` will save the edited text
@@ -97,12 +101,14 @@ bottom | Scroll to the bottom of the text in the pane
97
101
  top | Scroll to the top of the text in the pane
98
102
 
99
103
  # class String extensions
100
- Method extensions provided for the class String:
104
+ Method extensions provided for the class String.
105
+
106
+ A color can either be an integer in the range 0-255 for the usual 256 colors in a terminal, or it can be a string representing RGB. So both of these are valid: `string.fg(219)` and `string.fg("4d22a0")`.
101
107
 
102
108
  Method | Description
103
109
  ---------------|---------------------------------------------------------------
104
110
  fg(fg) | Set text to be printed with the foreground color `fg` (example: `"TEST".fg(84)`)
105
- bg(bg) | Set text to be printed with the background color `bg` (example: `"TEST".bg(196)`)
111
+ bg(bg) | Set text to be printed with the background color `bg` (example: `"TEST".bg("dd32a9")`)
106
112
  fb(fg, bg) | Set text to be printed with the foreground color `fg` and background color `bg` (example: `"TEST".fb(84,196)`)
107
113
  b | Set text to be printed in bold (example: `"TEST".b`)
108
114
  i | Set text to be printed in italic (example: `"TEST".i`)
data/lib/rcurses.rb CHANGED
@@ -5,7 +5,7 @@
5
5
  # Web_site: http://isene.com/
6
6
  # Github: https://github.com/isene/rcurses
7
7
  # License: Public domain
8
- # Version: 4.0: Added border_refresh to refresh only the border of a pane
8
+ # Version: 4.5: Full RGB support in addition to 256-colors
9
9
 
10
10
  require 'io/console' # Basic gem for rcurses
11
11
  require 'io/wait' # stdin handling
@@ -1,124 +1,160 @@
1
+ # string_extensions.rb
2
+
1
3
  class String
2
- # Existing methods...
3
- def fg(fg) ; color(self, "\e[38;5;#{fg}m", "\e[0m") ; end
4
- def bg(bg) ; color(self, "\e[48;5;#{bg}m", "\e[0m") ; end
5
- def fb(fg, bg); color(self, "\e[38;5;#{fg};48;5;#{bg}m"); end
6
- def b ; color(self, "\e[1m", "\e[22m") ; end
7
- def i ; color(self, "\e[3m", "\e[23m") ; end
8
- def u ; color(self, "\e[4m", "\e[24m") ; end
9
- def l ; color(self, "\e[5m", "\e[25m") ; end
10
- def r ; color(self, "\e[7m", "\e[27m") ; end
11
-
12
- # Internal function
4
+ # 256-color or truecolor RGB foreground
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[0m"]
9
+ else
10
+ ["\e[38;5;#{color}m", "\e[0m"]
11
+ end
12
+ color(self, sp, ep)
13
+ end
14
+
15
+ # 256-color or truecolor RGB background
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[0m"]
20
+ else
21
+ ["\e[48;5;#{color}m", "\e[0m"]
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[0m")
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.
13
56
  def color(text, sp, ep = "\e[0m")
14
- # Replace every newline with a newline followed by the start sequence,
15
- # so that formatting is reestablished on each new line.
16
- text = text.gsub("\n", "#{ep}\n#{sp}")
17
- "#{sp}#{text}#{ep}"
57
+ t = text.gsub("\n", "#{ep}\n#{sp}")
58
+ "#{sp}#{t}#{ep}"
18
59
  end
19
60
 
20
- # Use format "TEST".c("204,45,bui") to print "TEST" in bold, underline, italic, fg=204 and bg=45
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
21
63
  def c(code)
22
- prop = "\e["
23
- prop += "38;5;#{code.match(/^\d+/).to_s};" if code.match(/^\d+/)
24
- prop += "48;5;#{code.match(/(?<=,)\d+/).to_s};" if code.match(/(?<=,)\d+/)
25
- prop += "1;" if code.include?('b')
26
- prop += "3;" if code.include?('i')
27
- prop += "4;" if code.include?('u')
28
- prop += "5;" if code.include?('l')
29
- prop += "7;" if code.include?('r')
30
- prop.chop! if prop[-1] == ";"
31
- prop += "m"
32
- prop += self
33
- prop += "\e[0m"
34
- prop
35
- end
64
+ parts = code.split(',')
65
+ seq = []
36
66
 
37
- def clean_ansi
38
- dup
39
- # 1) kill any “\e[0m” that follows another ANSI‐code (so “\e[7m\e[0mtest” → “\e[7mtest”)
40
- .gsub(/\e\[[\d;]+m\e\[0m/, '\0'.sub("\e[0m", ''))
41
- # 2) kill any “\e[0m” that immediately precedes non‐ESC output (so stray resets just before text)
42
- .gsub(/\e\[0m(?=[^\e])/, '')
43
- # 3) kill leading or trailing reset codes
44
- .gsub(/\A\e\[0m/, '')
45
- .gsub(/\e\[0m\z/, '')
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"
46
92
  end
47
93
 
94
+ # Strip all ANSI SGR sequences
48
95
  def pure
49
- self.gsub(/\e\[\d+(?:;\d+)*m/, '')
96
+ gsub(/\e\[\d+(?:;\d+)*m/, '')
50
97
  end
51
98
 
52
- # Shortens the visible (pure) text to n characters, preserving any ANSI sequences.
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
53
105
  def shorten(n)
54
106
  count = 0
55
- result = ""
56
- i = 0
57
- while i < self.length && count < n
58
- if self[i] == "\e" # start of an ANSI escape sequence
59
- if m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/)
60
- result << m[1]
61
- i += m[1].length
62
- else
63
- # Fallback if pattern doesn’t match: treat as a normal character.
64
- result << self[i]
65
- i += 1
66
- count += 1
67
- end
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
68
114
  else
69
- result << self[i]
115
+ out << self[i]
116
+ i += 1
70
117
  count += 1
71
- i += 1
72
118
  end
73
119
  end
74
- result
120
+
121
+ out
75
122
  end
76
123
 
77
- # Inserts the given substring at the provided visible character position.
78
- # A negative position means insertion at the end.
79
- # When inserting at the end and the string ends with an ANSI escape sequence,
80
- # the insertion is placed before that trailing ANSI sequence.
124
+ # Insert +insertion+ at visible position +pos+ (negative end),
125
+ # respecting and re-inserting existing ANSI sequences.
81
126
  def inject(insertion, pos)
82
- pure_text = self.pure
83
- visible_length = pure_text.length
84
- pos = visible_length if pos < 0
127
+ pure_txt = pure
128
+ visible_len = pure_txt.length
129
+ pos = visible_len if pos < 0
85
130
 
86
- count = 0
87
- result = ""
88
- i = 0
89
- injected = false
90
-
91
- while i < self.length
92
- if self[i] == "\e" # Start of an ANSI sequence.
93
- if m = self[i..-1].match(/\A(\e\[\d+(?:;\d+)*m)/)
94
- result << m[1]
95
- i += m[1].length
96
- else
97
- result << self[i]
98
- i += 1
99
- end
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
100
137
  else
101
138
  if count == pos && !injected
102
- result << insertion
139
+ out << insertion
103
140
  injected = true
104
141
  end
105
- result << self[i]
106
- count += 1
107
- i += 1
142
+ out << self[i]
143
+ count += 1
144
+ i += 1
108
145
  end
109
146
  end
110
147
 
111
- # If we haven't injected (i.e. pos equals visible_length),
112
- # check for a trailing ANSI sequence and insert before it.
113
148
  unless injected
114
- if result =~ /(\e\[\d+(?:;\d+)*m)\z/
149
+ if out =~ /(\e\[\d+(?:;\d+)*m)\z/
115
150
  trailing = $1
116
- result = result[0...-trailing.length] + insertion + trailing
151
+ out = out[0...-trailing.length] + insertion + trailing
117
152
  else
118
- result << insertion
153
+ out << insertion
119
154
  end
120
155
  end
121
156
 
122
- result
157
+ out
123
158
  end
124
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.0'
4
+ version: '4.5'
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-05-02 00:00:00.000000000 Z
11
+ date: 2025-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -30,8 +30,7 @@ description: 'Create curses applications for the terminal easier than ever. Crea
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
32
  in panes. Cursor movement around the terminal. New in 3.8: Fixed border fragments
33
- upon utf-8 characters. 4.0: Added border_refresh to refresh only the border of a
34
- pane.'
33
+ upon utf-8 characters. 4.5: Full RGB support in addition to 256-colors.'
35
34
  email: g@isene.com
36
35
  executables: []
37
36
  extensions: []