]> git.cworth.org Git - sup/blob - lib/sup/buffer.rb
03aead6d801af25bf2b9717b0990688667b60931
[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   ## aaahhh, user input. who would have though that such a simple
17   ## idea would be SO FUCKING COMPLICATED?! because apparently
18   ## Ncurses.getch (and Curses.getch), even in cbreak mode, BLOCKS
19   ## ALL THREAD ACTIVITY. as in, no threads anywhere will run while
20   ## it's waiting for input. ok, fine, so we wrap it in a select. Of
21   ## course we also rely on Ncurses.getch to tell us when an xterm
22   ## resize has occurred, which select won't catch, so we won't
23   ## resize outselves after a sigwinch until the user hits a key.
24   ## and installing our own sigwinch handler means that the screen
25   ## size returned by getmaxyx() DOESN'T UPDATE! and Kernel#trap
26   ## RETURNS NIL as the previous handler! 
27   ##
28   ## so basically, resizing with multi-threaded ruby Ncurses
29   ## applications will always be broken.
30   ##
31   ## i've coined a new word for this: lametarded.
32   def nonblocking_getch
33     if IO.select([$stdin], nil, nil, nil)
34       Ncurses.getch
35     else
36       nil
37     end
38   end
39
40   module_function :rows, :cols, :nonblocking_getch
41
42   KEY_CANCEL = "\a"[0] # ctrl-g
43 end
44
45 module Redwood
46
47 class Buffer
48   attr_reader :mode, :x, :y, :width, :height, :title
49   bool_reader :dirty
50
51   def initialize window, mode, width, height, opts={}
52     @w = window
53     @mode = mode
54     @dirty = true
55     @focus = false
56     @title = opts[:title] || ""
57     @x, @y, @width, @height = 0, 0, width, height
58   end
59
60   def content_height; @height - 1; end
61   def content_width; @width; end
62
63   def resize rows, cols
64     @width = cols
65     @height = rows
66     mode.resize rows, cols
67   end
68
69   def redraw
70     draw if @dirty
71     draw_status
72     commit
73   end
74   def mark_dirty; @dirty = true; end
75
76   def commit
77     @dirty = false
78     @w.noutrefresh
79   end
80
81   def draw
82     @mode.draw
83     draw_status
84     commit
85   end
86
87   ## s nil means a blank line!
88   def write y, x, s, opts={}
89     return if x >= @width || y >= @height
90
91     @w.attrset Colormap.color_for(opts[:color] || :none, opts[:highlight])
92     s ||= ""
93     maxl = @width - x
94     @w.mvaddstr y, x, s[0 ... maxl]
95     unless s.length >= maxl || opts[:no_fill]
96       @w.mvaddstr(y, x + s.length, " " * (maxl - s.length))
97     end
98   end
99
100   def clear
101     @w.clear
102   end
103
104   def draw_status
105     write @height - 1, 0, " [#{mode.name}] #{title}   #{mode.status}",
106       :color => :status_color
107   end
108
109   def focus
110     @focus = true
111     @dirty = true
112     @mode.focus
113   end
114
115   def blur
116     @focus = false
117     @dirty = true
118     @mode.blur
119   end
120 end
121
122 class BufferManager
123   include Singleton
124
125   attr_reader :focus_buf
126
127   def initialize
128     @name_map = {}
129     @buffers = []
130     @focus_buf = nil
131     @dirty = true
132     @minibuf_stack = []
133     @textfields = {}
134     @flash = nil
135     @freeze = false
136
137     self.class.i_am_the_instance self
138   end
139
140   def buffers; @name_map.to_a; end
141
142   def focus_on buf
143     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
144       @buffers.member? buf
145     return if buf == @focus_buf 
146     @focus_buf.blur if @focus_buf
147     @focus_buf = buf
148     @focus_buf.focus
149   end
150
151   def raise_to_front buf
152     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless
153       @buffers.member? buf
154     @buffers.delete buf
155     @buffers.push buf
156     focus_on buf
157     @dirty = true
158   end
159
160   def roll_buffers
161     raise_to_front @buffers.first
162   end
163
164   def roll_buffers_backwards
165     return unless @buffers.length > 1
166     raise_to_front @buffers[@buffers.length - 2]
167   end
168
169   def handle_input c
170     @focus_buf && @focus_buf.mode.handle_input(c)
171   end
172
173   def exists? n; @name_map.member? n; end
174   def [] n; @name_map[n]; end
175   def []= n, b
176     raise ArgumentError, "duplicate buffer name" if b && @name_map.member?(n)
177     @name_map[n] = b
178   end
179
180   def completely_redraw_screen
181     return if @freeze
182     Ncurses.clear
183     @dirty = true
184     draw_screen
185   end
186
187   def handle_resize
188     return if @freeze
189     rows, cols = Ncurses.rows, Ncurses.cols
190     @buffers.each { |b| b.resize rows - 1, cols }
191     completely_redraw_screen
192     flash "resized to #{rows}x#{cols}"
193   end
194
195   def draw_screen skip_minibuf=false
196     return if @freeze
197
198     ## disabling this for the time being, to help with debugging
199     ## (currently we only have one buffer visible at a time).
200     ## TODO: reenable this if we allow multiple buffers
201     false && @buffers.inject(@dirty) do |dirty, buf|
202       dirty ? buf.draw : buf.redraw
203       dirty || buf.dirty?
204     end
205     ## quick hack
206     true && (@dirty ? @buffers.last.draw : @buffers.last.redraw)
207     
208     draw_minibuf unless skip_minibuf
209     @dirty = false
210     Ncurses.doupdate
211   end
212
213   ## gets the mode from the block, which is only called if the buffer
214   ## doesn't already exist. this is useful in the case that generating
215   ## the mode is expensive, as it often is.
216   def spawn_unless_exists title, opts={}
217     if @name_map.member? title
218       Redwood::log "buffer '#{title}' already exists, raising to front"
219       raise_to_front @name_map[title]
220     else
221       mode = yield
222       spawn title, mode, opts
223     end
224     @name_map[title]
225   end
226
227   def spawn title, mode, opts={}
228     realtitle = title
229     num = 2
230     while @name_map.member? realtitle
231       realtitle = "#{title} #{num}"
232       num += 1
233     end
234
235     Redwood::log "spawning buffer \"#{realtitle}\""
236     width = opts[:width] || Ncurses.cols
237     height = opts[:height] || Ncurses.rows - 1
238
239     ## since we are currently only doing multiple full-screen modes,
240     ## use stdscr for each window. once we become more sophisticated,
241     ## we may need to use a new Ncurses::WINDOW
242     ##
243     ## w = Ncurses::WINDOW.new(height, width, (opts[:top] || 0),
244     ## (opts[:left] || 0))
245     w = Ncurses.stdscr
246     raise "nil window" unless w
247     
248     b = Buffer.new w, mode, width, height, :title => realtitle
249     mode.buffer = b
250     @name_map[realtitle] = b
251     if opts[:hidden]
252       @buffers.unshift b
253       focus_on b unless @focus_buf
254     else
255       @buffers.push b
256       raise_to_front b
257     end
258     b
259   end
260
261   def kill_all_buffers
262     kill_buffer @buffers.first until @buffers.empty?
263   end
264
265   def kill_buffer buf
266     raise ArgumentError, "buffer not on stack: #{buf.inspect}" unless @buffers.member? buf
267     Redwood::log "killing buffer \"#{buf.title}\""
268
269     buf.mode.cleanup
270     @buffers.delete buf
271     @name_map.delete buf.title
272     @focus_buf = nil if @focus_buf == buf
273     if @buffers.empty?
274       ## TODO: something intelligent here
275       ## for now I will simply prohibit killing the inbox buffer.
276     else
277       raise_to_front @buffers.last
278     end
279   end
280
281   def ask domain, question, default=nil
282     @textfields[domain] ||= TextField.new Ncurses.stdscr, Ncurses.rows - 1, 0,
283                             Ncurses.cols
284     tf = @textfields[domain]
285
286     ## this goddamn ncurses form shit is a fucking 1970's
287     ## nightmare. jesus christ. the exact sequence of ncurses events
288     ## that needs to happen in order to display a form and have the
289     ## entire screen not disappear and have the cursor in the right
290     ## place is TOO FUCKING COMPLICATED.
291     tf.activate question, default
292     @dirty = true
293     draw_screen true
294
295     ret = nil
296     @freeze = true
297     tf.position_cursor
298     Ncurses.refresh
299     while tf.handle_input(Ncurses.nonblocking_getch); end
300     @freeze = false
301
302     ret = tf.value
303     tf.deactivate
304     @dirty = true
305
306     ret
307   end
308
309   ## some pretty lame code in here!
310   def ask_getch question, accept=nil
311     accept = accept.split(//).map { |x| x[0] } if accept
312
313     flash question
314     Ncurses.curs_set 1
315     Ncurses.move Ncurses.rows - 1, question.length + 1
316     Ncurses.refresh
317
318     ret = nil
319     done = false
320     @freeze = true
321     until done
322       key = Ncurses.nonblocking_getch
323       if key == Ncurses::KEY_CANCEL
324         done = true
325       elsif (accept && accept.member?(key)) || !accept
326         ret = key
327         done = true
328       end
329     end
330     @freeze = false
331     Ncurses.curs_set 0
332     erase_flash
333     draw_screen
334     Ncurses.curs_set 0
335
336     ret
337   end
338
339   def ask_yes_or_no question
340     [?y, ?Y].member? ask_getch(question, "ynYN")
341   end
342
343   def draw_minibuf
344     s = @flash || @minibuf_stack.reverse.find { |x| x } || ""
345
346     Ncurses.attrset Colormap.color_for(:none)
347     Ncurses.mvaddstr Ncurses.rows - 1, 0, s + (" " * [Ncurses.cols - s.length,
348                                                       0].max)
349   end
350
351   def say s, id=nil
352     id ||= @minibuf_stack.length
353     @minibuf_stack[id] = s
354     unless @freeze
355       draw_screen
356       Ncurses.refresh
357     end
358     id
359   end
360
361   def erase_flash; @flash = nil; end
362
363   def flash s
364     @flash = s
365     unless @freeze
366       draw_screen
367       Ncurses.refresh
368     end
369   end
370
371   def clear id
372     @minibuf_stack[id] = nil
373     if id == @minibuf_stack.length - 1
374       id.downto(0) do |i|
375         break unless @minibuf_stack[i].nil?
376         @minibuf_stack.delete_at i
377       end
378     end
379     unless @freeze
380       draw_screen
381       Ncurses.refresh
382     end
383   end
384
385   def shell_out command
386     @freeze = true
387     Ncurses.endwin
388     system command
389     Ncurses.refresh
390     Ncurses.curs_set 0
391     @freeze = false
392   end
393 end
394 end