class RubyText::Window

Reopening: Wrapper for curses windows

Wrapper for a curses window

Reopening: Coordinate handling (1-based!)

Constants

Horiz
ScreenStack

Attributes

bg[RW]
c0[R]
cols[R]
cwin[R]
fg[RW]
height[R]
r0[R]
rows[R]
scrolling[R]
width[R]

Public Class Methods

clear(win) click to toggle source
# File lib/output.rb, line 103
def self.clear(win)   # delete this?
  num = win.maxx * win.maxy - 1
  win.setpos(0, 0)
  win.addstr(' '*num)
  win.setpos(0, 0)
  win.refresh
end
colorize!(cwin, fg, bg) click to toggle source

Set up a window with fg/bg

# File lib/color.rb, line 45
def self.colorize!(cwin, fg, bg)
  cp = RubyText::Color.pair(fg, bg)
  cwin.color_set(cp)
  num = cwin.maxx * cwin.maxy
  cwin.setpos 0,0
  cwin.addstr(' '*num)
  cwin.setpos 0,0
  cwin.refresh
rescue => err
  File.open("/tmp/#{__method__}.out", "w") do |f|
    f.puts err
    f.puts err.backtrace
  end
end
main(fg: White, bg: Blue, scroll: false) click to toggle source
# File lib/window.rb, line 46
def self.main(fg: White, bg: Blue, scroll: false)
  debug "Starting #main..."
  main_win = Curses.init_screen
  Curses.start_color
  self.colorize!(main_win, fg, bg)
  rows, cols = main_win.maxy, main_win.maxx
  win = self.make(main_win, rows, cols, 0, 0, border: false,
            fg: fg, bg: bg, scroll: scroll)
  debug "...started #main"
  win
rescue => err
  File.open("/tmp/main.out", "w") {|f| f.puts err.inspect; f.puts err.backtrace } 
end
make(cwin, high, wide, r0, c0, border: true, fg: White, bg: Black, scroll: false) click to toggle source

FIXME try again to inline this

# File lib/window.rb, line 62
def self.make(cwin, high, wide, r0, c0, border: true, fg: White, bg: Black, scroll: false)
  obj = self.allocate
  obj.instance_eval do 
    @outer = @cwin = cwin
    @wide, @high, @r0, @c0 = wide, high, r0, c0
    @fg, @bg = fg, bg
    @border = border
    @rows, @cols = high, wide
    @width, @height = @cols + 2, @rows + 2 if @border
  end
  obj.scrolling(scroll)
  obj
end
new(high=nil, wide=nil, r0=0, c0=0, border=false, fg=White, bg=Blue, scroll=false) click to toggle source

Better to use Window.window IRL

# File lib/window.rb, line 24
def initialize(high=nil, wide=nil, r0=0, c0=0, border=false, 
               fg=White, bg=Blue, scroll=false)
  @wide, @high, @r0, @c0 = wide, high, r0, c0
  @border, @fg, @bg      = border, fg, bg
  @cwin = Curses::Window.new(high, wide, r0, c0)
  colorize!(fg, bg)
  if @border
    @cwin.box(Vert, Horiz)
    @outer = @cwin
    @outer.refresh
    @cwin = Curses::Window.new(high-2, wide-2, r0+1, c0+1)
    colorize!(fg, bg)
  else
    @outer = @cwin
  end
  @rows, @cols = @cwin.maxy, @cwin.maxx  # unnecessary really...
  @width, @height = @cols + 2, @rows + 2 if @border
  @scrolling = scroll
  @cwin.scrollok(scroll) 
  @cwin.refresh
end

Public Instance Methods

[](r, c) click to toggle source

def go(r0, c0)

r, c = coords(r0, c0)
save = self.rc
goto r, c 
if block_given?
  yield 
  goto *save
end

end

# File lib/output.rb, line 138
def [](r, c)
  r0, c0 = self.rc
  @cwin.setpos(r, c)
  ch = @cwin.inch
  @cwin.setpos(r0, c0)
  ch.chr
end
[]=(r, c, char) click to toggle source
# File lib/output.rb, line 146
def []=(r, c, char)
  r0, c0 = self.rc
  @cwin.setpos(r, c)
  @cwin.addch(char[0].ord|Curses::A_NORMAL)
  @cwin.setpos(r0, c0)
  @cwin.refresh
