class Textbringer::Window

Constants

ALT_ALPHA_BASE
ALT_NUMBER_BASE
HAVE_GET_KEY_MODIFIERS
KEY_NAMES

Attributes

bottom_of_window[R]
buffer[R]
columns[R]
lines[R]
mode_line[R]
top_of_window[R]
window[R]
x[R]
y[R]

Public Class Methods

beep() click to toggle source
# File lib/textbringer/window.rb, line 213
def self.beep
  Curses.beep
end
colors() click to toggle source
# File lib/textbringer/window.rb, line 106
def self.colors
  Curses.colors
end
columns() click to toggle source
# File lib/textbringer/window.rb, line 186
def self.columns
  Curses.cols
end
current() click to toggle source
# File lib/textbringer/window.rb, line 32
def self.current
  @@current
end
current=(window) click to toggle source
# File lib/textbringer/window.rb, line 36
def self.current=(window)
  if window.deleted?
    window = @@list.first
  end
  @@current.save_point if @@current && !@@current.deleted?
  @@current = window
  @@current.restore_point
  Buffer.current = window.buffer
end
delete_other_windows() click to toggle source
# File lib/textbringer/window.rb, line 69
def self.delete_other_windows
  if @@current.echo_area?
    raise EditorError, "Can't expand the echo area to full screen"
  end
  @@list.delete_if do |window|
    if window.current? || window.echo_area?
      false
    else
      window.delete
      true
    end
  end
  @@current.move(0, 0)
  @@current.resize(Window.lines - 1, @@current.columns)
end
delete_window(target = @@current) click to toggle source
# File lib/textbringer/window.rb, line 46
def self.delete_window(target = @@current)
  if target.echo_area?
    raise EditorError, "Can't delete the echo area"
  end
  if @@list.size == 2
    raise EditorError, "Can't delete the sole window"
  end
  i = @@list.index(target)
  return if i.nil?
  if i == 0
    window = @@list[1]
    window.move(0, 0)
  else
    window = @@list[i - 1]
  end
  window.resize(target.lines + window.lines, window.columns)
  target.delete
  @@list.delete_at(i)
  if target == @@current
    self.current = window
  end
end
echo_area() click to toggle source
# File lib/textbringer/window.rb, line 94
def self.echo_area
  @@echo_area
end
has_colors=(value) click to toggle source
# File lib/textbringer/window.rb, line 98
def self.has_colors=(value)
  @@has_colors = value
end
has_colors?() click to toggle source
# File lib/textbringer/window.rb, line 102
def self.has_colors?
  @@has_colors
end
lines() click to toggle source
# File lib/textbringer/window.rb, line 182
def self.lines
  Curses.lines
end
list(include_echo_area: false) click to toggle source
# File lib/textbringer/window.rb, line 24
def self.list(include_echo_area: false)
  if include_echo_area
    @@list.dup
  else
    @@list.reject(&:echo_area?)
  end
end
load_faces() click to toggle source
# File lib/textbringer/window.rb, line 115
def self.load_faces
  require_relative "faces/basic"
  require_relative "faces/programming"
end
new(lines, columns, y, x) click to toggle source
# File lib/textbringer/window.rb, line 220
def initialize(lines, columns, y, x)
  @lines = lines
  @columns = columns
  @y = y
  @x = x
  initialize_window(lines, columns, y, x)
  @window.keypad = true
  @window.scrollok(false)
  @window.idlok(true)
  @buffer = nil
  @top_of_window = nil
  @bottom_of_window = nil
  @point_mark = nil
  @deleted = false
  @raw_key_buffer = []
  @key_buffer = []
end
other_window() click to toggle source
# File lib/textbringer/window.rb, line 85
def self.other_window
  i = @@list.index(@@current)
  begin
    i += 1
    window = @@list[i % @@list.size]
  end while !window.active?
  self.current = window
end
redisplay() click to toggle source
# File lib/textbringer/window.rb, line 160
def self.redisplay
  return if Controller.current.executing_keyboard_macro?
  return if Window.current.has_input?
  @@list.each do |window|
    window.redisplay unless window.current?
  end
  current.redisplay
  update
end
redraw() click to toggle source
# File lib/textbringer/window.rb, line 170
def self.redraw
  @@list.each do |window|
    window.redraw unless window.current?
  end
  current.redraw
  update
