lib/sup/buffer.rb in sup-0.2 vs lib/sup/buffer.rb in sup-0.3
- old
+ new
@@ -72,26 +72,30 @@
@height = rows
@dirty = true
mode.resize rows, cols
end
- def redraw
- draw if @dirty
- draw_status
+ def redraw status
+ if @dirty
+ draw status
+ else
+ draw_status status
+ end
+
commit
end
def mark_dirty; @dirty = true; end
def commit
@dirty = false
@w.noutrefresh
end
- def draw
+ def draw status
@mode.draw
- draw_status
+ draw_status status
commit
end
## s nil means a blank line!
def write y, x, s, opts={}
@@ -108,13 +112,12 @@
def clear
@w.clear
end
- def draw_status
- write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
- :color => :status_color
+ def draw_status status
+ write @height - 1, 0, status, :color => :status_color
end
def focus
@focus = true
@dirty = true
@@ -131,10 +134,39 @@
class BufferManager
include Singleton
attr_reader :focus_buf
+ ## 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.
+ CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
+
+ HookManager.register "status-bar-text", <<EOS
+Sets the status bar. The default status bar contains the mode name, the buffer
+title, and the mode status. Note that this will be called at least once per
+keystroke, so excessive computation is discouraged.
+
+Variables:
+ num_inbox: number of messages in inbox
+ num_inbox_unread: total number of messages marked as unread
+ num_total: total number of messages in the index
+ num_spam: total number of messages marked as spam
+ title: title of the current buffer
+ mode: current mode name (string)
+ status: current mode status (string)
+Return value: a string to be used as the status bar.
+EOS
+
+ HookManager.register "terminal-title-text", <<EOS
+Sets the title of the current terminal, if applicable. Note that this will be
+called at least once per keystroke, so excessive computation is discouraged.
+
+Variables: the same as status-bar-text hook.
+Return value: a string to be used as the terminal title.
+EOS
+
def initialize
@name_map = {}
@buffers = []
@focus_buf = nil
@dirty = true
@@ -148,19 +180,20 @@
end
def buffers; @name_map.to_a; end
def focus_on buf
- raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
+ return unless @buffers.member? buf
+
return if buf == @focus_buf
@focus_buf.blur if @focus_buf
@focus_buf = buf
@focus_buf.focus
end
def raise_to_front buf
- raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
+ return unless @buffers.member? buf
@buffers.delete buf
if @buffers.length > 0 && @buffers.last.force_to_top?
@buffers.insert(-2, buf)
else
@@ -188,11 +221,17 @@
@buffers.last.force_to_top = false
raise_to_front @buffers[@buffers.length - 2]
end
def handle_input c
- @focus_buf && @focus_buf.mode.handle_input(c)
+ if @focus_buf
+ if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
+ @focus_buf.mode.cancel_search!
+ @focus_buf.mark_dirty
+ end
+ @focus_buf.mode.handle_input c
+ end
end
def exists? n; @name_map.member? n; end
def [] n; @name_map[n]; end
def []= n, b
@@ -202,58 +241,73 @@
end
def completely_redraw_screen
return if @shelled
+ 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
+ draw_screen :sync => false, :status => status, :title => title
end
end
def draw_screen opts={}
return if @shelled
+ status, title =
+ if opts.member? :status
+ [opts[:status], opts[:title]]
+ else
+ get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
+ end
+
+ print "\033]2;#{title}\07" if title
+
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
+ buf.draw status
dirty
end
## quick hack
if true
buf = @buffers.last
buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
- @dirty ? buf.draw : buf.redraw
+ @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
- ## gets the mode from the block, which is only called if the buffer
- ## doesn't already exist. this is useful in the case that generating
- ## the mode is expensive, as it often is.
+ ## 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.
def spawn_unless_exists title, opts={}
- if @name_map.member? title
- raise_to_front @name_map[title] unless opts[:hidden]
- else
- mode = yield
- spawn title, mode, opts
- end
- @name_map[title]
+ 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
def spawn title, mode, opts={}
raise ArgumentError, "title must be a string" unless title.is_a? String
realtitle = title
@@ -332,38 +386,49 @@
@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
+ last = @buffers.last
+ @focus_buf ||= last
+ raise_to_front last
end
end
def ask_with_completions domain, question, completions, default=nil
ask domain, question, default do |s|
completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
end
end
- def ask_many_with_completions domain, question, completions, default=nil, sep=" "
+ def ask_many_with_completions domain, question, completions, default=nil
ask domain, question, default do |partial|
prefix, target =
- case partial#.gsub(/#{sep}+/, sep)
+ case partial
when /^\s*$/
["", ""]
- when /^(.+#{sep})$/
- [$1, ""]
- when /^(.*#{sep})?(.+?)$/
+ when /^(.*\s+)?(.*?)$/
[$1 || "", $2]
else
raise "william screwed up completion: #{partial.inspect}"
end
completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
end
end
+ 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
+ Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
+ target ||= prefix.pop || ""
+ prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
+ Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
+ completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
+ end
+ end
+
def ask_for_filename domain, question, default=nil
answer = ask domain, question, default do |s|
if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
full = $1
name = $2.empty? ? Etc.getlogin : $2
@@ -421,17 +486,15 @@
def ask_for_contacts domain, question, default_contacts=[]
default = default_contacts.map { |s| s.to_s }.join(" ")
default += " " unless default.empty?
- recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.longname, c.email] }
- contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.longname, c.email] }
+ 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] }
- Redwood::log "recent: #{recent.inspect}"
-
completions = (recent + contacts).flatten.uniq.sort
- answer = BufferManager.ask_many_with_completions domain, question, completions, default, /\s*,\s*/
+ answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
if answer
answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
end
end
@@ -452,16 +515,14 @@
## FUCKING COMPLICATED.
Ncurses.sync do
tf.activate question, default, &block
@dirty = true
draw_screen :skip_minibuf => true, :sync => false
+ tf.position_cursor
+ Ncurses.refresh
end
- ret = nil
- tf.position_cursor
- Ncurses.sync { Ncurses.refresh }
-
while true
c = Ncurses.nonblocking_getch
next unless c # getch timeout
break unless tf.handle_input c # process keystroke
@@ -625,9 +686,33 @@
end
@shelled = false
end
private
+ def default_status_bar buf
+ " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
+ end
+
+ def default_terminal_title buf
+ "Sup #{Redwood::VERSION} :: #{buf.title}"
+ end
+
+ 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
def users
unless @users
@users = []
while(u = Etc.getpwent)