Class: Redwood::BufferManager

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

Constant Summary

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)

Constructor Details

- (BufferManager) initialize

Returns a new instance of BufferManager



179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/sup/buffer.rb', line 179

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

  self.class.i_am_the_instance self
end

Instance Attribute Details

- (Object) focus_buf (readonly)

Returns the value of attribute focus_buf



139
140
141
# File 'lib/sup/buffer.rb', line 139

def focus_buf
  @focus_buf
end

Instance Method Details

- (Object) [](n)



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

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

- (Object) []=(n, b)

Raises:

  • (ArgumentError)


246
247
248
249
250
# File 'lib/sup/buffer.rb', line 246

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

- (Object) ask(domain, question, default = nil, &block)

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



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

def ask domain, question, default=nil, &block
  raise "impossible!" if @asking
  @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.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
end

- (Object) ask_for_contacts(domain, question, default_contacts = [])



497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
# File 'lib/sup/buffer.rb', line 497

def ask_for_contacts domain, question, default_contacts=[]
  default = default_contacts.map { |s| s.to_s }.join(" ")
  default += " " unless default.empty?
  
  recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.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

- (Object) ask_for_filename(domain, question, default = nil)



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

def ask_for_filename domain, question, default=nil
  answer = ask domain, question, default do |s|
    if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
      full = $1
      name = $2.empty? ? Etc.getlogin : $2
      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)
        spawn_modal "file browser", FileBrowserMode.new(answer)
      else
        File.expand_path answer
      end
  end

  answer
end

- (Object) ask_for_labels(domain, question, default_labels, forbidden_labels = [])

returns an array of labels



476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
# File 'lib/sup/buffer.rb', line 476

def ask_for_labels domain, question, default_labels, forbidden_labels=[]
  default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
  default = default_labels.join(" ")
  default += " " unless default.empty?

  applyable_labels = (LabelManager.applyable_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.split(/\s+/).map { |l| l.intern }
  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

- (Object) ask_getch(question, accept = nil)



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

def ask_getch question, accept=nil
  raise "impossible!" if @asking

  accept = accept.split(//).map { |x| x[0] } 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.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

- (Object) ask_many_emails_with_completions(domain, question, completions, default = nil)



431
432
433
434
435
436
437
438
# File 'lib/sup/buffer.rb', line 431

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 || ""
    prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
    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

- (Object) ask_many_with_completions(domain, question, completions, default = nil)



415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/sup/buffer.rb', line 415

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

    completions.select { |x| x =~ /^#{Regexp::escape target}/i }.map { |x| [prefix + x, x] }
  end
end

- (Object) ask_with_completions(domain, question, completions, default = nil)



409
410
411
412
413
# File 'lib/sup/buffer.rb', line 409

def ask_with_completions domain, question, completions, default=nil
  ask domain, question, default do |s|
    completions.select { |x| x =~ /^#{Regexp::escape s}/i }.map { |x| [x, x] }
  end
end

- (Object) ask_yes_or_no(question)

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



606
607
608
609
610
611
612
613
614
615
# File 'lib/sup/buffer.rb', line 606

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

- (Object) buffers



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

def buffers; @name_map.to_a; end

- (Object) clear(id)

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



696
697
698
699
700
701
702
703
704
705
706
707
708
# File 'lib/sup/buffer.rb', line 696

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

- (Object) completely_redraw_screen



252
253
254
255
256
257
258
259
260
261
262
# File 'lib/sup/buffer.rb', line 252

def completely_redraw_screen
  return if @shelled

  status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock

  Ncurses.sync do
    @dirty = true
    Ncurses.clear
    draw_screen :sync => false, :status => status, :title => title
  end
end

- (Object) draw_minibuf(opts = {})



644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
# File 'lib/sup/buffer.rb', line 644

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

- (Object) draw_screen(opts = {})



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

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

- (Object) erase_flash



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

def erase_flash; @flash = nil; end

- (Boolean) exists?(n)

Returns:

  • (Boolean)


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

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

- (Object) flash(s)



689
690
691
692
# File 'lib/sup/buffer.rb', line 689

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

- (Object) focus_on(buf)



196
197
198
199
200
201
202
# File 'lib/sup/buffer.rb', line 196

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

- (Object) handle_input(c)



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

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

- (Object) kill_all_buffers



390
391
392
# File 'lib/sup/buffer.rb', line 390

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

- (Object) kill_all_buffers_safely



375
376
377
378
379
380
381
382
# File 'lib/sup/buffer.rb', line 375

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

- (Object) kill_buffer(buf)

Raises:

  • (ArgumentError)


394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/sup/buffer.rb', line 394

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

- (Object) kill_buffer_safely(buf)



384
385
386
387
388
# File 'lib/sup/buffer.rb', line 384

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

- (Object) minibuf_lines



636
637
638
639
640
641
642
# File 'lib/sup/buffer.rb', line 636

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

- (Object) raise_to_front(buf)



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

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

- (Object) resolve_input_with_keymap(c, keymap)

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.



623
624
625
626
627
628
629
630
631
632
633
634
# File 'lib/sup/buffer.rb', line 623

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

- (Object) roll_buffers

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.



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

def roll_buffers
  @buffers.last.force_to_top = false
  raise_to_front @buffers.first
end

- (Object) roll_buffers_backwards



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

def roll_buffers_backwards
  return unless @buffers.length > 1
  @buffers.last.force_to_top = false
  raise_to_front @buffers[@buffers.length - 2]
end

- (Object) say(s, id = nil)



662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# File 'lib/sup/buffer.rb', line 662

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

- (Object) shell_out(command)



710
711
712
713
714
715
716
717
718
719
# File 'lib/sup/buffer.rb', line 710

def shell_out command
  @shelled = true
  Ncurses.sync do
    Ncurses.endwin
    system command
    Ncurses.refresh
    Ncurses.curs_set 0
  end
  @shelled = false
end

- (Object) spawn(title, mode, opts = {})

Raises:

  • (ArgumentError)


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

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] || false)
  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

- (Object) spawn_modal(title, mode, opts = {})

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



355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
# File 'lib/sup/buffer.rb', line 355

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

  until mode.done?
    c = Ncurses.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

- (Object) spawn_unless_exists(title, opts = {})

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.



309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/sup/buffer.rb', line 309

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