end
_putch(ch) click to toggle source
# File lib/output.rb, line 63
def _putch(ch)
  @cwin.addch(ch)
end
add_title(str, align = :center) click to toggle source
# File lib/window.rb, line 76
def add_title(str, align = :center)
  raise "No border" unless @border
  len = str.length  # What if it's too long?
  start = case align
            when :left;   1
            when :center; (@outer.maxx - len)/2
            when :right;  @outer.maxx - len - 1
          end
  @outer.setpos 0, start
  @outer.addstr str
  @outer.refresh
end
background(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0) { || ... } click to toggle source
# File lib/window.rb, line 124
def background(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0)
  saveback(high, wide, r, c)
  yield
  restback(high, wide, r, c)
end
beep() click to toggle source
# File lib/window.rb, line 173
def beep
  Curses.beep
end
bg=(sym) click to toggle source

Set background color

# File lib/color.rb, line 85
def bg=(sym)
  set_colors(@fg, sym)
end
bottom() click to toggle source

Move cursor to bottom of window

# File lib/navigation.rb, line 89
def bottom 
  r, c = rc
  rmax = self.rows - 1
  go rmax, c
end
boxme() click to toggle source
# File lib/output.rb, line 154
def boxme
  @outer.box(Vert, Horiz)
  @outer.refresh
end
center(str) click to toggle source
# File lib/output.rb, line 8
def center(str)
  r, c = self.rc
  n = @cwin.maxx - str.length
  go r, n/2
  self.puts str
end
clear() click to toggle source
# File lib/output.rb, line 111
def clear
  cwin.setpos(cwin.maxx, cwin.maxy)
  cwin.addstr(' ')
  num = cwin.maxx * cwin.maxy - 1
  cwin.addstr(' '*num)
  cwin.setpos(0, 0)
  cwin.refresh
end
colorize!(fg, bg) click to toggle source

Set up a window with fg/bg

# File lib/color.rb, line 69
def colorize!(fg, bg)
  set_colors(fg, bg)
  num = @cwin.maxx * @cwin.maxy
  self.home
  self.go(0, 0) { @cwin.addstr(' '*num) }
  @cwin.refresh
end
coords(r, c) click to toggle source

Handle special coordinate names (symbols)

# File lib/navigation.rb, line 7
def coords(r, c)
  r = case
        when r == :center
          self.rows / 2 
        when r == :top
          0
        when r == :bottom
          self.rows - 1
        else
          r
        end
  c = case
        when c == :center
          self.cols / 2 
        when c == :left
          0
        when c == :right
          self.cols - 1
        else
          c
        end
  [r, c]
end
crlf() click to toggle source
# File lib/output.rb, line 85
def crlf     # Technically not output...
  r, c = rc
  if @scrolling
    if r == @rows - 1  # bottom row
      scroll
      left!
    else
      go r+1, 0
    end
  else
    if r == @rows - 1  # bottom row
      left!
    else
      go r+1, 0
    end
  end
end
delegate_output(sym, *args) click to toggle source
# File lib/output.rb, line 21
def delegate_output(sym, *args)
  self.cwin.attrset(0)
  args = [""] if args.empty?
  args += ["\n"] if sym == :puts
  set_colors(@fg, @bg)
  meth = sym == :p ? :inspect : :to_s
  args.map! {|x| effect?(x) ? x : x.send(meth) }
  args.each do |arg|  
    if arg.is_a?(RubyText::Effects)
      arg.set(self)
    else
      arg.effect.set(self) if arg.respond_to? :effect
      arg.each_char {|ch| ch == "\n" ? crlf : @cwin.addch(ch) }
      @cwin.refresh
    end
  end
  crlf if sym == :p   # no implicit newline
  set_colors(@fg, @bg)
  @cwin.refresh
end
down(n=1) click to toggle source

Move cursor down

# File lib/navigation.rb, line 61
def down(n=1)
  r, c = rc
  go r+n, c
end
down!() click to toggle source

Move cursor to bottom of window

# File lib/navigation.rb, line 103
def down!
  bottom
end
effect?(arg) click to toggle source

FIXME Please refactor the Hal out of this.

# File lib/output.rb, line 17
def effect?(arg)
  arg.is_a?(RubyText::Effects)
end
fg=(sym) click to toggle source

Set foreground color

# File lib/color.rb, line 79
def fg=(sym)
  set_colors(sym, @bg)
