9
9
from typing import Any , TypedDict
10
10
11
11
import questionary
12
- import questionary .prompts .text
13
12
from prompt_toolkit .key_binding import KeyBindings
14
13
from prompt_toolkit .key_binding .key_processor import KeyPressEvent
15
14
from prompt_toolkit .keys import Keys
15
+ from prompt_toolkit .styles import Style
16
16
17
17
from commitizen import factory , git , out
18
18
from commitizen .config import BaseConfig
@@ -45,8 +45,11 @@ class CommitArgs(TypedDict, total=False):
45
45
retry : bool
46
46
47
47
48
- def _handle_questionary_prompt (question : CzQuestion , cz_style : Any ) -> dict [str , Any ]:
49
- """Handle questionary prompt with error handling."""
48
+ def _handle_questionary_prompt (question : CzQuestion , cz_style : Style ) -> dict [str , Any ]:
49
+ """Handle questionary prompt with multiline and error handling."""
50
+ if question ["type" ] == "input" and question .get ("multiline" , False ):
51
+ return _handle_multiline_question (question , cz_style )
52
+
50
53
try :
51
54
answer = questionary .prompt ([question ], style = cz_style )
52
55
if not answer :
@@ -55,13 +58,76 @@ def _handle_questionary_prompt(question: CzQuestion, cz_style: Any) -> dict[str,
55
58
except ValueError as err :
56
59
root_err = err .__context__
57
60
if isinstance (root_err , CzException ):
58
- raise CustomError (root_err . __str__ ( ))
61
+ raise CustomError (str ( root_err ))
59
62
raise err
60
63
61
64
62
- def _handle_multiline_fallback (multiline_question : InputQuestion , cz_style : Any ) -> dict [str , Any ]:
63
- """Handle fallback to standard behavior if custom multiline approach fails."""
64
- return _handle_questionary_prompt (multiline_question , cz_style )
65
+ def _handle_multiline_question (
66
+ question : InputQuestion , cz_style : Style
67
+ ) -> dict [str , Any ]:
68
+ """Handle multiline input questions."""
69
+ is_optional = (
70
+ question .get ("default" ) == ""
71
+ or "skip" in question .get ("message" , "" ).lower ()
72
+ or "[enter] to skip" in question .get ("message" , "" ).lower ()
73
+ )
74
+
75
+ guidance = (
76
+ "💡 Press Enter on empty line to skip, Alt+Enter to finish"
77
+ if is_optional
78
+ else "💡 Press Alt+Enter to finish"
79
+ )
80
+ out .info (guidance )
81
+
82
+ def _handle_key_press (event : KeyPressEvent , is_finish_key : bool ) -> None :
83
+ buffer = event .current_buffer
84
+ is_empty = not buffer .text .strip ()
85
+
86
+ if is_empty :
87
+ if is_optional and not is_finish_key :
88
+ event .app .exit (result = "" )
89
+ elif not is_optional :
90
+ out .error (
91
+ "⚠ This field is required. Please enter some content or press Ctrl+C to abort."
92
+ )
93
+ out .line ("> " , end = "" , flush = True )
94
+ else :
95
+ event .app .exit (result = buffer .text )
96
+ else :
97
+ if is_finish_key :
98
+ event .app .exit (result = buffer .text )
99
+ else :
100
+ buffer .newline ()
101
+
102
+ bindings = KeyBindings ()
103
+
104
+ @bindings .add (Keys .Enter )
105
+ def _ (event : KeyPressEvent ) -> None :
106
+ _handle_key_press (event , is_finish_key = False )
107
+
108
+ @bindings .add (Keys .Escape , Keys .Enter )
109
+ def _ (event : KeyPressEvent ) -> None :
110
+ _handle_key_press (event , is_finish_key = True )
111
+
112
+ result = questionary .text (
113
+ message = question ["message" ],
114
+ multiline = True ,
115
+ style = cz_style ,
116
+ key_bindings = bindings ,
117
+ ).unsafe_ask ()
118
+
119
+ if result is None :
120
+ result = question .get ("default" , "" )
121
+
122
+ if "filter" in question :
123
+ try :
124
+ result = question ["filter" ](result )
125
+ except Exception as e :
126
+ out .error (f"⚠ { str (e )} " )
127
+ out .line ("> " , end = "" , flush = True )
128
+ return _handle_multiline_question (question , cz_style )
129
+
130
+ return {question ["name" ]: result }
65
131
66
132
67
133
class Commit :
@@ -92,116 +158,12 @@ def _prompt_commit_questions(self) -> str:
92
158
questions = cz .questions ()
93
159
answers = {}
94
160
95
- # Handle questions one by one to support custom continuation
96
161
for question in questions :
97
162
if question ["type" ] == "list" :
98
163
question ["use_shortcuts" ] = self .config .settings ["use_shortcuts" ]
99
- answer = _handle_questionary_prompt (question , cz .style )
100
- answers .update (answer )
101
- elif question ["type" ] == "input" and question .get ("multiline" , False ):
102
- is_optional = (
103
- question .get ("default" ) == ""
104
- or "skip" in question .get ("message" , "" ).lower ()
105
- )
106
164
107
- if is_optional :
108
- out .info (
109
- "💡 Multiline input:\n Press Enter on empty line to skip, Enter after text for new lines, Alt+Enter to finish"
110
- )
111
- else :
112
- out .info (
113
- "💡 Multiline input:\n Press Enter for new lines and Alt+Enter to finish"
114
- )
115
-
116
- # Create custom multiline input with Enter-on-empty behavior for optional fields
117
-
118
- multiline_question = question .copy ()
119
- multiline_question ["multiline" ] = True
120
-
121
- if is_optional :
122
- # Create custom key bindings for optional fields
123
- bindings = KeyBindings ()
124
-
125
- @bindings .add (Keys .Enter )
126
- def _ (event : KeyPressEvent ) -> None :
127
- buffer = event .current_buffer
128
- # If buffer is completely empty, submit
129
- if not buffer .text .strip ():
130
- event .app .exit (result = buffer .text )
131
- else :
132
- # If there's text, add new line
133
- buffer .newline ()
134
-
135
- # Use the text prompt directly with custom bindings
136
- try :
137
- result = questionary .prompts .text .text (
138
- message = question ["message" ],
139
- multiline = True ,
140
- style = cz .style ,
141
- key_bindings = bindings ,
142
- ).ask ()
143
-
144
- field_name = question ["name" ]
145
- if result is None :
146
- result = question .get ("default" , "" )
147
-
148
- # Apply filter if present
149
- if "filter" in question :
150
- result = question ["filter" ](result )
151
-
152
- answer = {field_name : result }
153
- answers .update (answer )
154
-
155
- except Exception :
156
- # Fallback to standard behavior if custom approach fails
157
- answer = _handle_multiline_fallback (multiline_question , cz .style )
158
- answers .update (answer )
159
- else :
160
- # Required fields - don't allow newline on empty first line and show error
161
- bindings = KeyBindings ()
162
-
163
- @bindings .add (Keys .Enter )
164
- def _ (event : KeyPressEvent ) -> None :
165
- buffer = event .current_buffer
166
- # If buffer is completely empty (no content at all), show error and don't allow newline
167
- if not buffer .text .strip ():
168
- # Show error message with prompt
169
- out .error (
170
- "\n ⚠ This field is required. Please enter some content or press Ctrl+C to abort."
171
- )
172
- print ("> " , end = "" , flush = True )
173
- # Don't do anything - require content first
174
- pass
175
- else :
176
- # If there's text, add new line
177
- buffer .newline ()
178
-
179
- try :
180
- result = questionary .prompts .text .text (
181
- message = question ["message" ],
182
- multiline = True ,
183
- style = cz .style ,
184
- key_bindings = bindings ,
185
- ).ask ()
186
-
187
- field_name = question ["name" ]
188
- if result is None :
189
- result = ""
190
-
191
- # Apply filter if present
192
- if "filter" in question :
193
- result = question ["filter" ](result )
194
-
195
- answer = {field_name : result }
196
- answers .update (answer )
197
-
198
- except Exception :
199
- # Fallback to standard behavior if custom approach fails
200
- answer = _handle_multiline_fallback (multiline_question , cz .style )
201
- answers .update (answer )
202
- else :
203
- answer = _handle_questionary_prompt (question , cz .style )
204
- answers .update (answer )
165
+ answer = _handle_questionary_prompt (question , cz .style )
166
+ answers .update (answer )
205
167
206
168
message = cz .message (answers )
207
169
message_len = len (message .partition ("\n " )[0 ].strip ())
0 commit comments