Merge branch 'master' of git@github.com:elliottcable/nfoiled
[nfoiled.git] / lib / nfoiled / window.rb
blob0e3eaad1aace364c97265a26b1448f17096172fa
1 module Nfoiled
2   ##
3   # An `Nfoiled::Window` is a "box" in the terminal to which output can be
4   # printed and from which input can be received. A basic Nfoiled application
5   # will utilize only one of these, a single `Window` covering the entirety
6   # of the `Terminal`'s available area.
7   class Window
8     
9     class <<self
10       # This is simply an accessor for all the windows on the current Terminal.
11       attr_reader :windows
12       def windows; Terminal.current.windows; end
13     end
14     
15     # ==============
16     # = Attributes =
17     # ==============
18     
19     # The Y co-ordinate of the top left corner of this `Window`'s bounding box
20     attr_reader :top
21     # The X co-ordinate of the top left corner of this `Window`'s bounding box
22     attr_reader :left
23     # The height in lines of this `Window`'s bounding box
24     attr_reader :height
25     # The width in columns of this `Window`'s bounding box
26     attr_reader :width
27     
28     # The actual window object as returned by Ncurses
29     attr_reader :wrapee
30     
31     # The `Terminal` that this `Window` pertains to
32     attr_reader :owner
33     
34     # The status of the global 'input lock' on this window. While locked, all
35     # `#getk` calls will return nil.
36     # 
37     # If you plan to use `#getb`, always check for a lock first - otherwise
38     # you could grab a Unicode byte right from the waiting hands of a Unicode
39     # character `Thread`!
40     attr_accessor :input_locked
41     alias_method :input_locked?, :input_locked
42     def lock_input!; self.input_locked = true; end
43     def unlock_input!; self.input_locked = false; end
44     
45     # The proc to be run when a character is received
46     attr_accessor :on_key
47     
48     # ====================
49     # = Setup / Teardown =
50     # ====================
51     
52     ##
53     # Responsible for creating a new `Window`, this will also take care of
54     # initializing Ncurses if necessary. See `newwin(3X)`.
55     def initialize opts = Hash.new
56       Nfoiled::initialize
57       
58       @wrapee = ::Ncurses.newwin(
59         opts[:height] ? @height = opts[:height] : ::Ncurses.LINES,
60         opts[:width]  ? @width =  opts[:width]  : ::Ncurses.COLS,
61         opts[:top]    ? @top =    opts[:top]    : 0,
62         opts[:left]   ? @left =   opts[:left]   : 0)
63       
64       ::Ncurses.wtimeout(wrapee, 0) # Prevents ncurses from blocking for input
65       
66       (@owner = Terminal.current).windows << self
67     end
68     
69     ##
70     # Destroys the `wrapee` of this `Window`, and removes this `Window`
71     # from its owning `Terminal`'s `#windows`. See `delwin(3X)`.
72     def destroy!
73       ::Ncurses.delwin(wrapee)
74       @wrapee = nil
75       owner.windows.delete self
76     end
77     
78     # =========
79     # = Input =
80     # =========
81     
82     ##
83     # Gets a single byte from the input buffer for this window. Returns nil if
84     # there are no new characters in the buffer. See `wgetch(3X)`.
85     def getb
86       byte = ::Ncurses.wgetch(wrapee)
87       byte == -1 ? nil : byte
88     end
89     
90     ##
91     # Gets a single `Key` from the input buffer for this window.
92     # 
93     # This will asynchronously yield the `Key` to a block you provide, if such
94     # a block is given - in this mode, this method will return `true` if a
95     # `getk` is currently possible (input may become temporarily locked under
96     # certain circumstances), and `nil` if there is no `Key` to get.
97     # 
98     # For the most part, you can expect blocks to be yielded extremely
99     # quickly; however, don't count on this (a sudden, large paste of long-
100     # byte UTF-8 characters could cause each subsequent `getk` to take longer
101     # to yield).
102     # 
103     # This method can also be employed in a synchronous manner if no block is
104     # given; in this mode it acts as no more than a wrapper for `Window#getb`
105     # that automatically processes the ASCII char into a `Key` object. Higher-
106     # byte sequences such as Unicode UTF-8 will be treated as errors in this
107     # mode, returning `false`. Otherwise the `Key`-wrapped ASCII is returned.
108     def getk
109       return nil if self.input_locked?
110       byte = getb
111       return byte unless byte
112       if block_given?
113         case byte
114         when 0..127
115           yield Key.ascii byte
116         when 128..191, 192..193, 254..255
117           yield Key.new "�"
118         when 194..223, 224..239, 240..244, 245..247, 248..251, 252..253
119           handle_unicode byte, &Proc.new
120         end
121         return true
122       else # We'll synchronously return the ASCII value wrapped in a `Key`.
123         case byte
124         when 0..127
125           return Key.ascii byte
126         else
127           return Key.new "�"
128         end
129       end
130     end
131     
132     private
133       ##
134       # Handles a UTF-8 sequence from `getk`.
135       def handle_unicode byte, &handler
136         case byte
137           when 194..223; then bytes = 2 # 2 byte sequence
138           when 224..239; then bytes = 3 # 3 byte sequence
139           when 240..244; then bytes = 4 # 4 byte sequence below 10FFFF
140           when 245..247; then bytes = 4 # 4 byte sequence above 10FFFF
141           when 248..251; then bytes = 5 # 5 byte sequence
142           when 252..253; then bytes = 6 # 6 byte sequence
143         end
144         
145         Thread.start(self, byte, bytes, handler) do |window, first_byte, bytes, handler|
146           uba = [first_byte]
147           
148           Thread.pass while window.input_locked?
149           window.lock_input!
150           
151           until uba.length >= bytes
152             byte = getb
153             if byte
154               case byte
155               when 0..127, 192..193, 254..255, 194..223, 224..239, 240..244, 245..247, 248..251, 252..253
156                 Thread.start(handler) {|handler| handler[Key.new "�"] }
157                 window.unlock_input!
158                 Thread.kill
159               when 128..191
160                 uba << byte
161               end
162             end
163             
164             Thread.pass
165           end
166           
167           Thread.start(handler) {|handler| handler[Key.new uba.pack('C*')] }
168           window.unlock_input!
169         end
170       end
171       
172       
173     public
174     
175     ##
176     # This sets this `Window` as the current `Terminal.acceptor`.
177     def focus!
178       owner.acceptor = self
179       update
180     end
181     
182     ##
183     # Defines a block that controls how the global input loop from
184     # `Nfoiled::read!` handles input when this window has focus.
185     # 
186     # This acts as both a getter & setter, depending on whether a block is
187     # passed in or not.
188     def on_key
189       block_given? ? @on_key = Proc.new : @on_key
190     end
191     
192     # ==========
193     # = Output =
194     # ==========
195     
196     ##
197     # Prints a string to the window
198     def print stringish
199       wrapee.printw stringish.to_s
200       update
201     end
202     
203     ##
204     # Updates the virtual screen associated with this window. See `wnoutrefresh(3X)`.
205     def update
206       wrapee.wnoutrefresh
207     end
208     
209     ##
210     # Prints a string, followed by a newline, to the window
211     def puts stringish
212       self.print stringish.to_s + "\n"
213     end
214   end