diff --git a/lib/textbringer.rb b/lib/textbringer.rb index a029a6ac..871091f7 100644 --- a/lib/textbringer.rb +++ b/lib/textbringer.rb @@ -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" diff --git a/lib/textbringer/color.rb b/lib/textbringer/color.rb index 3b9be3f0..f94d949b 100644 --- a/lib/textbringer/color.rb +++ b/lib/textbringer/color.rb @@ -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, diff --git a/lib/textbringer/commands/lsp.rb b/lib/textbringer/commands/lsp.rb index 002db7b9..cf4183e7 100644 --- a/lib/textbringer/commands/lsp.rb +++ b/lib/textbringer/commands/lsp.rb @@ -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 diff --git a/lib/textbringer/commands/misc.rb b/lib/textbringer/commands/misc.rb index 068d8de1..4431f283 100644 --- a/lib/textbringer/commands/misc.rb +++ b/lib/textbringer/commands/misc.rb @@ -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 diff --git a/lib/textbringer/face.rb b/lib/textbringer/face.rb index 53f54293..24fcbba3 100644 --- a/lib/textbringer/face.rb +++ b/lib/textbringer/face.rb @@ -1,4 +1,4 @@ -require "curses" +require_relative "terminal" module Textbringer class Face @@ -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 diff --git a/lib/textbringer/floating_window.rb b/lib/textbringer/floating_window.rb index e349a292..57be67f4 100644 --- a/lib/textbringer/floating_window.rb +++ b/lib/textbringer/floating_window.rb @@ -1,4 +1,4 @@ -require "curses" +require_relative "terminal" module Textbringer class FloatingWindow < Window @@ -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 @@ -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 @@ -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 @@ -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] diff --git a/lib/textbringer/terminal.rb b/lib/textbringer/terminal.rb new file mode 100644 index 00000000..bc79f61f --- /dev/null +++ b/lib/textbringer/terminal.rb @@ -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") + # 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 + ) + end + end + end + end + end +end diff --git a/lib/textbringer/terminal/attributes.rb b/lib/textbringer/terminal/attributes.rb new file mode 100644 index 00000000..0e3badde --- /dev/null +++ b/lib/textbringer/terminal/attributes.rb @@ -0,0 +1,56 @@ +module Textbringer + module Terminal + # Text attribute constants (bitmask values matching curses conventions) + A_BOLD = 1 << 0 + A_UNDERLINE = 1 << 1 + A_REVERSE = 1 << 2 + + # Color pair shift (upper bits store color pair number) + COLOR_PAIR_SHIFT = 8 + COLOR_PAIR_MASK = 0xFFFF << COLOR_PAIR_SHIFT + + # Standard ANSI color numbers + COLOR_BLACK = 0 + COLOR_RED = 1 + COLOR_GREEN = 2 + COLOR_YELLOW = 3 + COLOR_BLUE = 4 + COLOR_MAGENTA = 5 + COLOR_CYAN = 6 + COLOR_WHITE = 7 + + def self.color_pair(n) + n << COLOR_PAIR_SHIFT + end + + # Generate SGR (Select Graphic Rendition) escape sequence + def self.sgr(attrs, fg = -1, bg = -1) + params = [0] + params << 1 if (attrs & A_BOLD) != 0 + params << 4 if (attrs & A_UNDERLINE) != 0 + params << 7 if (attrs & A_REVERSE) != 0 + + if fg >= 0 + if fg < 8 + params << (30 + fg) + elsif fg < 16 + params << (90 + fg - 8) + else + params << 38 << 5 << fg + end + end + + if bg >= 0 + if bg < 8 + params << (40 + bg) + elsif bg < 16 + params << (100 + bg - 8) + else + params << 48 << 5 << bg + end + end + + "\e[#{params.join(';')}m" + end + end +end diff --git a/lib/textbringer/terminal/input.rb b/lib/textbringer/terminal/input.rb new file mode 100644 index 00000000..b1a46023 --- /dev/null +++ b/lib/textbringer/terminal/input.rb @@ -0,0 +1,250 @@ +module Textbringer + module Terminal + module Input + # Timeout in seconds for distinguishing ESC from escape sequences + DEFAULT_ESCAPE_TIMEOUT = 0.025 + + # CSI sequence final bytes to key symbol mapping + CSI_KEYS = { + "A" => :up, + "B" => :down, + "C" => :right, + "D" => :left, + "F" => :end, + "H" => :home, + "Z" => :btab, # Shift-Tab + } + + # CSI sequences with numeric parameters: ESC [ ~ + CSI_TILDE_KEYS = { + 1 => :home, + 2 => :ic, # Insert + 3 => :dc, # Delete + 4 => :end, + 5 => :ppage, # Page Up + 6 => :npage, # Page Down + 7 => :home, + 8 => :end, + 11 => :f1, + 12 => :f2, + 13 => :f3, + 14 => :f4, + 15 => :f5, + 17 => :f6, + 18 => :f7, + 19 => :f8, + 20 => :f9, + 21 => :f10, + 23 => :f11, + 24 => :f12, + } + + # SS3 sequences: ESC O + SS3_KEYS = { + "A" => :up, + "B" => :down, + "C" => :right, + "D" => :left, + "F" => :end, + "H" => :home, + "P" => :f1, + "Q" => :f2, + "R" => :f3, + "S" => :f4, + } + + # KEY_NAMES maps integer key codes to symbols (for compatibility) + # In our implementation, we return symbols directly, but maintain + # this for the Window::KEY_NAMES lookup + KEY_NAMES = {} + + # Map key symbols to integer codes for backward compatibility + KEY_CODE_BASE = 0x100 + KEY_CODES = {} + [:up, :down, :right, :left, :home, :end, :dc, :ic, + :ppage, :npage, :btab, :resize, + :f1, :f2, :f3, :f4, :f5, :f6, :f7, :f8, :f9, :f10, :f11, :f12, + ].each_with_index do |sym, i| + code = KEY_CODE_BASE + i + KEY_CODES[sym] = code + KEY_NAMES[code] = sym + end + + KEY_RESIZE = KEY_CODES[:resize] + + class Reader + def initialize(input = STDIN) + @input = input + @escape_timeout = DEFAULT_ESCAPE_TIMEOUT + @buf = [] + end + + attr_accessor :escape_timeout + + # Read a single key event. Returns: + # - String for regular characters (including UTF-8) + # - Integer key code for special keys (looked up via KEY_NAMES) + # - nil if no input available (nonblocking mode) + def get_char(blocking: true, timeout_ms: -1) + c = read_byte(blocking: blocking, timeout_ms: timeout_ms) + return nil if c.nil? + + if c == 0x1b # ESC + return parse_escape_sequence + elsif c < 0x80 + c.chr + else + read_utf8(c) + end + end + + private + + def read_byte(blocking: true, timeout_ms: -1) + unless @buf.empty? + return @buf.shift + end + + if !blocking + byte = @input.read_nonblock(1, exception: false) + return nil if byte.nil? || byte == :wait_readable + return byte.ord + end + + if timeout_ms >= 0 + timeout_sec = timeout_ms / 1000.0 + if IO.select([@input], nil, nil, timeout_sec) + byte = @input.read_nonblock(1, exception: false) + return nil if byte.nil? || byte == :wait_readable + return byte.ord + end + return nil + end + + # Blocking read + byte = @input.getbyte + byte + end + + def peek_byte(timeout) + unless @buf.empty? + return @buf.first + end + if IO.select([@input], nil, nil, timeout) + byte = @input.read_nonblock(1, exception: false) + if byte.nil? || byte == :wait_readable + return nil + end + @buf.push(byte.ord) + return byte.ord + end + nil + end + + def parse_escape_sequence + # Check if there's more input coming quickly (escape sequence) + next_byte = peek_byte(@escape_timeout) + if next_byte.nil? + # Standalone ESC + return "\e" + end + + @buf.shift + case next_byte + when 0x5b # [ -> CSI + parse_csi + when 0x4f # O -> SS3 + parse_ss3 + else + # Alt + character + if next_byte < 0x80 + ch = next_byte.chr + # Return ESC, push the char back for next read + @buf.unshift(next_byte) + return "\e" + else + @buf.unshift(next_byte) + return "\e" + end + end + end + + def parse_csi + params = +"" + loop do + b = peek_byte(0.1) + if b.nil? + # Incomplete sequence, return what we have + return "\e" + end + @buf.shift + ch = b.chr + if ch.match?(/[0-9;]/) + params << ch + else + # Final byte + return resolve_csi(params, ch) + end + end + end + + def resolve_csi(params, final) + if final == "~" + num = params.to_i + sym = CSI_TILDE_KEYS[num] + if sym + KEY_CODES[sym] + else + nil # Unknown sequence + end + elsif CSI_KEYS[final] + KEY_CODES[CSI_KEYS[final]] + else + nil # Unknown CSI sequence + end + end + + def parse_ss3 + b = peek_byte(0.1) + if b.nil? + return "\e" + end + @buf.shift + ch = b.chr + sym = SS3_KEYS[ch] + if sym + KEY_CODES[sym] + else + nil + end + end + + def read_utf8(first_byte) + # Determine how many continuation bytes to expect + if first_byte & 0xE0 == 0xC0 + len = 1 + elsif first_byte & 0xF0 == 0xE0 + len = 2 + elsif first_byte & 0xF8 == 0xF0 + len = 3 + else + # Invalid UTF-8 start byte + return first_byte.chr(Encoding::BINARY) + end + + bytes = [first_byte] + len.times do + b = read_byte(blocking: true, timeout_ms: 100) + if b.nil? || (b & 0xC0) != 0x80 + @buf.unshift(b) if b + return bytes.pack("C*").force_encoding(Encoding::BINARY) + end + bytes << b + end + + bytes.pack("C*").force_encoding(Encoding::UTF_8) + end + end + end + end +end diff --git a/lib/textbringer/terminal/pad.rb b/lib/textbringer/terminal/pad.rb new file mode 100644 index 00000000..430986a4 --- /dev/null +++ b/lib/textbringer/terminal/pad.rb @@ -0,0 +1,140 @@ +module Textbringer + module Terminal + class Pad + attr_reader :cury, :curx + + def initialize(lines, columns) + @lines = lines + @columns = columns + @cury = 0 + @curx = 0 + @attrs = 0 + @fg = -1 + @bg = -1 + @buffer = ScreenBuffer.new(lines, columns) + @closed = false + end + + def maxy + @lines + end + + def maxx + @columns + end + + def erase + @buffer.clear + @cury = 0 + @curx = 0 + end + + def setpos(y, x) + @cury = y + @curx = x + end + + def addstr(s) + s.each_char do |c| + break if @cury >= @lines + w = Buffer.display_width(c) + if @curx + w > @columns + if @cury + 1 >= @lines + break + end + @cury += 1 + @curx = 0 + end + @buffer[@cury, @curx] = Cell.new(c, @attrs, @fg, @bg, false) + if w == 2 && @curx + 1 < @columns + @buffer[@cury, @curx + 1] = Cell.new("", @attrs, @fg, @bg, true) + end + @curx += w + if @curx >= @columns + if @cury + 1 < @lines + @cury += 1 + @curx = 0 + end + end + end + end + + def attr_set(attrs, pair) + pair_info = Terminal.pair_info(pair) + @attrs = attrs + @fg = pair_info ? pair_info[0] : -1 + @bg = pair_info ? pair_info[1] : -1 + end + + def attrset(attrs) + @attrs = attrs & ~COLOR_PAIR_MASK + pair = (attrs & COLOR_PAIR_MASK) >> COLOR_PAIR_SHIFT + pair_info = Terminal.pair_info(pair) + @fg = pair_info ? pair_info[0] : -1 + @bg = pair_info ? pair_info[1] : -1 + end + + def attron(attr) + @attrs |= attr + end + + def attroff(attr) + @attrs &= ~attr + end + + def clrtoeol + x = @curx + while x < @columns + @buffer[@cury, x] = Cell.new(" ", @attrs, @fg, @bg, false) + x += 1 + end + end + + # noutrefresh for Pad takes source/dest rectangle + def noutrefresh(pad_min_y, pad_min_x, screen_min_y, screen_min_x, screen_max_y, screen_max_x) + if Terminal.virtual_screen + height = screen_max_y - screen_min_y + 1 + width = screen_max_x - screen_min_x + 1 + Terminal.virtual_screen.copy_from( + @buffer, pad_min_y, pad_min_x, + screen_min_y, screen_min_x, + height, width + ) + end + end + + def redraw + # No-op; will be refreshed on next noutrefresh + end + + def close + @closed = true + end + + def resize(lines, columns) + @lines = lines + @columns = columns + @buffer = ScreenBuffer.new(lines, columns) + end + + def move(y, x) + # Pads don't have screen position; position is set during noutrefresh + end + + def keypad=(flag) + end + + def scrollok(flag) + end + + def idlok(flag) + end + + def nodelay=(flag) + end + + def timeout=(ms) + end + end + end +end diff --git a/lib/textbringer/terminal/screen_buffer.rb b/lib/textbringer/terminal/screen_buffer.rb new file mode 100644 index 00000000..760d89ee --- /dev/null +++ b/lib/textbringer/terminal/screen_buffer.rb @@ -0,0 +1,125 @@ +module Textbringer + module Terminal + # A single character cell on screen + Cell = Struct.new(:char, :attrs, :fg, :bg, :wide_padding) do + def ==(other) + other.is_a?(Cell) && + char == other.char && + attrs == other.attrs && + fg == other.fg && + bg == other.bg && + wide_padding == other.wide_padding + end + end + + class ScreenBuffer + attr_reader :lines, :cols + + def initialize(lines, cols, dirty: false) + @lines = lines + @cols = cols + # Use a NUL sentinel when dirty so flush_diff re-renders every cell, + # including spaces, ensuring correct SGR across the whole screen. + sentinel = dirty ? "\x00" : " " + @cells = Array.new(lines) { Array.new(cols) { Cell.new(sentinel, 0, -1, -1, false) } } + end + + def resize(lines, cols) + @cells = Array.new(lines) { |y| + Array.new(cols) { |x| + if y < @lines && x < @cols + @cells[y][x] + else + Cell.new(" ", 0, -1, -1, false) + end + } + } + @lines = lines + @cols = cols + end + + def [](y, x) + @cells[y][x] + end + + def []=(y, x, cell) + @cells[y][x] = cell + end + + def clear + @lines.times do |y| + @cols.times do |x| + @cells[y][x] = Cell.new(" ", 0, -1, -1, false) + end + end + end + + def clear_row(y) + @cols.times do |x| + @cells[y][x] = Cell.new(" ", 0, -1, -1, false) + end + end + + # Copy cells from src buffer region into this buffer at dest position + def copy_from(src, src_y, src_x, dst_y, dst_x, height, width) + height.times do |dy| + sy = src_y + dy + dy_dst = dst_y + dy + next if sy >= src.lines || dy_dst >= @lines + width.times do |dx| + sx = src_x + dx + dx_dst = dst_x + dx + next if sx >= src.cols || dx_dst >= @cols + @cells[dy_dst][dx_dst] = src[sy, sx].dup + end + end + end + + # Compute diff and generate output to transform physical into this (virtual) + def flush_diff(physical) + output = +"" + cur_attrs = -1 + cur_fg = -2 + cur_bg = -2 + last_y = -1 + last_x = -1 + + @lines.times do |y| + x = 0 + while x < @cols + vc = @cells[y][x] + pc = physical[y, x] + if vc != pc + # Move cursor if not contiguous + if y != last_y || x != last_x + output << "\e[#{y + 1};#{x + 1}H" + end + # Set attributes if changed + if vc.attrs != cur_attrs || vc.fg != cur_fg || vc.bg != cur_bg + output << Terminal.sgr(vc.attrs, vc.fg, vc.bg) + cur_attrs = vc.attrs + cur_fg = vc.fg + cur_bg = vc.bg + end + if vc.wide_padding + # Skip padding cells (they follow a wide char) + last_y = y + last_x = x + 1 + else + output << vc.char + char_width = Buffer.display_width(vc.char) + last_y = y + last_x = x + char_width + end + # Update physical + physical[y, x] = vc.dup + end + x += 1 + end + end + + output + end + end + end +end diff --git a/lib/textbringer/terminal/window.rb b/lib/textbringer/terminal/window.rb new file mode 100644 index 00000000..28dbf499 --- /dev/null +++ b/lib/textbringer/terminal/window.rb @@ -0,0 +1,158 @@ +module Textbringer + module Terminal + class Window + attr_reader :cury, :curx + + def initialize(lines, columns, y, x) + @lines = lines + @columns = columns + @y = y + @x = x + @cury = 0 + @curx = 0 + @attrs = 0 + @fg = -1 + @bg = -1 + @buffer = ScreenBuffer.new(lines, columns) + @keypad = false + @nodelay = false + @timeout_ms = -1 + @input_reader = nil + @closed = false + end + + def keypad=(flag) + @keypad = flag + end + + def scrollok(flag) + # Not needed for our implementation + end + + def idlok(flag) + # Not needed for our implementation + end + + def nodelay=(flag) + @nodelay = flag + end + + def timeout=(ms) + @timeout_ms = ms + end + + def maxy + @lines + end + + def maxx + @columns + end + + def move(y, x) + @y = y + @x = x + end + + def resize(lines, columns) + @lines = lines + @columns = columns + @buffer = ScreenBuffer.new(lines, columns) + end + + def erase + @buffer.clear + @cury = 0 + @curx = 0 + end + + def clrtoeol + x = @curx + while x < @columns + @buffer[@cury, x] = Cell.new(" ", @attrs, @fg, @bg, false) + x += 1 + end + end + + def setpos(y, x) + @cury = y + @curx = x + end + + def addstr(s) + s.each_char do |c| + break if @cury >= @lines + w = Buffer.display_width(c) + if @curx + w > @columns + # Wrap or clip + if @cury + 1 >= @lines + break + end + @cury += 1 + @curx = 0 + end + @buffer[@cury, @curx] = Cell.new(c, @attrs, @fg, @bg, false) + if w == 2 && @curx + 1 < @columns + @buffer[@cury, @curx + 1] = Cell.new("", @attrs, @fg, @bg, true) + end + @curx += w + if @curx >= @columns + if @cury + 1 < @lines + @cury += 1 + @curx = 0 + end + end + end + end + + def attr_set(attrs, pair) + pair_info = Terminal.pair_info(pair) + @attrs = attrs + @fg = pair_info ? pair_info[0] : -1 + @bg = pair_info ? pair_info[1] : -1 + end + + def attrset(attrs) + @attrs = attrs & ~COLOR_PAIR_MASK + pair = (attrs & COLOR_PAIR_MASK) >> COLOR_PAIR_SHIFT + pair_info = Terminal.pair_info(pair) + @fg = pair_info ? pair_info[0] : -1 + @bg = pair_info ? pair_info[1] : -1 + end + + def noutrefresh + # Copy our local buffer to the global virtual screen + if Terminal.virtual_screen + @buffer.lines.times do |dy| + @buffer.cols.times do |dx| + sy = @y + dy + sx = @x + dx + if sy < Terminal.virtual_screen.lines && sx < Terminal.virtual_screen.cols + Terminal.virtual_screen[sy, sx] = @buffer[dy, dx].dup + end + end + end + # Record cursor position in screen coordinates for doupdate + Terminal.set_cursor(@y + @cury, @x + @curx) + end + end + + def redraw + noutrefresh + end + + def close + @closed = true + end + + def get_char + reader = Terminal.input_reader + return nil unless reader + reader.get_char( + blocking: !@nodelay, + timeout_ms: @timeout_ms + ) + end + end + end +end diff --git a/lib/textbringer/window.rb b/lib/textbringer/window.rb index 6b8bf890..f469deda 100644 --- a/lib/textbringer/window.rb +++ b/lib/textbringer/window.rb @@ -1,31 +1,11 @@ -require "curses" +require_relative "terminal" require_relative "window/fallback_characters" -unless Curses::Window.method_defined?(:attr_set) - using Module.new { - refine Curses::Window do - def attr_set(attrs, pair) - attrset(attrs | Curses.color_pair(pair)) - end - end - } -end - module Textbringer class Window Cursor = Struct.new(:y, :x) - KEY_NAMES = {} - Curses.constants.grep(/\AKEY_/).each do |name| - KEY_NAMES[Curses.const_get(name)] = - name.slice(/\AKEY_(.*)/, 1).downcase.intern - end - - HAVE_GET_KEY_MODIFIERS = defined?(Curses.get_key_modifiers) - if HAVE_GET_KEY_MODIFIERS - ALT_NUMBER_BASE = Curses::ALT_0 - ?0.ord - ALT_ALPHA_BASE = Curses::ALT_A - ?a.ord - end + KEY_NAMES = Terminal::Input::KEY_NAMES.dup @@started = false @@list = [] @@ -129,11 +109,11 @@ def self.has_colors? end def self.colors - Curses.colors + Terminal.colors end def self.set_default_colors(fg, bg) - Curses.assume_default_colors(Color[fg], Color[bg]) + Terminal.assume_default_colors(Color[fg], Color[bg]) Window.redraw end @@ -147,14 +127,9 @@ def self.start if @@started raise EditorError, "Already started" end - Curses.init_screen - Curses.noecho - Curses.raw - Curses.nonl - self.has_colors = Curses.has_colors? + Terminal.init_screen + self.has_colors = Terminal.has_colors? if has_colors? - Curses.start_color - Curses.use_default_colors load_faces else Face.define :mode_line, reverse: true @@ -180,10 +155,7 @@ def self.start win.close end @@list.clear - Curses.echo - Curses.noraw - Curses.nl - Curses.close_screen + Terminal.close_screen @@started = false end end @@ -216,15 +188,15 @@ def self.redraw end def self.update - Curses.doupdate + Terminal.doupdate end def self.lines - Curses.lines + Terminal.lines end def self.columns - Curses.cols + Terminal.cols end def self.resize @@ -251,7 +223,7 @@ def self.resize end def self.beep - Curses.beep + Terminal.beep end attr_reader :buffer, :lines, :columns, :y, :x, :window, :mode_line @@ -343,15 +315,6 @@ def current_or_minibuffer_selected? def read_event key = get_char if key.is_a?(Integer) - if HAVE_GET_KEY_MODIFIERS - if Curses::ALT_0 <= key && key <= Curses::ALT_9 - @key_buffer.push((key - ALT_NUMBER_BASE).chr) - return "\e" - elsif Curses::ALT_A <= key && key <= Curses::ALT_Z - @key_buffer.push((key - ALT_ALPHA_BASE).chr) - return "\e" - end - end KEY_NAMES[key] || key else key&.encode(Encoding::UTF_8) @@ -678,8 +641,8 @@ def shrink_if_larger_than_buffer private def initialize_window(num_lines, num_columns, y, x) - @window = Curses::Window.new(num_lines - 1, num_columns, y, x) - @mode_line = Curses::Window.new(1, num_columns, y + num_lines - 1, x) + @window = Terminal::Window.new(num_lines - 1, num_columns, y, x) + @mode_line = Terminal::Window.new(1, num_columns, y + num_lines - 1, x) end def framer @@ -802,8 +765,6 @@ def compose_character(point, cury, curx, c) when /\p{Variation_Selector}/ c += nextc when /[\p{Mn}\p{Me}]/ # nonspacing & enclosing marks - # Normalize パ (U+30CF + U+309A) to パ (U+30D1) so that curses can - # caluculate display width correctly. newc = (c + nextc).unicode_normalize(:nfc) return c if newc.size != c.size c = newc @@ -933,32 +894,11 @@ def delete_marks def get_char if @key_buffer.empty? - Curses.save_key_modifiers(true) if HAVE_GET_KEY_MODIFIERS - begin - need_retry = false - if @raw_key_buffer.empty? - key = @window.get_char - else - key = @raw_key_buffer.shift - end - if HAVE_GET_KEY_MODIFIERS - mods = Curses.get_key_modifiers - if key.is_a?(String) && key.ascii_only? - if (mods & Curses::PDC_KEY_MODIFIER_CONTROL) != 0 - key = key == ?? ? "\x7f" : (key.ord & 0x9f).chr - end - if (mods & Curses::PDC_KEY_MODIFIER_ALT) != 0 - if key == "\0" - # Alt + `, Alt + < etc. return NUL, so ignore it. - need_retry = true - else - @key_buffer.push(key) - key = "\e" - end - end - end - end - end while need_retry + if @raw_key_buffer.empty? + key = @window.get_char + else + key = @raw_key_buffer.shift + end key else @key_buffer.shift @@ -1101,7 +1041,7 @@ def editable_columns private def initialize_window(num_lines, num_columns, y, x) - @window = Curses::Window.new(num_lines, num_columns, y, x) + @window = Terminal::Window.new(num_lines, num_columns, y, x) end def escape(s) diff --git a/test/test_helper.rb b/test/test_helper.rb index 977d2044..ad5c3231 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,7 +1,6 @@ require "simplecov" require "test/unit" require "tmpdir" -require "curses" if ENV["RBS_TRACE"] == "1" require "rbs-trace" @@ -19,114 +18,85 @@ end SimpleCov.start("textbringer") -module Curses - if defined?(Curses.get_key_modifiers) - class << self - undef get_key_modifiers - undef save_key_modifiers - end - else - KEY_OFFSET = 0xec00 - ALT_0 = KEY_OFFSET + 0x97 - ALT_9 = KEY_OFFSET + 0xa0 - ALT_A = KEY_OFFSET + 0xa1 - ALT_Z = KEY_OFFSET + 0xba - ALT_NUMBER_BASE = ALT_0 - ?0.ord - ALT_ALPHA_BASE = ALT_A - ?a.ord - - PDC_KEY_MODIFIER_SHIFT = 1 - PDC_KEY_MODIFIER_CONTROL = 2 - PDC_KEY_MODIFIER_ALT = 4 - PDC_KEY_MODIFIER_NUMLOCK = 8 - end - - @key_modifiers = 0 - - def self.save_key_modifiers(flag) - end - - def self.get_key_modifiers - @key_modifiers - end - - def self.set_key_modifiers(key_modifiers) - @key_modifiers = key_modifiers - end -end - require "textbringer" -module Curses - @fake_lines = 24 - @fake_cols = 80 - @fake_colors = 256 - @fake_default_colors = [-1, -1] -end +module Textbringer + module Terminal + @fake_lines = 24 + @fake_cols = 80 + @fake_colors = 256 + @fake_default_colors = [-1, -1] -class << Curses - [ - :init_screen, :close_screen, - :echo, :noecho, - :raw, :noraw, - :nl, :nonl, - :unget_char, - :start_color, - :use_default_colors, - :init_pair, - :beep, - :doupdate - ].each do |name| - undef_method name - define_method(name) do |*args| - end - end + class << self + [ + :init_screen, :close_screen, :reinit_screen, + :echo, :noecho, + :raw, :noraw, + :nl, :nonl, + :unget_char, + :start_color, + :use_default_colors, + :beep, + :doupdate, + :save_key_modifiers, + ].each do |name| + undef_method name if method_defined?(name) + define_method(name) do |*args| + end + end - undef lines - def lines - @fake_lines - end + undef_method :init_pair if method_defined?(:init_pair) + define_method(:init_pair) do |*args| + end - def lines=(lines) - @fake_lines = lines - end + undef_method :lines + def lines + @fake_lines + end - undef cols - def cols - @fake_cols - end + def lines=(lines) + @fake_lines = lines + end - def cols=(cols) - @fake_cols = cols - end + undef_method :cols + def cols + @fake_cols + end - undef has_colors? - def has_colors? - true - end + def cols=(cols) + @fake_cols = cols + end - undef color_pair - def color_pair(n) - 0 - end + undef_method :has_colors? + def has_colors? + true + end - undef colors - def colors - @fake_colors - end + undef_method :color_pair + def color_pair(n) + 0 + end - def colors=(colors) - @fake_colors = colors - end + undef_method :colors + def colors + @fake_colors + end - if defined?(Curses.assume_default_colors) - undef assume_default_colors - end - def assume_default_colors(fg, bg) - @fake_default_colors = [fg, bg] - end + def colors=(colors) + @fake_colors = colors + end - def default_colors - @fake_default_colors + if method_defined?(:assume_default_colors) + undef_method :assume_default_colors + end + def assume_default_colors(fg, bg) + @fake_default_colors = [fg, bg] + end + + def default_colors + @fake_default_colors + end + end end end @@ -165,7 +135,7 @@ def call_read_event_method(read_event_method) end end - class FakeCursesWindow + class FakeTerminalWindow attr_reader :cury, :curx, :contents def initialize(lines, columns, y, x) @@ -224,10 +194,10 @@ def push_key(key) def method_missing(mid, *args) end end - ::Curses.send(:remove_const, :Window) - ::Curses.const_set(:Window, FakeCursesWindow) + Terminal.send(:remove_const, :Window) if Terminal.const_defined?(:Window) + Terminal.const_set(:Window, FakeTerminalWindow) - class FakeCursesPad + class FakeTerminalPad attr_reader :cury, :curx, :contents def initialize(lines, columns) @@ -277,19 +247,17 @@ def noutrefresh(pad_min_y, pad_min_x, screen_min_y, screen_min_x, screen_max_y, def method_missing(mid, *args) end end - if ::Curses.const_defined?(:Pad) - ::Curses.send(:remove_const, :Pad) - end - ::Curses.const_set(:Pad, FakeCursesPad) - + Terminal.send(:remove_const, :Pad) if Terminal.const_defined?(:Pad) + Terminal.const_set(:Pad, FakeTerminalPad) + class Window class << self def lines=(lines) - Curses.lines = lines + Terminal.lines = lines end def columns=(columns) - Curses.cols = columns + Terminal.cols = columns end def setup_for_test diff --git a/test/textbringer/commands/test_buffers.rb b/test/textbringer/commands/test_buffers.rb index 832913d9..250ea76a 100644 --- a/test/textbringer/commands/test_buffers.rb +++ b/test/textbringer/commands/test_buffers.rb @@ -121,7 +121,7 @@ def test_quoted_insert quoted_insert(3) assert_equal("\C-v\C-l\C-l\C-l", Buffer.current.to_s) - push_keys([Curses::KEY_LEFT]) + push_keys([Terminal::Input::KEY_CODES[:left]]) assert_raise(EditorError) do quoted_insert end diff --git a/test/textbringer/terminal/test_input.rb b/test/textbringer/terminal/test_input.rb new file mode 100644 index 00000000..ddb9050c --- /dev/null +++ b/test/textbringer/terminal/test_input.rb @@ -0,0 +1,158 @@ +require_relative "../../test_helper" + +class TestTerminalInput < Test::Unit::TestCase + def setup + @read_io, @write_io = IO.pipe + @reader = Textbringer::Terminal::Input::Reader.new(@read_io) + @reader.escape_timeout = 0.01 + end + + def teardown + @read_io.close unless @read_io.closed? + @write_io.close unless @write_io.closed? + end + + def test_read_ascii + @write_io.write("a") + assert_equal("a", @reader.get_char) + end + + def test_read_control_char + @write_io.write("\C-a") + assert_equal("\C-a", @reader.get_char) + end + + def test_read_standalone_escape + @write_io.write("\e") + assert_equal("\e", @reader.get_char) + end + + def test_read_csi_arrow_keys + key_codes = Textbringer::Terminal::Input::KEY_CODES + + @write_io.write("\e[A") + assert_equal(key_codes[:up], @reader.get_char) + + @write_io.write("\e[B") + assert_equal(key_codes[:down], @reader.get_char) + + @write_io.write("\e[C") + assert_equal(key_codes[:right], @reader.get_char) + + @write_io.write("\e[D") + assert_equal(key_codes[:left], @reader.get_char) + end + + def test_read_csi_home_end + key_codes = Textbringer::Terminal::Input::KEY_CODES + + @write_io.write("\e[H") + assert_equal(key_codes[:home], @reader.get_char) + + @write_io.write("\e[F") + assert_equal(key_codes[:end], @reader.get_char) + end + + def test_read_csi_tilde_keys + key_codes = Textbringer::Terminal::Input::KEY_CODES + + @write_io.write("\e[2~") + assert_equal(key_codes[:ic], @reader.get_char) + + @write_io.write("\e[3~") + assert_equal(key_codes[:dc], @reader.get_char) + + @write_io.write("\e[5~") + assert_equal(key_codes[:ppage], @reader.get_char) + + @write_io.write("\e[6~") + assert_equal(key_codes[:npage], @reader.get_char) + end + + def test_read_function_keys_via_csi + key_codes = Textbringer::Terminal::Input::KEY_CODES + + @write_io.write("\e[11~") + assert_equal(key_codes[:f1], @reader.get_char) + + @write_io.write("\e[15~") + assert_equal(key_codes[:f5], @reader.get_char) + + @write_io.write("\e[24~") + assert_equal(key_codes[:f12], @reader.get_char) + end + + def test_read_ss3_keys + key_codes = Textbringer::Terminal::Input::KEY_CODES + + @write_io.write("\eOP") + assert_equal(key_codes[:f1], @reader.get_char) + + @write_io.write("\eOQ") + assert_equal(key_codes[:f2], @reader.get_char) + + @write_io.write("\eOH") + assert_equal(key_codes[:home], @reader.get_char) + + @write_io.write("\eOF") + assert_equal(key_codes[:end], @reader.get_char) + end + + def test_read_alt_key + # Alt+a sends ESC followed by 'a' + @write_io.write("\ea") + result = @reader.get_char + # Should return ESC (the 'a' is buffered for next read) + assert_equal("\e", result) + assert_equal("a", @reader.get_char) + end + + def test_read_utf8 + @write_io.write("あ") + assert_equal("あ", @reader.get_char) + + @write_io.write("漢") + assert_equal("漢", @reader.get_char) + end + + def test_read_nonblocking + result = @reader.get_char(blocking: false) + assert_nil(result) + + @write_io.write("x") + result = @reader.get_char(blocking: false) + assert_equal("x", result) + end + + def test_read_with_timeout + result = @reader.get_char(timeout_ms: 10) + assert_nil(result) + + @write_io.write("y") + result = @reader.get_char(timeout_ms: 100) + assert_equal("y", result) + end + + def test_read_shift_tab + key_codes = Textbringer::Terminal::Input::KEY_CODES + @write_io.write("\e[Z") + assert_equal(key_codes[:btab], @reader.get_char) + end + + def test_key_names_mapping + key_names = Textbringer::Terminal::Input::KEY_NAMES + key_codes = Textbringer::Terminal::Input::KEY_CODES + + assert_equal(:up, key_names[key_codes[:up]]) + assert_equal(:down, key_names[key_codes[:down]]) + assert_equal(:left, key_names[key_codes[:left]]) + assert_equal(:right, key_names[key_codes[:right]]) + assert_equal(:home, key_names[key_codes[:home]]) + assert_equal(:end, key_names[key_codes[:end]]) + assert_equal(:dc, key_names[key_codes[:dc]]) + assert_equal(:ppage, key_names[key_codes[:ppage]]) + assert_equal(:npage, key_names[key_codes[:npage]]) + assert_equal(:f1, key_names[key_codes[:f1]]) + assert_equal(:f12, key_names[key_codes[:f12]]) + end +end diff --git a/test/textbringer/terminal/test_screen_buffer.rb b/test/textbringer/terminal/test_screen_buffer.rb new file mode 100644 index 00000000..2bbfe779 --- /dev/null +++ b/test/textbringer/terminal/test_screen_buffer.rb @@ -0,0 +1,106 @@ +require_relative "../../test_helper" + +class TestScreenBuffer < Test::Unit::TestCase + def setup + @buffer = Textbringer::Terminal::ScreenBuffer.new(3, 5) + end + + def test_initialize + assert_equal(3, @buffer.lines) + assert_equal(5, @buffer.cols) + cell = @buffer[0, 0] + assert_equal(" ", cell.char) + assert_equal(0, cell.attrs) + assert_equal(-1, cell.fg) + assert_equal(-1, cell.bg) + assert_equal(false, cell.wide_padding) + end + + def test_set_and_get_cell + cell = Textbringer::Terminal::Cell.new("X", 0, 1, 2, false) + @buffer[1, 3] = cell + assert_equal("X", @buffer[1, 3].char) + assert_equal(1, @buffer[1, 3].fg) + assert_equal(2, @buffer[1, 3].bg) + end + + def test_clear + @buffer[0, 0] = Textbringer::Terminal::Cell.new("A", 0, 1, 2, false) + @buffer.clear + assert_equal(" ", @buffer[0, 0].char) + assert_equal(-1, @buffer[0, 0].fg) + end + + def test_clear_row + @buffer[1, 0] = Textbringer::Terminal::Cell.new("B", 0, 3, 4, false) + @buffer[1, 1] = Textbringer::Terminal::Cell.new("C", 0, 3, 4, false) + @buffer.clear_row(1) + assert_equal(" ", @buffer[1, 0].char) + assert_equal(" ", @buffer[1, 1].char) + end + + def test_resize + @buffer[0, 0] = Textbringer::Terminal::Cell.new("Z", 0, 5, 6, false) + @buffer.resize(4, 6) + assert_equal(4, @buffer.lines) + assert_equal(6, @buffer.cols) + # Old content preserved + assert_equal("Z", @buffer[0, 0].char) + # New cells are blank + assert_equal(" ", @buffer[3, 5].char) + end + + def test_copy_from + src = Textbringer::Terminal::ScreenBuffer.new(2, 3) + src[0, 0] = Textbringer::Terminal::Cell.new("A", 0, -1, -1, false) + src[0, 1] = Textbringer::Terminal::Cell.new("B", 0, -1, -1, false) + src[1, 0] = Textbringer::Terminal::Cell.new("C", 0, -1, -1, false) + + @buffer.copy_from(src, 0, 0, 1, 1, 2, 2) + assert_equal("A", @buffer[1, 1].char) + assert_equal("B", @buffer[1, 2].char) + assert_equal("C", @buffer[2, 1].char) + end + + def test_flush_diff_no_changes + physical = Textbringer::Terminal::ScreenBuffer.new(3, 5) + output = @buffer.flush_diff(physical) + assert_equal("", output) + end + + def test_flush_diff_with_changes + physical = Textbringer::Terminal::ScreenBuffer.new(3, 5) + @buffer[0, 0] = Textbringer::Terminal::Cell.new("X", 0, -1, -1, false) + output = @buffer.flush_diff(physical) + assert_match(/X/, output) + # Physical should now match virtual + assert_equal("X", physical[0, 0].char) + end + + def test_flush_diff_cursor_positioning + physical = Textbringer::Terminal::ScreenBuffer.new(3, 5) + @buffer[1, 2] = Textbringer::Terminal::Cell.new("Y", 0, -1, -1, false) + output = @buffer.flush_diff(physical) + # Should contain cursor positioning for row 2, col 3 (1-indexed) + assert_match(/\e\[2;3H/, output) + end + + def test_flush_diff_attributes + physical = Textbringer::Terminal::ScreenBuffer.new(3, 5) + @buffer[0, 0] = Textbringer::Terminal::Cell.new("B", + Textbringer::Terminal::A_BOLD, 1, -1, false) + output = @buffer.flush_diff(physical) + # Should contain SGR sequence with bold + assert_match(/\e\[/, output) + assert_match(/B/, output) + end + + def test_cell_equality + cell1 = Textbringer::Terminal::Cell.new("A", 0, -1, -1, false) + cell2 = Textbringer::Terminal::Cell.new("A", 0, -1, -1, false) + cell3 = Textbringer::Terminal::Cell.new("B", 0, -1, -1, false) + + assert_equal(cell1, cell2) + assert_not_equal(cell1, cell3) + end +end diff --git a/test/textbringer/test_color.rb b/test/textbringer/test_color.rb index 5a48c566..ca9e60e7 100644 --- a/test/textbringer/test_color.rb +++ b/test/textbringer/test_color.rb @@ -2,9 +2,9 @@ class TestColor < Textbringer::TestCase def test_aref - assert_equal(Curses::COLOR_BLACK, Color["black"]) - assert_equal(Curses::COLOR_MAGENTA, Color["magenta"]) - assert_equal(Curses::COLOR_WHITE, Color["white"]) + assert_equal(Terminal::COLOR_BLACK, Color["black"]) + assert_equal(Terminal::COLOR_MAGENTA, Color["magenta"]) + assert_equal(Terminal::COLOR_WHITE, Color["white"]) assert_equal(8, Color["brightblack"]) assert_equal(15, Color["brightwhite"]) @@ -23,18 +23,18 @@ def test_aref assert_equal(8, Color[8]) assert_equal(255, Color[255]) - Curses.colors = 16 + Terminal.colors = 16 assert_equal(15, Color["brightwhite"]) assert_equal(-1, Color["#000000"]) - Curses.colors = 8 - assert_equal(Curses::COLOR_WHITE, Color["white"]) + Terminal.colors = 8 + assert_equal(Terminal::COLOR_WHITE, Color["white"]) assert_equal(-1, Color["brightblack"]) assert_raise(EditorError) do Color["foo"] end ensure - Curses.colors = 256 + Terminal.colors = 256 end end diff --git a/test/textbringer/test_face.rb b/test/textbringer/test_face.rb index e320ab4e..849ecbe6 100644 --- a/test/textbringer/test_face.rb +++ b/test/textbringer/test_face.rb @@ -4,17 +4,17 @@ class TestFace < Textbringer::TestCase def test_define foo = Face.define(:foo, foreground: "yellow") assert_equal(foo, Face[:foo]) - assert_equal(0, foo.attributes & Curses::A_BOLD) - assert_equal(0, foo.attributes & Curses::A_UNDERLINE) + assert_equal(0, foo.attributes & Terminal::A_BOLD) + assert_equal(0, foo.attributes & Terminal::A_UNDERLINE) bar = Face.define(:bar, foreground: "red", bold: true) assert_equal(bar, Face[:bar]) - assert_equal(Curses::A_BOLD, bar.attributes & Curses::A_BOLD) - assert_equal(0, bar.attributes & Curses::A_UNDERLINE) + assert_equal(Terminal::A_BOLD, bar.attributes & Terminal::A_BOLD) + assert_equal(0, bar.attributes & Terminal::A_UNDERLINE) bar2 = Face.define(:bar, foreground: "green", underline: true) assert_same(bar, bar2) assert_equal(bar, Face[:bar]) - assert_equal(0, bar.attributes & Curses::A_BOLD) - assert_equal(Curses::A_UNDERLINE, bar.attributes & Curses::A_UNDERLINE) + assert_equal(0, bar.attributes & Terminal::A_BOLD) + assert_equal(Terminal::A_UNDERLINE, bar.attributes & Terminal::A_UNDERLINE) ensure Face.delete(:foo) Face.delete(:bar) diff --git a/test/textbringer/test_window.rb b/test/textbringer/test_window.rb index d0c482ad..a67187ff 100644 --- a/test/textbringer/test_window.rb +++ b/test/textbringer/test_window.rb @@ -304,30 +304,8 @@ def test_read_event @window.window.push_key("a") assert_equal("a", @window.read_event) - @window.window.push_key(Curses::KEY_RIGHT) + @window.window.push_key(Terminal::Input::KEY_CODES[:right]) assert_equal(:right, @window.read_event) - - @window.window.push_key(Curses::ALT_0 + 3) - assert_equal("\e", @window.read_event) - assert_equal("3", @window.read_event) - - @window.window.push_key(Curses::ALT_A + 5) - assert_equal("\e", @window.read_event) - assert_equal("f", @window.read_event) - - Curses.set_key_modifiers(Curses::PDC_KEY_MODIFIER_CONTROL) - @window.window.push_key("a") - assert_equal("\C-a", @window.read_event) - @window.window.push_key("?") - assert_equal("\x7f", @window.read_event) - - Curses.set_key_modifiers(Curses::PDC_KEY_MODIFIER_ALT) - @window.window.push_key("\0") - @window.window.push_key("a") - assert_equal("\e", @window.read_event) - assert_equal("a", @window.read_event) - ensure - Curses.set_key_modifiers(0) end def test_read_event_nonblock @@ -340,14 +318,6 @@ def test_wait_input @window.window.push_key("a") assert_equal("a", @window.wait_input(1)) - - Curses.set_key_modifiers(Curses::PDC_KEY_MODIFIER_ALT) - @window.window.push_key("\0") - @window.window.push_key("a") - assert_equal("\e", @window.read_event) - assert_equal("a", @window.wait_input(1)) - ensure - Curses.set_key_modifiers(0) end def test_has_input? @@ -355,14 +325,6 @@ def test_has_input? @window.window.push_key("a") assert_equal(true, @window.has_input?) - - Curses.set_key_modifiers(Curses::PDC_KEY_MODIFIER_ALT) - @window.window.push_key("\0") - @window.window.push_key("a") - assert_equal("\e", @window.read_event) - assert_equal(true, @window.has_input?) - ensure - Curses.set_key_modifiers(0) end def test_echo_area_redisplay @@ -426,10 +388,10 @@ def test_s_start def test_s_set_default_colors Window.set_default_colors("black", "white") - assert_equal([0, 7], Curses.default_colors) + assert_equal([0, 7], Terminal.default_colors) Window.set_default_colors("default", "default") - assert_equal([-1, -1], Curses.default_colors) + assert_equal([-1, -1], Terminal.default_colors) end private diff --git a/textbringer.gemspec b/textbringer.gemspec index 2cc319bf..edbcdd5d 100644 --- a/textbringer.gemspec +++ b/textbringer.gemspec @@ -26,7 +26,6 @@ Gem::Specification.new do |spec| spec.add_runtime_dependency "irb" spec.add_runtime_dependency "nkf" spec.add_runtime_dependency "drb" - spec.add_runtime_dependency "curses", ">= 1.2.7" spec.add_runtime_dependency "unicode-display_width", ">= 1.1" spec.add_runtime_dependency "clipboard", ">= 1.1" spec.add_runtime_dependency "fiddle"