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