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
54 def initialize window, mode, width, height, opts={}
59 @title = opts[:title] || ""
60 @x, @y, @width, @height = 0, 0, width, height
63 def content_height; @height - 1; end
64 def content_width; @width; end
67 return if cols == @width && rows == @height
71 mode.resize rows, cols
80 def mark_dirty; @dirty = true; end
93 ## s nil means a blank line!
94 def write y, x, s, opts={}
95 return if x >= @width || y >= @height
97 @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
100 @w.mvaddstr y, x, s[0 ... maxl]
101 unless s.length >= maxl || opts[:no_fill]
102 @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
111 write @height - 1, 0, " [#{mode.name}] #{title} #{mode.status}",
112 :color => :status_color
131 attr_reader :focus_buf
139 @minibuf_mutex = Mutex.new
144 self.class.i_am_the_instance self
147 def buffers; @name_map.to_a; end
150 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
151 return if buf == @focus_buf
152 @focus_buf.blur if @focus_buf
157 def raise_to_front buf
158 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
166 raise_to_front @buffers.first
169 def roll_buffers_backwards
170 return unless @buffers.length > 1
171 raise_to_front @buffers[@buffers.length - 2]
175 @focus_buf && @focus_buf.mode.handle_input(c)
178 def exists? n; @name_map.member? n; end
179 def [] n; @name_map[n]; end
181 raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
185 def completely_redraw_screen
191 draw_screen :sync => false
197 rows, cols = Ncurses.rows, Ncurses.cols
198 @buffers.each { |b| b.resize rows - minibuf_lines, cols }
199 completely_redraw_screen
200 flash "Resized to #{rows}x#{cols}"
203 def draw_screen opts={}
206 Ncurses.mutex.lock unless opts[:sync] == false
208 ## disabling this for the time being, to help with debugging
209 ## (currently we only have one buffer visible at a time).
210 ## TODO: reenable this if we allow multiple buffers
211 false && @buffers.inject(@dirty) do |dirty, buf|
212 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
213 @dirty ? buf.draw : buf.redraw
219 buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
220 @dirty ? buf.draw : buf.redraw
223 draw_minibuf :sync => false unless opts[:skip_minibuf]
226 Ncurses.refresh if opts[:refresh]
227 Ncurses.mutex.unlock unless opts[:sync] == false
230 ## gets the mode from the block, which is only called if the buffer
231 ## doesn't already exist. this is useful in the case that generating
232 ## the mode is expensive, as it often is.
233 def spawn_unless_exists title, opts={}
234 if @name_map.member? title
235 raise_to_front @name_map[title] unless opts[:hidden]
238 spawn title, mode, opts
243 def spawn title, mode, opts={}
246 while @name_map.member? realtitle
247 realtitle = "#{title} <#{num}>"
251 width = opts[:width] || Ncurses.cols
252 height = opts[:height] || Ncurses.rows - 1
254 ## since we are currently only doing multiple full-screen modes,
255 ## use stdscr for each window. once we become more sophisticated,
256 ## we may need to use a new Ncurses::WINDOW
258 ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
259 ## (opts[:left] || 0))
261 b = Buffer.new w, mode, width, height, :title => realtitle
263 @name_map[realtitle] = b
266 focus_on b unless @focus_buf
275 kill_buffer @buffers.first until @buffers.empty?
279 raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
283 @name_map.delete buf.title
284 @focus_buf = nil if @focus_buf == buf
286 ## TODO: something intelligent here
287 ## for now I will simply prohibit killing the inbox buffer.
289 raise_to_front @buffers.last
293 ## not really thread safe.
294 def ask domain, question, default=nil
295 raise "impossible!" if @asking
297 @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
298 tf = @textfields[domain]
300 ## this goddamn ncurses form shit is a fucking 1970's
301 ## nightmare. jesus christ. the exact sequence of ncurses events
302 ## that needs to happen in order to display a form and have the
303 ## entire screen not disappear and have the cursor in the right
304 ## place is TOO FUCKING COMPLICATED.
306 tf.activate question, default
308 draw_screen :skip_minibuf => true, :sync => false
313 Ncurses.sync { Ncurses.refresh }
316 while tf.handle_input(Ncurses.nonblocking_getch); end
320 Ncurses.sync { tf.deactivate }
326 ## some pretty lame code in here!
327 def ask_getch question, accept=nil
328 accept = accept.split(//).map { |x| x[0] } if accept
333 Ncurses.move Ncurses.rows - 1, question.length + 1
341 key = Ncurses.nonblocking_getch
342 if key == Ncurses::KEY_CANCEL
344 elsif (accept && accept.member?(key)) || !accept
355 draw_screen :sync => false
362 def ask_yes_or_no question
363 r = ask_getch(question, "ynYN")
375 @minibuf_mutex.synchronize do
378 @minibuf_stack.compact.size, 1].max
382 def draw_minibuf opts={}
384 @minibuf_mutex.synchronize do
385 m = @minibuf_stack.compact
386 m << @flash if @flash
390 Ncurses.mutex.lock unless opts[:sync] == false
391 Ncurses.attrset Colormap.color_for(:none)
392 adj = @asking ? 2 : 1
393 m.each_with_index do |s, i|
394 Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
396 Ncurses.refresh if opts[:refresh]
397 Ncurses.mutex.unlock unless opts[:sync] == false
402 @minibuf_mutex.synchronize do
404 id ||= @minibuf_stack.length
405 @minibuf_stack[id] = s
409 draw_screen :refresh => true
411 draw_minibuf :refresh => true
424 def erase_flash; @flash = nil; end
428 draw_screen :refresh => true
431 ## a little tricky because we can't just delete_at id because ids
432 ## are relative (they're positions into the array).
434 @minibuf_mutex.synchronize do
435 @minibuf_stack[id] = nil
436 if id == @minibuf_stack.length - 1
438 break if @minibuf_stack[i]
439 @minibuf_stack.delete_at i
444 draw_screen :refresh => true
447 def shell_out command