prompts 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e372e3b7b11291f493908b8b18d2430e9b49d11d9807e4db768326717a1890c4
4
- data.tar.gz: accd4c59fc36421be3561c18ad42f45f65d521fe4c21195a7e886e68a1be3e8b
3
+ metadata.gz: 00154e514a6a247088b47d92dd842df0628e33a7b9abf56f31d8a402f6174a16
4
+ data.tar.gz: 65a2a890904ef991dd55e042081efad7762106d63f0039f3db566e9df262c800
5
5
  SHA512:
6
- metadata.gz: 10e4d017de9d2854bd059427db91c193fd1b1c5c7a9c6a901552150bdfa82934469ae00bd0397de49db715e53de0dae1208f65692f17219d389884bdcddb9d1b
7
- data.tar.gz: df6f62ffb22cdd98c29e52b55c5ab02fc2e468828ebea09a41e926f3c800a62a1994e59982ad4ff1a98d1305ea50a0b7fe5bdcd315952996660c5c128d01b99c
6
+ metadata.gz: 609f6e7c90beb7732f2b7853f25dbc44bce3e79ca5acdadd186a1455babea98e1364d2fff40dd380372974702f392774c8fd69b17e14241df8312ebc422e0e44
7
+ data.tar.gz: aec86323aa427982ad95a58e9d4ab2fc73dfa0df358a4b8d88919fbf1c851321a25491a80b49b6618ed44af61ac24f819fc9f9fc55a9691858a41bc349587357
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2024-08-20
4
+
5
+ - Add `TextPrompt`
6
+ - Add `SelectPrompt`
7
+ - Add `ConfirmPrompt`
8
+ - Add `PausePrompt`
9
+ - Add `Form`
10
+
3
11
  ## [0.1.0] - 2024-08-07
4
12
 
5
13
  - Initial release
