Class: Redwood::BufferManager

Inherits:
Object
  • Object
show all
Includes:
Singleton
Defined in:
lib/sup/buffer.rb

Constant Summary collapse

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.

"n"

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Singleton

included

Constructor Details

#initializeBufferManager

Returns a new instance of BufferManager.



202
203
204
205
206
207
208
209
210
211
212
213
214
215
# File 'lib/sup/buffer.rb', line 202

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

Instance Attribute Details

#focus_bufObject (readonly)

Returns the value of attribute focus_buf.



162
163
164
# File 'lib/sup/buffer.rb', line 162

def focus_buf
  @focus_buf
end

Instance Method Details

#[](n) ⇒ Object



278
# File 'lib/sup/buffer.rb', line 278

def [] n; @name_map[n]; end

#[]=(n, b) ⇒ Object

Raises:

  • (ArgumentError)


279
280
281
282
283
# File 'lib/sup/buffer.rb', line 279

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) ⇒ Object

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



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
# File 'lib/sup/buffer.rb', line 573

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.safe_nonblocking_getch
    next unless c # 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.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) ⇒ Object



564
565
566
567
568
569
# File 'lib/sup/buffer.rb', line 564

def  domain, question
  completions = AccountManager.user_emails
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, ""
  answer = AccountManager..email if answer == ""
  AccountManager. Person.from_address(answer).email if answer
end

#ask_for_contacts(domain, question, default_contacts = []) ⇒ Object



547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
# File 'lib/sup/buffer.rb', line 547

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) ⇒ Object



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
# File 'lib/sup/buffer.rb', line 488

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 = []) ⇒ Object

returns an array of labels



524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
# File 'lib/sup/buffer.rb', line 524

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) ⇒ Object



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
# File 'lib/sup/buffer.rb', line 628

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.safe_nonblocking_getch or next
    if key == Ncurses::KEY_CANCEL
      done = true
    elsif accept.nil? || accept.empty? || accept.member?(key)
      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) ⇒ Object



475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/sup/buffer.rb', line 475

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) ⇒ Object



457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/sup/buffer.rb', line 457

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) ⇒ Object



450
451
452
453
454
455
# File 'lib/sup/buffer.rb', line 450

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) ⇒ Object

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



665
666
667
668
669
670
671
672
673
674
# File 'lib/sup/buffer.rb', line 665

def ask_yes_or_no question
  case(r = ask_getch question, "ynYN")
  when ?y.ord, ?Y.ord
    true
  when nil
    nil
  else
    false
  end
end

#buffersObject



220
# File 'lib/sup/buffer.rb', line 220

def buffers; @name_map.to_a; end

#clear(id) ⇒ Object

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



755
756
757
758
759
760
761
762
763
764
765
766
767
# File 'lib/sup/buffer.rb', line 755

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_screenObject



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# File 'lib/sup/buffer.rb', line 285

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 = {}) ⇒ Object



703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
# File 'lib/sup/buffer.rb', line 703

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 = {}) ⇒ Object



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
# File 'lib/sup/buffer.rb', line 305

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_flashObject



746
# File 'lib/sup/buffer.rb', line 746

def erase_flash; @flash = nil; end

#exists?(n) ⇒ Boolean

Returns:

  • (Boolean)


277
# File 'lib/sup/buffer.rb', line 277

def exists? n; @name_map.member? n; end

#flash(s) ⇒ Object



748
749
750
751
# File 'lib/sup/buffer.rb', line 748

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

#focus_on(buf) ⇒ Object



223
224
225
226
227
228
229
# File 'lib/sup/buffer.rb', line 223

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) ⇒ Object



267
268
269
270
271
272
273
274
275
# File 'lib/sup/buffer.rb', line 267

def handle_input c
  if @focus_buf
    if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY.ord
      @focus_buf.mode.cancel_search!
      @focus_buf.mark_dirty
    end
    @focus_buf.mode.handle_input c
  end
end

#kill_all_buffersObject



431
432
433
# File 'lib/sup/buffer.rb', line 431

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

#kill_all_buffers_safelyObject



416
417
418
419
420
421
422
423
# File 'lib/sup/buffer.rb', line 416

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) ⇒ Object

Raises:

  • (ArgumentError)


435
436
437
438
439
440
441
442
443
444
445
446
447
448
# File 'lib/sup/buffer.rb', line 435

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) ⇒ Object



425
426
427
428
429
# File 'lib/sup/buffer.rb', line 425

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

#minibuf_linesObject



695
696
697
698
699
700
701
# File 'lib/sup/buffer.rb', line 695

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) ⇒ Object



231
232
233
234
235
236
237
238
239
240
# File 'lib/sup/buffer.rb', line 231

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) ⇒ Object

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.



682
683
684
685
686
687
688
689
690
691
692
693
# File 'lib/sup/buffer.rb', line 682

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_buffersObject

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.



250
251
252
253
254
# File 'lib/sup/buffer.rb', line 250

def roll_buffers
  bufs = rollable_buffers
  bufs.last.force_to_top = false
  raise_to_front bufs.first
end

#roll_buffers_backwardsObject



256
257
258
259
260
261
# File 'lib/sup/buffer.rb', line 256

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_buffersObject



263
264
265
# File 'lib/sup/buffer.rb', line 263

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

#say(s, id = nil) ⇒ Object



721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
# File 'lib/sup/buffer.rb', line 721

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) ⇒ Object



769
770
771
772
773
774
775
776
777
778
779
# File 'lib/sup/buffer.rb', line 769

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

Returns:

  • (Boolean)


221
# File 'lib/sup/buffer.rb', line 221

def shelled?; @shelled; end

#sigwinch_happened!Object



217
# File 'lib/sup/buffer.rb', line 217

def sigwinch_happened!; @sigwinch_mutex.synchronize { @sigwinch_happened = true } end

#sigwinch_happened?Boolean

Returns:

  • (Boolean)


218
# File 'lib/sup/buffer.rb', line 218

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

#spawn(title, mode, opts = {}) ⇒ Object

Raises:

  • (ArgumentError)


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
# File 'lib/sup/buffer.rb', line 363

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 = {}) ⇒ Object

requires the mode to have #done? and #value methods



396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# File 'lib/sup/buffer.rb', line 396

def spawn_modal title, mode, opts={}
  b = spawn title, mode, opts
  draw_screen

  until mode.done?
    c = Ncurses.safe_nonblocking_getch
    next unless c # getch timeout
    break if c == 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 = {}) ⇒ Object

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.



350
351
352
353
354
355
356
357
358
359
360
361
# File 'lib/sup/buffer.rb', line 350

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