end
flash() click to toggle source
# File lib/window.rb, line 177
def flash
  Curses.flash
end
gets(history: [], limit: nil, tab: [], default: "", capture: []) click to toggle source
# File lib/output.rb, line 266
def gets(history: [], limit: nil, tab: [], default: "", capture: [])
  # needs improvement
  # echo assumed to be OFF, keypad ON
  @history = history
  gs = GetString.new(self, default, history: history, limit: limit, tab: tab, 
                     capture: capture)
  count = 0
  loop do
    count += 1      # Escape and 'capture' chars have special meaning if first char
    ch = self.getch
    case ch
      when *capture 
        return ch if count == 1
        gs.add(ch)
      when Escape
        return Escape if count == 1
        gs.enter
        break
      when CtlD
        return CtlD if count == 1
        gs.enter
        break
      when Enter
        gs.enter
        break
      when BS, DEL, 63   # backspace, del, ^H (huh?)
        gs.backspace
      when Tab
        gs.complete
      when Left
        gs.left_arrow
      when Right
        gs.right_arrow
      when Up
        next if @history.nil?  # move this?
        gs.history_prev
      when Down
        next if @history.nil?  # move this?
        gs.history_next
      when Integer
        Curses.beep
      else
        gs.add(ch)
    end
  end
  gs.value
rescue => err
  str = err.to_s + "\n" + err.backtrace.join("\n")
  raise str
end
go(r0, c0) { || ... } click to toggle source

Go to specified row/column in current window,

execute block, and return cursor
# File lib/navigation.rb, line 42
def go(r0, c0)
  r, c = coords(r0, c0)
  save = self.rc
  goto r, c 
  if block_given?
    yield 
    goto *save
  end
end
goto(r, c) click to toggle source

Go to specified row/column in current window

# File lib/navigation.rb, line 33
def goto(r, c)  # only accepts numbers!
  @cwin.setpos(r, c)
  @cwin.refresh
end
home() click to toggle source

Move cursor to home (upper left)

# File lib/navigation.rb, line 124
def home
  go 0, 0
end
left(n=1) click to toggle source

Move cursor left

# File lib/navigation.rb, line 68
def left(n=1)
  r, c = rc
  go r, c-n
end
left!() click to toggle source

Move cursor to far left of window

# File lib/navigation.rb, line 109
def left!
  r, c = rc
  go r, 0
end
menu(r: :center, c: :center, items:, curr: 0, border: true, sticky: false, title: nil, fg: Green, bg: Black) click to toggle source

Simple menu with rows of strings (or Procs)

multimenu(r: :center, c: :center, items:, curr: 0, selected: [], title: nil, sel_fg: Yellow, fg: White, bg: Blue) click to toggle source

Menu for multiple selections (buggy/unused?)

# File lib/menu.rb, line 138
def multimenu(r: :center, c: :center, 
              items:, curr: 0, selected: [],
              title: nil, sel_fg: Yellow, fg: White, bg: Blue)
  RubyText.hide_cursor
  high = items.size + 2
  wide = items.map(&:length).max + 5
  tlen = title.length + 8 rescue 0
  wide = [wide, tlen].max
  row, col = self.coords(r, c)
  row = row - high/2 if r == :center
  col = col - wide/2 if c == :center
  r, c = row, col
  self.saveback(high, wide, r, c)
  mr, mc = r+self.r0, c+self.c0
  mwin = RubyText.window(high, wide, r: mr, c: mc, 
                         fg: fg, bg: bg, title: title)
  Curses.stdscr.keypad(true)
  sel = curr
  max = items.size - 1
  loop do
    RubyText.hide_cursor  # FIXME should be unnecessary
    items.each.with_index do |item, row|
      mwin.go row, 0
      style = (sel == row) ? :reverse : :normal
      color = selected.include?(row) ? sel_fg : fg
      label = (" "*2 + item + " "*8)[0..wide-1]
      mwin.print fx(label, color, style)
    end
    ch = getch
    case ch
      when Up
        sel -= 1 if sel > 0
      when Down
        sel += 1 if sel < max
      when Esc
        self.restback(high, wide, r, c)
        RubyText.show_cursor
        return []
      when Enter
        self.restback(high, wide, r, c)
        RubyText.show_cursor
        return selected.map {|i| items[i] }
      when " "
        selected << sel
        sel += 1 if sel < max
      else Curses.beep
    end
    RubyText.show_cursor
  end
