class Redwood::BufferManager

Constants

CONTINUE_IN_BUFFER_SEARCH_KEY

we have to define the key used to continue in-buffer search here, because it has special semantics that BufferManager deals with—current searches are canceled by any keypress except this one.

Attributes

focus_buf[R]

Public Class Methods

new() click to toggle source
# File lib/sup/buffer.rb, line 148
def initialize
  @name_map = {}
  @buffers = []
  @focus_buf = nil
  @dirty = true
  @minibuf_stack = []
  @minibuf_mutex = Mutex.new
  @textfields = {}
  @flash = nil
  @shelled = @asking = false
  @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
  @sigwinch_happened = false
  @sigwinch_mutex = Mutex.new
end

Public Instance Methods

[](n;) click to toggle source
# File lib/sup/buffer.rb, line 231
def [] n; @name_map[n]; end
[]=(n, b) click to toggle source
# File lib/sup/buffer.rb, line 232
def []= n, b
  raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
  raise ArgumentError, "title must be a string" unless n.is_a? String
  @name_map[n] = b
end
ask(domain, question, default=nil, &block) click to toggle source

for simplicitly, we always place the question at the very bottom of the screen

# File lib/sup/buffer.rb, line 538
def ask domain, question, default=nil, &block
  raise "impossible!" if @asking
  raise "Question too long" if Ncurses.cols <= question.length
  @asking = true

  @textfields[domain] ||= TextField.new
  tf = @textfields[domain]
  completion_buf = nil

  status, title = get_status_and_title @focus_buf

  Ncurses.sync do
    tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
    @dirty = true # for some reason that blanks the whole fucking screen
    draw_screen :sync => false, :status => status, :title => title
    tf.position_cursor
    Ncurses.refresh
  end

  while true
    c = Ncurses::CharCode.get
    next unless c.present? # getch timeout
    break unless tf.handle_input c # process keystroke

    if tf.new_completions?
      kill_buffer completion_buf if completion_buf

      shorts = tf.completions.map { |full, short| short }
      prefix_len = shorts.shared_prefix(caseless=true).length

      mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
      completion_buf = spawn "<completions>", mode, :height => 10

      draw_screen :skip_minibuf => true
      tf.position_cursor
    elsif tf.roll_completions?
      completion_buf.mode.roll
      draw_screen :skip_minibuf => true
      tf.position_cursor
    end

    Ncurses.sync { Ncurses.refresh }
  end

  kill_buffer completion_buf if completion_buf

  @dirty = true
  @asking = false
  Ncurses.sync do
    tf.deactivate
    draw_screen :sync => false, :status => status, :title => title
  end
  tf.value.tap { |x| x }
end
ask_for_account(domain, question) click to toggle source
# File lib/sup/buffer.rb, line 529
def ask_for_account domain, question
  completions = AccountManager.user_emails
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, ""
  answer = AccountManager.default_account.email if answer == ""
  AccountManager.account_for Person.from_address(answer).email if answer
end
ask_for_contacts(domain, question, default_contacts=[]) click to toggle source
# File lib/sup/buffer.rb, line 512
def ask_for_contacts domain, question, default_contacts=[]
  default = default_contacts.is_a?(String) ? default_contacts : default_contacts.map { |s| s.to_s }.join(", ")
  default += " " unless default.empty?

  recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
  contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }

  completions = (recent + contacts).flatten.uniq
  completions += HookManager.run("extra-contact-addresses") || []

  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default

  if answer
    answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
  end
