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