Module: Rcurses
- Defined in:
- lib/rcurses.rb,
lib/rcurses/pane.rb,
lib/rcurses/input.rb,
lib/rcurses/cursor.rb,
lib/rcurses/general.rb
Defined Under Namespace
Modules: Cursor, Input Classes: Pane
Constant Summary collapse
- @@terminal_state_saved =
false- @@original_stty_state =
nil
Class Method Summary collapse
-
.cleanup! ⇒ Object
Public: Restore terminal to normal mode, clear screen, show cursor.
- .clear_screen ⇒ Object
-
.display_error(error) ⇒ Object
Private: Display error information after terminal cleanup.
-
.display_width(str) ⇒ Object
Simple, fast display_width function (like original 4.8.3).
-
.display_width_unicode(str) ⇒ Object
Comprehensive Unicode display width (available but not used in performance-critical paths).
-
.init! ⇒ Object
Public: Initialize Rcurses.
-
.log_error_to_file(error) ⇒ Object
Private: Log error details to a PID-specific file.
- .restore_terminal_state ⇒ Object
-
.run(&block) ⇒ Object
Public: Run a block with proper error handling and terminal cleanup This ensures errors are displayed after terminal is restored.
- .save_terminal_state(install_handlers = false) ⇒ Object
- .setup_signal_handlers ⇒ Object
- .with_terminal_protection(install_handlers = true) ⇒ Object
Class Method Details
.cleanup! ⇒ Object
Public: Restore terminal to normal mode, clear screen, show cursor. Idempotent: subsequent calls do nothing.
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/rcurses.rb', line 76 def cleanup! return if @cleaned_up # Restore terminal to normal mode begin if @using_stty # If we used stty for init, use it for cleanup system("stty sane 2>/dev/null") elsif RUBY_VERSION >= "3.4.0" # Ruby 3.4+ with timeout protection begin Timeout::timeout(0.5) do $stdin.cooked! $stdin.echo = true end rescue Timeout::Error # Fallback if cooked! hangs system("stty sane 2>/dev/null") end else # Original code for Ruby < 3.4 $stdin.cooked! $stdin.echo = true end rescue => e # Last resort fallback system("stty sane 2>/dev/null") end # Only clear screen if there's no error to display # This preserves the error context on screen if @error_to_display.nil? Rcurses.clear_screen else # Just move cursor to bottom of screen without clearing print "\e[999;1H" # Move to bottom-left print "\e[K" # Clear current line end Cursor.show @cleaned_up = true # Display any captured error after terminal is restored if @error_to_display display_error(@error_to_display) end end |
.clear_screen ⇒ Object
5 6 7 8 |
# File 'lib/rcurses/general.rb', line 5 def self.clear_screen # ANSI code \e[2J clears the screen, and \e[H moves the cursor to the top left. print "\e[2J\e[H" end |
.display_error(error) ⇒ Object
Private: Display error information after terminal cleanup
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/rcurses.rb', line 126 def display_error(error) # Log error to file if RCURSES_ERROR_LOG is set log_error_to_file(error) if ENV['RCURSES_ERROR_LOG'] == '1' # Only display if we're in a TTY and not in a test environment return unless $stdout.tty? # Add some spacing and make the error very visible puts "\n\n\e[41;37m APPLICATION CRASHED \e[0m" puts "\e[31m═══════════════════════════════════════════════════════════\e[0m" puts "\e[33mError Type:\e[0m #{error.class}" puts "\e[33mMessage:\e[0m #{error.message}" # Show backtrace if debug mode is enabled if ENV['DEBUG'] || ENV['RCURSES_DEBUG'] puts "\n\e[33mBacktrace:\e[0m" error.backtrace.first(15).each do |line| puts " \e[90m#{line}\e[0m" end else puts "\n\e[90mTip: Set DEBUG=1 or RCURSES_DEBUG=1 to see the full backtrace\e[0m" end puts "\e[31m═══════════════════════════════════════════════════════════\e[0m" puts "\e[33mNote:\e[0m The application state above shows where the error occurred." # Show log file location if logging is enabled if ENV['RCURSES_ERROR_LOG'] == '1' puts "\e[33mError logged to:\e[0m /tmp/rcurses_errors_#{Process.pid}.log" end puts "" end |
.display_width(str) ⇒ Object
Simple, fast display_width function (like original 4.8.3)
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/rcurses/general.rb', line 62 def self.display_width(str) return 0 if str.nil? || str.empty? width = 0 str.each_char do |char| cp = char.ord if cp == 0 # NUL – no width elsif cp < 32 || (cp >= 0x7F && cp < 0xA0) # Control characters: no width width += 0 # Approximate common wide ranges: elsif (cp >= 0x1100 && cp <= 0x115F) || cp == 0x2329 || cp == 0x232A || (cp >= 0x2E80 && cp <= 0xA4CF) || (cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFE10 && cp <= 0xFE19) || (cp >= 0xFE30 && cp <= 0xFE6F) || (cp >= 0xFF00 && cp <= 0xFF60) || (cp >= 0xFFE0 && cp <= 0xFFE6) width += 2 else width += 1 end end width end |
.display_width_unicode(str) ⇒ Object
Comprehensive Unicode display width (available but not used in performance-critical paths)
92 93 94 95 96 97 98 |
# File 'lib/rcurses/general.rb', line 92 def self.display_width_unicode(str) return 0 if str.nil? || str.empty? # ... full Unicode implementation available when needed ... # For now, just delegate to the simple version display_width(str) end |
.init! ⇒ Object
Public: Initialize Rcurses. Switches terminal into raw/no-echo and registers cleanup handlers. Idempotent.
24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/rcurses.rb', line 24 def init! return if @initialized return unless $stdin.tty? # enter raw mode, disable echo # Ruby 3.4+ compatibility: Handle potential blocking in raw! begin if RUBY_VERSION >= "3.4.0" # Flush outputs before changing terminal mode (Ruby 3.4+ requirement) $stdout.flush if $stdout.respond_to?(:flush) $stderr.flush if $stderr.respond_to?(:flush) # Use timeout to detect hanging raw! call begin Timeout::timeout(0.5) do $stdin.raw! $stdin.echo = false end rescue Timeout::Error # Fallback to stty for Ruby 3.4+ if raw! hangs system("stty raw -echo 2>/dev/null") @using_stty = true end else # Original code for Ruby < 3.4 $stdin.raw! $stdin.echo = false end rescue Errno::ENOTTY, Errno::ENODEV # Not a real terminal return end # ensure cleanup on normal exit at_exit do # Capture any unhandled exception if $! && !$!.is_a?(SystemExit) && !$!.is_a?(Interrupt) @error_to_display = $! end cleanup! end # ensure cleanup on signals %w[INT TERM].each do |sig| trap(sig) { cleanup!; exit } end @initialized = true end |
.log_error_to_file(error) ⇒ Object
Private: Log error details to a PID-specific file
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 |
# File 'lib/rcurses.rb', line 160 def log_error_to_file(error) return unless ENV['RCURSES_ERROR_LOG'] == '1' log_file = "/tmp/rcurses_errors_#{Process.pid}.log" = Time.now.strftime("%Y-%m-%d %H:%M:%S") begin File.open(log_file, "a") do |f| f.puts "=" * 60 f.puts "RCURSES ERROR LOG - #{timestamp}" f.puts "PID: #{Process.pid}" f.puts "Program: #{$0}" f.puts "Working Directory: #{Dir.pwd}" f.puts "Ruby Version: #{RUBY_VERSION}" f.puts "Rcurses Version: 6.1.1" f.puts "=" * 60 f.puts "Error Class: #{error.class}" f.puts "Error Message: #{error.message}" f.puts f.puts "Full Backtrace:" error.backtrace.each_with_index do |line, i| f.puts " #{i}: #{line}" end f.puts f.puts "Environment Variables:" ENV.select { |k, v| k.start_with?('RCURSES') || k == 'DEBUG' }.each do |k, v| f.puts " #{k}=#{v}" end f.puts "=" * 60 f.puts end rescue => file_error # Silently fail if we can't write to log file - don't interfere with main error display # This prevents cascading errors that could hide the original problem end end |
.restore_terminal_state ⇒ Object
18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
# File 'lib/rcurses/general.rb', line 18 def self.restore_terminal_state if @@terminal_state_saved && @@original_stty_state begin # Restore terminal settings system("stty #{@@original_stty_state} 2>/dev/null") # Reset terminal print "\e[0m\e[?25h\e[?7h\e[r" STDOUT.flush rescue # Fallback restoration begin STDIN.cooked! rescue nil STDIN.echo = true rescue nil rescue end end end @@terminal_state_saved = false end |
.run(&block) ⇒ Object
Public: Run a block with proper error handling and terminal cleanup This ensures errors are displayed after terminal is restored
199 200 201 202 203 204 205 206 207 208 209 |
# File 'lib/rcurses.rb', line 199 def run(&block) init! begin yield rescue StandardError => e @error_to_display = e raise ensure # cleanup! will be called by at_exit handler end end |
.save_terminal_state(install_handlers = false) ⇒ Object
10 11 12 13 14 15 16 |
# File 'lib/rcurses/general.rb', line 10 def self.save_terminal_state(install_handlers = false) unless @@terminal_state_saved @@original_stty_state = `stty -g 2>/dev/null`.chomp rescue nil @@terminal_state_saved = true setup_signal_handlers if install_handlers end end |
.setup_signal_handlers ⇒ Object
38 39 40 41 42 43 44 45 46 47 48 49 50 |
# File 'lib/rcurses/general.rb', line 38 def self.setup_signal_handlers ['TERM', 'INT', 'QUIT', 'HUP'].each do |signal| Signal.trap(signal) do restore_terminal_state exit(1) end end # Handle WINCH (window size change) gracefully Signal.trap('WINCH') do # Just ignore for now - applications should handle this themselves end end |
.with_terminal_protection(install_handlers = true) ⇒ Object
52 53 54 55 56 57 58 59 |
# File 'lib/rcurses/general.rb', line 52 def self.with_terminal_protection(install_handlers = true) save_terminal_state(install_handlers) begin yield ensure restore_terminal_state end end |