end
output(&block) click to toggle source
# File lib/output.rb, line 120
def output(&block)
  $stdscr = self
  block.call
  $stdscr = STDSCR
end
p(*args) click to toggle source
# File lib/output.rb, line 50
def p(*args)
  delegate_output(:p, *args)
end
print(*args) click to toggle source
putch(ch, r: nil, c: nil, fx: nil) click to toggle source
# File lib/output.rb, line 67
def putch(ch, r: nil, c: nil, fx: nil)
  debug("putch: #{[ch, r, c, fx].inspect}")
  if r.nil? && c.nil? && fx.nil?
    _putch(ch) 
  else
    r0, c0 = self.rc
    r ||= r0
    c ||= c0
    go(r, c) do
      fx.set(self) if fx
      val = fx.value rescue 0
      @cwin.addch(ch.ord|val)
    end
    fx.reset(self) if fx
  end
  @cwin.refresh
end
puts(*args) click to toggle source
# File lib/output.rb, line 42
def puts(*args)
  delegate_output(:puts, *args)
end
radio_menu(r: :center, c: :center, items:, curr: 0, border: true, title: nil, fg: Green, bg: Black) click to toggle source

Menu to choose a single setting and retain it

# File lib/menu.rb, line 200
def radio_menu(r: :center, c: :center, items:, curr: 0, 
         # Handle current value better?
         border: true,
         title: nil, fg: Green, bg: Black)
  RubyText.hide_cursor
  if items.is_a?(Hash)
    results = items.values
    items = items.keys
    hash_flag = true
  else
    results = items
  end
  
  high = items.size
  wide = items.map(&:length).max + 3
  high += 2 if border
  wide += 2 if border

  tlen = title.length + 8 rescue 0
  wide = [wide, tlen].max
  row, col = self.coords(r, c)
  row = row - high/2 if r == :center
  col = col - wide/2 if c == :center
  r, c = row, col
  self.saveback(high, wide, r, c)
  mr, mc = r+self.r0, c+self.c0
  title = nil unless border
  mwin = RubyText.window(high, wide, r: mr, c: mc, border: border,
                         fg: fg, bg: bg, title: title)
  Curses.stdscr.keypad(true)
  sel = curr
  max = items.size - 1
  loop do
    RubyText.hide_cursor  # FIXME should be unnecessary
    items.each.with_index do |item, row|
      mark = row == curr ? ">" : " "
      mwin.go row, 0
      style = (sel == row) ? :reverse : :normal
      label = "#{mark} #{item}"
      mwin.print fx(label, style)
    end
    ch = getch
    case ch
      when Up
        sel -= 1 if sel > 0
      when Down
        sel += 1 if sel < max
      when Esc
        self.restback(high, wide, r, c)
        RubyText.show_cursor
        return [nil, nil]
      when " "
        mwin[curr, 0] = " "
        mwin[sel, 0] = ">"
        curr = sel
      when Enter
        self.restback(high, wide, r, c)
        RubyText.show_cursor
        choice = results[sel]
        return [sel, choice] if choice.is_a? String
        result = choice.call
        return [nil, nil] if result.nil? || result.empty?
        return result
      else Curses.beep
    end
    RubyText.show_cursor
  end
end
rc() click to toggle source

Return current row/column

# File lib/navigation.rb, line 130
def rc
  [@cwin.cury, @cwin.curx]
end
rcprint(r, c, *args) click to toggle source
# File lib/output.rb, line 54
def rcprint(r, c, *args)
  self.go(r, c) { self.print *args }
end
rcprint!(r, c, *args) click to toggle source
# File lib/output.rb, line 58
def rcprint!(r, c, *args)
  self.go(r, c)  # Cursor isn't restored
  self.print *args
end
refresh() click to toggle source
# File lib/output.rb, line 159
def refresh
  @cwin.refresh
end
restback(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0) click to toggle source
# File lib/window.rb, line 144
def restback(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0)
  save = ScreenStack.pop
  pos = save.shift
  0.upto(high-1) do |h|
    line = ""
    0.upto(wide-1) {|w| line << save.shift }
    row, col = h+r-1, c-1
    row += 1 if self == STDSCR   # wtf?
    col += 1 if self == STDSCR
    self.go row, col
    self.print line
  end
  self.go *pos
  @cwin.refresh
