fancy_gets_ex 0.1.6

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