rbcurse-core 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. data/README.md +69 -0
  2. data/VERSION +1 -0
  3. data/examples/abasiclist.rb +151 -0
  4. data/examples/alpmenu.rb +46 -0
  5. data/examples/app.sample +17 -0
  6. data/examples/atree.rb +100 -0
  7. data/examples/common/file.rb +45 -0
  8. data/examples/data/README.markdown +9 -0
  9. data/examples/data/brew.txt +38 -0
  10. data/examples/data/color.2 +37 -0
  11. data/examples/data/gemlist.txt +60 -0
  12. data/examples/data/lotr.txt +12 -0
  13. data/examples/data/ports.txt +136 -0
  14. data/examples/data/table.txt +37 -0
  15. data/examples/data/tasks.csv +88 -0
  16. data/examples/data/tasks.txt +27 -0
  17. data/examples/data/todo.txt +10 -0
  18. data/examples/data/todocsv.csv +28 -0
  19. data/examples/data/unix1.txt +21 -0
  20. data/examples/data/unix2.txt +11 -0
  21. data/examples/dbdemo.rb +487 -0
  22. data/examples/dirtree.rb +90 -0
  23. data/examples/newtabbedwindow.rb +100 -0
  24. data/examples/newtesttabp.rb +92 -0
  25. data/examples/tabular.rb +132 -0
  26. data/examples/tasks.rb +167 -0
  27. data/examples/term2.rb +83 -0
  28. data/examples/testkeypress.rb +72 -0
  29. data/examples/testlistbox.rb +158 -0
  30. data/examples/testmessagebox.rb +140 -0
  31. data/examples/testree.rb +106 -0
  32. data/examples/testwsshortcuts.rb +66 -0
  33. data/examples/testwsshortcuts2.rb +127 -0
  34. data/lib/rbcurse.rb +8 -0
  35. data/lib/rbcurse/core/docs/index.txt +73 -0
  36. data/lib/rbcurse/core/include/action.rb +40 -0
  37. data/lib/rbcurse/core/include/appmethods.rb +112 -0
  38. data/lib/rbcurse/core/include/bordertitle.rb +41 -0
  39. data/lib/rbcurse/core/include/chunk.rb +182 -0
  40. data/lib/rbcurse/core/include/io.rb +953 -0
  41. data/lib/rbcurse/core/include/listcellrenderer.rb +140 -0
  42. data/lib/rbcurse/core/include/listeditable.rb +317 -0
  43. data/lib/rbcurse/core/include/listscrollable.rb +590 -0
  44. data/lib/rbcurse/core/include/listselectable.rb +264 -0
  45. data/lib/rbcurse/core/include/multibuffer.rb +83 -0
  46. data/lib/rbcurse/core/include/orderedhash.rb +77 -0
  47. data/lib/rbcurse/core/include/ractionevent.rb +67 -0
  48. data/lib/rbcurse/core/include/rchangeevent.rb +27 -0
  49. data/lib/rbcurse/core/include/rhistory.rb +62 -0
  50. data/lib/rbcurse/core/include/rinputdataevent.rb +47 -0
  51. data/lib/rbcurse/core/include/vieditable.rb +170 -0
  52. data/lib/rbcurse/core/system/colormap.rb +163 -0
  53. data/lib/rbcurse/core/system/keyboard.rb +150 -0
  54. data/lib/rbcurse/core/system/keydefs.rb +30 -0
  55. data/lib/rbcurse/core/system/ncurses.rb +218 -0
  56. data/lib/rbcurse/core/system/panel.rb +162 -0
  57. data/lib/rbcurse/core/system/window.rb +901 -0
  58. data/lib/rbcurse/core/util/ansiparser.rb +117 -0
  59. data/lib/rbcurse/core/util/app.rb +1235 -0
  60. data/lib/rbcurse/core/util/basestack.rb +407 -0
  61. data/lib/rbcurse/core/util/bottomline.rb +1850 -0
  62. data/lib/rbcurse/core/util/colorparser.rb +71 -0
  63. data/lib/rbcurse/core/util/focusmanager.rb +31 -0
  64. data/lib/rbcurse/core/util/padreader.rb +189 -0
  65. data/lib/rbcurse/core/util/rcommandwindow.rb +587 -0
  66. data/lib/rbcurse/core/util/rdialogs.rb +619 -0
  67. data/lib/rbcurse/core/util/viewer.rb +149 -0
  68. data/lib/rbcurse/core/util/widgetshortcuts.rb +505 -0
  69. data/lib/rbcurse/core/widgets/applicationheader.rb +102 -0
  70. data/lib/rbcurse/core/widgets/box.rb +58 -0
  71. data/lib/rbcurse/core/widgets/divider.rb +310 -0
  72. data/lib/rbcurse/core/widgets/keylabelprinter.rb +178 -0
  73. data/lib/rbcurse/core/widgets/rcombo.rb +238 -0
  74. data/lib/rbcurse/core/widgets/rcontainer.rb +415 -0
  75. data/lib/rbcurse/core/widgets/rlink.rb +30 -0
  76. data/lib/rbcurse/core/widgets/rlist.rb +723 -0
  77. data/lib/rbcurse/core/widgets/rmenu.rb +939 -0
  78. data/lib/rbcurse/core/widgets/rmenulink.rb +22 -0
  79. data/lib/rbcurse/core/widgets/rmessagebox.rb +373 -0
  80. data/lib/rbcurse/core/widgets/rprogress.rb +118 -0
  81. data/lib/rbcurse/core/widgets/rtabbedpane.rb +615 -0
  82. data/lib/rbcurse/core/widgets/rtabbedwindow.rb +68 -0
  83. data/lib/rbcurse/core/widgets/rtextarea.rb +920 -0
  84. data/lib/rbcurse/core/widgets/rtextview.rb +780 -0
  85. data/lib/rbcurse/core/widgets/rtree.rb +787 -0
  86. data/lib/rbcurse/core/widgets/rwidget.rb +3040 -0
  87. data/lib/rbcurse/core/widgets/scrollbar.rb +143 -0
  88. data/lib/rbcurse/core/widgets/statusline.rb +94 -0
  89. data/lib/rbcurse/core/widgets/tabular.rb +264 -0
  90. data/lib/rbcurse/core/widgets/tabularwidget.rb +1211 -0
  91. data/lib/rbcurse/core/widgets/textpad.rb +516 -0
  92. data/lib/rbcurse/core/widgets/tree/treecellrenderer.rb +150 -0
  93. data/lib/rbcurse/core/widgets/tree/treemodel.rb +428 -0
  94. metadata +156 -0
