]> git.cworth.org Git - sup/blobdiff - lib/sup/buffer.rb
ask when quitting with unsaved buffers
[sup] / lib / sup / buffer.rb
index 03aead6d801af25bf2b9717b0990688667b60931..4785a14e5069ee0bf7610349541534f3d1ce4de3 100644 (file)
@@ -13,6 +13,9 @@ module Ncurses
     lamer.first
   end
 
+  def mutex; @mutex ||= Mutex.new; end
+  def sync &b; mutex.synchronize(&b); end
+
   ## aaahhh, user input. who would have though that such a simple
   ## idea would be SO FUCKING COMPLICATED?! because apparently
   ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
@@ -37,7 +40,7 @@ module Ncurses
     end
   end
 
-  module_function :rows, :cols, :nonblocking_getch
+  module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
 
   KEY_CANCEL = "\a"[0] # ctrl-g
 end
@@ -60,9 +63,11 @@ class Buffer
   def content_height; @height - 1; end
   def content_width; @width; end
 
-  def resize rows, cols
+  def resize rows, cols 
+    return if cols == @width && rows == @height
     @width = cols
     @height = rows
+    @dirty = true
     mode.resize rows, cols
   end
 
@@ -71,6 +76,7 @@ class Buffer
     draw_status
     commit
   end
+
   def mark_dirty; @dirty = true; end
 
   def commit
@@ -130,9 +136,10 @@ class BufferManager
     @focus_buf = nil
     @dirty = true
     @minibuf_stack = []
+    @minibuf_mutex = Mutex.new
     @textfields = {}
     @flash = nil
-    @freeze = false
+    @shelled = @asking = false
 
     self.class.i_am_the_instance self
   end
@@ -140,8 +147,7 @@ class BufferManager
   def buffers; @name_map.to_a; end
 
   def focus_on buf
-    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
-      @buffers.member? buf
+    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
     return if buf == @focus_buf 
     @focus_buf.blur if @focus_buf
     @focus_buf = buf
@@ -149,8 +155,7 @@ class BufferManager
   end
 
   def raise_to_front buf
-    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
-      @buffers.member? buf
+    raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
     @buffers.delete buf
     @buffers.push buf
     focus_on buf
@@ -178,36 +183,48 @@ class BufferManager
   end
 
   def completely_redraw_screen
-    return if @freeze
-    Ncurses.clear
-    @dirty = true
-    draw_screen
+    return if @shelled
+
+    Ncurses.sync do
+      @dirty = true
+      Ncurses.clear
+      draw_screen :sync => false
+    end
   end
 
   def handle_resize
-    return if @freeze
+    return if @shelled
     rows, cols = Ncurses.rows, Ncurses.cols
-    @buffers.each { |b| b.resize rows - 1, cols }
+    @buffers.each { |b| b.resize rows - minibuf_lines, cols }
     completely_redraw_screen
-    flash "resized to #{rows}x#{cols}"
+    flash "Resized to #{rows}x#{cols}"
   end
 
-  def draw_screen skip_minibuf=false
-    return if @freeze
+  def draw_screen opts={}
+    return if @shelled
+
+    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|
-      dirty ? buf.draw : buf.redraw
-      dirty || buf.dirty?
+      buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
+      @dirty ? buf.draw : buf.redraw
     end
+
     ## quick hack
-    true && (@dirty ? @buffers.last.draw : @buffers.last.redraw)
-    
-    draw_minibuf unless skip_minibuf
+    if true
+      buf = @buffers.last
+      buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
+      @dirty ? buf.draw : buf.redraw
+    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
 
   ## gets the mode from the block, which is only called if the buffer
@@ -215,8 +232,7 @@ class BufferManager
   ## the mode is expensive, as it often is.
   def spawn_unless_exists title, opts={}
     if @name_map.member? title
-      Redwood::log "buffer '#{title}' already exists, raising to front"
-      raise_to_front @name_map[title]
+      raise_to_front @name_map[title] unless opts[:hidden]
     else
       mode = yield
       spawn title, mode, opts
@@ -228,11 +244,10 @@ class BufferManager
     realtitle = title
     num = 2
     while @name_map.member? realtitle
-      realtitle = "#{title} #{num}"
+      realtitle = "#{title} <#{num}>"
       num += 1
     end
 
-    Redwood::log "spawning buffer \"#{realtitle}\""
     width = opts[:width] || Ncurses.cols
     height = opts[:height] || Ncurses.rows - 1
 
@@ -243,8 +258,6 @@ class BufferManager
     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
     ## (opts[:left] || 0))
     w = Ncurses.stdscr
-    raise "nil window" unless w
-    
     b = Buffer.new w, mode, width, height, :title => realtitle
     mode.buffer = b
     @name_map[realtitle] = b
@@ -258,13 +271,27 @@ class BufferManager
     b
   end
 
