clack 0.1.1 → 0.1.2
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 +7 -0
- data/lib/clack/core/prompt.rb +9 -1
- data/lib/clack/core/settings.rb +1 -0
- data/lib/clack/prompts/autocomplete.rb +1 -0
- data/lib/clack/prompts/autocomplete_multiselect.rb +1 -0
- data/lib/clack/prompts/confirm.rb +1 -0
- data/lib/clack/prompts/group_multiselect.rb +1 -0
- data/lib/clack/prompts/multiline_text.rb +200 -0
- data/lib/clack/prompts/multiselect.rb +1 -0
- data/lib/clack/prompts/password.rb +1 -0
- data/lib/clack/prompts/path.rb +1 -0
- data/lib/clack/prompts/select.rb +1 -0
- data/lib/clack/prompts/select_key.rb +1 -0
- data/lib/clack/prompts/text.rb +1 -0
- data/lib/clack/validators.rb +11 -0
- data/lib/clack/version.rb +1 -1
- data/lib/clack.rb +14 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3035b53cb615be8f42827389fc118990457a06e20e9a118c9eb77a97ff5352f8
|
|
4
|
+
data.tar.gz: 98b12f8e74d475a50987a10c719f231050d83484989acd101c4e03bf11ad4b5e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 770e87fe78a09c35ed8150aaee98dbc75ccadb8d23ba39a2b2ba684e00e5cf50b536e1cc0bce00aee9e9aaf22baa2dad566c75c0f64d7b80d799a70009bd2057
|
|
7
|
+
data.tar.gz: f77bf005240ce8533d1b1110bcd430409847fa9263682df3b7362c72b599570a77ad9eb2fad31bbc8690599348bdbd026a05733d5dad357aaa9824120382c8f5
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.2]
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- `multiline_text` prompt for multi-line input (Ctrl+D to submit)
|
|
9
|
+
- `help:` option on all prompts for contextual help text
|
|
10
|
+
- Documentation for blocking validation (database/API calls work out of the box)
|
|
11
|
+
|
|
5
12
|
## [0.1.1]
|
|
6
13
|
|
|
7
14
|
### Fixed
|
data/lib/clack/core/prompt.rb
CHANGED
|
@@ -65,11 +65,13 @@ module Clack
|
|
|
65
65
|
attr_reader :error_message
|
|
66
66
|
|
|
67
67
|
# @param message [String] the prompt message to display
|
|
68
|
+
# @param help [String, nil] optional help text shown below the message
|
|
68
69
|
# @param validate [Proc, nil] optional validation proc; returns error string or nil
|
|
69
70
|
# @param input [IO] input stream (default: $stdin)
|
|
70
71
|
# @param output [IO] output stream (default: $stdout)
|
|
71
|
-
def initialize(message:, validate: nil, input: $stdin, output: $stdout)
|
|
72
|
+
def initialize(message:, help: nil, validate: nil, input: $stdin, output: $stdout)
|
|
72
73
|
@message = message
|
|
74
|
+
@help = help
|
|
73
75
|
@validate = validate
|
|
74
76
|
@input = input
|
|
75
77
|
@output = output
|
|
@@ -246,6 +248,12 @@ module Clack
|
|
|
246
248
|
(@state == :error) ? Colors.yellow(Symbols::S_BAR_END) : Colors.gray(Symbols::S_BAR_END)
|
|
247
249
|
end
|
|
248
250
|
|
|
251
|
+
def help_line
|
|
252
|
+
return "" unless @help
|
|
253
|
+
|
|
254
|
+
"#{bar} #{Colors.dim(@help)}\n"
|
|
255
|
+
end
|
|
256
|
+
|
|
249
257
|
def cursor_block
|
|
250
258
|
Colors.inverse(" ")
|
|
251
259
|
end
|
data/lib/clack/core/settings.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Clack
|
|
|
10
10
|
KEY_BACKSPACE = "\b" # ASCII 8: Backspace
|
|
11
11
|
KEY_DELETE = "\u007F" # ASCII 127: Delete (often sent by backspace key)
|
|
12
12
|
KEY_CTRL_C = "\u0003" # ASCII 3: Ctrl+C (interrupt)
|
|
13
|
+
KEY_CTRL_D = "\u0004" # ASCII 4: Ctrl+D (EOF, used for multiline submit)
|
|
13
14
|
KEY_ESCAPE = "\e" # ASCII 27: Escape
|
|
14
15
|
KEY_ENTER = "\r" # ASCII 13: Carriage return
|
|
15
16
|
KEY_NEWLINE = "\n" # ASCII 10: Line feed
|
|
@@ -136,6 +136,7 @@ module Clack
|
|
|
136
136
|
lines = []
|
|
137
137
|
lines << "#{bar}\n"
|
|
138
138
|
lines << "#{symbol_for_state} #{@message}\n"
|
|
139
|
+
lines << help_line
|
|
139
140
|
lines << "#{active_bar} #{Colors.dim("Search:")} #{search_input_display}#{match_count}\n"
|
|
140
141
|
|
|
141
142
|
visible_options.each_with_index do |opt, idx|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clack
|
|
4
|
+
module Prompts
|
|
5
|
+
# Multi-line text input prompt with line navigation.
|
|
6
|
+
#
|
|
7
|
+
# Features:
|
|
8
|
+
# - Enter inserts newline, Ctrl+D submits
|
|
9
|
+
# - Arrow key navigation (up/down between lines, left/right within line)
|
|
10
|
+
# - Backspace merges lines when at line start
|
|
11
|
+
# - Validation support
|
|
12
|
+
#
|
|
13
|
+
# @example Basic usage
|
|
14
|
+
# message = Clack.multiline_text(message: "Enter your commit message:")
|
|
15
|
+
#
|
|
16
|
+
# @example With initial value and validation
|
|
17
|
+
# content = Clack.multiline_text(
|
|
18
|
+
# message: "Description:",
|
|
19
|
+
# initial_value: "feat: ",
|
|
20
|
+
# validate: ->(v) { "Required!" if v.strip.empty? }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
23
|
+
class MultilineText < Core::Prompt
|
|
24
|
+
# @param message [String] the prompt message
|
|
25
|
+
# @param initial_value [String, nil] pre-filled editable text (can contain newlines)
|
|
26
|
+
# @param validate [Proc, nil] validation proc returning error string or nil
|
|
27
|
+
# @param opts [Hash] additional options passed to {Core::Prompt}
|
|
28
|
+
def initialize(message:, initial_value: nil, **opts)
|
|
29
|
+
super(message:, **opts)
|
|
30
|
+
@lines = parse_initial_value(initial_value)
|
|
31
|
+
@line_index = @lines.length - 1
|
|
32
|
+
@column = current_line.grapheme_clusters.length
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
def handle_key(key)
|
|
38
|
+
return if terminal_state?
|
|
39
|
+
|
|
40
|
+
@state = :active if @state == :error
|
|
41
|
+
|
|
42
|
+
case key
|
|
43
|
+
when Core::Settings::KEY_CTRL_D
|
|
44
|
+
submit
|
|
45
|
+
when Core::Settings::KEY_ESCAPE, Core::Settings::KEY_CTRL_C
|
|
46
|
+
@state = :cancel
|
|
47
|
+
when Core::Settings::KEY_ENTER, Core::Settings::KEY_NEWLINE
|
|
48
|
+
insert_newline
|
|
49
|
+
else
|
|
50
|
+
handle_input(key)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def handle_input(key)
|
|
55
|
+
case key
|
|
56
|
+
when "\e[A" then move_up
|
|
57
|
+
when "\e[B" then move_down
|
|
58
|
+
when "\e[C" then move_right
|
|
59
|
+
when "\e[D" then move_left
|
|
60
|
+
else handle_text_input(key)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def submit
|
|
65
|
+
@value = @lines.join("\n")
|
|
66
|
+
super
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def build_frame
|
|
70
|
+
lines = []
|
|
71
|
+
lines << "#{bar}\n"
|
|
72
|
+
lines << "#{symbol_for_state} #{@message} #{Colors.dim("(Ctrl+D to submit)")}\n"
|
|
73
|
+
lines << help_line
|
|
74
|
+
|
|
75
|
+
@lines.each_with_index do |line, idx|
|
|
76
|
+
display = (idx == @line_index) ? line_with_cursor(line) : line
|
|
77
|
+
lines << "#{active_bar} #{display}\n"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
lines << "#{bar_end}\n"
|
|
81
|
+
lines[-1] = "#{Colors.yellow(Symbols::S_BAR_END)} #{Colors.yellow(@error_message)}\n" if @state == :error
|
|
82
|
+
|
|
83
|
+
lines.join
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_final_frame
|
|
87
|
+
lines = []
|
|
88
|
+
lines << "#{bar}\n"
|
|
89
|
+
lines << "#{symbol_for_state} #{@message}\n"
|
|
90
|
+
|
|
91
|
+
if @state == :cancel
|
|
92
|
+
@lines.each { |line| lines << "#{bar} #{Colors.strikethrough(Colors.dim(line))}\n" }
|
|
93
|
+
else
|
|
94
|
+
@lines.each { |line| lines << "#{bar} #{Colors.dim(line)}\n" }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
lines.join
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def parse_initial_value(value)
|
|
103
|
+
return [""] if value.nil? || value.empty?
|
|
104
|
+
|
|
105
|
+
value.split("\n", -1) # -1 preserves trailing empty strings
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def current_line
|
|
109
|
+
@lines[@line_index] || ""
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def line_with_cursor(line)
|
|
113
|
+
chars = line.grapheme_clusters
|
|
114
|
+
return cursor_block if chars.empty?
|
|
115
|
+
return "#{line}#{cursor_block}" if @column >= chars.length
|
|
116
|
+
|
|
117
|
+
before = chars[0...@column].join
|
|
118
|
+
current = Colors.inverse(chars[@column])
|
|
119
|
+
after = chars[(@column + 1)..].join
|
|
120
|
+
"#{before}#{current}#{after}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def insert_newline
|
|
124
|
+
chars = current_line.grapheme_clusters
|
|
125
|
+
before = chars[0...@column].join
|
|
126
|
+
after = chars[@column..].join
|
|
127
|
+
|
|
128
|
+
@lines[@line_index] = before
|
|
129
|
+
@lines.insert(@line_index + 1, after)
|
|
130
|
+
@line_index += 1
|
|
131
|
+
@column = 0
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def move_up
|
|
135
|
+
return if @line_index.zero?
|
|
136
|
+
|
|
137
|
+
@line_index -= 1
|
|
138
|
+
clamp_column
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def move_down
|
|
142
|
+
return if @line_index >= @lines.length - 1
|
|
143
|
+
|
|
144
|
+
@line_index += 1
|
|
145
|
+
clamp_column
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def move_left
|
|
149
|
+
return if @column.zero?
|
|
150
|
+
|
|
151
|
+
@column -= 1
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def move_right
|
|
155
|
+
max = current_line.grapheme_clusters.length
|
|
156
|
+
return if @column >= max
|
|
157
|
+
|
|
158
|
+
@column += 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def clamp_column
|
|
162
|
+
max = current_line.grapheme_clusters.length
|
|
163
|
+
@column = [@column, max].min
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def handle_text_input(key)
|
|
167
|
+
return handle_backspace if Core::Settings.backspace?(key)
|
|
168
|
+
return unless Core::Settings.printable?(key)
|
|
169
|
+
|
|
170
|
+
chars = current_line.grapheme_clusters
|
|
171
|
+
chars.insert(@column, key)
|
|
172
|
+
@lines[@line_index] = chars.join
|
|
173
|
+
@column += 1
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def handle_backspace
|
|
177
|
+
if @column.zero?
|
|
178
|
+
return if @line_index.zero?
|
|
179
|
+
|
|
180
|
+
merge_line_up
|
|
181
|
+
else
|
|
182
|
+
chars = current_line.grapheme_clusters
|
|
183
|
+
chars.delete_at(@column - 1)
|
|
184
|
+
@lines[@line_index] = chars.join
|
|
185
|
+
@column -= 1
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def merge_line_up
|
|
190
|
+
return if @line_index.zero?
|
|
191
|
+
|
|
192
|
+
current_content = @lines.delete_at(@line_index)
|
|
193
|
+
@line_index -= 1
|
|
194
|
+
prev_length = current_line.grapheme_clusters.length
|
|
195
|
+
@lines[@line_index] = current_line + current_content
|
|
196
|
+
@column = prev_length
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
data/lib/clack/prompts/path.rb
CHANGED
data/lib/clack/prompts/select.rb
CHANGED
data/lib/clack/prompts/text.rb
CHANGED
data/lib/clack/validators.rb
CHANGED
|
@@ -4,11 +4,22 @@ module Clack
|
|
|
4
4
|
# Built-in validators for common validation patterns.
|
|
5
5
|
# Use these with the `validate:` option on prompts.
|
|
6
6
|
#
|
|
7
|
+
# Validation procs can perform any operation including slow I/O (database
|
|
8
|
+
# lookups, API calls, etc.) - they simply block until complete.
|
|
9
|
+
#
|
|
7
10
|
# @example Using built-in validators
|
|
8
11
|
# Clack.text(message: "Name?", validate: Clack::Validators.required)
|
|
9
12
|
# Clack.text(message: "Email?", validate: Clack::Validators.format(/@/, "Must be an email"))
|
|
10
13
|
# Clack.password(message: "Password?", validate: Clack::Validators.min_length(8))
|
|
11
14
|
#
|
|
15
|
+
# @example Database validation (blocking I/O)
|
|
16
|
+
# Clack.text(
|
|
17
|
+
# message: "Email?",
|
|
18
|
+
# validate: ->(email) {
|
|
19
|
+
# "Already taken" if User.exists?(email: email)
|
|
20
|
+
# }
|
|
21
|
+
# )
|
|
22
|
+
#
|
|
12
23
|
# @example Combining validators
|
|
13
24
|
# Clack.text(
|
|
14
25
|
# message: "Username?",
|
data/lib/clack/version.rb
CHANGED
data/lib/clack.rb
CHANGED
|
@@ -12,6 +12,7 @@ require_relative "clack/core/prompt"
|
|
|
12
12
|
require_relative "clack/core/options_helper"
|
|
13
13
|
require_relative "clack/core/text_input_helper"
|
|
14
14
|
require_relative "clack/prompts/text"
|
|
15
|
+
require_relative "clack/prompts/multiline_text"
|
|
15
16
|
require_relative "clack/prompts/password"
|
|
16
17
|
require_relative "clack/prompts/confirm"
|
|
17
18
|
require_relative "clack/prompts/select"
|
|
@@ -128,6 +129,19 @@ module Clack
|
|
|
128
129
|
Prompts::Text.new(message:, **opts).run
|
|
129
130
|
end
|
|
130
131
|
|
|
132
|
+
# Prompt for multi-line text input.
|
|
133
|
+
#
|
|
134
|
+
# Enter inserts a newline, Ctrl+D submits. Useful for commit messages,
|
|
135
|
+
# notes, or any multi-line content.
|
|
136
|
+
#
|
|
137
|
+
# @param message [String] the prompt message
|
|
138
|
+
# @param initial_value [String, nil] pre-filled editable text (can contain newlines)
|
|
139
|
+
# @param validate [Proc, nil] validation function returning error message or nil
|
|
140
|
+
# @return [String, CANCEL] user input (lines joined with \n) or CANCEL if cancelled
|
|
141
|
+
def multiline_text(message:, **opts)
|
|
142
|
+
Prompts::MultilineText.new(message:, **opts).run
|
|
143
|
+
end
|
|
144
|
+
|
|
131
145
|
# Prompt for password input (masked display).
|
|
132
146
|
#
|
|
133
147
|
# @param message [String] the prompt message
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: clack
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Steve Whittaker
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- lib/clack/prompts/autocomplete_multiselect.rb
|
|
40
40
|
- lib/clack/prompts/confirm.rb
|
|
41
41
|
- lib/clack/prompts/group_multiselect.rb
|
|
42
|
+
- lib/clack/prompts/multiline_text.rb
|
|
42
43
|
- lib/clack/prompts/multiselect.rb
|
|
43
44
|
- lib/clack/prompts/password.rb
|
|
44
45
|
- lib/clack/prompts/path.rb
|