lib/sup/util/ncurses.rb (8729B) - raw
1 require 'ncursesw'
2 require 'sup/util'
3
4 if defined? Ncurses
5 module Ncurses
6
7 ## Helper class for storing keycodes
8 ## and multibyte characters.
9 class CharCode < String
10 ## Status code allows us to detect
11 ## printable characters and control codes.
12 attr_reader :status
13
14 ## Reads character from user input.
15 def self.nonblocking_getwch
16 # If we get input while we're shelled, we'll ignore it for the
17 # moment and use Ncurses.sync to wait until the shell_out is done.
18 begin
19 s, c = Redwood::BufferManager.shelled? ? Ncurses.sync { nil } : Ncurses.get_wch
20 break if s != Ncurses::ERR
21 end until IO.select([$stdin], nil, nil, 2)
22 [s, c]
23 end
24
25 ## Returns empty singleton.
26 def self.empty
27 Empty.instance
28 end
29
30 ## Creates new instance of CharCode
31 ## that keeps a given keycode.
32 def self.keycode(c)
33 generate c, Ncurses::KEY_CODE_YES
34 end
35
36 ## Creates new instance of CharCode
37 ## that keeps a printable character.
38 def self.character(c)
39 generate c, Ncurses::OK
40 end
41
42 ## Generates new object like new
43 ## but for empty or erroneous objects
44 ## it returns empty singleton.
45 def self.generate(c = nil, status = Ncurses::OK)
46 if status == Ncurses::ERR || c.nil? || c === Ncurses::ERR
47 empty
48 else
49 new(c, status)
50 end
51 end
52
53 ## Gets character from input.
54 ## Pretends ctrl-c's are ctrl-g's.
55 def self.get handle_interrupt=true
56 begin
57 status, code = nonblocking_getwch
58 generate code, status
59 rescue Interrupt => e
60 raise e unless handle_interrupt
61 keycode Ncurses::KEY_CANCEL
62 end
63 end
64
65 ## Enables dumb mode for any new instance.
66 def self.dumb!
67 @dumb = true
68 end
69
70 ## Asks if dumb mode was set
71 def self.dumb?
72 defined?(@dumb) && @dumb
73 end
74
75 def initialize(c = "", status = Ncurses::OK)
76 @status = status
77 c = "" if c.nil?
78 return super("") if status == Ncurses::ERR
79 c = enc_char(c) if c.is_a?(Integer)
80 super c.length > 1 ? c[0,1] : c
81 end
82
83 ## Proxy method for String's replace
84 def replace(c)
85 return self if c.object_id == object_id
86 if c.is_a?(self.class)
87 @status = c.status
88 super(c)
89 else
90 @status = Ncurses::OK
91 c = "" if c.nil?
92 c = enc_char(c) if c.is_a?(Integer)
93 super c.length > 1 ? c[0,1] : c
94 end
95 end
96
97 def to_character ; character? ? self : "<#{code}>" end ## Returns character or code as a string
98 def to_keycode ; keycode? ? code : Ncurses::ERR end ## Returns keycode or ERR if it's not a keycode
99 def to_sequence ; bytes.to_a end ## Returns unpacked sequence of bytes for a character
100 def code ; ord end ## Returns decimal representation of a character
101 def is_keycode?(c) ; keycode? && code == c end ## Tests if keycode matches
102 def is_character?(c); character? && self == c end ## Tests if character matches
103 def try_keycode ; keycode? ? code : nil end ## Returns dec. code if keycode, nil otherwise
104 def try_character ; character? ? self : nil end ## Returns character if character, nil otherwise
105 def keycode ; try_keycode end ## Alias for try_keycode
106 def character ; try_character end ## Alias for try_character
107 def character? ; dumb? || @status == Ncurses::OK end ## Returns true if character
108 def character! ; @status = Ncurses::OK ; self end ## Sets character flag
109 def keycode? ; dumb? || @status == Ncurses::KEY_CODE_YES end ## Returns true if keycode
110 def keycode! ; @status = Ncurses::KEY_CODE_YES ; self end ## Sets keycode flag
111 def keycode=(c) ; replace(c); keycode! ; self end ## Sets keycode
112 def present? ; not empty? end ## Proxy method
113 def printable? ; character? end ## Alias for character?
114 def dumb? ; self.class.dumb? end ## True if we cannot distinguish keycodes from characters
115
116 # Empty singleton that
117 # keeps GC from going crazy.
118 class Empty < CharCode
119 include Redwood::Singleton
120
121 ## Wrap methods that may change us
122 ## and generate new object instead.
123 [ :"[]=", :"<<", :replace, :insert, :prepend, :append, :concat, :force_encoding, :setbyte ].
124 select{ |m| public_method_defined?(m) }.
125 concat(public_instance_methods.grep(/!\z/)).
126 each do |m|
127 class_eval <<-EVAL
128 def #{m}(*args)
129 CharCode.new.#{m}(*args)
130 end
131 EVAL
132 end
133
134 ## proxy with class-level instance variable delegation
135 def self.dumb?
136 superclass.dumb? or !!@dumb
137 end
138
139 def self.empty
140 instance
141 end
142
143 def initialize
144 super("", Ncurses::ERR)
145 end
146
147 def empty? ; true end ## always true
148 def present? ; false end ## always false
149 def clear ; self end ## always self
150
151 self
152 end.init # CharCode::Empty
153
154 private
155
156 ## Tries to make external character right.
157 def enc_char(c)
158 begin
159 character = c.chr($encoding)
160 rescue RangeError, ArgumentError
161 begin
162 character = [c].pack('U')
163 rescue RangeError
164 begin
165 character = c.chr
166 rescue
167 begin
168 character = [c].pack('C')
169 rescue
170 character = ""
171 @status = Ncurses::ERR
172 end
173 end
174 end
175 character.fix_encoding!
176 end
177 end
178 end # class CharCode
179
180 def rows
181 lame, lamer = [], []
182 stdscr.getmaxyx lame, lamer
183 lame.first
184 end
185
186 def cols
187 lame, lamer = [], []
188 stdscr.getmaxyx lame, lamer
189 lamer.first
190 end
191
192 def curx
193 lame, lamer = [], []
194 stdscr.getyx lame, lamer
195 lamer.first
196 end
197
198 ## Create replacement wrapper for form_driver_w (), which is not (yet) a standard
199 ## function in ncurses. Some systems (Mac OS X) does not have a working
200 ## form_driver that accepts wide chars. We are just falling back to form_driver, expect problems.
201 def prepare_form_driver
202 if not defined? Form.form_driver_w
203 warn "Your Ncursesw does not have a form_driver_w function (wide char aware), " \
204 "non-ASCII chars may not work on your system."
205 Form.module_eval <<-FRM_DRV, __FILE__, __LINE__ + 1
206 def form_driver_w form, status, c
207 form_driver form, c
208 end
209 module_function :form_driver_w
210 module DriverHelpers
211 def form_driver c
212 if !c.dumb? && c.printable?
213 c.each_byte do |code|
214 Ncurses::Form.form_driver @form, code
215 end
216 else
217 Ncurses::Form.form_driver @form, c.code
218 end
219 end
220 end
221 FRM_DRV
222 end # if not defined? Form.form_driver_w
223 if not defined? Ncurses.get_wch
224 warn "Your Ncursesw does not have a get_wch function (wide char aware), " \
225 "non-ASCII chars may not work on your system."
226 Ncurses.module_eval <<-GET_WCH, __FILE__, __LINE__ + 1
227 def get_wch
228 c = getch
229 c == Ncurses::ERR ? [c, 0] : [Ncurses::OK, c]
230 end
231 module_function :get_wch
232 GET_WCH
233 CharCode.dumb!
234 end # if not defined? Ncurses.get_wch
235 end
236
237 def mutex; @mutex ||= Mutex.new; end
238 def sync &b; mutex.synchronize(&b); end
239
240 module_function :rows, :cols, :curx, :mutex, :sync, :prepare_form_driver
241
242 remove_const :KEY_ENTER
243 remove_const :KEY_CANCEL
244
245 KEY_ENTER = 10
246 KEY_CANCEL = 7 # ctrl-g
247 KEY_TAB = 9
248
249 module Form
250 ## This module contains helpers that ease
251 ## using form_driver_ methods when @form is present.
252 module DriverHelpers
253 private
254
255 ## Ncurses::Form.form_driver_w wrapper for keycodes and control characters.
256 def form_driver_key c
257 form_driver CharCode.keycode(c)
258 end
259
260 ## Ncurses::Form.form_driver_w wrapper for printable characters.
261 def form_driver_char c
262 form_driver CharCode.character(c)
263 #c.is_a?(Integer) ? c : c.ord
264 end
265
266 ## Ncurses::Form.form_driver_w wrapper for charcodes.
267 def form_driver c
268 Ncurses::Form.form_driver_w @form, c.status, c.code
269 end
270 end # module DriverHelpers
271 end # module Form
272
273 end # module Ncurses
274 end # if defined? Ncurses