+  def kill_all_buffers_safely
+    until @buffers.empty?
+      ## inbox mode always claims it's unkillable. we'll ignore it.
+      return false unless @buffers.first.mode.is_a?(InboxMode) || @buffers.first.mode.killable?
+      kill_buffer @buffers.first
+    end
+    true
+  end
+
+  def kill_buffer_safely buf
+    return false unless buf.mode.killable?
+    kill_buffer buf
+    true
+  end
+
   def kill_all_buffers
     kill_buffer @buffers.first until @buffers.empty?
   end
 
   def kill_buffer buf
     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
-    Redwood::log "killing buffer \"#{buf.title}\""
 
     buf.mode.cleanup
     @buffers.delete buf
@@ -278,9 +305,11 @@ class BufferManager
     end
   end
 
+  ## not really thread safe.
   def ask domain, question, default=nil
-    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0,
-                            Ncurses.cols
+    raise "impossible!" if @asking
+
+    @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
     tf = @textfields[domain]
 
     ## this goddamn ncurses form shit is a fucking 1970's
@@ -288,19 +317,22 @@ class BufferManager
     ## 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.
-    tf.activate question, default
-    @dirty = true
-    draw_screen true
+    Ncurses.sync do
+      tf.activate question, default
+      @dirty = true
+      draw_screen :skip_minibuf => true, :sync => false
+    end
 
     ret = nil
-    @freeze = true
     tf.position_cursor
-    Ncurses.refresh
+    Ncurses.sync { Ncurses.refresh }
+
+    @asking = true
     while tf.handle_input(Ncurses.nonblocking_getch); end
-    @freeze = false
+    @asking = false
 
     ret = tf.value
-    tf.deactivate
+    Ncurses.sync { tf.deactivate }
     @dirty = true
 
     ret
@@ -311,13 +343,15 @@ class BufferManager
     accept = accept.split(//).map { |x| x[0] } if accept
 
     flash question
-    Ncurses.curs_set 1
-    Ncurses.move Ncurses.rows - 1, question.length + 1
-    Ncurses.refresh
+    Ncurses.sync do
+      Ncurses.curs_set 1
+      Ncurses.move Ncurses.rows - 1, question.length + 1
+      Ncurses.refresh
+    end
 
     ret = nil
     done = false
-    @freeze = true
+    @shelled = true
     until done
       key = Ncurses.nonblocking_getch
       if key == Ncurses::KEY_CANCEL
@@ -327,33 +361,78 @@ class BufferManager
         done = true
       end
     end
-    @freeze = false
-    Ncurses.curs_set 0
-    erase_flash
-    draw_screen
-    Ncurses.curs_set 0
+
+    @shelled = false
+
+    Ncurses.sync do
+      Ncurses.curs_set 0
+      erase_flash
+      draw_screen :sync => false
+      Ncurses.curs_set 0
+    end
 
     ret
   end
 
+  ## returns true (y), false (n), or nil (ctrl-g / cancel)
   def ask_yes_or_no question
-    [?y, ?Y].member? ask_getch(question, "ynYN")
+    case(r = ask_getch question, "ynYN")
+    when ?y, ?Y
+      true
+    when nil
+      nil
+    else
+      false
+    end
   end
 
-  def draw_minibuf
-    s = @flash || @minibuf_stack.reverse.find { |x| x } || ""
+  def minibuf_lines
+    @minibuf_mutex.synchronize do
+      [(@flash ? 1 : 0) + 
+       (@asking ? 1 : 0) +
+       @minibuf_stack.compact.size, 1].max
+    end
+  end
+  
+  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)
-    Ncurses.mvaddstr Ncurses.rows - 1, 0, s + (" " * [Ncurses.cols - s.length,
-                                                      0].max)
+    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
 
   def say s, id=nil
-    id ||= @minibuf_stack.length
-    @minibuf_stack[id] = s
-    unless @freeze
-      draw_screen
-      Ncurses.refresh
+    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
@@ -362,33 +441,34 @@ class BufferManager
 
   def flash s
     @flash = s
-    unless @freeze
-      draw_screen
-      Ncurses.refresh
-    end
+    draw_screen :refresh => true
   end
 
+  ## a little tricky because we can't just delete_at id because ids
+  ## are relative (they're positions into the array).
   def clear id
-    @minibuf_stack[id] = nil
-    if id == @minibuf_stack.length - 1
-      id.downto(0) do |i|
-        break unless @minibuf_stack[i].nil?
-        @minibuf_stack.delete_at i
+    @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
-    unless @freeze
-      draw_screen
-      Ncurses.refresh
-    end
+
+    draw_screen :refresh => true
   end
 
   def shell_out command
-    @freeze = true
-    Ncurses.endwin
-    system command
-    Ncurses.refresh
-    Ncurses.curs_set 0
-    @freeze = false
+    @shelled = true
+    Ncurses.sync do
+      Ncurses.endwin
+      system command
+      Ncurses.refresh
+      Ncurses.curs_set 0
+    end
+    @shelled = false
   end
 end
 end