7 stdscr.getmaxyx lame, lamer
13 stdscr.getmaxyx lame, lamer
17 def mutex; @mutex ||= Mutex.new; end
18 def sync &b; mutex.synchronize(&b); end
20 ## magically, this stuff seems to work now. i could swear it didn't
23 if IO.select([$stdin], nil, nil, 1)
30 module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
33 KEY_CANCEL = ?\a # ctrl-g
40 attr_reader :mode, :x, :y, :width, :height, :title
42 bool_accessor :force_to_top
44 def initialize window, mode, width, height, opts={}
49 @title = opts[:title] || ""
50 @force_to_top = opts[:force_to_top] || false
51 @x, @y, @width, @height = 0, 0, width, height
54 def content_height; @height - 1; end
55 def content_width; @width; end
58 return if cols == @width && rows == @height
62 mode.resize rows, cols
71 def mark_dirty; @dirty = true; end
84 ## s nil means a blank line!
85 def write y, x, s, opts={}
86 return if x >= @width || y >= @height
88 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
91 @w.mvaddstr y, x, s[0 ... maxl]
92 unless s.length >= maxl || opts[:no_fill]
93 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
102 write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
103 :color => :status_color
122 attr_reader :focus_buf
130 @minibuf_mutex = Mutex.new
133 @shelled = @asking = false
135 self.class.i_am_the_instance self
138 def buffers; @name_map.to_a; end
141 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
142 return if buf == @focus_buf
143 @focus_buf.blur if @focus_buf
148 def raise_to_front buf
149 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
152 if @buffers.length > 0 && @buffers.last.force_to_top?
153 @buffers.insert(-2, buf)
161 ## we reset force_to_top when rolling buffers. this is so that the
162 ## human can actually still move buffers around, while still
163 ## programmatically being able to pop stuff up in the middle of
164 ## drawing a window without worrying about covering it up.
166 ## if we ever start calling roll_buffers programmatically, we will
167 ## have to change this. but it's not clear that we will ever actually
170 @buffers.last.force_to_top = false
171 raise_to_front @buffers.first
174 def roll_buffers_backwards
175 return unless @buffers.length > 1
176 @buffers.last.force_to_top = false
177 raise_to_front @buffers[@buffers.length - 2]
181 @focus_buf && @focus_buf.mode.handle_input(c)
184 def exists? n; @name_map.member? n; end
185 def [] n; @name_map[n]; end
187 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
188 raise ArgumentError, "title must be a string" unless n.is_a? String
192 def completely_redraw_screen
198 draw_screen :sync => false
202 def draw_screen opts={}
205 Ncurses.mutex.lock unless opts[:sync] == false
207 ## disabling this for the time being, to help with debugging
208 ## (currently we only have one buffer visible at a time).
209 ## TODO: reenable this if we allow multiple buffers
210 false && @buffers.inject(@dirty) do |dirty, buf|
211 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
212 #dirty ? buf.draw : buf.redraw
220 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
221 @dirty ? buf.draw : buf.redraw
224 draw_minibuf :sync => false unless opts[:skip_minibuf]
228 Ncurses.refresh if opts[:refresh]
229 Ncurses.mutex.unlock unless opts[:sync] == false
232 ## gets the mode from the block, which is only called if the buffer
233 ## doesn't already exist. this is useful in the case that generating
234 ## the mode is expensive, as it often is.
235 def spawn_unless_exists title, opts={}
236 if @name_map.member? title
237 raise_to_front @name_map[title] unless opts[:hidden]
240 spawn title, mode, opts
245 def spawn title, mode, opts={}
246 raise ArgumentError, "title must be a string" unless title.is_a? String
249 while @name_map.member? realtitle
250 realtitle = "#{title} <#{num}>"
254 width = opts[:width] || Ncurses.cols
255 height = opts[:height] || Ncurses.rows - 1
257 ## since we are currently only doing multiple full-screen modes,
258 ## use stdscr for each window. once we become more sophisticated,
259 ## we may need to use a new Ncurses::WINDOW
261 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
262 ## (opts[:left] || 0))
264 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
266 @name_map[realtitle] = b
270 focus_on b unless @focus_buf
277 ## requires the mode to have #done? and #value methods
278 def spawn_modal title, mode, opts={}
279 b = spawn title, mode, opts
283 c = Ncurses.nonblocking_getch
284 next unless c # getch timeout
285 break if c == Ncurses::KEY_CANCEL
295 def kill_all_buffers_safely
296 until @buffers.empty?
297 ## inbox mode always claims it's unkillable. we'll ignore it.
298 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
299 kill_buffer @buffers.last
304 def kill_buffer_safely buf
305 return false unless buf.mode.killable?
311 kill_buffer @buffers.first until @buffers.empty?
315 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
319 @name_map.delete buf.title
320 @focus_buf = nil if @focus_buf == buf
322 ## TODO: something intelligent here
323 ## for now I will simply prohibit killing the inbox buffer.
325 raise_to_front @buffers.last
329 def ask_with_completions domain, question, completions, default=nil
330 ask domain, question, default do |s|
331 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
335 ## returns an ARRAY of filenames!
336 def ask_for_filenames domain, question, default=nil
337 answer = ask domain, question, default do |s|
338 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
340 name = $2.empty? ? Etc.getlogin : $2
341 dir = Etc.getpwnam(name).dir rescue nil
343 [[s.sub(full, dir), "~#{name}"]]
345 users.select { |u| u =~ /^#{name}/ }.map do |u|
346 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
349 else # regular filename completion
350 Dir["#{s}*"].sort.map do |fn|
351 suffix = File.directory?(fn) ? "/" : ""
352 [fn + suffix, File.basename(fn) + suffix]
360 spawn_modal "file browser", FileBrowserMode.new
361 elsif File.directory?(answer)
362 spawn_modal "file browser", FileBrowserMode.new(answer)
371 def ask domain, question, default=nil, &block
372 raise "impossible!" if @asking
375 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
376 tf = @textfields[domain]
379 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
380 ## jesus christ. the exact sequence of ncurses events that needs
381 ## to happen in order to display a form and have the entire screen
382 ## not disappear and have the cursor in the right place is TOO
383 ## FUCKING COMPLICATED.
385 tf.activate question, default, &block
387 draw_screen :skip_minibuf => true, :sync => false
392 Ncurses.sync { Ncurses.refresh }
395 c = Ncurses.nonblocking_getch
396 next unless c # getch timeout
397 break unless tf.handle_input c # process keystroke
399 if tf.new_completions?
400 kill_buffer completion_buf if completion_buf
406 File.basename(tf.value).length
409 mode = CompletionMode.new tf.completions.map { |full, short| short }, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
410 completion_buf = spawn "<completions>", mode, :height => 10
412 draw_screen :skip_minibuf => true
414 elsif tf.roll_completions?
415 completion_buf.mode.roll
417 draw_screen :skip_minibuf => true
421 Ncurses.sync { Ncurses.refresh }
424 Ncurses.sync { tf.deactivate }
425 kill_buffer completion_buf if completion_buf
432 ## some pretty lame code in here!
433 def ask_getch question, accept=nil
434 accept = accept.split(//).map { |x| x[0] } if accept
439 Ncurses.move Ncurses.rows - 1, question.length + 1
447 key = Ncurses.nonblocking_getch or next
448 if key == Ncurses::KEY_CANCEL
450 elsif (accept && accept.member?(key)) || !accept
461 draw_screen :sync => false
468 ## returns true (y), false (n), or nil (ctrl-g / cancel)
469 def ask_yes_or_no question
470 case(r = ask_getch question, "ynYN")
481 @minibuf_mutex.synchronize do
484 @minibuf_stack.compact.size, 1].max
488 def draw_minibuf opts={}
490 @minibuf_mutex.synchronize do
491 m = @minibuf_stack.compact
492 m << @flash if @flash
496 Ncurses.mutex.lock unless opts[:sync] == false
497 Ncurses.attrset Colormap.color_for(:none)
498 adj = @asking ? 2 : 1
499 m.each_with_index do |s, i|
500 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
502 Ncurses.refresh if opts[:refresh]
503 Ncurses.mutex.unlock unless opts[:sync] == false
509 @minibuf_mutex.synchronize do
511 id ||= @minibuf_stack.length
512 @minibuf_stack[id] = s
516 draw_screen :refresh => true
518 draw_minibuf :refresh => true
531 def erase_flash; @flash = nil; end
535 draw_screen :refresh => true
538 ## a little tricky because we can't just delete_at id because ids
539 ## are relative (they're positions into the array).
541 @minibuf_mutex.synchronize do
542 @minibuf_stack[id] = nil
543 if id == @minibuf_stack.length - 1
545 break if @minibuf_stack[i]
546 @minibuf_stack.delete_at i
551 draw_screen :refresh => true
554 def shell_out command
570 while(u = Etc.getpwent)