Skip to content

Commit 022c09e

Browse files
committed
Fix #58: mapValidated on MultipleChoice prompts and move error display to bottom
Two bugs existed in `PromptFramework.mapValidated` when used with `MultipleChoice` prompts: 1. After a validation rejection showed an error message, subsequent non-ENTER actions (TAB toggle, arrow keys) did not clear the error. The inner framework's status was permanently stuck at `Running(Left(err))` because non-ENTER handlers use `identity` for status, causing stale error rendering and visual artifacts (duplicate/shifted items on terminal). 2. After a first rejection, selecting then deselecting and pressing ENTER again produced no visible feedback because the rendering was already identical to the displayed error state. Fix in `PromptFramework.mapValidated` (two coordinated changes): - `renderState`: translate the outer wrapper's `Running(Left(err))` status directly into an inner `self.Status.Running(Left(err))` for rendering, instead of always reading `self.currentStatus()` which could be stale. - `handleEvent`: backward-propagate `self.Status.Init` instead of `self.Status.Running(Left(err))`, guarded by `innerWasFinished` to only apply when the inner was actually `Finished` (not when it was `Running` with its own validation error, e.g. `InteractiveTextInput`). Fix in `InteractiveMultipleChoice.renderState`: - Move the error message rendering from between instructions and options to after the options list. When no error is present, a blank line is rendered in its place. This keeps the line count constant across error/non-error states, preventing terminal scroll-induced cursor offset issues that caused duplicate rendering when the prompt was near the terminal bottom. Also adds a `validated-multi` interactive example for manual testing.
1 parent e86cf8d commit 022c09e

34 files changed

Lines changed: 441 additions & 20 deletions

