rbcurse-core 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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