Force UTF-8 in ask_many_emails..
[sup:mainline.git] / lib / sup / buffer.rb
1 require 'etc'
2 require 'thread'
3
4 begin
5   require 'ncursesw'
6 rescue LoadError
7   require 'ncurses'
8 end
9
10 if defined? Ncurses
11 module Ncurses
12   def rows
13     lame, lamer = [], []
14     stdscr.getmaxyx lame, lamer
15     lame.first
16   end
17
18   def cols
19     lame, lamer = [], []
20     stdscr.getmaxyx lame, lamer
21     lamer.first
22   end
23
24   def curx
25     lame, lamer = [], []
26     stdscr.getyx lame, lamer
27     lamer.first
28   end
29
30   def mutex; @mutex ||= Mutex.new; end
31   def sync &b; mutex.synchronize(&b); end
32
33   ## magically, this stuff seems to work now. i could swear it didn't
34   ## before. hm.
35   def nonblocking_getch
36     ## INSANTIY
37     ## it is NECESSARY to wrap Ncurses.getch in a select() otherwise all
38     ## background threads will be BLOCKED. (except in very modern versions
39     ## of libncurses-ruby. the current one on ubuntu seems to work well.)
40     if IO.select([$stdin], nil, nil, 0.5)
41       if Redwood::BufferManager.shelled?
42         # If we get input while we're shelled, we'll ignore it for the
43         # moment and use Ncurses.sync to wait until the shell_out is done.
44         Ncurses.sync { nil }
45       else
46         Ncurses.getch
47       end
48     end
49   end
50
51   ## pretends ctrl-c's are ctrl-g's
52   def safe_nonblocking_getch
53     nonblocking_getch
54   rescue Interrupt
55     KEY_CANCEL
56   end
57
58   module_function :rows, :cols, :curx, :nonblocking_getch, :safe_nonblocking_getch, :mutex, :sync
59
60   remove_const :KEY_ENTER
61   remove_const :KEY_CANCEL
62
63   KEY_ENTER = 10
64   KEY_CANCEL = 7 # ctrl-g
65   KEY_TAB = 9
66 end
67 end
68
69 module Redwood
70
71 class InputSequenceAborted < StandardError; end
72
73 class Buffer
74   attr_reader :mode, :x, :y, :width, :height, :title, :atime
75   bool_reader :dirty, :system
76   bool_accessor :force_to_top
77
78   def initialize window, mode, width, height, opts={}
79     @w = window
80     @mode = mode
81     @dirty = true
82     @focus = false
83     @title = opts[:title] || ""
84     @force_to_top = opts[:force_to_top] || false
85     @x, @y, @width, @height = 0, 0, width, height
86     @atime = Time.at 0
87     @system = opts[:system] || false
88   end
89
90   def content_height; @height - 1; end
91   def content_width; @width; end
92
93   def resize rows, cols
94     return if cols == @width && rows == @height
95     @width = cols
96     @height = rows
97     @dirty = true
98     mode.resize rows, cols
99   end
100
101   def redraw status
102     if @dirty
103       draw status
104     else
105       draw_status status
106     end
107
108     commit
109   end
110
111   def mark_dirty; @dirty = true; end
112
113   def commit
114     @dirty = false
115     @w.noutrefresh
116   end
117
118   def draw status
119     @mode.draw
120     draw_status status
121     commit
122     @atime = Time.now
123   end
124
125   ## s nil means a blank line!
126   def write y, x, s, opts={}
127     return if x >= @width || y >= @height
128
129     @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
130     s ||= ""
131     maxl = @width - x # maximum display width width
132     stringl = maxl    # string "length"
133
134     # fill up the line with blanks to overwrite old screen contents
135     @w.mvaddstr y, x, " " * maxl unless opts[:no_fill]
136
137     ## the next horribleness is thanks to ruby's lack of widechar support
138     stringl += 1 while stringl < s.length && s[0 ... stringl].display_length < maxl
139     @w.mvaddstr y, x, s[0 ... stringl]
140   end
141
142   def clear
143     @w.clear
144   end
145
146   def draw_status status
147     write @height - 1, 0, status, :color => :status_color
148   end
149
150   def focus
151     @focus = true
152     @dirty = true
153     @mode.focus
154   end
155
156   def blur
157     @focus = false
158     @dirty = true
159     @mode.blur
160   end
161 end
162
163 class BufferManager
164   include Singleton
165
166   attr_reader :focus_buf
167
168   ## we have to define the key used to continue in-buffer search here, because
169   ## it has special semantics that BufferManager deals with---current searches
170   ## are canceled by any keypress except this one.
171   CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
172
173   HookManager.register "status-bar-text", <<EOS
174 Sets the status bar. The default status bar contains the mode name, the buffer
175 title, and the mode status. Note that this will be called at least once per
176 keystroke, so excessive computation is discouraged.
177
178 Variables:
179          num_inbox: number of messages in inbox
180   num_inbox_unread: total number of messages marked as unread
181          num_total: total number of messages in the index
182           num_spam: total number of messages marked as spam
183              title: title of the current buffer
184               mode: current mode name (string)
185             status: current mode status (string)
186 Return value: a string to be used as the status bar.
187 EOS
188
189   HookManager.register "terminal-title-text", <<EOS
190 Sets the title of the current terminal, if applicable. Note that this will be
191 called at least once per keystroke, so excessive computation is discouraged.
192
193 Variables: the same as status-bar-text hook.
194 Return value: a string to be used as the terminal title.
195 EOS
196
197   HookManager.register "extra-contact-addresses", <<EOS
198 A list of extra addresses to propose for tab completion, etc. when the
199 user is entering an email address. Can be plain email addresses or can
200 be full "User Name <email@domain.tld>" entries.
201
202 Variables: none
203 Return value: an array of email address strings.
204 EOS
205
206   def initialize
207     @name_map = {}
208     @buffers = []
209     @focus_buf = nil
210     @dirty = true
211     @minibuf_stack = []
212     @minibuf_mutex = Mutex.new
213     @textfields = {}
214     @flash = nil
215     @shelled = @asking = false
216     @in_x = ENV["TERM"] =~ /(xterm|rxvt|screen)/
217     @sigwinch_happened = false
218     @sigwinch_mutex = Mutex.new
219   end
220
221   def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end
222   def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end
223
224   def buffers; @name_map.to_a; end
225   def shelled?; @shelled; end
226
227   def focus_on buf
228     return unless @buffers.member? buf
229     return if buf == @focus_buf
230     @focus_buf.blur if @focus_buf
231     @focus_buf = buf
232     @focus_buf.focus
233   end
234
235   def raise_to_front buf
236     @buffers.delete(buf) or return
237     if @buffers.length > 0 && @buffers.last.force_to_top?
238       @buffers.insert(-2, buf)
239     else
240       @buffers.push buf
241     end
242     focus_on @buffers.last
243     @dirty = true
244   end
245
246   ## we reset force_to_top when rolling buffers. this is so that the
247   ## human can actually still move buffers around, while still
248   ## programmatically being able to pop stuff up in the middle of
249   ## drawing a window without worrying about covering it up.
250   ##
251   ## if we ever start calling roll_buffers programmatically, we will
252   ## have to change this. but it's not clear that we will ever actually
253   ## do that.
254   def roll_buffers
255     bufs = rollable_buffers
256     bufs.last.force_to_top = false
257     raise_to_front bufs.first
258   end
259
260   def roll_buffers_backwards
261     bufs = rollable_buffers
262     return unless bufs.length > 1
263     bufs.last.force_to_top = false
264     raise_to_front bufs[bufs.length - 2]
265   end
266
267   def rollable_buffers
268     @buffers.select { |b| !b.system? || @buffers.last == b }
269   end
270
271   def handle_input c
272     if @focus_buf
273       if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
274         @focus_buf.mode.cancel_search!
275         @focus_buf.mark_dirty
276       end
277       @focus_buf.mode.handle_input c
278     end
279   end
280
281   def exists? n; @name_map.member? n; end
282   def [] n; @name_map[n]; end
283   def []= n, b
284     raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
285     raise ArgumentError, "title must be a string" unless n.is_a? String
286     @name_map[n] = b
287   end
288
289   def completely_redraw_screen
290     return if @shelled
291
292     ## this magic makes Ncurses get the new size of the screen
293     Ncurses.endwin
294     Ncurses.stdscr.keypad 1
295     Ncurses.curs_set 0
296     Ncurses.refresh
297     @sigwinch_mutex.synchronize { @sigwinch_happened = false }
298     debug "new screen size is #{Ncurses.rows} x #{Ncurses.cols}"
299
300     status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
301
302     Ncurses.sync do
303       @dirty = true
304       Ncurses.clear
305       draw_screen :sync => false, :status => status, :title => title
306     end
307   end
308
309   def draw_screen opts={}
310     return if @shelled
311
312     status, title =
313       if opts.member? :status
314         [opts[:status], opts[:title]]
315       else
316         raise "status must be supplied if draw_screen is called within a sync" if opts[:sync] == false
317         get_status_and_title @focus_buf # must be called outside of the ncurses lock
318       end
319
320     ## http://rtfm.etla.org/xterm/ctlseq.html (see Operating System Controls)
321     print "\033]0;#{title}\07" if title && @in_x
322
323     Ncurses.mutex.lock unless opts[:sync] == false
324
325     ## disabling this for the time being, to help with debugging
326     ## (currently we only have one buffer visible at a time).
327     ## TODO: reenable this if we allow multiple buffers
328     false && @buffers.inject(@dirty) do |dirty, buf|
329       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
330       #dirty ? buf.draw : buf.redraw
331       buf.draw status
332       dirty
333     end
334
335     ## quick hack
336     if true
337       buf = @buffers.last
338       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
339       @dirty ? buf.draw(status) : buf.redraw(status)
340     end
341
342     draw_minibuf :sync => false unless opts[:skip_minibuf]
343
344     @dirty = false
345     Ncurses.doupdate
346     Ncurses.refresh if opts[:refresh]
347     Ncurses.mutex.unlock unless opts[:sync] == false
348   end
349
350   ## if the named buffer already exists, pops it to the front without
351   ## calling the block. otherwise, gets the mode from the block and
352   ## creates a new buffer. returns two things: the buffer, and a boolean
353   ## indicating whether it's a new buffer or not.
354   def spawn_unless_exists title, opts={}
355     new =
356       if @name_map.member? title
357         raise_to_front @name_map[title] unless opts[:hidden]
358         false
359       else
360         mode = yield
361         spawn title, mode, opts
362         true
363       end
364     [@name_map[title], new]
365   end
366
367   def spawn title, mode, opts={}
368     raise ArgumentError, "title must be a string" unless title.is_a? String
369     realtitle = title
370     num = 2
371     while @name_map.member? realtitle
372       realtitle = "#{title} <#{num}>"
373       num += 1
374     end
375
376     width = opts[:width] || Ncurses.cols
377     height = opts[:height] || Ncurses.rows - 1
378
379     ## since we are currently only doing multiple full-screen modes,
380     ## use stdscr for each window. once we become more sophisticated,
381     ## we may need to use a new Ncurses::WINDOW
382     ##
383     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
384     ## (opts[:left] || 0))
385     w = Ncurses.stdscr
386     b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => opts[:force_to_top], :system => opts[:system]
387     mode.buffer = b
388     @name_map[realtitle] = b
389
390     @buffers.unshift b
391     if opts[:hidden]
392       focus_on b unless @focus_buf
393     else
394       raise_to_front b
395     end
396     b
397   end
398
399   ## requires the mode to have #done? and #value methods
400   def spawn_modal title, mode, opts={}
401     b = spawn title, mode, opts
402     draw_screen
403
404     until mode.done?
405       c = Ncurses.safe_nonblocking_getch
406       next unless c # getch timeout
407       break if c == Ncurses::KEY_CANCEL
408       begin
409         mode.handle_input c
410       rescue InputSequenceAborted # do nothing
411       end
412       draw_screen
413       erase_flash
414     end
415
416     kill_buffer b
417     mode.value
418   end
419
420   def kill_all_buffers_safely
421     until @buffers.empty?
422       ## inbox mode always claims it's unkillable. we'll ignore it.
423       return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
424       kill_buffer @buffers.last
425     end
426     true
427   end
428
429   def kill_buffer_safely buf
430     return false unless buf.mode.killable?
431     kill_buffer buf
432     true
433   end
434
435   def kill_all_buffers
436     kill_buffer @buffers.first until @buffers.empty?
437   end
438
439   def kill_buffer buf
440     raise ArgumentError, "buffer not on stack: #{buf}: #{buf.title.inspect}" unless @buffers.member? buf
441
442     buf.mode.cleanup
443     @buffers.delete buf
444     @name_map.delete buf.title
445     @focus_buf = nil if @focus_buf == buf
446     if @buffers.empty?
447       ## TODO: something intelligent here
448       ## for now I will simply prohibit killing the inbox buffer.
449     else
450       raise_to_front @buffers.last
451     end
452   end
453
454   def ask_with_completions domain, question, completions, default=nil
455     ask domain, question, default do |s|
456       s.force_encoding 'UTF-8' if s.methods.include?(:encoding)
457       completions.select { |x| x =~ /^#{Regexp::escape s}/iu }.map { |x| [x, x] }
458     end
459   end
460
461   def ask_many_with_completions domain, question, completions, default=nil
462     ask domain, question, default do |partial|
463       prefix, target =
464         case partial
465         when /^\s*$/
466           ["", ""]
467         when /^(.*\s+)?(.*?)$/
468           [$1 || "", $2]
469         else
470           raise "william screwed up completion: #{partial.inspect}"
471         end
472
473       prefix.force_encoding 'UTF-8' if prefix.methods.include?(:encoding)
474       target.force_encoding 'UTF-8' if target.methods.include?(:encoding)
475       completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
476     end
477   end
478
479   def ask_many_emails_with_completions domain, question, completions, default=nil
480     ask domain, question, default do |partial|
481       prefix, target = partial.split_on_commas_with_remainder
482       target ||= prefix.pop || ""
483       target.force_encoding 'UTF-8' if target.methods.include?(:encoding)
484
485       prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
486       prefix.force_encoding 'UTF-8' if prefix.methods.include?(:encoding)
487
488       completions.select { |x| x =~ /^#{Regexp::escape target}/i }.sort_by { |c| [ContactManager.contact_for(c) ? 0 : 1, c] }.map { |x| [prefix + x, x] }
489     end
490   end
491
492   def ask_for_filename domain, question, default=nil, allow_directory=false
493     answer = ask domain, question, default do |s|
494       if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
495         full = $1
496         name = $2.empty? ? Etc.getlogin : $2
497         dir = Etc.getpwnam(name).dir rescue nil
498         if dir
499           [[s.sub(full, dir), "~#{name}"]]
500         else
501           users.select { |u| u =~ /^#{Regexp::escape name}/ }.map do |u|
502             [s.sub("~#{name}", "~#{u}"), "~#{u}"]
503           end
504         end
505       else # regular filename completion
506         Dir["#{s}*"].sort.map do |fn|
507           suffix = File.directory?(fn) ? "/" : ""
508           [fn + suffix, File.basename(fn) + suffix]
509         end
510       end
511     end
512
513     if answer
514       answer =
515         if answer.empty?
516           spawn_modal "file browser", FileBrowserMode.new
517         elsif File.directory?(answer) && !allow_directory
518           spawn_modal "file browser", FileBrowserMode.new(answer)
519         else
520           File.expand_path answer
521         end
522     end
523
524     answer
525   end
526
527   ## returns an array of labels
528   def ask_for_labels domain, question, default_labels, forbidden_labels=[]
529     default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
530     default = default_labels.to_a.join(" ")
531     default += " " unless default.empty?
532
533     # here I would prefer to give more control and allow all_labels instead of
534     # user_defined_labels only
535     applyable_labels = (LabelManager.user_defined_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
536
537     answer = ask_many_with_completions domain, question, applyable_labels, default
538
539     return unless answer
540
541     user_labels = answer.to_set_of_symbols
542     user_labels.each do |l|
543       if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
544         BufferManager.flash "'#{l}' is a reserved label!"
545         return
546       end
547     end
548     user_labels
549   end
550
551   def ask_for_contacts domain, question, default_contacts=[]
552     default = default_contacts.is_a?(String) ? default_contacts : default_contacts.map { |s| s.to_s }.join(", ")
553     default += " " unless default.empty?
554
555     recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
556     contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
557
558     completions = (recent + contacts).flatten.uniq
559     completions += HookManager.run("extra-contact-addresses") || []
560     answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
561
562     if answer
563       answer.split_on_commas.map { |x| ContactManager.contact_for(x) || Person.from_address(x) }
564     end
565   end
566
567   def ask_for_account domain, question
568     completions = AccountManager.user_emails
569     answer = BufferManager.ask_many_emails_with_completions domain, question, completions, ""
570     answer = AccountManager.default_account.email if answer == ""
571     AccountManager.account_for Person.from_address(answer).email if answer
572   end
573
574   ## for simplicitly, we always place the question at the very bottom of the
575   ## screen
576   def ask domain, question, default=nil, &block
577     raise "impossible!" if @asking
578     @asking = true
579
580     @textfields[domain] ||= TextField.new
581     tf = @textfields[domain]
582     completion_buf = nil
583
584     status, title = get_status_and_title @focus_buf
585
586     Ncurses.sync do
587       tf.activate Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols, question, default, &block
588       @dirty = true # for some reason that blanks the whole fucking screen
589       draw_screen :sync => false, :status => status, :title => title
590       tf.position_cursor
591       Ncurses.refresh
592     end
593
594     while true
595       c = Ncurses.safe_nonblocking_getch
596       next unless c # getch timeout
597       break unless tf.handle_input c # process keystroke
598
599       if tf.new_completions?
600         kill_buffer completion_buf if completion_buf
601
602         shorts = tf.completions.map { |full, short| short }
603         prefix_len = shorts.shared_prefix.length
604
605         mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
606         completion_buf = spawn "<completions>", mode, :height => 10
607
608         draw_screen :skip_minibuf => true
609         tf.position_cursor
610       elsif tf.roll_completions?
611         completion_buf.mode.roll
612         draw_screen :skip_minibuf => true
613         tf.position_cursor
614       end
615
616       Ncurses.sync { Ncurses.refresh }
617     end
618
619     kill_buffer completion_buf if completion_buf
620
621     @dirty = true
622     @asking = false
623     Ncurses.sync do
624       tf.deactivate
625       draw_screen :sync => false, :status => status, :title => title
626     end
627     tf.value.tap { |x| x.force_encoding Encoding::UTF_8 if x && x.respond_to?(:encoding) }
628   end
629
630   def ask_getch question, accept=nil
631     raise "impossible!" if @asking
632
633     accept = accept.split(//).map { |x| x.ord } if accept
634
635     status, title = get_status_and_title @focus_buf
636     Ncurses.sync do
637       draw_screen :sync => false, :status => status, :title => title
638       Ncurses.mvaddstr Ncurses.rows - 1, 0, question
639       Ncurses.move Ncurses.rows - 1, question.length + 1
640       Ncurses.curs_set 1
641       Ncurses.refresh
642     end
643
644     @asking = true
645     ret = nil
646     done = false
647     until done
648       key = Ncurses.safe_nonblocking_getch or next
649       if key == Ncurses::KEY_CANCEL
650         done = true
651       elsif accept.nil? || accept.empty? || accept.member?(key)
652         ret = key
653         done = true
654       end
655     end
656
657     @asking = false
658     Ncurses.sync do
659       Ncurses.curs_set 0
660       draw_screen :sync => false, :status => status, :title => title
661     end
662
663     ret
664   end
665
666   ## returns true (y), false (n), or nil (ctrl-g / cancel)
667   def ask_yes_or_no question
668     case(r = ask_getch question, "ynYN")
669     when ?y.ord, ?Y.ord
670       true
671     when nil
672       nil
673     else
674       false
675     end
676   end
677
678   ## turns an input keystroke into an action symbol. returns the action
679   ## if found, nil if not found, and throws InputSequenceAborted if
680   ## the user aborted a multi-key sequence. (Because each of those cases
681   ## should be handled differently.)
682   ##
683   ## this is in BufferManager because multi-key sequences require prompting.
684   def resolve_input_with_keymap c, keymap
685     action, text = keymap.action_for c
686     while action.is_a? Keymap # multi-key commands, prompt
687       key = BufferManager.ask_getch text
688       unless key # user canceled, abort
689         erase_flash
690         raise InputSequenceAborted
691       end
692       action, text = action.action_for(key) if action.has_key?(key)
693     end
694     action
695   end
696
697   def minibuf_lines
698     @minibuf_mutex.synchronize do
699       [(@flash ? 1 : 0) +
700        (@asking ? 1 : 0) +
701        @minibuf_stack.compact.size, 1].max
702     end
703   end
704
705   def draw_minibuf opts={}
706     m = nil
707     @minibuf_mutex.synchronize do
708       m = @minibuf_stack.compact
709       m << @flash if @flash
710       m << "" if m.empty? unless @asking # to clear it
711     end
712
713     Ncurses.mutex.lock unless opts[:sync] == false
714     Ncurses.attrset Colormap.color_for(:none)
715     adj = @asking ? 2 : 1
716     m.each_with_index do |s, i|
717       Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
718     end
719     Ncurses.refresh if opts[:refresh]
720     Ncurses.mutex.unlock unless opts[:sync] == false
721   end
722
723   def say s, id=nil
724     new_id = nil
725
726     @minibuf_mutex.synchronize do
727       new_id = id.nil?
728       id ||= @minibuf_stack.length
729       @minibuf_stack[id] = s
730     end
731
732     if new_id
733       draw_screen :refresh => true
734     else
735       draw_minibuf :refresh => true
736     end
737
738     if block_given?
739       begin
740         yield id
741       ensure
742         clear id
743       end
744     end
745     id
746   end
747
748   def erase_flash; @flash = nil; end
749
750   def flash s
751     @flash = s
752     draw_screen :refresh => true
753   end
754
755   ## a little tricky because we can't just delete_at id because ids
756   ## are relative (they're positions into the array).
757   def clear id
758     @minibuf_mutex.synchronize do
759       @minibuf_stack[id] = nil
760       if id == @minibuf_stack.length - 1
761         id.downto(0) do |i|
762           break if @minibuf_stack[i]
763           @minibuf_stack.delete_at i
764         end
765       end
766     end
767
768     draw_screen :refresh => true
769   end
770
771   def shell_out command
772     @shelled = true
773     Ncurses.sync do
774       Ncurses.endwin
775       system command
776       Ncurses.stdscr.keypad 1
777       Ncurses.refresh
778       Ncurses.curs_set 0
779     end
780     @shelled = false
781   end
782
783 private
784
785   def default_status_bar buf
786     " [#{buf.mode.name}] #{buf.title}   #{buf.mode.status}"
787   end
788
789   def default_terminal_title buf
790     "Sup #{Redwood::VERSION} :: #{buf.title}"
791   end
792
793   def get_status_and_title buf
794     opts = {
795       :num_inbox => lambda { Index.num_results_for :label => :inbox },
796       :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
797       :num_total => lambda { Index.size },
798       :num_spam => lambda { Index.num_results_for :label => :spam },
799       :title => buf.title,
800       :mode => buf.mode.name,
801       :status => buf.mode.status
802     }
803
804     statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
805     term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
806
807     [statusbar_text, term_title_text]
808   end
809
810   def users
811     unless @users
812       @users = []
813       while(u = Etc.getpwent)
814         @users << u.name
815       end
816     end
817     @users
818   end
819 end
820 end