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.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/sup/buffer.rb', line 144

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.



104
105
106
# File 'lib/sup/buffer.rb', line 104

def focus_buf
  @focus_buf
end

Instance Method Details

#[](n) ⇒ Object



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

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

#[]=(n, b) ⇒ Object

Raises:

  • (ArgumentError)


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

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



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

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

#ask_for_account(domain, question) ⇒ Object



525
526
527
528
529
530
# File 'lib/sup/buffer.rb', line 525

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



508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# File 'lib/sup/buffer.rb', line 508

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



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

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



485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
# File 'lib/sup/buffer.rb', line 485

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



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

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

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



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

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



418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# File 'lib/sup/buffer.rb', line 418

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

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



411
412
413
414
415
416
# File 'lib/sup/buffer.rb', line 411

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)



627
628
629
630
631
632
633
634
635
636
# File 'lib/sup/buffer.rb', line 627

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

#buffersObject



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

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



717
718
719
720
721
722
723
724
725
726
727
728
729
# File 'lib/sup/buffer.rb', line 717

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



234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
# File 'lib/sup/buffer.rb', line 234

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



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

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



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

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



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

def erase_flash; @flash = nil; end

#exists?(n) ⇒ Boolean

Returns:

  • (Boolean)


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

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

#flash(s) ⇒ Object



710
711
712
713
# File 'lib/sup/buffer.rb', line 710

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

#focus_on(buf) ⇒ Object



172
173
174
175
176
177
178
# File 'lib/sup/buffer.rb', line 172

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



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

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

#kill_all_buffersObject



380
381
382
# File 'lib/sup/buffer.rb', line 380

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

#kill_all_buffers_safelyObject



365
366
367
368
369
370
371
372
# File 'lib/sup/buffer.rb', line 365

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)


384
385
386
387
388
389
390
391
392
393
394
395
396
397
# File 'lib/sup/buffer.rb', line 384

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



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

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

#minibuf_linesObject



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

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



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

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.



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

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.



199
200
201
202
203
# File 'lib/sup/buffer.rb', line 199

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

#roll_buffers_backwardsObject



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

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



212
213
214
# File 'lib/sup/buffer.rb', line 212

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

#say(s, id = nil) ⇒ Object



683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
# File 'lib/sup/buffer.rb', line 683

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



731
732
733
734
735
736
737
738
739
740
741
# File 'lib/sup/buffer.rb', line 731

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)


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

def shelled?; @shelled; end

#sigwinch_happened!Object



159
160
161
162
163
164
165
# File 'lib/sup/buffer.rb', line 159

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

#sigwinch_happened?Boolean

Returns:

  • (Boolean)


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

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

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

Raises:

  • (ArgumentError)


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

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



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

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

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



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

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