rcurses 4.0 → 4.6

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: 240aec267dd4c5e8377ffba6844848463a8d4a0c5291f159f354e9c600e6a264
4
+ data.tar.gz: 3d3534900f41903901758496d0ed0494c400c6ca45dfbd9eb3265b1b924fd108
5
5
  SHA512:
6
- metadata.gz: e866db8fa1db394653b90a88c73632d024b7b565db7f984360004b6d4bb4b83c56650e2784669bf827f32585dbbbb9918f98016d5a6612ee39ff352e194b7f84
7
- data.tar.gz: 67ceadf4ccb04fb5708e0faa33253099156cddedd9fe906d5d24a11d9ccfeac836a945ff7990f19df380f64061a986836a6432206e5f7a8cd09cb6f9e112e81b
6
+ metadata.gz: 92a3f431d49eb85bd4c2a25429acfe87acb2cb1a553c4ebad78614f3e34c3f9bcd23037b725b72a117404fa22397a8d5e8a6ba0d1b6193b227c126bcb4f87e80
7
+ data.tar.gz: 07e0d869aa2025be468a98d359ef2f1894751b301c5ec37965fe75b6bb97bec452b01bcd67c6ddb3fa6a08fe7f7289d85082eaf0153ac6b487d70d417759e1eb
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/pane.rb CHANGED
@@ -186,7 +186,7 @@ module Rcurses
186
186
  @raw_txt = cont.split("\n")
187
187
  @lazy_txt = [] # This will hold the processed (wrapped) lines as needed.
188
188
  @lazy_index = 0 # Pointer to the next raw line to process.
189
- @cached_text = cont
189
+ @cached_text = cont.dup
190
190
  @cached_w = @w
191
191
  end
192
192
 
@@ -365,15 +365,19 @@ module Rcurses
365
365
  begin
366
366
  STDIN.cooked! rescue nil
367
367
  STDIN.echo = true rescue nil
368
- Rcurses::Cursor.show
368
+ # Prepare content with visible newline markers
369
369
  content = @text.pure.gsub("\n", "¬\n")
370
- @ix = 0
371
- @line = 0
372
- @pos = 0
373
- @txt = refresh(content)
370
+ # Reset editing cursor state
371
+ @ix = 0
372
+ @line = 0
373
+ @pos = 0
374
+ # Initial render sets @txt internally for display and cursor math
375
+ refresh(content)
376
+ Rcurses::Cursor.show
374
377
  input_char = ''
375
378
 
376
379
  while input_char != 'ESC'
380
+ # Move the terminal cursor to the logical text cursor
377
381
  row(@y + @line)
378
382
  col(@x + @pos)
379
383
  input_char = getchr(flush: false)
@@ -424,9 +428,7 @@ module Rcurses
424
428
  current_line_length = @txt[@ix + @line]&.length || 0
425
429
  @pos = current_line_length
426
430
  when 'C-HOME'
427
- @ix = 0
428
- @line = 0
429
- @pos = 0
431
+ @ix = 0; @line = 0; @pos = 0
430
432
  when 'C-END'
431
433
  total_lines = @txt.length
432
434
  @ix = [total_lines - @h, 0].max
@@ -443,6 +445,7 @@ module Rcurses
443
445
  right
444
446
  end
445
447
 
448
+ # Handle any buffered input
446
449
  while IO.select([$stdin], nil, nil, 0)
447
450
  input_char = $stdin.read_nonblock(1) rescue nil
448
451
  break unless input_char
@@ -451,7 +454,9 @@ module Rcurses
451
454
  right
452
455
  end
453
456
 
454
- @txt = refresh(content)
457
+ # Re-render without overwriting the internal @txt
458
+ refresh(content)
459
+ Rcurses::Cursor.show
455
460
  end
456
461
  ensure
457
462
  STDIN.raw! rescue nil
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 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.
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/, '')
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/, '')
50
102
  end
51
103
 
52
- # Shortens the visible (pure) text to n characters, preserving any ANSI sequences.
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.6'
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-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -30,8 +30,8 @@ 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.6: Fixed a broken pane.edit. Fixed ANSI termination bug
34
+ in RGB support.'
35
35
  email: g@isene.com
36
36
  executables: []
37
37
  extensions: []