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.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +123 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/fancy_gets.gemspec +40 -0
- data/lib/fancy_gets.rb +466 -0
- data/lib/fancy_gets/version.rb +3 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/CODE_OF_CONDUCT.md
ADDED
@@ -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
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -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
|
data/bin/setup
ADDED
data/fancy_gets.gemspec
ADDED
@@ -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
|
data/lib/fancy_gets.rb
ADDED
@@ -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
|
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: []
|