fancy_gets_ex 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c0e7ab096362db3d6666cd7c7d77374dd51c4935
4
+ data.tar.gz: 4f3317beefad8d355245e26d906d663d0be35760
5
+ SHA512:
6
+ metadata.gz: 5f76d4ea4f8a257fa81c59cdaf94f045423b6c4a54e7292b139010b922cb5c8f438d284eea131e2c732da0405e8e1bb1ed47632a27a1869cc47e842d23d2746c
7
+ data.tar.gz: 1b6fe46e9b0c6f850c14ec50c3cbff04b98facbc933aa568102dcadc8e00ab050a566046709e705b51facbb519b4aa8a7e90ebc0ee578fc49b68b66525a748d4
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.0
5
+ before_install: gem install bundler -v 1.12.5
@@ -0,0 +1,49 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, and in the interest of
4
+ fostering an open and welcoming community, we pledge to respect all people who
5
+ contribute through reporting issues, posting feature requests, updating
6
+ documentation, submitting pull requests or patches, and other activities.
7
+
8
+ We are committed to making participation in this project a harassment-free
9
+ experience for everyone, regardless of level of experience, gender, gender
10
+ identity and expression, sexual orientation, disability, personal appearance,
11
+ body size, race, ethnicity, age, religion, or nationality.
12
+
13
+ Examples of unacceptable behavior by participants include:
14
+
15
+ * The use of sexualized language or imagery
16
+ * Personal attacks
17
+ * Trolling or insulting/derogatory comments
18
+ * Public or private harassment
19
+ * Publishing other's private information, such as physical or electronic
20
+ addresses, without explicit permission
21
+ * Other unethical or unprofessional conduct
22
+
23
+ Project maintainers have the right and responsibility to remove, edit, or
24
+ reject comments, commits, code, wiki edits, issues, and other contributions
25
+ that are not aligned to this Code of Conduct, or to ban temporarily or
26
+ permanently any contributor for other behaviors that they deem inappropriate,
27
+ threatening, offensive, or harmful.
28
+
29
+ By adopting this Code of Conduct, project maintainers commit themselves to
30
+ fairly and consistently applying these principles to every aspect of managing
31
+ this project. Project maintainers who do not follow or enforce the Code of
32
+ Conduct may be permanently removed from the project team.
33
+
34
+ This code of conduct applies both within project spaces and in public spaces
35
+ when an individual is representing the project or its community.
36
+
37
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
38
+ reported by contacting a project maintainer at lorint@gmail.com. All
39
+ complaints will be reviewed and investigated and will result in a response that
40
+ is deemed necessary and appropriate to the circumstances. Maintainers are
41
+ obligated to maintain confidentiality with regard to the reporter of an
42
+ incident.
43
+
44
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage],
45
+ version 1.3.0, available at
46
+ [http://contributor-covenant.org/version/1/3/0/][version]
47
+
48
+ [homepage]: http://contributor-covenant.org
49
+ [version]: http://contributor-covenant.org/version/1/3/0/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in fancy_gets.gemspec
4
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Lorin Thwaits
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,123 @@
1
+ ## NOTE
2
+
3
+ This repository is forked from https://github.com/lorint/fancy_gets and contains
4
+ a number of fixes for gets_password functionalityy. Current plan is to retire this repository and corresponding gems as soon as fixes get merged upstream
5
+ (see https://github.com/lorint/fancy_gets/pull/1)
6
+
7
+ # FancyGets
8
+
9
+ This gem exists to banish crusty UX that our users endure at the command line.
10
+
11
+ For far too long we've been stuck with just gets and getc. When prompting the
12
+ user with a list of choices, wouldn't it be nice to have the feel of a < select >
13
+ in HTML? Or to auto-suggest options as they type? Or perhaps offer a password
14
+ entry with asterisks instead of just sitting silent, which confuses many users?
15
+
16
+ Read on.
17
+
18
+ ## Installation
19
+
20
+ Very straightforward ... this simple entry in the Gemfile, after which make sure to run "bundle":
21
+
22
+ ```ruby
23
+ gem 'fancy_gets'
24
+ ```
25
+
26
+ Or have it end up in your /usr/lib/ruby/gems/... folder with:
27
+
28
+ $ gem install fancy_gets
29
+
30
+ And at the top of any CLI app do the require and include:
31
+
32
+ ```ruby
33
+ require 'fancy_gets'
34
+ include FancyGets
35
+ ```
36
+
37
+ And then you can impress all manner of people accustomed to the stark limitations
38
+ of command line apps. Heck, this even makes them fun again.
39
+
40
+ ## gets_list
41
+
42
+ Imagine you have this cool array of beach things. Have the user pick one.
43
+
44
+ ```ruby
45
+ toys = ["Skimboard", "Volleyball", "Kite", "Beach Ball", "Water Gun", "Frisbee"]
46
+ picked_toy = gets_list(toys)
47
+ puts "\nBringing a #{picked_toy} sounds like loads of fun at the beach."
48
+ ```
49
+
50
+ And perhaps a little later you'd like to ask again what they'd like, plus
51
+ give a default of what they had picked before.
52
+
53
+ ```ruby
54
+ new_toy = gets_list(toys, picked_toy)
55
+ puts "\nCool! This time you've brought a #{new_toy}."
56
+ ```
57
+
58
+ If you don't prefer the default > Toy Name < prompts, feel free to have your own
59
+ prefix and suffix applied to choices as the user arrows up and down, and supply
60
+ your own prompt text if you like. This is the full syntax for gets_list, and
61
+ the false indicates it's not doing multiple choice.
62
+
63
+ ```ruby
64
+ another_toy = gets_list(toys, false, nil, "==>", "<== PARTY TIME!", "Use arrows to pick something awesome.")
65
+ puts "\nSo much to love about #{another_toy}."
66
+ ```
67
+
68
+ Another cool thing this allows is to change the color of selected items. You may want
69
+ to check out Michał Kalbarczyk's [colorize gem](https://github.com/fazibear/colorize "Michał loves all things \033") for more info.
70
+
71
+ ```ruby
72
+ another_toy = gets_list(toys, false, nil, "\033[1;31m", "\033[0m <==", "Use arrows to pick something awesome.")
73
+ puts "\nSo much to love about #{another_toy}."
74
+ ```
75
+
76
+ Easy to have multiple choices, and bring back an array. In this case it already
77
+ has chosen the kite and water gun.
78
+
79
+ ```ruby
80
+ picked_toys = gets_list(toys, true, ["Kite", "Water Gun"])
81
+ puts "\nYou've picked #{picked_toys.join(", ")}."
82
+ ```
83
+
84
+ ## gets_auto_suggest
85
+
86
+ Still using the same cool array of things, let's have the user see auto-suggest text
87
+ as they type. As soon as the proper term appears, they can hit ENTER and the full
88
+ string for that item is returned. The search is case and color insensitive.
89
+
90
+ ```ruby
91
+ toys = ["Skimboard", "Volleyball", "Kite", "Beach Ball", "Water Gun", "Frisbee"]
92
+ picked_toy = gets_auto_suggest(toys)
93
+ puts "\nYou chose #{picked_toy}."
94
+ ```
95
+
96
+ And as above, you can offer a default choice. This can be set with a full or partial
97
+ string.
98
+
99
+ ```ruby
100
+ new_toy = gets_auto_suggest(toys, picked_toy[0..2])
101
+ puts "\nChanging it up for #{new_toy}."
102
+ ```
103
+
104
+ ## gets_password
105
+
106
+ The final bit of coolness is a simple guarded password entry. All variables used
107
+ by the gem are local, so after returning a response any plain text which was entered
108
+ does not stick around past a garbage collection.
109
+
110
+ ```ruby
111
+ pwd = gets_password
112
+ puts "\nI think I heard you whisper, \"#{pwd}\"."
113
+ ```
114
+
115
+ This also allows default text to be provided, although I can't easily think of a
116
+ circumstance in which that's useful. But perhaps to you it could be.
117
+
118
+ Bug reports and pull requests are welcome: https://github.com/lorint/fancy_gets.
119
+
120
+
121
+ ## License
122
+
123
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "fancy_gets"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,40 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'fancy_gets/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "fancy_gets_ex"
8
+ spec.version = FancyGets::VERSION
9
+ spec.authors = ["Lorin Thwaits"]
10
+ spec.email = ["lorint@gmail.com"]
11
+
12
+ spec.summary = %q{Enhanced gets with listbox, auto-complete, and password support}
13
+ spec.description = %q{This gem exists to banish crusty UX that our users endure at the command line.
14
+
15
+ For far too long we've been stuck with just gets and getc. When prompting the
16
+ user with a list of choices, wouldn't it be nice to have the feel of a <select>
17
+ in HTML? Or to auto-suggest options as they type? Or perhaps offer a password
18
+ entry with asterisks instead of just sitting silent, which confuses many users?
19
+
20
+ It's all here. Enjoy!}
21
+ spec.homepage = "http://polangeles.com/gems/fancy_gets"
22
+ spec.license = "MIT"
23
+
24
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
25
+ # to allow pushing to a single host or delete this section to allow pushing to any host.
26
+ if spec.respond_to?(:metadata)
27
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
28
+ else
29
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
30
+ end
31
+
32
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ spec.bindir = "exe"
34
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
35
+ spec.require_paths = ["lib"]
36
+
37
+ spec.add_development_dependency "bundler", "~> 1.12"
38
+ spec.add_development_dependency "rake", "~> 10.0"
39
+ spec.add_development_dependency "rspec", "~> 3.0"
40
+ end
@@ -0,0 +1,466 @@
1
+ require "fancy_gets/version"
2
+ require 'io/console'
3
+
4
+ module FancyGets
5
+ def gets_auto_suggest(words = nil, default = "")
6
+ FancyGets.gets_internal_core(false, false, words, default)
7
+ end
8
+
9
+ def gets_password(default = "")
10
+ FancyGets.gets_internal_core(false, true, nil, default)
11
+ end
12
+
13
+ # Show a list of stuff, potentially some highlighted, and allow people to up-down arrow around and pick stuff
14
+ def gets_list(words, is_multiple = false, chosen = nil, prefix = "> ", postfix = " <", info = nil, height = nil)
15
+ on_change = nil
16
+ on_select = nil
17
+ if words.is_a? Hash
18
+ is_multiple = words[:is_multiple] || false
19
+ chosen = words[:chosen]
20
+ prefix = words[:prefix] || "> "
21
+ postfix = words[:postfix] || " <"
22
+ info = words[:info]
23
+ height = words[:height] || nil
24
+ on_change = words[:on_change]
25
+ on_select = words[:on_select]
26
+ words = words[:list]
27
+ else
28
+ # Trying to supply parameters but left out a "true" for is_multiple?
29
+ if is_multiple.is_a?(Enumerable) || is_multiple.is_a?(String) || is_multiple.is_a?(Fixnum)
30
+ chosen = is_multiple
31
+ is_multiple = false
32
+ end
33
+ end
34
+ # Slightly inclined to ditch this in case the things they're choosing really are Enumerable
35
+ is_multiple = true if chosen.is_a?(Enumerable)
36
+ FancyGets.gets_internal_core(true, is_multiple, words, chosen, prefix, postfix, info, height, on_change, on_select)
37
+ end
38
+
39
+ # The internal routine that makes all the magic happen
40
+ def self.gets_internal_core(is_list, is_password, word_objects = nil, chosen = nil, prefix = "> ", postfix = " <", info = nil, height = nil, on_change = nil, on_select = nil)
41
+ # OK -- second parameter, is_password, means is_multiple when is_list is true
42
+ is_multiple = is_list & is_password
43
+ unless word_objects.nil? || is_list
44
+ word_objects.sort! {|wo1, wo2| wo1.to_s <=> wo2.to_s}
45
+ end
46
+ words = word_objects.nil? ? [] : word_objects.map(&:to_s)
47
+ if is_multiple
48
+ chosen ||= [0] unless is_multiple
49
+ else
50
+ if chosen.is_a?(Enumerable)
51
+ # Maybe find first string or object that matches the stuff they have sequenced in the chosen array
52
+ string = chosen.first.to_s
53
+ else
54
+ string = chosen.to_s
55
+ end
56
+ end
57
+ position = 0
58
+ # After tweaking the down arrow code, might work OK with 3 things
59
+ height = words.length unless !height.nil? && height.is_a?(Numeric) && height >= 4
60
+ winheight = IO.console.winsize.first - 3
61
+ height = words.length if height > words.length
62
+ height = winheight if height > winheight
63
+ offset = 0
64
+ sugg = ""
65
+ prev_sugg = ""
66
+
67
+ # gsub causes any color changes to not offset spacing
68
+ uncolor = lambda { |word| word.gsub(/\033\[[0-9;]+m/, "") }
69
+
70
+ max_word_length = words.map{|word| uncolor.call(word).length}.max
71
+
72
+ write_sugg = lambda do
73
+ # Find first word that case-insensitive matches what they've typed
74
+ if string.empty?
75
+ sugg = ""
76
+ else
77
+ sugg = words.select { |word| uncolor.call(word).downcase.start_with? string.downcase }.first || ""
78
+ end
79
+ extra_spaces = uncolor.call(prev_sugg).length - uncolor.call(sugg).length
80
+ extra_spaces = 0 if extra_spaces < 0
81
+ " - #{sugg}#{" " * extra_spaces} #{"\b" * ((uncolor.call(sugg).length + 4 + extra_spaces) + string.length - position)}"
82
+ end
83
+
84
+ pre_length = uncolor.call(prefix).length
85
+ post_length = uncolor.call(postfix).length
86
+ pre_post_length = pre_length + post_length
87
+
88
+ # Used for dropdown select / deselect
89
+ clear_dropdown_info = lambda do
90
+ print "\b" * (uncolor.call(words[position]).length + pre_post_length)
91
+ print (27.chr + 91.chr + 66.chr) * ((height + offset) - position)
92
+ info_length = uncolor.call(info).length
93
+ print " " * info_length + "\b" * info_length
94
+ end
95
+ make_select = lambda do |is_select, is_go_to_front = false, is_end_at_front = false|
96
+ word = words[position]
97
+ print "\b" * (uncolor.call(word).length + pre_post_length) if is_go_to_front
98
+ if is_select
99
+ print "#{prefix}#{word}#{postfix}"
100
+ else
101
+ print "#{" " * pre_length}#{word}#{" " * post_length}"
102
+ end
103
+ print " " * (max_word_length - uncolor.call(words[position]).length)
104
+ print "\b" * (max_word_length - uncolor.call(words[position]).length)
105
+ print "\b" * (uncolor.call(word).length + pre_post_length) if is_end_at_front
106
+ end
107
+
108
+ write_info = lambda do |new_info|
109
+ # Put the response into the info line, as long as it's short enough!
110
+ new_info.gsub!("\n", " ")
111
+ new_info_length = uncolor.call(new_info).length
112
+ console_width = IO.console.winsize.last
113
+ # Might have to trim if it's a little too wide
114
+ new_info = new_info[0...console_width] if console_width < new_info_length
115
+ # Arrow down to the info line
116
+ distance_down = (height + offset) - position
117
+ print (27.chr + 91.chr + 66.chr) * distance_down
118
+ # To start of info line
119
+ word_length = uncolor.call(words[position]).length + pre_post_length
120
+ print "\b" * word_length
121
+ # Write out the new response
122
+ prev_info_length = uncolor.call(info).length
123
+ difference = prev_info_length - new_info_length
124
+ difference = 0 if difference < 0
125
+ print new_info + " " * difference
126
+ info = new_info
127
+ # Go up to where we originated
128
+ print (27.chr + 91.chr + 65.chr) * distance_down
129
+ # Arrow left or right to get to the right spot again
130
+ new_info_length += difference
131
+ print (new_info_length > word_length ? "\b" : (27.chr + 91.chr + 67.chr)) * (new_info_length - word_length).abs
132
+ end
133
+
134
+ handle_on_select = lambda do |focused|
135
+ if on_select.is_a? Proc
136
+ response = on_select.call({chosen: chosen, focused: focused})
137
+ new_info = nil
138
+ if response.is_a? Hash
139
+ chosen = response[:chosen] || chosen
140
+ new_info = response[:info]
141
+ elsif response.is_a? String
142
+ new_info = response
143
+ end
144
+ unless new_info.nil?
145
+ write_info.call(new_info)
146
+ end
147
+ end
148
+ end
149
+
150
+ # **********************************************
151
+ # ******************** DOWN ********************
152
+ # Doesn't work with a height of 3 when there's more than 3 in the list
153
+ # (somehow up arrow can work OK with this)
154
+ arrow_down = lambda do
155
+ if position < words.length - 1
156
+ is_shift = false
157
+ handle_on_select.call(word_objects[position + 1])
158
+ # Now moving down past the bottom of the shown window?
159
+ is_before_end = height + offset < words.length
160
+ if is_before_end && position == (height - 2) + offset
161
+ print "\b" * (uncolor.call(words[position]).length + pre_post_length)
162
+ print (27.chr + 91.chr + 65.chr) * (height - 3)
163
+ if offset == 0
164
+ print (27.chr + 91.chr + 65.chr)
165
+ puts "#{" " * pre_length}#{"↑" * max_word_length}"
166
+ end
167
+ offset += 1
168
+ ((offset + 1)..(offset + (height - 4))).each do |i|
169
+ end_fill = max_word_length - uncolor.call(words[i]).length
170
+ puts (is_multiple && chosen.include?(i)) ? "#{prefix}#{words[i]}#{postfix}#{" " * end_fill}" : "#{" " * pre_length}#{words[i]}#{" " * (end_fill + post_length)}"
171
+ end
172
+ is_shift = true
173
+ end
174
+ make_select.call(chosen.include?(position) && is_shift && is_multiple, true, true) if is_shift || !is_multiple
175
+ w1 = uncolor.call(words[position]).length
176
+ position += 1
177
+ print 27.chr + 91.chr + 66.chr
178
+ if is_shift || !is_multiple
179
+ if is_shift && height + offset == words.length # Go down and write the last one
180
+ print 27.chr + 91.chr + 66.chr
181
+ position += 1
182
+ make_select.call(is_shift && chosen.include?(position), false, true)
183
+ print 27.chr + 91.chr + 65.chr # And back up
184
+ position -= 1
185
+ end
186
+ make_select.call((chosen.include?(position) && is_shift) || !is_multiple)
187
+ else
188
+ w2 = uncolor.call(words[position]).length
189
+ print (w1 > w2 ? "\b" : (27.chr + 91.chr + 67.chr)) * (w1 - w2).abs
190
+ end
191
+ end
192
+ end
193
+
194
+ # **********************************************
195
+ # ********************** UP ********************
196
+ arrow_up = lambda do
197
+ if position > 0
198
+ is_shift = false
199
+ handle_on_select.call(word_objects[position - 1])
200
+ # Now moving up past the top of the shown window?
201
+ if position > 1 && position <= offset + 1 # - (offset > 1 ? 0 : -1)
202
+ print "\b" * (uncolor.call(words[position]).length + pre_post_length)
203
+ offset -= 1
204
+ # Up next to the top, and write the first word over the up arrows
205
+ if offset == 0
206
+ print (27.chr + 91.chr + 65.chr)
207
+ end_fill = max_word_length - uncolor.call(words[0]).length
208
+ puts (is_multiple && chosen.include?(0)) ? "#{prefix}#{words[0]}#{postfix}#{" " * end_fill}" : "#{" " * pre_length}#{words[0]}#{" " * (end_fill + post_length)}"
209
+ end
210
+ ((offset + 1)..(offset + height - 2)).each do |i|
211
+ end_fill = max_word_length - uncolor.call(words[i]).length
212
+ puts ((!is_multiple && i == (offset + 1)) || (is_multiple && chosen.include?(i))) ? "#{prefix}#{words[i]}#{postfix}#{" " * end_fill}" : "#{" " * pre_length}#{words[i]}#{" " * (end_fill + post_length)}"
213
+ end
214
+ if offset == words.length - height - 1
215
+ puts "#{" " * pre_length}#{"↓" * max_word_length}"
216
+ print (27.chr + 91.chr + 65.chr)
217
+ end
218
+ print (27.chr + 91.chr + 65.chr) * (height - 2)
219
+ is_shift = true
220
+ position -= 1
221
+ w1 = -pre_post_length
222
+ else
223
+ make_select.call(chosen.include?(position) && is_shift && is_multiple, true, true) if is_shift || !is_multiple
224
+ w1 = uncolor.call(words[position]).length
225
+ position -= 1
226
+ print 27.chr + 91.chr + 65.chr
227
+ end
228
+ if !is_shift && !is_multiple
229
+ make_select.call(chosen.include?(position) || !is_multiple)
230
+ else
231
+ w2 = uncolor.call(words[position]).length
232
+ print (w1 > w2 ? "\b" : (27.chr + 91.chr + 67.chr)) * (w1 - w2).abs
233
+ end
234
+ end
235
+ end
236
+
237
+ # Initialize everything
238
+ if is_list
239
+ # Maybe confirm the height is adequate by checking out IO.console.winsize
240
+ case chosen.class.name
241
+ when "Fixnum"
242
+ chosen = [chosen]
243
+ when "String"
244
+ if words.include?(chosen)
245
+ chosen = [words.index(chosen)]
246
+ else
247
+ chosen = []
248
+ end
249
+ when "Array"
250
+ chosen.each_with_index do |item, i|
251
+ case item.class.name
252
+ when "String"
253
+ chosen[i] = words.index(item)
254
+ when "Fixnum"
255
+ chosen[i] = nil if item < 0 || item >= words.length
256
+ else
257
+ chosen[i] = word_objects.index(item)
258
+ end
259
+ end
260
+ chosen.select{|item| !item.nil?}.uniq
261
+ else
262
+ if word_objects.include?(chosen)
263
+ chosen = [word_objects.index(chosen)]
264
+ else
265
+ chosen = []
266
+ end
267
+ end
268
+ chosen ||= []
269
+ chosen = [0] if chosen == [] && !is_multiple
270
+ position = chosen.first if chosen.length > 0
271
+ # If there's more options than we can fit at once
272
+ if height < words.length
273
+ # ... put the chosen one a third of the way down the screen
274
+ offset = position - (height / 3)
275
+ offset = words.length - height if offset > words.length - height
276
+ end
277
+
278
+ # **********************************************
279
+ # **********************************************
280
+ # **********************************************
281
+ # **********************************************
282
+ # **********************************************
283
+
284
+ # Scrolled any amount downwards?
285
+ # was: if height < words.length
286
+ puts "#{" " * pre_length}#{"↑" * max_word_length}" if offset > 0
287
+ last_word = (height - 2) + offset + (height + offset < words.length ? 0 : 1)
288
+ # Write all the visible words
289
+ ((offset + (offset > 0 ? 1 : 0))..last_word).each { |i| puts chosen.include?(i) ? "#{prefix}#{words[i]}#{postfix}" : "#{" " * pre_length}#{words[i]}" }
290
+ # Can't fit it all?
291
+ puts "#{" " * pre_length}#{"↓" * max_word_length}" if height + offset < words.length
292
+
293
+ info ||= "Use arrow keys#{is_multiple ? ", spacebar to toggle, and ENTER to save" : " and ENTER to make a choice"}"
294
+ print info + (27.chr + 91.chr + 65.chr) * ((last_word - position) + (height + offset < words.length ? 2 : 1))
295
+ # To end of text on starting line
296
+ info_length = uncolor.call(info).length
297
+ word_length = uncolor.call(words[position]).length + pre_post_length
298
+ print (info_length > word_length ? "\b" : (27.chr + 91.chr + 67.chr)) * (info_length - word_length).abs
299
+ else
300
+ position = string.length
301
+ if is_password
302
+ print "*" * string.length
303
+ else
304
+ print string + write_sugg.call
305
+ end
306
+ end
307
+ loop do
308
+ ch = STDIN.getch
309
+ code = ch.ord
310
+ case code
311
+ when 3 # CTRL-C
312
+ clear_dropdown_info.call if is_list
313
+ # puts "o: #{offset} p: #{position} h: #{height} wl: #{words.length}"
314
+ exit
315
+ when 13 # ENTER
316
+ if is_list
317
+ clear_dropdown_info.call
318
+ else
319
+ print "\n"
320
+ end
321
+ break
322
+ when 27 # ESC -- which means lots of special stuff
323
+ case ch = STDIN.getch.ord
324
+ when 79 # Function keys
325
+ # puts "ESC 79"
326
+ case ch = STDIN.getch.ord
327
+ when 80 #F1
328
+ # puts "F1"
329
+ when 81 #F2
330
+ when 82 #F3
331
+ when 83 #F4
332
+ when 84 #F5
333
+ when 85 #F6
334
+ when 86 #F7
335
+ when 87 #F8
336
+ when 88 #F9
337
+ when 89 #F10
338
+ when 90 #F11
339
+ when 91 #F12
340
+ # puts "F12"
341
+ when 92 #F13
342
+ end
343
+ when 91 # Arrow keys
344
+ case ch = STDIN.getch.ord
345
+ when 68 # Arrow left
346
+ if !is_list && position > 0
347
+ print "\b" # 27.chr + 91.chr + 68.chr
348
+ position -= 1
349
+ end
350
+ when 67 # Arrow right
351
+ if !is_list && position < string.length
352
+ print 27.chr + 91.chr + 67.chr
353
+ position += 1
354
+ end
355
+ when 66 # - down
356
+ if is_list
357
+ arrow_down.call
358
+ end
359
+ when 65 # - up
360
+ if is_list
361
+ arrow_up.call
362
+ end
363
+ when 51 # - Delete forwards?
364
+ else
365
+ # puts "ESC 91 #{ch}"
366
+ end
367
+ else
368
+ # Something wacky?
369
+ # puts "code #{ch} #{STDIN.getch.ord} #{STDIN.getch.ord} #{STDIN.getch.ord} #{STDIN.getch.ord}"
370
+ end
371
+ when 127 # Backspace
372
+ if !is_list && position > 0
373
+ string = string[0...position - 1] + string[position..-1]
374
+ if words.empty?
375
+ position -= 1
376
+ print "\b#{is_password ? "*" * (string.length - position) : string[position..-1]} #{"\b" * (string.length - position + 1)}"
377
+ else
378
+ prev_sugg = sugg
379
+ position -= 1
380
+ print "\b#{string[position..-1]}#{write_sugg.call}"
381
+ end
382
+ end
383
+ when 126 # Delete (forwards)
384
+ if !is_list && position < string.length
385
+ string = string[0...position] + string[position + 1..-1]
386
+ if words.empty?
387
+ print "#{is_password ? "*" * (string.length - position) : string[position..-1]} #{"\b" * (string.length - position + 1)}"
388
+ else
389
+ prev_sugg = sugg
390
+ print "#{string[position..-1]}#{write_sugg.call}"
391
+ end
392
+ end
393
+ else # Insert character
394
+ if is_list
395
+ case ch
396
+ when " "
397
+ if is_multiple
398
+ # Toggle this entry
399
+ does_include = chosen.include?(position)
400
+ is_rejected = false
401
+ if on_change.is_a? Proc
402
+ # Generate what would happen if this change goes through
403
+ if does_include
404
+ new_chosen = chosen - [position]
405
+ else
406
+ new_chosen = chosen + [position]
407
+ end
408
+ chosen_objects = new_chosen.sort.map{|choice| word_objects[choice]}
409
+ response = on_change.call({chosen: chosen_objects, changed: word_objects[position], is_chosen: !does_include})
410
+ new_info = nil
411
+ if response.is_a? Hash
412
+ is_rejected = response[:is_rejected]
413
+ # If they told us exactly what the choices should now be, make that happen
414
+ if !response.nil? && response[:chosen].is_a?(Enumerable)
415
+ chosen = response[:chosen].map {|choice| word_objects.index(choice)}
416
+ is_rejected = true
417
+ end
418
+ new_info = response[:info]
419
+ elsif response.is_a? String
420
+ new_info = response
421
+ end
422
+ unless new_info.nil?
423
+ write_info.call(new_info)
424
+ end
425
+ end
426
+ unless is_rejected
427
+ if does_include
428
+ chosen -= [position]
429
+ else
430
+ chosen += [position]
431
+ end
432
+ make_select.call(!does_include, true)
433
+ end
434
+ else
435
+ # Allows Windows to have a way to at least use single-select lists
436
+ clear_dropdown_info.call
437
+ break
438
+ end
439
+ when "j" # Down
440
+ arrow_down.call
441
+ when "k" # Up
442
+ arrow_up.call
443
+ end
444
+ else
445
+ string = string[0...position] + ch + string[position..-1]
446
+ if words.empty?
447
+ ch = "*" if is_password
448
+ position += 1
449
+ print "#{ch}#{is_password ? "*" * (string.length - position) : string[position..-1]}#{"\b" * (string.length - position)}"
450
+ else
451
+ prev_sugg = sugg
452
+ position += 1
453
+ print "#{ch}#{string[position..-1]}#{write_sugg.call}"
454
+ end
455
+ end
456
+ end
457
+ end
458
+
459
+ if is_list
460
+ # Put chosen stuff in same order as it's listed in the words array
461
+ is_multiple ? chosen.sort.map {|c| word_objects[c] } : word_objects[position]
462
+ else
463
+ sugg.empty? ? string : word_objects[words.index(sugg)]
464
+ end
465
+ end
466
+ end
@@ -0,0 +1,3 @@
1
+ module FancyGets
2
+ VERSION = "0.1.6"
3
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fancy_gets_ex
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.6
5
+ platform: ruby
6
+ authors:
7
+ - Lorin Thwaits
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-03-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.12'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.12'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: |-
56
+ This gem exists to banish crusty UX that our users endure at the command line.
57
+
58
+ For far too long we've been stuck with just gets and getc. When prompting the
59
+ user with a list of choices, wouldn't it be nice to have the feel of a <select>
60
+ in HTML? Or to auto-suggest options as they type? Or perhaps offer a password
61
+ entry with asterisks instead of just sitting silent, which confuses many users?
62
+
63
+ It's all here. Enjoy!
64
+ email:
65
+ - lorint@gmail.com
66
+ executables: []
67
+ extensions: []
68
+ extra_rdoc_files: []
69
+ files:
70
+ - ".gitignore"
71
+ - ".rspec"
72
+ - ".travis.yml"
73
+ - CODE_OF_CONDUCT.md
74
+ - Gemfile
75
+ - LICENSE.txt
76
+ - README.md
77
+ - Rakefile
78
+ - bin/console
79
+ - bin/setup
80
+ - fancy_gets.gemspec
81
+ - lib/fancy_gets.rb
82
+ - lib/fancy_gets/version.rb
83
+ homepage: http://polangeles.com/gems/fancy_gets
84
+ licenses:
85
+ - MIT
86
+ metadata:
87
+ allowed_push_host: https://rubygems.org
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.4.8
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Enhanced gets with listbox, auto-complete, and password support
108
+ test_files: []