rbcurse 1.1.5 → 1.2.0.pre

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