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.



206
207
208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/sup/buffer.rb', line 206

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.



166
167
168
# File 'lib/sup/buffer.rb', line 166

def focus_buf
  @focus_buf
end

Instance Method Details

#[](n) ⇒ Object



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

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

#[]=(n, b) ⇒ Object

Raises:

  • (ArgumentError)


283
284
285
286
287
# File 'lib/sup/buffer.rb', line 283

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



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

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.force_encoding Encoding::UTF_8 if x && x.respond_to?(:encoding) }
end

#ask_for_account(domain, question) ⇒ Object



567
568
569
570
571
572
# File 'lib/sup/buffer.rb', line 567

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



551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# File 'lib/sup/buffer.rb', line 551

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



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

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}/ }.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



528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
# File 'lib/sup/buffer.rb', line 528

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



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

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



479
480
481
482
483
484
485
486
487
488
489
490
# File 'lib/sup/buffer.rb', line 479

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.force_encoding 'UTF-8' if target.methods.include?(:encoding)

    prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
    prefix.force_encoding 'UTF-8' if prefix.methods.include?(:encoding)

    completions.select { |x| x =~ /^#{Regexp::escape target}/i }.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



461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/sup/buffer.rb', line 461

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.force_encoding 'UTF-8' if prefix.methods.include?(:encoding)
    target.force_encoding 'UTF-8' if target.methods.include?(:encoding)
    completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
  end
end

#ask_with_completions(domain, question, completions, default = nil) ⇒ Object



454
455
456
457
458
459
# File 'lib/sup/buffer.rb', line 454

def ask_with_completions domain, question, completions, default=nil
  ask domain, question, default do |s|
    s.force_encoding 'UTF-8' if s.methods.include?(: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)



668
669
670
671
672
673
674
675
676
677
# File 'lib/sup/buffer.rb', line 668

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



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

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



758
759
760
761
762
763
764
765
766
767
768
769
770
# File 'lib/sup/buffer.rb', line 758

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



289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/sup/buffer.rb', line 289

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



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

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(:none)
  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



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

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



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

def erase_flash; @flash = nil; end

#exists?(n) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#flash(s) ⇒ Object



751
752
753
754
# File 'lib/sup/buffer.rb', line 751

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

#focus_on(buf) ⇒ Object



227
228
229
230
231
232
233
# File 'lib/sup/buffer.rb', line 227

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



271
272
273
274
275
276
277
278
279
# File 'lib/sup/buffer.rb', line 271

def 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

#kill_all_buffersObject



435
436
437
# File 'lib/sup/buffer.rb', line 435

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

#kill_all_buffers_safelyObject



420
421
422
423
424
425
426
427
# File 'lib/sup/buffer.rb', line 420

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)


439
440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/sup/buffer.rb', line 439

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



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

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

#minibuf_linesObject



698
699
700
701
702
703
704
# File 'lib/sup/buffer.rb', line 698

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



235
236
237
238
239
240
241
242
243
244
# File 'lib/sup/buffer.rb', line 235

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.



685
686
687
688
689
690
691
692
693
694
695
696
# File 'lib/sup/buffer.rb', line 685

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.



254
255
256
257
258
# File 'lib/sup/buffer.rb', line 254

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

#roll_buffers_backwardsObject



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

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



267
268
269
# File 'lib/sup/buffer.rb', line 267

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

#say(s, id = nil) ⇒ Object



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

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



772
773
774
775
776
777
778
779
780
781
782
# File 'lib/sup/buffer.rb', line 772

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)


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

def shelled?; @shelled; end

#sigwinch_happened!Object



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

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

#sigwinch_happened?Boolean

Returns:

  • (Boolean)


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

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

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

Raises:

  • (ArgumentError)


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

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



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
# File 'lib/sup/buffer.rb', line 400

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.



354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/sup/buffer.rb', line 354

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