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
187 return if buf == @focus_buf
188 @focus_buf.blur if @focus_buf
193 def raise_to_front buf
194 @buffers.delete(buf) or return
195 if @buffers.length > 0 && @buffers.last.force_to_top?
196 @buffers.insert(-2, buf)
204 ## we reset force_to_top when rolling buffers. this is so that the
205 ## human can actually still move buffers around, while still
206 ## programmatically being able to pop stuff up in the middle of
207 ## drawing a window without worrying about covering it up.
209 ## if we ever start calling roll_buffers programmatically, we will
210 ## have to change this. but it's not clear that we will ever actually
213 @buffers.last.force_to_top = false
214 raise_to_front @buffers.first
217 def roll_buffers_backwards
218 return unless @buffers.length > 1
219 @buffers.last.force_to_top = false
220 raise_to_front @buffers[@buffers.length - 2]
225 if @focus_buf.mode.in_search? && c != CONTINUE_IN_BUFFER_SEARCH_KEY[0]
226 @focus_buf.mode.cancel_search!
227 @focus_buf.mark_dirty
229 @focus_buf.mode.handle_input c
233 def exists? n; @name_map.member? n; end
234 def [] n; @name_map[n]; end
236 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
237 raise ArgumentError, "title must be a string" unless n.is_a? String
241 def completely_redraw_screen
244 status, title = get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
249 draw_screen :sync => false, :status => status, :title => title
253 def draw_screen opts={}
257 if opts.member? :status
258 [opts[:status], opts[:title]]
260 get_status_and_title(@focus_buf) # must be called outside of the ncurses lock
263 print "\033]2;#{title}\07" if title
265 Ncurses.mutex.lock unless opts[:sync] == false
267 ## disabling this for the time being, to help with debugging
268 ## (currently we only have one buffer visible at a time).
269 ## TODO: reenable this if we allow multiple buffers
270 false && @buffers.inject(@dirty) do |dirty, buf|
271 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
272 #dirty ? buf.draw : buf.redraw
280 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
281 @dirty ? buf.draw(status) : buf.redraw(status)
284 draw_minibuf :sync => false unless opts[:skip_minibuf]
288 Ncurses.refresh if opts[:refresh]
289 Ncurses.mutex.unlock unless opts[:sync] == false
292 ## if the named buffer already exists, pops it to the front without
293 ## calling the block. otherwise, gets the mode from the block and
294 ## creates a new buffer. returns two things: the buffer, and a boolean
295 ## indicating whether it's a new buffer or not.
296 def spawn_unless_exists title, opts={}
298 if @name_map.member? title
299 raise_to_front @name_map[title] unless opts[:hidden]
303 spawn title, mode, opts
306 [@name_map[title], new]
309 def spawn title, mode, opts={}
310 raise ArgumentError, "title must be a string" unless title.is_a? String
313 while @name_map.member? realtitle
314 realtitle = "#{title} <#{num}>"
318 width = opts[:width] || Ncurses.cols
319 height = opts[:height] || Ncurses.rows - 1
321 ## since we are currently only doing multiple full-screen modes,
322 ## use stdscr for each window. once we become more sophisticated,
323 ## we may need to use a new Ncurses::WINDOW
325 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
326 ## (opts[:left] || 0))
328 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
330 @name_map[realtitle] = b
334 focus_on b unless @focus_buf
341 ## requires the mode to have #done? and #value methods
342 def spawn_modal title, mode, opts={}
343 b = spawn title, mode, opts
347 c = Ncurses.nonblocking_getch
348 next unless c # getch timeout
349 break if c == Ncurses::KEY_CANCEL
359 def kill_all_buffers_safely
360 until @buffers.empty?
361 ## inbox mode always claims it's unkillable. we'll ignore it.
362 return false unless @buffers.last.mode.is_a?(InboxMode) || @buffers.last.mode.killable?
363 kill_buffer @buffers.last
368 def kill_buffer_safely buf
369 return false unless buf.mode.killable?
375 kill_buffer @buffers.first until @buffers.empty?
379 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
383 @name_map.delete buf.title
384 @focus_buf = nil if @focus_buf == buf
386 ## TODO: something intelligent here
387 ## for now I will simply prohibit killing the inbox buffer.
389 raise_to_front @buffers.last
393 def ask_with_completions domain, question, completions, default=nil
394 ask domain, question, default do |s|
395 completions.select { |x| x =~ /^#{s}/i }.map { |x| [x, x] }
399 def ask_many_with_completions domain, question, completions, default=nil
400 ask domain, question, default do |partial|
405 when /^(.*\s+)?(.*?)$/
408 raise "william screwed up completion: #{partial.inspect}"
411 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
415 def ask_many_emails_with_completions domain, question, completions, default=nil
416 ask domain, question, default do |partial|
417 prefix, target = partial.split_on_commas_with_remainder
418 Redwood::log "before: prefix #{prefix.inspect}, target #{target.inspect}"
419 target ||= prefix.pop || ""
420 prefix = prefix.join(", ") + (prefix.empty? ? "" : ", ")
421 Redwood::log "after: prefix #{prefix.inspect}, target #{target.inspect}"
422 completions.select { |x| x =~ /^#{target}/i }.map { |x| [prefix + x, x] }
426 def ask_for_filename domain, question, default=nil
427 answer = ask domain, question, default do |s|
428 if s =~ /(~([^\s\/]*))/ # twiddle directory expansion
430 name = $2.empty? ? Etc.getlogin : $2
431 dir = Etc.getpwnam(name).dir rescue nil
433 [[s.sub(full, dir), "~#{name}"]]
435 users.select { |u| u =~ /^#{name}/ }.map do |u|
436 [s.sub("~#{name}", "~#{u}"), "~#{u}"]
439 else # regular filename completion
440 Dir["#{s}*"].sort.map do |fn|
441 suffix = File.directory?(fn) ? "/" : ""
442 [fn + suffix, File.basename(fn) + suffix]
450 spawn_modal "file browser", FileBrowserMode.new
451 elsif File.directory?(answer)
452 spawn_modal "file browser", FileBrowserMode.new(answer)
461 ## returns an array of labels
462 def ask_for_labels domain, question, default_labels, forbidden_labels=[]
463 default_labels = default_labels - forbidden_labels - LabelManager::RESERVED_LABELS
464 default = default_labels.join(" ")
465 default += " " unless default.empty?
467 applyable_labels = (LabelManager.applyable_labels - forbidden_labels).map { |l| LabelManager.string_for l }.sort_by { |s| s.downcase }
469 answer = ask_many_with_completions domain, question, applyable_labels, default
473 user_labels = answer.split(/\s+/).map { |l| l.intern }
474 user_labels.each do |l|
475 if forbidden_labels.include?(l) || LabelManager::RESERVED_LABELS.include?(l)
476 BufferManager.flash "'#{l}' is a reserved label!"
483 def ask_for_contacts domain, question, default_contacts=[]
484 default = default_contacts.map { |s| s.to_s }.join(" ")
485 default += " " unless default.empty?
487 recent = Index.load_contacts(AccountManager.user_emails, :num => 10).map { |c| [c.full_address, c.email] }
488 contacts = ContactManager.contacts.map { |c| [ContactManager.alias_for(c), c.full_address, c.email] }
490 completions = (recent + contacts).flatten.uniq.sort
491 answer = BufferManager.ask_many_emails_with_completions domain, question, completions, default
494 answer.split_on_commas.map { |x| ContactManager.contact_for(x.downcase) || PersonManager.person_for(x) }
499 def ask domain, question, default=nil, &block
500 raise "impossible!" if @asking
503 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
504 tf = @textfields[domain]
507 ## this goddamn ncurses form shit is a fucking 1970's nightmare.
508 ## jesus christ. the exact sequence of ncurses events that needs
509 ## to happen in order to display a form and have the entire screen
510 ## not disappear and have the cursor in the right place is TOO
511 ## FUCKING COMPLICATED.
513 tf.activate question, default, &block
515 draw_screen :skip_minibuf => true, :sync => false
521 c = Ncurses.nonblocking_getch
522 next unless c # getch timeout
523 break unless tf.handle_input c # process keystroke
525 if tf.new_completions?
526 kill_buffer completion_buf if completion_buf
528 shorts = tf.completions.map { |full, short| short }
529 prefix_len = shorts.shared_prefix.length
531 mode = CompletionMode.new shorts, :header => "Possible completions for \"#{tf.value}\": ", :prefix_len => prefix_len
532 completion_buf = spawn "<completions>", mode, :height => 10
534 draw_screen :skip_minibuf => true
536 elsif tf.roll_completions?
537 completion_buf.mode.roll
538 draw_screen :skip_minibuf => true
542 Ncurses.sync { Ncurses.refresh }
545 Ncurses.sync { tf.deactivate }
546 kill_buffer completion_buf if completion_buf
553 ## some pretty lame code in here!
554 def ask_getch question, accept=nil
555 accept = accept.split(//).map { |x| x[0] } if accept
560 Ncurses.move Ncurses.rows - 1, question.length + 1
568 key = Ncurses.nonblocking_getch or next
569 if key == Ncurses::KEY_CANCEL
571 elsif (accept && accept.member?(key)) || !accept
582 draw_screen :sync => false
589 ## returns true (y), false (n), or nil (ctrl-g / cancel)
590 def ask_yes_or_no question
591 case(r = ask_getch question, "ynYN")
602 @minibuf_mutex.synchronize do
605 @minibuf_stack.compact.size, 1].max
609 def draw_minibuf opts={}
611 @minibuf_mutex.synchronize do
612 m = @minibuf_stack.compact
613 m << @flash if @flash
617 Ncurses.mutex.lock unless opts[:sync] == false
618 Ncurses.attrset Colormap.color_for(:none)
619 adj = @asking ? 2 : 1
620 m.each_with_index do |s, i|
621 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
623 Ncurses.refresh if opts[:refresh]
624 Ncurses.mutex.unlock unless opts[:sync] == false
630 @minibuf_mutex.synchronize do
632 id ||= @minibuf_stack.length
633 @minibuf_stack[id] = s
637 draw_screen :refresh => true
639 draw_minibuf :refresh => true
652 def erase_flash; @flash = nil; end
656 draw_screen :refresh => true
659 ## a little tricky because we can't just delete_at id because ids
660 ## are relative (they're positions into the array).
662 @minibuf_mutex.synchronize do
663 @minibuf_stack[id] = nil
664 if id == @minibuf_stack.length - 1
666 break if @minibuf_stack[i]
667 @minibuf_stack.delete_at i
672 draw_screen :refresh => true
675 def shell_out command
687 def default_status_bar buf
688 " [#{buf.mode.name}] #{buf.title} #{buf.mode.status}"
691 def default_terminal_title buf
692 "Sup #{Redwood::VERSION} :: #{buf.title}"
695 def get_status_and_title buf
697 :num_inbox => lambda { Index.num_results_for :label => :inbox },
698 :num_inbox_unread => lambda { Index.num_results_for :labels => [:inbox, :unread] },
699 :num_total => lambda { Index.size },
700 :num_spam => lambda { Index.num_results_for :label => :spam },
702 :mode => buf.mode.name,
703 :status => buf.mode.status
706 statusbar_text = HookManager.run("status-bar-text", opts) || default_status_bar(buf)
707 term_title_text = HookManager.run("terminal-title-text", opts) || default_terminal_title(buf)
709 [statusbar_text, term_title_text]
715 while(u = Etc.getpwent)