Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/textbringer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative "textbringer/errors"
require_relative "textbringer/ring"
require_relative "textbringer/buffer"
require_relative "textbringer/terminal"
require_relative "textbringer/window"
require_relative "textbringer/floating_window"
require_relative "textbringer/keymap"
Expand Down
18 changes: 9 additions & 9 deletions lib/textbringer/color.rb
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
require "curses"
require_relative "terminal"

module Textbringer
module Color
BASIC_COLORS = {
"default" => -1,
"black" => Curses::COLOR_BLACK,
"red" => Curses::COLOR_RED,
"green" => Curses::COLOR_GREEN,
"yellow" => Curses::COLOR_YELLOW,
"blue" => Curses::COLOR_BLUE,
"magenta" => Curses::COLOR_MAGENTA,
"cyan" => Curses::COLOR_CYAN,
"white" => Curses::COLOR_WHITE,
"black" => Terminal::COLOR_BLACK,
"red" => Terminal::COLOR_RED,
"green" => Terminal::COLOR_GREEN,
"yellow" => Terminal::COLOR_YELLOW,
"blue" => Terminal::COLOR_BLUE,
"magenta" => Terminal::COLOR_MAGENTA,
"cyan" => Terminal::COLOR_CYAN,
"white" => Terminal::COLOR_WHITE,
"brightblack" => 8,
"brightred" => 9,
"brightgreen" => 10,
Expand Down
2 changes: 1 addition & 1 deletion lib/textbringer/commands/lsp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ def lsp_show_signature_window(label)
# Close any existing signature window
lsp_close_signature_window

columns = [[Buffer.display_width(label) + 2, Curses.cols - 2].min, 1].max
columns = [[Buffer.display_width(label) + 2, Terminal.cols - 2].min, 1].max
win = FloatingWindow.at_cursor(
lines: 1,
columns: columns
Expand Down
5 changes: 4 additions & 1 deletion lib/textbringer/commands/misc.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ module Commands
end

define_command(:suspend_textbringer) do
Curses.close_screen
Terminal.close_screen
Process.kill(:STOP, 0)
Terminal.reinit_screen
Window.resize
Window.redraw
end

define_command(:execute_command) do
Expand Down
12 changes: 6 additions & 6 deletions lib/textbringer/face.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "curses"
require_relative "terminal"

module Textbringer
class Face
Expand Down Expand Up @@ -37,13 +37,13 @@ def update(foreground: -1, background: -1,
@bold = bold
@underline = underline
@reverse = reverse
Curses.init_pair(@color_pair,
Terminal.init_pair(@color_pair,
Color[foreground], Color[background])
@text_attrs = 0
@text_attrs |= Curses::A_BOLD if bold
@text_attrs |= Curses::A_UNDERLINE if underline
@text_attrs |= Curses::A_REVERSE if reverse
@attributes = Curses.color_pair(@color_pair) | @text_attrs
@text_attrs |= Terminal::A_BOLD if bold
@text_attrs |= Terminal::A_UNDERLINE if underline
@text_attrs |= Terminal::A_REVERSE if reverse
@attributes = Terminal.color_pair(@color_pair) | @text_attrs
self
end
end
Expand Down
18 changes: 9 additions & 9 deletions lib/textbringer/floating_window.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require "curses"
require_relative "terminal"

module Textbringer
class FloatingWindow < Window
Expand Down Expand Up @@ -55,8 +55,8 @@ def self.at_cursor(lines:, columns:, window: Window.current, buffer: nil, face:
end

def self.centered(lines:, columns:, buffer: nil, face: :floating_window, current_line_face: nil)
y = (Curses.lines - lines) / 2
x = (Curses.cols - columns) / 2
y = (Terminal.lines - lines) / 2
x = (Terminal.cols - columns) / 2
new(lines, columns, y, x, buffer: buffer, face: face, current_line_face: current_line_face)
end

Expand Down Expand Up @@ -263,9 +263,9 @@ def redisplay

private

# Override to create Curses::Pad instead of Curses::Window
# Override to create Terminal::Pad instead of Terminal::Window
def initialize_window(num_lines, num_columns, y, x)
@window = Curses::Pad.new(num_lines, num_columns)
@window = Terminal::Pad.new(num_lines, num_columns)
# Note: Pad position is set during refresh, not at creation
# No mode_line for floating windows
@mode_line = nil
Expand All @@ -277,7 +277,7 @@ def self.calculate_cursor_position(lines, columns, window)
cursor_x = window.x + window.cursor.x

# Prefer below cursor
space_below = Curses.lines - cursor_y - 2 # -2 for echo area
space_below = Terminal.lines - cursor_y - 2 # -2 for echo area
space_above = cursor_y # Screen space above cursor

if space_below >= lines
Expand All @@ -286,14 +286,14 @@ def self.calculate_cursor_position(lines, columns, window)
y = cursor_y - lines
else
# Not enough space, show below and clip
y = [cursor_y + 1, Curses.lines - lines - 1].max
y = [cursor_y + 1, Terminal.lines - lines - 1].max
y = [y, 0].max
end

# Adjust x to prevent overflow
x = cursor_x
if x + columns > Curses.cols
x = [Curses.cols - columns, 0].max
if x + columns > Terminal.cols
x = [Terminal.cols - columns, 0].max
end

[y, x]
Expand Down
240 changes: 240 additions & 0 deletions lib/textbringer/terminal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
require_relative "terminal/attributes"
require_relative "terminal/screen_buffer"
require_relative "terminal/input"
require_relative "terminal/window"
require_relative "terminal/pad"

module Textbringer
module Terminal
module Termios
# TIOCGWINSZ: get terminal window size
TIOCGWINSZ = case RUBY_PLATFORM
when /linux/ then 0x5413
when /darwin/ then 0x40087468
else 0x5413
end
end
@color_pairs = {} # pair_number => [fg, bg]
@virtual_screen = nil
@physical_screen = nil
@input_reader = nil
@lines = 24
@cols = 80
@old_tio = nil
@colors = 256
@cursor_y = 0
@cursor_x = 0

class << self
attr_reader :virtual_screen, :physical_screen, :input_reader

def init_screen
# Query terminal size
update_size
# Set up raw mode
@old_stty = `stty -g`.chomp
system("stty raw -echo -icanon -isig")
Comment on lines +32 to +36
# Enable alternate screen buffer
STDOUT.write("\e[?1049h")
# Enable mouse tracking could go here
# Hide cursor during updates
STDOUT.write("\e[?25l")
# Clear screen
STDOUT.write("\e[2J\e[H")
STDOUT.flush

# Set up screen buffers
@virtual_screen = ScreenBuffer.new(@lines, @cols)
@physical_screen = ScreenBuffer.new(@lines, @cols)

# Set up input reader
@input_reader = Input::Reader.new(STDIN)

# Handle SIGWINCH
install_sigwinch_handler

# Enable keypad sequences
STDOUT.write("\e[?1h") # Application cursor keys
STDOUT.flush
end

def close_screen
return unless @old_stty
# Show cursor
STDOUT.write("\e[?25h")
# Reset attributes
STDOUT.write("\e[0m")
# Disable alternate screen buffer
STDOUT.write("\e[?1049l")
# Reset cursor keys to normal mode
STDOUT.write("\e[?1l")
STDOUT.flush
# Restore terminal settings
system("stty #{@old_stty}")
@old_stty = nil
@virtual_screen = nil
@physical_screen = nil
@input_reader = nil
end

def reinit_screen
# Re-initialize after suspend/resume
update_size
@old_stty = `stty -g`.chomp
system("stty raw -echo -icanon -isig")
STDOUT.write("\e[?1049h")
STDOUT.write("\e[?25l")
STDOUT.write("\e[0m\e[2J\e[H")
STDOUT.write("\e[?1h")
STDOUT.flush

@virtual_screen = ScreenBuffer.new(@lines, @cols)
@physical_screen = ScreenBuffer.new(@lines, @cols, dirty: true)
@input_reader = Input::Reader.new(STDIN)
end

def echo
# No-op for our implementation
end

def noecho
# Already handled in raw mode
end

def raw
# Already handled in init_screen
end

def noraw
# No-op; restored in close_screen
end

def nl
# No-op
end

def nonl
# No-op
end

def has_colors?
# Check if terminal supports colors via TERM
term = ENV["TERM"] || ""
!term.empty? && term != "dumb"
end

def start_color
# Already available via ANSI sequences
end

def use_default_colors
# Already supported
end

def colors
@colors
end

def assume_default_colors(fg, bg)
# Store and apply default colors
@default_fg = fg
@default_bg = bg
end

def init_pair(pair_num, fg, bg)
@color_pairs[pair_num] = [fg, bg]
end

def color_pair(pair_num)
pair_num << COLOR_PAIR_SHIFT
end

def pair_info(pair_num)
@color_pairs[pair_num]
end

def lines
@lines
end

def cols
@cols
end

def beep
STDOUT.write("\a")
STDOUT.flush
end

def doupdate
return unless @virtual_screen && @physical_screen

output = @virtual_screen.flush_diff(@physical_screen)
unless output.empty?
STDOUT.write(output)
end
# Move cursor to the position set by the last noutrefresh
STDOUT.write("\e[#{@cursor_y + 1};#{@cursor_x + 1}H")
# Always reset SGR so the terminal is never left in a face's state
STDOUT.write("\e[0m")
STDOUT.write("\e[?25h")
STDOUT.flush
end

def set_cursor(y, x)
@cursor_y = y
@cursor_x = x
end

def unget_char(ch)
# Not commonly used, but support it via input reader
end

def save_key_modifiers(flag)
# Not applicable for ANSI terminals
end

private

def update_size
# TIOCGWINSZ ioctl fills a winsize struct: rows, cols, xpixel, ypixel (each uint16)
buf = "\x00" * 8
if STDOUT.respond_to?(:ioctl) &&
STDOUT.ioctl(Termios::TIOCGWINSZ, buf) >= 0
rows, cols = buf.unpack("S!S!")
if rows > 0 && cols > 0
@lines = rows
@cols = cols
return
end
end
@lines = (ENV["LINES"] || 24).to_i
@cols = (ENV["COLUMNS"] || 80).to_i
rescue Errno::ENOTTY, NotImplementedError
@lines = (ENV["LINES"] || 24).to_i
@cols = (ENV["COLUMNS"] || 80).to_i
end

def install_sigwinch_handler
Signal.trap(:WINCH) do
old_lines = @lines
old_cols = @cols
update_size
if @lines != old_lines || @cols != old_cols
@virtual_screen = ScreenBuffer.new(@lines, @cols)
# Dirty physical forces flush_diff to re-render every cell,
# ensuring correct SGR even for spaces and line endings.
@physical_screen = ScreenBuffer.new(@lines, @cols, dirty: true)
# Reset SGR before clearing so \e[2J uses default background.
STDOUT.write("\e[0m\e[2J")
STDOUT.flush
# Push a resize event
@input_reader&.instance_variable_get(:@buf)&.push(
Input::KEY_RESIZE
)
Comment on lines +231 to +234
end
end
end
end
end
end
Loading
Loading