rbhex-core 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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