end
resize() click to toggle source
# File lib/textbringer/window.rb, line 190
def self.resize
  @@list.delete_if do |window|
    if !window.echo_area? &&
        window.y > Window.lines - CONFIG[:window_min_height]
      window.delete
      true
    else
      false
    end
  end
  @@list.each_with_index do |window, i|
    unless window.echo_area?
      if i < @@list.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
set_default_colors(fg, bg) click to toggle source
# File lib/textbringer/window.rb, line 110
def self.set_default_colors(fg, bg)
  Curses.assume_default_colors(Color[fg], Color[bg])
  Window.redraw
end
start() { || ... } click to toggle source
# File lib/textbringer/window.rb, line 120
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?
  if has_colors?
    Curses.start_color
    Curses.use_default_colors
    load_faces
  end
  begin
    window =
      Textbringer::Window.new(Window.lines - 1, Window.columns, 0, 0)
    window.buffer = Buffer.new_buffer("*scratch*")
    @@list.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
    @@list.push(@@echo_area)
    @@started = true
    yield
  ensure
    @@list.each do |win|
      win.close
    end
    @@list.clear
    Curses.echo
    Curses.noraw
    Curses.nl
    Curses.close_screen
    @@started = false
  end
end
update() click to toggle source
# File lib/textbringer/window.rb, line 178
def self.update
  Curses.doupdate
end

Public Instance Methods

active?() click to toggle source
# File lib/textbringer/window.rb, line 242
def active?
  true
end
buffer=(buffer) click to toggle source
# File lib/textbringer/window.rb, line 265
def buffer=(buffer)
  delete_marks
  @buffer = buffer
  @top_of_window = @buffer.new_mark(@buffer.point_min)
  if @buffer[:top_of_window]
    @top_of_window.location = @buffer[:top_of_window].location
  end
  @bottom_of_window = @buffer.new_mark(@buffer.point_min)
  if @buffer[:bottom_of_window]
    @bottom_of_window.location = @buffer[:bottom_of_window].location
  end
  @point_mark = @buffer.new_mark
end
close() click to toggle source
# File lib/textbringer/window.rb, line 261
def close
  @window.close
end
current?() click to toggle source
# File lib/textbringer/window.rb, line 291
def current?
  self == @@current
end
delete() click to toggle source
# File lib/textbringer/window.rb, line 250
def delete
  unless @deleted
    if current?
      Window.current = @@list.first
    end
    delete_marks
    @window.close
    @deleted = true
  end
end
deleted?() click to toggle source
# File lib/textbringer/window.rb, line 246
def deleted?
  @deleted
end
echo_area?() click to toggle source
# File lib/textbringer/window.rb, line 238
def echo_area?
  false
end
enlarge(n) click to toggle source
# File lib/textbringer/window.rb, line 569
def enlarge(n)
  if n > 0
    max_height = Window.lines -
      CONFIG[:window_min_height] * (@@list.size - 2) - 1
    new_lines = [lines + n, max_height].min
    needed_lines = new_lines - lines
    resize(new_lines, columns)
    i = @@list.index(self)
    indices = (i + 1).upto(@@list.size - 2).to_a +
      (i - 1).downto(0).to_a
    indices.each do |j|
      break if needed_lines == 0
      window = @@list[j]
      extended_lines = [
        window.lines - CONFIG[:window_min_height],
        needed_lines
      ].min
      window.resize(window.lines - extended_lines, window.columns)
      needed_lines -= extended_lines
    end
    y = 0
    @@list.each do |win|
      win.move(y, win.x)
      y += win.lines
    end
  elsif n < 0 && @@list.size > 2
    new_lines = [lines + n, CONFIG[:window_min_height]].max
    diff = lines - new_lines
    resize(new_lines, columns)
    i = @@list.index(self)
    if i < @@list.size - 2
      window = @@list[i + 1]
      window.move(window.y - diff, window.x)
    else
      window = @@list[i - 1]
      move(self.y + diff, self.x)
    end
    window.resize(window.lines + diff, window.columns)
  end
end
has_input?() click to toggle source
# File lib/textbringer/window.rb, line 338
def has_input?
  if !@raw_key_buffer.empty? || !@key_buffer.empty?
    return true
  end
  @window.nodelay = true
  begin
    c = @window.get_char
    if c
      @raw_key_buffer.push(c)
    end
    !c.nil?
  ensure
    @window.nodelay = false
  end