rescue => err
  puts "Error!"
  puts err
  puts err.backtrace.join("\n")
  sleep 8
end
right(n=1) click to toggle source

Move cursor right

# File lib/navigation.rb, line 75
def right(n=1)
  r, c = rc
  go r, c+n
end
right!() click to toggle source

Move cursor to far left of window

# File lib/navigation.rb, line 116
def right!
  r, c = rc
  cmax = self.cols - 1
  go r, cmax
end
saveback(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0) click to toggle source
# File lib/window.rb, line 130
def saveback(high=STDSCR.rows, wide=STDSCR.cols, r=0, c=0)
  debug "saveback: #{[high, wide, r, c].inspect}"
  save = [self.rc]
  0.upto(high-1) do |h|
    0.upto(wide-1) do |w|
      row, col = h+r-1, w+c-1
      row += 1 if self == STDSCR   # wtf?
      col += 1 if self == STDSCR
      save << self[row, col]
    end
  end
  ScreenStack.push save
end
screen_text(file = nil) click to toggle source
# File lib/window.rb, line 111
def screen_text(file = nil)   # rename?
  lines = []
  0.upto(self.rows-1) do |r|
    line = ""
    0.upto(self.cols-1) do |c|
      line << self[r, c]
    end
    lines << line
  end
  File.open(file, "w") {|f| f.puts lines }  if file
  lines
end
scroll(n=1) click to toggle source
# File lib/window.rb, line 96
def scroll(n=1)
  if n < 0
    @cwin.scrl(n)
    (-n).times {|i| rcprint i, 0, (' '*@cols) }
  else
    n.times do |i|
      @cwin.scroll
      scrolling(false)
      rcprint @rows-1, 0, (' '*@cols)
      scrolling
    end
  end
  @cwin.refresh
end
set_colors(fg, bg) click to toggle source

Assign color pair to curses window

# File lib/color.rb, line 62
def set_colors(fg, bg)
  cp = RubyText::Color.pair(fg, bg)
  @cwin.color_set(cp)
end
top() click to toggle source

Move cursor to top of window

# File lib/navigation.rb, line 82
def top
  r, c = rc
  go 0, c
end
topmenu(items:, curr: 0, fg: Green, bg: Black) click to toggle source

One-line menu at top of window

# File lib/menu.rb, line 11
def topmenu(items:, curr: 0, fg: Green, bg: Black)
  r, c = 0, 0
  high = 1

  RubyText.hide_cursor
  if items.is_a?(Hash)
    results = items.values
    items = items.keys
    hash_flag = true
  else
    results = items
  end

  width = 0   # total width
  cols = []   # start-column of each item
  items.each do |item| 
    cols << width
    iwide = item.to_s.length + 2
    width += iwide
  end

  r, c = self.coords(r, c)
  self.saveback(high, width, r, c)
  mr, mc = r+self.r0, c+self.c0
  mwin = RubyText.window(high, width, r: mr, c: mc, fg: fg, bg: bg, border: false, title: nil)
  Curses.stdscr.keypad(true)
  sel = curr
  max = items.size - 1
  loop do
    items.each.with_index do |item, num|
      item = " #{item} "
      mwin.go 0, cols[num]
      style = (sel == num) ? :reverse : :normal
      mwin.print fx(item, style)
    end
    ch = getch
    case ch
      when Left
        sel -= 1 if sel > 0
      when Right
        sel += 1 if sel < max
      when Esc, " "   # spacebar also quits
        self.restback(high, width, r, c)
        RubyText.show_cursor
        return [nil, nil]
      when Down, Enter
        self.restback(high, width, r, c)
        RubyText.show_cursor
        choice = results[sel]
        return [sel, choice] if choice.is_a? String
        result = choice.call
        next if result.nil?
        next if result.empty?
        return result
    else Curses.beep
    end
    RubyText.show_cursor
  end
end
up(n=1) click to toggle source

Move cursor up

# File lib/navigation.rb, line 54
def up(n=1)
  r, c = rc
  go r-n, c
end
up!() click to toggle source

Move cursor to top of window

# File lib/navigation.rb, line 97
def up!
  top
end
yesno() click to toggle source

Simple yes/no decision

# File lib/menu.rb, line 191
def yesno
  # TODO: Accept YyNn
  r, c = STDSCR.rc
  num, str = STDSCR.menu(r: r, c: c+6, items: ["yes", "no"])
  num == 0
end