From 1fb314fe57cdf9790e64cdf247c3c74ef26158fd Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 28 Nov 2024 18:15:14 +0000 Subject: [PATCH] Derived prompts redesign Additionally some better formatting settings, fixes to rendering, and minor updates --- .scalafmt.conf | 16 +- build.sbt | 40 ++--- .../src/main/scala/PromptsIO.scala | 6 +- .../scala-jvm-native/PromptsPlatform.scala | 8 +- .../scala/InteractiveMultipleChoice.scala | 74 +++----- .../main/scala/InteractiveSingleChoice.scala | 78 ++++----- .../src/main/scala/InteractiveTextInput.scala | 95 ++++------ modules/core/src/main/scala/Prompt.scala | 99 +++++++++-- .../src/main/scala/PromptChainMacros.scala | 5 +- .../core/src/main/scala/PromptFramework.scala | 162 ++++++++++++++---- .../src/main/scalajs/PromptsPlatform.scala | 4 +- .../snapshots/core/derived_validated_input | 96 +++++++++++ modules/core/src/snapshots/core/number_input | 162 ++++++++++++++++++ modules/core/src/snapshots/core/promptchain | 1 + .../snapshots/coreJS/derived_validated_input | 96 +++++++++++ .../core/src/snapshots/coreJS/number_input | 162 ++++++++++++++++++ modules/core/src/snapshots/coreJS/promptchain | 1 + .../coreNative/derived_validated_input | 96 +++++++++++ .../src/snapshots/coreNative/number_input | 162 ++++++++++++++++++ .../core/src/snapshots/coreNative/promptchain | 1 + .../core/src/test/scala/ExampleTests.scala | 62 ++++++- .../core/src/test/scala/TerminalTests.scala | 7 +- .../src/main/scala-jvm-native/sync.scala | 79 +++++---- project/plugins.sbt | 5 + 24 files changed, 1235 insertions(+), 282 deletions(-) create mode 100644 modules/core/src/snapshots/core/derived_validated_input create mode 100644 modules/core/src/snapshots/core/number_input create mode 100644 modules/core/src/snapshots/coreJS/derived_validated_input create mode 100644 modules/core/src/snapshots/coreJS/number_input create mode 100644 modules/core/src/snapshots/coreNative/derived_validated_input create mode 100644 modules/core/src/snapshots/coreNative/number_input diff --git a/.scalafmt.conf b/.scalafmt.conf index d2fa8e1..346ca69 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -10,12 +10,18 @@ fileOverride { "glob:**.sbt" { runner.dialect = scala212source3 } - - "glob:**/project/**.*" { + + "glob:**/project/plugins.sbt" { runner.dialect = scala212source3 + newlines.topLevelStatementBlankLines = [ + { + blanks = 1, + minBreaks = 0 + } + ] } +} - "glob:**/snapshots-sbt-plugin/**.*" { - runner.dialect = scala212source3 - } +rewrite { + trailingCommas.style = "always" } diff --git a/build.sbt b/build.sbt index d5ad7c8..30403d2 100644 --- a/build.sbt +++ b/build.sbt @@ -11,22 +11,22 @@ inThisBuild( sonatypeCredentialHost := "s01.oss.sonatype.org", resolvers ++= Resolver.sonatypeOssRepos("releases"), homepage := Some( - url("https://github.com/neandertech/cue4s") + url("https://github.com/neandertech/cue4s"), ), startYear := Some(2023), licenses := List( - "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") + "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0"), ), developers := List( Developer( "keynmol", "Anton Sviridov", "velbm@pm.me", - url("https://blog.indoorvivants.com") - ) + url("https://blog.indoorvivants.com"), + ), ), - version := (if (!sys.env.contains("CI")) "dev" else version.value) - ) + version := (if (!sys.env.contains("CI")) "dev" else version.value), + ), ) val Versions = new { @@ -41,7 +41,7 @@ val Versions = new { lazy val munitSettings = Seq( libraryDependencies += { "org.scalameta" %%% "munit" % Versions.munit % Test - } + }, ) lazy val root = project @@ -55,7 +55,7 @@ lazy val core = projectMatrix .in(file("modules/core")) .defaultAxes(defaults*) .settings( - name := "cue4s" + name := "cue4s", ) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) @@ -70,7 +70,7 @@ lazy val core = projectMatrix libraryDependencies += "com.lihaoyi" %%% "fansi" % Versions.fansi, libraryDependencies += "net.java.dev.jna" % "jna" % Versions.jna, - nativeConfig ~= (_.withIncrementalCompilation(true)) + nativeConfig ~= (_.withIncrementalCompilation(true)), ) .enablePlugins(SnapshotsPlugin) .settings(superMatrix) @@ -79,7 +79,7 @@ lazy val catsEffect = projectMatrix .in(file("modules/cats-effect")) .defaultAxes(defaults*) .settings( - name := "cue4s-cats-effect" + name := "cue4s-cats-effect", ) .dependsOn(core) .settings(munitSettings) @@ -92,7 +92,7 @@ lazy val catsEffect = projectMatrix scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), libraryDependencies += "org.typelevel" %%% "cats-effect" % Versions.catsEffect, - nativeConfig ~= (_.withIncrementalCompilation(true)) + nativeConfig ~= (_.withIncrementalCompilation(true)), ) .enablePlugins(SnapshotsPlugin) @@ -103,7 +103,7 @@ lazy val example = projectMatrix .enablePlugins(JavaAppPackaging) .settings( name := "example", - noPublish + noPublish, ) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) @@ -115,7 +115,7 @@ lazy val example = projectMatrix scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), nativeConfig ~= (_.withIncrementalCompilation(true) .withSourceLevelDebuggingConfig(SourceLevelDebuggingConfig.enabled)), - Compile / mainClass := Some("cue4s_example.sync") + Compile / mainClass := Some("cue4s_example.sync"), ) .settings(superMatrix) @@ -142,7 +142,7 @@ lazy val exampleCatsEffect = projectMatrix .enablePlugins(JavaAppPackaging) .settings( name := "example", - noPublish + noPublish, ) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) @@ -152,7 +152,7 @@ lazy val exampleCatsEffect = projectMatrix scalaJSUseMainModuleInitializer := true, Compile / mainClass := Some("example.catseffect.ioExample"), scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - nativeConfig ~= (_.withIncrementalCompilation(true)) + nativeConfig ~= (_.withIncrementalCompilation(true)), ) lazy val docs = @@ -164,7 +164,7 @@ lazy val docs = val noPublish = Seq( publish / skip := true, - publishLocal / skip := true + publishLocal / skip := true, ) val defaults = @@ -174,7 +174,7 @@ val scalafixRules = Seq( "OrganizeImports", "DisableSyntax", "LeakingImplicitClassVal", - "NoValInForComprehension" + "NoValInForComprehension", ).mkString(" ") val CICommands = Seq( @@ -186,14 +186,14 @@ val CICommands = Seq( "scalafmtCheckAll", "scalafmtSbtCheck", s"scalafix --check $scalafixRules", - "headerCheck" + "headerCheck", ).mkString(";") val PrepareCICommands = Seq( s"scalafix --rules $scalafixRules", "scalafmtAll", "scalafmtSbt", - "headerCreate" + "headerCreate", ).mkString(";") addCommandAlias("ci", CICommands) @@ -202,7 +202,7 @@ addCommandAlias("preCI", PrepareCICommands) addCommandAlias( "testSnapshots", - """set Test/envVars += ("SNAPSHOTS_INTERACTIVE" -> "true"); test""" + """set Test/envVars += ("SNAPSHOTS_INTERACTIVE" -> "true"); test""", ) addCommandAlias("checkDocs", "docs/mdoc --in README.md") diff --git a/modules/cats-effect/src/main/scala/PromptsIO.scala b/modules/cats-effect/src/main/scala/PromptsIO.scala index cb973ba..4c2d121 100644 --- a/modules/cats-effect/src/main/scala/PromptsIO.scala +++ b/modules/cats-effect/src/main/scala/PromptsIO.scala @@ -30,12 +30,14 @@ class PromptsIO private ( prompt: Prompt[A] ): IO[Completion[A]] = val inputProvider = InputProvider(out) - val handler = prompt.handler(terminal, out, colors) + val framework = prompt.framework(terminal, out, colors) // TODO: provide native CE interface here IO.executionContext .flatMap: ec => - IO.fromFuture(IO(inputProvider.evaluateFuture(handler)(using ec))) + IO.fromFuture( + IO(inputProvider.evaluateFuture(framework.handler)(using ec)) + ) .guarantee(IO(terminal.cursorShow())) .guarantee(IO(inputProvider.close())) diff --git a/modules/core/src/main/scala-jvm-native/PromptsPlatform.scala b/modules/core/src/main/scala-jvm-native/PromptsPlatform.scala index 7454d44..d0d61c0 100644 --- a/modules/core/src/main/scala-jvm-native/PromptsPlatform.scala +++ b/modules/core/src/main/scala-jvm-native/PromptsPlatform.scala @@ -28,9 +28,9 @@ private trait PromptsPlatform: prompt match case p: Prompt[?] => try - val handler = p.asInstanceOf[Prompt[R]].handler(terminal, out, colors) + val framework = p.asInstanceOf[Prompt[R]].framework(terminal, out, colors) - inputProvider.evaluate(handler) + inputProvider.evaluate(framework.handler) finally terminal.cursorShow() inputProvider.close() @@ -45,9 +45,9 @@ private trait PromptsPlatform: createTerminal: Output => Terminal = Terminal.ansi(_), colors: Boolean = true )(using ExecutionContext): Future[Completion[R]] = - val handler = prompt.handler(createTerminal(out), out, colors) + val framework = prompt.framework(createTerminal(out), out, colors) - val f = inputProvider.evaluateFuture(handler) + val f = inputProvider.evaluateFuture(framework.handler) f.onComplete(_ => terminal.cursorShow()) f.onComplete(_ => inputProvider.close()) f diff --git a/modules/core/src/main/scala/InteractiveMultipleChoice.scala b/modules/core/src/main/scala/InteractiveMultipleChoice.scala index 3502593..2d241a0 100644 --- a/modules/core/src/main/scala/InteractiveMultipleChoice.scala +++ b/modules/core/src/main/scala/InteractiveMultipleChoice.scala @@ -21,12 +21,11 @@ private[cue4s] class InteractiveMultipleChoice( terminal: Terminal, out: Output, colors: Boolean, - windowSize: Int -) extends PromptFramework(terminal, out): + windowSize: Int, +) extends PromptFramework[List[String]](terminal, out): import InteractiveMultipleChoice.* override type PromptState = State - override type Result = List[String] private lazy val preSelected = prompt.alts.zipWithIndex.filter(_._1._2).map(_._2) @@ -42,8 +41,8 @@ private[cue4s] class InteractiveMultipleChoice( display = InfiniscrollableState( showing = Some(altsWithIndex.map(_._2) -> 0), windowStart = 0, - windowSize = windowSize - ) + windowSize = windowSize, + ), ) private lazy val altMapping = altsWithIndex.map(_.swap).toMap @@ -51,7 +50,10 @@ private[cue4s] class InteractiveMultipleChoice( private lazy val formatting = TextFormatting(colors) import formatting.* - override def renderState(st: State): List[String] = + override def renderState( + st: State, + error: Option[PromptError], + ): List[String] = val lines = List.newBuilder[String] st.status match @@ -80,7 +82,7 @@ private[cue4s] class InteractiveMultipleChoice( else if filtered.size > st.display.windowSize && idx == st.display.windowSize - 1 && filtered.indexOf(id) != filtered.size - 1 then s" ↓ $alt".bold - else s" $alt" + else s" $alt", ) end if end match @@ -90,8 +92,7 @@ private[cue4s] class InteractiveMultipleChoice( if ids.isEmpty then lines += "nothing selected".underline else - ids.toList.sorted - .map(altMapping) + ids .foreach: value => lines += s" ‣ " + value.bold @@ -104,57 +105,36 @@ private[cue4s] class InteractiveMultipleChoice( lines.result() end renderState - override def handleEvent(event: Event): Next[List[String]] = + override def result(state: State): Either[PromptError, List[String]] = + Right(state.selected.toList.sorted.map(altMapping).toList) + + override def handleEvent(event: Event) = event match - case Event.Init => - terminal.cursorHide() - printPrompt() - Next.Continue + case Event.Init => PromptAction.Start case Event.Key(KeyEvent.UP) => - stateTransition(_.updateDisplay(_.up)) - printPrompt() - Next.Continue + PromptAction.Update(_.updateDisplay(_.up)) case Event.Key(KeyEvent.DOWN) => - stateTransition(_.updateDisplay(_.down)) - printPrompt() - Next.Continue + PromptAction.Update(_.updateDisplay(_.down)) case Event.Key(KeyEvent.ENTER) => - stateTransition(_.finish) - currentState().status match - case Status.Canceled => Next.Stop - case Status.Running => Next.Continue - case Status.Finished(ids) => - val stringValues = ids.toList.sorted.map(altMapping.apply) - printPrompt() - terminal.cursorShow() - Next.Done(stringValues) + PromptAction.Submit(result => state => state.finish(result)) case Event.Key(KeyEvent.TAB) => - stateTransition(_.toggle) - printPrompt() - Next.Continue + PromptAction.Update(_.toggle) case Event.Key(KeyEvent.DELETE) => - stateTransition(_.trimText) - printPrompt() - Next.Continue + PromptAction.Update(_.trimText) case Event.Char(which) => - stateTransition(_.addText(which.toChar)) - printPrompt() - Next.Continue + PromptAction.Update(_.addText(which.toChar)) case Event.Interrupt => - stateTransition(_.cancel) - printPrompt() - terminal.cursorShow() - Next.Stop + PromptAction.UpdateAndStop(_.cancel) case _ => - Next.Continue + PromptAction.Continue end match end handleEvent @@ -163,7 +143,7 @@ end InteractiveMultipleChoice private[cue4s] object InteractiveMultipleChoice: enum Status: case Running - case Finished(ids: Set[Int]) + case Finished(ids: List[String]) case Canceled case class State( @@ -171,13 +151,13 @@ private[cue4s] object InteractiveMultipleChoice: selected: Set[Int], all: List[(String, Int)], status: Status, - display: InfiniscrollableState + display: InfiniscrollableState, ): def updateDisplay(f: InfiniscrollableState => InfiniscrollableState) = copy(display = f(display)) - def finish = copy(status = Status.Finished(selected)) + def finish(result: List[String]) = copy(status = Status.Finished(result)) def cancel = copy(status = Status.Canceled) @@ -198,7 +178,7 @@ private[cue4s] object InteractiveMultipleChoice: val newFiltered = all.filter((alt, _) => newText.trim.isEmpty || alt .toLowerCase() - .contains(newText.toLowerCase().trim()) + .contains(newText.toLowerCase().trim()), ) if newFiltered.nonEmpty then display.showing match diff --git a/modules/core/src/main/scala/InteractiveSingleChoice.scala b/modules/core/src/main/scala/InteractiveSingleChoice.scala index a2c2535..aa8ea44 100644 --- a/modules/core/src/main/scala/InteractiveSingleChoice.scala +++ b/modules/core/src/main/scala/InteractiveSingleChoice.scala @@ -21,18 +21,23 @@ private[cue4s] class InteractiveSingleChoice( terminal: Terminal, out: Output, colors: Boolean, - windowSize: Int -) extends PromptFramework(terminal, out): + windowSize: Int, +) extends PromptFramework[String](terminal, out): import InteractiveSingleChoice.* override type PromptState = State - override type Result = String override def isRunning(state: State): Boolean = state.status == Status.Running private lazy val altsWithIndex = prompt.alts.zipWithIndex private lazy val altMapping = altsWithIndex.map(_.swap).toMap + override def result(state: State): Either[PromptError, String] = + state.display.showing + .map(_._2) + .map(altMapping) + .toRight(PromptError("nothing selected")) + override def initialState = State( text = "", all = altsWithIndex, @@ -40,59 +45,44 @@ private[cue4s] class InteractiveSingleChoice( display = InfiniscrollableState( showing = Some(altsWithIndex.map(_._2) -> 0), windowStart = 0, - windowSize = windowSize - ) + windowSize = windowSize, + ), ) - override def handleEvent(event: Event): Next[String] = + override def handleEvent(event: Event) = event match - case Event.Init => - printPrompt() - Next.Continue + case Event.Init => PromptAction.Start + case Event.Key(KeyEvent.UP) => - stateTransition(_.updateDisplay(_.up)) - printPrompt() - Next.Continue + PromptAction.Update(_.updateDisplay(_.up)) + case Event.Key(KeyEvent.DOWN) => - stateTransition(_.updateDisplay(_.down)) - printPrompt() - Next.Continue + PromptAction.Update(_.updateDisplay(_.down)) case Event.Key(KeyEvent.ENTER) => // enter - stateTransition(_.finish) - currentState().status match - case Status.Running => Next.Continue - case Status.Finished(idx) => - val stringValue = altMapping(idx) - printPrompt() - Next.Done(stringValue) - case Status.Canceled => Next.Stop + PromptAction.Submit(result => state => state.finish(result)) case Event.Key(KeyEvent.DELETE) => // enter - stateTransition(_.trimText) - printPrompt() - Next.Continue + PromptAction.Update(_.trimText) case Event.Char(which) => - stateTransition(_.addText(which.toChar)) - printPrompt() - Next.Continue + PromptAction.Update(_.addText(which.toChar)) case Event.Interrupt => - stateTransition(_.cancel) - printPrompt() - terminal.cursorShow() - Next.Stop + PromptAction.UpdateAndStop(_.cancel) case _ => - Next.Continue + PromptAction.Continue end match end handleEvent private lazy val formatting = TextFormatting(colors) import formatting.* - override def renderState(st: State): List[String] = + override def renderState( + st: State, + error: Option[PromptError], + ): List[String] = val lines = List.newBuilder[String] st.status match @@ -119,11 +109,10 @@ private[cue4s] class InteractiveSingleChoice( idx == st.display.windowSize - 1 && filtered.indexOf(id) != filtered.size - 1 then s" ↓ $alt".bold - else s" $alt".bold + else s" $alt".bold, ) end match - case Status.Finished(idx) => - val value = altMapping(idx) + case Status.Finished(value) => lines += "✔ ".green + (prompt.lab + " ").cyan + value.bold @@ -141,23 +130,20 @@ end InteractiveSingleChoice private[cue4s] object InteractiveSingleChoice: enum Status: case Running - case Finished(idx: Int) + case Finished(selected: String) case Canceled case class State( text: String, all: List[(String, Int)], status: Status, - display: InfiniscrollableState + display: InfiniscrollableState, ): def updateDisplay(f: InfiniscrollableState => InfiniscrollableState) = copy(display = f(display)) - def finish = - display.showing match - case None => this - case Some((_, selected)) => - copy(status = Status.Finished(selected)) + def finish(result: String) = + copy(status = Status.Finished(result)) def cancel = copy(status = Status.Canceled) @@ -172,7 +158,7 @@ private[cue4s] object InteractiveSingleChoice: val newFiltered = all.filter((alt, _) => newText.trim.isEmpty || alt .toLowerCase() - .contains(newText.toLowerCase().trim()) + .contains(newText.toLowerCase().trim()), ) if newFiltered.nonEmpty then display.showing match diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala index bb7f6f2..a2624d6 100644 --- a/modules/core/src/main/scala/InteractiveTextInput.scala +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -17,74 +17,60 @@ package cue4s private[cue4s] class InteractiveTextInput( - prompt: Prompt.Input, + prompt: String, terminal: Terminal, out: Output, - colors: Boolean -) extends PromptFramework(terminal, out): + colors: Boolean, + validate: String => Option[PromptError], +) extends PromptFramework[String](terminal, out): import InteractiveTextInput.* override type PromptState = State - override type Result = String - override def initialState: State = State("", prompt.validate, Status.Running) + override def initialState: State = State("", Status.Running) override def isRunning(state: State): Boolean = state.status == Status.Running - override def handleEvent(event: Event): Next[Result] = + override def result(state: State) = + validate(state.text).toLeft(state.text) + + override def handleEvent(event: Event) = event match - case Event.Init => - printPrompt() - Next.Continue - - case Event.Key(KeyEvent.ENTER) => // enter - stateTransition(_.finish) - currentState().status match - case Status.Running => Next.Continue - case Status.Finished(result) => - printPrompt() - terminal.cursorShow() - Next.Done(result) - case Status.Canceled => - Next.Stop - - case Event.Key(KeyEvent.DELETE) => // enter - stateTransition(_.trimText) - printPrompt() - Next.Continue - - case Event.Char(which) => - stateTransition(_.addText(which.toChar)) - printPrompt() - Next.Continue - - case Event.Interrupt => - stateTransition(_.cancel) - printPrompt() - terminal.cursorShow() - Next.Stop - - case _ => - Next.Continue + case Event.Init => PromptAction.Start + + case Event.Key(KeyEvent.ENTER) => + PromptAction.Submit(result => state => state.finish(result)) + + case Event.Key(KeyEvent.DELETE) => + PromptAction.Update(_.trimText) + + case Event.Char(which) => PromptAction.Update(_.addText(which.toChar)) + + case Event.Interrupt => PromptAction.UpdateAndStop(_.cancel) + + case _ => PromptAction.Continue end match end handleEvent private lazy val formatting = TextFormatting(colors) import formatting.* - override def renderState(st: State): List[String] = + override def renderState( + st: State, + error: Option[PromptError], + ): List[String] = val lines = List.newBuilder[String] st.status match case Status.Running => - lines += prompt.lab.cyan + " > " + st.text.bold - st.error.foreach: err => + lines += prompt.cyan + " > " + st.text.bold + error.foreach: err => lines += err.red - case Status.Finished(result) => - lines += "✔ ".green + prompt.lab.cyan + " " + st.text.bold + case Status.Finished(res) => + lines += "✔ ".green + prompt.cyan + " " + st.text.bold lines += "" case Status.Canceled => - lines += "× ".red + prompt.lab.cyan + lines += "× ".red + prompt.cyan lines += "" end match @@ -96,24 +82,17 @@ end InteractiveTextInput private[cue4s] object InteractiveTextInput: enum Status: case Running - case Finished(result: String) + case Finished(res: String) case Canceled case class State( text: String, - validate: String => Option[PromptError], - status: Status + status: Status, ): - lazy val error = validate(text) - - def cancel = copy(status = Status.Canceled) - - def finish = - error match - case None => copy(status = Status.Finished(text)) - case Some(value) => this + def cancel = copy(status = Status.Canceled) + def finish(result: String) = copy(status = Status.Finished(result)) + def addText(t: Char) = copy(text = text + t) - def addText(t: Char) = copy(text = text + t) - def trimText = copy(text = text.dropRight(1)) + def trimText = copy(text = text.dropRight(1)) end State end InteractiveTextInput diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 05cdc74..60a93a1 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -17,39 +17,114 @@ package cue4s trait Prompt[Result]: - private[cue4s] def handler( + self => + private[cue4s] def framework( terminal: Terminal, output: Output, colors: Boolean - ): Handler[Result] + ): PromptFramework[Result] + + def map[Derived](f: Result => Derived): Prompt[Derived] = + mapValidated(r => Right(f(r))) + + def mapValidated[Derived]( + f: Result => Either[PromptError, Derived] + ): Prompt[Derived] = + new Prompt[Derived]: + override def framework( + terminal: Terminal, + output: Output, + colors: Boolean + ): PromptFramework[Derived] = + self.framework(terminal, output, colors).mapValidated(f) +end Prompt object Prompt: - case class Input( + case class Input private ( lab: String, validate: String => Option[PromptError] = _ => None ) extends Prompt[String]: - override def handler( + + def this(lab: String) = this(lab, _ => None) + + def validate(f: String => Option[PromptError]): Input = + copy(validate = (n: String) => validate(n).orElse(f(n))) + + override def framework( terminal: Terminal, output: Output, colors: Boolean - ): Handler[String] = - InteractiveTextInput(this, terminal, output, colors).handler + ) = InteractiveTextInput(lab, terminal, output, colors, validate) end Input + object Input: + def apply(lab: String): Input = new Input(lab) + + case class NumberInput[N: Numeric] private ( + lab: String, + validateNumber: N => Option[PromptError] = (_: N) => None + ) extends Prompt[N]: + private val num = Numeric[N] + + def this(lab: String) = this(lab, _ => None) + + def validate(f: N => Option[PromptError]): NumberInput[N] = + copy(validateNumber = (n: N) => validateNumber(n).orElse(f(n))) + + def positive = validate(n => + Option.when(num.lteq(n, num.zero))(PromptError("must be positive")) + ) + + def negative = validate(n => + Option.when(num.lteq(n, num.zero))(PromptError("must be negative")) + ) + + def min(value: N): NumberInput[N] = validate(n => + Option.when(num.lt(n, value))(PromptError(s"must be no less than $value")) + ) + + def max(value: N): NumberInput[N] = validate(n => + Option.when(num.gt(n, value))(PromptError(s"must be no more than $value")) + ) + + override def framework( + terminal: Terminal, + output: Output, + colors: Boolean + ) = + val lifted = (n: N) => validateNumber(n).toLeft(n) + + val transform = (s: String) => + Numeric[N] + .parseString(s) + .toRight(PromptError("not a valid number")) + .flatMap(lifted) + + val stringValidate = transform(_: String).left.toOption + + InteractiveTextInput(lab, terminal, output, colors, stringValidate) + .mapValidated(transform) + end framework + end NumberInput + + object NumberInput: + val int = NumberInput[Int].apply(_, _ => None) + val float = NumberInput[Float].apply(_, _ => None) + val double = NumberInput[Float].apply(_, _ => None) case class SingleChoice(lab: String, alts: List[String], windowSize: Int = 10) extends Prompt[String]: - override def handler( + override def framework( terminal: Terminal, output: Output, colors: Boolean - ): Handler[String] = + ) = InteractiveSingleChoice( this, terminal, output, colors, windowSize - ).handler + ) end SingleChoice case class MultipleChoice private ( @@ -57,18 +132,18 @@ object Prompt: alts: List[(String, Boolean)], windowSize: Int ) extends Prompt[List[String]]: - override def handler( + override def framework( terminal: Terminal, output: Output, colors: Boolean - ): Handler[List[String]] = + ) = InteractiveMultipleChoice( this, terminal, output, colors, windowSize - ).handler + ) end MultipleChoice object MultipleChoice: diff --git a/modules/core/src/main/scala/PromptChainMacros.scala b/modules/core/src/main/scala/PromptChainMacros.scala index ea6163e..a721356 100644 --- a/modules/core/src/main/scala/PromptChainMacros.scala +++ b/modules/core/src/main/scala/PromptChainMacros.scala @@ -145,7 +145,7 @@ private[cue4s] object PromptChainMacros: s => if s.trim.isEmpty then None else value(s) val label = ${ hints.name }.getOrElse($nm) - val prompt = Prompt.Input(label, validate) + val prompt = Prompt.Input(label).validate(validate) PromptStep[Tuple, String]( prompt, @@ -160,7 +160,8 @@ private[cue4s] object PromptChainMacros: val label = ${ hints.name }.getOrElse($nm) val prompt = - if ${ hints.options }.isEmpty then Prompt.Input(label, validate) + if ${ hints.options }.isEmpty then + Prompt.Input(label).validate(validate) else Prompt.SingleChoice(label, ${ hints.options }.getOrElse(Nil)) PromptStep[Tuple, String]( diff --git a/modules/core/src/main/scala/PromptFramework.scala b/modules/core/src/main/scala/PromptFramework.scala index 6e542bc..ab91449 100644 --- a/modules/core/src/main/scala/PromptFramework.scala +++ b/modules/core/src/main/scala/PromptFramework.scala @@ -1,44 +1,93 @@ -/* - * Copyright 2023 Neandertech - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - package cue4s -private[cue4s] trait PromptFramework(terminal: Terminal, out: Output): +private[cue4s] trait PromptFramework[Result](terminal: Terminal, out: Output): + self => type PromptState - type Result def initialState: PromptState - def handleEvent(event: Event): Next[Result] - def renderState(state: PromptState): List[String] + def handleEvent(event: Event): PromptAction[Result] + def renderState(state: PromptState, error: Option[PromptError]): List[String] def isRunning(state: PromptState): Boolean + def result(state: PromptState): Either[PromptError, Result] + + final def mapValidated[Derived]( + f: Result => Either[PromptError, Derived], + ): PromptFramework[Derived] = + new PromptFramework[Derived](terminal, out): + + override def initialState: PromptState = self.initialState + + override def renderState( + state: PromptState, + error: Option[PromptError], + ): List[String] = + self.renderState(state, result(state).left.toOption) + + override def isRunning(state: PromptState): Boolean = + self.isRunning(state) + + override def result(state: PromptState): Either[PromptError, Derived] = + self.result(state).flatMap(f) + + override type PromptState = self.PromptState + + override def handleEvent( + event: Event, + ): PromptAction[Derived] = + self.handleEvent(event) match + case self.PromptAction.Submit(submit) => + self.result(currentState()) match + case Left(value) => PromptAction.Continue + case Right(value) => + val finish = submit(value) + f(value) match + case Left(value) => PromptAction.Continue + case Right(value) => + PromptAction.Submit(_ => finish) + + case self.PromptAction.Start => PromptAction.Start + case self.PromptAction.Stop => PromptAction.Stop + case self.PromptAction.Continue => PromptAction.Continue + case self.PromptAction.Update(f) => + PromptAction.Update(f) + case self.PromptAction.UpdateAndStop(f) => + PromptAction.UpdateAndStop(f) + + end mapValidated + final val handler = new Handler[Result]: - def apply(event: Event) = handleEvent(event) + override def apply(ev: Event): Next[Result] = + handleEvent(ev) match + case PromptAction.Continue => Next.Continue + case PromptAction.Stop => Next.Stop + case PromptAction.Start => + printPrompt() + Next.Continue + case PromptAction.UpdateAndStop(f) => + stateTransition(f) + printPrompt() + Next.Stop + case PromptAction.Submit(finish) => + result(currentState()) match + case Left(value) => Next.Continue + case Right(value) => + stateTransition(finish(value)) + printPrompt() + Next.Done(value) + + case PromptAction.Update(f) => + stateTransition(f) + printPrompt() + Next.Continue final def currentState(): PromptState = state.current final def stateTransition(s: PromptState => PromptState) = state = state.nextFn(s) - rendering = rendering.nextFn: currentRendering => - val newRendering = renderState(state.current) - if newRendering.length < currentRendering.length then - newRendering ++ List.fill( - currentRendering.length - newRendering.length - )("") - else newRendering + rendering = rendering.next( + renderState(state.current, result(state.current).left.toOption), + ) end stateTransition final def printPrompt() = @@ -49,10 +98,39 @@ private[cue4s] trait PromptFramework(terminal: Terminal, out: Output): // initial print rendering.current.foreach(out.outLn) moveUp(rendering.current.length).moveHorizontalTo(0) - case Some(value) => + case Some(previousRendering) => + val paddingLength = + (previousRendering.length - rendering.current.length).max(0) + + inline def pad(n: Int) = List.fill(n)("") + + val (current, previous) = + if rendering.current.length > previousRendering.length then + ( + rendering.current, + previousRendering ++ pad( + rendering.current.length - previousRendering.length, + ), + ) + else + ( + rendering.current ++ pad( + previousRendering.length - rendering.current.length, + ), + previousRendering, + ) + + out.logLn( + current + .zip(previous) + .toString(), + ) + + out.logLn(paddingLength.toString) + def render = - rendering.current - .zip(value) + current + .zip(previous) .foreach: (line, oldLine) => if line != oldLine then moveHorizontalTo(0).eraseEntireLine() @@ -61,19 +139,29 @@ private[cue4s] trait PromptFramework(terminal: Terminal, out: Output): if isRunning(currentState()) then render - moveUp(rendering.current.length).moveHorizontalTo(0) + moveUp(current.length).moveHorizontalTo(0) else // we are finished render + // out.logLn(paddingLength) // do not leave empty lines behind - move cursor up - moveUp(rendering.current.reverse.takeWhile(_.isEmpty()).length) - .moveHorizontalTo(0) + moveUp(paddingLength).moveHorizontalTo(0) + + end if end match end printPrompt private var state = Transition( - initialState + initialState, ) - private var rendering = Transition(renderState(currentState())) - + private var rendering = Transition( + renderState(currentState(), result(currentState()).left.toOption), + ) + enum PromptAction[-Result]: + case Submit(f: Result => PromptState => PromptState) + case Update(f: PromptState => PromptState) + case UpdateAndStop(f: PromptState => PromptState) + case Continue + case Stop + case Start end PromptFramework diff --git a/modules/core/src/main/scalajs/PromptsPlatform.scala b/modules/core/src/main/scalajs/PromptsPlatform.scala index 17d9e01..ac91b91 100644 --- a/modules/core/src/main/scalajs/PromptsPlatform.scala +++ b/modules/core/src/main/scalajs/PromptsPlatform.scala @@ -25,8 +25,8 @@ private trait PromptsPlatform: def future[R]( prompt: Prompt[R] )(using ExecutionContext): Future[Completion[R]] = - val handler = prompt.handler(terminal, out, colors) + val framework = prompt.framework(terminal, out, colors) - inputProvider.evaluateFuture(handler) + inputProvider.evaluateFuture(framework.handler) end future end PromptsPlatform diff --git a/modules/core/src/snapshots/core/derived_validated_input b/modules/core/src/snapshots/core/derived_validated_input new file mode 100644 index 0000000..97e589e --- /dev/null +++ b/modules/core/src/snapshots/core/derived_validated_input @@ -0,0 +1,96 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('g') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('d') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo ┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go ┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g ┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('b') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > b ┃ +┃look up, it's not b ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > bl ┃ +┃look up, it's not bl ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('u') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blu ┃ +┃look up, it's not blu ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('e') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ What color is the sky? blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/core/number_input b/modules/core/src/snapshots/core/number_input new file mode 100644 index 0000000..fb05b07 --- /dev/null +++ b/modules/core/src/snapshots/core/number_input @@ -0,0 +1,162 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3. ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3. ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('h') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('1') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31. ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31. ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('2') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 2 ┃ +┃must be no less than 5.0 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('5') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25. ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25.0 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ think of a number 25.0 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/core/promptchain b/modules/core/src/snapshots/core/promptchain index 18ce971..58c5fc2 100644 --- a/modules/core/src/snapshots/core/promptchain +++ b/modules/core/src/snapshots/core/promptchain @@ -32,6 +32,7 @@ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━┓ ┃✔ Sir...? hello┃ ┃ ┃ +┃ ┃ ┗━━━━━━━━━━━━━━━┛ Event.Init diff --git a/modules/core/src/snapshots/coreJS/derived_validated_input b/modules/core/src/snapshots/coreJS/derived_validated_input new file mode 100644 index 0000000..97e589e --- /dev/null +++ b/modules/core/src/snapshots/coreJS/derived_validated_input @@ -0,0 +1,96 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('g') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('d') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo ┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go ┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g ┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('b') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > b ┃ +┃look up, it's not b ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > bl ┃ +┃look up, it's not bl ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('u') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blu ┃ +┃look up, it's not blu ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('e') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ What color is the sky? blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/coreJS/number_input b/modules/core/src/snapshots/coreJS/number_input new file mode 100644 index 0000000..e23c634 --- /dev/null +++ b/modules/core/src/snapshots/coreJS/number_input @@ -0,0 +1,162 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.┃ +┃must be no less than 5┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3. ┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('h') +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('1') +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.┃ +┃must be no more than 30┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0┃ +┃must be no more than 30 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0┃ +┃must be no more than 30 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31. ┃ +┃must be no more than 30 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('2') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 2 ┃ +┃must be no less than 5 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('5') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25. ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25.0┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ think of a number 25.0┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/coreJS/promptchain b/modules/core/src/snapshots/coreJS/promptchain index 18ce971..58c5fc2 100644 --- a/modules/core/src/snapshots/coreJS/promptchain +++ b/modules/core/src/snapshots/coreJS/promptchain @@ -32,6 +32,7 @@ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━┓ ┃✔ Sir...? hello┃ ┃ ┃ +┃ ┃ ┗━━━━━━━━━━━━━━━┛ Event.Init diff --git a/modules/core/src/snapshots/coreNative/derived_validated_input b/modules/core/src/snapshots/coreNative/derived_validated_input new file mode 100644 index 0000000..97e589e --- /dev/null +++ b/modules/core/src/snapshots/coreNative/derived_validated_input @@ -0,0 +1,96 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('g') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('o') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('d') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > good┃ +┃look up, it's not good ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > goo ┃ +┃look up, it's not goo ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > go ┃ +┃look up, it's not go ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > g ┃ +┃look up, it's not g ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > ┃ +┃look up, it's not ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('b') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > b ┃ +┃look up, it's not b ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > bl ┃ +┃look up, it's not bl ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('u') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blu ┃ +┃look up, it's not blu ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('e') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃What color is the sky? > blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ What color is the sky? blue┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/coreNative/number_input b/modules/core/src/snapshots/coreNative/number_input new file mode 100644 index 0000000..fb05b07 --- /dev/null +++ b/modules/core/src/snapshots/coreNative/number_input @@ -0,0 +1,162 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3. ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3.0 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3. ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('h') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > h ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('3') +┏━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('1') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31. ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31.0 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31. ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 31 ┃ +┃must be no more than 30.0┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 3 ┃ +┃must be no less than 5.0 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > ┃ +┃not a valid number ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('2') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 2 ┃ +┃must be no less than 5.0 ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('5') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('.') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25. ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('0') +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃think of a number > 25.0 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ think of a number 25.0 ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/snapshots/coreNative/promptchain b/modules/core/src/snapshots/coreNative/promptchain index 18ce971..58c5fc2 100644 --- a/modules/core/src/snapshots/coreNative/promptchain +++ b/modules/core/src/snapshots/coreNative/promptchain @@ -32,6 +32,7 @@ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━┓ ┃✔ Sir...? hello┃ ┃ ┃ +┃ ┃ ┗━━━━━━━━━━━━━━━┛ Event.Init diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index ec7cccf..5ed020a 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -94,23 +94,25 @@ class ExampleTests extends munit.FunSuite, TerminalTests: ) terminalTest("alternatives.cancel.input")( - Prompt.Input( - "how do you do fellow kids?", - value => + Prompt + .Input( + "how do you do fellow kids?" + ) + .validate(value => if value.length < 4 then Some(PromptError("too short!")) else None - ), + ), list(Event.Init, Event.Interrupt), Next.Stop ) terminalTestComplete("input")( - Prompt.Input( - "how do you do fellow kids?", - value => + Prompt + .Input("how do you do fellow kids?") + .validate(value => if value.length < 4 then Some(PromptError("too short!")) else None - ), + ), list( Event.Init, chars("go"), @@ -121,6 +123,50 @@ class ExampleTests extends munit.FunSuite, TerminalTests: "good" ) + terminalTestComplete("derived.validated.input")( + Prompt + .Input("What color is the sky?") + .mapValidated: + case "blue" => Right(true) + case other => Left(PromptError(s"look up, it's not $other")) + , + list( + Event.Init, + chars("go"), + ENTER, // prevents submission + chars("od"), + ENTER, + delete("good"), + chars("blue"), + ENTER + ), + true + ) + + terminalTestComplete("number.input")( + Prompt.NumberInput + .float("think of a number") + .min(5.0) + .max(30.0), + list( + Event.Init, + chars("3.0"), + ENTER, + delete("3.0"), + ENTER, + chars("h"), + ENTER, + delete("h"), + ENTER, + chars("31.0"), + ENTER, + delete("31.0"), + chars("25.0"), + ENTER + ), + 25.0f + ) + terminalTestComplete("alternatives.typing")( Prompt.SingleChoice( "How do you do fellow kids?", diff --git a/modules/core/src/test/scala/TerminalTests.scala b/modules/core/src/test/scala/TerminalTests.scala index 3719685..a557f49 100644 --- a/modules/core/src/test/scala/TerminalTests.scala +++ b/modules/core/src/test/scala/TerminalTests.scala @@ -9,6 +9,9 @@ trait TerminalTests extends MunitSnapshotsIntegration: case object LogTag extends munit.Tag("logtest") + def delete(str: String): List[Event] = + List.fill(str.length())(Event.Key(KeyEvent.DELETE)) + def chars(str: String): List[Event] = str.map(Event.Char(_)).toList @@ -92,10 +95,10 @@ trait TerminalTests extends MunitSnapshotsIntegration: val logger: String => Unit = if log then s => sb.append(s + "\n") else _ => () val term = TracingTerminal(Output.Delegate(_ => (), logger)) - val capturing = Output.Delegate(term.writer, s => sb.append(s + "\n")) + val capturing = Output.Delegate(term.writer, term.log(_)) val handler = - prompt.handler(term, capturing, colors = false) + prompt.framework(term, capturing, colors = false).handler var result = Option.empty[Next[T]] val eventsProcessed = List.newBuilder[Event] diff --git a/modules/example/src/main/scala-jvm-native/sync.scala b/modules/example/src/main/scala-jvm-native/sync.scala index 5951eba..836ac88 100644 --- a/modules/example/src/main/scala-jvm-native/sync.scala +++ b/modules/example/src/main/scala-jvm-native/sync.scala @@ -29,44 +29,49 @@ import cue4s.* val prompts = Prompts() - val day = prompts - .sync( - Prompt.SingleChoice( - "How was your day?", - List( - "amazing", - "productive", - "relaxing", - "stressful", - "exhausting", - "challenging", - "wonderful", - "uneventful", - "interesting", - "exciting", - "boring", - "demanding", - "satisfying", - "frustrating", - "peaceful", - "overwhelming", - "busy", - "calm", - "enjoyable", - "memorable", - "ordinary", - "fantastic", - "rewarding", - "chaotic" - ), - windowSize = 7 - ) - ) - .toOption - info = info.copy(day = day) + // val day = prompts + // .sync( + // Prompt.SingleChoice( + // "How was your day?", + // List( + // "amazing", + // "productive", + // "relaxing", + // "stressful", + // "exhausting", + // "challenging", + // "wonderful", + // "uneventful", + // "interesting", + // "exciting", + // "boring", + // "demanding", + // "satisfying", + // "frustrating", + // "peaceful", + // "overwhelming", + // "busy", + // "calm", + // "enjoyable", + // "memorable", + // "ordinary", + // "fantastic", + // "rewarding", + // "chaotic" + // ), + // windowSize = 7 + // ) + // ) + // .toOption + // info = info.copy(day = day) + + val newValue = Prompt + .Input("Where do you work?") + .mapValidated: s => + Either.cond(s == "blue", true, PromptError("yooooo?!")) - val work = prompts.sync(Prompt.Input("Where do you work?")).toOption - info = info.copy(work = work) + val work = prompts.sync(newValue).toOption + // info = info.copy(work = work) val letters = prompts .sync( diff --git a/project/plugins.sbt b/project/plugins.sbt index 4ff4aec..11b70ca 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,10 +1,14 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") + addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") // Code quality addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") + addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") + addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") + addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") // Compiled documentation @@ -12,6 +16,7 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") // Scala.js and Scala Native addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") + addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16")