modules/core/src/main/scala/InteractiveMultipleChoice.scala

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@ private[cue4s] class InteractiveMultipleChoice(
6363
lines += "? ".focused + lab.prompt + s" $promptCue ".prompt + st.text.input
6464
lines += "Tab".emphasis + " to toggle, " + "Shift+Tab".emphasis + " to toggle all, " + "Enter".emphasis + " to submit."
6565

66-
status match
67-
case Status.Running(Left(err)) =>
68-
lines += err.error
69-
case _ =>
70-
7166
st.display.showing match
7267
case None =>
7368
lines += "no matches...".noMatches
@@ -95,6 +90,12 @@ private[cue4s] class InteractiveMultipleChoice(
9590
end if
9691
end match
9792

93+
status match
94+
case Status.Running(Left(err)) =>
95+
lines += err.error
96+
case _ =>
97+
lines += ""
98+
9899
case Status.Finished(ids) =>
99100
lines += s"$promptDone ".focused + lab.prompt
100101

modules/core/src/main/scala/PromptFramework.scala

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ trait PromptFramework[Result](terminal: Terminal, out: Output)
154154
state: PromptState,
155155
status: Status,
156156
): List[String] =
157-
self.renderState(state, self.currentStatus())
157+
val innerStatus = status match
158+
case Status.Running(Left(err)) => self.Status.Running(Left(err))
159+
case _ => self.currentStatus()
160+
self.renderState(state, innerStatus)
158161

159162
override def handleEvent(
160163
event: Event,
@@ -163,6 +166,10 @@ trait PromptFramework[Result](terminal: Terminal, out: Output)
163166
case self.PromptAction.Update(statusChange, stateChange) =>
164167
self.stateTransition(stateChange, statusChange)
165168

169+
val innerWasFinished = self.currentStatus() match
170+
case self.Status.Finished(_) => true
171+
case _ => false
172+
166173
val refinedStatus =
167174
self.currentStatus() match
168175
case self.Status.Finished(result) =>
@@ -174,12 +181,14 @@ trait PromptFramework[Result](terminal: Terminal, out: Output)
174181
case self.Status.Running(r) => Status.Running(r.flatMap(f))
175182
case self.Status.Init => Status.Init
176183

177-
// propagate information backwards...
184+
// When outer validation rejects a Finished result,
185+
// reset the inner to Init so it can accept new events
186+
// and doesn't carry stale error status
178187
refinedStatus match
179-
case Status.Running(Left(err)) =>
188+
case Status.Running(Left(_)) if innerWasFinished =>
180189
self.stateTransition(
181190
identity,
182-
_ => self.Status.Running(Left(err)),
191+
_ => self.Status.Init,
183192
)
184193
case _ =>
185194

modules/core/src/snapshots/core/alternatives_cancel_multiple

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Event.Init
77
┃ ◯ sweet potato ┃
88
┃ ◯ fried chicken ┃
99
┃ ┃
10+
┃ ┃
1011
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1112
Event.Key(TAB)
1213
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -17,6 +18,7 @@ Event.Key(TAB)
1718
┃ ◯ sweet potato ┃
1819
┃ ◯ fried chicken ┃
1920
┃ ┃
21+
┃ ┃
2022
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
2123
Event.Key(DOWN)
2224
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -27,6 +29,7 @@ Event.Key(DOWN)
2729
┃ ◯ sweet potato ┃
2830
┃ ◯ fried chicken ┃
2931
┃ ┃
32+
┃ ┃
3033
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
3134
Event.Key(TAB)
3235
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -37,6 +40,7 @@ Event.Key(TAB)
3740
┃ ◯ sweet potato ┃
3841
┃ ◯ fried chicken ┃
3942
┃ ┃
43+
┃ ┃
4044
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
4145
Event.Key(DOWN)
4246
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -47,6 +51,7 @@ Event.Key(DOWN)
4751
┃ ◯ sweet potato ┃
4852
┃ ◯ fried chicken ┃
4953
┃ ┃
54+
┃ ┃
5055
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
5156
Event.Interrupt
5257
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -57,4 +62,5 @@ Event.Interrupt
5762
┃ ┃
5863
┃ ┃
5964
┃ ┃
65+
┃ ┃
6066
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

modules/core/src/snapshots/core/alternatives_cancel_multiple_derived

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Event.Init
77
┃ ◉ sweet potato ┃
88
┃ ◉ fried chicken ┃
99
┃ ┃
10+
┃ ┃
1011
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1112
Event.Key(TAB)
1213
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -17,28 +18,29 @@ Event.Key(TAB)
1718
┃ ◉ sweet potato ┃
1819
┃ ◉ fried chicken ┃
1920
┃ ┃
21+
┃ ┃
2022
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
2123
Event.Key(ENTER)
2224
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2325
┃? What would you like for lunch › ┃
2426
┃Tab to toggle, Shift+Tab to toggle all, Enter to submit.┃
25-
┃show pizza some love! ┃
2627
┃ ◯ pizza ┃
2728
┃ ◉ steak ┃
2829
┃ ◉ sweet potato ┃
2930
┃ ◉ fried chicken ┃
31+
┃show pizza some love! ┃
3032
┃ ┃
3133
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
3234
Event.Key(TAB)
3335
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
3436
┃? What would you like for lunch › ┃
3537
┃Tab to toggle, Shift+Tab to toggle all, Enter to submit.┃
36-
┃show pizza some love! ┃
3738
┃ ◉ pizza ┃
3839
┃ ◉ steak ┃
3940
┃ ◉ sweet potato ┃
4041
┃ ◉ fried chicken ┃
4142
┃ ┃
43+
┃ ┃
4244
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
4345
Event.Key(ENTER)
4446
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓

modules/core/src/snapshots/core/alternatives_infiniscroll_multiple

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Event.Init
66
┃ ◯ steak ┃
77
┃ ↓ sweet potato ┃
88
┃ ┃
9+
┃ ┃
910
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1011
Event.Key(TAB)
1112
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -15,6 +16,7 @@ Event.Key(TAB)
1516
┃ ◯ steak ┃
1617
┃ ↓ sweet potato ┃
1718
┃ ┃
19+
┃ ┃
1820
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
1921
Event.Key(DOWN)
2022
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -24,6 +26,7 @@ Event.Key(DOWN)
2426
┃ ◯ steak ┃
2527
┃ ↓ sweet potato ┃
2628
┃ ┃
29+
┃ ┃
2730
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
2831
Event.Key(TAB)
2932
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -33,6 +36,7 @@ Event.Key(TAB)
3336
┃ ◉ steak ┃
3437
┃ ↓ sweet potato ┃
3538
┃ ┃
39+
┃ ┃
3640
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
3741
Event.Key(DOWN)
3842
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -42,6 +46,7 @@ Event.Key(DOWN)
4246
┃ ◯ sweet potato ┃
4347
┃ ↓ fried chicken ┃
4448
┃ ┃
49+
┃ ┃
4550
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
4651
Event.Key(DOWN)
4752
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -51,6 +56,7 @@ Event.Key(DOWN)
5156
┃ ◯ fried chicken ┃
5257
┃ ◯ sushi ┃
5358
┃ ┃
59+
┃ ┃
5460
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
5561
Event.Key(TAB)
5662
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -60,6 +66,7 @@ Event.Key(TAB)
6066
┃ ◉ fried chicken ┃
6167
┃ ◯ sushi ┃
6268
┃ ┃
69+
┃ ┃
6370
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
6471
Event.Key(DOWN)
6572
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -69,6 +76,7 @@ Event.Key(DOWN)
6976
┃ ◉ fried chicken ┃
7077
┃ ◯ sushi ┃
7178
┃ ┃
79+
┃ ┃
7280
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
7381
Event.Key(TAB)
7482
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -78,6 +86,7 @@ Event.Key(TAB)
7886
┃ ◉ fried chicken ┃
7987
┃ ◉ sushi ┃
8088
┃ ┃
89+
┃ ┃
8190
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
8291
Event.Key(ENTER)
8392
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
@@ -87,4 +96,5 @@ Event.Key(ENTER)
8796
┃ ◉ fried chicken ┃
8897
┃ ◉ sushi ┃
8998
┃ ┃
99+
┃ ┃
90100
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

0 commit comments

Comments
 (0)