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
Public Class Methods
# 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
# File lib/sup/buffer.rb, line 231 def [] n; @name_map[n]; end
# 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
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
# 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
# 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
# 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
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
# 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
# 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
# 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* 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
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
# File lib/sup/buffer.rb, line 173 def buffers; @name_map.to_a; end
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
# 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
# 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
# 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
# File lib/sup/buffer.rb, line 712 def erase_flash; @flash = nil; end
# File lib/sup/buffer.rb, line 230 def exists? n; @name_map.member? n; end
# File lib/sup/buffer.rb, line 714 def flash s @flash = s draw_screen :refresh => true end
# 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
# 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
# File lib/sup/buffer.rb, line 384 def kill_all_buffers kill_buffer @buffers.first until @buffers.empty? end
# 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
# 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
# File lib/sup/buffer.rb, line 378 def kill_buffer_safely buf return false unless buf.mode.killable? kill_buffer buf true end
# 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
# 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
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
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
# 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
# File lib/sup/buffer.rb, line 216 def rollable_buffers @buffers.select { |b| !(b.system? || b.hidden?) || @buffers.last == b } end
# 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
# 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
# File lib/sup/buffer.rb, line 174 def shelled?; @shelled; end
# 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
# File lib/sup/buffer.rb, line 171 def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
# 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
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
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
# File lib/sup/buffer.rb, line 749 def default_status_bar buf " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}" end
# File lib/sup/buffer.rb, line 753 def default_terminal_title buf "Sup #{Redwood::VERSION} :: #{buf.title}" end
# 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
# File lib/sup/buffer.rb, line 774 def users unless @users @users = [] while(u = Etc.getpwent) @users << u.name end end @users end