rcurses 6.0.2 → 6.1.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 +151 -7
- data/lib/rcurses.rb +50 -4
- data/lib/string_extensions.rb +50 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ce484ba636e927e6344a8bb0552c49aec18df5cf3d2ebcb1c80c1d3927da3005
|
4
|
+
data.tar.gz: 9565880d7a799a27f364e3ec543f69c5061db9e9951684b3747cad5380b5ce85
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b6406ba055a63fcd8b187920f4280931b046d651919b08e3792db6a09de8ef5d5383de853847cd44cebf579ccb71c8a7a19b2d147cf825579dca6c96113130b1
|
7
|
+
data.tar.gz: 5522b3932739d6b726bed53a9b349006f0c7ab44bd79038008ed1f4db2cde530d0fe95e6f38d25bbf627b03a2dea4e8dd10f89991761ea048c65eec79f5dd92c
|
data/README.md
CHANGED
@@ -10,14 +10,23 @@ 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
|
14
|
-
- **
|
15
|
-
- **
|
16
|
-
- **
|
17
|
-
- **
|
18
|
-
- **Performance optimizations** - Maintains the speed of version 4.8.3
|
13
|
+
# NOTE: Version 6.1.1 adds optional error logging!
|
14
|
+
- **Error logging** - Set `RCURSES_ERROR_LOG=1` to log crashes to `/tmp/rcurses_errors_PID.log`
|
15
|
+
- **Race-condition free** - Uses process-specific filenames to avoid conflicts
|
16
|
+
- **Stack trace preservation** - Full backtraces logged even when screen is cleared
|
17
|
+
- **Completely opt-in** - Zero impact unless explicitly enabled
|
19
18
|
- **Full backward compatibility** - All existing applications work unchanged
|
20
19
|
|
20
|
+
Previous improvements in 6.1.0:
|
21
|
+
- **Safe regex substitution** - New `safe_gsub` methods prevent ANSI code corruption
|
22
|
+
- **ANSI detection** - Check if strings contain ANSI codes with `has_ansi?`
|
23
|
+
- **Visible length calculation** - Get true text length with `visible_length`
|
24
|
+
- **Conditional coloring** - Apply colors only when needed with `safe_fg`/`safe_bg`
|
25
|
+
|
26
|
+
Previous major improvements in 5.0.0:
|
27
|
+
- Memory leak fixes, terminal state protection, enhanced Unicode support
|
28
|
+
- Error handling improvements and performance optimizations
|
29
|
+
|
21
30
|
Version 4.5 gave full RGB support in addition to 256-colors. 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)
|
22
31
|
|
23
32
|
# Why?
|
@@ -94,7 +103,7 @@ Method | Description
|
|
94
103
|
new/init | Initializes a Pane with optional arguments `x, y, w, h, fg and bg`
|
95
104
|
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)
|
96
105
|
refresh | Refreshes/redraws the Pane with content
|
97
|
-
border_refresh | Refresh the Pane border only
|
106
|
+
border_refresh | Refresh the Pane border only - **CRITICAL**: Use this after changing border property
|
98
107
|
full_refresh | Refreshes/redraws the Pane with content completely (without diff rendering)
|
99
108
|
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
|
100
109
|
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
|
@@ -129,12 +138,34 @@ pure | Strip text of any "dressing" (example: with `text = "TEST".b`,
|
|
129
138
|
clean_ansi | Strip seemingly uncolored strings of ansi code (those that are enclosed in "\e[0m"
|
130
139
|
shorten(n) | Shorten the pure version of the string to 'n' characters, preserving any ANSI coding
|
131
140
|
inject("chars",pos) | Inject "chars" at position 'pos' in the pure version of the string (if 'pos' is '-1', then append at end). Preserves any ANSI code
|
141
|
+
safe_gsub(pattern, replacement) | Apply regex substitution without corrupting existing ANSI codes (example: `colored_text.safe_gsub(/\[([^\]]*)\]/) { |m| m.fg(46) }`)
|
142
|
+
safe_gsub!(pattern, replacement) | In-place version of safe_gsub
|
143
|
+
has_ansi? | Check if string contains ANSI escape sequences (returns true/false)
|
144
|
+
visible_length | Get the visible length of text (excluding ANSI codes)
|
145
|
+
safe_fg(fg) | Apply foreground color only if string doesn't already have ANSI codes
|
146
|
+
safe_bg(bg) | Apply background color only if string doesn't already have ANSI codes
|
132
147
|
|
133
148
|
PS: Blink does not work in conjunction with setting a background color in urxvt. It does work in gnome-terminal. But the overall performance in urxvt as orders of magnitude better than gnome-terminal.
|
134
149
|
|
135
150
|
# Cleaning up upon exit
|
136
151
|
End a program with `Rcurses.clear_screen` to clear the screen for any rcurses residues.
|
137
152
|
|
153
|
+
# Error Logging (New in 6.1.1)
|
154
|
+
When applications using rcurses crash, stack traces are typically lost because rcurses clears the screen on exit. To debug crashes, you can now enable error logging:
|
155
|
+
|
156
|
+
```bash
|
157
|
+
RCURSES_ERROR_LOG=1 my_rcurses_app
|
158
|
+
```
|
159
|
+
|
160
|
+
This will create a detailed error log at `/tmp/rcurses_errors_PID.log` containing:
|
161
|
+
- Full stack trace with line numbers
|
162
|
+
- Error class and message
|
163
|
+
- Process information (PID, program name, working directory)
|
164
|
+
- Ruby and rcurses version information
|
165
|
+
- Relevant environment variables
|
166
|
+
|
167
|
+
Each process gets its own log file (using PID) to prevent race conditions when multiple rcurses applications are running simultaneously. The feature is completely opt-in and has zero performance impact when disabled.
|
168
|
+
|
138
169
|
# module Cursor
|
139
170
|
To use this module, first do `include Rcurses::Cursor`. Create a new cursor object with `mycursor = Rcurses::Cursor`. Then you can apply the following methods to `mycursor`:
|
140
171
|
|
@@ -242,6 +273,119 @@ end
|
|
242
273
|
You can also pass a timeout to `getchr` with `getchr(time)` to wait for `time` number of seconds and returning `nil` if the user does not press a key.
|
243
274
|
|
244
275
|
|
276
|
+
# Proper Initialization and Common Pitfalls
|
277
|
+
|
278
|
+
## Critical: Proper Application Initialization
|
279
|
+
|
280
|
+
When building TUI applications with rcurses, follow this exact initialization pattern to avoid blank/frozen screens:
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
#!/usr/bin/env ruby
|
284
|
+
require 'rcurses'
|
285
|
+
require 'io/wait' # CRITICAL for stdin flush
|
286
|
+
|
287
|
+
class MyApp
|
288
|
+
include Rcurses
|
289
|
+
include Rcurses::Input
|
290
|
+
|
291
|
+
def run
|
292
|
+
# 1. Initialize rcurses FIRST
|
293
|
+
Rcurses.init!
|
294
|
+
|
295
|
+
# 2. Get terminal size
|
296
|
+
@h, @w = IO.console.winsize
|
297
|
+
|
298
|
+
# 3. Create your panes
|
299
|
+
@pane_top = Pane.new(1, 1, @w, 1, 255, 235)
|
300
|
+
@pane_left = Pane.new(1, 3, @w/3, @h-4, 15, 0)
|
301
|
+
@pane_right = Pane.new(@w/3+2, 3, @w-@w/3-2, @h-4, 255, 0)
|
302
|
+
|
303
|
+
# 4. Load initial data and render
|
304
|
+
load_data
|
305
|
+
render_all
|
306
|
+
|
307
|
+
# 5. CRITICAL: Flush stdin before main loop
|
308
|
+
# This prevents cursor position requests from blocking initial render
|
309
|
+
$stdin.getc while $stdin.wait_readable(0)
|
310
|
+
|
311
|
+
# 6. Main loop pattern
|
312
|
+
loop do
|
313
|
+
render_all # Render first
|
314
|
+
chr = getchr # Then get input
|
315
|
+
handle_input(chr) # Then handle it
|
316
|
+
break if chr == 'q'
|
317
|
+
end
|
318
|
+
ensure
|
319
|
+
# Clean up
|
320
|
+
Cursor.show
|
321
|
+
end
|
322
|
+
|
323
|
+
def render_all
|
324
|
+
# Use text= and refresh, NOT say for initial renders
|
325
|
+
@pane_top.text = "My Application"
|
326
|
+
@pane_top.refresh
|
327
|
+
|
328
|
+
@pane_left.text = @content
|
329
|
+
@pane_left.refresh
|
330
|
+
end
|
331
|
+
end
|
332
|
+
```
|
333
|
+
|
334
|
+
## Common Pitfalls and Solutions
|
335
|
+
|
336
|
+
### 1. Blank Panes on Startup
|
337
|
+
**Problem:** All panes appear blank until user input.
|
338
|
+
**Cause:** Terminal sends cursor position request (`\e[6n`) that blocks rendering.
|
339
|
+
**Solution:** Always flush stdin before main loop: `$stdin.getc while $stdin.wait_readable(0)`
|
340
|
+
|
341
|
+
### 2. Using `say` vs `text=` and `refresh`
|
342
|
+
**When to use `say`:**
|
343
|
+
- For status messages and one-time updates
|
344
|
+
- When you want to reset scroll position (ix = 0)
|
345
|
+
|
346
|
+
**When to use `text=` and `refresh`:**
|
347
|
+
- In render loops where you control scrolling
|
348
|
+
- When you need to preserve scroll position
|
349
|
+
|
350
|
+
```ruby
|
351
|
+
# For render loops - preserves scroll
|
352
|
+
@pane.text = content
|
353
|
+
@pane.ix = saved_scroll_position # Maintain scroll
|
354
|
+
@pane.refresh
|
355
|
+
|
356
|
+
# For status messages - resets scroll
|
357
|
+
@pane.say("Processing...")
|
358
|
+
```
|
359
|
+
|
360
|
+
### 3. Double Rendering
|
361
|
+
**Problem:** Calling both `say` and `refresh` causes double rendering.
|
362
|
+
**Solution:** `say` already calls refresh internally - don't call refresh after say.
|
363
|
+
|
364
|
+
### 4. Missing require 'io/wait'
|
365
|
+
**Problem:** `wait_readable` method not found.
|
366
|
+
**Solution:** Always include `require 'io/wait'` for stdin flush.
|
367
|
+
|
368
|
+
### 5. Border Changes Not Visible
|
369
|
+
**Problem:** Changing pane border property doesn't show visual changes.
|
370
|
+
**Cause:** Border changes require explicit refresh to be displayed.
|
371
|
+
**Solution:** Use `border_refresh` method after changing border property.
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
# Wrong - border change not visible
|
375
|
+
@pane.border = true
|
376
|
+
|
377
|
+
# Correct - border change visible immediately
|
378
|
+
@pane.border = true
|
379
|
+
@pane.border_refresh
|
380
|
+
```
|
381
|
+
|
382
|
+
## Reference Applications
|
383
|
+
|
384
|
+
Study these applications for best practices:
|
385
|
+
- [RTFM](https://github.com/isene/RTFM) - File manager with complex pane management
|
386
|
+
- [GiTerm](https://github.com/isene/GiTerm) - Git interface with dynamic content
|
387
|
+
- [T-REX](https://github.com/isene/T-REX) - Calculator with simpler UI
|
388
|
+
|
245
389
|
# Example
|
246
390
|
|
247
391
|
Try this in `irb`:
|
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: 6.
|
8
|
+
# Version: 6.1.1: Added optional error logging with RCURSES_ERROR_LOG=1
|
9
9
|
|
10
10
|
require 'io/console' # Basic gem for rcurses
|
11
11
|
require 'io/wait' # stdin handling
|
@@ -124,15 +124,18 @@ module Rcurses
|
|
124
124
|
|
125
125
|
# Private: Display error information after terminal cleanup
|
126
126
|
def display_error(error)
|
127
|
+
# Log error to file if RCURSES_ERROR_LOG is set
|
128
|
+
log_error_to_file(error) if ENV['RCURSES_ERROR_LOG'] == '1'
|
129
|
+
|
127
130
|
# Only display if we're in a TTY and not in a test environment
|
128
131
|
return unless $stdout.tty?
|
129
|
-
|
132
|
+
|
130
133
|
# Add some spacing and make the error very visible
|
131
134
|
puts "\n\n\e[41;37m APPLICATION CRASHED \e[0m"
|
132
135
|
puts "\e[31m═══════════════════════════════════════════════════════════\e[0m"
|
133
136
|
puts "\e[33mError Type:\e[0m #{error.class}"
|
134
137
|
puts "\e[33mMessage:\e[0m #{error.message}"
|
135
|
-
|
138
|
+
|
136
139
|
# Show backtrace if debug mode is enabled
|
137
140
|
if ENV['DEBUG'] || ENV['RCURSES_DEBUG']
|
138
141
|
puts "\n\e[33mBacktrace:\e[0m"
|
@@ -142,11 +145,54 @@ module Rcurses
|
|
142
145
|
else
|
143
146
|
puts "\n\e[90mTip: Set DEBUG=1 or RCURSES_DEBUG=1 to see the full backtrace\e[0m"
|
144
147
|
end
|
145
|
-
|
148
|
+
|
146
149
|
puts "\e[31m═══════════════════════════════════════════════════════════\e[0m"
|
147
150
|
puts "\e[33mNote:\e[0m The application state above shows where the error occurred."
|
151
|
+
|
152
|
+
# Show log file location if logging is enabled
|
153
|
+
if ENV['RCURSES_ERROR_LOG'] == '1'
|
154
|
+
puts "\e[33mError logged to:\e[0m /tmp/rcurses_errors_#{Process.pid}.log"
|
155
|
+
end
|
148
156
|
puts ""
|
149
157
|
end
|
158
|
+
|
159
|
+
# Private: Log error details to a PID-specific file
|
160
|
+
def log_error_to_file(error)
|
161
|
+
return unless ENV['RCURSES_ERROR_LOG'] == '1'
|
162
|
+
|
163
|
+
log_file = "/tmp/rcurses_errors_#{Process.pid}.log"
|
164
|
+
timestamp = Time.now.strftime("%Y-%m-%d %H:%M:%S")
|
165
|
+
|
166
|
+
begin
|
167
|
+
File.open(log_file, "a") do |f|
|
168
|
+
f.puts "=" * 60
|
169
|
+
f.puts "RCURSES ERROR LOG - #{timestamp}"
|
170
|
+
f.puts "PID: #{Process.pid}"
|
171
|
+
f.puts "Program: #{$0}"
|
172
|
+
f.puts "Working Directory: #{Dir.pwd}"
|
173
|
+
f.puts "Ruby Version: #{RUBY_VERSION}"
|
174
|
+
f.puts "Rcurses Version: 6.1.1"
|
175
|
+
f.puts "=" * 60
|
176
|
+
f.puts "Error Class: #{error.class}"
|
177
|
+
f.puts "Error Message: #{error.message}"
|
178
|
+
f.puts
|
179
|
+
f.puts "Full Backtrace:"
|
180
|
+
error.backtrace.each_with_index do |line, i|
|
181
|
+
f.puts " #{i}: #{line}"
|
182
|
+
end
|
183
|
+
f.puts
|
184
|
+
f.puts "Environment Variables:"
|
185
|
+
ENV.select { |k, v| k.start_with?('RCURSES') || k == 'DEBUG' }.each do |k, v|
|
186
|
+
f.puts " #{k}=#{v}"
|
187
|
+
end
|
188
|
+
f.puts "=" * 60
|
189
|
+
f.puts
|
190
|
+
end
|
191
|
+
rescue => file_error
|
192
|
+
# Silently fail if we can't write to log file - don't interfere with main error display
|
193
|
+
# This prevents cascading errors that could hide the original problem
|
194
|
+
end
|
195
|
+
end
|
150
196
|
|
151
197
|
# Public: Run a block with proper error handling and terminal cleanup
|
152
198
|
# This ensures errors are displayed after terminal is restored
|
data/lib/string_extensions.rb
CHANGED
@@ -156,5 +156,55 @@ class String
|
|
156
156
|
|
157
157
|
out
|
158
158
|
end
|
159
|
+
|
160
|
+
# Safely apply regex replacements without corrupting ANSI sequences
|
161
|
+
# This method temporarily removes ANSI codes, applies the regex, then restores them
|
162
|
+
def safe_gsub(pattern, replacement = nil, &block)
|
163
|
+
# Store all ANSI sequences and replace with placeholders
|
164
|
+
ansi_sequences = []
|
165
|
+
placeholder_text = self.gsub(/\e\[[0-9;]*m/) do |match|
|
166
|
+
ansi_sequences << match
|
167
|
+
"⟨ANSI#{ansi_sequences.length - 1}⟩"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Apply the regex to the placeholder text
|
171
|
+
result = if block_given?
|
172
|
+
placeholder_text.gsub(pattern, &block)
|
173
|
+
else
|
174
|
+
placeholder_text.gsub(pattern, replacement)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Restore ANSI sequences
|
178
|
+
ansi_sequences.each_with_index do |ansi, index|
|
179
|
+
result.gsub!("⟨ANSI#{index}⟩", ansi)
|
180
|
+
end
|
181
|
+
|
182
|
+
result
|
183
|
+
end
|
184
|
+
|
185
|
+
# Safe version of gsub! that modifies in place
|
186
|
+
def safe_gsub!(pattern, replacement = nil, &block)
|
187
|
+
result = safe_gsub(pattern, replacement, &block)
|
188
|
+
self.replace(result)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Check if string contains ANSI codes
|
192
|
+
def has_ansi?
|
193
|
+
!!(self =~ /\e\[[0-9;]*m/)
|
194
|
+
end
|
195
|
+
|
196
|
+
# Get the visible length (without ANSI codes)
|
197
|
+
def visible_length
|
198
|
+
pure.length
|
199
|
+
end
|
200
|
+
|
201
|
+
# Apply color only if not already colored
|
202
|
+
def safe_fg(color)
|
203
|
+
has_ansi? ? self : fg(color)
|
204
|
+
end
|
205
|
+
|
206
|
+
def safe_bg(color)
|
207
|
+
has_ansi? ? self : bg(color)
|
208
|
+
end
|
159
209
|
end
|
160
210
|
|
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: 6.
|
4
|
+
version: 6.1.1
|
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-
|
11
|
+
date: 2025-09-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: clipboard
|