end
ask_for_filename(domain, question, default=nil, allow_directory=false) click to toggle source
# File lib/sup/buffer.rb, line 453
def ask_for_filename domain, question, default=nil, allow_directory=false
  answer = ask domain, question, default do |s|
    if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
      full = $1
      name = $2.empty? ? Etc.getlogin : $2
      dir = Etc.getpwnam(name).dir rescue nil
      if dir
        [[s.sub(full, dir), "~#{name}"]]
      else
        users.select { |u| u =~ /^#{Regexp::escape name}/u }.map do |u|
          [s.sub("~#{name}", "~#{u}"), "~#{u}"]
        end
      end
    else # regular filename completion
      Dir["#{s}*"].sort.map do |fn|
        suffix = File.directory?(fn) ? "/" : ""
        [fn + suffix, File.basename(fn) + suffix]
      end
    end
  end

  if answer
    answer =
      if answer.empty?
        spawn_modal "file browser", FileBrowserMode.new
      elsif File.directory?(answer) && !allow_directory
        spawn_modal "file browser", FileBrowserMode.new(answer)
      else
        File.expand_path answer
      end
  end

  answer
end
ask_for_labels(domain, question, default_labels, forbidden_labels=[]) click to toggle source

returns an array of labels

# File lib/sup/buffer.rb, line 489
def ask_for_labels domain, question, default_labels, forbidden_labels=[]
  default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
  default = default_labels.to_a.join(" ")
  default += " " unless default.empty?

  # here I would prefer to give more control and allow all_labels instead of
  # user_defined_labels only
  applyable_labels = (LabelManager.user_defined_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }

  answer = ask_many_with_completions domain, question, applyable_labels, default

  return unless answer

  user_labels = answer.to_set_of_symbols
  user_labels.each do |l|
    if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
      BufferManager.flash "'#{l}' is a reserved label!"
      return
    end
  end
  user_labels
end
ask_getch(question, accept=nil) click to toggle source
# File lib/sup/buffer.rb, line 593
def ask_getch question, accept=nil
  raise "impossible!" if @asking

  accept = accept.split(//).map { |x| x.ord } if accept

  status, title = get_status_and_title @focus_buf
  Ncurses.sync do
    draw_screen :sync => false, :status => status, :title => title
    Ncurses.mvaddstr Ncurses.rows - 1, 0, question
    Ncurses.move Ncurses.rows - 1, question.length + 1
    Ncurses.curs_set 1
    Ncurses.refresh
  end

  @asking = true
  ret = nil
  done = false
  until done
    key = Ncurses::CharCode.get
    next if key.empty?
    if key.is_keycode? Ncurses::KEY_CANCEL
      done = true
    elsif accept.nil? || accept.empty? || accept.member?(key.code)
      ret = key
      done = true
    end
  end

  @asking = false
  Ncurses.sync do
    Ncurses.curs_set 0
    draw_screen :sync => false, :status => status, :title => title
  end

  ret
end
ask_many_emails_with_completions(domain, question, completions, default=nil) click to toggle source
# File lib/sup/buffer.rb, line 440
def ask_many_emails_with_completions domain, question, completions, default=nil
  ask domain, question, default do |partial|
    prefix, target = partial.split_on_commas_with_remainder
    target ||= prefix.pop || ""
    target.fix_encoding!

    prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
    prefix.fix_encoding!

    completions.select { |x| x =~ /^#{Regexp::escape target}/iu }.sort_by { |c| [ContactManager.contact_for(c) ? 0 : 1, c] }.map { |x| [prefix + x, x] }
  end
end
ask_many_with_completions(domain, question, completions, default=nil) click to toggle source
# File lib/sup/buffer.rb, line 422
def ask_many_with_completions domain, question, completions, default=nil
  ask domain, question, default do |partial|
    prefix, target =
      case partial
      when /^\s*$/
        ["", ""]
      when /^(.*\s+)?(.*?)$/
        [$1 || "", $2]
      else
        raise "william screwed up completion: #{partial.inspect}"
      end

    prefix.fix_encoding!
    target.fix_encoding!
    completions.select { |x| x =~ /^#{Regexp::escape target}/iu }.map { |x| [prefix + x, x] }
  end
end
ask_with_completions(domain, question, completions, default=nil) click to toggle source

ask* functions. these functions display a one-line text field with a prompt at the bottom of the screen. answers typed or choosen by tab-completion

common arguments are:

domain: token used as key for @textfields, which seems to be a

dictionary of input field objects

question: string used as prompt completions: array of possible answers, that can be completed by using

the tab key

default: default value to return

# File lib/sup/buffer.rb, line 415
def ask_with_completions domain, question, completions, default=nil
  ask domain, question, default do |s|
    s.fix_encoding!
    completions.select { |x| x =~ /^#{Regexp::escape s}/iu }.map { |x| [x, x] }
  end
end
ask_yes_or_no(question) click to toggle source

returns true (y), false (n), or nil (ctrl-g / cancel)

# File lib/sup/buffer.rb, line 631
def ask_yes_or_no question
  case(r = ask_getch question, "ynYN")
  when ?y, ?Y
    true
  when nil
    nil
  else
    false
  end
end
buffers() click to toggle source
# File lib/sup/buffer.rb, line 173
def buffers; @name_map.to_a; end
clear(id) click to toggle source

a little tricky because we can't just delete_at id because ids are relative (they're positions into the array).

# File lib/sup/buffer.rb, line 721
def clear id
  @minibuf_mutex.synchronize do
    @minibuf_stack[id] = nil
    if id == @minibuf_stack.length - 1
      id.downto(0) do |i|
        break if @minibuf_stack[i]
        @minibuf_stack.delete_at i
      end
    end
  end

  draw_screen :refresh => true
end
completely_redraw_screen() click to toggle source
# File lib/sup/buffer.rb, line 238
def completely_redraw_screen
  return if @shelled

  ## this magic makes Ncurses get the new size of the screen
  Ncurses.endwin
  Ncurses.stdscr.keypad 1
  Ncurses.curs_set 0
  Ncurses.refresh
  @sigwinch_mutex.synchronize { @sigwinch_happened = false }
  debug "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"

  status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock

  Ncurses.sync do
    @dirty = true
    Ncurses.clear
    draw_screen :sync => false, :status => status, :title => title
  end
end
draw_minibuf(opts={}) click to toggle source
# File lib/sup/buffer.rb, line 669
def draw_minibuf opts={}
  m = nil
  @minibuf_mutex.synchronize do
    m = @minibuf_stack.compact
    m << @flash if @flash
    m << "" if m.empty? unless @asking # to clear it
  end

  Ncurses.mutex.lock unless opts[:sync] == false
  Ncurses.attrset Colormap.color_for(:text_color)
  adj = @asking ? 2 : 1
  m.each_with_index do |s, i|
    Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
  end
  Ncurses.refresh if opts[:refresh]
  Ncurses.mutex.unlock unless opts[:sync] == false
end
draw_screen(opts={}) click to toggle source
# File lib/sup/buffer.rb, line 258
def draw_screen opts={}
  return if @shelled

  status, title =
    if opts.member? :status
      [opts[:status], opts[:title]]
    else
      raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
      get_status_and_title @focus_buf # must be called outside of the ncurses lock
    end

  ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
  print "\033]0;#{title}\07" if title && @in_x

  Ncurses.mutex.lock unless opts[:sync] == false

  ## disabling this for the time being, to help with debugging
  ## (currently we only have one buffer visible at a time).
  ## TODO: reenable this if we allow multiple buffers
  false && @buffers.inject(@dirty) do |dirty, buf|
    buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
    #dirty ? buf.draw : buf.redraw
    buf.draw status
    dirty
  end

  ## quick hack
  if true
    buf = @buffers.last
    buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
    @dirty ? buf.draw(status) : buf.redraw(status)
  end

  draw_minibuf :sync => false unless opts[:skip_minibuf]

  @dirty = false
  Ncurses.doupdate
  Ncurses.refresh if opts[:refresh]
  Ncurses.mutex.unlock unless opts[:sync] == false
end
erase_flash() click to toggle source
# File lib/sup/buffer.rb, line 712
def erase_flash; @flash = nil; end
exists?(n;) click to toggle source
# File lib/sup/buffer.rb, line 230
def exists? n; @name_map.member? n; end
flash(s) click to toggle source
# File lib/sup/buffer.rb, line 714
def flash s
  @flash = s
  draw_screen :refresh => true
end
focus_on(buf) click to toggle source
# File lib/sup/buffer.rb, line 176
def focus_on buf
  return unless @buffers.member? buf
  return if buf == @focus_buf
  @focus_buf.blur if @focus_buf
  @focus_buf = buf
  @focus_buf.focus
end
handle_input(c) click to toggle source
# File lib/sup/buffer.rb, line 220
def handle_input c
  if @focus_buf
    if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY
      @focus_buf.mode.cancel_search!
      @focus_buf.mark_dirty
    end
    @focus_buf.mode.handle_input c
  end
end
kill_all_buffers() click to toggle source
# File lib/sup/buffer.rb, line 384
def kill_all_buffers
  kill_buffer @buffers.first until @buffers.empty?
end
kill_all_buffers_safely() click to toggle source
# File lib/sup/buffer.rb, line 369
def kill_all_buffers_safely
  until @buffers.empty?
    ## inbox mode always claims it's unkillable. we'll ignore it.
    return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
    kill_buffer @buffers.last
  end
  true
end
kill_buffer(buf) click to toggle source
# File lib/sup/buffer.rb, line 388
def kill_buffer buf
  raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf

  buf.mode.cleanup
  @buffers.delete buf
  @name_map.delete buf.title
  @focus_buf = nil if @focus_buf == buf
  if @buffers.empty?
    ## TODO: something intelligent here
    ## for now I will simply prohibit killing the inbox buffer.
  else
    raise_to_front @buffers.last
  end
end
kill_buffer_safely(buf) click to toggle source
# File lib/sup/buffer.rb, line 378
def kill_buffer_safely buf
  return false unless buf.mode.killable?
  kill_buffer buf
  true
end
minibuf_lines() click to toggle source
# File lib/sup/buffer.rb, line 661
def minibuf_lines
  @minibuf_mutex.synchronize do
    [(@flash ? 1 : 0) +
     (@asking ? 1 : 0) +
     @minibuf_stack.compact.size, 1].max
  end
end
raise_to_front(buf) click to toggle source
# File lib/sup/buffer.rb, line 184
def raise_to_front buf
  @buffers.delete(buf) or return
  if @buffers.length > 0 && @buffers.last.force_to_top?
    @buffers.insert(-2, buf)
  else
    @buffers.push buf
  end
  focus_on @buffers.last
  @dirty = true
end
resolve_input_with_keymap(c, keymap) click to toggle source

turns an input keystroke into an action symbol. returns the action if found, nil if not found, and throws InputSequenceAborted if the user aborted a multi-key sequence. (Because each of those cases should be handled differently.)

this is in BufferManager because multi-key sequences require prompting.

# File lib/sup/buffer.rb, line 648
def resolve_input_with_keymap c, keymap
  action, text = keymap.action_for c
  while action.is_a? Keymap # multi-key commands, prompt
    key = BufferManager.ask_getch text
    unless key # user canceled, abort
      erase_flash
      raise InputSequenceAborted
    end
    action, text = action.action_for(key) if action.has_key?(key)
  end
  action
end
roll_buffers() click to toggle source

we reset force_to_top when rolling buffers. this is so that the human can actually still move buffers around, while still programmatically being able to pop stuff up in the middle of drawing a window without worrying about covering it up.

if we ever start calling #roll_buffers programmatically, we will have to change this. but it's not clear that we will ever actually do that.

# File lib/sup/buffer.rb, line 203
def roll_buffers
  bufs = rollable_buffers
  bufs.last.force_to_top = false
  raise_to_front bufs.first
end
roll_buffers_backwards() click to toggle source
# File lib/sup/buffer.rb, line 209
def roll_buffers_backwards
  bufs = rollable_buffers
  return unless bufs.length > 1
  bufs.last.force_to_top = false
  raise_to_front bufs[bufs.length - 2]
end
rollable_buffers() click to toggle source
# File lib/sup/buffer.rb, line 216
def rollable_buffers
  @buffers.select { |b| !(b.system? || b.hidden?) || @buffers.last == b }
end
say(s, id=nil) { |id| ... } click to toggle source
# File lib/sup/buffer.rb, line 687
def say s, id=nil
  new_id = nil

  @minibuf_mutex.synchronize do
    new_id = id.nil?
    id ||= @minibuf_stack.length
    @minibuf_stack[id] = s
  end

  if new_id
    draw_screen :refresh => true
  else
    draw_minibuf :refresh => true
  end

  if block_given?
    begin
      yield id
    ensure
      clear id
    end
  end
  id
end
shell_out(command) click to toggle source
# File lib/sup/buffer.rb, line 735
def shell_out command
  @shelled = true
  Ncurses.sync do
    Ncurses.endwin
    system command
    Ncurses.stdscr.keypad 1
    Ncurses.refresh
    Ncurses.curs_set 0
  end
  @shelled = false
end
shelled?() click to toggle source
# File lib/sup/buffer.rb, line 174
def shelled?; @shelled; end
sigwinch_happened!() click to toggle source
# File lib/sup/buffer.rb, line 163
def sigwinch_happened!
  @sigwinch_mutex.synchronize do
    return if @sigwinch_happened
    @sigwinch_happened = true
    Ncurses.ungetch ?\C-l.ord
  end
end
sigwinch_happened?() click to toggle source
# File lib/sup/buffer.rb, line 171
def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
spawn(title, mode, opts={}) click to toggle source
# File lib/sup/buffer.rb, line 316
def spawn title, mode, opts={}
  raise ArgumentError, "title must be a string" unless title.is_a? String
  realtitle = title
  num = 2
  while @name_map.member? realtitle
    realtitle = "#{title} <#{num}>"
    num += 1
  end

  width = opts[:width] || Ncurses.cols
  height = opts[:height] || Ncurses.rows - 1

  ## since we are currently only doing multiple full-screen modes,
  ## use stdscr for each window. once we become more sophisticated,
  ## we may need to use a new Ncurses::WINDOW
  ##
  ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
  ## (opts[:left] || 0))
  w = Ncurses.stdscr
  b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => opts[:force_to_top], :system => opts[:system]
  mode.buffer = b
  @name_map[realtitle] = b

  @buffers.unshift b
  if opts[:hidden]
    focus_on b unless @focus_buf
  else
    raise_to_front b
  end
  b
end
spawn_modal(title, mode, opts={}) click to toggle source

requires the mode to have done? and value methods

# File lib/sup/buffer.rb, line 349
def spawn_modal title, mode, opts={}
  b = spawn title, mode, opts
  draw_screen

  until mode.done?
    c = Ncurses::CharCode.get
    next unless c.present? # getch timeout
    break if c.is_keycode? Ncurses::KEY_CANCEL
    begin
      mode.handle_input c
    rescue InputSequenceAborted # do nothing
    end
    draw_screen
    erase_flash
  end

  kill_buffer b
  mode.value
end
spawn_unless_exists(title, opts={}) { || ... } click to toggle source

if the named buffer already exists, pops it to the front without calling the block. otherwise, gets the mode from the block and creates a new buffer. returns two things: the buffer, and a boolean indicating whether it's a new buffer or not.

# File lib/sup/buffer.rb, line 303
def spawn_unless_exists title, opts={}
  new =
    if @name_map.member? title
      raise_to_front @name_map[title] unless opts[:hidden]
      false
    else
      mode = yield
      spawn title, mode, opts
      true
    end
  [@name_map[title], new]
end

Private Instance Methods

default_status_bar(buf) click to toggle source
# File lib/sup/buffer.rb, line 749
def default_status_bar buf
  " [#{buf.mode.name}] #{buf.title}   #{buf.mode.status}"
end
default_terminal_title(buf) click to toggle source
# File lib/sup/buffer.rb, line 753
def default_terminal_title buf
  "Sup #{Redwood::VERSION} :: #{buf.title}"
end
get_status_and_title(buf) click to toggle source
# File lib/sup/buffer.rb, line 757
def get_status_and_title buf
  opts = {
    :num_inbox => lambda { Index.num_results_for :label => :inbox },
    :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
    :num_total => lambda { Index.size },
    :num_spam => lambda { Index.num_results_for :label => :spam },
    :title => buf.title,
    :mode => buf.mode.name,
    :status => buf.mode.status
  }

  statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
  term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)

  [statusbar_text, term_title_text]
end
users() click to toggle source
# File lib/sup/buffer.rb, line 774
def users
  unless @users
    @users = []
    while(u = Etc.getpwent)
      @users << u.name
    end
  end
  @users
end