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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a9749ca99273a0ed19da8274041da1b04f7355f523761169b509e3234737be7b
4
- data.tar.gz: 8a097667af010ba054486e6a22407fefdb021752a9ce018ac2a0489f060f4140
3
+ metadata.gz: 3035b53cb615be8f42827389fc118990457a06e20e9a118c9eb77a97ff5352f8
4
+ data.tar.gz: 98b12f8e74d475a50987a10c719f231050d83484989acd101c4e03bf11ad4b5e
5
5
  SHA512:
6
- metadata.gz: 6aa1f190f08a4fc285e682f6838518951ddc25647eaf034e06bdf7d1b1d06b15b0cc7cb2de3bd4c5ead11adcaea00b7cd8e2c6e8d03f28287b084f187766dd28
7
- data.tar.gz: f6b11ea693a6ba6440a907c01b25a33ade736d074eaed61095998b873385466ee1c086ad2bb78546287e2e038a56a4cab23f509c678e3c9ecb752bca6c540907
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
@@ -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
@@ -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
@@ -89,6 +89,7 @@ module Clack
89
89
  lines = []
90
90
  lines << "#{bar}\n"
91
91
  lines << "#{symbol_for_state} #{@message}\n"
92
+ lines << help_line
92
93
  lines << "#{active_bar} #{input_display}\n"
93
94
 
94
95
  visible_options.each_with_index do |opt, idx|
@@ -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|
@@ -65,6 +65,7 @@ module Clack
65
65
  lines = []
66
66
  lines << "#{bar}\n"
67
67
  lines << "#{symbol_for_state} #{@message}\n"
68
+ lines << help_line
68
69
  lines << "#{bar} #{options_display}\n"
69
70
  lines << "#{Colors.gray(Symbols::S_BAR_END)}\n"
70
71
  lines.join
@@ -59,6 +59,7 @@ module Clack
59
59
  lines = []
60
60
  lines << "#{bar}\n"
61
61
  lines << "#{symbol_for_state} #{@message}\n"
62
+ lines << help_line
62
63
 
63
64
  prev_was_group = false
64
65
  @flat_items.each_with_index do |item, 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
@@ -94,6 +94,7 @@ module Clack
94
94
  lines = []
95
95
  lines << "#{bar}\n"
96
96
  lines << "#{symbol_for_state} #{@message}\n"
97
+ lines << help_line
97
98
 
98
99
  visible_options.each_with_index do |opt, idx|
99
100
  actual_idx = @scroll_offset + idx
@@ -41,6 +41,7 @@ module Clack
41
41
  lines = []
42
42
  lines << "#{bar}\n"
43
43
  lines << "#{symbol_for_state} #{@message}\n"
44
+ lines << help_line
44
45
  lines << "#{active_bar} #{masked_display}\n"
45
46
  lines << "#{bar_end}\n"
46
47
 
@@ -115,6 +115,7 @@ module Clack
115
115
  lines = []
116
116
  lines << "#{bar}\n"
117
117
  lines << "#{symbol_for_state} #{@message}\n"
118
+ lines << help_line
118
119
  lines << "#{active_bar} #{input_display}\n"
119
120
 
120
121
  visible_suggestions.each_with_index do |path, idx|
@@ -69,6 +69,7 @@ module Clack
69
69
  lines = []
70
70
  lines << "#{bar}\n"
71
71
  lines << "#{symbol_for_state} #{@message}\n"
72
+ lines << help_line
72
73
 
73
74
  visible_options.each_with_index do |opt, idx|
74
75
  actual_idx = @scroll_offset + idx
@@ -55,6 +55,7 @@ module Clack
55
55
  lines = []
56
56
  lines << "#{bar}\n"
57
57
  lines << "#{symbol_for_state} #{@message}\n"
58
+ lines << help_line
58
59
 
59
60
  @options.each do |opt|
60
61
  lines << "#{bar} #{option_display(opt)}\n"
@@ -70,6 +70,7 @@ module Clack
70
70
  lines = []
71
71
  lines << "#{bar}\n"
72
72
  lines << "#{symbol_for_state} #{@message}\n"
73
+ lines << help_line
73
74
  lines << "#{active_bar} #{input_display}\n"
74
75
  lines << "#{bar_end}\n" if @state == :active || @state == :initial
75
76
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clack
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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