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

Constructor Details

#initializeBufferManager

Returns a new instance of BufferManager.



168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/sup/buffer.rb', line 168

def initialize
  @name_map = {}
  @buffers = []
  @focus_buf = nil
  @dirty = true
  @minibuf_stack = []
  @minibuf_mutex = Mutex.new
  @textfields = {}
  @flash = nil
  @shelled = @asking = false

  self.class.i_am_the_instance self
end

Instance Attribute Details

#focus_bufObject (readonly)

Returns the value of attribute focus_buf.



137
138
139
# File 'lib/sup/buffer.rb', line 137

def focus_buf
  @focus_buf
end

Instance Method Details

#[](n) ⇒ Object



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

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

#[]=(n, b) ⇒ Object

Raises:

  • (ArgumentError)


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

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



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

def ask domain, question, default=nil, &block
  raise "impossible!" if @asking
  @asking = true

  @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
  tf = @textfields[domain]
  completion_buf = nil

  ## this goddamn ncurses form shit is a fucking 1970's nightmare.
  ## jesus christ. the exact sequence of ncurses events that needs
  ## to happen in order to display a form and have the entire screen
  ## not disappear and have the cursor in the right place is TOO
  ## FUCKING COMPLICATED.
  Ncurses.sync do
    tf.activate question, default, &block
    @dirty = true
    draw_screen :skip_minibuf => true, :sync => false
    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
  
  Ncurses.sync { tf.deactivate }
  kill_buffer completion_buf if completion_buf
  @dirty = true
  @asking = false
  draw_screen
  tf.value
end

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



487
488
489
490
491
492
493
494
495
496
497
498
499
500
# File 'lib/sup/buffer.rb', line 487

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.sort
  answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default

  if answer
    answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
  end
end

#ask_for_filename(domain, question, default = nil) ⇒ Object



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

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 =~ /^#{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
        answer
      end
  end

  answer
end

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

returns an array of labels



466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
# File 'lib/sup/buffer.rb', line 466

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

#ask_getch(question, accept = nil) ⇒ Object

some pretty lame code in here!



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

def ask_getch question, accept=nil
  accept = accept.split(//).map { |x| x[0] } if accept

  flash question
  Ncurses.sync do
    Ncurses.curs_set 1
    Ncurses.move Ncurses.rows - 1, question.length + 1
    Ncurses.refresh
  end

  ret = nil
  done = false
  @shelled = true
  until done
    key = Ncurses.nonblocking_getch or next
    if key == Ncurses::KEY_CANCEL
      done = true
    elsif (accept && accept.member?(key)) || !accept
      ret = key
      done = true
    end
  end

  @shelled = false

  Ncurses.sync do
    Ncurses.curs_set 0
    erase_flash
    draw_screen :sync => false
    Ncurses.curs_set 0
  end

  ret
end

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



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

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
    Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
    target ||= prefix.pop || ""
    prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
    Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
    completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
  end
end

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



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

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 =~ /^#{target}/i }.map { |x| [prefix + x, x] }
  end
end

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



397
398
399
400
401
# File 'lib/sup/buffer.rb', line 397

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

#ask_yes_or_no(question) ⇒ Object

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



594
595
596
597
598
599
600
601
602
603
# File 'lib/sup/buffer.rb', line 594

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

#buffersObject



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

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



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

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



243
244
245
246
247
248
249
250
251
252
253
# File 'lib/sup/buffer.rb', line 243

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

#draw_minibuf(opts = {}) ⇒ Object



613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
# File 'lib/sup/buffer.rb', line 613

def draw_minibuf opts={}
  m = nil
  @minibuf_mutex.synchronize do
    m = @minibuf_stack.compact
    m << @flash if @flash
    m << "" if m.empty?
  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



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

def draw_screen opts={}
  return if @shelled

  status, title =
    if opts.member? :status
      [opts[:status], opts[:title]]
    else
      get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
    end

  print "\033]2;#{title}\07" if title

  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



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

def erase_flash; @flash = nil; end

#exists?(n) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#flash(s) ⇒ Object



658
659
660
661
# File 'lib/sup/buffer.rb', line 658

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

#focus_on(buf) ⇒ Object



184
185
186
187
188
189
190
191
# File 'lib/sup/buffer.rb', line 184

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



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

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



376
377
378
# File 'lib/sup/buffer.rb', line 376

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

#kill_all_buffers_safelyObject



361
362
363
364
365
366
367
368
# File 'lib/sup/buffer.rb', line 361

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)


380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/sup/buffer.rb', line 380

def kill_buffer buf
  raise ArgumentError, "buffer not on stack: #{buf.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
    last = @buffers.last
    @focus_buf ||= last
    raise_to_front last
  end
end

#kill_buffer_safely(buf) ⇒ Object



370
371
372
373
374
# File 'lib/sup/buffer.rb', line 370

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

#minibuf_linesObject



605
606
607
608
609
610
611
# File 'lib/sup/buffer.rb', line 605

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



193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/sup/buffer.rb', line 193

def raise_to_front buf
  return unless @buffers.member? buf

  @buffers.delete buf
  if @buffers.length > 0 && @buffers.last.force_to_top?
    @buffers.insert(-2, buf)
  else
    @buffers.push buf
    focus_on buf
  end
  @dirty = true
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.



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

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

#roll_buffers_backwardsObject



219
220
221
222
223
# File 'lib/sup/buffer.rb', line 219

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

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

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



679
680
681
682
683
684
685
686
687
688
# File 'lib/sup/buffer.rb', line 679

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

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

Raises:

  • (ArgumentError)


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

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

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

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



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/sup/buffer.rb', line 344

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
    mode.handle_input c
    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.



298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/sup/buffer.rb', line 298

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