]> git.cworth.org Git - sup/blob - lib/sup/buffer.rb
fixed asking so that it doesn't interfere with the rest of the minibuf
[sup] / lib / sup / buffer.rb
1 require 'thread'
2
3 module Ncurses
4   def rows
5     lame, lamer = [], []
6     stdscr.getmaxyx lame, lamer
7     lame.first
8   end
9
10   def cols
11     lame, lamer = [], []
12     stdscr.getmaxyx lame, lamer
13     lamer.first
14   end
15
16   def mutex; @mutex ||= Mutex.new; end
17   def sync &b; mutex.synchronize &b; end
18
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! 
30   ##
31   ## so basically, resizing with multi-threaded ruby Ncurses
32   ## applications will always be broken.
33   ##
34   ## i've coined a new word for this: lametarded.
35   def nonblocking_getch
36     if IO.select([$stdin], nil, nil, nil)
37       Ncurses.getch
38     else
39       nil
40     end
41   end
42
43   module_function :rows, :cols, :nonblocking_getch, :mutex, :sync
44
45   KEY_CANCEL = "\a"[0] # ctrl-g
46 end
47
48 module Redwood
49
50 class Buffer
51   attr_reader :mode, :x, :y, :width, :height, :title
52   bool_reader :dirty
53
54   def initialize window, mode, width, height, opts={}
55     @w = window
56     @mode = mode
57     @dirty = true
58     @focus = false
59     @title = opts[:title] || ""
60     @x, @y, @width, @height = 0, 0, width, height
61   end
62
63   def content_height; @height - 1; end
64   def content_width; @width; end
65
66   def resize rows, cols 
67     return if cols == @width && rows == @height
68     @width = cols
69     @height = rows
70     @dirty = true
71     mode.resize rows, cols
72   end
73
74   def redraw
75     draw if @dirty
76     draw_status
77     commit
78   end
79
80   def mark_dirty; @dirty = true; end
81
82   def commit
83     @dirty = false
84     @w.noutrefresh
85   end
86
87   def draw
88     @mode.draw
89     draw_status
90     commit
91   end
92
93   ## s nil means a blank line!
94   def write y, x, s, opts={}
95     return if x >= @width || y >= @height
96
97     @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
98     s ||= ""
99     maxl = @width - x
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))
103     end
104   end
105
106   def clear
107     @w.clear
108   end
109
110   def draw_status
111     write @height - 1, 0, " [#{mode.name}] #{title}   #{mode.status}",
112       :color => :status_color
113   end
114
115   def focus
116     @focus = true
117     @dirty = true
118     @mode.focus
119   end
120
121   def blur
122     @focus = false
123     @dirty = true
124     @mode.blur
125   end
126 end
127
128 class BufferManager
129   include Singleton
130
131   attr_reader :focus_buf
132
133   def initialize
134     @name_map = {}
135     @buffers = []
136     @focus_buf = nil
137     @dirty = true
138     @minibuf_stack = []
139     @minibuf_mutex = Mutex.new
140     @textfields = {}
141     @flash = nil
142     @shelled = false
143
144     self.class.i_am_the_instance self
145   end
146
147   def buffers; @name_map.to_a; end
148
149   def focus_on buf
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
153     @focus_buf = buf
154     @focus_buf.focus
155   end
156
157   def raise_to_front buf
158     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
159     @buffers.delete buf
160     @buffers.push buf
161     focus_on buf
162     @dirty = true
163   end
164
165   def roll_buffers
166     raise_to_front @buffers.first
167   end
168
169   def roll_buffers_backwards
170     return unless @buffers.length > 1
171     raise_to_front @buffers[@buffers.length - 2]
172   end
173
174   def handle_input c
175     @focus_buf && @focus_buf.mode.handle_input(c)
176   end
177
178   def exists? n; @name_map.member? n; end
179   def [] n; @name_map[n]; end
180   def []= n, b
181     raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
182     @name_map[n] = b
183   end
184
185   def completely_redraw_screen
186     return if @shelled
187
188     Ncurses.sync do
189       @dirty = true
190       Ncurses.clear
191       draw_screen :sync => false
192     end
193   end
194
195   def handle_resize
196     return if @shelled
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}"
201   end
202
203   def draw_screen opts={}
204     return if @shelled
205
206     Ncurses.mutex.lock unless opts[:sync] == false
207
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
214     end
215
216     ## quick hack
217     if true
218       buf = @buffers.last
219       buf.resize Ncurses.rows - minibuf_lines, Ncurses.cols
220       @dirty ? buf.draw : buf.redraw
221     end
222
223     draw_minibuf :sync => false unless opts[:skip_minibuf]
224     @dirty = false
225     Ncurses.doupdate
226     Ncurses.refresh if opts[:refresh]
227     Ncurses.mutex.unlock unless opts[:sync] == false
228   end
229
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       Redwood::log "buffer '#{title}' already exists, raising to front"
236       raise_to_front @name_map[title] unless opts[:hidden]
237     else
238       mode = yield
239       spawn title, mode, opts
240     end
241     @name_map[title]
242   end
243
244   def spawn title, mode, opts={}
245     realtitle = title
246     num = 2
247     while @name_map.member? realtitle
248       realtitle = "#{title} <#{num}>"
249       num += 1
250     end
251
252     width = opts[:width] || Ncurses.cols
253     height = opts[:height] || Ncurses.rows - 1
254
255     ## since we are currently only doing multiple full-screen modes,
256     ## use stdscr for each window. once we become more sophisticated,
257     ## we may need to use a new Ncurses::WINDOW
258     ##
259     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
260     ## (opts[:left] || 0))
261     w = Ncurses.stdscr
262     b = Buffer.new w, mode, width, height, :title => realtitle
263     mode.buffer = b
264     @name_map[realtitle] = b
265     if opts[:hidden]
266       @buffers.unshift b
267       focus_on b unless @focus_buf
268     else
269       @buffers.push b
270       raise_to_front b
271     end
272     b
273   end
274
275   def kill_all_buffers
276     kill_buffer @buffers.first until @buffers.empty?
277   end
278
279   def kill_buffer buf
280     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
281
282     buf.mode.cleanup
283     @buffers.delete buf
284     @name_map.delete buf.title
285     @focus_buf = nil if @focus_buf == buf
286     if @buffers.empty?
287       ## TODO: something intelligent here
288       ## for now I will simply prohibit killing the inbox buffer.
289     else
290       raise_to_front @buffers.last
291     end
292   end
293
294   ## not really thread safe.
295   def ask domain, question, default=nil
296     raise "impossible!" if @asking
297
298     @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0, Ncurses.cols
299     tf = @textfields[domain]
300
301     ## this goddamn ncurses form shit is a fucking 1970's
302     ## nightmare. jesus christ. the exact sequence of ncurses events
303     ## that needs to happen in order to display a form and have the
304     ## entire screen not disappear and have the cursor in the right
305     ## place is TOO FUCKING COMPLICATED.
306     Ncurses.sync do
307       tf.activate question, default
308       @dirty = true
309       draw_screen :skip_minibuf => true, :sync => false
310     end
311
312     ret = nil
313     tf.position_cursor
314     Ncurses.sync { Ncurses.refresh }
315
316     @asking = true
317     while tf.handle_input(Ncurses.nonblocking_getch); end
318     @asking = false
319
320     ret = tf.value
321     Ncurses.sync { tf.deactivate }
322     @dirty = true
323
324     ret
325   end
326
327   ## some pretty lame code in here!
328   def ask_getch question, accept=nil
329     accept = accept.split(//).map { |x| x[0] } if accept
330
331     flash question
332     Ncurses.sync do
333       Ncurses.curs_set 1
334       Ncurses.move Ncurses.rows - 1, question.length + 1
335       Ncurses.refresh
336     end
337
338     ret = nil
339     done = false
340     @shelled = true
341     until done
342       key = Ncurses.nonblocking_getch
343       if key == Ncurses::KEY_CANCEL
344         done = true
345       elsif (accept && accept.member?(key)) || !accept
346         ret = key
347         done = true
348       end
349     end
350
351     @shelled = false
352
353     Ncurses.sync do
354       Ncurses.curs_set 0
355       erase_flash
356       draw_screen :sync => false
357       Ncurses.curs_set 0
358     end
359
360     ret
361   end
362
363   def ask_yes_or_no question
364     r = ask_getch(question, "ynYN")
365     case r
366     when ?y, ?Y
367       true
368     when nil
369       nil
370     else
371       false
372     end
373   end
374
375   def minibuf_lines
376     @minibuf_mutex.synchronize do
377       [(@flash ? 1 : 0) + 
378        (@asking ? 1 : 0) +
379        @minibuf_stack.compact.size, 1].max
380     end
381   end
382   
383   def draw_minibuf opts={}
384     m = nil
385     @minibuf_mutex.synchronize do
386       m = @minibuf_stack.compact
387       m << @flash if @flash
388       m << "" if m.empty?
389     end
390
391     Ncurses.mutex.lock unless opts[:sync] == false
392     Ncurses.attrset Colormap.color_for(:none)
393     adj = @asking ? 2 : 1
394     m.each_with_index do |s, i|
395       Ncurses.mvaddstr Ncurses.rows - i - adj, 0, s + (" " * [Ncurses.cols - s.length, 0].max)
396     end
397     Ncurses.refresh if opts[:refresh]
398     Ncurses.mutex.unlock unless opts[:sync] == false
399   end
400
401   def say s, id=nil
402     new_id = nil
403     @minibuf_mutex.synchronize do
404       new_id = id.nil?
405       id ||= @minibuf_stack.length
406       @minibuf_stack[id] = s
407     end
408
409     if new_id
410       draw_screen :refresh => true
411     else
412       draw_minibuf :refresh => true
413     end
414
415     if block_given?
416       begin
417         yield id
418       ensure
419         clear id
420       end
421     end
422     id
423   end
424
425   def erase_flash; @flash = nil; end
426
427   def flash s
428     @flash = s
429     draw_screen :refresh => true
430   end
431
432   ## a little tricky because we can't just delete_at id because ids
433   ## are relative (they're positions into the array).
434   def clear id
435     @minibuf_mutex.synchronize do
436       @minibuf_stack[id] = nil
437       if id == @minibuf_stack.length - 1
438         id.downto(0) do |i|
439           break if @minibuf_stack[i]
440           @minibuf_stack.delete_at i
441         end
442       end
443     end
444
445     draw_screen :refresh => true
446   end
447
448   def shell_out command
449     @shelled = true
450     Ncurses.sync do
451       Ncurses.endwin
452       system command
453       Ncurses.refresh
454       Ncurses.curs_set 0
455     end
456     @shelled = false
457   end
458 end
459 end