6 stdscr.getmaxyx lame, lamer
12 stdscr.getmaxyx lame, lamer
16 def mutex; @mutex ||= Mutex.new; end
17 def sync &b; mutex.synchronize(&b); end
19 ## aaahhh, user input. who would have though that such a simple
20 ## idea would be SO FUCKING COMPLICATED?! because apparently
21 ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
22 ## ALL THREAD ACTIVITY. as in, no threads anywhere will run while
23 ## it's waiting for input. ok, fine, so we wrap it in a select. Of
24 ## course we also rely on Ncurses.getch to tell us when an xterm
25 ## resize has occurred, which select won't catch, so we won't
26 ## resize outselves after a sigwinch until the user hits a key.
27 ## and installing our own sigwinch handler means that the screen
28 ## size returned by getmaxyx() DOESN'T UPDATE! and Kernel#trap
29 ## RETURNS NIL as the previous handler!
31 ## so basically, resizing with multi-threaded ruby Ncurses
32 ## applications will always be broken.
34 ## i've coined a new word for this: lametarded.
36 if IO.select([$stdin], nil, nil, nil)
43 module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
45 KEY_CANCEL = "\a"[0] # ctrl-g
51 attr_reader :mode, :x, :y, :width, :height, :title
53 bool_accessor :force_to_top
55 def initialize window, mode, width, height, opts={}
60 @title = opts[:title] || ""
61 @force_to_top = opts[:force_to_top] || false
62 @x, @y, @width, @height = 0, 0, width, height
65 def content_height; @height - 1; end
66 def content_width; @width; end
69 return if cols == @width && rows == @height
73 mode.resize rows, cols
82 def mark_dirty; @dirty = true; end
95 ## s nil means a blank line!
96 def write y, x, s, opts={}
97 return if x >= @width || y >= @height
99 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
102 @w.mvaddstr y, x, s[0 ... maxl]
103 unless s.length >= maxl || opts[:no_fill]
104 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
113 write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
114 :color => :status_color
133 attr_reader :focus_buf
141 @minibuf_mutex = Mutex.new
144 @shelled = @asking = false
146 self.class.i_am_the_instance self
149 def buffers; @name_map.to_a; end
152 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
153 return if buf == @focus_buf
154 @focus_buf.blur if @focus_buf
159 def raise_to_front buf
160 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
163 if @buffers.length > 0 && @buffers.last.force_to_top?
164 @buffers.insert(-2, buf)
172 ## we reset force_to_top when rolling buffers. this is so that the
173 ## human can actually still move buffers around, while still
174 ## programmatically being able to pop stuff up in the middle of
175 ## drawing a window without worrying about covering it up.
177 ## if we ever start calling roll_buffers programmatically, we will
178 ## have to change this. but it's not clear that we will ever actually
181 @buffers.last.force_to_top = false
182 raise_to_front @buffers.first
185 def roll_buffers_backwards
186 return unless @buffers.length > 1
187 @buffers.last.force_to_top = false
188 raise_to_front @buffers[@buffers.length - 2]
192 @focus_buf && @focus_buf.mode.handle_input(c)
195 def exists? n; @name_map.member? n; end
196 def [] n; @name_map[n]; end
198 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
202 def completely_redraw_screen
208 draw_screen :sync => false
214 rows, cols = Ncurses.rows, Ncurses.cols
215 @buffers.each { |b| b.resize rows - minibuf_lines, cols }
216 completely_redraw_screen
217 flash "Resized to #{rows}x#{cols}"
220 def draw_screen opts={}
223 Ncurses.mutex.lock unless opts[:sync] == false
225 ## disabling this for the time being, to help with debugging
226 ## (currently we only have one buffer visible at a time).
227 ## TODO: reenable this if we allow multiple buffers
228 false && @buffers.inject(@dirty) do |dirty, buf|
229 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
230 @dirty ? buf.draw : buf.redraw
236 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
237 @dirty ? buf.draw : buf.redraw
240 draw_minibuf :sync => false unless opts[:skip_minibuf]
243 Ncurses.refresh if opts[:refresh]
244 Ncurses.mutex.unlock unless opts[:sync] == false
247 ## gets the mode from the block, which is only called if the buffer
248 ## doesn't already exist. this is useful in the case that generating
249 ## the mode is expensive, as it often is.
250 def spawn_unless_exists title, opts={}
251 if @name_map.member? title
252 raise_to_front @name_map[title] unless opts[:hidden]
255 spawn title, mode, opts
260 def spawn title, mode, opts={}
263 while @name_map.member? realtitle
264 realtitle = "#{title} <#{num}>"
268 width = opts[:width] || Ncurses.cols
269 height = opts[:height] || Ncurses.rows - 1
271 ## since we are currently only doing multiple full-screen modes,
272 ## use stdscr for each window. once we become more sophisticated,
273 ## we may need to use a new Ncurses::WINDOW
275 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
276 ## (opts[:left] || 0))
278 b = Buffer.new w, mode, width, height, :title => realtitle, :force_to_top => (opts[:force_to_top] || false)
280 @name_map[realtitle] = b
284 focus_on b unless @focus_buf
291 def kill_all_buffers_safely
292 until @buffers.empty?
293 ## inbox mode always claims it's unkillable. we'll ignore it.
294 return false unless @buffers.first.mode.is_a?(InboxMode) || @buffers.first.mode.killable?
295 kill_buffer @buffers.first
300 def kill_buffer_safely buf
301 return false unless buf.mode.killable?
307 kill_buffer @buffers.first until @buffers.empty?
311 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
315 @name_map.delete buf.title
316 @focus_buf = nil if @focus_buf == buf
318 ## TODO: something intelligent here
319 ## for now I will simply prohibit killing the inbox buffer.
321 raise_to_front @buffers.last
325 ## not really thread safe.
326 def ask domain, question, default=nil
327 raise "impossible!" if @asking
329 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
330 tf = @textfields[domain]
332 ## this goddamn ncurses form shit is a fucking 1970's
333 ## nightmare. jesus christ. the exact sequence of ncurses events
334 ## that needs to happen in order to display a form and have the
335 ## entire screen not disappear and have the cursor in the right
336 ## place is TOO FUCKING COMPLICATED.
338 tf.activate question, default
340 draw_screen :skip_minibuf => true, :sync => false
345 Ncurses.sync { Ncurses.refresh }
348 while tf.handle_input(Ncurses.nonblocking_getch); end
352 Ncurses.sync { tf.deactivate }
358 ## some pretty lame code in here!
359 def ask_getch question, accept=nil
360 accept = accept.split(//).map { |x| x[0] } if accept
365 Ncurses.move Ncurses.rows - 1, question.length + 1
373 key = Ncurses.nonblocking_getch
374 if key == Ncurses::KEY_CANCEL
376 elsif (accept && accept.member?(key)) || !accept
387 draw_screen :sync => false
394 ## returns true (y), false (n), or nil (ctrl-g / cancel)
395 def ask_yes_or_no question
396 case(r = ask_getch question, "ynYN")
407 @minibuf_mutex.synchronize do
410 @minibuf_stack.compact.size, 1].max
414 def draw_minibuf opts={}
416 @minibuf_mutex.synchronize do
417 m = @minibuf_stack.compact
418 m << @flash if @flash
422 Ncurses.mutex.lock unless opts[:sync] == false
423 Ncurses.attrset Colormap.color_for(:none)
424 adj = @asking ? 2 : 1
425 m.each_with_index do |s, i|
426 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
428 Ncurses.refresh if opts[:refresh]
429 Ncurses.mutex.unlock unless opts[:sync] == false
435 @minibuf_mutex.synchronize do
437 id ||= @minibuf_stack.length
438 @minibuf_stack[id] = s
442 draw_screen :refresh => true
444 draw_minibuf :refresh => true
457 def erase_flash; @flash = nil; end
461 draw_screen :refresh => true
464 ## a little tricky because we can't just delete_at id because ids
465 ## are relative (they're positions into the array).
467 @minibuf_mutex.synchronize do
468 @minibuf_stack[id] = nil
469 if id == @minibuf_stack.length - 1
471 break if @minibuf_stack[i]
472 @minibuf_stack.delete_at i
477 draw_screen :refresh => true
480 def shell_out command