clack 0.6.1 → 0.7.0
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 +4 -4
- data/CHANGELOG.md +63 -0
- data/README.md +281 -14
- data/examples/migration_from_tty_prompt.rb +13 -4
- data/lib/clack/box.rb +11 -5
- data/lib/clack/colors.rb +9 -4
- data/lib/clack/core/chrome.rb +139 -0
- data/lib/clack/core/ci_mode.rb +6 -3
- data/lib/clack/core/key_reader.rb +78 -18
- data/lib/clack/core/options_helper.rb +43 -6
- data/lib/clack/core/prompt.rb +47 -72
- data/lib/clack/core/settings.rb +87 -6
- data/lib/clack/environment.rb +28 -4
- data/lib/clack/errors.rb +19 -0
- data/lib/clack/log.rb +31 -17
- data/lib/clack/note.rb +13 -6
- data/lib/clack/prompts/autocomplete.rb +13 -12
- data/lib/clack/prompts/autocomplete_multiselect.rb +12 -21
- data/lib/clack/prompts/confirm.rb +30 -12
- data/lib/clack/prompts/date.rb +1 -1
- data/lib/clack/prompts/group_multiselect.rb +51 -27
- data/lib/clack/prompts/multiline_text.rb +6 -6
- data/lib/clack/prompts/multiselect.rb +12 -19
- data/lib/clack/prompts/password.rb +2 -2
- data/lib/clack/prompts/path.rb +14 -3
- data/lib/clack/prompts/range.rb +37 -4
- data/lib/clack/prompts/select.rb +8 -1
- data/lib/clack/prompts/select_key.rb +53 -30
- data/lib/clack/prompts/spinner.rb +198 -17
- data/lib/clack/prompts/tasks.rb +34 -11
- data/lib/clack/prompts/text.rb +2 -2
- data/lib/clack/stream.rb +38 -18
- data/lib/clack/symbols.rb +6 -1
- data/lib/clack/task_log.rb +24 -8
- data/lib/clack/utils.rb +18 -6
- data/lib/clack/validators.rb +136 -7
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +197 -31
- metadata +3 -1
data/lib/clack/core/settings.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Clack
|
|
4
4
|
module Core
|
|
5
|
-
# Global configuration for key bindings, guide bar display,
|
|
5
|
+
# Global configuration for key bindings, guide bar display, keyboard hint
|
|
6
|
+
# display, input classification, and the user-facing cancel/error strings.
|
|
6
7
|
module Settings
|
|
7
8
|
# Navigation and control actions
|
|
8
9
|
ACTIONS = %i[up down left right space enter cancel].freeze
|
|
@@ -16,6 +17,8 @@ module Clack
|
|
|
16
17
|
KEY_ENTER = "\r" # ASCII 13: Carriage return
|
|
17
18
|
KEY_NEWLINE = "\n" # ASCII 10: Line feed
|
|
18
19
|
KEY_SPACE = " " # ASCII 32: Space
|
|
20
|
+
KEY_HOME = "\e[H" # Canonical Home (CSI H)
|
|
21
|
+
KEY_END = "\e[F" # Canonical End (CSI F)
|
|
19
22
|
|
|
20
23
|
# First printable ASCII character (space)
|
|
21
24
|
PRINTABLE_CHAR_MIN = 32
|
|
@@ -37,11 +40,35 @@ module Clack
|
|
|
37
40
|
KEY_CTRL_C => :cancel
|
|
38
41
|
}.freeze
|
|
39
42
|
|
|
43
|
+
# Terminals encode the same key in several ways: SS3 (+ESC O x+) in
|
|
44
|
+
# application cursor mode, and Home/End as +ESC [ H+, +ESC [ 1 ~+ or
|
|
45
|
+
# +ESC [ 7 ~+ depending on the terminal. {KeyReader} folds every variant
|
|
46
|
+
# into the canonical code on the right, so prompts and aliases only ever
|
|
47
|
+
# deal with one code per key.
|
|
48
|
+
KEY_NORMALIZE = {
|
|
49
|
+
"\eOA" => "\e[A",
|
|
50
|
+
"\eOB" => "\e[B",
|
|
51
|
+
"\eOC" => "\e[C",
|
|
52
|
+
"\eOD" => "\e[D",
|
|
53
|
+
"\eOH" => KEY_HOME,
|
|
54
|
+
"\e[1~" => KEY_HOME,
|
|
55
|
+
"\e[7~" => KEY_HOME,
|
|
56
|
+
"\eOF" => KEY_END,
|
|
57
|
+
"\e[4~" => KEY_END,
|
|
58
|
+
"\e[8~" => KEY_END
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Default user-facing strings, overridable via update(messages:) for localization.
|
|
62
|
+
DEFAULT_MESSAGES = {cancel: "Cancelled", error: "Something went wrong"}.freeze
|
|
63
|
+
MESSAGE_KEYS = DEFAULT_MESSAGES.keys.freeze
|
|
64
|
+
|
|
40
65
|
# Global configuration (mutable)
|
|
41
66
|
@config = {
|
|
42
67
|
aliases: ALIASES.dup,
|
|
43
68
|
with_guide: true,
|
|
44
|
-
|
|
69
|
+
show_instructions: true,
|
|
70
|
+
ci_mode: false,
|
|
71
|
+
messages: DEFAULT_MESSAGES
|
|
45
72
|
}
|
|
46
73
|
@config_mutex = Mutex.new
|
|
47
74
|
|
|
@@ -55,34 +82,67 @@ module Clack
|
|
|
55
82
|
# Update global settings
|
|
56
83
|
# @param aliases [Hash, nil] Custom key to action mappings (merged with defaults)
|
|
57
84
|
# @param with_guide [Boolean, nil] Whether to show guide bars
|
|
85
|
+
# @param show_instructions [Boolean, nil] Whether prompts show their keyboard hint footer
|
|
58
86
|
# @param ci_mode [Boolean, Symbol, nil] CI mode: true (always), :auto (detect), false (never)
|
|
87
|
+
# @param messages [Hash{Symbol=>String}, nil] Cancel/error strings, merged with the
|
|
88
|
+
# current values: +{cancel: "Cancelled", error: "Something went wrong"}+
|
|
59
89
|
# @return [Hash] Updated configuration
|
|
60
|
-
|
|
90
|
+
# @raise [ArgumentError] if messages is not a Hash, has an unknown key, or a non-String value
|
|
91
|
+
def update(aliases: nil, with_guide: nil, show_instructions: nil, ci_mode: nil, messages: nil)
|
|
92
|
+
validate_messages(messages) if messages
|
|
61
93
|
@config_mutex.synchronize do
|
|
62
94
|
@config[:aliases] = ALIASES.merge(aliases) if aliases
|
|
63
95
|
@config[:with_guide] = with_guide unless with_guide.nil?
|
|
96
|
+
@config[:show_instructions] = show_instructions unless show_instructions.nil?
|
|
64
97
|
@config[:ci_mode] = ci_mode unless ci_mode.nil?
|
|
98
|
+
# Values are frozen too (-value dedups without to_s), so a caller's
|
|
99
|
+
# mutable String cannot change global state after the fact.
|
|
100
|
+
@config[:messages] = @config[:messages].merge(messages.transform_values { |value| -value }).freeze if messages
|
|
65
101
|
@config.dup
|
|
66
102
|
end
|
|
67
103
|
end
|
|
68
104
|
|
|
105
|
+
# Current cancel/error strings.
|
|
106
|
+
# @return [Hash{Symbol=>String}] frozen
|
|
107
|
+
def messages = @config_mutex.synchronize { @config[:messages] }
|
|
108
|
+
|
|
109
|
+
# Look up one user-facing string.
|
|
110
|
+
# @param key [Symbol] :cancel or :error
|
|
111
|
+
# @return [String]
|
|
112
|
+
# @raise [KeyError] for an unknown key
|
|
113
|
+
def message(key) = messages.fetch(key)
|
|
114
|
+
|
|
69
115
|
# Reset settings to defaults
|
|
70
116
|
def reset!
|
|
71
117
|
@config_mutex.synchronize do
|
|
72
118
|
@config = {
|
|
73
119
|
aliases: ALIASES.dup,
|
|
74
120
|
with_guide: true,
|
|
75
|
-
|
|
121
|
+
show_instructions: true,
|
|
122
|
+
ci_mode: false,
|
|
123
|
+
messages: DEFAULT_MESSAGES
|
|
76
124
|
}
|
|
77
125
|
end
|
|
78
126
|
end
|
|
79
127
|
|
|
80
|
-
#
|
|
128
|
+
# Resolve whether guide bars are shown.
|
|
129
|
+
# @param override [Boolean, nil] per-call value; nil means "use the global setting"
|
|
81
130
|
# @return [Boolean]
|
|
82
|
-
def with_guide?
|
|
131
|
+
def with_guide?(override = nil)
|
|
132
|
+
return override unless override.nil?
|
|
133
|
+
|
|
83
134
|
@config_mutex.synchronize { @config[:with_guide] }
|
|
84
135
|
end
|
|
85
136
|
|
|
137
|
+
# Resolve whether keyboard hint footers are shown.
|
|
138
|
+
# @param override [Boolean, nil] per-call value; nil means "use the global setting"
|
|
139
|
+
# @return [Boolean]
|
|
140
|
+
def show_instructions?(override = nil)
|
|
141
|
+
return override unless override.nil?
|
|
142
|
+
|
|
143
|
+
@config_mutex.synchronize { @config[:show_instructions] }
|
|
144
|
+
end
|
|
145
|
+
|
|
86
146
|
# Look up the action mapped to a key code.
|
|
87
147
|
# @param key [String] key code from {KeyReader}
|
|
88
148
|
# @return [Symbol, nil] the action (:up, :down, :enter, etc.) or nil
|
|
@@ -91,6 +151,13 @@ module Clack
|
|
|
91
151
|
aliases[key] if ACTIONS.include?(aliases[key])
|
|
92
152
|
end
|
|
93
153
|
|
|
154
|
+
# Fold terminal-specific escape sequence variants into one canonical code.
|
|
155
|
+
# @param key [String] key code as assembled by {KeyReader}
|
|
156
|
+
# @return [String] the canonical code (unchanged when no variant matches)
|
|
157
|
+
def normalize_key(key)
|
|
158
|
+
KEY_NORMALIZE.fetch(key, key)
|
|
159
|
+
end
|
|
160
|
+
|
|
94
161
|
# Check if a key is a printable character (handles combining marks and multi-codepoint grapheme clusters)
|
|
95
162
|
def printable?(key)
|
|
96
163
|
key && key.grapheme_clusters.length == 1 && key.ord >= PRINTABLE_CHAR_MIN
|
|
@@ -100,6 +167,20 @@ module Clack
|
|
|
100
167
|
def backspace?(key)
|
|
101
168
|
[KEY_BACKSPACE, KEY_DELETE].include?(key)
|
|
102
169
|
end
|
|
170
|
+
|
|
171
|
+
private
|
|
172
|
+
|
|
173
|
+
# Raises before any state changes so a bad call leaves the config untouched.
|
|
174
|
+
def validate_messages(messages)
|
|
175
|
+
raise ArgumentError, "messages must be a Hash" unless messages.is_a?(Hash)
|
|
176
|
+
|
|
177
|
+
messages.each do |key, value|
|
|
178
|
+
unless MESSAGE_KEYS.include?(key)
|
|
179
|
+
raise ArgumentError, "unknown messages key: #{key.inspect} (expected #{MESSAGE_KEYS.map(&:inspect).join(", ")})"
|
|
180
|
+
end
|
|
181
|
+
raise ArgumentError, "messages[#{key.inspect}] must be a String" unless value.is_a?(String)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
103
184
|
end
|
|
104
185
|
end
|
|
105
186
|
end
|
data/lib/clack/environment.rb
CHANGED
|
@@ -57,12 +57,36 @@ module Clack
|
|
|
57
57
|
ENV["TERM"] == "dumb"
|
|
58
58
|
end
|
|
59
59
|
|
|
60
|
-
# Check if ANSI colors are supported
|
|
61
|
-
#
|
|
60
|
+
# Check if ANSI colors are supported on +output+.
|
|
61
|
+
#
|
|
62
|
+
# Resolution order, first match wins:
|
|
63
|
+
# 1. +FORCE_COLOR=0+ or +FORCE_COLOR=false+ (any case): +false+
|
|
64
|
+
# 2. +NO_COLOR+ set to a non-empty value: +false+ (see https://no-color.org)
|
|
65
|
+
# 3. +FORCE_COLOR+ set to any other non-empty value: +true+, even when
|
|
66
|
+
# +output+ is not a TTY or +TERM=dumb+
|
|
67
|
+
# 4. +output+ is not a TTY: +false+
|
|
68
|
+
# 5. +TERM=dumb+: +false+
|
|
69
|
+
# 6. Otherwise +true+
|
|
70
|
+
#
|
|
71
|
+
# An empty +NO_COLOR+ or +FORCE_COLOR+ is treated as unset. The result is
|
|
72
|
+
# not cached; every call re-reads the environment.
|
|
73
|
+
#
|
|
74
|
+
# {Colors.enabled?}, {Core::Cursor.enabled?} and {Symbols.unicode?} all
|
|
75
|
+
# delegate here, so these variables also control cursor escape sequences
|
|
76
|
+
# and the Unicode/ASCII symbol set (unless +CLACK_UNICODE+ overrides the
|
|
77
|
+
# latter).
|
|
78
|
+
#
|
|
79
|
+
# @param output [IO] Output stream to check (default: $stdout)
|
|
62
80
|
# @return [Boolean]
|
|
63
81
|
def colors_supported?(output = $stdout)
|
|
64
|
-
|
|
65
|
-
|
|
82
|
+
force = ENV["FORCE_COLOR"].to_s
|
|
83
|
+
# The binary copy matters: ENV values carry the locale encoding, and
|
|
84
|
+
# String#casecmp? raises ArgumentError on invalid bytes in that
|
|
85
|
+
# encoding. Symbols calls this at require time, so a stray
|
|
86
|
+
# FORCE_COLOR=$'\xff' would otherwise make `require "clack"` raise.
|
|
87
|
+
return false if force == "0" || force.b.casecmp?("false")
|
|
88
|
+
return false unless ENV["NO_COLOR"].to_s.empty?
|
|
89
|
+
return true unless force.empty?
|
|
66
90
|
return false unless tty?(output)
|
|
67
91
|
return false if dumb_terminal?
|
|
68
92
|
|
data/lib/clack/errors.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clack
|
|
4
|
+
# Raised when a prompt needs keystrokes but its input stream is not an
|
|
5
|
+
# interactive terminal (for example, stdin is a pipe, a file, or /dev/null).
|
|
6
|
+
#
|
|
7
|
+
# Inherits from IOError so existing +rescue IOError+ guards keep working.
|
|
8
|
+
# The message names the fix: run from a terminal, or enable CI mode so
|
|
9
|
+
# prompts auto-submit their defaults.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# begin
|
|
13
|
+
# Clack.text(message: "Name?")
|
|
14
|
+
# rescue Clack::NotATerminalError => e
|
|
15
|
+
# warn e.message
|
|
16
|
+
# exit 1
|
|
17
|
+
# end
|
|
18
|
+
class NotATerminalError < IOError; end
|
|
19
|
+
end
|
data/lib/clack/log.rb
CHANGED
|
@@ -6,6 +6,11 @@ module Clack
|
|
|
6
6
|
# Each method prints a message prefixed with a colored symbol. Multi-line
|
|
7
7
|
# messages are automatically aligned with a continuation bar on subsequent lines.
|
|
8
8
|
#
|
|
9
|
+
# With guides off (+with_guide: false+ or +Clack.update_settings(with_guide: false)+)
|
|
10
|
+
# the level symbol stays on the first line and only the continuation rail goes
|
|
11
|
+
# away, so +log.error+ still looks different from +log.info+ (upstream drops the
|
|
12
|
+
# symbol as well).
|
|
13
|
+
#
|
|
9
14
|
# Accessed via +Clack.log+:
|
|
10
15
|
#
|
|
11
16
|
# @example
|
|
@@ -23,22 +28,26 @@ module Clack
|
|
|
23
28
|
# log levels).
|
|
24
29
|
#
|
|
25
30
|
# @param msg [String] the message to display
|
|
26
|
-
# @param symbol [String, nil] custom prefix symbol (default: gray bar)
|
|
31
|
+
# @param symbol [String, nil] custom prefix symbol (default: gray bar, or nothing with guides off)
|
|
32
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
27
33
|
# @param output [IO] output stream (default: $stdout)
|
|
28
34
|
# @return [void]
|
|
29
35
|
#
|
|
30
36
|
# @example Custom symbol
|
|
31
|
-
# Clack.log.message("Deploying...", symbol: "
|
|
32
|
-
def message(msg = "", symbol: nil, output: $stdout)
|
|
33
|
-
|
|
37
|
+
# Clack.log.message("Deploying...", symbol: "✈")
|
|
38
|
+
def message(msg = "", symbol: nil, with_guide: nil, output: $stdout)
|
|
39
|
+
guide = Core::Settings.with_guide?(with_guide)
|
|
40
|
+
rail = Colors.gray(Symbols::S_BAR)
|
|
41
|
+
first = symbol || (guide ? rail : nil)
|
|
42
|
+
first_prefix = first ? "#{first} " : ""
|
|
43
|
+
next_prefix = guide ? "#{rail} " : ""
|
|
34
44
|
lines = msg.to_s.lines
|
|
35
45
|
|
|
36
46
|
if lines.empty?
|
|
37
|
-
output.puts
|
|
47
|
+
output.puts first.to_s
|
|
38
48
|
else
|
|
39
49
|
lines.each_with_index do |line, idx|
|
|
40
|
-
|
|
41
|
-
output.puts "#{prefix} #{line.chomp}"
|
|
50
|
+
output.puts "#{idx.zero? ? first_prefix : next_prefix}#{line.chomp}"
|
|
42
51
|
end
|
|
43
52
|
end
|
|
44
53
|
end
|
|
@@ -46,47 +55,52 @@ module Clack
|
|
|
46
55
|
# Print an informational message (blue symbol).
|
|
47
56
|
#
|
|
48
57
|
# @param msg [String] the message to display
|
|
58
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
49
59
|
# @param output [IO] output stream (default: $stdout)
|
|
50
60
|
# @return [void]
|
|
51
|
-
def info(msg, output: $stdout)
|
|
52
|
-
message(msg, symbol: Colors.blue(Symbols::S_INFO), output:)
|
|
61
|
+
def info(msg, with_guide: nil, output: $stdout)
|
|
62
|
+
message(msg, symbol: Colors.blue(Symbols::S_INFO), with_guide:, output:)
|
|
53
63
|
end
|
|
54
64
|
|
|
55
65
|
# Print a success message (green symbol).
|
|
56
66
|
#
|
|
57
67
|
# @param msg [String] the message to display
|
|
68
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
58
69
|
# @param output [IO] output stream (default: $stdout)
|
|
59
70
|
# @return [void]
|
|
60
|
-
def success(msg, output: $stdout)
|
|
61
|
-
message(msg, symbol: Colors.green(Symbols::S_SUCCESS), output:)
|
|
71
|
+
def success(msg, with_guide: nil, output: $stdout)
|
|
72
|
+
message(msg, symbol: Colors.green(Symbols::S_SUCCESS), with_guide:, output:)
|
|
62
73
|
end
|
|
63
74
|
|
|
64
75
|
# Print a step completion message (green submit symbol).
|
|
65
76
|
#
|
|
66
77
|
# @param msg [String] the message to display
|
|
78
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
67
79
|
# @param output [IO] output stream (default: $stdout)
|
|
68
80
|
# @return [void]
|
|
69
|
-
def step(msg, output: $stdout)
|
|
70
|
-
message(msg, symbol: Colors.green(Symbols::S_STEP_SUBMIT), output:)
|
|
81
|
+
def step(msg, with_guide: nil, output: $stdout)
|
|
82
|
+
message(msg, symbol: Colors.green(Symbols::S_STEP_SUBMIT), with_guide:, output:)
|
|
71
83
|
end
|
|
72
84
|
|
|
73
85
|
# Print a warning message (yellow symbol).
|
|
74
86
|
#
|
|
75
87
|
# @param msg [String] the message to display
|
|
88
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
76
89
|
# @param output [IO] output stream (default: $stdout)
|
|
77
90
|
# @return [void]
|
|
78
|
-
def warn(msg, output: $stdout)
|
|
79
|
-
message(msg, symbol: Colors.yellow(Symbols::S_WARN), output:)
|
|
91
|
+
def warn(msg, with_guide: nil, output: $stdout)
|
|
92
|
+
message(msg, symbol: Colors.yellow(Symbols::S_WARN), with_guide:, output:)
|
|
80
93
|
end
|
|
81
94
|
alias_method :warning, :warn
|
|
82
95
|
|
|
83
96
|
# Print an error message (red symbol).
|
|
84
97
|
#
|
|
85
98
|
# @param msg [String] the message to display
|
|
99
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
86
100
|
# @param output [IO] output stream (default: $stdout)
|
|
87
101
|
# @return [void]
|
|
88
|
-
def error(msg, output: $stdout)
|
|
89
|
-
message(msg, symbol: Colors.red(Symbols::S_ERROR), output:)
|
|
102
|
+
def error(msg, with_guide: nil, output: $stdout)
|
|
103
|
+
message(msg, symbol: Colors.red(Symbols::S_ERROR), with_guide:, output:)
|
|
90
104
|
end
|
|
91
105
|
end
|
|
92
106
|
end
|
data/lib/clack/note.rb
CHANGED
|
@@ -6,27 +6,33 @@ module Clack
|
|
|
6
6
|
class << self
|
|
7
7
|
# Render a note box to the output stream.
|
|
8
8
|
#
|
|
9
|
+
# With guides on, a rail line precedes the box and the bottom-left corner is
|
|
10
|
+
# a T-connector so the rail continues below. With guides off there is no
|
|
11
|
+
# leading rail and the corner is rounded (upstream behavior).
|
|
12
|
+
#
|
|
9
13
|
# @param message [String] the note content
|
|
10
14
|
# @param title [String, nil] optional title displayed above the box
|
|
15
|
+
# @param with_guide [Boolean, nil] show the guide rail (default: Clack.settings[:with_guide])
|
|
11
16
|
# @param output [IO] output stream (default: $stdout)
|
|
12
17
|
# @return [void]
|
|
13
|
-
def render(message = "", title: nil, output: $stdout)
|
|
18
|
+
def render(message = "", title: nil, with_guide: nil, output: $stdout)
|
|
19
|
+
guide = Core::Settings.with_guide?(with_guide)
|
|
14
20
|
lines = message.to_s.lines.map(&:chomp)
|
|
15
21
|
# Add empty lines at start and end like original
|
|
16
22
|
lines = ["", *lines, ""]
|
|
17
23
|
title_len = Clack::Utils.visible_length(title)
|
|
18
24
|
width = calculate_width(lines, title_len)
|
|
19
25
|
|
|
20
|
-
output.puts Colors.gray(Symbols::S_BAR)
|
|
26
|
+
output.puts Colors.gray(Symbols::S_BAR) if guide
|
|
21
27
|
output.puts build_top_border(title, title_len, width)
|
|
22
28
|
|
|
23
29
|
lines.each do |line|
|
|
24
30
|
pad = width - Clack::Utils.visible_length(line)
|
|
25
31
|
padded = pad.positive? ? line + (" " * pad) : line
|
|
26
|
-
output.puts "#{Colors.gray(Symbols::S_BAR)} #{
|
|
32
|
+
output.puts "#{Colors.gray(Symbols::S_BAR)} #{padded}#{Colors.gray(Symbols::S_BAR)}"
|
|
27
33
|
end
|
|
28
34
|
|
|
29
|
-
output.puts build_bottom_border(width)
|
|
35
|
+
output.puts build_bottom_border(width, guide)
|
|
30
36
|
end
|
|
31
37
|
|
|
32
38
|
private
|
|
@@ -48,9 +54,10 @@ module Clack
|
|
|
48
54
|
end
|
|
49
55
|
end
|
|
50
56
|
|
|
51
|
-
def build_bottom_border(width)
|
|
57
|
+
def build_bottom_border(width, guide)
|
|
52
58
|
border = Symbols::S_BAR_H * (width + 2)
|
|
53
|
-
|
|
59
|
+
corner = guide ? Symbols::S_CONNECT_LEFT : Symbols::S_CORNER_BOTTOM_LEFT
|
|
60
|
+
"#{Colors.gray(corner)}#{Colors.gray(border)}#{Colors.gray(Symbols::S_CORNER_BOTTOM_RIGHT)}"
|
|
54
61
|
end
|
|
55
62
|
end
|
|
56
63
|
end
|
|
@@ -107,23 +107,15 @@ module Clack
|
|
|
107
107
|
end
|
|
108
108
|
|
|
109
109
|
def build_frame
|
|
110
|
-
lines = []
|
|
111
|
-
lines << "#{
|
|
112
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
113
|
-
lines << help_line
|
|
114
|
-
lines << "#{active_bar} #{input_display}\n"
|
|
110
|
+
lines = [frame_header]
|
|
111
|
+
lines << "#{gutter(active_bar)}#{input_display}\n"
|
|
115
112
|
|
|
116
113
|
visible_options.each_with_index do |opt, idx|
|
|
117
114
|
actual_idx = @scroll_offset + idx
|
|
118
|
-
lines << "#{
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
if @state in :error | :warning
|
|
122
|
-
lines.concat(validation_message_lines)
|
|
123
|
-
else
|
|
124
|
-
lines << "#{bar_end}\n"
|
|
115
|
+
lines << "#{gutter(active_bar)}#{option_display(opt, actual_idx == @option_index)}\n"
|
|
125
116
|
end
|
|
126
117
|
|
|
118
|
+
lines << frame_footer
|
|
127
119
|
lines.join
|
|
128
120
|
end
|
|
129
121
|
|
|
@@ -141,6 +133,15 @@ module Clack
|
|
|
141
133
|
|
|
142
134
|
def navigable_items = @filtered
|
|
143
135
|
|
|
136
|
+
# Upstream autocomplete footer
|
|
137
|
+
def keyboard_hints
|
|
138
|
+
[
|
|
139
|
+
key_hint(Symbols::S_ARROWS_UP_DOWN, "to select"),
|
|
140
|
+
key_hint("Enter:", "confirm"),
|
|
141
|
+
key_hint("Type:", "to search")
|
|
142
|
+
]
|
|
143
|
+
end
|
|
144
|
+
|
|
144
145
|
def option_display(opt, active)
|
|
145
146
|
hint = (opt.hint && active) ? Colors.dim(" (#{opt.hint})") : ""
|
|
146
147
|
if active
|
|
@@ -101,27 +101,16 @@ module Clack
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
def build_frame
|
|
104
|
-
lines = []
|
|
105
|
-
lines << "#{
|
|
106
|
-
lines << "#{symbol_for_state} #{@message}\n"
|
|
107
|
-
lines << help_line
|
|
108
|
-
lines << "#{active_bar} #{Colors.dim("Search:")} #{input_display}#{match_count}\n"
|
|
104
|
+
lines = [frame_header]
|
|
105
|
+
lines << "#{gutter(active_bar)}#{Colors.dim("Search:")} #{input_display}#{match_count}\n"
|
|
109
106
|
|
|
110
107
|
visible_options.each_with_index do |opt, idx|
|
|
111
108
|
actual_idx = @scroll_offset + idx
|
|
112
|
-
lines << "#{active_bar}
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
lines << "#{active_bar} #{Colors.yellow("No matches found")}\n" if @filtered.empty? && !@search_text.empty?
|
|
116
|
-
|
|
117
|
-
lines << "#{active_bar} #{keyboard_hints}\n"
|
|
118
|
-
|
|
119
|
-
if @state in :error | :warning
|
|
120
|
-
lines.concat(validation_message_lines)
|
|
121
|
-
else
|
|
122
|
-
lines << "#{bar_end}\n"
|
|
109
|
+
lines << "#{gutter(active_bar)}#{option_display(opt, actual_idx == @option_index)}\n"
|
|
123
110
|
end
|
|
124
111
|
|
|
112
|
+
lines << "#{gutter(active_bar)}#{Colors.yellow("No matches found")}\n" if @filtered.empty? && !@search_text.empty?
|
|
113
|
+
lines << frame_footer
|
|
125
114
|
lines.join
|
|
126
115
|
end
|
|
127
116
|
|
|
@@ -150,12 +139,14 @@ module Clack
|
|
|
150
139
|
Colors.dim(" (#{@filtered.size} match#{"es" unless @filtered.size == 1})")
|
|
151
140
|
end
|
|
152
141
|
|
|
142
|
+
# Upstream autocomplete multiselect footer, minus Tab (Ruby has no Tab-select).
|
|
153
143
|
def keyboard_hints
|
|
154
|
-
|
|
155
|
-
"
|
|
156
|
-
"
|
|
157
|
-
"
|
|
158
|
-
|
|
144
|
+
[
|
|
145
|
+
key_hint(Symbols::S_ARROWS_UP_DOWN, "to navigate"),
|
|
146
|
+
key_hint("Space:", "select"),
|
|
147
|
+
key_hint("Enter:", "confirm"),
|
|
148
|
+
key_hint("Type:", "to search")
|
|
149
|
+
]
|
|
159
150
|
end
|
|
160
151
|
|
|
161
152
|
def update_filtered
|
|
@@ -18,17 +18,31 @@ module Clack
|
|
|
18
18
|
# initial_value: false
|
|
19
19
|
# )
|
|
20
20
|
#
|
|
21
|
+
# @example Stacked layout for long labels
|
|
22
|
+
# Clack.confirm(
|
|
23
|
+
# message: "Overwrite ~/.zshrc?",
|
|
24
|
+
# active: "Yes, back it up and replace it",
|
|
25
|
+
# inactive: "No, keep my existing file",
|
|
26
|
+
# vertical: true
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
21
29
|
class Confirm < Core::Prompt
|
|
22
30
|
# @param message [String] the prompt message
|
|
23
31
|
# @param active [String] label for the "yes" option (default: "Yes")
|
|
24
32
|
# @param inactive [String] label for the "no" option (default: "No")
|
|
25
|
-
# @param initial_value [
|
|
33
|
+
# @param initial_value [Object] initial selection (default: true); coerced to a
|
|
34
|
+
# Boolean, so nil and false select "no" and anything else selects "yes"
|
|
35
|
+
# @param vertical [Boolean] render the two options on separate lines (default: false)
|
|
26
36
|
# @param opts [Hash] additional options passed to {Core::Prompt}
|
|
27
|
-
def initialize(message:, active: "Yes", inactive: "No", initial_value: true, **opts)
|
|
37
|
+
def initialize(message:, active: "Yes", inactive: "No", initial_value: true, vertical: false, **opts)
|
|
28
38
|
super(message:, **opts)
|
|
29
39
|
@active_label = active
|
|
30
40
|
@inactive_label = inactive
|
|
31
|
-
@
|
|
41
|
+
@vertical = vertical
|
|
42
|
+
# Coerce like upstream (`!!opts.initialValue`) so the prompt always
|
|
43
|
+
# returns a real Boolean, even when the caller passes nil from ENV or
|
|
44
|
+
# an options hash.
|
|
45
|
+
@value = initial_value ? true : false
|
|
32
46
|
end
|
|
33
47
|
|
|
34
48
|
protected
|
|
@@ -46,23 +60,27 @@ module Clack
|
|
|
46
60
|
end
|
|
47
61
|
|
|
48
62
|
def build_frame
|
|
49
|
-
"#{frame_header}#{
|
|
63
|
+
"#{frame_header}#{row_prefix}#{options_display}\n#{frame_footer}"
|
|
50
64
|
end
|
|
51
65
|
|
|
52
66
|
def final_display = @value ? @active_label : @inactive_label
|
|
53
67
|
|
|
54
68
|
private
|
|
55
69
|
|
|
70
|
+
# Gutter prefix for an option row. Both the first row and the vertical
|
|
71
|
+
# joiner use it, so the rail colour follows the prompt state in one place.
|
|
72
|
+
# Empty when guides are off, so the vertical joiner collapses to a bare newline.
|
|
73
|
+
def row_prefix = gutter(active_bar)
|
|
74
|
+
|
|
56
75
|
def options_display
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
end
|
|
76
|
+
joiner = @vertical ? "\n#{row_prefix}" : " #{Colors.dim("/")} "
|
|
77
|
+
"#{radio(@active_label, @value)}#{joiner}#{radio(@inactive_label, !@value)}"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def radio(label, selected)
|
|
81
|
+
return "#{Colors.green(Symbols::S_RADIO_ACTIVE)} #{label}" if selected
|
|
64
82
|
|
|
65
|
-
"#{
|
|
83
|
+
"#{Colors.dim(Symbols::S_RADIO_INACTIVE)} #{Colors.dim(label)}"
|
|
66
84
|
end
|
|
67
85
|
end
|
|
68
86
|
end
|
data/lib/clack/prompts/date.rb
CHANGED