lib/textbringer/window.rb in textbringer-0.1.1 vs lib/textbringer/window.rb in textbringer-0.1.2

- old
+ new

@@ -1,22 +1,48 @@ # frozen_string_literal: true -require "ncursesw" +require "curses" require "unicode/display_width" +require "fiddle/import" module Textbringer + begin + # These features should be provided by curses.gem. + module PDCurses + extend Fiddle::Importer + dlload "pdcurses.dll" + extern "unsigned long PDC_get_key_modifiers(void)" + extern "int PDC_save_key_modifiers(unsigned char)" + extern "int PDC_return_key_modifiers(unsigned char)" + + KEY_MODIFIER_SHIFT = 1 + KEY_MODIFIER_CONTROL = 2 + KEY_MODIFIER_ALT = 4 + KEY_MODIFIER_NUMLOCK = 8 + end + rescue + remove_const :PDCurses + end + class Window KEY_NAMES = {} - Ncurses.constants.grep(/\AKEY_/).each do |name| - KEY_NAMES[Ncurses.const_get(name)] = + Curses.constants.grep(/\AKEY_/).each do |name| + KEY_NAMES[Curses.const_get(name)] = name.slice(/\AKEY_(.*)/, 1).downcase.intern end - UTF8_CHAR_LEN = - Buffer::UTF8_CHAR_LEN.each_with_object(Hash.new(1)) { |(k, v), h| - h[k.ord] = v - } + ALT_IS_FUNCTION_KEY = + /mswin32|mingw32/ =~ RUBY_PLATFORM && /PDCurses/ =~ Curses::VERSION + if ALT_IS_FUNCTION_KEY + 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 + end @@windows = [] @@current = nil @@echo_area = nil @@ -86,29 +112,29 @@ def self.echo_area @@echo_area end def self.start - Ncurses.initscr - Ncurses.noecho - Ncurses.raw + Curses.init_screen + Curses.noecho + Curses.raw begin window = Textbringer::Window.new(Window.lines - 1, Window.columns, 0, 0) window.buffer = Buffer.new_buffer("*scratch*") @@windows.push(window) Window.current = window @@echo_area = Textbringer::EchoArea.new(1, Window.columns, Window.lines - 1, 0) - Buffer.minibuffer.keymap = MINIBUFFER_LOCAL_MAP - @@echo_area.buffer = Buffer.minibuffer - @@windows.push(@@echo_area) + Buffer.minibuffer.keymap = MINIBUFFER_LOCAL_MAP + @@echo_area.buffer = Buffer.minibuffer + @@windows.push(@@echo_area) yield ensure - Ncurses.echo - Ncurses.noraw - Ncurses.endwin + Curses.echo + Curses.noraw + Curses.close_screen end end def self.redisplay @@windows.each do |window| @@ -125,61 +151,64 @@ current.redraw update end def self.update - Ncurses.doupdate + Curses.doupdate end def self.lines - Ncurses.LINES + Curses.lines end def self.columns - Ncurses.COLS + Curses.cols end def self.resize @@windows.delete_if do |window| - if window.y > Window.lines - 4 + if !window.echo_area? && window.y > Window.lines - 4 window.delete true else false end end @@windows.each_with_index do |window, i| - if i < @@windows.size - 1 - window.resize(window.lines, Window.columns) - else - window.resize(Window.lines - 1 - window.y, Window.columns) + unless window.echo_area? + if i < @@windows.size - 2 + window.resize(window.lines, Window.columns) + else + window.resize(Window.lines - 1 - window.y, Window.columns) + end end end @@echo_area.move(Window.lines - 1, 0) @@echo_area.resize(1, Window.columns) end def self.beep - Ncurses.beep + Curses.beep end attr_reader :buffer, :lines, :columns, :y, :x def initialize(lines, columns, y, x) @lines = lines @columns = columns @y = y @x = x initialize_window(lines, columns, y, x) - @window.keypad(true) + @window.keypad = true @window.scrollok(false) @window.idlok(true) @buffer = nil @top_of_window = nil @bottom_of_window = nil @point_mark = nil @deleted = false + @key_buffer = [] end def echo_area? false end @@ -196,11 +225,11 @@ unless @deleted if current? Window.current = @@windows.first end delete_marks - @window.del + @window.close @deleted = true end end def buffer=(buffer) @@ -231,62 +260,65 @@ def current? self == @@current end - def getch - key = @window.getch - if key.nil? - nil - elsif key > 0xff - KEY_NAMES[key] - else - len = UTF8_CHAR_LEN[key] - if len == 1 - key - else - buf = [key] - (len - 1).times do - c = @window.getch - if c.nil? || c < 0x80 || c > 0xbf - raise EditorError, "Malformed UTF-8 input" - end - buf.push(c) + def read_char + key = get_char + if key.is_a?(Integer) + if ALT_IS_FUNCTION_KEY + if ALT_0 <= key && key <= ALT_9 + @key_buffer.push((key - ALT_NUMBER_BASE).chr) + return "\e" + elsif ALT_A <= key && key <= ALT_Z + @key_buffer.push((key - ALT_ALPHA_BASE).chr) + return "\e" end - s = buf.pack("C*").force_encoding(Encoding::UTF_8) - if s.valid_encoding? - s.ord - else - raise EditorError, "Malformed UTF-8 input" - end end + KEY_NAMES[key] || key + else + key&.encode(Encoding::UTF_8)&.tr("\r", "\n") end end - def getch_nonblock - @window.nodelay(true) + def read_char_nonblock + @window.nodelay = true begin - getch + read_char ensure - @window.nodelay(false) + @window.nodelay = false end end def wait_input(msecs) - @window.timeout(msecs) + @window.timeout = msecs begin - c = @window.getch - if c && c >= 0 - Ncurses.ungetch(c) + c = @window.get_char + if c + Curses.unget_char(c) end c ensure - @window.timeout(-1) + @window.timeout = -1 end end + def has_input? + @window.nodelay = true + begin + c = @window.get_char + if c + Curses.unget_char(c) + end + !c.nil? + ensure + @window.nodelay = false + end + end + def redisplay + return if has_input? return if @buffer.nil? redisplay_mode_line @buffer.save_point do |saved| if current? point = saved @@ -296,85 +328,85 @@ end framer y = x = 0 @buffer.point_to_mark(@top_of_window) @window.erase - @window.move(0, 0) + @window.setpos(0, 0) if current? && @buffer.visible_mark && @buffer.point_after_mark?(@buffer.visible_mark) - @window.attron(Ncurses::A_REVERSE) + @window.attron(Curses::A_REVERSE) end while !@buffer.end_of_buffer? if @buffer.point_at_mark?(point) - y, x = @window.getcury, @window.getcurx + y, x = @window.cury, @window.curx if current? && @buffer.visible_mark if @buffer.point_after_mark?(@buffer.visible_mark) - @window.attroff(Ncurses::A_REVERSE) + @window.attroff(Curses::A_REVERSE) elsif @buffer.point_before_mark?(@buffer.visible_mark) - @window.attron(Ncurses::A_REVERSE) + @window.attron(Curses::A_REVERSE) end end end if current? && @buffer.visible_mark && @buffer.point_at_mark?(@buffer.visible_mark) if @buffer.point_after_mark?(point) - @window.attroff(Ncurses::A_REVERSE) + @window.attroff(Curses::A_REVERSE) elsif @buffer.point_before_mark?(point) - @window.attron(Ncurses::A_REVERSE) + @window.attron(Curses::A_REVERSE) end end c = @buffer.char_after if c == "\n" @window.clrtoeol - break if @window.getcury == lines - 2 # lines include mode line + break if @window.cury == lines - 2 # lines include mode line elsif c == "\t" - n = calc_tab_width(@window.getcurx) + n = calc_tab_width(@window.curx) c = " " * n else c = escape(c) end @window.addstr(c) - break if @window.getcury == lines - 2 && # lines include mode line - @window.getcurx == columns + break if @window.cury == lines - 2 && # lines include mode line + @window.curx == columns @buffer.forward_char end if current? && @buffer.visible_mark - @window.attroff(Ncurses::A_REVERSE) + @window.attroff(Curses::A_REVERSE) end @buffer.mark_to_point(@bottom_of_window) if @buffer.point_at_mark?(point) - y, x = @window.getcury, @window.getcurx + y, x = @window.cury, @window.curx end if x == columns - 1 c = @buffer.char_after(point.location) if c && Buffer.display_width(c) > 1 y += 1 x = 0 end end - @window.move(y, x) + @window.setpos(y, x) @window.noutrefresh end end def redraw - @window.redrawwin - @mode_line.redrawwin + @window.redraw + @mode_line.redraw end def move(y, x) @y = y @x = x - @window.mvwin(y, x) - @mode_line.mvwin(y + @window.getmaxy, x) + @window.move(y, x) + @mode_line.move(y + @window.maxy, x) end def resize(lines, columns) @lines = lines @columns = columns @window.resize(lines - 1, columns) - @mode_line.mvwin(@y + lines - 1, @x) + @mode_line.move(@y + lines - 1, @x) @mode_line.resize(1, columns) end def recenter @buffer.save_point do |saved| @@ -424,12 +456,12 @@ end private def initialize_window(num_lines, num_columns, y, x) - @window = Ncurses::WINDOW.new(num_lines - 1, num_columns, y, x) - @mode_line = Ncurses::WINDOW.new(1, num_columns, y + num_lines - 1, 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) end def framer @buffer.save_point do |saved| max = lines - 1 # lines include mode line @@ -452,12 +484,12 @@ end end def redisplay_mode_line @mode_line.erase - @mode_line.move(0, 0) - @mode_line.attron(Ncurses::A_REVERSE) + @mode_line.setpos(0, 0) + @mode_line.attron(Curses::A_REVERSE) @mode_line.addstr("#{@buffer.name} ") @mode_line.addstr("[+]") if @buffer.modified? @mode_line.addstr("[RO]") if @buffer.read_only? @mode_line.addstr("[#{@buffer.file_encoding.name}/") @mode_line.addstr("#{@buffer.file_format}] ") @@ -470,12 +502,12 @@ line, column = @buffer.get_line_and_column(@point_mark.location) end @mode_line.addstr(unicode_codepoint(c)) @mode_line.addstr(" #{line},#{column}") @mode_line.addstr(" (#{@buffer.mode&.name || 'None'})") - @mode_line.addstr(" " * (@mode_line.getmaxx - @mode_line.getcurx)) - @mode_line.attroff(Ncurses::A_REVERSE) + @mode_line.addstr(" " * (@mode_line.maxx - @mode_line.curx)) + @mode_line.attroff(Curses::A_REVERSE) @mode_line.noutrefresh end def unicode_codepoint(c) if c.nil? @@ -506,10 +538,13 @@ end def beginning_of_line_and_count(max_lines) e = @buffer.point @buffer.beginning_of_line + if e - @buffer.point < @columns + return 0 + end s = @buffer.substring(@buffer.point, e) bols = [@buffer.point] column = 0 while @buffer.point < e c = @buffer.char_after @@ -554,10 +589,40 @@ if @point_mark @point_mark.delete @point_mark = nil end end + + def get_char + if @key_buffer.empty? + PDCurses.PDC_save_key_modifiers(1) if defined?(PDCurses) + need_retry = false + begin + key = @window.get_char + if defined?(PDCurses) + mods = PDCurses.PDC_get_key_modifiers + if key.is_a?(String) && key.ascii_only? + if (mods & PDCurses::KEY_MODIFIER_CONTROL) != 0 + key = key == ?? ? "\x7f" : (key.ord & 0x9f).chr + end + if (mods & PDCurses::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 + key + else + @key_buffer.shift + end + end end class EchoArea < Window attr_accessor :prompt attr_writer :active @@ -593,44 +658,44 @@ def redisplay return if @buffer.nil? @buffer.save_point do |saved| @window.erase - @window.move(0, 0) + @window.setpos(0, 0) if @message - @window.addstr @message + @window.addstr(escape(@message)) else - @window.addstr @prompt + @window.addstr(escape(@prompt)) @buffer.beginning_of_line while !@buffer.end_of_buffer? if @buffer.point_at_mark?(saved) - y, x = @window.getcury, @window.getcurx + y, x = @window.cury, @window.curx end c = @buffer.char_after if c == "\n" break end - @window.addstr escape(c) + @window.addstr(escape(c)) @buffer.forward_char end if @buffer.point_at_mark?(saved) - y, x = @window.getcury, @window.getcurx + y, x = @window.cury, @window.curx end - @window.move(y, x) + @window.setpos(y, x) end @window.noutrefresh end end def redraw - @window.redrawwin + @window.redraw end def move(y, x) @y = y @x = x - @window.mvwin(y, x) + @window.move(y, x) end def resize(lines, columns) @lines = lines @columns = columns @@ -638,9 +703,9 @@ end private def initialize_window(num_lines, num_columns, y, x) - @window = Ncurses::WINDOW.new(num_lines, num_columns, y, x) + @window = Curses::Window.new(num_lines, num_columns, y, x) end end end