data/README.md CHANGED
@@ -1,24 +1,260 @@
1
1
  # Prompts
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ Prompts helps you to add beautiful and user-friendly forms to your command-line applications, with browser-like features including label text, help text, validation, and inline errors.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/prompts`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ It was originally inspired by the [Laravel Prompts](https://laravel.com/docs/11.x/prompts) package.
6
6
 
7
7
  ## Installation
8
8
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
10
-
11
9
  Install the gem and add to the application's Gemfile by executing:
12
10
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
11
+ ```bash
12
+ bundle add prompts
13
+ ```
14
14
 
15
15
  If bundler is not being used to manage dependencies, install the gem by executing:
16
16
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
17
+ ```bash
18
+ gem install prompts
19
+ ```
20
+
21
+ ## Philosophy
22
+
23
+ Prompts aims to provide beautiful and user-friendly forms for your command-line applications while keeping both the API and the implementation simple. This means Prompts is built with constraints in mind.
24
+
25
+ In order to minimize complexity, we build on top of the excellent [`reline`](https://github.com/ruby/reline) gem to provide a full-featured text input. Similarly, the text input is **always** rendered at the bottom of the screen. And inputing text is the **only** way to interact with the form.
26
+
27
+ In this way, this new library is similar to [Charm's Huh library](https://github.com/charmbracelet/huh) when used in "accessible" mode. But, with some UX improvements added to this interaction. Instead of simply appending new fields to the screen, Prompts' forms act like wizards 🧙‍♂️. Each field gets its own screen, and on each render loop, the screen is reset and repainted.
28
+
29
+ Finally, to keep internals simple, Prompts expects users to build and provide their own ANSI-formatted strings. However, we do make available the [`fmt`](https://github.com/hopsoft/fmt) gem as the recommended way to generate well formatted ANSI strings.
30
+
31
+ ## Available Prompts
32
+
33
+ ### Text
34
+
35
+ A `Text` prompt will prompt the user with the given question, accept their input, and then return it:
36
+
37
+ ```ruby
38
+ name = Prompts::TextPrompt.ask(label: "What is your name?")
39
+ ```
40
+
41
+ which generates a terminal screen like this (this representation doesn't show color):
42
+ <pre>
43
+
44
+ <b>What is your name?</b> <em>(Press Enter to submit)</em>
45
+ <b>Type your response and press Enter ⏎</b>
46
+
47
+ <b>></b> |
48
+ </pre>
49
+
50
+ You may also include a default value and an informational hint:
51
+
52
+ ```ruby
53
+ name = Prompts::TextPrompt.ask(
54
+ label: "What is your name?",
55
+ default: "John Doe",
56
+ hint: "This will be displayed on your profile."
57
+ )
58
+ ```
59
+
60
+ which generates a terminal screen like this (this representation doesn't show color):
61
+ <pre>
62
+
63
+ <b>What is your name?</b> <em>(Press Enter to submit)</em>
64
+ <b>This will be displayed on your profile.</b>
65
+
66
+ <b>></b> John Doe|
67
+ </pre>
68
+
69
+ #### Required values
70
+
71
+ If you require a value to be entered, you may pass the `required` argument:
72
+
73
+ ```ruby
74
+ name = Prompts::TextPrompt.ask(
75
+ label: "What is your name?",
76
+ required: true
77
+ )
78
+ ```
79
+
80
+ If you would like to customize the validation message, you may also pass a string:
81
+
82
+ ```ruby
83
+ name = Prompts::TextPrompt.ask(
84
+ label: "What is your name?",
85
+ required: "Your name is required."
86
+ )
87
+ ```
88
+
89
+ #### Additional Validation
90
+
91
+ Finally, if you would like to perform additional validation logic, you may pass a block/proc to the validate argument:
92
+
93
+ ```ruby
94
+ name = Prompts::TextPrompt.ask(
95
+ label: "What is your name?",
96
+ validate: ->(value) do
97
+ if value.length < 3
98
+ "The name must be at least 3 characters."
99
+ elsif value.length > 255
100
+ "The name must not exceed 255 characters."
101
+ end
102
+ end
103
+ )
104
+ ```
105
+
106
+ The block will receive the value that has been entered and may return an error message, or `nil` if the validation passes.
107
+
108
+ ### Select
109
+
110
+ If you need the user to select from a predefined set of choices, you may use the `Select` prompt:
111
+
112
+ ```ruby
113
+ role = Prompts::SelectPrompt.ask(
114
+ label: "What role should the user have?",
115
+ options: ["Member", "Contributor", "Owner"]
116
+ )
117
+ ```
118
+
119
+ which generates a terminal screen like this (this representation doesn't show color):
120
+ <pre>
121
+
122
+ <b>What role should the user have?</b> <em>(Enter the number of your choice)</em>
123
+ <b>Type your response and press Enter ⏎</b>
124
+ <b>1.</b> Member
125
+ <b>2.</b> Contributor
126
+ <b>3.</b> Owner
127
+
128
+ <b>></b> |
129
+ </pre>
130
+
131
+ You may also include a default value and an informational hint:
132
+
133
+ ```ruby
134
+ role = Prompts::SelectPrompt.ask(
135
+ label: "What role should the user have?",
136
+ options: ["Member", "Contributor", "Owner"],
137
+ default: "Owner",
138
+ hint: "The role may be changed at any time."
139
+ )
140
+ ```
141
+
142
+ which generates a terminal screen like this (this representation doesn't show color):
143
+ <pre>
144
+
145
+ <b>What role should the user have?</b> <em>(Enter the number of your choice)</em>
146
+ <b>The role may be changed at any time.</b>
147
+ <b>1.</b> Member
148
+ <b>2.</b> Contributor
149
+ <b>3.</b> Owner
150
+
151
+ <b>></b> 3|
152
+ </pre>
153
+
154
+ You may also pass a hash to the `options` argument to have the selected key returned instead of its value:
155
+
156
+ ```ruby
157
+ role = Prompts::SelectPrompt.ask(
158
+ label: "What role should the user have?",
159
+ options: {
160
+ member: "Member",
161
+ contributor: "Contributor",
162
+ owner: "Owner",
163
+ },
164
+ default: "owner"
165
+ )
166
+ ```
167
+
168
+ #### Additional Validation
169
+
170
+ Unlike other prompt classes, the `SelectPrompt` doesn't accept the `required` argument because it is not possible to select nothing. However, you may pass a block/proc to the `validate` argument if you need to present an option but prevent it from being selected:
171
+
172
+ ```ruby
173
+ role = Prompts::SelectPrompt.ask(
174
+ label: "What role should the user have?",
175
+ options: {
176
+ member: "Member",
177
+ contributor: "Contributor",
178
+ owner: "Owner",
179
+ },
180
+ validate: ->(value) do
181
+ if value == "owner" && User.where(role: "owner").exists?
182
+ "An owner already exists."
183
+ end
184
+ end
185
+ )
186
+ ```
187
+
188
+ If the `options` argument is a hash, then the block will receive the selected key, otherwise it will receive the selected value. The block may return an error message, or `nil` if the validation passes.
189
+
190
+ ### Confirm
191
+
192
+ If you need to ask the user for a "yes or no" confirmation, you may use the `ConfirmPrompt`. Users may press `y` or `n` (or `Y` or `N`) to select their response. This function will return either `true` or `false`.
193
+
194
+ ```ruby
195
+ confirmed = Prompts::ConfirmPrompt.ask(label: "Do you accept the terms?")
196
+ ```
197
+
198
+ which generates a terminal screen like this (this representation doesn't show color):
199
+ <pre>
200
+
201
+ <b>Do you accept the terms?</b> <em>(Press Enter to submit)</em>
202
+
203
+ <b>Choose [y/n]:</b> |
204
+ </pre>
205
+
206
+ You may also include a default value and an informational hint:
207
+
208
+ ```ruby
209
+ confirmed = Prompts::ConfirmPrompt.ask(
210
+ label: "Do you accept the terms?",
211
+ default: false,
212
+ hint: "The terms must be accepted to continue.",
213
+ )
214
+ ```
215
+
216
+ which generates a terminal screen like this (this representation doesn't show color):
217
+ <pre>
218
+
219
+ <b>Do you accept the terms?</b> <em>(Press Enter to submit)</em>
220
+ <b>The terms must be accepted to continue.</b>
221
+
222
+ <b>Choose [y/N]:</b> |
223
+ </pre>
224
+
225
+ #### Requiring "Yes"
226
+
227
+ If necessary, you may require your users to select "Yes" by passing the `required` argument:
228
+
229
+ ```ruby
230
+ confirmed = Prompts::ConfirmPrompt.ask(
231
+ label: "Do you accept the terms?",
232
+ required: true
233
+ )
234
+ ```
235
+
236
+ If you would like to customize the validation message, you may also pass a string:
237
+
238
+ ```ruby
239
+ confirmed = Prompts::ConfirmPrompt.ask(
240
+ label: "Do you accept the terms?",
241
+ required: "You must accept the terms to continue."
242
+ )
243
+ ```
244
+
245
+ ### Pause
246
+
247
+ The `PausePrompt` may be used to display informational text to the user and wait for them to confirm their desire to proceed by pressing the Enter / Return key:
248
+
249
+ ```ruby
250
+ Prompts::PausePrompt.ask
251
+ ```
18
252
 
19
- ## Usage
253
+ which generates a terminal screen like this (this representation doesn't show color):
254
+ <pre>
20
255
 
21
- TODO: Write usage instructions here
256
+ <b>Press Enter to continue...</b> |
257
+ </pre>
22
258
 
23
259
  ## Development
24
260
 
@@ -28,7 +264,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
28
264
 
29
265
  ## Contributing
30
266
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/prompts. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/prompts/blob/main/CODE_OF_CONDUCT.md).
267
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fractaledmind/prompts. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/fractaledmind/prompts/blob/main/CODE_OF_CONDUCT.md).
32
268
 
33
269
  ## License
34
270
 
@@ -36,4 +272,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
36
272
 
37
273
  ## Code of Conduct
38
274
 
39
- Everyone interacting in the Prompts project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/prompts/blob/main/CODE_OF_CONDUCT.md).
275
+ Everyone interacting in the Prompts project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/fractaledmind/prompts/blob/main/CODE_OF_CONDUCT.md).
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class Box
5
+ include TextUtils
6
+
7
+ SOLID_BORDER = { top_left: "┌", top_right: "┐", bottom_left: "└", bottom_right: "┘", horizontal: "─", vertical: "│" }.freeze
8
+ DOUBLE_BORDER = { top_left: "╔", top_right: "╗", bottom_left: "╚", bottom_right: "╝", horizontal: "═", vertical: "║" }.freeze
9
+ HEAVY_BORDER = { top_left: "┏", top_right: "┓", bottom_left: "┗", bottom_right: "┛", horizontal: "━", vertical: "┃" }.freeze
10
+ ROUNDED_BORDER = { top_left: "╭", top_right: "╮", bottom_left: "╰", bottom_right: "╯", horizontal: "─", vertical: "│" }.freeze
11
+
12
+ def initialize(width: MAX_WIDTH, padded: false, border_color: nil, border_style: :rounded)
13
+ @width = width
14
+ @padded = padded
15
+ @border_color = border_color
16
+ @line_padding = SPACE * 1
17
+ @border_parts = case border_style
18
+ when :solid then SOLID_BORDER
19
+ when :double then DOUBLE_BORDER
20
+ when :heavy then HEAVY_BORDER
21
+ else ROUNDED_BORDER
22
+ end
23
+ @content = []
24
+ end
25
+
26
+ def centered(text)
27
+ @content.concat align(text, :center)
28
+ end
29
+
30
+ def left(text)
31
+ @content.concat align(text, :left)
32
+ end
33
+
34
+ def right(text)
35
+ @content.concat align(text, :right)
36
+ end
37
+
38
+ def gap
39
+ @content.concat align(EMPTY, :center)
40
+ end
41
+
42
+ def lines
43
+ [].tap do |output|
44
+ output << top_border
45
+ align(EMPTY, :center).each { |line| output << @line_padding + line } if @padded
46
+ @content.each do |line|
47
+ output << @line_padding + line
48
+ end
49
+ align(EMPTY, :center).each { |line| output << @line_padding + line } if @padded
50
+ output << bottom_border
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def top_border
57
+ border = @border_parts[:top_left] + @border_parts[:horizontal] * (@width - 2) + @border_parts[:top_right]
58
+ Fmt("#{@line_padding}%{border}#{@border_color}", border: border)
59
+ end
60
+
61
+ def bottom_border
62
+ border = @border_parts[:bottom_left] + @border_parts[:horizontal] * (@width - 2) + @border_parts[:bottom_right]
63
+ Fmt("#{@line_padding}%{border}#{@border_color}", border: border)
64
+ end
65
+
66
+ def align(text, alignment, between: @border_parts[:vertical])
67
+ formatted_boundary = Fmt("%{boundary}#{@border_color}", boundary: between)
68
+ wrap_text(text, width: @width, line_prefix: formatted_boundary + SPACE, line_suffix: SPACE + formatted_boundary, alignment: alignment)
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class ConfirmPrompt < Prompt
5
+ def initialize(...)
6
+ super(...)
7
+
8
+ @prompt = if @default == false
9
+ "Choose [y/N]:"
10
+ elsif @default == true
11
+ "Choose [Y/n]:"
12
+ else
13
+ "Choose [y/n]:"
14
+ end
15
+ @default_boolean = @default
16
+ @default = nil
17
+ @instructions = "Press Enter to submit"
18
+ @validations << ->(choice) { "Invalid choice." if !["y", "n", "Y", "N", ""].include?(choice) }
19
+ end
20
+
21
+ private
22
+
23
+ def resolve_choice_from(response)
24
+ case response
25
+ when "y", "Y" then true
26
+ when "n", "N" then false
27
+ when "" then @default_boolean
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: false
2
+
3
+ module Prompts
4
+ class Content
5
+ attr_reader :slots
6
+
7
+ def initialize(width: MAX_WIDTH)
8
+ @slots = []
9
+ @frame_stack = []
10
+ @width = width
11
+ end
12
+
13
+ def paragraph(text)
14
+ paragraph = Paragraph.new(text, width: @width)
15
+ @slots.concat paragraph.lines
16
+ self
17
+ end
18
+
19
+ def gap
20
+ @slots << SPACE
21
+ self
22
+ end
23
+
24
+ def box(padded: false, border_color: nil, &block)
25
+ box = Box.new(width: @width, padded: padded, border_color: border_color)
26
+ yield(box)
27
+ @slots.concat box.lines
28
+ self
29
+ end
30
+
31
+ def render
32
+ clear_screen
33
+ render_frame
34
+ end
35
+
36
+ def reset!
37
+ @slots = @frame_stack.first.dup
38
+ end
39
+
40
+ def prepend(*lines)
41
+ @slots.unshift(*lines)
42
+ end
43
+
44
+ private
45
+
46
+ def clear_screen
47
+ jump_cursor_to_top
48
+ erase_down
49
+ end
50
+
51
+ def render_frame
52
+ @frame_stack << @slots.dup
53
+ OUTPUT.puts SPACE
54
+
55
+ return if @slots.empty?
56
+
57
+ OUTPUT.puts @slots.join("\n")
58
+ OUTPUT.puts SPACE
59
+ @slots.clear
60
+ end
61
+
62
+ def jump_cursor_to_top
63
+ OUTPUT.print "\033[H"
64
+ end
65
+
66
+ def erase_down
67
+ OUTPUT.print "\e[J"
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class Form
5
+ def initialize()
6
+ @content = nil
7
+ @prompts = []
8
+ @results = []
9
+ end
10
+
11
+ def content(&block)
12
+ @content = Prompts::Content.new
13
+ yield @content
14
+ @content
15
+ end
16
+
17
+ def text(&block)
18
+ prompt = TextPrompt.new
19
+ yield(prompt)
20
+ prepend_form_content_to_prompt(prompt)
21
+ @prompts << prompt
22
+ end
23
+
24
+ def select(&block)
25
+ prompt = SelectPrompt.new
26
+ yield(prompt)
27
+ prepend_form_content_to_prompt(prompt)
28
+ @prompts << prompt
29
+ end
30
+
31
+ def pause(&block)
32
+ prompt = PausePrompt.new
33
+ yield(prompt)
34
+ prepend_form_content_to_prompt(prompt)
35
+ @prompts << prompt
36
+ end
37
+
38
+ def confirm(&block)
39
+ prompt = ConfirmPrompt.new
40
+ yield(prompt)
41
+ prepend_form_content_to_prompt(prompt)
42
+ @prompts << prompt
43
+ end
44
+
45
+ def start
46
+ @prompts.each do |prompt|
47
+ @results << prompt.ask
48
+ end
49
+ @results
50
+ end
51
+
52
+ private
53
+
54
+ def prepend_form_content_to_prompt(prompt)
55
+ prompt.prepare_content
56
+ @content.gap
57
+ prompt.prepend_content(*@content.slots)
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class Paragraph
5
+ include TextUtils
6
+
7
+ LINE_PADDING = 3
8
+
9
+ def initialize(text, width: 60)
10
+ @text = text
11
+ @width = width - (LINE_PADDING + 1)
12
+ @line_padding = SPACE * LINE_PADDING
13
+ end
14
+
15
+ def lines
16
+ wrap_text(@text, width: @width, line_prefix: @line_padding, alignment: :none)
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class PausePrompt < Prompt
5
+ def initialize(...)
6
+ super(...)
7
+
8
+ @prompt = "Press Enter ⏎ to continue..."
9
+ end
10
+
11
+ def resolve_choice_from(response)
12
+ true
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "reline"
4
+
5
+ module Prompts
6
+ class Prompt
7
+ def self.ask(label: nil, prompt: "> ", hint: nil, default: nil, required: false, validate: nil)
8
+ instance = new(label: label, prompt: prompt, hint: hint, default: default, required: required, validate: validate)
9
+ yield instance if block_given?
10
+ instance.ask
11
+ end
12
+
13
+ def initialize(label: nil, prompt: "> ", hint: nil, default: nil, required: false, validate: nil)
14
+ @label = label
15
+ @prompt = prompt
16
+ @hint = hint
17
+ @default = default
18
+ @required = required
19
+ @validate = validate
20
+
21
+ @content = nil
22
+ @error = nil
23
+ @attempts = 0
24
+ @instructions = nil
25
+ @validations = []
26
+ @choice = nil
27
+ @content_prepared = false
28
+ end
29
+
30
+ def content(&block)
31
+ @content ||= Prompts::Content.new
32
+ yield @content
33
+ @content
34
+ end
35
+
36
+ def label(label)
37
+ @label = label
38
+ end
39
+
40
+ def hint(hint)
41
+ @hint = hint
42
+ end
43
+
44
+ def default(default)
45
+ @default = default
46
+ end
47
+
48
+ def ask
49
+ prepare_content if !@content_prepared
50
+ prepare_default if @default
51
+ prepare_validations
52
+
53
+ loop do
54
+ @content.render
55
+ *initial_prompt_lines, last_prompt_line = formatted_prompt
56
+ puts initial_prompt_lines.join("\n") if initial_prompt_lines.any?
57
+ response = Reline.readline(last_prompt_line, history = false).chomp
58
+ @choice = resolve_choice_from(response)
59
+
60
+ if (@error = ensure_validity(response))
61
+ @content.reset!
62
+ @content.paragraph Fmt("%{error}red|bold", error: @error + " Try again (×#{@attempts})...")
63
+ @attempts += 1
64
+ next
65
+ else
66
+ break @choice
67
+ end
68
+ end
69
+
70
+ @choice
71
+ rescue Interrupt
72
+ exit 0
73
+ end
74
+
75
+ def prepend_content(*lines)
76
+ @content.prepend(*lines)
77
+ end
78
+
79
+ def prepare_content
80
+ @content ||= Prompts::Content.new
81
+ @content.paragraph formatted_label if @label
82
+ @content.paragraph formatted_hint if @hint
83
+ @content.paragraph formatted_error if @error
84
+ @content_prepared = true
85
+ @content
86
+ end
87
+
88
+ private
89
+
90
+ def prepare_default
91
+ Reline.pre_input_hook = -> do
92
+ Reline.insert_text @default.to_s
93
+ # Remove the hook right away.
94
+ Reline.pre_input_hook = nil
95
+ end
96
+ end
97
+
98
+ def prepare_validations
99
+ if @required
100
+ error_message = @required.is_a?(String) ? @required : "Value cannot be empty."
101
+ @validations << ->(input) { error_message if input.empty? }
102
+ end
103
+
104
+ if @validate
105
+ @validations << @validate
106
+ end
107
+ end
108
+
109
+ def resolve_choice_from(response)
110
+ response
111
+ end
112
+
113
+ def formatted_prompt
114
+ prompt_with_space = @prompt.end_with?(SPACE) ? @prompt : @prompt + SPACE
115
+ ansi_prompt = Fmt("%{prompt}faint|bold", prompt: prompt_with_space)
116
+ @formatted_prompt ||= Paragraph.new(ansi_prompt, width: MAX_WIDTH).lines
117
+ end
118
+
119
+ def formatted_label
120
+ Fmt("%{label}cyan|bold %{instructions}faint|italic", label: @label, instructions: @instructions ? "(#{@instructions})" : "")
121
+ end
122
+
123
+ def formatted_hint
124
+ Fmt("%{hint}faint|bold", hint: @hint)
125
+ end
126
+
127
+ def formatted_error
128
+ Fmt("%{error}red|bold", error: @error + " Try again (×#{@attempts})...")
129
+ end
130
+
131
+ def ensure_validity(response)
132
+ @validations.each do |validation|
133
+ result = validation.call(response)
134
+ return result if result
135
+ end
136
+ nil
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class SelectPrompt < Prompt
5
+ def self.ask(options: nil, **kwargs)
6
+ instance = new(options: options, **kwargs)
7
+ yield instance if block_given?
8
+ instance.ask
9
+ end
10
+
11
+ def initialize(options: nil, **kwargs)
12
+ super(**kwargs)
13
+
14
+ @options = options.is_a?(Array) ? options.to_h { |item| [item, item] } : options
15
+ if (index = @options.keys.index(@default))
16
+ @default = index + 1
17
+ else
18
+ @default = nil
19
+ end
20
+ @instructions = "Enter the number of your choice"
21
+ @hint ||= "Type your response and press Enter ⏎"
22
+ @validations << ->(choice) { "Invalid choice." if !choice.to_i.between?(1, @options.size) }
23
+ end
24
+
25
+ def options(options)
26
+ @options = options
27
+ end
28
+
29
+ def prepare_content
30
+ super
31
+ @options.each_with_index do |(key, value), index|
32
+ @content.paragraph Fmt("%{prefix}faint|bold %{option}", prefix: "#{index + 1}.", option: value)
33
+ end
34
+ @content
35
+ end
36
+
37
+ private
38
+
39
+ def resolve_choice_from(response)
40
+ choice = response.to_i
41
+ key, value = @options.to_a[choice - 1]
42
+ value
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Prompts
4
+ class TextPrompt < Prompt
5
+ def initialize(...)
6
+ super(...)
7
+
8
+ @instructions = "Press Enter to submit"
9
+ @hint ||= "Type your response and press Enter ⏎"
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "unicode/display_width"
4
+ require "unicode/emoji"
5
+
6
+ module Prompts
7
+ module TextUtils
8
+ ANSI_REGEX = /\e\[[0-9;]*[a-zA-Z]/.freeze
9
+
10
+ def wrap_text(text, width:, line_prefix: EMPTY, line_suffix: EMPTY, alignment: :left)
11
+ words = text.scan(Regexp.union(/\S+/, ANSI_REGEX))
12
+ lines = []
13
+ line = +EMPTY
14
+ line_width = 0
15
+ prefix_width = Unicode::DisplayWidth.of(strip_ansi(line_prefix), 1, {}, emoji: true)
16
+ suffix_width = Unicode::DisplayWidth.of(strip_ansi(line_suffix), 1, {}, emoji: true)
17
+ available_width = width - prefix_width - suffix_width
18
+
19
+ words.each do |word|
20
+ word_width = Unicode::DisplayWidth.of(strip_ansi(word), 1, {}, emoji: true)
21
+
22
+ if (line_width + word_width) > available_width
23
+ lines << format_line(line.rstrip, available_width, alignment, line_prefix, line_suffix)
24
+ line = +EMPTY
25
+ line_width = 0
26
+ end
27
+
28
+ line << word + SPACE
29
+ line_width += word_width + 1
30
+ end
31
+
32
+ lines << format_line(line.rstrip, available_width, alignment, line_prefix, line_suffix)
33
+ lines
34
+ end
35
+
36
+ def format_line(line, available_width, alignment, prefix, suffix)
37
+ line_width = Unicode::DisplayWidth.of(strip_ansi(line), 1, {}, emoji: true)
38
+ padding = [available_width - line_width, 0].max
39
+
40
+ case alignment
41
+ when :none
42
+ prefix + line + suffix
43
+ when :left
44
+ prefix + line + (SPACE * padding) + suffix
45
+ when :right
46
+ prefix + (SPACE * padding) + line + suffix
47
+ when :center
48
+ left_padding = padding / 2
49
+ right_padding = padding - left_padding
50
+ prefix + (SPACE * left_padding) + line + (SPACE * right_padding) + suffix
51
+ end
52
+ end
53
+
54
+ def strip_ansi(text)
55
+ text.gsub(ANSI_REGEX, EMPTY)
56
+ end
57
+ end
58
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Prompts
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/prompts.rb CHANGED
@@ -1,8 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "io/console"
4
+ require "reline"
5
+ require "fmt"
6
+ require "rainbow"
7
+
8
+ Fmt.add_rainbow_filters
9
+
3
10
  require_relative "prompts/version"
11
+ require_relative "prompts/prompt"
12
+ require_relative "prompts/text_utils"
13
+ require_relative "prompts/content"
14
+ require_relative "prompts/paragraph"
15
+ require_relative "prompts/box"
16
+ require_relative "prompts/pause_prompt"
17
+ require_relative "prompts/confirm_prompt"
18
+ require_relative "prompts/text_prompt"
19
+ require_relative "prompts/select_prompt"
20
+ require_relative "prompts/form"
4
21
 
5
22
  module Prompts
23
+ EMPTY = "".freeze
24
+ SPACE = " ".freeze
25
+ MAX_WIDTH = 80
26
+ OUTPUT = $stdout
27
+
6
28
  class Error < StandardError; end
7
- # Your code goes here...
29
+
30
+ class << self
31
+ def Form(&block)
32
+ form = Prompts::Form.new
33
+ yield(form)
34
+ form.start
35
+ end
36
+ end
8
37
  end
metadata CHANGED
@@ -1,15 +1,85 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prompts
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stephen Margheim
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-07 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2024-08-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: unicode-display_width
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: unicode-emoji
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: reline
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: fmt
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rainbow
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
13
83
  description: Inspired by the Laravel Prompts project
14
84
  email:
15
85
  - stephen.margheim@gmail.com
@@ -24,6 +94,16 @@ files:
24
94
  - README.md
25
95
  - Rakefile
26
96
  - lib/prompts.rb
97
+ - lib/prompts/box.rb
98
+ - lib/prompts/confirm_prompt.rb
99
+ - lib/prompts/content.rb
100
+ - lib/prompts/form.rb
101
+ - lib/prompts/paragraph.rb
102
+ - lib/prompts/pause_prompt.rb
103
+ - lib/prompts/prompt.rb
104
+ - lib/prompts/select_prompt.rb
105
+ - lib/prompts/text_prompt.rb
106
+ - lib/prompts/text_utils.rb
27
107
  - lib/prompts/version.rb
28
108
  - sig/prompts.rbs
29
109
  homepage: https://github.com/fractaledmind/prompts