File: buffer.rb

package info (click to toggle)
sup-mail 1.3-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,412 kB
  • sloc: ruby: 13,047; sh: 167; makefile: 12
file content (780 lines) | stat: -rw-r--r-- 21,976 bytes parent folder | download | duplicates (5)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
# encoding: utf-8

require 'etc'
require 'thread'
require 'ncursesw'

require 'sup/util/ncurses'

module Redwood

class InputSequenceAborted < StandardError; end

class Buffer
  attr_reader :mode, :x, :y, :width, :height, :title, :atime
  bool_reader :dirty, :system
  bool_accessor :force_to_top, :hidden

  def initialize window, mode, width, height, opts={}
    @w = window
    @mode = mode
    @dirty = true
    @focus = false
    @title = opts[:title] || ""
    @force_to_top = opts[:force_to_top] || false
    @hidden = opts[:hidden] || false
    @x, @y, @width, @height = 0, 0, width, height
    @atime = Time.at 0
    @system = opts[:system] || false
  end

  def content_height; @height - 1; end
  def content_width; @width; end

  def resize rows, cols
    return if cols == @width && rows == @height
    @width = cols
    @height = rows
    @dirty = true
    mode.resize rows, cols
  end

  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 status
    @mode.draw
    draw_status status
    commit
    @atime = Time.now
  end

  ## s nil means a blank line!
  def write y, x, s, opts={}
    return if x >= @width || y >= @height

    @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
    s ||= ""
    maxl = @width - x # maximum display width width

    # fill up the line with blanks to overwrite old screen contents
    @w.mvaddstr y, x, " " * maxl unless opts[:no_fill]

    @w.mvaddstr y, x, s.slice_by_display_length(maxl)
  end

  def clear
    @w.clear
  end

  def draw_status status
    write @height - 1, 0, status, :color => :status_color
  end

  def focus
    @focus = true
    @dirty = true
    @mode.focus
  end

  def blur
    @focus = false
    @dirty = true
    @mode.blur
  end
end

class BufferManager
  include Redwood::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

  HookManager.register "extra-contact-addresses", <<EOS
A list of extra addresses to propose for tab completion, etc. when the
user is entering an email address. Can be plain email addresses or can
be full "User Name <email@domain.tld>" entries.

Variables: none
Return value: an array of email address strings.
EOS

  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

  def sigwinch_happened!
    @sigwinch_mutex.synchronize do
      return if @sigwinch_happened
      @sigwinch_happened = true
      Ncurses.ungetch ?\C-l.ord
    end
  end

  def sigwinch_happened?; @sigwinch_mutex.synchronize { @sigwinch_happened } end

  def buffers; @name_map.to_a; end
  def shelled?; @shelled; end

  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

  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

  ## 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.
  def roll_buffers
    bufs = rollable_buffers
    bufs.last.force_to_top = false
    raise_to_front bufs.first
  end

  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

  def rollable_buffers
    @buffers.select { |b| !(b.system? || b.hidden?) || @buffers.last == b }
  end

  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

  def exists? n; @name_map.member? n; end
  def [] n; @name_map[n]; end
  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

  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

  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

  ## 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={}
    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
    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
  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

  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

  def kill_buffer_safely buf
    return false unless buf.mode.killable?
    kill_buffer buf
    true
  end

  def kill_all_buffers
    kill_buffer @buffers.first until @buffers.empty?
  end

  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

  ## 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
  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

  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

  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

  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
  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

  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

  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

  ## for simplicitly, we always place the question at the very bottom of the
  ## screen
  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

  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

  ## returns true (y), false (n), or nil (ctrl-g / cancel)
  def ask_yes_or_no question
    case(r = ask_getch question, "ynYN")
    when ?y, ?Y
      true
    when nil
      nil
    else
      false
    end
  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.
  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

  def minibuf_lines
    @minibuf_mutex.synchronize do
      [(@flash ? 1 : 0) +
       (@asking ? 1 : 0) +
       @minibuf_stack.compact.size, 1].max
    end
  end

  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

  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

  def erase_flash; @flash = nil; end

  def flash s
    @flash = s
    draw_screen :refresh => true
  end

  ## a little tricky because we can't just delete_at id because ids
  ## are relative (they're positions into the array).
  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

  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

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)
        @users << u.name
      end
    end
    @users
  end
end
end