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
87 def mark_dirty; @dirty = true; end
100 ## s nil means a blank line!
101 def write y, x, s, opts={}
102 return if x >= @width || y >= @height
104 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
107 @w.mvaddstr y, x, s[0 ... maxl]
108 unless s.length >= maxl || opts[:no_fill]
109 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
117 def draw_status status
118 write @height - 1, 0, status, :color => :status_color
137 attr_reader :focus_buf
139 ## we have to define the key used to continue in-buffer search here, because
140 ## it has special semantics that BufferManager deals with---current searches
141 ## are canceled by any keypress except this one.
142 CONTINUE_IN_BUFFER_SEARCH_KEY = "n"
144 HookManager.register "status-bar-text", <<EOS
145 Sets the status bar. The default status bar contains the mode name, the buffer
146 title, and the mode status. Note that this will be called at least once per
147 keystroke, so excessive computation is discouraged.
150 num_inbox: number of messages in inbox
151 num_inbox_unread: total number of messages marked as unread
152 num_total: total number of messages in the index
153 num_spam: total number of messages marked as spam
154 title: title of the current buffer
155 mode: current mode name (string)
156 status: current mode status (string)
157 Return value: a string to be used as the status bar.
160 HookManager.register "terminal-title-text", <<EOS
161 Sets the title of the current terminal, if applicable. Note that this will be
162 called at least once per keystroke, so excessive computation is discouraged.
164 Variables: the same as status-bar-text hook.
165 Return value: a string to be used as the terminal title.
174 @minibuf_mutex = Mutex.new
177 @shelled = @asking = false
179 self.class.i_am_the_instance self
182 def buffers; @name_map.to_a; end
185 return unless @buffers.member? buf
186 return if buf == @focus_buf
187 @focus_buf.blur if @focus_buf
192 def raise_to_front buf
193 @buffers.delete(buf) or return
194 if @buffers.length > 0 && @buffers.last.force_to_top?
195 @buffers.insert(-2, buf)
199 focus_on @buffers.last
203 ## we reset force_to_top when rolling buffers. this is so that the
204 ## human can actually still move buffers around, while still
205 ## programmatically being able to pop stuff up in the middle of
206 ## drawing a window without worrying about covering it up.
208 ## if we ever start calling roll_buffers programmatically, we will
209 ## have to change this. but it's not clear that we will ever actually
212 @buffers.last.force_to_top = false
213 raise_to_front @buffers.first
216 def roll_buffers_backwards
217 return unless @buffers.length > 1
218 @buffers.last.force_to_top = false
219 raise_to_front @buffers[@buffers.length - 2]
224 if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
225 @focus_buf.mode.cancel_search!
226 @focus_buf.mark_dirty
228 @focus_buf.mode.handle_input c
232 def exists? n; @name_map.member? n; end
233 def [] n; @name_map[n]; end
235 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
236 raise ArgumentError, "title must be a string" unless n.is_a? String
240 def completely_redraw_screen
243 status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
248 draw_screen :sync => false, :status => status, :title => title
252 def draw_screen opts={}
256 if opts.member? :status
257 [opts[:status], opts[:title]]
259 get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
262 print "\033]2;#{title}\07" if title
264 Ncurses.mutex.lock unless opts[:sync] == false
266 ## disabling this for the time being, to help with debugging
267 ## (currently we only have one buffer visible at a time).
268 ## TODO: reenable this if we allow multiple buffers
269 false && @buffers.inject(@dirty) do |dirty, buf|
270 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
271 #dirty ? buf.draw : buf.redraw
279 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
280 @dirty ? buf.draw(status) : buf.redraw(status)
283 draw_minibuf :sync => false unless opts[:skip_minibuf]
287 Ncurses.refresh if opts[:refresh]
288 Ncurses.mutex.unlock unless opts[:sync] == false
291 ## if the named buffer already exists, pops it to the front without
292 ## calling the block. otherwise, gets the mode from the block and
293 ## creates a new buffer. returns two things: the buffer, and a boolean
294 ## indicating whether it's a new buffer or not.
295 def spawn_unless_exists title, opts={}
297 if @name_map.member? title
298 raise_to_front @name_map[title] unless opts[:hidden]
302 spawn title, mode, opts
305 [@name_map[title], new]
308 def spawn title, mode, opts={}
309 raise ArgumentError, "title must be a string" unless title.is_a? String
312 while @name_map.member? realtitle
313 realtitle = "#{title} <#{num}>"
317 width = opts[:width] || Ncurses.cols
318 height = opts[:height] || Ncurses.rows - 1
320 ## since we are currently only doing multiple full-screen modes,
321 ## use stdscr for each window. once we become more sophisticated,
322 ## we may need to use a new Ncurses::WINDOW
324 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
325 ## (opts[:left] || 0))
327 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
329 @name_map[realtitle] = b
333 focus_on b unless @focus_buf
340 ## requires the mode to have #done? and #value methods
341 def spawn_modal title, mode, opts={}
342 b = spawn title, mode, opts
346 c = Ncurses.nonblocking_getch
347 next unless c # getch timeout
348 break if c == Ncurses::KEY_CANCEL
358 def kill_all_buffers_safely
359 until @buffers.empty?
360 ## inbox mode always claims it's unkillable. we'll ignore it.
361 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
362 kill_buffer @buffers.last
367 def kill_buffer_safely buf
368 return false unless buf.mode.killable?
374 kill_buffer @buffers.first until @buffers.empty?
378 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
382 @name_map.delete buf.title
383 @focus_buf = nil if @focus_buf == buf
385 ## TODO: something intelligent here
386 ## for now I will simply prohibit killing the inbox buffer.
388 raise_to_front @buffers.last
392 def ask_with_completions domain, question, completions, default=nil
393 ask domain, question, default do |s|
394 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
398 def ask_many_with_completions domain, question, completions, default=nil
399 ask domain, question, default do |partial|
404 when /^(.*\s+)?(.*?)$/
407 raise "william screwed up completion: #{partial.inspect}"
410 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
414 def ask_many_emails_with_completions domain, question, completions, default=nil
415 ask domain, question, default do |partial|
416 prefix, target = partial.split_on_commas_with_remainder
417 Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
418 target ||= prefix.pop || ""
419 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
420 Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
421 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
425 def ask_for_filename domain, question, default=nil
426 answer = ask domain, question, default do |s|
427 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
429 name = $2.empty? ? Etc.getlogin : $2
430 dir = Etc.getpwnam(name).dir rescue nil
432 [[s.sub(full, dir), "~#{name}"]]
434 users.select { |u| u =~ /^#{name}/ }.map do |u|
435 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
438 else # regular filename completion
439 Dir["#{s}*"].sort.map do |fn|
440 suffix = File.directory?(fn) ? "/" : ""
441 [fn + suffix, File.basename(fn) + suffix]
449 spawn_modal "file browser", FileBrowserMode.new
450 elsif File.directory?(answer)
451 spawn_modal "file browser", FileBrowserMode.new(answer)
460 ## returns an array of labels
461 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
462 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
463 default = default_labels.join(" ")
464 default += " " unless default.empty?
466 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
468 answer = ask_many_with_completions domain, question, applyable_labels, default
472 user_labels = answer.split(/\s+/).map { |l| l.intern }
473 user_labels.each do |l|
474 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
475 BufferManager.flash "'#{l}' is a reserved label!"
482 def ask_for_contacts domain, question, default_contacts=[]
483 default = default_contacts.map { |s| s.to_s }.join(" ")
484 default += " " unless default.empty?
486 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
487 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
489 completions = (recent + contacts).flatten.uniq.sort
490 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
493 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
498 def ask domain, question, default=nil, &block
499 raise "impossible!" if @asking
502 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
503 tf = @textfields[domain]
506 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
507 ## jesus christ. the exact sequence of ncurses events that needs
508 ## to happen in order to display a form and have the entire screen
509 ## not disappear and have the cursor in the right place is TOO
510 ## FUCKING COMPLICATED.
512 tf.activate question, default, &block
514 draw_screen :skip_minibuf => true, :sync => false
520 c = Ncurses.nonblocking_getch
521 next unless c # getch timeout
522 break unless tf.handle_input c # process keystroke
524 if tf.new_completions?
525 kill_buffer completion_buf if completion_buf
527 shorts = tf.completions.map { |full, short| short }
528 prefix_len = shorts.shared_prefix.length
530 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
531 completion_buf = spawn "<completions>", mode, :height => 10
533 draw_screen :skip_minibuf => true
535 elsif tf.roll_completions?
536 completion_buf.mode.roll
537 draw_screen :skip_minibuf => true
541 Ncurses.sync { Ncurses.refresh }
544 Ncurses.sync { tf.deactivate }
545 kill_buffer completion_buf if completion_buf
552 ## some pretty lame code in here!
553 def ask_getch question, accept=nil
554 accept = accept.split(//).map { |x| x[0] } if accept
559 Ncurses.move Ncurses.rows - 1, question.length + 1
567 key = Ncurses.nonblocking_getch or next
568 if key == Ncurses::KEY_CANCEL
570 elsif (accept && accept.member?(key)) || !accept
581 draw_screen :sync => false
588 ## returns true (y), false (n), or nil (ctrl-g / cancel)
589 def ask_yes_or_no question
590 case(r = ask_getch question, "ynYN")
601 @minibuf_mutex.synchronize do
604 @minibuf_stack.compact.size, 1].max
608 def draw_minibuf opts={}
610 @minibuf_mutex.synchronize do
611 m = @minibuf_stack.compact
612 m << @flash if @flash
616 Ncurses.mutex.lock unless opts[:sync] == false
617 Ncurses.attrset Colormap.color_for(:none)
618 adj = @asking ? 2 : 1
619 m.each_with_index do |s, i|
620 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
622 Ncurses.refresh if opts[:refresh]
623 Ncurses.mutex.unlock unless opts[:sync] == false
629 @minibuf_mutex.synchronize do
631 id ||= @minibuf_stack.length
632 @minibuf_stack[id] = s
636 draw_screen :refresh => true
638 draw_minibuf :refresh => true
651 def erase_flash; @flash = nil; end
655 draw_screen :refresh => true
658 ## a little tricky because we can't just delete_at id because ids
659 ## are relative (they're positions into the array).
661 @minibuf_mutex.synchronize do
662 @minibuf_stack[id] = nil
663 if id == @minibuf_stack.length - 1
665 break if @minibuf_stack[i]
666 @minibuf_stack.delete_at i
671 draw_screen :refresh => true
674 def shell_out command
686 def default_status_bar buf
687 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
690 def default_terminal_title buf
691 "Sup #{Redwood::VERSION} :: #{buf.title}"
694 def get_status_and_title buf
696 :num_inbox => lambda { Index.num_results_for :label => :inbox },
697 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
698 :num_total => lambda { Index.size },
699 :num_spam => lambda { Index.num_results_for :label => :spam },
701 :mode => buf.mode.name,
702 :status => buf.mode.status
705 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
706 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
708 [statusbar_text, term_title_text]
714 while(u = Etc.getpwent)