lib/sup/buffer.rb in sup-0.0.8 vs lib/sup/buffer.rb in sup-0.1
- old
+ new
@@ -1,5 +1,6 @@
+require 'etc'
require 'thread'
module Ncurses
def rows
lame, lamer = [], []
@@ -14,37 +15,25 @@
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
- ## ALL THREAD ACTIVITY. as in, no threads anywhere will run while
- ## it's waiting for input. ok, fine, so we wrap it in a select. Of
- ## course we also rely on Ncurses.getch to tell us when an xterm
- ## resize has occurred, which select won't catch, so we won't
- ## resize outselves after a sigwinch until the user hits a key.
- ## and installing our own sigwinch handler means that the screen
- ## size returned by getmaxyx() DOESN'T UPDATE! and Kernel#trap
- ## RETURNS NIL as the previous handler!
- ##
- ## so basically, resizing with multi-threaded ruby Ncurses
- ## applications will always be broken.
- ##
- ## i've coined a new word for this: lametarded.
+ ## magically, this stuff seems to work now. i could swear it didn't
+ ## before. hm.
def nonblocking_getch
- if IO.select([$stdin], nil, nil, nil)
+ if IO.select([$stdin], nil, nil, 1)
Ncurses.getch
else
nil
end
end
module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
- KEY_CANCEL = "\a"[0] # ctrl-g
+ KEY_ENTER = 10
+ KEY_CANCEL = ?\a # ctrl-g
+ KEY_TAB = 9
end
module Redwood
class Buffer
@@ -194,10 +183,11 @@
def exists? n; @name_map.member? n; end
def [] n; @name_map[n]; end
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
def completely_redraw_screen
return if @shelled
@@ -207,39 +197,34 @@
Ncurses.clear
draw_screen :sync => false
end
end
- def handle_resize
- return if @shelled
- rows, cols = Ncurses.rows, Ncurses.cols
- @buffers.each { |b| b.resize rows - minibuf_lines, cols }
- completely_redraw_screen
- flash "Resized to #{rows}x#{cols}"
- end
-
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|
buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
- @dirty ? buf.draw : buf.redraw
+ #dirty ? buf.draw : buf.redraw
+ buf.draw
+ dirty
end
## quick hack
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
@@ -256,10 +241,11 @@
end
@name_map[title]
end
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
@@ -286,15 +272,33 @@
raise_to_front b
end
b
end
+ ## requires the mode to have #done? and #value methods
+ 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
+
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
+ return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
+ kill_buffer @buffers.last
end
true
end
def kill_buffer_safely buf
@@ -320,41 +324,111 @@
else
raise_to_front @buffers.last
end
end
- ## not really thread safe.
- def ask domain, question, default=nil
+ def ask_with_completions domain, question, completions, default=nil
+ ask domain, question, default do |s|
+ completions.select { |x| x =~ /^#{s}/i }.map { |x| [x.downcase, x] }
+ end
+ end
+
+ ## returns an ARRAY of filenames!
+ def ask_for_filenames 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
+
+ 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.
+ ## 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
+ tf.activate question, default, &block
@dirty = true
draw_screen :skip_minibuf => true, :sync => false
end
ret = nil
tf.position_cursor
Ncurses.sync { Ncurses.refresh }
- @asking = true
- while tf.handle_input(Ncurses.nonblocking_getch); end
- @asking = false
+ while true
+ c = Ncurses.nonblocking_getch
+ next unless c # getch timeout
+ break unless tf.handle_input c # process keystroke
- ret = tf.value
+ if tf.new_completions?
+ kill_buffer completion_buf if completion_buf
+
+ prefix_len =
+ if tf.value =~ /\/$/
+ 0
+ else
+ File.basename(tf.value).length
+ end
+
+ mode = CompletionMode.new tf.completions.map { |full, short| short }, :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
-
- ret
+ @asking = false
+ draw_screen
+ tf.value
end
## some pretty lame code in here!
def ask_getch question, accept=nil
accept = accept.split(//).map { |x| x[0] } if accept
@@ -368,11 +442,11 @@
ret = nil
done = false
@shelled = true
until done
- key = Ncurses.nonblocking_getch
+ key = Ncurses.nonblocking_getch or next
if key == Ncurses::KEY_CANCEL
done = true
elsif (accept && accept.member?(key)) || !accept
ret = key
done = true
@@ -484,8 +558,20 @@
system command
Ncurses.refresh
Ncurses.curs_set 0
end
@shelled = false
+ end
+
+private
+
+ def users
+ unless @users
+ @users = []
+ while(u = Etc.getpwent)
+ @users << u.name
+ end
+ end
+ @users
end
end
end