end
highlight() click to toggle source
# File lib/textbringer/window.rb, line 354
def highlight
  @highlight_on = {}
  @highlight_off = {}
  return if !@@has_colors || !CONFIG[:syntax_highlight] || @buffer.binary?
  syntax_table = @buffer.mode.syntax_table || DEFAULT_SYNTAX_TABLE
  if @buffer.bytesize < CONFIG[:highlight_buffer_size_limit]
    base_pos = @buffer.point_min
    s = @buffer.to_s
  else
    base_pos = @buffer.point
    len = columns * (lines - 1) / 2 * 3
    s = @buffer.substring(@buffer.point, @buffer.point + len).scrub("")
  end
  return if !s.valid_encoding?
  re_str = syntax_table.map { |name, re|
    "(?<#{name}>#{re})"
  }.join("|")
  re = Regexp.new(re_str)
  names = syntax_table.keys
  s.scan(re) do
    b = base_pos + $`.bytesize
    e = b + $&.bytesize
    if b < @buffer.point && @buffer.point < e
      b = @buffer.point
    end
    name = names.find { |n| $~[n] }
    attributes = Face[name]&.attributes
    if attributes
      @highlight_on[b] = attributes
      @highlight_off[e] = attributes
    end
  end
end
move(y, x) click to toggle source
# File lib/textbringer/window.rb, line 494
def move(y, x)
  @y = y
  @x = x
  @window.move(y, x)
  @mode_line.move(y + @window.maxy, x)
end
read_event() click to toggle source
# File lib/textbringer/window.rb, line 295
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)
  end
end
read_event_nonblock() click to toggle source
# File lib/textbringer/window.rb, line 313
def read_event_nonblock
  @window.nodelay = true
  begin
    read_event
  ensure
    @window.nodelay = false
  end
end
recenter() click to toggle source
# File lib/textbringer/window.rb, line 509
def recenter
  @buffer.save_point do |saved|
    max = (lines - 1) / 2
    count = beginning_of_line_and_count(max)
    while count < max
      break if @buffer.point == 0
      @buffer.backward_char
      count += beginning_of_line_and_count(max - count - 1) + 1
    end
    @buffer.mark_to_point(@top_of_window)
  end
end
recenter_if_needed() click to toggle source
# File lib/textbringer/window.rb, line 522
def recenter_if_needed
  if @buffer.point_before_mark?(@top_of_window) ||
     @buffer.point_after_mark?(@bottom_of_window)
    recenter
  end
end
redisplay() click to toggle source
# File lib/textbringer/window.rb, line 388
def redisplay
  return if @buffer.nil?
  redisplay_mode_line
  @buffer.save_point do |saved|
    if current?
      point = saved
    else
      point = @point_mark
      @buffer.point_to_mark(@point_mark)
    end
    framer
    y = x = 0
    @buffer.point_to_mark(@top_of_window)
    highlight
    @window.erase
    @window.setpos(0, 0)
    @window.attrset(0)
    if current? && @buffer.visible_mark &&
       @buffer.point_after_mark?(@buffer.visible_mark)
      @window.attron(Curses::A_REVERSE)
    end
    while !@buffer.end_of_buffer?
      cury, curx = @window.cury, @window.curx
      if @buffer.point_at_mark?(point)
        y, x = cury, curx
        if current? && @buffer.visible_mark
          if @buffer.point_after_mark?(@buffer.visible_mark)
            @window.attroff(Curses::A_REVERSE)
          elsif @buffer.point_before_mark?(@buffer.visible_mark)
            @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(Curses::A_REVERSE)
        elsif @buffer.point_before_mark?(point)
          @window.attron(Curses::A_REVERSE)
        end
      end
      if attrs = @highlight_off[@buffer.point]
        @window.attroff(attrs)
      end
      if attrs = @highlight_on[@buffer.point]
        @window.attron(attrs)
      end
      c = @buffer.char_after
      if c == "\n"
        @window.clrtoeol
        break if cury == lines - 2   # lines include mode line
        @window.setpos(cury + 1, 0)
        @buffer.forward_char
        next
      elsif c == "\t"
        n = calc_tab_width(curx)
        c = " " * n
      else
        c = escape(c)
      end
      if curx < columns - 4
        newx = nil
      else
        newx = curx + Buffer.display_width(c)
        if newx > columns
          if cury == lines - 2
            break
          else
            @window.clrtoeol
            @window.setpos(cury + 1, 0)
          end
        end
      end
      if Buffer.display_width(c) == 0
        # ncurses on macOS prints U+FEFF, U+FE0F etc. as space,
        # so ignore it
      else
        @window.addstr(c)
      end
      break if newx == columns && cury == lines - 2
      @buffer.forward_char
    end
    if current? && @buffer.visible_mark
      @window.attroff(Curses::A_REVERSE)
    end
    @buffer.mark_to_point(@bottom_of_window)
    if @buffer.point_at_mark?(point)
      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.setpos(y, x)
    @window.noutrefresh
  end
end
redraw() click to toggle source
# File lib/textbringer/window.rb, line 489
def redraw
  @window.redraw
  @mode_line.redraw
end
resize(lines, columns) click to toggle source
# File lib/textbringer/window.rb, line 501
def resize(lines, columns)
  @lines = lines
  @columns = columns
  @window.resize(lines - 1, columns)
  @mode_line.move(@y + lines - 1, @x)
  @mode_line.resize(1, columns)
end
restore_point() click to toggle source
# File lib/textbringer/window.rb, line 287
def restore_point
  @buffer.point_to_mark(@point_mark)
end
save_point() click to toggle source
# File lib/textbringer/window.rb, line 279
def save_point
  @buffer[:top_of_window] ||= @buffer.new_mark
  @buffer[:top_of_window].location = @top_of_window.location
  @buffer[:bottom_of_window] ||= @buffer.new_mark
  @buffer[:bottom_of_window].location = @bottom_of_window.location
  @buffer.mark_to_point(@point_mark)
end
scroll_down() click to toggle source
# File lib/textbringer/window.rb, line 539
def scroll_down
  if @top_of_window.location == @buffer.point_min
    raise RangeError, "Beginning of buffer"
  end
  @buffer.point_to_mark(@top_of_window)
  @buffer.next_line
  @buffer.beginning_of_line
  @top_of_window.location = 0
end
scroll_up() click to toggle source
# File lib/textbringer/window.rb, line 529
def scroll_up
  if @bottom_of_window.location == @buffer.point_max
    raise RangeError, "End of buffer"
  end
  @buffer.point_to_mark(@bottom_of_window)
  @buffer.previous_line
  @buffer.beginning_of_line
  @buffer.mark_to_point(@top_of_window)
end
shrink(n) click to toggle source
# File lib/textbringer/window.rb, line 610
def shrink(n)
  enlarge(-n)
end
shrink_if_larger_than_buffer() click to toggle source
# File lib/textbringer/window.rb, line 614
def shrink_if_larger_than_buffer
  @buffer.save_point do
    @buffer.end_of_buffer
    @buffer.skip_re_backward(/\s/)
    count = beginning_of_line_and_count(Window.lines) + 1
    while !@buffer.beginning_of_buffer?
      @buffer.backward_char
      count += beginning_of_line_and_count(Window.lines) + 1
    end
    if lines - 1 > count
      shrink(lines - 1 - count)
    end
  end
end
split(other_lines = nil) click to toggle source
# File lib/textbringer/window.rb, line 549
def split(other_lines = nil)
  old_lines = lines
  if other_lines
    if other_lines < CONFIG[:window_min_height]
      raise EditorError, "Window too small"
    end
    new_lines = lines - other_lines
  else
    new_lines = (old_lines / 2.0).ceil
  end
  if new_lines < CONFIG[:window_min_height]
    raise EditorError, "Window too small"
  end
  resize(new_lines, columns)
  new_window = Window.new(old_lines - new_lines, columns, y + new_lines, x)
  new_window.buffer = buffer
  i = @@list.index(self)
  @@list.insert(i + 1, new_window)
end
wait_input(msecs) click to toggle source
# File lib/textbringer/window.rb, line 322
def wait_input(msecs)
  if !@raw_key_buffer.empty? || !@key_buffer.empty?
    return @raw_key_buffer.first || @key_buffer.first
  end
  @window.timeout = msecs
  begin
    c = @window.get_char
    if c
      @raw_key_buffer.push(c)
    end
    c
  ensure
    @window.timeout = -1
  end
end

Private Instance Methods

beginning_of_line_and_count(max_lines, columns = @columns) click to toggle source
# File lib/textbringer/window.rb, line 715
def beginning_of_line_and_count(max_lines, columns = @columns)
  e = @buffer.point
  @buffer.beginning_of_line
  bols = [@buffer.point]
  column = 0
  while @buffer.point < e
    c = @buffer.char_after
    if c == ?\t
      n = calc_tab_width(column)
      str = " " * n
    else
      str = escape(c)
    end
    column += Buffer.display_width(str)
    if column > columns
      # Don't forward_char if column > columns
      # to handle multibyte characters across the end of lines.
      bols.push(@buffer.point)
      column = 0
    else
      @buffer.forward_char
      if column == columns
        bols.push(@buffer.point)
        column = 0
      end
    end
  end
  if bols.size > max_lines
    @buffer.goto_char(bols[-max_lines])
    max_lines
  else
    @buffer.goto_char(bols.first)
    bols.size - 1
  end
end
calc_tab_width(column) click to toggle source
# File lib/textbringer/window.rb, line 709
def calc_tab_width(column)
  tw = @buffer[:tab_width]
  n = tw - column % tw
  n.nonzero? || tw
end
delete_marks() click to toggle source
# File lib/textbringer/window.rb, line 751
def delete_marks
  if @top_of_window
    @top_of_window.delete
    @top_of_window = nil
  end
  if @bottom_of_window
    @bottom_of_window.delete
    @bottom_of_window = nil
  end
  if @point_mark
    @point_mark.delete
    @point_mark = nil
  end
end
escape(s) click to toggle source
# File lib/textbringer/window.rb, line 692
def escape(s)
  if !s.valid_encoding?
    s = s.b
  end
  if @buffer.binary?
    s.gsub(/[\0-\b\v-\x1f\x7f]/) { |c|
      "^" + (c.ord ^ 0x40).chr
    }.gsub(/[\x80-\xff]/n) { |c|
      "<%02X>" % c.ord
    }
  else
    s.gsub(/[\0-\b\v-\x1f\x7f]/) { |c|
      "^" + (c.ord ^ 0x40).chr
    }
  end
end
framer() click to toggle source
# File lib/textbringer/window.rb, line 636
def framer
  @buffer.save_point do |saved|
    max = lines - 1   # lines include mode line
    count = beginning_of_line_and_count(max)
    new_start_loc = @buffer.point
    if @buffer.point_before_mark?(@top_of_window)
      @buffer.mark_to_point(@top_of_window)
      return
    end
    while count < max
      break if @buffer.point_at_mark?(@top_of_window)
      break if @buffer.point == 0
      new_start_loc = @buffer.point
      @buffer.backward_char
      count += beginning_of_line_and_count(max - count - 1) + 1
    end
    if count >= lines - 1     # lines include mode line
      @top_of_window.location = new_start_loc
    end
  end
end
get_char() click to toggle source
# File lib/textbringer/window.rb, line 766
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
    key
  else
    @key_buffer.shift
  end
end
initialize_window(num_lines, num_columns, y, x) click to toggle source
# File lib/textbringer/window.rb, line 631
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)
end
redisplay_mode_line() click to toggle source
# File lib/textbringer/window.rb, line 658
def redisplay_mode_line
  @mode_line.erase
  @mode_line.setpos(0, 0)
  attrs = @@has_colors ? Face[:mode_line].attributes : Curses::A_REVERSE
  @mode_line.attrset(attrs)
  @mode_line.addstr("#{@buffer.input_method_status} #{@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}] ")
  if current? || @buffer.point_at_mark?(@point_mark)
    c = @buffer.char_after
    line = @buffer.current_line
    column = @buffer.current_column
  else
    c = @buffer.char_after(@point_mark.location)
    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(" " * (columns - @mode_line.curx))
  @mode_line.attrset(0)
  @mode_line.noutrefresh
end
unicode_codepoint(c) click to toggle source
# File lib/textbringer/window.rb, line 684
def unicode_codepoint(c)
  if c.nil?
    "<EOF>"
  else
    "U+%04X" % c.ord
  end
end