@@ -0,0 +1,407 @@
1
+ #
2
+ # Common stack flow functionality
3
+ # * Name: basestack.rb
4
+ # * Description: Classes that allow user to stack and flow components
5
+ #
6
+ # * Date: 30.10.11 - 12:57
7
+ # * Last update: 30.10.11 - 12:57
8
+ #
9
+ module RubyCurses
10
+ module ModStack
11
+ #
12
+ # Base class for stacks and flows.
13
+ # Will manage determining row col and width height of objects
14
+ # Stacks place objects one below another. Flows place objects to the
15
+ # right of the previous. Orientation can be reversed.
16
+ #
17
+ class BaseStack
18
+ attr_accessor :components
19
+ attr_reader :config
20
+ attr_accessor :form
21
+ def initialize config={}, components=[]
22
+ @config = config
23
+ config.each do |k, v|
24
+ instance_variable_set "@#{k}", v
25
+ end
26
+ @components = components
27
+ @calc_needed = true
28
+ end
29
+ # XXX if user sets later, we won't be checking the config
30
+ # We check the actual variables which config sets in init
31
+ %w[ parent_component width height weight row col orientation].each { |e|
32
+ eval(
33
+ "def #{e}
34
+ @config[:#{e}]
35
+ end
36
+ def #{e}=(val)
37
+ @config[:#{e}]=val
38
+ instance_variable_set \"@#{e}\", val
39
+ @calc_needed = true
40
+ end"
41
+ )
42
+ }
43
+ alias :parent :parent_component
44
+ #alias :parent= :parent_component
45
+ def repaint # stack
46
+ $log.debug "XXX: stack repaint recalc #{@calc_needed} "
47
+ @components.each { |e| e.form = @form unless e.form } #unless @calc_needed
48
+ recalc if @calc_needed
49
+ @components.each { |e| e.repaint }
50
+ end
51
+ def repaint_all x
52
+ @calc_needed = true
53
+ end
54
+ def override_graphic gr
55
+ @graphic = gr
56
+ end
57
+ def focusable; false; end
58
+ # Calculates row col and width height
59
+ # for each subc-omponent based on coords of Container
60
+ # This is to be called only when the container has got its coordinates (i.e
61
+ # Containers repaint). This should be in this objects repaint.
62
+ def recalc
63
+ @calc_needed = false
64
+ comp = self
65
+ if comp.is_a? BaseStack
66
+ check_coords comp
67
+ @margin_left ||= 0
68
+ @margin_right ||= 0
69
+ @margin_top ||= 0
70
+ @margin_bottom ||= 0
71
+ if comp.is_a? Stack
72
+ r = row + @margin_top
73
+ rem = 0
74
+ ht = height - (@margin_top + @margin_bottom)
75
+ if @orientation == :bottom
76
+ mult = -1
77
+ comps = @components.reverse
78
+ r = row + height - @margin_bottom
79
+
80
+ else
81
+ mult = 1
82
+ comps = @components
83
+
84
+ end
85
+ comps.each { |e|
86
+ # should only happen if expandable FIXME
87
+ e.height = 0.01 * e.weight * (ht - (e.margin_top + e.margin_bottom))
88
+ hround = e.height.floor
89
+ rem += e.height - hround
90
+ e.height = hround #- (@margin_top + @margin_bottom)
91
+ # rounding creates a problem, since 0.5 gets rounded up and we can exceed bound
92
+ # So i floor, and maintain the lost space, and add it back when it exceeds 1
93
+ # This way the last components gets stretched to meet the end, which is required
94
+ # when the height of the stack is odd and there's a left-over row
95
+ if rem >= 1
96
+ e.height += 1
97
+ rem = 0
98
+ end
99
+ # Item level margins have not been accounted for when calculating weightages, and
100
+ # should not be used on the weightage axis
101
+ r += e.margin_top
102
+ if @orientation == :bottom
103
+ r += e.height * mult
104
+ e.row = r
105
+ else
106
+ e.row = r
107
+ r += e.height + 0
108
+ end
109
+ e.width = width - (@margin_left + @margin_right + e.margin_left + e.margin_right)
110
+ e.col = col + @margin_left + e.margin_left # ??? XXX
111
+ $log.debug "XXX: recalc stack #{e.widget.class} r:#{e.row} c:#{e.col} h:#{e.height} = we:#{e.weight} * h:#{height} "
112
+ #e.col_offset = col_offset # ??? XXX
113
+ check_coords e
114
+ e.repaint_all(true)
115
+ e.recalc if e.is_a? BaseStack
116
+ }
117
+ elsif comp.is_a? Flow
118
+ c = col + @margin_left #+ col_offset
119
+ rem = 0
120
+ wd = width - (@margin_left + @margin_right)
121
+ # right_to_left orientation
122
+ if @orientation == :right
123
+ mult = -1
124
+ comps = @components.reverse
125
+ c = col + width - @margin_right
126
+ $log.debug "XXX: ORIENT1f recalc #{@orientation} "
127
+ else
128
+ mult = 1
129
+ comps = @components
130
+ $log.debug "XXX: ORIENT2f recalc #{@orientation} "
131
+ end
132
+ comps.each { |e|
133
+ e.width = e.weight * wd * 0.01
134
+ wround = e.width.floor
135
+ rem += e.width - wround
136
+ e.width = wround
137
+ # see comment in prev block regarding remaininder
138
+ if rem >= 1
139
+ e.width += 1
140
+ rem = 0
141
+ end
142
+ e.height = height - (@margin_top + @margin_bottom) #* weight * 0.01
143
+ #e.height = e.height.round
144
+ if @orientation == :right
145
+ c += e.width * mult # mult 1 or -1
146
+ e.col = c
147
+ else
148
+ e.col = c
149
+ c += e.width * mult # mult 1 or -1
150
+ end
151
+ e.row = row + @margin_top
152
+ check_coords e
153
+ $log.debug "XXX: recalc flow #{e.widget.class} r:#{e.row} c:#{e.col} h:#{e.height} = we:#{e.weight} * w:#{width} "
154
+ e.repaint_all(true) # why not happening when we change row, hieght etc
155
+ e.recalc if e.is_a? BaseStack
156
+ }
157
+ end
158
+ else
159
+ alert "in else recalc DOES NOT COME HERE "
160
+ comp.col = comp.parent.col
161
+ comp.row = comp.parent.row
162
+ comp.height = comp.parent.height
163
+ comp.width = comp.parent.width
164
+ $log.debug "XXX: recalc else #{comp.class} r #{comp.row} c #{comp.col} . h #{comp} height w #{comp.width} "
165
+ end
166
+ end
167
+ # Traverses the comopnent tree and calculates weightages for all components
168
+ # based on what has been specified by user
169
+ def check_coords e # stack
170
+ r = e.row
171
+ c = e.col
172
+ if r >= row + height
173
+ $log.warn "XXX: WARN e.class is out of bounds row #{r} "
174
+ e.visible = false
175
+ end
176
+ if c >= col + width
177
+ $log.warn "XXX: WARN e.class is out of bounds col #{c} "
178
+ e.visible = false
179
+ end
180
+ end
181
+ def increase c=@current_component
182
+ p = self #c.parent_component
183
+ ci = p.components.index(c)
184
+ ni = ci + 1
185
+ if p.components[ni].nil?
186
+ ni = nil
187
+ end
188
+ case p
189
+ when Flow
190
+ # increase width of current and reduce from neighbor
191
+ if ni
192
+ n = p.components[ni]
193
+ $log.debug "XXX: INC fl current #{ci}, total#{p.components.count}, next #{n} "
194
+
195
+ c.width += 1
196
+ n.width -= 1
197
+ n.col += 1
198
+ end
199
+
200
+ when Stack
201
+ if ni
202
+ n = p.components[ni]
203
+ $log.debug "XXX: INC fl current #{ci}, total#{p.components.count}, next #{n} "
204
+
205
+ c.height += 1
206
+ n.height -= 1
207
+ n.row += 1
208
+ end
209
+ $log.debug "XXX: INC st current #{ci}, total#{p.components.count} "
210
+ end
211
+
212
+ end
213
+ def decrease c=@current_component
214
+ p = self #c.parent_component
215
+ ci = p.components.index(c)
216
+ ni = ci + 1
217
+ if p.components[ni].nil?
218
+ ni = nil
219
+ end
220
+ case p
221
+ when Flow
222
+ # increase width of current and reduce from neighbor
223
+ if ni
224
+ n = p.components[ni]
225
+ $log.debug "XXX: INC fl current #{ci}, total#{p.components.count}, next #{n} "
226
+
227
+ c.width -= 1
228
+ n.width += 1
229
+ n.col -= 1
230
+ end
231
+
232
+ when Stack
233
+ if ni
234
+ n = p.components[ni]
235
+ $log.debug "XXX: INC fl current #{ci}, total#{p.components.count}, next #{n} "
236
+
237
+ c.height -= 1
238
+ n.height += 1
239
+ n.row -= 1
240
+ end
241
+ $log.debug "XXX: INC st current #{ci}, total#{p.components.count} "
242
+ end
243
+
244
+ end
245
+ def to_s
246
+ @components
247
+ end
248
+ end # class Base
249
+ # A stack positions objects one below the other
250
+ class Stack < BaseStack; end
251
+ # A flow positions objects in a left to right
252
+ class Flow < BaseStack; end
253
+ #
254
+ # A wrapper over widget mostly because it adds weight and margins
255
+ #
256
+ class Item
257
+ attr_reader :config, :widget
258
+ attr_reader :margin_top, :margin_left, :margin_bottom, :margin_right
259
+ def initialize config={}, widget
260
+ @config = config
261
+ config.each do |k, v|
262
+ instance_variable_set "@#{k}", v
263
+ end
264
+ @margin_left ||= 0
265
+ @margin_right ||= 0
266
+ @margin_top ||= 0
267
+ @margin_bottom ||= 0
268
+ @widget = widget
269
+ end
270
+ def weight; @config[:weight]||100; end
271
+ def weight=(val); @config[:weight]=val; end
272
+ def repaint; @widget.repaint; end
273
+ %w[ form parent parent_component width height row col row_offset col_offset focusable].each { |e|
274
+ eval(
275
+ "def #{e}
276
+ @widget.#{e}
277
+ end
278
+ def #{e}=(val)
279
+ @widget.#{e}=val
280
+ end"
281
+ )
282
+ }
283
+ def method_missing(sym, *args, &block)
284
+ @widget.send sym, *args, &block
285
+ end
286
+ end # class Item
287
+
288
+ # --------------------- module level ------------------------------#
289
+ # General routin to traverse components and their components
290
+ def traverse c, &block
291
+ if c.is_a? BaseStack
292
+ yield c
293
+ c.components.each { |e|
294
+ yield e
295
+ }
296
+ c.components.each { |e| traverse(e, &block) }
297
+ @ctr -= 1
298
+ else
299
+ end
300
+ end
301
+
302
+ # traverse the components and their children
303
+ #
304
+ def each &block
305
+ @components.each { |e| traverse e, &block }
306
+ end
307
+ # module level
308
+ private
309
+ def _stack type, config={}, &block
310
+ case type
311
+ when :stack
312
+ s = Stack.new(config)
313
+ when :flow
314
+ s = Flow.new(config)
315
+ end
316
+ _add s
317
+ @active << s
318
+ yield_or_eval &block if block_given?
319
+ @active.pop
320
+ # if active is empty then this is where we could calculate
321
+ # percentatges and do recalc, thus making it independent
322
+ end
323
+ # module level
324
+ private
325
+ private
326
+ def _add s
327
+ if @active.empty?
328
+ $log.debug "XXX: ADDING TO components #{s} "
329
+ unless s.is_a? BaseStack
330
+ raise "No stack or flow to add to. Results may not be what you want"
331
+ end
332
+ @components << s
333
+ else
334
+ @active.last.components << s
335
+ end
336
+ __add s
337
+ end
338
+
339
+ # module level
340
+ private
341
+ public
342
+ def stack config={}, &block
343
+ _stack :stack, config, &block
344
+ end
345
+ def flow config={}, &block
346
+ _stack :flow, config, &block
347
+ end
348
+ # module level
349
+ private
350
+ def add w, config={}
351
+ i = Item.new config, w
352
+ _add i
353
+ end
354
+ alias :add_widget :add
355
+ # module level
356
+ private
357
+ def calc_weightages2 components, parent
358
+ #puts " #{@ctr} --> #{c.type}, wt: #{c.config[:weight]} "
359
+ @ctr += 1
360
+ wt = 0
361
+ cnt = 0
362
+ sz = components.count
363
+ $log.debug "XXX: calc COMP COUNT #{sz} "
364
+ # calculate how much weightage has been given by user
365
+ # so we can allocate average to other components
366
+ components.each { |e|
367
+ if e.config[:weight]
368
+ wt += e.config[:weight]
369
+ cnt += 1
370
+ end
371
+ $log.debug "XXX: INC setting parent #{parent} to #{e} "
372
+ e.config[:parent] = parent
373
+ e.config[:level] = @ctr
374
+ }
375
+ used = sz - cnt
376
+ $log.debug "XXX: ADDING calc COMP COUNT #{sz} - #{cnt} "
377
+ if used > 0
378
+ avg = (100-wt)/used
379
+ # Allocate average to other components
380
+ components.each { |e| e.config[:weight] = avg unless e.config[:weight] }
381
+ end
382
+ components.each { |e| calc_weightages2(e.components, e) if e.respond_to? :components }
383
+ @ctr -= 1
384
+ end
385
+ # module level
386
+ private
387
+ # given an widget, return the item, so we can change weight or some other config
388
+ def item_for widget
389
+ each do |e|
390
+ if e.is_a? Item
391
+ if e.widget == widget
392
+ return e
393
+ end
394
+ end
395
+ end
396
+ return nil
397
+ end
398
+ # module level
399
+ # returns the parent (flow or stack) for a given widget
400
+ # allowing user to change configuration such as weight
401
+ def parent_of widget
402
+ f = item_for widget
403
+ return f.config[:parent] if f
404
+ return nil
405
+ end
406
+ end # mod modstack
407
+ end # mod
@@ -0,0 +1,1850 @@
1
+ require "date"
2
+ require "erb"
3
+ require 'pathname'
4
+ =begin
5
+ * Name : bottomline.rb
6
+ * Description : routines for input at bottom of screen like vim, or anyother line
7
+ * :
8
+ * Author : rkumar
9
+ * Date : 2010-10-25 12:45
10
+ * License :
11
+ Same as Ruby's License (http://www.ruby-lang.org/LICENSE.txt)
12
+
13
+ The character input routines are from io.rb, however, the user-interface to the input
14
+ is copied from the Highline project (James Earl Gray) with permission.
15
+
16
+ May later use a Label and Field.
17
+
18
+ NOTE : Pls avoid directly using this class. I am trying to redo this so ask, agree and say
19
+ can create their own window and be done with it. The hurdle in that is that ask calls
20
+ say, so when to close the window is not clear within say. Some shakeup is expected by
21
+ 1.4.0 or so.
22
+
23
+ =end
24
+ module RubyCurses
25
+
26
+ # just so the program does not bomb due to a tiny feature
27
+ # I do not raise error on nil array, i create a dummy array
28
+ # which you likely will not be able to use, in any case it will have only one value
29
+ class History < Struct.new(:array, :current_index)
30
+ attr_reader :last_index
31
+ attr_reader :current_index
32
+ attr_reader :array
33
+ def initialize a=nil, c=0
34
+ #raise "Array passed to History cannot be nil" unless a
35
+ #@max_index = a.size
36
+ @array = a || []
37
+ @current_index = c
38
+ @last_index = c
39
+ end
40
+ def last
41
+ @current_index = max_index
42
+ @array.last
43
+ end
44
+ def first
45
+ @current_index = 0
46
+ @array.first
47
+ end
48
+ def max_index
49
+ @array.size - 1
50
+ end
51
+ def up
52
+ item = @array[@current_index]
53
+ previous
54
+ return item
55
+ end
56
+ def next
57
+ @last_index = @current_index
58
+ if @current_index + 1 > max_index
59
+ @current_index = 0
60
+ else
61
+ @current_index += 1
62
+ end
63
+ @array[@current_index]
64
+ end
65
+ def previous
66
+ @last_index = @current_index
67
+ if @current_index - 1 < 0
68
+ @current_index = max_index()
69
+ else
70
+ @current_index -= 1
71
+ end
72
+ @array[@current_index]
73
+ end
74
+ def is_last?
75
+ @current_index == max_index()
76
+ end
77
+ def push item
78
+ $log.debug " XXX history push #{item} " if $log.debug?
79
+ @array.push item
80
+ @current_index = max_index
81
+ end
82
+ end # class
83
+ # some variables are polluting space of including app,
84
+ # we should make this a class.
85
+ class Bottomline
86
+ attr_accessor :window
87
+ attr_accessor :message_row
88
+ attr_accessor :name # for debugging
89
+ def initialize win=nil, row=nil
90
+ @window = win
91
+ #@window.wrefresh
92
+ #Ncurses::Panel.update_panels
93
+ #@message_row = row
94
+ @message_row = 0 # 2011-10-8
95
+ end
96
+ #
97
+ # create a window at bottom and show and hide it.
98
+ # Causing a stack overflow since Window creates a bottomline too !
99
+ #
100
+ def _create_footer_window h = 1 , w = Ncurses.COLS, t = Ncurses.LINES-1, l = 0
101
+ ewin = VER::Window.new(h, w , t, l)
102
+ #ewin.bkgd(Ncurses.COLOR_PAIR($promptcolor));
103
+ @window = ewin
104
+ return ewin
105
+ end
106
+
107
+ class QuestionError < StandardError
108
+ # do nothing, just creating a unique error type
109
+ end
110
+ class Question
111
+ # An internal HighLine error. User code does not need to trap this.
112
+ class NoAutoCompleteMatch < StandardError
113
+ # do nothing, just creating a unique error type
114
+ end
115
+
116
+ #
117
+ # Create an instance of HighLine::Question. Expects a _question_ to ask
118
+ # (can be <tt>""</tt>) and an _answer_type_ to convert the answer to.
119
+ # The _answer_type_ parameter must be a type recognized by
120
+ # Question.convert(). If given, a block is yeilded the new Question
121
+ # object to allow custom initializaion.
122
+ #
123
+ def initialize( question, answer_type )
124
+ # initialize instance data
125
+ @question = question
126
+ @answer_type = answer_type
127
+
128
+ @character = nil
129
+ @limit = nil
130
+ @echo = true
131
+ @readline = false
132
+ @whitespace = :strip
133
+ @_case = nil
134
+ @default = nil
135
+ @validate = nil
136
+ @above = nil
137
+ @below = nil
138
+ @in = nil
139
+ @confirm = nil
140
+ @gather = false
141
+ @first_answer = nil
142
+ @directory = Pathname.new(File.expand_path(File.dirname($0)))
143
+ @glob = "*"
144
+ @responses = Hash.new
145
+ @overwrite = false
146
+ @history = nil
147
+
148
+ # allow block to override settings
149
+ yield self if block_given?
150
+
151
+ #$log.debug " XXX default #{@default}" if $log.debug?
152
+ #$log.debug " XXX history #{@history}" if $log.debug?
153
+
154
+ # finalize responses based on settings
155
+ build_responses
156
+ end
157
+
158
+ # The ERb template of the question to be asked.
159
+ attr_accessor :question
160
+ # The type that will be used to convert this answer.
161
+ attr_accessor :answer_type
162
+ #
163
+ # Can be set to +true+ to use HighLine's cross-platform character reader
164
+ # instead of fetching an entire line of input. (Note: HighLine's character
165
+ # reader *ONLY* supports STDIN on Windows and Unix.) Can also be set to
166
+ # <tt>:getc</tt> to use that method on the input stream.
167
+ #
168
+ # *WARNING*: The _echo_ and _overwrite_ attributes for a question are
169
+ # ignored when using the <tt>:getc</tt> method.
170
+ #
171
+ attr_accessor :character
172
+ #
173
+ # Allows you to set a character limit for input.
174
+ #
175
+ # If not set, a default of 100 is used
176
+ #
177
+ attr_accessor :limit
178
+ #
179
+ # Can be set to +true+ or +false+ to control whether or not input will
180
+ # be echoed back to the user. A setting of +true+ will cause echo to
181
+ # match input, but any other true value will be treated as to String to
182
+ # echo for each character typed.
183
+ #
184
+ # This requires HighLine's character reader. See the _character_
185
+ # attribute for details.
186
+ #
187
+ # *Note*: When using HighLine to manage echo on Unix based systems, we
188
+ # recommend installing the termios gem. Without it, it's possible to type
189
+ # fast enough to have letters still show up (when reading character by
190
+ # character only).
191
+ #
192
+ attr_accessor :echo
193
+ #
194
+ # Use the Readline library to fetch input. This allows input editing as
195
+ # well as keeping a history. In addition, tab will auto-complete
196
+ # within an Array of choices or a file listing.
197
+ #
198
+ # *WARNING*: This option is incompatible with all of HighLine's
199
+ # character reading modes and it causes HighLine to ignore the
200
+ # specified _input_ stream.
201
+ #
202
+ # this messes up in ncurses RK 2010-10-24 12:23
203
+ attr_accessor :readline
204
+ #
205
+ # Used to control whitespace processing for the answer to this question.
206
+ # See HighLine::Question.remove_whitespace() for acceptable settings.
207
+ #
208
+ attr_accessor :whitespace
209
+ #
210
+ # Used to control character case processing for the answer to this question.
211
+ # See HighLine::Question.change_case() for acceptable settings.
212
+ #
213
+ attr_accessor :_case
214
+ # Used to provide a default answer to this question.
215
+ attr_accessor :default
216
+ #
217
+ # If set to a Regexp, the answer must match (before type conversion).
218
+ # Can also be set to a Proc which will be called with the provided
219
+ # answer to validate with a +true+ or +false+ return.
220
+ #
221
+ attr_accessor :validate
222
+ # Used to control range checks for answer.
223
+ attr_accessor :above, :below
224
+ # If set, answer must pass an include?() check on this object.
225
+ attr_accessor :in
226
+ #
227
+ # Asks a yes or no confirmation question, to ensure a user knows what
228
+ # they have just agreed to. If set to +true+ the question will be,
229
+ # "Are you sure? " Any other true value for this attribute is assumed
230
+ # to be the question to ask. When +false+ or +nil+ (the default),
231
+ # answers are not confirmed.
232
+ #
233
+ attr_accessor :confirm
234
+ #
235
+ # When set, the user will be prompted for multiple answers which will
236
+ # be collected into an Array or Hash and returned as the final answer.
237
+ #
238
+ # You can set _gather_ to an Integer to have an Array of exactly that
239
+ # many answers collected, or a String/Regexp to match an end input which
240
+ # will not be returned in the Array.
241
+ #
242
+ # Optionally _gather_ can be set to a Hash. In this case, the question
243
+ # will be asked once for each key and the answers will be returned in a
244
+ # Hash, mapped by key. The <tt>@key</tt> variable is set before each
245
+ # question is evaluated, so you can use it in your question.
246
+ #
247
+ attr_accessor :gather
248
+ #
249
+ # When set to a non *nil* value, this will be tried as an answer to the
250
+ # question. If this answer passes validations, it will become the result
251
+ # without the user ever being prompted. Otherwise this value is discarded,
252
+ # and this Question is resolved as a normal call to HighLine.ask().
253
+ #
254
+ attr_writer :first_answer
255
+ #
256
+ # The directory from which a user will be allowed to select files, when
257
+ # File or Pathname is specified as an _answer_type_. Initially set to
258
+ # <tt>Pathname.new(File.expand_path(File.dirname($0)))</tt>.
259
+ #
260
+ attr_accessor :directory
261
+ #
262
+ # The glob pattern used to limit file selection when File or Pathname is
263
+ # specified as an _answer_type_. Initially set to <tt>"*"</tt>.
264
+ #
265
+ attr_accessor :glob
266
+ #
267
+ # A Hash that stores the various responses used by HighLine to notify
268
+ # the user. The currently used responses and their purpose are as
269
+ # follows:
270
+ #
271
+ # <tt>:ambiguous_completion</tt>:: Used to notify the user of an
272
+ # ambiguous answer the auto-completion
273
+ # system cannot resolve.
274
+ # <tt>:ask_on_error</tt>:: This is the question that will be
275
+ # redisplayed to the user in the event
276
+ # of an error. Can be set to
277
+ # <tt>:question</tt> to repeat the
278
+ # original question.
279
+ # <tt>:invalid_type</tt>:: The error message shown when a type
280
+ # conversion fails.
281
+ # <tt>:no_completion</tt>:: Used to notify the user that their
282
+ # selection does not have a valid
283
+ # auto-completion match.
284
+ # <tt>:not_in_range</tt>:: Used to notify the user that a
285
+ # provided answer did not satisfy
286
+ # the range requirement tests.
287
+ # <tt>:not_valid</tt>:: The error message shown when
288
+ # validation checks fail.
289
+ #
290
+ attr_reader :responses
291
+ #
292
+ # When set to +true+ the question is asked, but output does not progress to
293
+ # the next line. The Cursor is moved back to the beginning of the question
294
+ # line and it is cleared so that all the contents of the line disappear from
295
+ # the screen.
296
+ #
297
+ attr_accessor :overwrite
298
+
299
+ #
300
+ # If the user presses tab in ask(), then this proc is used to fill in
301
+ # values. Typically, for files. e.g.
302
+ #
303
+ # q.completion_proc = Proc.new {|str| Dir.glob(str +"*") }
304
+ #
305
+ attr_accessor :completion_proc
306
+
307
+ #
308
+ # Called when any character is pressed with the string.
309
+ #
310
+ # q.change_proc = Proc.new {|str| Dir.glob(str +"*") }
311
+ #
312
+ attr_accessor :change_proc
313
+ #
314
+ # Called when any control-key is pressed, one that we are not handling
315
+ #
316
+ # q.key_handler_proc = Proc.new {|ch| xxxx) }
317
+ #
318
+ attr_accessor :key_handler_proc
319
+
320
+ #
321
+ # text to be shown if user presses M-h
322
+ #
323
+ attr_accessor :helptext
324
+ attr_accessor :color_pair
325
+ attr_accessor :history
326
+
327
+ #
328
+ # Returns the provided _answer_string_ or the default answer for this
329
+ # Question if a default was set and the answer is empty.
330
+ # NOTE: in our case, the user actually edits this value (in highline it
331
+ # is used if user enters blank)
332
+ #
333
+ def answer_or_default( answer_string )
334
+ if answer_string.length == 0 and not @default.nil?
335
+ @default
336
+ else
337
+ answer_string
338
+ end
339
+ end
340
+
341
+ #
342
+ # Called late in the initialization process to build intelligent
343
+ # responses based on the details of this Question object.
344
+ #
345
+ def build_responses( )
346
+ ### WARNING: This code is quasi-duplicated in ###
347
+ ### Menu.update_responses(). Check there too when ###
348
+ ### making changes! ###
349
+ append_default unless default.nil?
350
+ @responses = { :ambiguous_completion =>
351
+ "Ambiguous choice. " +
352
+ "Please choose one of #{@answer_type.inspect}.",
353
+ :ask_on_error =>
354
+ "? ",
355
+ :invalid_type =>
356
+ "You must enter a valid #{@answer_type}.",
357
+ :no_completion =>
358
+ "You must choose one of " +
359
+ "#{@answer_type.inspect}.",
360
+ :not_in_range =>
361
+ "Your answer isn't within the expected range " +
362
+ "(#{expected_range}).",
363
+ :not_valid =>
364
+ "Your answer isn't valid (must match " +
365
+ "#{@validate.inspect})." }.merge(@responses)
366
+ ### WARNING: This code is quasi-duplicated in ###
367
+ ### Menu.update_responses(). Check there too when ###
368
+ ### making changes! ###
369
+ end
370
+
371
+ #
372
+ # Returns the provided _answer_string_ after changing character case by
373
+ # the rules of this Question. Valid settings for whitespace are:
374
+ #
375
+ # +nil+:: Do not alter character case.
376
+ # (Default.)
377
+ # <tt>:up</tt>:: Calls upcase().
378
+ # <tt>:upcase</tt>:: Calls upcase().
379
+ # <tt>:down</tt>:: Calls downcase().
380
+ # <tt>:downcase</tt>:: Calls downcase().
381
+ # <tt>:capitalize</tt>:: Calls capitalize().
382
+ #
383
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
384
+ #
385
+ def change_case( answer_string )
386
+ if [:up, :upcase].include?(@_case)
387
+ answer_string.upcase
388
+ elsif [:down, :downcase].include?(@_case)
389
+ answer_string.downcase
390
+ elsif @_case == :capitalize
391
+ answer_string.capitalize
392
+ else
393
+ answer_string
394
+ end
395
+ end
396
+
397
+ #
398
+ # Transforms the given _answer_string_ into the expected type for this
399
+ # Question. Currently supported conversions are:
400
+ #
401
+ # <tt>[...]</tt>:: Answer must be a member of the passed Array.
402
+ # Auto-completion is used to expand partial
403
+ # answers.
404
+ # <tt>lambda {...}</tt>:: Answer is passed to lambda for conversion.
405
+ # Date:: Date.parse() is called with answer.
406
+ # DateTime:: DateTime.parse() is called with answer.
407
+ # File:: The entered file name is auto-completed in
408
+ # terms of _directory_ + _glob_, opened, and
409
+ # returned.
410
+ # Float:: Answer is converted with Kernel.Float().
411
+ # Integer:: Answer is converted with Kernel.Integer().
412
+ # +nil+:: Answer is left in String format. (Default.)
413
+ # Pathname:: Same as File, save that a Pathname object is
414
+ # returned.
415
+ # String:: Answer is converted with Kernel.String().
416
+ # Regexp:: Answer is fed to Regexp.new().
417
+ # Symbol:: The method to_sym() is called on answer and
418
+ # the result returned.
419
+ # <i>any other Class</i>:: The answer is passed on to
420
+ # <tt>Class.parse()</tt>.
421
+ #
422
+ # This method throws ArgumentError, if the conversion cannot be
423
+ # completed for any reason.
424
+ #
425
+ def convert( answer_string )
426
+ if @answer_type.nil?
427
+ answer_string
428
+ elsif [Float, Integer, String].include?(@answer_type)
429
+ Kernel.send(@answer_type.to_s.to_sym, answer_string)
430
+ elsif @answer_type == Symbol
431
+ answer_string.to_sym
432
+ elsif @answer_type == Regexp
433
+ Regexp.new(answer_string)
434
+ elsif @answer_type.is_a?(Array) or [File, Pathname].include?(@answer_type)
435
+ # cheating, using OptionParser's Completion module
436
+ choices = selection
437
+ #choices.extend(OptionParser::Completion)
438
+ #answer = choices.complete(answer_string)
439
+ answer = choices # bug in completion of optparse
440
+ if answer.nil?
441
+ raise NoAutoCompleteMatch
442
+ end
443
+ if @answer_type.is_a?(Array)
444
+ #answer.last # we don't need this anylonger
445
+ answer_string # we have already selected
446
+ elsif @answer_type == File
447
+ File.open(File.join(@directory.to_s, answer_string))
448
+ else
449
+ #Pathname.new(File.join(@directory.to_s, answer.last))
450
+ Pathname.new(File.join(@directory.to_s, answer_string))
451
+ end
452
+ elsif [Date, DateTime].include?(@answer_type) or @answer_type.is_a?(Class)
453
+ @answer_type.parse(answer_string)
454
+ elsif @answer_type.is_a?(Proc)
455
+ @answer_type[answer_string]
456
+ end
457
+ end
458
+
459
+ # Returns a english explination of the current range settings.
460
+ def expected_range( )
461
+ expected = [ ]
462
+
463
+ expected << "above #{@above}" unless @above.nil?
464
+ expected << "below #{@below}" unless @below.nil?
465
+ expected << "included in #{@in.inspect}" unless @in.nil?
466
+
467
+ case expected.size
468
+ when 0 then ""
469
+ when 1 then expected.first
470
+ when 2 then expected.join(" and ")
471
+ else expected[0..-2].join(", ") + ", and #{expected.last}"
472
+ end
473
+ end
474
+
475
+ # Returns _first_answer_, which will be unset following this call.
476
+ def first_answer( )
477
+ @first_answer
478
+ ensure
479
+ @first_answer = nil
480
+ end
481
+
482
+ # Returns true if _first_answer_ is set.
483
+ def first_answer?( )
484
+ not @first_answer.nil?
485
+ end
486
+
487
+ #
488
+ # Returns +true+ if the _answer_object_ is greater than the _above_
489
+ # attribute, less than the _below_ attribute and included?()ed in the
490
+ # _in_ attribute. Otherwise, +false+ is returned. Any +nil+ attributes
491
+ # are not checked.
492
+ #
493
+ def in_range?( answer_object )
494
+ (@above.nil? or answer_object > @above) and
495
+ (@below.nil? or answer_object < @below) and
496
+ (@in.nil? or @in.include?(answer_object))
497
+ end
498
+
499
+ #
500
+ # Returns the provided _answer_string_ after processing whitespace by
501
+ # the rules of this Question. Valid settings for whitespace are:
502
+ #
503
+ # +nil+:: Do not alter whitespace.
504
+ # <tt>:strip</tt>:: Calls strip(). (Default.)
505
+ # <tt>:chomp</tt>:: Calls chomp().
506
+ # <tt>:collapse</tt>:: Collapses all whitspace runs to a
507
+ # single space.
508
+ # <tt>:strip_and_collapse</tt>:: Calls strip(), then collapses all
509
+ # whitspace runs to a single space.
510
+ # <tt>:chomp_and_collapse</tt>:: Calls chomp(), then collapses all
511
+ # whitspace runs to a single space.
512
+ # <tt>:remove</tt>:: Removes all whitespace.
513
+ #
514
+ # An unrecognized choice (like <tt>:none</tt>) is treated as +nil+.
515
+ #
516
+ # This process is skipped, for single character input.
517
+ #
518
+ def remove_whitespace( answer_string )
519
+ if @whitespace.nil?
520
+ answer_string
521
+ elsif [:strip, :chomp].include?(@whitespace)
522
+ answer_string.send(@whitespace)
523
+ elsif @whitespace == :collapse
524
+ answer_string.gsub(/\s+/, " ")
525
+ elsif [:strip_and_collapse, :chomp_and_collapse].include?(@whitespace)
526
+ result = answer_string.send(@whitespace.to_s[/^[a-z]+/])
527
+ result.gsub(/\s+/, " ")
528
+ elsif @whitespace == :remove
529
+ answer_string.gsub(/\s+/, "")
530
+ else
531
+ answer_string
532
+ end
533
+ end
534
+
535
+ #
536
+ # Returns an Array of valid answers to this question. These answers are
537
+ # only known when _answer_type_ is set to an Array of choices, File, or
538
+ # Pathname. Any other time, this method will return an empty Array.
539
+ #
540
+ def selection( )
541
+ if @answer_type.is_a?(Array)
542
+ @answer_type
543
+ elsif [File, Pathname].include?(@answer_type)
544
+ Dir[File.join(@directory.to_s, @glob)].map do |file|
545
+ File.basename(file)
546
+ end
547
+ else
548
+ [ ]
549
+ end
550
+ end
551
+
552
+ # Stringifies the question to be asked.
553
+ def to_str( )
554
+ @question
555
+ end
556
+
557
+ #
558
+ # Returns +true+ if the provided _answer_string_ is accepted by the
559
+ # _validate_ attribute or +false+ if it's not.
560
+ #
561
+ # It's important to realize that an answer is validated after whitespace
562
+ # and case handling.
563
+ #
564
+ def valid_answer?( answer_string )
565
+ @validate.nil? or
566
+ (@validate.is_a?(Regexp) and answer_string =~ @validate) or
567
+ (@validate.is_a?(Proc) and @validate[answer_string])
568
+ end
569
+
570
+ private
571
+
572
+ #
573
+ # Adds the default choice to the end of question between <tt>|...|</tt>.
574
+ # Trailing whitespace is preserved so the function of HighLine.say() is
575
+ # not affected.
576
+ #
577
+ def append_default( )
578
+ if @question =~ /([\t ]+)\Z/
579
+ @question << "|#{@default}|#{$1}"
580
+ elsif @question == ""
581
+ @question << "|#{@default}| "
582
+ elsif @question[-1, 1] == "\n"
583
+ @question[-2, 0] = " |#{@default}|"
584
+ else
585
+ @question << " |#{@default}|"
586
+ end
587
+ end
588
+ end # class
589
+
590
+ # Menu objects encapsulate all the details of a call to HighLine.choose().
591
+ # Using the accessors and Menu.choice() and Menu.choices(), the block passed
592
+ # to HighLine.choose() can detail all aspects of menu display and control.
593
+ #
594
+ class Menu < Question
595
+ #
596
+ # Create an instance of HighLine::Menu. All customization is done
597
+ # through the passed block, which should call accessors and choice() and
598
+ # choices() as needed to define the Menu. Note that Menus are also
599
+ # Questions, so all that functionality is available to the block as
600
+ # well.
601
+ #
602
+ def initialize( )
603
+ #
604
+ # Initialize Question objects with ignored values, we'll
605
+ # adjust ours as needed.
606
+ #
607
+ super("Ignored", [ ], &nil) # avoiding passing the block along
608
+
609
+ @items = [ ]
610
+ @hidden_items = [ ]
611
+ @help = Hash.new("There's no help for that topic.")
612
+
613
+ @index = :number
614
+ @index_suffix = ". "
615
+ @select_by = :index_or_name
616
+ @flow = :rows
617
+ @list_option = nil
618
+ @header = nil
619
+ @prompt = "? "
620
+ @layout = :list
621
+ @shell = false
622
+ @nil_on_handled = false
623
+
624
+ # Override Questions responses, we'll set our own.
625
+ @responses = { }
626
+ # Context for action code.
627
+ @highline = nil
628
+
629
+ yield self if block_given?
630
+
631
+ init_help if @shell and not @help.empty?
632
+ end
633
+
634
+ #
635
+ # An _index_ to append to each menu item in display. See
636
+ # Menu.index=() for details.
637
+ #
638
+ attr_reader :index
639
+ #
640
+ # The String placed between an _index_ and a menu item. Defaults to
641
+ # ". ". Switches to " ", when _index_ is set to a String (like "-").
642
+ #
643
+ attr_accessor :index_suffix
644
+ #
645
+ # The _select_by_ attribute controls how the user is allowed to pick a
646
+ # menu item. The available choices are:
647
+ #
648
+ # <tt>:index</tt>:: The user is allowed to type the numerical
649
+ # or alphetical index for their selection.
650
+ # <tt>:index_or_name</tt>:: Allows both methods from the
651
+ # <tt>:index</tt> option and the
652
+ # <tt>:name</tt> option.
653
+ # <tt>:name</tt>:: Menu items are selected by typing a portion
654
+ # of the item name that will be
655
+ # auto-completed.
656
+ #
657
+ attr_accessor :select_by
658
+ #
659
+ # This attribute is passed directly on as the mode to HighLine.list() by
660
+ # all the preset layouts. See that method for appropriate settings.
661
+ #
662
+ attr_accessor :flow
663
+ #
664
+ # This setting is passed on as the third parameter to HighLine.list()
665
+ # by all the preset layouts. See that method for details of its
666
+ # effects. Defaults to +nil+.
667
+ #
668
+ attr_accessor :list_option
669
+ #
670
+ # Used by all the preset layouts to display title and/or introductory
671
+ # information, when set. Defaults to +nil+.
672
+ #
673
+ attr_accessor :header
674
+ #
675
+ # Used by all the preset layouts to ask the actual question to fetch a
676
+ # menu selection from the user. Defaults to "? ".
677
+ #
678
+ attr_accessor :prompt
679
+ #
680
+ # An ERb _layout_ to use when displaying this Menu object. See
681
+ # Menu.layout=() for details.
682
+ #
683
+ attr_reader :layout
684
+ #
685
+ # When set to +true+, responses are allowed to be an entire line of
686
+ # input, including details beyond the command itself. Only the first
687
+ # "word" of input will be matched against the menu choices, but both the
688
+ # command selected and the rest of the line will be passed to provided
689
+ # action blocks. Defaults to +false+.
690
+ #
691
+ attr_accessor :shell
692
+ #
693
+ # When +true+, any selected item handled by provided action code, will
694
+ # return +nil+, instead of the results to the action code. This may
695
+ # prove handy when dealing with mixed menus where only the names of
696
+ # items without any code (and +nil+, of course) will be returned.
697
+ # Defaults to +false+.
698
+ #
699
+ attr_accessor :nil_on_handled
700
+
701
+ #
702
+ # Adds _name_ to the list of available menu items. Menu items will be
703
+ # displayed in the order they are added.
704
+ #
705
+ # An optional _action_ can be associated with this name and if provided,
706
+ # it will be called if the item is selected. The result of the method
707
+ # will be returned, unless _nil_on_handled_ is set (when you would get
708
+ # +nil+ instead). In _shell_ mode, a provided block will be passed the
709
+ # command chosen and any details that followed the command. Otherwise,
710
+ # just the command is passed. The <tt>@highline</tt> variable is set to
711
+ # the current HighLine context before the action code is called and can
712
+ # thus be used for adding output and the like.
713
+ #
714
+ def choice( name, help = nil, &action )
715
+ @items << [name, action]
716
+
717
+ @help[name.to_s.downcase] = help unless help.nil?
718
+ update_responses # rebuild responses based on our settings
719
+ end
720
+
721
+ #
722
+ # A shortcut for multiple calls to the sister method choice(). <b>Be
723
+ # warned:</b> An _action_ set here will apply to *all* provided
724
+ # _names_. This is considered to be a feature, so you can easily
725
+ # hand-off interface processing to a different chunk of code.
726
+ #
727
+ def choices( *names, &action )
728
+ names.each { |n| choice(n, &action) }
729
+ end
730
+
731
+ # Identical to choice(), but the item will not be listed for the user.
732
+ def hidden( name, help = nil, &action )
733
+ @hidden_items << [name, action]
734
+
735
+ @help[name.to_s.downcase] = help unless help.nil?
736
+ end
737
+
738
+ #
739
+ # Sets the indexing style for this Menu object. Indexes are appended to
740
+ # menu items, when displayed in list form. The available settings are:
741
+ #
742
+ # <tt>:number</tt>:: Menu items will be indexed numerically, starting
743
+ # with 1. This is the default method of indexing.
744
+ # <tt>:letter</tt>:: Items will be indexed alphabetically, starting
745
+ # with a.
746
+ # <tt>:none</tt>:: No index will be appended to menu items.
747
+ # <i>any String</i>:: Will be used as the literal _index_.
748
+ #
749
+ # Setting the _index_ to <tt>:none</tt> a literal String, also adjusts
750
+ # _index_suffix_ to a single space and _select_by_ to <tt>:none</tt>.
751
+ # Because of this, you should make a habit of setting the _index_ first.
752
+ #
753
+ def index=( style )
754
+ @index = style
755
+
756
+ # Default settings.
757
+ if @index == :none or @index.is_a?(String)
758
+ @index_suffix = " "
759
+ @select_by = :name
760
+ end
761
+ end
762
+
763
+ #
764
+ # Initializes the help system by adding a <tt>:help</tt> choice, some
765
+ # action code, and the default help listing.
766
+ #
767
+ def init_help( )
768
+ return if @items.include?(:help)
769
+
770
+ topics = @help.keys.sort
771
+ help_help = @help.include?("help") ? @help["help"] :
772
+ "This command will display helpful messages about " +
773
+ "functionality, like this one. To see the help for " +
774
+ "a specific topic enter:\n\thelp [TOPIC]\nTry asking " +
775
+ "for help on any of the following:\n\n" +
776
+ "<%= list(#{topics.inspect}, :columns_across) %>"
777
+ choice(:help, help_help) do |command, topic|
778
+ topic.strip!
779
+ topic.downcase!
780
+ if topic.empty?
781
+ @highline.say(@help["help"])
782
+ else
783
+ @highline.say("= #{topic}\n\n#{@help[topic]}")
784
+ end
785
+ end
786
+ end
787
+
788
+ #
789
+ # Used to set help for arbitrary topics. Use the topic <tt>"help"</tt>
790
+ # to override the default message.
791
+ #
792
+ def help( topic, help )
793
+ @help[topic] = help
794
+ end
795
+
796
+ #
797
+ # Setting a _layout_ with this method also adjusts some other attributes
798
+ # of the Menu object, to ideal defaults for the chosen _layout_. To
799
+ # account for that, you probably want to set a _layout_ first in your
800
+ # configuration block, if needed.
801
+ #
802
+ # Accepted settings for _layout_ are:
803
+ #
804
+ # <tt>:list</tt>:: The default _layout_. The _header_ if set
805
+ # will appear at the top on its own line with
806
+ # a trailing colon. Then the list of menu
807
+ # items will follow. Finally, the _prompt_
808
+ # will be used as the ask()-like question.
809
+ # <tt>:one_line</tt>:: A shorter _layout_ that fits on one line.
810
+ # The _header_ comes first followed by a
811
+ # colon and spaces, then the _prompt_ with menu
812
+ # items between trailing parenthesis.
813
+ # <tt>:menu_only</tt>:: Just the menu items, followed up by a likely
814
+ # short _prompt_.
815
+ # <i>any ERb String</i>:: Will be taken as the literal _layout_. This
816
+ # String can access <tt>@header</tt>,
817
+ # <tt>@menu</tt> and <tt>@prompt</tt>, but is
818
+ # otherwise evaluated in the typical HighLine
819
+ # context, to provide access to utilities like
820
+ # HighLine.list() primarily.
821
+ #
822
+ # If set to either <tt>:one_line</tt>, or <tt>:menu_only</tt>, _index_
823
+ # will default to <tt>:none</tt> and _flow_ will default to
824
+ # <tt>:inline</tt>.
825
+ #
826
+ def layout=( new_layout )
827
+ @layout = new_layout
828
+
829
+ # Default settings.
830
+ case @layout
831
+ when :one_line, :menu_only
832
+ self.index = :none
833
+ @flow = :inline
834
+ end
835
+ end
836
+
837
+ #
838
+ # This method returns all possible options for auto-completion, based
839
+ # on the settings of _index_ and _select_by_.
840
+ #
841
+ def options( )
842
+ # add in any hidden menu commands
843
+ @items.concat(@hidden_items)
844
+
845
+ by_index = if @index == :letter
846
+ l_index = "`"
847
+ @items.map { "#{l_index.succ!}" }
848
+ else
849
+ (1 .. @items.size).collect { |s| String(s) }
850
+ end
851
+ by_name = @items.collect { |c| c.first }
852
+
853
+ case @select_by
854
+ when :index then
855
+ by_index
856
+ when :name
857
+ by_name
858
+ else
859
+ by_index + by_name
860
+ end
861
+ ensure
862
+ # make sure the hidden items are removed, before we return
863
+ @items.slice!(@items.size - @hidden_items.size, @hidden_items.size)
864
+ end
865
+
866
+ #
867
+ # This method processes the auto-completed user selection, based on the
868
+ # rules for this Menu object. If an action was provided for the
869
+ # selection, it will be executed as described in Menu.choice().
870
+ #
871
+ def select( highline_context, selection, details = nil )
872
+ # add in any hidden menu commands
873
+ @items.concat(@hidden_items)
874
+
875
+ # Find the selected action.
876
+ name, action = if selection =~ /^\d+$/
877
+ @items[selection.to_i - 1]
878
+ else
879
+ l_index = "`"
880
+ index = @items.map { "#{l_index.succ!}" }.index(selection)
881
+ $log.debug "iindex #{index}, #{@items} " if $log.debug?
882
+ @items.find { |c| c.first == selection } or @items[index]
883
+ end
884
+
885
+ # Run or return it.
886
+ if not @nil_on_handled and not action.nil?
887
+ @highline = highline_context
888
+ if @shell
889
+ action.call(name, details)
890
+ else
891
+ action.call(name)
892
+ end
893
+ elsif action.nil?
894
+ name
895
+ else
896
+ nil
897
+ end
898
+ ensure
899
+ # make sure the hidden items are removed, before we return
900
+ @items.slice!(@items.size - @hidden_items.size, @hidden_items.size)
901
+ end
902
+
903
+ #
904
+ # Allows Menu objects to pass as Arrays, for use with HighLine.list().
905
+ # This method returns all menu items to be displayed, complete with
906
+ # indexes.
907
+ #
908
+ def to_ary( )
909
+ case @index
910
+ when :number
911
+ @items.map { |c| "#{@items.index(c) + 1}#{@index_suffix}#{c.first}" }
912
+ when :letter
913
+ l_index = "`"
914
+ @items.map { |c| "#{l_index.succ!}#{@index_suffix}#{c.first}" }
915
+ when :none
916
+ @items.map { |c| "#{c.first}" }
917
+ else
918
+ @items.map { |c| "#{index}#{@index_suffix}#{c.first}" }
919
+ end
920
+ end
921
+
922
+ #
923
+ # Allows Menu to behave as a String, just like Question. Returns the
924
+ # _layout_ to be rendered, which is used by HighLine.say().
925
+ #
926
+ def to_str( )
927
+ case @layout
928
+ when :list
929
+ '<%= if @header.nil? then '' else "#{@header}:\n" end %>' +
930
+ "<%= list( @menu, #{@flow.inspect},
931
+ #{@list_option.inspect} ) %>" +
932
+ "<%= @prompt %>"
933
+ when :one_line
934
+ '<%= if @header.nil? then '' else "#{@header}: " end %>' +
935
+ "<%= @prompt %>" +
936
+ "(<%= list( @menu, #{@flow.inspect},
937
+ #{@list_option.inspect} ) %>)" +
938
+ "<%= @prompt[/\s*$/] %>"
939
+ when :menu_only
940
+ "<%= list( @menu, #{@flow.inspect},
941
+ #{@list_option.inspect} ) %><%= @prompt %>"
942
+ else
943
+ @layout
944
+ end
945
+ end
946
+
947
+ #
948
+ # This method will update the intelligent responses to account for
949
+ # Menu specific differences. This overrides the work done by
950
+ # Question.build_responses().
951
+ #
952
+ def update_responses( )
953
+ append_default unless default.nil?
954
+ @responses = @responses.merge(
955
+ :ambiguous_completion =>
956
+ "Ambiguous choice. " +
957
+ "Please choose one of #{options.inspect}.",
958
+ :ask_on_error =>
959
+ "? ",
960
+ :invalid_type =>
961
+ "You must enter a valid #{options}.",
962
+ :no_completion =>
963
+ "You must choose one of " +
964
+ "#{options.inspect}.",
965
+ :not_in_range =>
966
+ "Your answer isn't within the expected range " +
967
+ "(#{expected_range}).",
968
+ :not_valid =>
969
+ "Your answer isn't valid (must match " +
970
+ "#{@validate.inspect})."
971
+ )
972
+ end
973
+ end
974
+ def ask(question, answer_type=String, &details)
975
+ $log.debug "XXXX inside ask win #{@window} "
976
+ @window ||= _create_footer_window
977
+ #@window.show #unless @window.visible?
978
+
979
+ @question ||= Question.new(question, answer_type, &details)
980
+ say(@question) #unless @question.echo == true
981
+
982
+ @completion_proc = @question.completion_proc
983
+ @change_proc = @question.change_proc
984
+ @key_handler_proc = @question.key_handler_proc
985
+ @default = @question.default
986
+ $log.debug "XXX: ASK RBGETS got default: #{@default} "
987
+ @helptext = @question.helptext
988
+ @answer_type = @question.answer_type
989
+ if @question.answer_type.is_a? Array
990
+ @completion_proc = Proc.new{|str| @answer_type.dup.grep Regexp.new("^#{str}") }
991
+ end
992
+
993
+ begin
994
+ # FIXME a C-c still returns default to user !
995
+ @answer = @question.answer_or_default(get_response)
996
+ unless @question.valid_answer?(@answer)
997
+ explain_error(:not_valid)
998
+ raise QuestionError
999
+ end
1000
+
1001
+ @answer = @question.convert(@answer)
1002
+
1003
+ if @question.in_range?(@answer)
1004
+ if @question.confirm
1005
+ # need to add a layer of scope to ask a question inside a
1006
+ # question, without destroying instance data
1007
+ context_change = self.class.new(@input, @output, @wrap_at, @page_at)
1008
+ if @question.confirm == true
1009
+ confirm_question = "Are you sure? "
1010
+ else
1011
+ # evaluate ERb under initial scope, so it will have
1012
+ # access to @question and @answer
1013
+ template = ERB.new(@question.confirm, nil, "%")
1014
+ confirm_question = template.result(binding)
1015
+ end
1016
+ unless context_change.agree(confirm_question)
1017
+ explain_error(nil)
1018
+ raise QuestionError
1019
+ end
1020
+ end
1021
+
1022
+ @answer
1023
+ else
1024
+ explain_error(:not_in_range)
1025
+ raise QuestionError
1026
+ end
1027
+ rescue QuestionError
1028
+ retry
1029
+ rescue ArgumentError, NameError => error
1030
+ #raise
1031
+ raise if error.is_a?(NoMethodError)
1032
+ if error.message =~ /ambiguous/
1033
+ # the assumption here is that OptionParser::Completion#complete
1034
+ # (used for ambiguity resolution) throws exceptions containing
1035
+ # the word 'ambiguous' whenever resolution fails
1036
+ explain_error(:ambiguous_completion)
1037
+ else
1038
+ explain_error(:invalid_type)
1039
+ end
1040
+ retry
1041
+ rescue Question::NoAutoCompleteMatch
1042
+ explain_error(:no_completion)
1043
+ retry
1044
+ rescue Interrupt
1045
+ $log.warn "User interrupted ask() get_response does not want operation to proceed"
1046
+ return nil
1047
+ ensure
1048
+ @question = nil # Reset Question object.
1049
+ $log.debug "XXX: HIDE B AT ENSURE OF ASK"
1050
+ hide_bottomline # assuming this method made it visible, not sure if this is called.
1051
+ end
1052
+ end
1053
+ #
1054
+ # bottomline user has to hide window if he called say().
1055
+ # Call this if you find the window persists after using some method from here
1056
+ # usually say or ask.
1057
+ #
1058
+ # NOTE: after callign this you must call window.show. Otherwise, next time
1059
+ # you call this, it will not hide.
1060
+ #
1061
+ # @param [int, float] time to sleep before hiding window.
1062
+ #
1063
+ def hide wait=nil
1064
+ if @window
1065
+ $log.debug "XXX: HIDE BOTTOMLINE INSIDE"
1066
+ sleep(wait) if wait
1067
+ #if @window.visible?
1068
+ #@window.hide # THIS HAS SUDDENLY STOPPED WORKING
1069
+ @window.destroy
1070
+ @window = nil
1071
+ #@window.wrefresh
1072
+ #Ncurses::Panel.update_panels
1073
+ #end
1074
+ end
1075
+ end
1076
+ alias :hide_bottomline :hide
1077
+ #
1078
+ # destroy window, to be called by app when shutting down
1079
+ # since we are normally hiding the window only.
1080
+ def destroy
1081
+ $log.debug "bottomline destroy... #{@window} "
1082
+ @window.destroy if @window
1083
+ @window = nil
1084
+ end
1085
+
1086
+ #
1087
+ # The basic output method for HighLine objects.
1088
+ #
1089
+ # The _statement_ parameter is processed as an ERb template, supporting
1090
+ # embedded Ruby code. The template is evaluated with a binding inside
1091
+ # the HighLine instance.
1092
+ # NOTE: modified from original highline, does not care about space at end of
1093
+ # question. Also, ansi color constants will not work. Be careful what ruby code
1094
+ # you pass in.
1095
+ #
1096
+ # NOTE: This uses a window, so it will persist in the last row. You must call
1097
+ # hide_bottomline to remove the window. It is preferable to call say_with_pause
1098
+ # from user programs
1099
+ #
1100
+ def say statement, config={}
1101
+ @window ||= _create_footer_window
1102
+ #@window.show #unless @window.visible?
1103
+ $log.debug "XXX: inside say win #{@window} !"
1104
+ case statement
1105
+ when Question
1106
+
1107
+ if config.has_key? :color_pair
1108
+ $log.debug "INSIDE QUESTION 2 " if $log.debug?
1109
+ else
1110
+ $log.debug "XXXX SAY using colorpair: #{statement.color_pair} " if $log.debug?
1111
+ config[:color_pair] = statement.color_pair
1112
+ end
1113
+ else
1114
+ $log.debug "XXX INSDIE SAY #{statement.class} " if $log.debug?
1115
+ end
1116
+ statement = statement.to_str
1117
+ template = ERB.new(statement, nil, "%")
1118
+ statement = template.result(binding)
1119
+
1120
+ @prompt_length = statement.length # required by ask since it prints after
1121
+ @statement = statement #
1122
+ clear_line
1123
+ print_str statement, config
1124
+ end
1125
+ #
1126
+ # display some text at bottom and wait for a key before hiding window
1127
+ #
1128
+ def say_with_pause statement, config={}
1129
+ @window ||= _create_footer_window
1130
+ #@window.show #unless @window.visible? # 2011-10-14 23:52:52
1131
+ say statement, config
1132
+ @window.wrefresh
1133
+ Ncurses::Panel.update_panels
1134
+ ch=@window.getchar()
1135
+ hide_bottomline
1136
+ end
1137
+ # since say does not leave the screen, it is not exactly recommended
1138
+ # as it will hide what's below. It's better to call pause, or this, which
1139
+ # will quickly go off. If the message is not important enough to ask for a pause,
1140
+ # the will flicker on screen, but not for too long.
1141
+ def say_with_wait statement, config={}
1142
+ @window ||= _create_footer_window
1143
+ #@window.show #unless @window.visible? # 2011-10-14 23:52:59
1144
+ say statement, config
1145
+ @window.wrefresh
1146
+ Ncurses::Panel.update_panels
1147
+ sleep 0.5
1148
+ hide_bottomline
1149
+ end
1150
+ # A helper method for sending the output stream and error and repeat
1151
+ # of the question.
1152
+ #
1153
+ # FIXME: since we write on one line in say, this often gets overidden
1154
+ # by next say or ask
1155
+ def explain_error( error )
1156
+ say_with_pause(@question.responses[error]) unless error.nil?
1157
+ if @question.responses[:ask_on_error] == :question
1158
+ say(@question)
1159
+ elsif @question.responses[:ask_on_error]
1160
+ say(@question.responses[:ask_on_error])
1161
+ end
1162
+ end
1163
+
1164
+ #
1165
+ # Internal method for printing a string
1166
+ #
1167
+ def print_str(text, config={})
1168
+ win = config.fetch(:window, @window) # assuming its in App
1169
+ x = config.fetch :x, 0 # @message_row # Ncurses.LINES-1, 0 since one line window 2011-10-8
1170
+ y = config.fetch :y, 0
1171
+ $log.debug "XXX: print_str #{win} with text : #{text} at #{x} #{y} "
1172
+ color = config[:color_pair] || $datacolor
1173
+ raise "no window for ask print in #{self.class} name: #{name} " unless win
1174
+ color=Ncurses.COLOR_PAIR(color);
1175
+ win.attron(color);
1176
+ #win.mvprintw(x, y, "%-40s" % text);
1177
+ win.mvprintw(x, y, "%s" % text);
1178
+ win.attroff(color);
1179
+ win.refresh # FFI NW 2011-09-9 , added back gets overwritten
1180
+ end
1181
+
1182
+ # actual input routine, gets each character from user, taking care of echo, limit,
1183
+ # completion proc, and some control characters such as C-a, C-e, C-k
1184
+ # Taken from io.rb, has some improvements to it. However, does not print the prompt
1185
+ # any longer
1186
+ # Completion proc is vim style, on pressing tab it cycles through options
1187
+ def rbgetstr
1188
+ r = @message_row
1189
+ c = 0
1190
+ win = @window
1191
+ @limit = @question.limit
1192
+ @history = @question.history
1193
+ @history_list = History.new(@history)
1194
+ maxlen = @limit || 100 # fixme
1195
+
1196
+
1197
+ raise "rbgetstr got no window. bottomline.rb" if win.nil?
1198
+ ins_mode = false
1199
+ oldstr = nil # for tab completion, origal word entered by user
1200
+ default = @default || ""
1201
+ $log.debug "XXX: RBGETS got default: #{@default} "
1202
+ if @default && @history
1203
+ if !@history.include?(default)
1204
+ @history_list.push default
1205
+ end
1206
+ end
1207
+
1208
+ len = @prompt_length
1209
+
1210
+ # clear the area of len+maxlen
1211
+ color = $datacolor
1212
+ str = ""
1213
+ #str = default
1214
+ cpentries = nil
1215
+ #clear_line len+maxlen+1
1216
+ #print_str(prompt+str)
1217
+ print_str(str, :y => @prompt_length+0) if @default
1218
+ len = @prompt_length + str.length
1219
+ begin
1220
+ Ncurses.noecho();
1221
+ curpos = str.length
1222
+ prevchar = 0
1223
+ entries = nil
1224
+ while true
1225
+ ch=win.getchar()
1226
+ $log.debug " XXXX FFI rbgetstr got ch:#{ch}, str:#{str}. "
1227
+ case ch
1228
+ when 3 # -1 # C-c # sometimes this causes an interrupt and crash
1229
+ return -1, nil
1230
+ when ?\C-g.getbyte(0) # ABORT, emacs style
1231
+ return -1, nil
1232
+ when 10, 13 # hits ENTER, complete entry and return
1233
+ @history_list.push str
1234
+ break
1235
+ when ?\C-h.getbyte(0), ?\C-?.getbyte(0), KEY_BSPACE, 263 # delete previous character/backspace
1236
+ # C-h is giving 263 i/o 8. 2011-09-19
1237
+ len -= 1 if len > @prompt_length
1238
+ curpos -= 1 if curpos > 0
1239
+ str.slice!(curpos)
1240
+ clear_line len+maxlen+1, @prompt_length
1241
+ when 330 # delete character on cursor
1242
+ str.slice!(curpos) #rescue next
1243
+ clear_line len+maxlen+1, @prompt_length
1244
+ when ?\M-h.getbyte(0) # HELP KEY
1245
+ helptext = @helptext || "No help provided"
1246
+ print_help(helptext)
1247
+ clear_line len+maxlen+1
1248
+ print_str @statement # UGH
1249
+ #return 7, nil
1250
+ #next
1251
+ when KEY_LEFT
1252
+ curpos -= 1 if curpos > 0
1253
+ len -= 1 if len > @prompt_length
1254
+ win.move r, c+len # since getchar is not going back on del and bs wmove to move FFIWINDOW
1255
+ win.wrefresh
1256
+ next
1257
+ when KEY_RIGHT
1258
+ if curpos < str.length
1259
+ curpos += 1 #if curpos < str.length
1260
+ len += 1
1261
+ win.move r, c+len # since getchar is not going back on del and bs
1262
+ win.wrefresh
1263
+ end
1264
+ next
1265
+ when ?\C-a.getbyte(0)
1266
+ #olen = str.length
1267
+ clear_line len+maxlen+1, @prompt_length
1268
+ len -= curpos
1269
+ curpos = 0
1270
+ win.move r, c+len # since getchar is not going back on del and bs
1271
+ when ?\C-e.getbyte(0)
1272
+ olen = str.length
1273
+ len += (olen - curpos)
1274
+ curpos = olen
1275
+ clear_line len+maxlen+1, @prompt_length
1276
+ win.move r, c+len # since getchar is not going back on del and bs
1277
+
1278
+ when ?\M-i.getbyte(0)
1279
+ ins_mode = !ins_mode
1280
+ next
1281
+ when ?\C-k.getbyte(0) # delete forward
1282
+ @delete_buffer = str.slice!(curpos..-1) #rescue next
1283
+ clear_line len+maxlen+1, @prompt_length
1284
+ when ?\C-u.getbyte(0) # delete to the left of cursor till start of line
1285
+ @delete_buffer = str.slice!(0..curpos-1) #rescue next
1286
+ curpos = 0
1287
+ clear_line len+maxlen+1, @prompt_length
1288
+ len = @prompt_length
1289
+ when ?\C-y.getbyte(0) # paste what's in delete buffer
1290
+ if @delete_buffer
1291
+ olen = str.length
1292
+ str << @delete_buffer if @delete_buffer
1293
+ curpos = str.length
1294
+ len += str.length - olen
1295
+ end
1296
+ when KEY_TAB # TAB
1297
+ if !@completion_proc.nil?
1298
+ # place cursor at end of completion
1299
+ # after all completions, what user entered should come back so he can edit it
1300
+ if prevchar == 9
1301
+ if !entries.nil? and !entries.empty?
1302
+ olen = str.length
1303
+ str = entries.delete_at(0)
1304
+ str = str.to_s.dup
1305
+ #str = entries[@current_index].dup
1306
+ #@current_index += 1
1307
+ #@current_index = 0 if @current_index == entries.length
1308
+ curpos = str.length
1309
+ len += str.length - olen
1310
+ clear_line len+maxlen+1, @prompt_length
1311
+ else
1312
+ olen = str.length
1313
+ str = oldstr if oldstr
1314
+ curpos = str.length
1315
+ len += str.length - olen
1316
+ clear_line len+maxlen+1, @prompt_length
1317
+ prevchar = ch = nil # so it can start again completing
1318
+ end
1319
+ else
1320
+ #@current_index = 0
1321
+ tabc = @completion_proc unless tabc
1322
+ next unless tabc
1323
+ oldstr = str.dup
1324
+ olen = str.length
1325
+ entries = tabc.call(str).dup
1326
+ $log.debug "XXX tab [#{str}] got #{entries} "
1327
+ str = entries.delete_at(0) unless entries.nil? or entries.empty?
1328
+ #str = entries[@current_index].dup unless entries.nil? or entries.empty?
1329
+ #@current_index += 1
1330
+ #@current_index = 0 if @current_index == entries.length
1331
+ str = str.to_s.dup
1332
+ if str
1333
+ curpos = str.length
1334
+ len += str.length - olen
1335
+ else
1336
+ alert "NO MORE 2"
1337
+ end
1338
+ end
1339
+ else
1340
+ # there's another type of completion that bash does, which is irritating
1341
+ # compared to what vim does, it does partial completion
1342
+ if cpentries
1343
+ olen = str.length
1344
+ if cpentries.size == 1
1345
+ str = cpentries.first.dup
1346
+ elsif cpentries.size > 1
1347
+ str = shortest_match(cpentries).dup
1348
+ end
1349
+ curpos = str.length
1350
+ len += str.length - olen
1351
+ end
1352
+ end
1353
+ when ?\C-a.getbyte(0) .. ?\C-z.getbyte(0)
1354
+ # here' swhere i wish i could pass stuff back without closing
1355
+ # I'd like the user to be able to scroll list or do something based on
1356
+ # control or other keys
1357
+ if @key_handler_proc # added 2011-11-3 7:38 PM
1358
+ @key_handler_proc.call(ch)
1359
+ next
1360
+ else
1361
+ Ncurses.beep
1362
+ end
1363
+ when KEY_UP
1364
+ if @history && !@history.empty?
1365
+ olen = str.length
1366
+ str = if prevchar == KEY_UP
1367
+ @history_list.previous
1368
+ elsif prevchar == KEY_DOWN
1369
+ @history_list.previous
1370
+ else
1371
+ @history_list.last
1372
+ end
1373
+ str = str.dup
1374
+ curpos = str.length
1375
+ len += str.length - olen
1376
+ clear_line len+maxlen+1, @prompt_length
1377
+ else # try to pick up default, seems we don't get it 2011-10-14
1378
+ if @default
1379
+ olen = str.length
1380
+ str = @default
1381
+ str = str.dup
1382
+ curpos = str.length
1383
+ len += str.length - olen
1384
+ clear_line len+maxlen+1, @prompt_length
1385
+ end
1386
+ end
1387
+ when KEY_DOWN
1388
+ if @history && !@history.empty?
1389
+ olen = str.length
1390
+ str = if prevchar == KEY_UP
1391
+ @history_list.next
1392
+ elsif prevchar == KEY_DOWN
1393
+ @history_list.next
1394
+ else
1395
+ @history_list.first
1396
+ end
1397
+ str = str.dup
1398
+ curpos = str.length
1399
+ len += str.length - olen
1400
+ clear_line len+maxlen+1, @prompt_length
1401
+ end
1402
+
1403
+ else
1404
+ if ch < 0 || ch > 255
1405
+ Ncurses.beep
1406
+ next
1407
+ end
1408
+ # if control char, beep
1409
+ if ch.chr =~ /[[:cntrl:]]/
1410
+ Ncurses.beep
1411
+ next
1412
+ end
1413
+ # we need to trap KEY_LEFT and RIGHT and what of UP for history ?
1414
+ if ins_mode
1415
+ str[curpos] = ch.chr
1416
+ else
1417
+ str.insert(curpos, ch.chr) # FIXME index out of range due to changeproc
1418
+ end
1419
+ len += 1
1420
+ curpos += 1
1421
+ break if str.length >= maxlen
1422
+ end
1423
+ case @question.echo
1424
+ when true
1425
+ begin
1426
+ cpentries = @change_proc.call(str) if @change_proc # added 2010-11-09 23:28
1427
+ rescue => exc
1428
+ $log.error "bottomline: change_proc EXC #{exc} " if $log.debug?
1429
+ $log.error( exc) if exc
1430
+ $log.error(exc.backtrace.join("\n")) if exc
1431
+ Ncurses.error
1432
+ end
1433
+ print_str(str, :y => @prompt_length+0)
1434
+ when false
1435
+ # noop, no echoing what is typed
1436
+ else
1437
+ print_str(@question.echo * str.length, :y => @prompt_length+0)
1438
+ end
1439
+ win.move r, c+len # more for arrow keys, curpos may not be end
1440
+ win.wrefresh # 2011-10-10
1441
+ prevchar = ch
1442
+ end
1443
+ $log.debug "XXXW bottomline: after while loop"
1444
+
1445
+ str = default if str == ""
1446
+ ensure
1447
+ Ncurses.noecho();
1448
+ end
1449
+ return 0, str
1450
+ end
1451
+
1452
+ # compares entries in array and returns longest common starting string
1453
+ # as happens in bash when pressing tab
1454
+ # abc abd abe will return ab
1455
+ def shortest_match a
1456
+ #return "" if a.nil? || a.empty? # should not be called in such situations
1457
+ raise "shortest_match should not be called with nil or empty array" if a.nil? || a.empty? # should not be called in such situations as caller program will err.
1458
+
1459
+ l = a.inject do |memo,word|
1460
+ str = ""
1461
+ 0.upto(memo.size) do |i|
1462
+ if memo[0..i] == word[0..i]
1463
+ str = memo[0..i]
1464
+ else
1465
+ break
1466
+ end
1467
+ end
1468
+ str
1469
+ end
1470
+ end
1471
+ # clears line from 0, not okay in some cases
1472
+ def clear_line len=100, from=0
1473
+ print_str("%-*s" % [len," "], :y => from)
1474
+ end
1475
+
1476
+ def print_help(helptext)
1477
+ # best to popup a window and hsow that with ENTER to dispell
1478
+ print_str("%-*s" % [helptext.length+2," "])
1479
+ print_str("%s" % helptext)
1480
+ sleep(5)
1481
+ end
1482
+ def get_response
1483
+ return @question.first_answer if @question.first_answer?
1484
+ # we always use character reader, so user's value does not matter
1485
+
1486
+ #if @question.character.nil?
1487
+ # if @question.echo == true #and @question.limit.nil?
1488
+ $log.debug "XXX: before RBGETS got default: #{@default} "
1489
+ ret, str = rbgetstr
1490
+ if ret == 0
1491
+ return @question.change_case(@question.remove_whitespace(str))
1492
+ end
1493
+ if ret == -1
1494
+ raise Interrupt
1495
+ end
1496
+ return ""
1497
+ end
1498
+ def agree( yes_or_no_question, character = nil )
1499
+ ask(yes_or_no_question, lambda { |yn| yn.downcase[0] == ?y}) do |q|
1500
+ q.validate = /\Ay(?:es)?|no?\Z/i
1501
+ q.responses[:not_valid] = 'Please enter "yes" or "no".'
1502
+ q.responses[:ask_on_error] = :question
1503
+ q.character = character
1504
+ q.limit = 1 if character
1505
+
1506
+ yield q if block_given?
1507
+ end
1508
+ end
1509
+
1510
+ # presents given list in numbered format in a window above last line
1511
+ # and accepts input on last line
1512
+ # The list is a list of strings. e.g.
1513
+ # %w{ ruby perl python haskell }
1514
+ # Multiple levels can be given as:
1515
+ # list = %w{ ruby perl python haskell }
1516
+ # list[0] = %w{ ruby ruby1.9 ruby 1.8 rubinius jruby }
1517
+ # In this case, "ruby" is the first level option. The others are used
1518
+ # in the second level. This might make it clearer. first3 has 2 choices under it.
1519
+ # [ "first1" , "first2", ["first3", "second1", "second2"], "first4"]
1520
+ #
1521
+ # Currently, we return an array containing each selected level
1522
+ #
1523
+ # @return [Array] selected option/s from list
1524
+ def numbered_menu list1, config={}
1525
+ if list1.nil? || list1.empty?
1526
+ say_with_pause "empty list passed to numbered_menu"
1527
+ return nil
1528
+ end
1529
+ prompt = config[:prompt] || "Select one: "
1530
+ require 'rbcurse/core/util/rcommandwindow'
1531
+ layout = { :height => 5, :width => Ncurses.COLS-1, :top => Ncurses.LINES-6, :left => 0 }
1532
+ rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title]
1533
+ w = rc.window
1534
+ # should we yield rc, so user can bind keys or whatever
1535
+ # attempt a loop so we do levels.
1536
+ retval = []
1537
+ begin
1538
+ while true
1539
+ rc.display_menu list1, :indexing => :number
1540
+ ret = ask(prompt, Integer ) { |q| q.in = 1..list1.size }
1541
+ val = list1[ret-1]
1542
+ if val.is_a? Array
1543
+ retval << val[0]
1544
+ $log.debug "NL: #{retval} "
1545
+ list1 = val[1..-1]
1546
+ rc.clear
1547
+ else
1548
+ retval << val
1549
+ $log.debug "NL1: #{retval} "
1550
+ break
1551
+ end
1552
+ end
1553
+ ensure
1554
+ rc.destroy
1555
+ rc = nil
1556
+ end
1557
+ #list1[ret-1]
1558
+ $log.debug "NL2: #{retval} , #{retval.class} "
1559
+ retval
1560
+ end
1561
+ # Allows a selection in which options are shown over prompt. As user types
1562
+ # options are narrowed down.
1563
+ # NOTE: For a directory we are not showing a slash, so currently you
1564
+ # have to enter the slash manually when searching.
1565
+ # FIXME we can put remarks in fron as in memacs such as [No matches] or [single completion]
1566
+ # @param [Array] a list of items to select from
1567
+ # NOTE: if you use this please copy it to your app. This does not conform to highline's
1568
+ # choose, and I'd like to somehow get it to be identical.
1569
+ #
1570
+ def choose list1, config={}
1571
+ dirlist = true
1572
+ start = 0
1573
+ case list1
1574
+ when NilClass
1575
+ #list1 = Dir.glob("*")
1576
+ list1 = Dir.glob("*").collect { |f| File.directory?(f) ? f+"/" : f }
1577
+ when String
1578
+ list1 = Dir.glob(list1).collect { |f| File.directory?(f) ? f+"/" : f }
1579
+ when Array
1580
+ dirlist = false
1581
+ # let it be, that's how it should come
1582
+ else
1583
+ # Dir listing as default
1584
+ #list1 = Dir.glob("*")
1585
+ list1 = Dir.glob("*").collect { |f| File.directory?(f) ? f+"/" : f }
1586
+ end
1587
+ require 'rbcurse/core/util/rcommandwindow'
1588
+ prompt = config[:prompt] || "Choose: "
1589
+ layout = { :height => 5, :width => Ncurses.COLS-1, :top => Ncurses.LINES-6, :left => 0 }
1590
+ rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title]
1591
+ begin
1592
+ w = rc.window
1593
+ rc.display_menu list1
1594
+ # earlier wmove bombed, now move is (window.rb 121)
1595
+ str = ask(prompt) { |q| q.change_proc = Proc.new { |str| w.wmove(1,1) ; w.wclrtobot;
1596
+
1597
+ l = list1.select{|e| e.index(str)==0} ; # select those starting with str
1598
+
1599
+ if (l.size == 0 || str[-1]=='/') && dirlist
1600
+ # used to help complete directories so we can drill up and down
1601
+ #l = Dir.glob(str+"*")
1602
+ l = Dir.glob(str +"*").collect { |f| File.directory?(f) ? f+"/" : f }
1603
+ end
1604
+ rc.display_menu l;
1605
+ l
1606
+ }
1607
+ q.key_handler_proc = Proc.new { |ch|
1608
+ # this is not very good since it does not respect above list which is filtered
1609
+ # # need to clear the screen before printing - FIXME
1610
+ case ch
1611
+ when ?\C-n.getbyte(0)
1612
+ start += 2 if start < list1.length - 2
1613
+
1614
+ w.wmove(1,1) ; w.wclrtobot;
1615
+ rc.display_menu list1, :startindex => start
1616
+ when ?\C-p.getbyte(0)
1617
+ start -= 2 if start > 2
1618
+ w.wmove(1,1) ; w.wclrtobot;
1619
+ rc.display_menu list1, :startindex => start
1620
+ else
1621
+ alert "unhalderlind by jey "
1622
+ end
1623
+
1624
+ }
1625
+ }
1626
+ # need some validation here that its in the list TODO
1627
+ ensure
1628
+ rc.destroy
1629
+ rc = nil
1630
+ $log.debug "XXX: HIDE B IN ENSURE"
1631
+ hide_bottomline # since we called ask() we need to close bottomline
1632
+ end
1633
+ $log.debug "XXX: HIDE B AT END OF ASK"
1634
+ #hide_bottomline # since we called ask() we need to close bottomline
1635
+ return str
1636
+ end
1637
+ def display_text_interactive text, config={}
1638
+ require 'rbcurse/core/util/rcommandwindow'
1639
+ ht = config[:height] || 15
1640
+ layout = { :height => ht, :width => Ncurses.COLS-1, :top => Ncurses.LINES-ht+1, :left => 0 }
1641
+ rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title]
1642
+ w = rc.window
1643
+ #rc.text "There was a quick brown fox who ran over the lazy dog and then went over the moon over and over again and again"
1644
+ rc.display_interactive(text) { |l|
1645
+ l.focussed_attrib = 'bold' # Ncurses::A_UNDERLINE
1646
+ l.focussed_symbol = '>'
1647
+ }
1648
+ rc = nil
1649
+ end
1650
+ #def display_list_interactive text, config={}
1651
+ # returns a ListObject since you may not know what the list itself contained
1652
+ # You can do ret.list[ret.current_index] to get value
1653
+ def display_list text, config={}
1654
+ require 'rbcurse/core/util/rcommandwindow'
1655
+ ht = config[:height] || 15
1656
+ layout = { :height => ht, :width => Ncurses.COLS-1, :top => Ncurses.LINES-ht+1, :left => 0 }
1657
+ rc = CommandWindow.new nil, :layout => layout, :box => true, :title => config[:title]
1658
+ w = rc.window
1659
+ ret = rc.display_interactive text
1660
+ rc = nil
1661
+ ret
1662
+ end
1663
+ #
1664
+ # This method is HighLine's menu handler. For simple usage, you can just
1665
+ # pass all the menu items you wish to display. At that point, choose() will
1666
+ # build and display a menu, walk the user through selection, and return
1667
+ # their choice amoung the provided items. You might use this in a case
1668
+ # statement for quick and dirty menus.
1669
+ #
1670
+ # However, choose() is capable of much more. If provided, a block will be
1671
+ # passed a HighLine::Menu object to configure. Using this method, you can
1672
+ # customize all the details of menu handling from index display, to building
1673
+ # a complete shell-like menuing system. See HighLine::Menu for all the
1674
+ # methods it responds to.
1675
+ #
1676
+ # Raises EOFError if input is exhausted.
1677
+ #
1678
+ def XXXchoose( *items, &details )
1679
+ @menu = @question = Menu.new(&details)
1680
+ @menu.choices(*items) unless items.empty?
1681
+
1682
+ # Set _answer_type_ so we can double as the Question for ask().
1683
+ @menu.answer_type = if @menu.shell
1684
+ lambda do |command| # shell-style selection
1685
+ first_word = command.to_s.split.first || ""
1686
+
1687
+ options = @menu.options
1688
+ options.extend(OptionParser::Completion)
1689
+ answer = options.complete(first_word)
1690
+
1691
+ if answer.nil?
1692
+ raise Question::NoAutoCompleteMatch
1693
+ end
1694
+
1695
+ [answer.last, command.sub(/^\s*#{first_word}\s*/, "")]
1696
+ end
1697
+ else
1698
+ @menu.options # normal menu selection, by index or name
1699
+ end
1700
+
1701
+ # Provide hooks for ERb layouts.
1702
+ @header = @menu.header
1703
+ @prompt = @menu.prompt
1704
+
1705
+ if @menu.shell
1706
+ selected = ask("Ignored", @menu.answer_type)
1707
+ @menu.select(self, *selected)
1708
+ else
1709
+ selected = ask("Ignored", @menu.answer_type)
1710
+ @menu.select(self, selected)
1711
+ end
1712
+ end
1713
+
1714
+ # Each member of the _items_ Array is passed through ERb and thus can contain
1715
+ # their own expansions. Color escape expansions do not contribute to the
1716
+ # final field width.
1717
+ #
1718
+ def list( items, mode = :rows, option = nil )
1719
+ items = items.to_ary.map do |item|
1720
+ ERB.new(item, nil, "%").result(binding)
1721
+ end
1722
+
1723
+ case mode
1724
+ when :inline
1725
+ option = " or " if option.nil?
1726
+
1727
+ case items.size
1728
+ when 0
1729
+ ""
1730
+ when 1
1731
+ items.first
1732
+ when 2
1733
+ "#{items.first}#{option}#{items.last}"
1734
+ else
1735
+ items[0..-2].join(", ") + "#{option}#{items.last}"
1736
+ end
1737
+ when :columns_across, :columns_down
1738
+ max_length = actual_length(
1739
+ items.max { |a, b| actual_length(a) <=> actual_length(b) }
1740
+ )
1741
+
1742
+ if option.nil?
1743
+ limit = @wrap_at || 80
1744
+ option = (limit + 2) / (max_length + 2)
1745
+ end
1746
+
1747
+ items = items.map do |item|
1748
+ pad = max_length + (item.length - actual_length(item))
1749
+ "%-#{pad}s" % item
1750
+ end
1751
+ row_count = (items.size / option.to_f).ceil
1752
+
1753
+ if mode == :columns_across
1754
+ rows = Array.new(row_count) { Array.new }
1755
+ items.each_with_index do |item, index|
1756
+ rows[index / option] << item
1757
+ end
1758
+
1759
+ rows.map { |row| row.join(" ") + "\n" }.join
1760
+ else
1761
+ columns = Array.new(option) { Array.new }
1762
+ items.each_with_index do |item, index|
1763
+ columns[index / row_count] << item
1764
+ end
1765
+
1766
+ list = ""
1767
+ columns.first.size.times do |index|
1768
+ list << columns.map { |column| column[index] }.
1769
+ compact.join(" ") + "\n"
1770
+ end
1771
+ list
1772
+ end
1773
+ else
1774
+ items.map { |i| "#{i}\n" }.join
1775
+ end
1776
+ end
1777
+ end # module
1778
+ end # module
1779
+ if __FILE__ == $PROGRAM_NAME
1780
+
1781
+ #tabc = Proc.new {|str| Dir.glob(str +"*") }
1782
+ require 'rbcurse/core/util/app'
1783
+ require 'forwardable'
1784
+ #include Bottomline
1785
+
1786
+ #$tt = Bottomline.new
1787
+ #module Kernel
1788
+ #extend Forwardable
1789
+ #def_delegators :$tt, :ask, :say, :agree, :choose, :numbered_menu
1790
+ #end
1791
+ App.new do
1792
+ header = app_header "rbcurse 1.2.0", :text_center => "**** Demo", :text_right =>"New Improved!", :color => :black, :bgcolor => :white, :attr => :bold
1793
+ message "Press F1 to exit from here"
1794
+
1795
+ #stack :margin_top => 2, :margin => 5, :width => 30 do
1796
+ #end # stack
1797
+ #-----------------#------------------
1798
+
1799
+ #choose do |menu|
1800
+ #menu.prompt = "Please choose your favorite programming language? "
1801
+ ##menu.layout = :one_line
1802
+ #
1803
+ #menu.choice :ruby do say("Good choice!") end
1804
+ #menu.choice(:python) do say("python Not from around here, are you?") end
1805
+ #menu.choice(:perl) do say("perl Not from around here, are you?") end
1806
+ #menu.choice(:rake) do say("rake Not from around here, are you?") end
1807
+ #end
1808
+ entry = {}
1809
+ entry[:file] = ask("File? ", Pathname) do |q|
1810
+ q.completion_proc = Proc.new {|str| Dir.glob(str +"*") }
1811
+ q.helptext = "Enter start of filename and tab to get completion"
1812
+ end
1813
+ alert "file: #{entry[:file]} "
1814
+ $log.debug "FILE: #{entry[:file]} "
1815
+ entry[:command] = ask("Command? ", %w{archive delete read refresh delete!})
1816
+ exit unless agree("Wish to continue? ", false)
1817
+ entry[:address] = ask("Address? ") { |q| q.color_pair = $promptcolor }
1818
+ entry[:company] = ask("Company? ") { |q| q.default = "none" }
1819
+ entry[:password] = ask("password? ") { |q|
1820
+ q.echo = '*'
1821
+ q.limit = 4
1822
+ }
1823
+ =begin
1824
+ entry[:state] = ask("State? ") do |q|
1825
+ q._case = :up
1826
+ q.validate = /\A[A-Z]{2}\Z/
1827
+ q.helptext = "Enter 2 characters for your state"
1828
+ end
1829
+ entry[:zip] = ask("Zip? ") do |q|
1830
+ q.validate = /\A\d{5}(?:-?\d{4})?\Z/
1831
+ end
1832
+ entry[:phone] = ask( "Phone? ",
1833
+ lambda { |p| p.delete("^0-9").
1834
+ sub(/\A(\d{3})/, '(\1) ').
1835
+ sub(/(\d{4})\Z/, '-\1') } ) do |q|
1836
+ q.validate = lambda { |p| p.delete("^0-9").length == 10 }
1837
+ q.responses[:not_valid] = "Enter a phone numer with area code."
1838
+ end
1839
+ entry[:age] = ask("Age? ", Integer) { |q| q.in = 0..105 }
1840
+ entry[:birthday] = ask("Birthday? ", Date)
1841
+ entry[:interests] = ask( "Interests? (comma separated list) ",
1842
+ lambda { |str| str.split(/,\s*/) } )
1843
+ entry[:description] = ask("Enter a description for this contact.") do |q|
1844
+ q.whitespace = :strip_and_collapse
1845
+ end
1846
+ =end
1847
+ $log.debug "ENTRY: #{entry} " if $log.debug?
1848
+ #puts entry
1849
+ end # app
1850
+ end # FILE