-
-
Notifications
You must be signed in to change notification settings - Fork 136
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add the ability to specify keys to select list items #152
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -629,7 +629,30 @@ choices = [ | |
] | ||
``` | ||
|
||
You can specify `:key` as an additional option which will be used as short name for selecting the choice via keyboard key press. | ||
You can specify `:key` as an additional option which will be used as short name for selecting the choice via keyboard key press. When used with `:enum`, the key is displayed instead of a number. | ||
|
||
```ruby | ||
choices = [{name: "small", key: "s"}, {name: "medium", key: "m"}, {name: "large", key: "l"}] | ||
prompt.select("What size?", choices, enum: ')') | ||
# => | ||
# What size? (Press ↑/↓ arrow to move and Enter to select) | ||
# ‣ s) small | ||
# m) medium | ||
# l) large | ||
``` | ||
|
||
When providing `:key`, you can override the displayed text with `:key_name` - as in the case of using the escape key: | ||
|
||
|
||
```ruby | ||
choices = [{name: "next", key: "n"}, {name: "previous", key: "p"}, {name: "exit", key: :escape, key_name: "esc"}] | ||
prompt.select("Do what?", choices, enum: ')') | ||
# => | ||
# Do what? (Press ↑/↓ arrow to move and Enter to select) | ||
# ‣ n) next | ||
# p) previous | ||
# esc) exit | ||
``` | ||
|
||
Another way to create menu with choices is using the DSL and the `choice` method. For example, the previous array of choices with hash values can be translated as: | ||
|
||
|
@@ -768,6 +791,30 @@ end | |
# ‣ 3. Jax | ||
``` | ||
|
||
If your choices include the `:key` and (optionally) `:key_name` settings, those values will be displayed instead of numbers. | ||
|
||
```ruby | ||
choices = [{name: "small", key: "s"}, {name: "medium", key: "m"}, {name: "large", key: "l"}] | ||
prompt.select("What size?", choices, enum: ')') | ||
# => | ||
# What size? (Press ↑/↓ arrow to move and Enter to select) | ||
# ‣ s) small | ||
# m) medium | ||
# l) large | ||
``` | ||
|
||
#### 2.6.2.3 `:key_action` | ||
|
||
When using `:enum` or Choice `:key` settings, you can change the keypress behavior. By default, `:key_action` is `:move`: pressing a number key or corresponding `:key` will move the cursor to select the choice. You can also use `key_action: :select` to make a keypress immediately select the item. | ||
|
||
|
||
```ruby | ||
choices = [{name: "small", key: "s"}, {name: "medium", key: "m"}, {name: "large", key: "l"}] | ||
prompt.select("What size?", choices, enum: ')', key_action: :select) | ||
``` | ||
|
||
In the above example, when pressing "l", the "large" option will be immediately selected and the prompt will exit. | ||
Comment on lines
+806
to
+816
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this concept, makes the selection even more powerful! |
||
|
||
#### 2.6.2.3 `:help` | ||
|
||
You can configure help message with `:help` and when to display it with `:show_help` options. The help can be displayed on `start`, `never` or `always`: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,6 +67,13 @@ def self.convert_hash(val) | |
# @api public | ||
attr_reader :key | ||
|
||
# The text to display when this choice is used with :enum (i.e. in List) | ||
# Defaults to :key. Use `nil` or `false` to disable showing the enum for | ||
# this Choice. | ||
# | ||
# @api public | ||
attr_reader :key_name | ||
Comment on lines
+70
to
+75
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In light of the previous comment about |
||
|
||
# The text to display for disabled choice | ||
# | ||
# @api public | ||
|
@@ -76,9 +83,10 @@ def self.convert_hash(val) | |
# | ||
# @api public | ||
def initialize(name, value, **options) | ||
@name = name | ||
@value = value | ||
@key = options[:key] | ||
@name = name | ||
@value = value | ||
@key = options[:key] | ||
@key_name = options[:key_name] || @key | ||
@disabled = options[:disabled].nil? ? false : options[:disabled] | ||
freeze | ||
end | ||
|
@@ -113,7 +121,8 @@ def ==(other) | |
return false unless other.is_a?(self.class) | ||
name == other.name && | ||
value == other.value && | ||
key == other.key | ||
key == other.key && | ||
key_name == other.key_name | ||
end | ||
|
||
# Object string representation | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,14 @@ class List | |
# Allowed keys for filter, along with backspace and canc. | ||
FILTER_KEYS_MATCHER = /\A([[:alnum:]]|[[:punct:]])\Z/.freeze | ||
|
||
# Allowed values for :key_action | ||
# move: move to the choice | ||
# select: select the choice | ||
ALLOWED_KEY_ACTIONS = [ | ||
:move, | ||
:select | ||
] | ||
|
||
# Create instance of TTY::Prompt::List menu. | ||
# | ||
# @param Hash options | ||
|
@@ -36,6 +44,7 @@ def initialize(prompt, **options) | |
@prompt = prompt | ||
@prefix = options.fetch(:prefix) { @prompt.prefix } | ||
@enum = options.fetch(:enum) { nil } | ||
@key_action = options.fetch(:key_action) { :move } | ||
@default = Array(options[:default]) | ||
@choices = Choices.new | ||
@active_color = options.fetch(:active_color) { @prompt.active_color } | ||
|
@@ -108,6 +117,9 @@ def per_page(value) | |
@per_page = value | ||
end | ||
|
||
# Get the number of items per page. | ||
# | ||
# @api public | ||
def page_size | ||
(@per_page || Paginator::DEFAULT_PAGE_SIZE) | ||
end | ||
|
@@ -201,6 +213,7 @@ def choice(*value, &block) | |
else | ||
@choices << value | ||
end | ||
check_choice_consistency(@choices.last) | ||
end | ||
|
||
# Add multiple choices, or return them. | ||
|
@@ -222,7 +235,10 @@ def choices(values = (not_set = true)) | |
end | ||
else | ||
@filter_cache = {} | ||
values.each { |val| @choices << val } | ||
values.each do |val| | ||
@choices << val | ||
check_choice_consistency(@choices.last) | ||
end | ||
end | ||
end | ||
|
||
|
@@ -249,25 +265,35 @@ def enumerate? | |
[email protected]? | ||
end | ||
|
||
def search_choice_in(searchable) | ||
searchable.find { |i| !choices[i - 1].disabled? } | ||
end | ||
|
||
# Handle pressed numeric keys. Used to select/move to enumerated choices. | ||
# | ||
# @api private | ||
def keynum(event) | ||
return unless enumerate? | ||
|
||
value = event.value.to_i | ||
return unless (1..choices.count).cover?(value) | ||
return if choices[value - 1].disabled? | ||
@active = value | ||
@done = true if @key_action == :select | ||
end | ||
|
||
# Select the currently hilighted item. | ||
# | ||
# @api private | ||
def keyenter(*) | ||
@done = true unless choices.empty? | ||
end | ||
alias keyreturn keyenter | ||
alias keyspace keyenter | ||
|
||
def search_choice_in(searchable) | ||
searchable.find { |i| !choices[i - 1].disabled? } | ||
end | ||
|
||
# Move the the selection up. | ||
# | ||
# @api private | ||
def keyup(*) | ||
searchable = (@active - 1).downto(1).to_a | ||
prev_active = search_choice_in(searchable) | ||
|
@@ -285,6 +311,9 @@ def keyup(*) | |
@by_page = false | ||
end | ||
|
||
# Move the selection down. | ||
# | ||
# @api private | ||
def keydown(*) | ||
searchable = ((@active + 1)..choices.length) | ||
next_active = search_choice_in(searchable) | ||
|
@@ -307,6 +336,8 @@ def keydown(*) | |
# | ||
# When the choice on a page is outside of next page range then | ||
# adjust it to the last item, otherwise leave unchanged. | ||
# | ||
# @api private | ||
def keyright(*) | ||
if (@active + page_size) <= @choices.size | ||
searchable = ((@active + page_size)..choices.length) | ||
|
@@ -328,6 +359,7 @@ def keyright(*) | |
end | ||
alias keypage_down keyright | ||
|
||
# @api private | ||
def keyleft(*) | ||
if (@active - page_size) > 0 | ||
searchable = ((@active - page_size)..choices.length) | ||
|
@@ -341,22 +373,42 @@ def keyleft(*) | |
end | ||
alias keypage_up keyleft | ||
|
||
# Handle entered non-numeric characters. When `filterable?`, apply them | ||
# to the filter text. Otherwise, match the input character against Choice | ||
# :key settings. | ||
# | ||
# @api private | ||
def keypress(event) | ||
return unless filterable? | ||
|
||
if event.value =~ FILTER_KEYS_MATCHER | ||
@filter << event.value | ||
@active = 1 | ||
# if filterable, ignore :key? | ||
if filterable? | ||
if event.value =~ FILTER_KEYS_MATCHER | ||
@filter << event.value | ||
@active = 1 | ||
end | ||
else | ||
# check for a matching Choice :key by key 'value', and then by 'name' | ||
# this allows for either an alnum like "a", or a special like :escape | ||
choice = choices.find_by(:key, event.value) || choices.find_by(:key, event.key.name) | ||
if choice | ||
@active = choices.index(choice) + 1 | ||
@done = true if @key_action == :select | ||
end | ||
end | ||
end | ||
|
||
# Remove characters from the filter text. | ||
# | ||
# @api private | ||
def keydelete(*) | ||
return unless filterable? | ||
|
||
@filter.clear | ||
@active = 1 | ||
end | ||
|
||
# Remove characters from the filter text. | ||
# | ||
# @api private | ||
def keybackspace(*) | ||
return unless filterable? | ||
|
||
|
@@ -366,11 +418,30 @@ def keybackspace(*) | |
|
||
private | ||
|
||
# Make sure incompatible options or bad values are not present. | ||
# | ||
# @api private | ||
def check_options_consistency(options) | ||
if options.key?(:enum) && options.key?(:filter) | ||
raise ConfigurationError, | ||
"Enumeration can't be used with filter" | ||
end | ||
|
||
if options.key?(:key_action) && !ALLOWED_KEY_ACTIONS.include?(options[:key_action]) | ||
raise ConfigurationError, | ||
"Invalid :key_action => %p. Must be one of: %s" % | ||
[ options[:key_action], ALLOWED_KEY_ACTIONS.map(&:inspect).join(', ') ] | ||
end | ||
end | ||
|
||
# Make sure settings on a Choice are not incompatible with any options. | ||
# | ||
# @api private | ||
def check_choice_consistency(choice) | ||
if filterable? && choice.key | ||
raise ConfigurationError, | ||
"Filtering can't be used with Choice :key settings" | ||
end | ||
end | ||
|
||
# Setup default option and active selection | ||
|
@@ -536,7 +607,16 @@ def render_menu | |
|
||
sync_paginators if @paging_changed | ||
paginator.paginate(choices, @active, @per_page) do |choice, index| | ||
num = enumerate? ? (index + 1).to_s + @enum + " " : "" | ||
# enumerate by provided :key, :key_name, or index number. | ||
num = "" | ||
if enumerate? | ||
if choice.key && choice.key_name | ||
num = choice.key_name.to_s + @enum + " " | ||
elsif !choice.key | ||
num = (index + 1).to_s + @enum + " " | ||
end | ||
end | ||
|
||
message = if index + 1 == @active && !choice.disabled? | ||
selected = "#{@symbols[:marker]} #{num}#{choice.name}" | ||
@prompt.decorate(selected.to_s, @active_color) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel as though the
:enum
should still act as an enumerated list of items, even when combined with key selection. Potentially the display could resolve this by appending key afterwards like this:There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this could be side-by-side, and would likely be a lot more intuitive (i.e. you expect
:enum
to add the numbers, but for some reason:key
overrides that)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree that this can/should be split up into more manageable chunks. As far as display, the options I see are a) as you said, an appended
[key]
text, or b) underlining, like alt-key hotkey representation in many OSes. The second would be tricky in a situation where you want the:key
to be a letter that is not in the word, or up/downcase version of it (i.e. fish, Fish, fiSh).As for non-
select
usage, I had not considered that to be honest - it should definitely be supported, and I agree that:key_action => select
really only makes sense for select. Maybe there can be a specificMenu
class that works more like an interactive menu system, which is what I intended this for, versus adding that functionality into justselect
.So, you'd like the changes split like this:
Choice
/Choices
changes and related tests., and "check" item(s) in
multi_select`:key_action => select
If that all sounds good to you, I can get 1 & 2 done and we can talk more about 3.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The a) suggestion seems to me a bit more straightforward in a sense that some keys don't directly correspond with letters like
esc
key. I like the idea of underlined letters but I worry about how the support looks in various terminals. Often the underline is not displayed at all. In general the b) option feels a bit more fragile to me.There may be an opportunity to create some common menu abstraction. I definitely want to pursue an idea of splitting up the whole library into plugins. It would remain a single gem but all the menus would rely on common interfaces and hence provide a way for people to create their own prompts or enhance the current ones.
The plan sounds good to me. Could I be cheeky and suggest an item 0? When you made changes to slider prompt, you suggested that the
default
apart from index could accept a name. I like this idea and would extend it toselect
,multi_select
andenum_select
prompts as well. This could be then released with the slider changes. What do you think?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. While it would be cooler looking, it definitely comes with a lot more complication. a) it is.
This would be neat. Making the "prompt" things more modular would be beneficial. If you need help with that, you know how to find me :)
Ah, of course - I forgot about that! I will definitely do that as well!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi Katelyn, it's been a while since we had our last discussion about this feature. In the meantime, I've updated the
default
option for all the prompts and made some changes to theChoice
. I wonder if you have time to revisit adding key support again?