9 stdscr.getmaxyx lame, lamer
15 stdscr.getmaxyx lame, lamer
21 stdscr.getyx lame, lamer
25 def mutex; @mutex ||= Mutex.new; end
26 def sync &b; mutex.synchronize(&b); end
28 ## magically, this stuff seems to work now. i could swear it didn't
31 if IO.select([$stdin], nil, nil, 1)
38 module_function :rows, :cols, :curx, :nonblocking_getch, :mutex, :sync
40 remove_const :KEY_ENTER
41 remove_const :KEY_CANCEL
44 KEY_CANCEL = 7 # ctrl-g
52 attr_reader :mode, :x, :y, :width, :height, :title
54 bool_accessor :force_to_top
56 def initialize window, mode, width, height, opts={}
61 @title = opts[:title] || ""
62 @force_to_top = opts[:force_to_top] || false
63 @x, @y, @width, @height = 0, 0, width, height
66 def content_height; @height - 1; end
67 def content_width; @width; end
70 return if cols == @width && rows == @height
74 mode.resize rows, cols
83 def mark_dirty; @dirty = true; end
96 ## s nil means a blank line!
97 def write y, x, s, opts={}
98 return if x >= @width || y >= @height
100 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
103 @w.mvaddstr y, x, s[0 ... maxl]
104 unless s.length >= maxl || opts[:no_fill]
105 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
114 write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
115 :color => :status_color
134 attr_reader :focus_buf
136 ## we have to define the key used to continue in-buffer search here, because
137 ## it has special semantics that BufferManager deals with---current searches
138 ## are canceled by any keypress except this one.
139 CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
147 @minibuf_mutex = Mutex.new
150 @shelled = @asking = false
152 self.class.i_am_the_instance self
155 def buffers; @name_map.to_a; end
158 return unless @buffers.member? buf
160 return if buf == @focus_buf
161 @focus_buf.blur if @focus_buf
166 def raise_to_front buf
167 return unless @buffers.member? buf
170 if @buffers.length > 0 && @buffers.last.force_to_top?
171 @buffers.insert(-2, buf)
179 ## we reset force_to_top when rolling buffers. this is so that the
180 ## human can actually still move buffers around, while still
181 ## programmatically being able to pop stuff up in the middle of
182 ## drawing a window without worrying about covering it up.
184 ## if we ever start calling roll_buffers programmatically, we will
185 ## have to change this. but it's not clear that we will ever actually
188 @buffers.last.force_to_top = false
189 raise_to_front @buffers.first
192 def roll_buffers_backwards
193 return unless @buffers.length > 1
194 @buffers.last.force_to_top = false
195 raise_to_front @buffers[@buffers.length - 2]
200 if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
201 @focus_buf.mode.cancel_search!
202 @focus_buf.mark_dirty
204 @focus_buf.mode.handle_input c
208 def exists? n; @name_map.member? n; end
209 def [] n; @name_map[n]; end
211 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
212 raise ArgumentError, "title must be a string" unless n.is_a? String
216 def completely_redraw_screen
222 draw_screen :sync => false
226 def draw_screen opts={}
229 Ncurses.mutex.lock unless opts[:sync] == false
231 ## disabling this for the time being, to help with debugging
232 ## (currently we only have one buffer visible at a time).
233 ## TODO: reenable this if we allow multiple buffers
234 false && @buffers.inject(@dirty) do |dirty, buf|
235 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
236 #dirty ? buf.draw : buf.redraw
244 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
245 @dirty ? buf.draw : buf.redraw
248 draw_minibuf :sync => false unless opts[:skip_minibuf]
252 Ncurses.refresh if opts[:refresh]
253 Ncurses.mutex.unlock unless opts[:sync] == false
256 ## gets the mode from the block, which is only called if the buffer
257 ## doesn't already exist. this is useful in the case that generating
258 ## the mode is expensive, as it often is.
259 def spawn_unless_exists title, opts={}
260 if @name_map.member? title
261 raise_to_front @name_map[title] unless opts[:hidden]
264 spawn title, mode, opts
269 def spawn title, mode, opts={}
270 raise ArgumentError, "title must be a string" unless title.is_a? String
273 while @name_map.member? realtitle
274 realtitle = "#{title} <#{num}>"
278 width = opts[:width] || Ncurses.cols
279 height = opts[:height] || Ncurses.rows - 1
281 ## since we are currently only doing multiple full-screen modes,
282 ## use stdscr for each window. once we become more sophisticated,
283 ## we may need to use a new Ncurses::WINDOW
285 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
286 ## (opts[:left] || 0))
288 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
290 @name_map[realtitle] = b
294 focus_on b unless @focus_buf
301 ## requires the mode to have #done? and #value methods
302 def spawn_modal title, mode, opts={}
303 b = spawn title, mode, opts
307 c = Ncurses.nonblocking_getch
308 next unless c # getch timeout
309 break if c == Ncurses::KEY_CANCEL
319 def kill_all_buffers_safely
320 until @buffers.empty?
321 ## inbox mode always claims it's unkillable. we'll ignore it.
322 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
323 kill_buffer @buffers.last
328 def kill_buffer_safely buf
329 return false unless buf.mode.killable?
335 kill_buffer @buffers.first until @buffers.empty?
339 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
343 @name_map.delete buf.title
344 @focus_buf = nil if @focus_buf == buf
346 ## TODO: something intelligent here
347 ## for now I will simply prohibit killing the inbox buffer.
355 def ask_with_completions domain, question, completions, default=nil
356 ask domain, question, default do |s|
357 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
361 def ask_many_with_completions domain, question, completions, default=nil
362 ask domain, question, default do |partial|
367 when /^(.*\s+)?(.*?)$/
370 raise "william screwed up completion: #{partial.inspect}"
373 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
377 def ask_many_emails_with_completions domain, question, completions, default=nil
378 ask domain, question, default do |partial|
379 prefix, target = partial.split_on_commas_with_remainder
380 Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
381 target ||= prefix.pop || ""
382 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
383 Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
384 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
388 def ask_for_filename domain, question, default=nil
389 answer = ask domain, question, default do |s|
390 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
392 name = $2.empty? ? Etc.getlogin : $2
393 dir = Etc.getpwnam(name).dir rescue nil
395 [[s.sub(full, dir), "~#{name}"]]
397 users.select { |u| u =~ /^#{name}/ }.map do |u|
398 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
401 else # regular filename completion
402 Dir["#{s}*"].sort.map do |fn|
403 suffix = File.directory?(fn) ? "/" : ""
404 [fn + suffix, File.basename(fn) + suffix]
412 spawn_modal "file browser", FileBrowserMode.new
413 elsif File.directory?(answer)
414 spawn_modal "file browser", FileBrowserMode.new(answer)
423 ## returns an array of labels
424 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
425 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
426 default = default_labels.join(" ")
427 default += " " unless default.empty?
429 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
431 answer = ask_many_with_completions domain, question, applyable_labels, default
435 user_labels = answer.split(/\s+/).map { |l| l.intern }
436 user_labels.each do |l|
437 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
438 BufferManager.flash "'#{l}' is a reserved label!"
445 def ask_for_contacts domain, question, default_contacts=[]
446 default = default_contacts.map { |s| s.to_s }.join(" ")
447 default += " " unless default.empty?
449 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
450 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
452 completions = (recent + contacts).flatten.uniq.sort
453 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
456 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
461 def ask domain, question, default=nil, &block
462 raise "impossible!" if @asking
465 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
466 tf = @textfields[domain]
469 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
470 ## jesus christ. the exact sequence of ncurses events that needs
471 ## to happen in order to display a form and have the entire screen
472 ## not disappear and have the cursor in the right place is TOO
473 ## FUCKING COMPLICATED.
475 tf.activate question, default, &block
477 draw_screen :skip_minibuf => true, :sync => false
482 Ncurses.sync { Ncurses.refresh }
485 c = Ncurses.nonblocking_getch
486 next unless c # getch timeout
487 break unless tf.handle_input c # process keystroke
489 if tf.new_completions?
490 kill_buffer completion_buf if completion_buf
492 shorts = tf.completions.map { |full, short| short }
493 prefix_len = shorts.shared_prefix.length
495 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
496 completion_buf = spawn "<completions>", mode, :height => 10
498 draw_screen :skip_minibuf => true
500 elsif tf.roll_completions?
501 completion_buf.mode.roll
502 draw_screen :skip_minibuf => true
506 Ncurses.sync { Ncurses.refresh }
509 Ncurses.sync { tf.deactivate }
510 kill_buffer completion_buf if completion_buf
517 ## some pretty lame code in here!
518 def ask_getch question, accept=nil
519 accept = accept.split(//).map { |x| x[0] } if accept
524 Ncurses.move Ncurses.rows - 1, question.length + 1
532 key = Ncurses.nonblocking_getch or next
533 if key == Ncurses::KEY_CANCEL
535 elsif (accept && accept.member?(key)) || !accept
546 draw_screen :sync => false
553 ## returns true (y), false (n), or nil (ctrl-g / cancel)
554 def ask_yes_or_no question
555 case(r = ask_getch question, "ynYN")
566 @minibuf_mutex.synchronize do
569 @minibuf_stack.compact.size, 1].max
573 def draw_minibuf opts={}
575 @minibuf_mutex.synchronize do
576 m = @minibuf_stack.compact
577 m << @flash if @flash
581 Ncurses.mutex.lock unless opts[:sync] == false
582 Ncurses.attrset Colormap.color_for(:none)
583 adj = @asking ? 2 : 1
584 m.each_with_index do |s, i|
585 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
587 Ncurses.refresh if opts[:refresh]
588 Ncurses.mutex.unlock unless opts[:sync] == false
594 @minibuf_mutex.synchronize do
596 id ||= @minibuf_stack.length
597 @minibuf_stack[id] = s
601 draw_screen :refresh => true
603 draw_minibuf :refresh => true
616 def erase_flash; @flash = nil; end
620 draw_screen :refresh => true
623 ## a little tricky because we can't just delete_at id because ids
624 ## are relative (they're positions into the array).
626 @minibuf_mutex.synchronize do
627 @minibuf_stack[id] = nil
628 if id == @minibuf_stack.length - 1
630 break if @minibuf_stack[i]
631 @minibuf_stack.delete_at i
636 draw_screen :refresh => true
639 def shell_out command
655 while(u = Etc.getpwent)