Skip to content

Replace curses with pure ruby terminal#219

Open
shugo wants to merge 4 commits intomainfrom
replace-curses-with-pure-ruby-terminal
Open

Replace curses with pure ruby terminal#219
shugo wants to merge 4 commits intomainfrom
replace-curses-with-pure-ruby-terminal

Conversation

@shugo
Copy link
Copy Markdown
Owner

@shugo shugo commented Mar 13, 2026

No description provided.

shugo and others added 4 commits March 13, 2026 00:06
Remove the native curses gem dependency and replace it with a pure Ruby
Terminal module that talks directly to the terminal via ANSI/xterm escape
sequences. This eliminates the C extension dependency, simplifying
installation and improving portability.

New Terminal module provides:
- Screen init/teardown via ANSI escape sequences
- Differential screen updates (virtual vs physical buffer diffing)
- Escape sequence input parser (CSI, SS3, UTF-8)
- Terminal::Window and Terminal::Pad matching the curses API surface
- SIGWINCH handling for terminal resize
- Suspend/resume support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
noutrefresh now records the cursor's screen coordinates via
Terminal.set_cursor, and doupdate emits a cursor-move escape
sequence to that position before showing the cursor.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace IO.console_size with a direct ioctl call using TIOCGWINSZ,
which reads the current window size from the kernel and gives accurate
results. Falls back to LINES/COLUMNS env vars if the ioctl fails.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three related causes:

1. The SIGWINCH handler called \e[2J while the terminal's SGR state could
   be anything (e.g. the modeline's reverse-video).  \e[2J fills with the
   current background, so cleared cells showed the modeline face instead
   of the default background.  Fixed by emitting \e[0m before \e[2J.

2. flush_diff skips cells that match the physical model.  The physical
   screen was initialised with blank spaces (default attrs), matching the
   virtual screen's own spaces, so those cells were never re-rendered —
   they kept the wrong background left by the tainted \e[2J.  Fixed by
   initialising the post-resize physical screen with a NUL sentinel char
   that never matches any real virtual cell, forcing a complete re-render.

3. After doupdate the terminal could be left in the last face's SGR state
   (e.g. reverse-video from the modeline).  Fixed by always emitting
   \e[0m after the cursor-positioning sequence in doupdate.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the Curses-based rendering/input layer with a pure-Ruby ANSI terminal backend, updating core windowing logic and tests to use the new Textbringer::Terminal APIs.

Changes:

  • Remove the curses gem runtime dependency and route windowing/rendering through a new Textbringer::Terminal implementation.
  • Add terminal primitives for attributes, input decoding, screen buffering, windows, and pads (ANSI diff-based rendering + escape-sequence key parsing).
  • Update/extend test suite to cover the new terminal input and screen buffer behavior, and migrate existing Curses-coupled assertions to Terminal equivalents.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
textbringer.gemspec Drops the curses runtime dependency.
lib/textbringer.rb Ensures the new terminal layer is loaded by default.
lib/textbringer/window.rb Switches Window implementation from Curses to Textbringer::Terminal APIs and key name mapping.
lib/textbringer/floating_window.rb Replaces Curses sizing/pad usage with Terminal sizing/pads.
lib/textbringer/face.rb Replaces Curses color-pair + attribute constants with Terminal equivalents.
lib/textbringer/color.rb Replaces Curses color constants with Terminal color constants.
lib/textbringer/commands/misc.rb Implements suspend/resume by closing and reinitializing the terminal and forcing redraw/resize.
lib/textbringer/commands/lsp.rb Replaces Curses.cols with Terminal.cols for signature window sizing.
lib/textbringer/terminal.rb New ANSI terminal driver: raw mode, alt screen, SIGWINCH handling, diff flush, cursor management.
lib/textbringer/terminal/attributes.rb New attribute + color constants and SGR generation.
lib/textbringer/terminal/input.rb New escape-sequence-based input reader with keycode mapping.
lib/textbringer/terminal/screen_buffer.rb New virtual/physical screen buffers and diff-to-ANSI output generator.
lib/textbringer/terminal/window.rb New window abstraction backed by ScreenBuffer with noutrefresh/get_char.
lib/textbringer/terminal/pad.rb New pad abstraction backed by ScreenBuffer with rectangular noutrefresh.
test/test_helper.rb Reworks test fakes/stubs from Curses to Terminal and swaps fake Window/Pad constants.
test/textbringer/test_window.rb Updates key/input + default color tests to use Terminal keycodes/default colors.
test/textbringer/test_face.rb Updates attribute assertions to use Terminal constants.
test/textbringer/test_color.rb Updates color assertions and color-count manipulation to Terminal.
test/textbringer/commands/test_buffers.rb Updates special key injection to Terminal keycodes.
test/textbringer/terminal/test_screen_buffer.rb Adds direct unit tests for ScreenBuffer/Cell and diff output.
test/textbringer/terminal/test_input.rb Adds direct unit tests for escape-sequence parsing and key mappings.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +32 to +36
# Query terminal size
update_size
# Set up raw mode
@old_stty = `stty -g`.chomp
system("stty raw -echo -icanon -isig")
@input_reader = nil
@lines = 24
@cols = 80
@old_tio = nil
else
# Alt + character
if next_byte < 0x80
ch = next_byte.chr
Comment on lines +231 to +234
# Push a resize event
@input_reader&.instance_variable_get(:@buf)&.push(
Input::KEY_RESIZE
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants