From af19c37d62fa890407f713e088a8b9528c4d9506 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 28 Jan 2024 12:08:17 +0000 Subject: [PATCH 01/32] Start refactoring --- build.sbt | 5 +- modules/core/src/main/scala/ANSI.scala | 220 +++++++++++++----- modules/core/src/main/scala/Completion.scala | 6 + modules/core/src/main/scala/Example.scala | 2 + .../core/src/main/scala/InputProvider.scala | 9 - modules/core/src/main/scala/Interactive.scala | 129 +--------- .../main/scala/InteractiveAlternatives.scala | 116 +++++++++ .../src/main/scala/InteractiveTextInput.scala | 57 +++++ modules/core/src/main/scala/Next.scala | 5 + modules/core/src/main/scala/Prompt.scala | 2 +- project/plugins.sbt | 2 +- 11 files changed, 353 insertions(+), 200 deletions(-) create mode 100644 modules/core/src/main/scala/Completion.scala create mode 100644 modules/core/src/main/scala/InteractiveAlternatives.scala create mode 100644 modules/core/src/main/scala/InteractiveTextInput.scala create mode 100644 modules/core/src/main/scala/Next.scala diff --git a/build.sbt b/build.sbt index 760c852..b3b686b 100644 --- a/build.sbt +++ b/build.sbt @@ -11,7 +11,7 @@ inThisBuild( organization := "com.indoorvivants", organizationName := "Anton Sviridov", homepage := Some( - url("https://github.com/indoorvivants/scala-library-template") + url("https://github.com/neandertech/proompts") ), startYear := Some(2023), licenses := List( @@ -74,7 +74,8 @@ lazy val core = projectMatrix ), scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0" + libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", + nativeConfig ~= (_.withIncrementalCompilation(true)) ) lazy val docs = projectMatrix diff --git a/modules/core/src/main/scala/ANSI.scala b/modules/core/src/main/scala/ANSI.scala index 8e02897..6545800 100644 --- a/modules/core/src/main/scala/ANSI.scala +++ b/modules/core/src/main/scala/ANSI.scala @@ -16,92 +16,188 @@ package com.indoorvivants.proompts -private[proompts] object ANSI: - final val ESC = '\u001b' - final val CSI = s"$ESC[" +trait Terminal: + self => + def cursorShow(): self.type + + def cursorHide(): self.type + + def screenClear(): self.type + + def moveUp(n: Int): self.type - inline def call(name: Char, inline args: Int*) = - s"$CSI${args.mkString(";")}$name" + def moveDown(n: Int): self.type - inline def m(args: Int*) = - call('m', args*) + def moveForward(n: Int): self.type - object cursor: - inline def show() = - s"$CSI?25h" + def moveBack(n: Int): self.type - inline def hide() = - s"$CSI?25l" + def moveNextLine(n: Int): self.type - object screen: - inline def clear() = - s"${ESC}c" + def movePreviousLine(n: Int): self.type - object move: - inline def up(n: Int) = - call('A', n) + def moveHorizontalTo(column: Int): self.type - inline def down(n: Int) = - call('B', n) + def moveToPosition(row: Int, column: Int): self.type - inline def forward(n: Int) = - call('C', n) + def eraseToEndOfLine(): self.type - inline def back(n: Int) = - call('D', n) + def eraseToBeginningOfLine(): self.type - inline def nextLine(n: Int) = - call('E', n) + def eraseEntireLine(): self.type - inline def previousLine(n: Int) = - call('F', n) + def eraseToEndOfScreen(): self.type - inline def horizontalTo(column: Int) = - call('G', column) + def eraseToBeginningOfScreen(): self.type - inline def position(row: Int, column: Int) = - call('H', row, column) - end move + def eraseEntireScreen(): self.type - object erase: - object line: - inline def apply(n: Int) = - call('K', n) + def save(): self.type - inline def toEndOfLine() = - apply(0) + def restore(): self.type +end Terminal - inline def toBeginningOfLine() = - apply(1) +object Terminal: + def ansi(writer: String => Unit) = ANSI(writer) - inline def entireLine() = - apply(2) - end line +class ANSI(writer: String => Unit) extends Terminal: + import ANSI.{ESC, CSI} - object display: - inline def apply(n: Int) = - call('J', n) + private inline def call(name: Char, inline args: Int*): this.type = + writer(s"$CSI${args.mkString(";")}$name") + this - inline def toEndOfScreen() = - apply(0) + private inline def call(v: String): this.type = + writer(v) + this - inline def toBeinningOfScreen() = - apply(1) + override inline def cursorHide(): this.type = call(s"$CSI?25h") + override inline def cursorShow(): this.type = call(s"$CSI?25h") - inline def entireScreen() = - apply(2) - end display - end erase + private inline def lineEraseMode(n: Int): this.type = + call('K', n) - inline def save() = - call('s') + private inline def screenEraseMode(n: Int): this.type = + call('K', n) - inline def restore() = - call('u') + override inline def eraseEntireLine(): this.type = lineEraseMode(2) + override inline def eraseEntireScreen(): this.type = screenEraseMode(2) - inline def withRestore[A](writer: String => Unit)(inline f: => A) = - writer(save()) + override inline def eraseToBeginningOfLine(): this.type = lineEraseMode(1) + override inline def eraseToBeginningOfScreen(): this.type = screenEraseMode(1) + + override inline def eraseToEndOfLine(): this.type = lineEraseMode(0) + override inline def eraseToEndOfScreen(): this.type = screenEraseMode(0) + + override inline def moveBack(n: Int): this.type = call('D', n) + override inline def moveDown(n: Int): this.type = call('B', n) + override inline def moveForward(n: Int): this.type = call('C', n) + override inline def moveHorizontalTo(column: Int): this.type = + call('G', column) + override inline def moveNextLine(n: Int): this.type = call('E', n) + override inline def movePreviousLine(n: Int): this.type = call('F', n) + override inline def moveToPosition(row: Int, column: Int): this.type = + call('H', row, column) + override inline def moveUp(n: Int): this.type = + call('A', n) + + override inline def restore(): this.type = call('u') + override inline def save(): this.type = call('s') + override inline def screenClear(): this.type = call(s"${ESC}c") + + inline def withRestore[A](inline f: => A) = + save() f - writer(restore()) + restore() end ANSI + +object ANSI: + final val ESC = '\u001b' + final val CSI = s"$ESC[" + +// inline def call(name: Char, inline args: Int*) = +// s"$CSI${args.mkString(";")}$name" + +// inline def m(args: Int*) = +// call('m', args*) + +// object cursor: +// inline def show() = +// s"$CSI?25h" + +// inline def hide() = +// s"$CSI?25l" + +// object screen: +// inline def clear() = +// s"${ESC}c" + +// object move: +// inline def up(n: Int) = +// call('A', n) + +// inline def down(n: Int) = +// call('B', n) + +// inline def forward(n: Int) = +// call('C', n) + +// inline def back(n: Int) = +// call('D', n) + +// inline def nextLine(n: Int) = +// call('E', n) + +// inline def previousLine(n: Int) = +// call('F', n) + +// inline def horizontalTo(column: Int) = +// call('G', column) + +// inline def position(row: Int, column: Int) = +// call('H', row, column) +// end move + +// object erase: +// object line: +// inline def apply(n: Int) = +// call('K', n) + +// inline def toEndOfLine() = +// apply(0) + +// inline def toBeginningOfLine() = +// apply(1) + +// inline def entireLine() = +// apply(2) +// end line + +// object display: +// inline def apply(n: Int) = +// call('J', n) + +// inline def toEndOfScreen() = +// apply(0) + +// inline def toBeinningOfScreen() = +// apply(1) + +// inline def entireScreen() = +// apply(2) +// end display +// end erase + +// inline def save() = +// call('s') + +// inline def restore() = +// call('u') + +// inline def withRestore[A](writer: String => Unit)(inline f: => A) = +// writer(save()) +// f +// writer(restore()) + +// end ANSI diff --git a/modules/core/src/main/scala/Completion.scala b/modules/core/src/main/scala/Completion.scala new file mode 100644 index 0000000..d6f018c --- /dev/null +++ b/modules/core/src/main/scala/Completion.scala @@ -0,0 +1,6 @@ +package com.indoorvivants.proompts + +enum Completion: + case Finished + case Interrupted + case Error(msg: String) diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 2135f0e..d8cdca1 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -11,6 +11,8 @@ import ANSI.* val result = InputProvider().attach(env => Interactive(prompt, env.writer).handler) + println(result) + // Process.stdout.write("how do you do") // Process.stdout.write(move.back(5)) // Process.stdout.write(erase.line.toEndOfLine()) diff --git a/modules/core/src/main/scala/InputProvider.scala b/modules/core/src/main/scala/InputProvider.scala index c4bb1bf..c7db3f7 100644 --- a/modules/core/src/main/scala/InputProvider.scala +++ b/modules/core/src/main/scala/InputProvider.scala @@ -1,14 +1,5 @@ package com.indoorvivants.proompts -enum Completion: - case Finished - case Interrupted - case Error(msg: String) - -enum Next: - case Stop, Continue - case Error(msg: String) - case class Environment(writer: String => Unit) abstract class Handler: diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 590dc46..4581982 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -1,142 +1,21 @@ package com.indoorvivants.proompts import ANSI.* -import com.indoorvivants.proompts.ANSI.move.horizontalTo def errln(o: Any) = System.err.println(o) class Interactive(var prompt: Prompt, writer: String => Unit): - def printPrompt() = - val lab = prompt.promptLabel - val lines = 0 - writer(move.horizontalTo(0)) - writer(erase.line.toEndOfLine()) - - errln(prompt) - errln(prompt.hashCode()) - prompt match - case Prompt.Input(label, state) => - writer(s"${fansi.Color.Cyan(lab)}${state.text}") - case p @ Prompt.Alternatives(label, alts, state) => - writer(s"${fansi.Color.Cyan(lab)}${state.text}") - withRestore(writer): - writer("\n") - - val filteredAlts = - alts.filter( - state.text.isEmpty() || _.toLowerCase().contains( - state.text.toLowerCase() - ) - ) - - errln(filteredAlts) - - val adjustedSelected = - state.selected.min(filteredAlts.length - 1).max(0) - - errln(adjustedSelected) - - val newState = - AlternativesState( - state.text, - selected = adjustedSelected, - showing = filteredAlts.length.min(1) - ) - - if filteredAlts.isEmpty then - writer(move.horizontalTo(0)) - writer(erase.line.toEndOfLine()) - writer(fansi.Underlined.On("no matches").toString) - else - filteredAlts.zipWithIndex.foreach: (alt, idx) => - writer(move.horizontalTo(0)) - writer(erase.line.toEndOfLine()) - - val view = - if idx == adjustedSelected then fansi.Color.Green("> " + alt) - else fansi.Bold.On("· " + alt) - writer(view.toString) - if idx != filteredAlts.length - 1 then writer("\n") - end if - - for _ <- 0 until state.showing - newState.showing do - writer(move.nextLine(1)) - writer(move.horizontalTo(0)) - writer(erase.line.toEndOfLine()) - prompt = p.copy(state = newState) - end match - end printPrompt - - def selectUp() = prompt match - case Prompt.Input(_, _) => - case p @ Prompt.Alternatives(_, _, state) => - prompt = - p.copy(state = state.copy(selected = (state.selected - 1).max(0))) - - def selectDown() = prompt match - case Prompt.Input(_, _) => - case p @ Prompt.Alternatives(_, _, state) => - prompt = - p.copy(state = state.copy(selected = (state.selected + 1).min(1000))) - - def appendText(t: Char) = - prompt match - case i @ Prompt.Input(label, state) => - prompt = i.copy(state = state.copy(text = state.text + t)) - case i @ Prompt.Alternatives(label, alts, state) => - prompt = i.copy(state = state.copy(text = state.text + t)) - - def trimText() = + val handler = prompt match - case i @ Prompt.Input(label, state) => - prompt = i.copy(state = - state.copy(text = state.text.take(state.text.length - 1)) - ) - case i @ Prompt.Alternatives(label, alts, state) => - prompt = i.copy(state = - state.copy(text = state.text.take(state.text.length - 1)) - ) + case p: Prompt.Input => InteractiveTextInput(p, writer).handler + case p: Prompt.Alternatives => InteractiveAlternatives(p, writer).handler - def handler = new Handler: - def apply(event: Event): Next = - errln(event) - event match - case Event.Init => - printPrompt() - Next.Continue - case Event.Key(KeyEvent.UP) => - selectUp() - printPrompt() - Next.Continue - case Event.Key(KeyEvent.DOWN) => - selectDown() - printPrompt() - Next.Continue - - case Event.Key(KeyEvent.ENTER) => // enter - Next.Stop - - case Event.Key(KeyEvent.DELETE) => // enter - trimText() - printPrompt() - Next.Continue - - case Event.Char(which) => - appendText(which.toChar) - printPrompt() - Next.Continue - - case _ => - Next.Continue - end match - end apply end Interactive -case class InputState(text: String) +case class TextInputState(text: String) case class AlternativesState( text: String, selected: Int, showing: Int ) - diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala new file mode 100644 index 0000000..2e4acdc --- /dev/null +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -0,0 +1,116 @@ +package com.indoorvivants.proompts + +import ANSI.* + +class InteractiveAlternatives( + prompt: Prompt.Alternatives, + writer: String => Unit +): + val terminal = Terminal.ansi(writer) + val lab = prompt.promptLabel + var state = prompt.state + + def printPrompt() = + val lines = 0 + + import terminal.* + + moveHorizontalTo(0) + eraseToEndOfLine() + + errln(prompt) + + writer(s"${fansi.Color.Cyan(lab)}${state.text}") + withRestore: + writer("\n") + + val filteredAlts = + prompt.alts.filter( + state.text.isEmpty() || _.toLowerCase().contains( + state.text.toLowerCase() + ) + ) + + errln(filteredAlts) + + val adjustedSelected = + state.selected.min(filteredAlts.length - 1).max(0) + + errln(adjustedSelected) + + val newState = + AlternativesState( + state.text, + selected = adjustedSelected, + showing = filteredAlts.length.min(1) + ) + + if filteredAlts.isEmpty then + moveHorizontalTo(0) + eraseToEndOfLine() + writer(fansi.Underlined.On("no matches").toString) + else + filteredAlts.zipWithIndex.foreach: (alt, idx) => + moveHorizontalTo(0) + eraseToEndOfLine() + val view = + if idx == adjustedSelected then fansi.Color.Green("> " + alt) + else fansi.Bold.On("· " + alt) + writer(view.toString) + if idx != filteredAlts.length - 1 then writer("\n") + end if + + for _ <- 0 until state.showing - newState.showing do + moveNextLine(1) + moveHorizontalTo(0) + eraseToEndOfLine() + state = newState + end printPrompt + + val handler = new Handler: + def apply(event: Event): Next = + errln(event) + event match + case Event.Init => + printPrompt() + Next.Continue + case Event.Key(KeyEvent.UP) => + selectUp() + printPrompt() + Next.Continue + case Event.Key(KeyEvent.DOWN) => + selectDown() + printPrompt() + Next.Continue + + case Event.Key(KeyEvent.ENTER) => // enter + Next.Stop + + case Event.Key(KeyEvent.DELETE) => // enter + trimText() + printPrompt() + Next.Continue + + case Event.Char(which) => + appendText(which.toChar) + printPrompt() + Next.Continue + + case _ => + Next.Continue + end match + end apply + + def selectUp() = state = state.copy(selected = (state.selected - 1).max(0)) + + def selectDown() = state = + state.copy(selected = (state.selected + 1).min(1000)) + + def appendText(t: Char) = + state = state.copy(text = state.text + t) + + def trimText() = + state = state.copy(text = state.text.take(state.text.length - 1)) + +end InteractiveAlternatives + diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala new file mode 100644 index 0000000..e002120 --- /dev/null +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -0,0 +1,57 @@ +package com.indoorvivants.proompts + +import ANSI.* + +class InteractiveTextInput( + prompt: Prompt.Input, + writer: String => Unit +): + val terminal = Terminal.ansi(writer) + val lab = prompt.promptLabel + var state = prompt.state + + def printPrompt() = + val lines = 0 + + import terminal.* + + moveHorizontalTo(0) + eraseToEndOfLine() + + errln(prompt) + + writer(s"${fansi.Color.Cyan(lab)}${state.text}") + end printPrompt + + val handler = new Handler: + def apply(event: Event): Next = + errln(event) + event match + case Event.Init => + printPrompt() + Next.Continue + + case Event.Key(KeyEvent.ENTER) => // enter + Next.Stop + + case Event.Key(KeyEvent.DELETE) => // enter + trimText() + printPrompt() + Next.Continue + + case Event.Char(which) => + appendText(which.toChar) + printPrompt() + Next.Continue + + case _ => + Next.Continue + end match + end apply + + def appendText(t: Char) = + state = state.copy(text = state.text + t) + + def trimText() = + state = state.copy(text = state.text.take(state.text.length - 1)) +end InteractiveTextInput diff --git a/modules/core/src/main/scala/Next.scala b/modules/core/src/main/scala/Next.scala new file mode 100644 index 0000000..a374f5d --- /dev/null +++ b/modules/core/src/main/scala/Next.scala @@ -0,0 +1,5 @@ +package com.indoorvivants.proompts + +enum Next: + case Stop, Continue + case Error(msg: String) diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 2113f7e..a07db6c 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -3,7 +3,7 @@ package com.indoorvivants.proompts import ANSI.* enum Prompt(label: String): - case Input(label: String, state: InputState) extends Prompt(label) + case Input(label: String, state: TextInputState) extends Prompt(label) case Alternatives(label: String, alts: List[String], state: AlternativesState) extends Prompt(label) diff --git a/project/plugins.sbt b/project/plugins.sbt index e261e13..3e74b36 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -15,4 +15,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") // Scala.js and Scala Native addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.15") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") From eee606461c9599c327eef8c20d27b521ef04207d Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 28 Jan 2024 13:23:42 +0000 Subject: [PATCH 02/32] Add tracing terminal --- build.sbt | 1 + .../src/main/scala/AlternativesState.scala | 23 ++++ .../scala/{ANSI.scala => AnsiTerminal.scala} | 54 +------- .../core/src/main/scala/CharCollector.scala | 18 ++- modules/core/src/main/scala/Completion.scala | 16 +++ modules/core/src/main/scala/Event.scala | 16 +++ modules/core/src/main/scala/Example.scala | 47 +++++-- .../core/src/main/scala/InputProvider.scala | 16 +++ modules/core/src/main/scala/Interactive.scala | 27 ++-- .../main/scala/InteractiveAlternatives.scala | 29 ++-- .../src/main/scala/InteractiveTextInput.scala | 21 ++- modules/core/src/main/scala/Next.scala | 16 +++ modules/core/src/main/scala/Prompt.scala | 28 ++-- modules/core/src/main/scala/Terminal.scala | 61 ++++++++ .../core/src/main/scala/TextInputState.scala | 19 +++ .../core/src/main/scala/TracingTerminal.scala | 130 ++++++++++++++++++ .../main/scalajs/InputProviderPlatform.scala | 21 ++- .../src/main/scalajs/NodeJSBindings.scala | 20 ++- modules/core/src/main/scalajvm/Ext.scala | 16 +++ modules/core/src/main/scalanative/Ext.scala | 26 +++- .../core/src/test/scala/ExampleTests.scala | 4 +- project/plugins.sbt | 4 +- 22 files changed, 507 insertions(+), 106 deletions(-) create mode 100644 modules/core/src/main/scala/AlternativesState.scala rename modules/core/src/main/scala/{ANSI.scala => AnsiTerminal.scala} (81%) create mode 100644 modules/core/src/main/scala/Terminal.scala create mode 100644 modules/core/src/main/scala/TextInputState.scala create mode 100644 modules/core/src/main/scala/TracingTerminal.scala diff --git a/build.sbt b/build.sbt index b3b686b..4f24a5c 100644 --- a/build.sbt +++ b/build.sbt @@ -72,6 +72,7 @@ lazy val core = projectMatrix scalaVersion, scalaBinaryVersion ), + scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", diff --git a/modules/core/src/main/scala/AlternativesState.scala b/modules/core/src/main/scala/AlternativesState.scala new file mode 100644 index 0000000..56047bf --- /dev/null +++ b/modules/core/src/main/scala/AlternativesState.scala @@ -0,0 +1,23 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +case class AlternativesState( + text: String, + selected: Int, + showing: Int +) diff --git a/modules/core/src/main/scala/ANSI.scala b/modules/core/src/main/scala/AnsiTerminal.scala similarity index 81% rename from modules/core/src/main/scala/ANSI.scala rename to modules/core/src/main/scala/AnsiTerminal.scala index 6545800..733f69e 100644 --- a/modules/core/src/main/scala/ANSI.scala +++ b/modules/core/src/main/scala/AnsiTerminal.scala @@ -1,5 +1,5 @@ /* - * Copyright 2020 Anton Sviridov + * Copyright 2023 Anton Sviridov * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,52 +16,8 @@ package com.indoorvivants.proompts -trait Terminal: - self => - def cursorShow(): self.type - - def cursorHide(): self.type - - def screenClear(): self.type - - def moveUp(n: Int): self.type - - def moveDown(n: Int): self.type - - def moveForward(n: Int): self.type - - def moveBack(n: Int): self.type - - def moveNextLine(n: Int): self.type - - def movePreviousLine(n: Int): self.type - - def moveHorizontalTo(column: Int): self.type - - def moveToPosition(row: Int, column: Int): self.type - - def eraseToEndOfLine(): self.type - - def eraseToBeginningOfLine(): self.type - - def eraseEntireLine(): self.type - - def eraseToEndOfScreen(): self.type - - def eraseToBeginningOfScreen(): self.type - - def eraseEntireScreen(): self.type - - def save(): self.type - - def restore(): self.type -end Terminal - -object Terminal: - def ansi(writer: String => Unit) = ANSI(writer) - -class ANSI(writer: String => Unit) extends Terminal: - import ANSI.{ESC, CSI} +class AnsiTerminal(writer: String => Unit) extends Terminal: + import AnsiTerminal.{ESC, CSI} private inline def call(name: Char, inline args: Int*): this.type = writer(s"$CSI${args.mkString(";")}$name") @@ -110,9 +66,9 @@ class ANSI(writer: String => Unit) extends Terminal: f restore() -end ANSI +end AnsiTerminal -object ANSI: +object AnsiTerminal: final val ESC = '\u001b' final val CSI = s"$ESC[" diff --git a/modules/core/src/main/scala/CharCollector.scala b/modules/core/src/main/scala/CharCollector.scala index 257b84a..380ceae 100644 --- a/modules/core/src/main/scala/CharCollector.scala +++ b/modules/core/src/main/scala/CharCollector.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts object CharCollector: @@ -27,7 +43,7 @@ object CharCollector: curState match case State.Init => char match - case ANSI.ESC => + case AnsiTerminal.ESC => (State.ESC_Started, Next.Continue) case 10 => emit(Event.Key(KeyEvent.ENTER)) diff --git a/modules/core/src/main/scala/Completion.scala b/modules/core/src/main/scala/Completion.scala index d6f018c..992bad3 100644 --- a/modules/core/src/main/scala/Completion.scala +++ b/modules/core/src/main/scala/Completion.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts enum Completion: diff --git a/modules/core/src/main/scala/Event.scala b/modules/core/src/main/scala/Event.scala index 4039e5d..b58704f 100644 --- a/modules/core/src/main/scala/Event.scala +++ b/modules/core/src/main/scala/Event.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts enum Event: diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index d8cdca1..e4b1541 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -1,19 +1,42 @@ -package com.indoorvivants.proompts +/* + * Copyright 2023 Anton Sviridov + * + * 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. + */ -import ANSI.* +package com.indoorvivants.proompts @main def hello = - var prompt = Prompt.Alternatives( - "How would you describe yourself?", - List("Sexylicious", "Shmexy", "Pexying") - ) + val term = TracingTerminal() + + val write = term.writer + + write("hello") + write("world\ntest") + write("yo") + write("bla") + // write("world\n") + // write("yo") + + // val prompt = Prompt.Alternatives( + // "How would you describe yourself?", + // List("Sexylicious", "Shmexy", "Pexying") + // ) - val result = - InputProvider().attach(env => Interactive(prompt, env.writer).handler) + // println( + // InputProvider().attach(env => Interactive(prompt, env.writer).handler) + // ) - println(result) + println(term.getPretty()) - // Process.stdout.write("how do you do") - // Process.stdout.write(move.back(5)) - // Process.stdout.write(erase.line.toEndOfLine()) end hello diff --git a/modules/core/src/main/scala/InputProvider.scala b/modules/core/src/main/scala/InputProvider.scala index c7db3f7..3b861d1 100644 --- a/modules/core/src/main/scala/InputProvider.scala +++ b/modules/core/src/main/scala/InputProvider.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts case class Environment(writer: String => Unit) diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 4581982..2a3b556 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -1,21 +1,28 @@ -package com.indoorvivants.proompts +/* + * Copyright 2023 Anton Sviridov + * + * 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. + */ -import ANSI.* +package com.indoorvivants.proompts def errln(o: Any) = System.err.println(o) class Interactive(var prompt: Prompt, writer: String => Unit): - val handler = + def handler = prompt match case p: Prompt.Input => InteractiveTextInput(p, writer).handler case p: Prompt.Alternatives => InteractiveAlternatives(p, writer).handler end Interactive - -case class TextInputState(text: String) -case class AlternativesState( - text: String, - selected: Int, - showing: Int -) diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 2e4acdc..c40cd05 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -1,6 +1,20 @@ -package com.indoorvivants.proompts +/* + * Copyright 2023 Anton Sviridov + * + * 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. + */ -import ANSI.* +package com.indoorvivants.proompts class InteractiveAlternatives( prompt: Prompt.Alternatives, @@ -8,7 +22,7 @@ class InteractiveAlternatives( ): val terminal = Terminal.ansi(writer) val lab = prompt.promptLabel - var state = prompt.state + var state = AlternativesState("", 0, prompt.alts.length) def printPrompt() = val lines = 0 @@ -18,8 +32,6 @@ class InteractiveAlternatives( moveHorizontalTo(0) eraseToEndOfLine() - errln(prompt) - writer(s"${fansi.Color.Cyan(lab)}${state.text}") withRestore: writer("\n") @@ -31,13 +43,11 @@ class InteractiveAlternatives( ) ) - errln(filteredAlts) + errln(s"Filtered alternatives: ${filteredAlts}") val adjustedSelected = state.selected.min(filteredAlts.length - 1).max(0) - errln(adjustedSelected) - val newState = AlternativesState( state.text, @@ -67,7 +77,7 @@ class InteractiveAlternatives( state = newState end printPrompt - val handler = new Handler: + def handler = new Handler: def apply(event: Event): Next = errln(event) event match @@ -113,4 +123,3 @@ class InteractiveAlternatives( state = state.copy(text = state.text.take(state.text.length - 1)) end InteractiveAlternatives - diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala index e002120..62b83ca 100644 --- a/modules/core/src/main/scala/InteractiveTextInput.scala +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -1,6 +1,20 @@ -package com.indoorvivants.proompts +/* + * Copyright 2023 Anton Sviridov + * + * 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. + */ -import ANSI.* +package com.indoorvivants.proompts class InteractiveTextInput( prompt: Prompt.Input, @@ -8,10 +22,9 @@ class InteractiveTextInput( ): val terminal = Terminal.ansi(writer) val lab = prompt.promptLabel - var state = prompt.state + var state = TextInputState("") def printPrompt() = - val lines = 0 import terminal.* diff --git a/modules/core/src/main/scala/Next.scala b/modules/core/src/main/scala/Next.scala index a374f5d..30c4991 100644 --- a/modules/core/src/main/scala/Next.scala +++ b/modules/core/src/main/scala/Next.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts enum Next: diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index a07db6c..66156a7 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -1,16 +1,24 @@ -package com.indoorvivants.proompts +/* + * Copyright 2023 Anton Sviridov + * + * 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. + */ -import ANSI.* +package com.indoorvivants.proompts enum Prompt(label: String): - case Input(label: String, state: TextInputState) extends Prompt(label) - case Alternatives(label: String, alts: List[String], state: AlternativesState) - extends Prompt(label) + case Input(label: String) extends Prompt(label) + case Alternatives(label: String, alts: List[String]) extends Prompt(label) def promptLabel = label + " > " - -object Prompt: - object Alternatives: - def apply(label: String, alts: List[String]): Prompt = - Prompt.Alternatives(label, alts, AlternativesState("", 0, alts.length)) diff --git a/modules/core/src/main/scala/Terminal.scala b/modules/core/src/main/scala/Terminal.scala new file mode 100644 index 0000000..423fd6f --- /dev/null +++ b/modules/core/src/main/scala/Terminal.scala @@ -0,0 +1,61 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +trait Terminal: + self => + def cursorShow(): self.type + + def cursorHide(): self.type + + def screenClear(): self.type + + def moveUp(n: Int): self.type + + def moveDown(n: Int): self.type + + def moveForward(n: Int): self.type + + def moveBack(n: Int): self.type + + def moveNextLine(n: Int): self.type + + def movePreviousLine(n: Int): self.type + + def moveHorizontalTo(column: Int): self.type + + def moveToPosition(row: Int, column: Int): self.type + + def eraseToEndOfLine(): self.type + + def eraseToBeginningOfLine(): self.type + + def eraseEntireLine(): self.type + + def eraseToEndOfScreen(): self.type + + def eraseToBeginningOfScreen(): self.type + + def eraseEntireScreen(): self.type + + def save(): self.type + + def restore(): self.type +end Terminal + +object Terminal: + def ansi(writer: String => Unit) = AnsiTerminal(writer) diff --git a/modules/core/src/main/scala/TextInputState.scala b/modules/core/src/main/scala/TextInputState.scala new file mode 100644 index 0000000..c098745 --- /dev/null +++ b/modules/core/src/main/scala/TextInputState.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +case class TextInputState(text: String) diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala new file mode 100644 index 0000000..8c7a26e --- /dev/null +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -0,0 +1,130 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +import scala.collection.mutable + +class TracingTerminal extends Terminal: + val WIDTH = 120 + val HEIGHT = 500 + var currentHeight = 0 + var currentWidth = 0 + var currentLine = 0 + var currentColumn = 0 + + def currentIndex() = WIDTH * currentLine + currentColumn + var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') + + override def screenClear(): this.type = ??? + + override def movePreviousLine(n: Int): this.type = ??? + + override def eraseToBeginningOfLine(): this.type = ??? + + override def eraseToEndOfLine(): this.type = ??? + + override def cursorShow(): this.type = ??? + + override def moveHorizontalTo(column: Int): this.type = ??? + + override def eraseEntireScreen(): this.type = ??? + + override def moveToPosition(row: Int, column: Int): this.type = ??? + + override def eraseToBeginningOfScreen(): this.type = ??? + + override def eraseEntireLine(): this.type = ??? + + override def moveBack(n: Int): this.type = ??? + + override def moveUp(n: Int): this.type = ??? + + override def save(): this.type = ??? + + override def cursorHide(): this.type = ??? + + override def moveDown(n: Int): this.type = ??? + + override def eraseToEndOfScreen(): this.type = ??? + + override def moveForward(n: Int): this.type = ??? + + override def moveNextLine(n: Int): this.type = ??? + + override def restore(): this.type = ??? + + def getLine(i: Int): String = + val start = WIDTH * i + new String(INTERNAL.slice(start, start + currentWidth)) + + def writer: String => Unit = + + val simpleWriter: String => Unit = l => + errln(s"Writing at $currentLine x $currentColumn: $l") + val newCurrentWidth = currentWidth max (currentColumn + l.length) + if newCurrentWidth > WIDTH then todo("line length overflow") + else + val start = currentIndex() + for idx <- 0 until l.length() do INTERNAL(start + idx) = l.charAt(idx) + currentColumn += l.length() + currentWidth = newCurrentWidth + + val multilineWriter: String => Unit = l => + val lines = l.linesIterator.zipWithIndex.toList + lines.foreach: (line, idx) => + simpleWriter(line) + currentColumn = 0 + currentLine += 1 + currentHeight = currentHeight max currentLine + + l => + if l.contains("\n") then multilineWriter(l) + else simpleWriter(l) + end writer + + def get(): String = + val cur = mutable.StringBuilder() + + for line <- 0 to currentHeight + do cur.append(getLine(line) + "\n") + + cur.result() + + def getPretty(): String = + val cur = mutable.StringBuilder() + val raw = get() + val maxLineLength = raw.linesIterator.map(_.length).maxOption.getOrElse(1) + cur.append("┏") + cur.append("━" * maxLineLength) + cur.append("┓\n") + + raw.linesIterator.foreach: line => + cur.append("┃") + cur.append(line) + cur.append("┃") + cur.append("\n") + + cur.append("┗") + cur.append("━" * maxLineLength) + cur.append("┛") + + cur.result() + end getPretty + +end TracingTerminal + +def todo(msg: String) = throw new NotImplementedError(s"not implemented: $msg") diff --git a/modules/core/src/main/scalajs/InputProviderPlatform.scala b/modules/core/src/main/scalajs/InputProviderPlatform.scala index da511f7..149ff04 100644 --- a/modules/core/src/main/scalajs/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajs/InputProviderPlatform.scala @@ -1,11 +1,29 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts -import scalajs.js import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport + import com.indoorvivants.proompts.CharCollector.State import com.indoorvivants.proompts.CharCollector.decode +import scalajs.js + trait InputProviderPlatform: def apply(): InputProvider = new InputProvider: override def attach( @@ -80,4 +98,3 @@ trait InputProviderPlatform: end attach override def close(): Unit = () end InputProviderPlatform - diff --git a/modules/core/src/main/scalajs/NodeJSBindings.scala b/modules/core/src/main/scalajs/NodeJSBindings.scala index f8358c5..bddcaaf 100644 --- a/modules/core/src/main/scalajs/NodeJSBindings.scala +++ b/modules/core/src/main/scalajs/NodeJSBindings.scala @@ -1,11 +1,29 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts -import scalajs.js import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport + import com.indoorvivants.proompts.CharCollector.State import com.indoorvivants.proompts.CharCollector.decode +import scalajs.js + @js.native trait ReadStream extends js.Object: def isRaw: js.UndefOr[Boolean] = js.native diff --git a/modules/core/src/main/scalajvm/Ext.scala b/modules/core/src/main/scalajvm/Ext.scala index cb84be9..98908fa 100644 --- a/modules/core/src/main/scalajvm/Ext.scala +++ b/modules/core/src/main/scalajvm/Ext.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts trait InputProviderPlatform: diff --git a/modules/core/src/main/scalanative/Ext.scala b/modules/core/src/main/scalanative/Ext.scala index c50354b..e188b1e 100644 --- a/modules/core/src/main/scalanative/Ext.scala +++ b/modules/core/src/main/scalanative/Ext.scala @@ -1,14 +1,32 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts +import scala.util.boundary + import scalanative.libc.stdio.getchar import scalanative.unsafe.* import scalanative.posix.termios.* -import scala.util.boundary, boundary.break - +import boundary.break import CharCollector.* class NativeInputProvider extends InputProvider: override def attach(f: Environment => Handler): Completion = + errln("what the fuck") changemode(1) var lastRead = 0 @@ -18,7 +36,7 @@ class NativeInputProvider extends InputProvider: lastRead val env = Environment(str => - System.err.println(str.getBytes().toList) + // System.err.println(str.toCharArray().toList) System.out.write(str.getBytes()) System.out.flush() ) @@ -42,6 +60,8 @@ class NativeInputProvider extends InputProvider: while read() != 0 do + errln("what") + val (newState, result) = decode(state, lastRead) result match diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index 709f295..160b65d 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -2,6 +2,6 @@ package com.indoorvivants.library class ExampleTests extends munit.FunSuite: test("test1") { - assert(implicitly[TypeClass[Int]].isPrimitive == true) - assert(implicitly[TypeClass[Boolean]].isPrimitive == true) + // assert(implicitly[TypeClass[Int]].isPrimitive == true) + // assert(implicitly[TypeClass[Boolean]].isPrimitive == true) } diff --git a/project/plugins.sbt b/project/plugins.sbt index 3e74b36..2d6f912 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -2,8 +2,8 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") // Code quality -addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.2") -addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % "0.3.6") +//addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.2") +addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % "0.3.6") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") From 59239b9db9d26d80a7f698ebe6f4d778bf75d978 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 28 Jan 2024 13:42:22 +0000 Subject: [PATCH 03/32] WIP --- modules/core/src/main/scala/Example.scala | 10 ++++++++- .../core/src/main/scala/TracingTerminal.scala | 21 +++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index e4b1541..3ab50f4 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -25,6 +25,15 @@ package com.indoorvivants.proompts write("world\ntest") write("yo") write("bla") + write("s") + + println(term.getPretty()) + + term.moveBack(3) + + write("wazooop") + + println(term.getPretty()) // write("world\n") // write("yo") @@ -37,6 +46,5 @@ package com.indoorvivants.proompts // InputProvider().attach(env => Interactive(prompt, env.writer).handler) // ) - println(term.getPretty()) end hello diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index 8c7a26e..f7821f7 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -29,7 +29,9 @@ class TracingTerminal extends Terminal: def currentIndex() = WIDTH * currentLine + currentColumn var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') - override def screenClear(): this.type = ??? + override def screenClear(): this.type = + INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') + this override def movePreviousLine(n: Int): this.type = ??? @@ -49,7 +51,10 @@ class TracingTerminal extends Terminal: override def eraseEntireLine(): this.type = ??? - override def moveBack(n: Int): this.type = ??? + override def moveBack(n: Int): this.type = + errln(s"Back $n characters") + currentColumn = (currentColumn - n) max 0 + this override def moveUp(n: Int): this.type = ??? @@ -112,9 +117,17 @@ class TracingTerminal extends Terminal: cur.append("━" * maxLineLength) cur.append("┓\n") - raw.linesIterator.foreach: line => + raw.linesIterator.zipWithIndex.foreach: (line, idx) => cur.append("┃") - cur.append(line) + if idx == currentLine then + val (pre, after) = line.splitAt(currentColumn) + cur.append(pre) + if currentColumn < currentWidth then + cur.append( + fansi.Color.Black(fansi.Back.White(line(currentColumn).toString())) + ) + cur.append(after.drop(1)) + else cur.append(line) cur.append("┃") cur.append("\n") From bab8ec54c8aa4bfded84bb5b3bb79b18a3133187 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 7 Feb 2024 20:20:28 +0000 Subject: [PATCH 04/32] Implement all the other methods --- .../core/src/main/scala/AnsiTerminal.scala | 5 -- modules/core/src/main/scala/Example.scala | 47 +++++++++--- modules/core/src/main/scala/Terminal.scala | 6 ++ .../core/src/main/scala/TracingTerminal.scala | 75 +++++++++++++++---- ...{Ext.scala => InputProviderPlatform.scala} | 0 ...{Ext.scala => InputProviderPlatform.scala} | 25 +------ 6 files changed, 105 insertions(+), 53 deletions(-) rename modules/core/src/main/scalajvm/{Ext.scala => InputProviderPlatform.scala} (100%) rename modules/core/src/main/scalanative/{Ext.scala => InputProviderPlatform.scala} (68%) diff --git a/modules/core/src/main/scala/AnsiTerminal.scala b/modules/core/src/main/scala/AnsiTerminal.scala index 733f69e..2d345ac 100644 --- a/modules/core/src/main/scala/AnsiTerminal.scala +++ b/modules/core/src/main/scala/AnsiTerminal.scala @@ -61,11 +61,6 @@ class AnsiTerminal(writer: String => Unit) extends Terminal: override inline def save(): this.type = call('s') override inline def screenClear(): this.type = call(s"${ESC}c") - inline def withRestore[A](inline f: => A) = - save() - f - restore() - end AnsiTerminal object AnsiTerminal: diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 3ab50f4..3c5ec8d 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -17,23 +17,49 @@ package com.indoorvivants.proompts @main def hello = - val term = TracingTerminal() - val write = term.writer + def testingProgram(terminal: Terminal, write: String => Unit) = + write("hello") + write("worldasdasdasd\ntest") + write("yo") + write("bla") + write("s") + terminal.moveBack(3) + write("kkkk") + write("kkkk\nhhhh") - write("hello") - write("world\ntest") - write("yo") - write("bla") - write("s") + val term = TracingTerminal() + val writer = term.writer + + testingProgram(term, writer) println(term.getPretty()) - term.moveBack(3) + // changemode(1) + val writer1 = (s: String) => System.out.print(s) + val ansi = Terminal.ansi(writer1) - write("wazooop") + testingProgram(ansi, writer1) - println(term.getPretty()) + // testingProgram() + + // val term = TracingTerminal() + + // val write = term.writer + + // write("hello") + // write("worldasdasdasd\ntest") + // write("yo") + // write("bla") + // write("s") + + // println(term.getPretty()) + + // term.moveBack(3) + + // write("wazooop") + + // println(term.getPretty()) // write("world\n") // write("yo") @@ -46,5 +72,4 @@ package com.indoorvivants.proompts // InputProvider().attach(env => Interactive(prompt, env.writer).handler) // ) - end hello diff --git a/modules/core/src/main/scala/Terminal.scala b/modules/core/src/main/scala/Terminal.scala index 423fd6f..73cf4ef 100644 --- a/modules/core/src/main/scala/Terminal.scala +++ b/modules/core/src/main/scala/Terminal.scala @@ -55,6 +55,12 @@ trait Terminal: def save(): self.type def restore(): self.type + + inline def withRestore[A](inline f: => A) = + save() + f + restore() + end Terminal object Terminal: diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index f7821f7..ad4694a 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -26,51 +26,95 @@ class TracingTerminal extends Terminal: var currentLine = 0 var currentColumn = 0 + var saved = Option.empty[(Int, Int)] + + def set(char: Char, line: Int, column: Int) = + INTERNAL(WIDTH * line + column) = char + def currentIndex() = WIDTH * currentLine + currentColumn var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') + def updateBounds() = + currentWidth = currentWidth max currentColumn + currentHeight = currentHeight max currentLine + override def screenClear(): this.type = INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') this - override def movePreviousLine(n: Int): this.type = ??? + override def movePreviousLine(n: Int): this.type = + currentLine = (currentLine - n) max 0 + this - override def eraseToBeginningOfLine(): this.type = ??? + override def eraseToBeginningOfLine(): this.type = + for column <- 0 to currentColumn do set(' ', currentLine, column) + this - override def eraseToEndOfLine(): this.type = ??? + override def eraseToEndOfLine(): this.type = + for column <- currentColumn to currentWidth do set(' ', currentLine, column) + this - override def cursorShow(): this.type = ??? + override def cursorShow(): this.type = this - override def moveHorizontalTo(column: Int): this.type = ??? + override def moveHorizontalTo(column: Int): this.type = + currentColumn = column + updateBounds() + this override def eraseEntireScreen(): this.type = ??? - override def moveToPosition(row: Int, column: Int): this.type = ??? + override def moveToPosition(row: Int, column: Int): this.type = + currentLine = row - 1 + currentColumn = column - 1 + updateBounds() + this override def eraseToBeginningOfScreen(): this.type = ??? - override def eraseEntireLine(): this.type = ??? + override def eraseEntireLine(): this.type = + for column <- 0 to currentWidth do set(' ', currentLine, column) + this override def moveBack(n: Int): this.type = errln(s"Back $n characters") currentColumn = (currentColumn - n) max 0 this - override def moveUp(n: Int): this.type = ??? + override def moveUp(n: Int): this.type = + currentLine = (currentLine - n) max 0 + this - override def save(): this.type = ??? + override def save(): this.type = + saved = Some((currentLine, currentColumn)) + this override def cursorHide(): this.type = ??? - override def moveDown(n: Int): this.type = ??? + override def moveDown(n: Int): this.type = + currentLine += n + updateBounds() + this override def eraseToEndOfScreen(): this.type = ??? - override def moveForward(n: Int): this.type = ??? + override def moveForward(n: Int): this.type = + currentColumn = (currentColumn + n) max 0 + updateBounds() + this - override def moveNextLine(n: Int): this.type = ??? + override def moveNextLine(n: Int): this.type = + currentLine += 1 + currentColumn = 0 + updateBounds() + this - override def restore(): this.type = ??? + override def restore(): this.type = + saved match + case None => this + case Some((line, column)) => + currentLine = line + currentColumn = column + this def getLine(i: Int): String = val start = WIDTH * i @@ -92,8 +136,9 @@ class TracingTerminal extends Terminal: val lines = l.linesIterator.zipWithIndex.toList lines.foreach: (line, idx) => simpleWriter(line) - currentColumn = 0 - currentLine += 1 + if idx != lines.length - 1 then + currentColumn = 0 + currentLine += 1 currentHeight = currentHeight max currentLine l => diff --git a/modules/core/src/main/scalajvm/Ext.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala similarity index 100% rename from modules/core/src/main/scalajvm/Ext.scala rename to modules/core/src/main/scalajvm/InputProviderPlatform.scala diff --git a/modules/core/src/main/scalanative/Ext.scala b/modules/core/src/main/scalanative/InputProviderPlatform.scala similarity index 68% rename from modules/core/src/main/scalanative/Ext.scala rename to modules/core/src/main/scalanative/InputProviderPlatform.scala index e188b1e..26a37be 100644 --- a/modules/core/src/main/scalanative/Ext.scala +++ b/modules/core/src/main/scalanative/InputProviderPlatform.scala @@ -1,30 +1,13 @@ -/* - * Copyright 2023 Anton Sviridov - * - * 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 com.indoorvivants.proompts import scala.util.boundary - import scalanative.libc.stdio.getchar import scalanative.unsafe.* import scalanative.posix.termios.* import boundary.break import CharCollector.* -class NativeInputProvider extends InputProvider: +class InputProviderPlatform extends InputProvider: override def attach(f: Environment => Handler): Completion = errln("what the fuck") changemode(1) @@ -78,10 +61,7 @@ class NativeInputProvider extends InputProvider: end attach override def close() = changemode(0) -end NativeInputProvider - -trait InputProviderPlatform: - def apply(): InputProvider = NativeInputProvider() +end InputProviderPlatform def changemode(dir: Int) = val oldt = stackalloc[termios]() @@ -94,3 +74,4 @@ def changemode(dir: Int) = tcsetattr(STDIN_FILENO, TCSANOW, newt) else tcsetattr(STDIN_FILENO, TCSANOW, oldt) end changemode + From 22196d3906c26bcdd64e2bcb6a14e97f70b0bf94 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 7 Feb 2024 21:11:24 +0000 Subject: [PATCH 05/32] Terminal tracing example --- modules/core/src/main/scala/Example.scala | 83 ++++++++----------- modules/core/src/main/scala/Interactive.scala | 12 ++- .../main/scala/InteractiveAlternatives.scala | 25 +++--- .../core/src/main/scala/TracingTerminal.scala | 26 +++++- .../scalanative/InputProviderPlatform.scala | 1 - 5 files changed, 79 insertions(+), 68 deletions(-) diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 3c5ec8d..611a383 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -18,58 +18,43 @@ package com.indoorvivants.proompts @main def hello = - def testingProgram(terminal: Terminal, write: String => Unit) = - write("hello") - write("worldasdasdasd\ntest") - write("yo") - write("bla") - write("s") - terminal.moveBack(3) - write("kkkk") - write("kkkk\nhhhh") + def testingProgram( + terminal: Terminal, + events: List[(Event, () => Unit)], + write: String => Unit + ) = + val i = InteractiveAlternatives( + terminal, + Prompt.Alternatives("How do you do fellow kids?", List("killa", "rizza")), + write, + colors = false + ) + + events.foreach: (ev, callback) => + i.handler(ev) + callback() + end testingProgram val term = TracingTerminal() val writer = term.writer - testingProgram(term, writer) - - println(term.getPretty()) - - // changemode(1) - val writer1 = (s: String) => System.out.print(s) - val ansi = Terminal.ansi(writer1) - - testingProgram(ansi, writer1) - - // testingProgram() - - // val term = TracingTerminal() - - // val write = term.writer - - // write("hello") - // write("worldasdasdasd\ntest") - // write("yo") - // write("bla") - // write("s") - - // println(term.getPretty()) - - // term.moveBack(3) - - // write("wazooop") - - // println(term.getPretty()) - // write("world\n") - // write("yo") - - // val prompt = Prompt.Alternatives( - // "How would you describe yourself?", - // List("Sexylicious", "Shmexy", "Pexying") - // ) - - // println( - // InputProvider().attach(env => Interactive(prompt, env.writer).handler) - // ) + val events = + List( + Event.Init, + Event.Key(KeyEvent.DOWN), + Event.Key(KeyEvent.UP), + Event.Char('r'.toInt) + ) + + testingProgram( + term, + events + .map(ev => + ev -> { () => + println(ev); println(term.getPretty()) + } + ), + writer + ) end hello diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 2a3b556..277bb17 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -18,11 +18,17 @@ package com.indoorvivants.proompts def errln(o: Any) = System.err.println(o) -class Interactive(var prompt: Prompt, writer: String => Unit): +class Interactive( + terminal: Terminal, + prompt: Prompt, + writer: String => Unit, + colors: Boolean +): def handler = prompt match - case p: Prompt.Input => InteractiveTextInput(p, writer).handler - case p: Prompt.Alternatives => InteractiveAlternatives(p, writer).handler + case p: Prompt.Input => InteractiveTextInput(p, writer).handler + case p: Prompt.Alternatives => + InteractiveAlternatives(terminal, p, writer, colors).handler end Interactive diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index c40cd05..9f3cd44 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -17,22 +17,26 @@ package com.indoorvivants.proompts class InteractiveAlternatives( + terminal: Terminal, prompt: Prompt.Alternatives, - writer: String => Unit + writer: String => Unit, + colors: Boolean ): - val terminal = Terminal.ansi(writer) - val lab = prompt.promptLabel - var state = AlternativesState("", 0, prompt.alts.length) + val lab = prompt.promptLabel + var state = AlternativesState("", 0, prompt.alts.length) + + def colored(msg: String)(f: String => fansi.Str) = + if colors then f(msg).toString else msg def printPrompt() = - val lines = 0 import terminal.* moveHorizontalTo(0) eraseToEndOfLine() - writer(s"${fansi.Color.Cyan(lab)}${state.text}") + writer(colored(lab + state.text)(fansi.Color.Cyan(_))) + withRestore: writer("\n") @@ -43,8 +47,6 @@ class InteractiveAlternatives( ) ) - errln(s"Filtered alternatives: ${filteredAlts}") - val adjustedSelected = state.selected.min(filteredAlts.length - 1).max(0) @@ -64,8 +66,10 @@ class InteractiveAlternatives( moveHorizontalTo(0) eraseToEndOfLine() val view = - if idx == adjustedSelected then fansi.Color.Green("> " + alt) - else fansi.Bold.On("· " + alt) + if idx == adjustedSelected then + if colors then fansi.Color.Green("> " + alt) else "> " + alt + else if colors then fansi.Bold.On("· " + alt) + else "· " + alt writer(view.toString) if idx != filteredAlts.length - 1 then writer("\n") end if @@ -79,7 +83,6 @@ class InteractiveAlternatives( def handler = new Handler: def apply(event: Event): Next = - errln(event) event match case Event.Init => printPrompt() diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index ad4694a..99a027c 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -18,7 +18,7 @@ package com.indoorvivants.proompts import scala.collection.mutable -class TracingTerminal extends Terminal: +class TracingTerminal(log: Boolean = false) extends Terminal: val WIDTH = 120 val HEIGHT = 500 var currentHeight = 0 @@ -34,6 +34,9 @@ class TracingTerminal extends Terminal: def currentIndex() = WIDTH * currentLine + currentColumn var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') + def log(msg: String): Unit = + if log then errln(s"[LINE=$currentLine, COL=$currentColumn] $msg") + def updateBounds() = currentWidth = currentWidth max currentColumn currentHeight = currentHeight max currentLine @@ -43,20 +46,24 @@ class TracingTerminal extends Terminal: this override def movePreviousLine(n: Int): this.type = + log(s"Moving $n lines up") currentLine = (currentLine - n) max 0 this override def eraseToBeginningOfLine(): this.type = + log(s"Erasing to beginning of line") for column <- 0 to currentColumn do set(' ', currentLine, column) this override def eraseToEndOfLine(): this.type = + log(s"Erasing to beginning of line") for column <- currentColumn to currentWidth do set(' ', currentLine, column) this override def cursorShow(): this.type = this override def moveHorizontalTo(column: Int): this.type = + log(s"Moving to column $column") currentColumn = column updateBounds() this @@ -64,6 +71,7 @@ class TracingTerminal extends Terminal: override def eraseEntireScreen(): this.type = ??? override def moveToPosition(row: Int, column: Int): this.type = + log(s"Moving to line ${row - 1}, column ${column}") currentLine = row - 1 currentColumn = column - 1 updateBounds() @@ -72,25 +80,29 @@ class TracingTerminal extends Terminal: override def eraseToBeginningOfScreen(): this.type = ??? override def eraseEntireLine(): this.type = + log(s"Erasing entire line") for column <- 0 to currentWidth do set(' ', currentLine, column) this override def moveBack(n: Int): this.type = - errln(s"Back $n characters") + log(s"Back $n characters") currentColumn = (currentColumn - n) max 0 this override def moveUp(n: Int): this.type = + log(s"Moving up $n lines") currentLine = (currentLine - n) max 0 this override def save(): this.type = + log(s"Saving position") saved = Some((currentLine, currentColumn)) this override def cursorHide(): this.type = ??? override def moveDown(n: Int): this.type = + log(s"Moving down $n lines") currentLine += n updateBounds() this @@ -98,6 +110,7 @@ class TracingTerminal extends Terminal: override def eraseToEndOfScreen(): this.type = ??? override def moveForward(n: Int): this.type = + log(s"Moving forward $n columns") currentColumn = (currentColumn + n) max 0 updateBounds() this @@ -111,7 +124,9 @@ class TracingTerminal extends Terminal: override def restore(): this.type = saved match case None => this + case Some((line, column)) => + log(s"Restoring cursor location to [LINE=$line, COL=$column]") currentLine = line currentColumn = column this @@ -123,7 +138,7 @@ class TracingTerminal extends Terminal: def writer: String => Unit = val simpleWriter: String => Unit = l => - errln(s"Writing at $currentLine x $currentColumn: $l") + log(s"Writing single line: `$l`") val newCurrentWidth = currentWidth max (currentColumn + l.length) if newCurrentWidth > WIDTH then todo("line length overflow") else @@ -133,7 +148,10 @@ class TracingTerminal extends Terminal: currentWidth = newCurrentWidth val multilineWriter: String => Unit = l => - val lines = l.linesIterator.zipWithIndex.toList + val lines = l.split("\n", -1).zipWithIndex.toList + log( + s"Writing multiple lines: ${lines.map(_._1).mkString("`", "`, `", "`")}" + ) lines.foreach: (line, idx) => simpleWriter(line) if idx != lines.length - 1 then diff --git a/modules/core/src/main/scalanative/InputProviderPlatform.scala b/modules/core/src/main/scalanative/InputProviderPlatform.scala index 26a37be..edd0048 100644 --- a/modules/core/src/main/scalanative/InputProviderPlatform.scala +++ b/modules/core/src/main/scalanative/InputProviderPlatform.scala @@ -9,7 +9,6 @@ import CharCollector.* class InputProviderPlatform extends InputProvider: override def attach(f: Environment => Handler): Completion = - errln("what the fuck") changemode(1) var lastRead = 0 From acb98e06080291977f1995dded9e883275108bfc Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 7 Feb 2024 21:41:52 +0000 Subject: [PATCH 06/32] WIP --- modules/core/src/main/scala/Example.scala | 8 +++++--- modules/core/src/main/scala/TracingTerminal.scala | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 611a383..4142e4a 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -25,7 +25,10 @@ package com.indoorvivants.proompts ) = val i = InteractiveAlternatives( terminal, - Prompt.Alternatives("How do you do fellow kids?", List("killa", "rizza")), + Prompt.Alternatives( + "How do you do fellow kids?", + List("killa", "rizza", "flizza") + ), write, colors = false ) @@ -35,14 +38,13 @@ package com.indoorvivants.proompts callback() end testingProgram - val term = TracingTerminal() + val term = TracingTerminal(logger = s => ()) // println(s)) val writer = term.writer val events = List( Event.Init, Event.Key(KeyEvent.DOWN), - Event.Key(KeyEvent.UP), Event.Char('r'.toInt) ) diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index 99a027c..4bf37cb 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -18,7 +18,7 @@ package com.indoorvivants.proompts import scala.collection.mutable -class TracingTerminal(log: Boolean = false) extends Terminal: +class TracingTerminal(logger: String => Unit = _ => ()) extends Terminal: val WIDTH = 120 val HEIGHT = 500 var currentHeight = 0 @@ -35,7 +35,7 @@ class TracingTerminal(log: Boolean = false) extends Terminal: var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') def log(msg: String): Unit = - if log then errln(s"[LINE=$currentLine, COL=$currentColumn] $msg") + logger(s"[LINE=$currentLine, COL=$currentColumn] $msg") def updateBounds() = currentWidth = currentWidth max currentColumn From e430a147851fd52fb00de71b133777712d3d8661 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 7 Feb 2024 21:51:37 +0000 Subject: [PATCH 07/32] WIP --- modules/core/src/main/scala/Example.scala | 5 ++++- .../core/src/main/scala/InteractiveAlternatives.scala | 9 ++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 4142e4a..beebdb5 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -45,7 +45,10 @@ package com.indoorvivants.proompts List( Event.Init, Event.Key(KeyEvent.DOWN), - Event.Char('r'.toInt) + Event.Char('r'.toInt), + Event.Key(KeyEvent.DELETE), + Event.Char('l'.toInt), + Event.Char('i'.toInt) ) testingProgram( diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 9f3cd44..9b46eeb 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -54,22 +54,21 @@ class InteractiveAlternatives( AlternativesState( state.text, selected = adjustedSelected, - showing = filteredAlts.length.min(1) + showing = filteredAlts.length.max(1) ) if filteredAlts.isEmpty then moveHorizontalTo(0) eraseToEndOfLine() - writer(fansi.Underlined.On("no matches").toString) + writer(colored("no matches")(fansi.Underlined.On(_))) else filteredAlts.zipWithIndex.foreach: (alt, idx) => moveHorizontalTo(0) eraseToEndOfLine() val view = if idx == adjustedSelected then - if colors then fansi.Color.Green("> " + alt) else "> " + alt - else if colors then fansi.Bold.On("· " + alt) - else "· " + alt + colored(s"> $alt")(fansi.Color.Green(_)) + else colored(s"· $alt")(fansi.Bold.On(_)) writer(view.toString) if idx != filteredAlts.length - 1 then writer("\n") end if From d0eefdd31a26a391e6d6daf850669c4df78c3385 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 7 Feb 2024 21:56:50 +0000 Subject: [PATCH 08/32] WIP --- modules/core/src/main/scala/Event.scala | 12 ++++++++++++ modules/core/src/main/scala/Example.scala | 8 +++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/scala/Event.scala b/modules/core/src/main/scala/Event.scala index b58704f..4cec6cc 100644 --- a/modules/core/src/main/scala/Event.scala +++ b/modules/core/src/main/scala/Event.scala @@ -22,5 +22,17 @@ enum Event: case Char(which: Int) case CSICode(bytes: List[Byte]) + override def toString(): String = + this match + case Init => "Event.Init" + case Key(which) => s"Event.Key($which)" + case Char(which) => s"Event.Char('${which.toChar}')" + case CSICode(bytes) => s"Event.CSICode(${bytes.mkString("[", ", ", "]")})" +end Event + +object Event: + object Char: + def apply(c: scala.Char): Event.Char = Event.Char(c.toInt) + enum KeyEvent: case UP, DOWN, LEFT, RIGHT, ENTER, DELETE diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index beebdb5..8e132f2 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -45,10 +45,12 @@ package com.indoorvivants.proompts List( Event.Init, Event.Key(KeyEvent.DOWN), - Event.Char('r'.toInt), + Event.Char('r'), Event.Key(KeyEvent.DELETE), - Event.Char('l'.toInt), - Event.Char('i'.toInt) + Event.Char('l'), + Event.Key(KeyEvent.DOWN), + Event.Key(KeyEvent.UP), + Event.Char('i') ) testingProgram( From c0b4e33b38cc437af1c917aee6d950ec44e68943 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 8 Feb 2024 11:41:54 +0000 Subject: [PATCH 09/32] Janky snapshot testing --- build.sbt | 132 +++++++++++++++++- modules/core/src/main/scala/Example.scala | 10 +- modules/core/src/main/scala/Interactive.scala | 7 +- .../main/scala/InteractiveAlternatives.scala | 12 +- .../src/main/scala/InteractiveTextInput.scala | 14 +- modules/core/src/main/scala/Output.scala | 39 ++++++ .../core/src/main/scala/TracingTerminal.scala | 4 +- .../test/resources/core-jvm/my.snapshot.thing | 1 + .../resources/core-jvm/my.snapshot.things | 1 + .../resources/core-native/my.snapshot.thing | 1 + .../resources/core-native/my.snapshot.things | 1 + .../core/src/test/scala/ExampleTests.scala | 7 +- .../core/src/test/scala/assertSnapshot.scala | 23 +++ 13 files changed, 226 insertions(+), 26 deletions(-) create mode 100644 modules/core/src/main/scala/Output.scala create mode 100644 modules/core/src/test/resources/core-jvm/my.snapshot.thing create mode 100644 modules/core/src/test/resources/core-jvm/my.snapshot.things create mode 100644 modules/core/src/test/resources/core-native/my.snapshot.thing create mode 100644 modules/core/src/test/resources/core-native/my.snapshot.things create mode 100644 modules/core/src/test/scala/assertSnapshot.scala diff --git a/build.sbt b/build.sbt index 4f24a5c..14842aa 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,4 @@ +import scala.io.StdIn Global / excludeLintKeys += logManager Global / excludeLintKeys += scalaJSUseMainModuleInitializer Global / excludeLintKeys += scalaJSLinkerConfig @@ -76,9 +77,131 @@ lazy val core = projectMatrix scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", - nativeConfig ~= (_.withIncrementalCompilation(true)) + nativeConfig ~= (_.withIncrementalCompilation(true)), + withSnapshotTesting ) +val checkSnapshots = taskKey[Unit]("") + +val withSnapshotTesting = Seq( + checkSnapshots := { + val bold = scala.Console.BOLD + val reset = scala.Console.RESET + val legend = + s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " + val modified = IO + .listFiles( + (Test / managedResourceDirectories).value.head / "snapshots-tmp" + ) + .toList + + if (modified.isEmpty) { + System.err.println("No snapshots to check") + } else { + + modified + .filter(_.getName.endsWith("__snap.new")) + .foreach { f => + val diffFile = new File(f.toString() + ".diff") + assert(diffFile.exists(), s"Diff file $diffFile not found") + + val diffContents = scala.io.Source + .fromFile(diffFile) + .getLines() + .mkString(System.lineSeparator()) + + val snapshotName :: destination :: newContentsLines = + scala.io.Source.fromFile(f).getLines().toList + + println( + s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" + ) + println( + s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" + ) + println(diffContents) + + println("\n\n") + print(legend) + + val choice = StdIn.readLine().trim + + if (choice == "a") { + IO.writeLines(new File(destination), newContentsLines) + IO.delete(f) + IO.delete(diffFile) + } + + } + } + + }, + Test / sourceGenerators += Def.task { + val platformSuffix = + virtualAxes.value.collectFirst { case p: VirtualAxis.PlatformAxis => + p + }.get + + val isNative = platformSuffix.value == "jvm" + val isJS = platformSuffix.value == "js" + val isJVM = !isNative && !isJS + + val name = moduleName.value + "-" + platformSuffix.value + + val snapshotsDestination = (Test / resourceDirectory).value / name + + val sourceDest = + (Test / managedSourceDirectories).value.head / "Snapshots.scala" + + val tmpDest = + (Test / managedResourceDirectories).value.head / "snapshots-tmp" + + IO.write(sourceDest, SnapshotsGenerate(snapshotsDestination, tmpDest)) + + IO.createDirectory(snapshotsDestination) + IO.createDirectory(tmpDest) + + Seq(sourceDest) + } +) + +def SnapshotsGenerate(path: File, tempPath: File) = + """ + |package proompts + |import scala.quoted.* // imports Quotes, Expr + |object Snapshots: + | inline def location(): String = "PATH" + | inline def tmpLocation(): String = "TEMP_PATH" + | inline def write(name: String, contents: String, diff: String): Unit = + | import java.io.FileWriter + | val tmpName = name + "__snap.new" + | val tmpDiff = name + "__snap.new.diff" + | val file = java.nio.file.Paths.get(location()).resolve(name) + | val tmpFile = java.nio.file.Paths.get(tmpLocation()).resolve(tmpName).toFile + | val tmpFileDiff = java.nio.file.Paths.get(tmpLocation()).resolve(tmpDiff).toFile + | scala.util.Using(new FileWriter(tmpFile)) { writer => + | writer.write(name + "\n") + | writer.write(file.toString + "\n") + | writer.write(contents) + | } + | scala.util.Using(new FileWriter(tmpFileDiff)) { writer => + | writer.write(diff) + | } + | inline def apply(inline name: String): Option[String] = + | ${ applyImpl('name) } + | private def applyImpl(x: Expr[String])(using + | Quotes + | ): Expr[Option[String]] = + | val path = java.nio.file.Paths.get(location()).resolve(x.valueOrAbort) + | if path.toFile.exists() then + | val str = scala.io.Source.fromFile(path.toFile, "utf-8").getLines().mkString(System.lineSeparator()) + | Expr(Some(str)) + | else Expr(None) + |end Snapshots + """.trim.stripMargin + .replace("TEMP_PATH", tempPath.toPath().toAbsolutePath().toString) + .replace("PATH", path.toPath().toAbsolutePath().toString) + lazy val docs = projectMatrix .in(file("myproject-docs")) .dependsOn(core) @@ -133,3 +256,10 @@ val PrepareCICommands = Seq( addCommandAlias("ci", CICommands) addCommandAlias("preCI", PrepareCICommands) + +addCommandAlias( + "testSnapshots", + """set Test/envVars += ("SNAPSHOTS_INTERACTIVE" -> "true"); test""" +) + +Global / onChangedBuildSource := ReloadOnSourceChanges diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index 8e132f2..e2d15d8 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -21,7 +21,7 @@ package com.indoorvivants.proompts def testingProgram( terminal: Terminal, events: List[(Event, () => Unit)], - write: String => Unit + out: Output ) = val i = InteractiveAlternatives( terminal, @@ -29,7 +29,7 @@ package com.indoorvivants.proompts "How do you do fellow kids?", List("killa", "rizza", "flizza") ), - write, + out, colors = false ) @@ -38,8 +38,8 @@ package com.indoorvivants.proompts callback() end testingProgram - val term = TracingTerminal(logger = s => ()) // println(s)) - val writer = term.writer + val term = TracingTerminal(Output.DarkVoid) + val capturing = Output.Delegate(term.writer, s => Output.StdOut.logLn(s)) val events = List( @@ -61,7 +61,7 @@ package com.indoorvivants.proompts println(ev); println(term.getPretty()) } ), - writer + capturing ) end hello diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 277bb17..29b30e1 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -21,14 +21,13 @@ def errln(o: Any) = System.err.println(o) class Interactive( terminal: Terminal, prompt: Prompt, - writer: String => Unit, + out: Output, colors: Boolean ): - def handler = prompt match - case p: Prompt.Input => InteractiveTextInput(p, writer).handler + case p: Prompt.Input => InteractiveTextInput(p, terminal, out, colors).handler case p: Prompt.Alternatives => - InteractiveAlternatives(terminal, p, writer, colors).handler + InteractiveAlternatives(terminal, p, out, colors).handler end Interactive diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 9b46eeb..6c7a387 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -19,7 +19,7 @@ package com.indoorvivants.proompts class InteractiveAlternatives( terminal: Terminal, prompt: Prompt.Alternatives, - writer: String => Unit, + out: Output, colors: Boolean ): val lab = prompt.promptLabel @@ -35,10 +35,10 @@ class InteractiveAlternatives( moveHorizontalTo(0) eraseToEndOfLine() - writer(colored(lab + state.text)(fansi.Color.Cyan(_))) + out.out(colored(lab + state.text)(fansi.Color.Cyan(_))) withRestore: - writer("\n") + out.out("\n") val filteredAlts = prompt.alts.filter( @@ -60,7 +60,7 @@ class InteractiveAlternatives( if filteredAlts.isEmpty then moveHorizontalTo(0) eraseToEndOfLine() - writer(colored("no matches")(fansi.Underlined.On(_))) + out.out(colored("no matches")(fansi.Underlined.On(_))) else filteredAlts.zipWithIndex.foreach: (alt, idx) => moveHorizontalTo(0) @@ -69,8 +69,8 @@ class InteractiveAlternatives( if idx == adjustedSelected then colored(s"> $alt")(fansi.Color.Green(_)) else colored(s"· $alt")(fansi.Bold.On(_)) - writer(view.toString) - if idx != filteredAlts.length - 1 then writer("\n") + out.out(view.toString) + if idx != filteredAlts.length - 1 then out.out("\n") end if for _ <- 0 until state.showing - newState.showing do diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala index 62b83ca..ae3aa12 100644 --- a/modules/core/src/main/scala/InteractiveTextInput.scala +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -18,11 +18,15 @@ package com.indoorvivants.proompts class InteractiveTextInput( prompt: Prompt.Input, - writer: String => Unit + terminal: Terminal, + out: Output, + colors: Boolean ): - val terminal = Terminal.ansi(writer) - val lab = prompt.promptLabel - var state = TextInputState("") + val lab = prompt.promptLabel + var state = TextInputState("") + + def colored(msg: String)(f: String => fansi.Str) = + if colors then f(msg).toString else msg def printPrompt() = @@ -33,7 +37,7 @@ class InteractiveTextInput( errln(prompt) - writer(s"${fansi.Color.Cyan(lab)}${state.text}") + out.out(colored(lab + state.text)(fansi.Color.Cyan(_))) end printPrompt val handler = new Handler: diff --git a/modules/core/src/main/scala/Output.scala b/modules/core/src/main/scala/Output.scala new file mode 100644 index 0000000..fa59ce4 --- /dev/null +++ b/modules/core/src/main/scala/Output.scala @@ -0,0 +1,39 @@ +package com.indoorvivants.proompts + +trait Output: + def out[A: AsString](a: A): Unit + def logLn[A: AsString](a: A): Unit + +trait AsString[A]: + extension (a: A) def render: String + +given AsString[String] with + extension (a: String) def render = a + +extension (o: Output) + def outLn[A: AsString](a: A) = + o.out(a.render + "\n") + def loggerOnly: Output = + new Output: + override def logLn[A: AsString](a: A): Unit = o.logLn(a) + override def out[A: AsString](a: A): Unit = () + +object Output: + object Std extends Output: + override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) + override def out[A: AsString](a: A): Unit = System.out.print(a.render) + + object StdOut extends Output: + override def logLn[A: AsString](a: A): Unit = System.out.println(a.render) + override def out[A: AsString](a: A): Unit = System.out.print(a.render) + + object DarkVoid extends Output: + override def logLn[A: AsString](a: A): Unit = () + override def out[A: AsString](a: A): Unit = () + + + class Delegate(writeOut: String => Unit, writeLog: String => Unit) + extends Output: + override def logLn[A: AsString](a: A): Unit = writeLog(a.render + "\n") + override def out[A: AsString](a: A): Unit = writeOut(a.render) +end Output diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index 4bf37cb..1125451 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -18,7 +18,7 @@ package com.indoorvivants.proompts import scala.collection.mutable -class TracingTerminal(logger: String => Unit = _ => ()) extends Terminal: +class TracingTerminal(out: Output) extends Terminal: val WIDTH = 120 val HEIGHT = 500 var currentHeight = 0 @@ -35,7 +35,7 @@ class TracingTerminal(logger: String => Unit = _ => ()) extends Terminal: var INTERNAL = Array.fill[Char](WIDTH * HEIGHT)(' ') def log(msg: String): Unit = - logger(s"[LINE=$currentLine, COL=$currentColumn] $msg") + out.logLn(s"[LINE=$currentLine, COL=$currentColumn] $msg") def updateBounds() = currentWidth = currentWidth max currentColumn diff --git a/modules/core/src/test/resources/core-jvm/my.snapshot.thing b/modules/core/src/test/resources/core-jvm/my.snapshot.thing new file mode 100644 index 0000000..3842ce6 --- /dev/null +++ b/modules/core/src/test/resources/core-jvm/my.snapshot.thing @@ -0,0 +1 @@ +is this! diff --git a/modules/core/src/test/resources/core-jvm/my.snapshot.things b/modules/core/src/test/resources/core-jvm/my.snapshot.things new file mode 100644 index 0000000..e21d227 --- /dev/null +++ b/modules/core/src/test/resources/core-jvm/my.snapshot.things @@ -0,0 +1 @@ +is this what you want?! diff --git a/modules/core/src/test/resources/core-native/my.snapshot.thing b/modules/core/src/test/resources/core-native/my.snapshot.thing new file mode 100644 index 0000000..419018b --- /dev/null +++ b/modules/core/src/test/resources/core-native/my.snapshot.thing @@ -0,0 +1 @@ +is this \ No newline at end of file diff --git a/modules/core/src/test/resources/core-native/my.snapshot.things b/modules/core/src/test/resources/core-native/my.snapshot.things new file mode 100644 index 0000000..e21d227 --- /dev/null +++ b/modules/core/src/test/resources/core-native/my.snapshot.things @@ -0,0 +1 @@ +is this what you want?! diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index 160b65d..6b28678 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -1,7 +1,8 @@ -package com.indoorvivants.library +package proompts class ExampleTests extends munit.FunSuite: test("test1") { - // assert(implicitly[TypeClass[Int]].isPrimitive == true) - // assert(implicitly[TypeClass[Boolean]].isPrimitive == true) + assertSnapshot("my.snapshot.things", "is this what you want?!") } + +end ExampleTests diff --git a/modules/core/src/test/scala/assertSnapshot.scala b/modules/core/src/test/scala/assertSnapshot.scala new file mode 100644 index 0000000..d54e9fc --- /dev/null +++ b/modules/core/src/test/scala/assertSnapshot.scala @@ -0,0 +1,23 @@ +package proompts +import munit.internal.difflib.Diffs +import munit.Assertions + +inline def assertSnapshot(inline name: String, contents: String) = + Snapshots(name) match + case None => + Snapshots.write( + name, + contents, + Diffs.create(contents, "").createDiffOnlyReport() + ) + + Assertions.fail( + s"No snapshot was found for $name, please run checkSnapshots command and accept a snapshot for this test" + ) + + case Some(value) => + val diff = Diffs.create(contents, value) + if !diff.isEmpty then + val diffReport = diff.createDiffOnlyReport() + Snapshots.write(name, contents, diffReport) + Assertions.assertNoDiff(contents, value) From ef77aefa291000c63849a82257f5bd094b714985 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 8 Feb 2024 11:48:15 +0000 Subject: [PATCH 10/32] Remove macros from generated code --- build.sbt | 14 ++++---------- .../src/test/resources/core-jvm/my.snapshot.things | 2 +- modules/core/src/test/scala/ExampleTests.scala | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/build.sbt b/build.sbt index 14842aa..571a046 100644 --- a/build.sbt +++ b/build.sbt @@ -168,7 +168,6 @@ val withSnapshotTesting = Seq( def SnapshotsGenerate(path: File, tempPath: File) = """ |package proompts - |import scala.quoted.* // imports Quotes, Expr |object Snapshots: | inline def location(): String = "PATH" | inline def tmpLocation(): String = "TEMP_PATH" @@ -188,15 +187,10 @@ def SnapshotsGenerate(path: File, tempPath: File) = | writer.write(diff) | } | inline def apply(inline name: String): Option[String] = - | ${ applyImpl('name) } - | private def applyImpl(x: Expr[String])(using - | Quotes - | ): Expr[Option[String]] = - | val path = java.nio.file.Paths.get(location()).resolve(x.valueOrAbort) - | if path.toFile.exists() then - | val str = scala.io.Source.fromFile(path.toFile, "utf-8").getLines().mkString(System.lineSeparator()) - | Expr(Some(str)) - | else Expr(None) + | val path = java.nio.file.Paths.get(location()).resolve(name) + | Option.when(path.toFile.exists()): + | scala.io.Source.fromFile(path.toFile, "utf-8").getLines().mkString(System.lineSeparator()) + | end apply |end Snapshots """.trim.stripMargin .replace("TEMP_PATH", tempPath.toPath().toAbsolutePath().toString) diff --git a/modules/core/src/test/resources/core-jvm/my.snapshot.things b/modules/core/src/test/resources/core-jvm/my.snapshot.things index e21d227..06cc1a8 100644 --- a/modules/core/src/test/resources/core-jvm/my.snapshot.things +++ b/modules/core/src/test/resources/core-jvm/my.snapshot.things @@ -1 +1 @@ -is this what you want?! +is this what you want? diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index 6b28678..f547fda 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -2,7 +2,7 @@ package proompts class ExampleTests extends munit.FunSuite: test("test1") { - assertSnapshot("my.snapshot.things", "is this what you want?!") + assertSnapshot("my.snapshot.things", "is this what you want?") } end ExampleTests From 823e13fe670b04e0a572a674c7cdbb54911c879b Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 8 Feb 2024 14:32:35 +0000 Subject: [PATCH 11/32] Make everything work on JS and separate snapshots-runtime --- build.sbt | 52 ++++++++++--------- modules/core/src/main/scala/Interactive.scala | 2 +- .../src/main/scalajs/NodeJSBindings.scala | 3 -- .../resources/core-js/alternatives.navigation | 14 +++++ .../resources/core-js/alternatives.typing | 35 +++++++++++++ .../test/resources/core-jvm/alternatives.init | 7 +++ .../core-jvm/alternatives.navigation | 14 +++++ .../resources/core-jvm/alternatives.typing | 35 +++++++++++++ .../test/resources/core-jvm/my.snapshot.thing | 1 - .../resources/core-jvm/my.snapshot.things | 1 - .../core-native/alternatives.navigation | 14 +++++ .../resources/core-native/alternatives.typing | 35 +++++++++++++ .../resources/core-native/my.snapshot.thing | 1 - .../resources/core-native/my.snapshot.things | 1 - .../core/src/test/scala/ExampleTests.scala | 27 ++++++++-- .../core/src/test/scala/TerminalTests.scala | 33 ++++++++++++ .../src/main/scala/Snapshots.scala | 22 ++++++++ .../src/main/scalajs/Platform.scala | 41 +++++++++++++++ .../src/main/scalajvm/Platform.scala | 25 +++++++++ .../src/main/scalanative/Platform.scala | 25 +++++++++ 20 files changed, 352 insertions(+), 36 deletions(-) create mode 100644 modules/core/src/test/resources/core-js/alternatives.navigation create mode 100644 modules/core/src/test/resources/core-js/alternatives.typing create mode 100644 modules/core/src/test/resources/core-jvm/alternatives.init create mode 100644 modules/core/src/test/resources/core-jvm/alternatives.navigation create mode 100644 modules/core/src/test/resources/core-jvm/alternatives.typing delete mode 100644 modules/core/src/test/resources/core-jvm/my.snapshot.thing delete mode 100644 modules/core/src/test/resources/core-jvm/my.snapshot.things create mode 100644 modules/core/src/test/resources/core-native/alternatives.navigation create mode 100644 modules/core/src/test/resources/core-native/alternatives.typing delete mode 100644 modules/core/src/test/resources/core-native/my.snapshot.thing delete mode 100644 modules/core/src/test/resources/core-native/my.snapshot.things create mode 100644 modules/core/src/test/scala/TerminalTests.scala create mode 100644 modules/snapshots-runtime/src/main/scala/Snapshots.scala create mode 100644 modules/snapshots-runtime/src/main/scalajs/Platform.scala create mode 100644 modules/snapshots-runtime/src/main/scalajvm/Platform.scala create mode 100644 modules/snapshots-runtime/src/main/scalanative/Platform.scala diff --git a/build.sbt b/build.sbt index 571a046..0e2066c 100644 --- a/build.sbt +++ b/build.sbt @@ -61,6 +61,7 @@ lazy val core = projectMatrix .settings( name := "core" ) + .dependsOn(snapshotsRuntime % "test->compile") .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) @@ -81,6 +82,32 @@ lazy val core = projectMatrix withSnapshotTesting ) +lazy val snapshotsRuntime = projectMatrix + .in(file("modules/snapshots-runtime")) + .defaultAxes(defaults*) + .settings( + name := "snapshots-runtime" + ) + .settings(munitSettings) + .jvmPlatform(Versions.scalaVersions) + .jsPlatform(Versions.scalaVersions, disableDependencyChecks) + .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + .enablePlugins(BuildInfoPlugin) + .settings( + buildInfoPackage := "com.indoorvivants.library.internal", + buildInfoKeys := Seq[BuildInfoKey]( + version, + scalaVersion, + scalaBinaryVersion + ), + scalacOptions += "-Wunused:all", + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), + libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", + nativeConfig ~= (_.withIncrementalCompilation(true)), + ) + + val checkSnapshots = taskKey[Unit]("") val withSnapshotTesting = Seq( @@ -168,30 +195,7 @@ val withSnapshotTesting = Seq( def SnapshotsGenerate(path: File, tempPath: File) = """ |package proompts - |object Snapshots: - | inline def location(): String = "PATH" - | inline def tmpLocation(): String = "TEMP_PATH" - | inline def write(name: String, contents: String, diff: String): Unit = - | import java.io.FileWriter - | val tmpName = name + "__snap.new" - | val tmpDiff = name + "__snap.new.diff" - | val file = java.nio.file.Paths.get(location()).resolve(name) - | val tmpFile = java.nio.file.Paths.get(tmpLocation()).resolve(tmpName).toFile - | val tmpFileDiff = java.nio.file.Paths.get(tmpLocation()).resolve(tmpDiff).toFile - | scala.util.Using(new FileWriter(tmpFile)) { writer => - | writer.write(name + "\n") - | writer.write(file.toString + "\n") - | writer.write(contents) - | } - | scala.util.Using(new FileWriter(tmpFileDiff)) { writer => - | writer.write(diff) - | } - | inline def apply(inline name: String): Option[String] = - | val path = java.nio.file.Paths.get(location()).resolve(name) - | Option.when(path.toFile.exists()): - | scala.io.Source.fromFile(path.toFile, "utf-8").getLines().mkString(System.lineSeparator()) - | end apply - |end Snapshots + |object Snapshots extends proompts.snapshots.Snapshots(location = "PATH", tmpLocation = "TEMP_PATH") """.trim.stripMargin .replace("TEMP_PATH", tempPath.toPath().toAbsolutePath().toString) .replace("PATH", path.toPath().toAbsolutePath().toString) diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 29b30e1..7c4d78b 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -24,7 +24,7 @@ class Interactive( out: Output, colors: Boolean ): - def handler = + val handler = prompt match case p: Prompt.Input => InteractiveTextInput(p, terminal, out, colors).handler case p: Prompt.Alternatives => diff --git a/modules/core/src/main/scalajs/NodeJSBindings.scala b/modules/core/src/main/scalajs/NodeJSBindings.scala index bddcaaf..48bba95 100644 --- a/modules/core/src/main/scalajs/NodeJSBindings.scala +++ b/modules/core/src/main/scalajs/NodeJSBindings.scala @@ -19,9 +19,6 @@ package com.indoorvivants.proompts import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport -import com.indoorvivants.proompts.CharCollector.State -import com.indoorvivants.proompts.CharCollector.decode - import scalajs.js @js.native diff --git a/modules/core/src/test/resources/core-js/alternatives.navigation b/modules/core/src/test/resources/core-js/alternatives.navigation new file mode 100644 index 0000000..a31c7ba --- /dev/null +++ b/modules/core/src/test/resources/core-js/alternatives.navigation @@ -0,0 +1,14 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃> rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-js/alternatives.typing b/modules/core/src/test/resources/core-js/alternatives.typing new file mode 100644 index 0000000..cf9b886 --- /dev/null +++ b/modules/core/src/test/resources/core-js/alternatives.typing @@ -0,0 +1,35 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('z') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > z┃ +┃> rizza ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? >  ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > l┃ +┃> killa ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('i') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > li┃ +┃> flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-jvm/alternatives.init b/modules/core/src/test/resources/core-jvm/alternatives.init new file mode 100644 index 0000000..256646b --- /dev/null +++ b/modules/core/src/test/resources/core-jvm/alternatives.init @@ -0,0 +1,7 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-jvm/alternatives.navigation b/modules/core/src/test/resources/core-jvm/alternatives.navigation new file mode 100644 index 0000000..a31c7ba --- /dev/null +++ b/modules/core/src/test/resources/core-jvm/alternatives.navigation @@ -0,0 +1,14 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃> rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-jvm/alternatives.typing b/modules/core/src/test/resources/core-jvm/alternatives.typing new file mode 100644 index 0000000..cf9b886 --- /dev/null +++ b/modules/core/src/test/resources/core-jvm/alternatives.typing @@ -0,0 +1,35 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('z') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > z┃ +┃> rizza ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? >  ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > l┃ +┃> killa ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('i') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > li┃ +┃> flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-jvm/my.snapshot.thing b/modules/core/src/test/resources/core-jvm/my.snapshot.thing deleted file mode 100644 index 3842ce6..0000000 --- a/modules/core/src/test/resources/core-jvm/my.snapshot.thing +++ /dev/null @@ -1 +0,0 @@ -is this! diff --git a/modules/core/src/test/resources/core-jvm/my.snapshot.things b/modules/core/src/test/resources/core-jvm/my.snapshot.things deleted file mode 100644 index 06cc1a8..0000000 --- a/modules/core/src/test/resources/core-jvm/my.snapshot.things +++ /dev/null @@ -1 +0,0 @@ -is this what you want? diff --git a/modules/core/src/test/resources/core-native/alternatives.navigation b/modules/core/src/test/resources/core-native/alternatives.navigation new file mode 100644 index 0000000..a31c7ba --- /dev/null +++ b/modules/core/src/test/resources/core-native/alternatives.navigation @@ -0,0 +1,14 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃> rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-native/alternatives.typing b/modules/core/src/test/resources/core-native/alternatives.typing new file mode 100644 index 0000000..cf9b886 --- /dev/null +++ b/modules/core/src/test/resources/core-native/alternatives.typing @@ -0,0 +1,35 @@ +Event.Init +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('z') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > z┃ +┃> rizza ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DELETE) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? >  ┃ +┃> killa ┃ +┃· rizza ┃ +┃· flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('l') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > l┃ +┃> killa ┃ +┃· flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Char('i') +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > li┃ +┃> flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-native/my.snapshot.thing b/modules/core/src/test/resources/core-native/my.snapshot.thing deleted file mode 100644 index 419018b..0000000 --- a/modules/core/src/test/resources/core-native/my.snapshot.thing +++ /dev/null @@ -1 +0,0 @@ -is this \ No newline at end of file diff --git a/modules/core/src/test/resources/core-native/my.snapshot.things b/modules/core/src/test/resources/core-native/my.snapshot.things deleted file mode 100644 index e21d227..0000000 --- a/modules/core/src/test/resources/core-native/my.snapshot.things +++ /dev/null @@ -1 +0,0 @@ -is this what you want?! diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index f547fda..39da5be 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -1,8 +1,27 @@ package proompts -class ExampleTests extends munit.FunSuite: - test("test1") { - assertSnapshot("my.snapshot.things", "is this what you want?") - } +import com.indoorvivants.proompts.* + +class ExampleTests extends munit.FunSuite, TerminalTests: + val prompt = Prompt.Alternatives( + "How do you do fellow kids?", + List("killa", "rizza", "flizza") + ) + + terminalTest("alternatives.navigation")( + prompt, + List(Event.Init, Event.Key(KeyEvent.DOWN)) + ) + + terminalTest("alternatives.typing")( + prompt, + List( + Event.Init, + Event.Char('z'), + Event.Key(KeyEvent.DELETE), + Event.Char('l'), + Event.Char('i') + ) + ) end ExampleTests diff --git a/modules/core/src/test/scala/TerminalTests.scala b/modules/core/src/test/scala/TerminalTests.scala new file mode 100644 index 0000000..49a7ea0 --- /dev/null +++ b/modules/core/src/test/scala/TerminalTests.scala @@ -0,0 +1,33 @@ +package proompts + +import com.indoorvivants.proompts.* + +trait TerminalTests: + self: munit.FunSuite => + + def terminalTest( + name: String + )(prompt: Prompt, events: List[Event])(implicit loc: munit.Location): Unit = + test(name) { + val result = + terminalSession( + name, + prompt, + events + ) + assertSnapshot(name, result) + } + + def terminalSession(name: String, prompt: Prompt, events: List[Event]) = + val sb = new java.lang.StringBuilder + val term = TracingTerminal(Output.DarkVoid) + val capturing = Output.Delegate(term.writer, s => sb.append(s + "\n")) + + val i = Interactive(term, prompt, capturing, colors = false) + events.foreach: ev => + sb.append(ev.toString() + "\n") + i.handler(ev) + sb.append(term.getPretty() + "\n") + sb.toString() + end terminalSession +end TerminalTests diff --git a/modules/snapshots-runtime/src/main/scala/Snapshots.scala b/modules/snapshots-runtime/src/main/scala/Snapshots.scala new file mode 100644 index 0000000..7cba64b --- /dev/null +++ b/modules/snapshots-runtime/src/main/scala/Snapshots.scala @@ -0,0 +1,22 @@ +package proompts.snapshots + +case class Snapshots(location: String, tmpLocation: String) extends Platform: + inline def write(name: String, contents: String, diff: String): Unit = + val tmpName = name + "__snap.new" + val tmpDiff = name + "__snap.new.diff" + val file = location.resolve(name) + val tmpFile = tmpLocation.resolve(tmpName) + val tmpFileDiff = tmpLocation.resolve(tmpDiff) + + val snapContents = + name + "\n" + file + "\n" + contents + + tmpFile.fileWriteContents(snapContents) + tmpFileDiff.fileWriteContents(diff) + end write + + inline def apply(inline name: String): Option[String] = + location.resolve(name).readFileContents() + + end apply +end Snapshots diff --git a/modules/snapshots-runtime/src/main/scalajs/Platform.scala b/modules/snapshots-runtime/src/main/scalajs/Platform.scala new file mode 100644 index 0000000..dbc9a1e --- /dev/null +++ b/modules/snapshots-runtime/src/main/scalajs/Platform.scala @@ -0,0 +1,41 @@ +package proompts.snapshots + +import scala.scalajs.js.annotation.JSImport +import scalajs.js + +trait Platform: + extension (s: String) + def resolve(segment: String): String = + s + "/" + segment + + def fileWriteContents(contents: String): Unit = + FS.writeFileSync(s, contents) + + def readFileContents(): Option[String] = + val exists = + FS.statSync( + s, + js.Dynamic.literal(throwIfNoEntry = false) + ) != js.undefined + Option.when(exists): + FS.readFileSync(s, js.Dynamic.literal(encoding = "utf8")) + end readFileContents + end extension + +end Platform + +@js.native +private[snapshots] trait FS extends js.Object: + def readFileSync(path: String, options: String | js.Object = ""): String = + js.native + def writeFileSync( + path: String, + contents: String, + options: String = "" + ): Unit = js.native + def statSync(path: String, options: js.Any): js.Any = js.native +end FS + +@js.native +@JSImport("node:fs", JSImport.Namespace) +private[snapshots] object FS extends FS diff --git a/modules/snapshots-runtime/src/main/scalajvm/Platform.scala b/modules/snapshots-runtime/src/main/scalajvm/Platform.scala new file mode 100644 index 0000000..0fab09e --- /dev/null +++ b/modules/snapshots-runtime/src/main/scalajvm/Platform.scala @@ -0,0 +1,25 @@ +package proompts.snapshots + +import java.nio.file.Paths +import java.io.FileWriter +import java.io.File + +private[snapshots] trait Platform: + extension (s: String) + def resolve(segment: String): String = + Paths.get(s).resolve(segment).toString() + + def fileWriteContents(contents: String): Unit = + scala.util.Using(new FileWriter(new File(s))) { writer => + writer.write(contents) + } + + def readFileContents(): Option[String] = + val file = new File(s) + Option.when(file.exists()): + scala.io.Source + .fromFile(file, "utf-8") + .getLines() + .mkString("\n") + end extension +end Platform diff --git a/modules/snapshots-runtime/src/main/scalanative/Platform.scala b/modules/snapshots-runtime/src/main/scalanative/Platform.scala new file mode 100644 index 0000000..0fab09e --- /dev/null +++ b/modules/snapshots-runtime/src/main/scalanative/Platform.scala @@ -0,0 +1,25 @@ +package proompts.snapshots + +import java.nio.file.Paths +import java.io.FileWriter +import java.io.File + +private[snapshots] trait Platform: + extension (s: String) + def resolve(segment: String): String = + Paths.get(s).resolve(segment).toString() + + def fileWriteContents(contents: String): Unit = + scala.util.Using(new FileWriter(new File(s))) { writer => + writer.write(contents) + } + + def readFileContents(): Option[String] = + val file = new File(s) + Option.when(file.exists()): + scala.io.Source + .fromFile(file, "utf-8") + .getLines() + .mkString("\n") + end extension +end Platform From 94220af06775794e914d0397b73681455f13c917 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 8 Feb 2024 16:17:17 +0000 Subject: [PATCH 12/32] Make Snapshots a SBT plugin and commit some sins --- .scalafmt.conf | 4 + build.sbt | 48 ++++--- .../src/main/scala/SnapshotsPlugin.scala | 118 ++++++++++++++++++ project/plugins.sbt | 19 +++ 4 files changed, 171 insertions(+), 18 deletions(-) create mode 100644 modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala diff --git a/.scalafmt.conf b/.scalafmt.conf index 003a1cb..d2fa8e1 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -14,4 +14,8 @@ fileOverride { "glob:**/project/**.*" { runner.dialect = scala212source3 } + + "glob:**/snapshots-sbt-plugin/**.*" { + runner.dialect = scala212source3 + } } diff --git a/build.sbt b/build.sbt index 0e2066c..e7b6cf8 100644 --- a/build.sbt +++ b/build.sbt @@ -66,20 +66,24 @@ lazy val core = projectMatrix .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) - .enablePlugins(BuildInfoPlugin) + .enablePlugins(SnapshotsPlugin) .settings( - buildInfoPackage := "com.indoorvivants.library.internal", - buildInfoKeys := Seq[BuildInfoKey]( - version, - scalaVersion, - scalaBinaryVersion - ), + snapshotsPackageName := "proompts", + snapshotsAddRuntimeDependency := false, + snapshotsProjectIdentifier := { + val platformSuffix = + virtualAxes.value.collectFirst { case p: VirtualAxis.PlatformAxis => + p + }.get + + moduleName.value + "-" + platformSuffix.value + + }, scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", - nativeConfig ~= (_.withIncrementalCompilation(true)), - withSnapshotTesting + nativeConfig ~= (_.withIncrementalCompilation(true)) ) lazy val snapshotsRuntime = projectMatrix @@ -92,22 +96,30 @@ lazy val snapshotsRuntime = projectMatrix .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + .settings( + scalacOptions += "-Wunused:all", + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), + nativeConfig ~= (_.withIncrementalCompilation(true)) + ) + +lazy val snapshotsSbtPlugin = project + .in(file("modules/snapshots-sbt-plugin")) + .settings( + sbtPlugin := true, + name := "sbt-proompt-snapshots" + ) .enablePlugins(BuildInfoPlugin) .settings( - buildInfoPackage := "com.indoorvivants.library.internal", + buildInfoPackage := "proomps.snapshots.sbtplugin", buildInfoKeys := Seq[BuildInfoKey]( version, scalaVersion, scalaBinaryVersion - ), - scalacOptions += "-Wunused:all", - scalaJSUseMainModuleInitializer := true, - scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", - nativeConfig ~= (_.withIncrementalCompilation(true)), + ) ) - +/* val checkSnapshots = taskKey[Unit]("") val withSnapshotTesting = Seq( @@ -199,7 +211,7 @@ def SnapshotsGenerate(path: File, tempPath: File) = """.trim.stripMargin .replace("TEMP_PATH", tempPath.toPath().toAbsolutePath().toString) .replace("PATH", path.toPath().toAbsolutePath().toString) - + */ lazy val docs = projectMatrix .in(file("myproject-docs")) .dependsOn(core) diff --git a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala new file mode 100644 index 0000000..a306b1a --- /dev/null +++ b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala @@ -0,0 +1,118 @@ +package proompts.snapshots.sbtplugin + +import sbt.Keys.* +import sbt.nio.Keys.* +import sbt.* +import scala.io.StdIn + +object SnapshotsPlugin extends AutoPlugin { + object autoImport { + val snapshotsProjectIdentifier = settingKey[String]("") + val snapshotsPackageName = settingKey[String]("") + val snapshotsAddRuntimeDependency = settingKey[Boolean]("") + + val snapshotsCheck = taskKey[Unit]("") + } + + import autoImport.* + + override def projectSettings: Seq[Setting[?]] = + Seq( + libraryDependencies ++= { + if (snapshotsAddRuntimeDependency.value) { + val cross = crossVersion.value match{ + case b: Binary => b.prefix + scalaBinaryVersion.value + case _ => scalaBinaryVersion.value + } + + Seq( + // TODO + "tech.neander" % s"snapshots-runtime_$cross" % BuildInfo.version + ) + } else Seq.empty + }, + snapshotsProjectIdentifier := moduleName.value, + snapshotsAddRuntimeDependency := true, + snapshotsCheck := { + val bold = scala.Console.BOLD + val reset = scala.Console.RESET + val legend = + s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " + val modified = IO + .listFiles( + (Test / managedResourceDirectories).value.head / "snapshots-tmp" + ) + .toList + + if (modified.isEmpty) { + System.err.println("No snapshots to check") + } else { + + modified + .filter(_.getName.endsWith("__snap.new")) + .foreach { f => + val diffFile = new File(f.toString() + ".diff") + assert(diffFile.exists(), s"Diff file $diffFile not found") + + val diffContents = scala.io.Source + .fromFile(diffFile) + .getLines() + .mkString(System.lineSeparator()) + + val snapshotName :: destination :: newContentsLines = + scala.io.Source.fromFile(f).getLines().toList + + println( + s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" + ) + println( + s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" + ) + println(diffContents) + + println("\n\n") + print(legend) + + val choice = StdIn.readLine().trim + + if (choice == "a") { + IO.writeLines(new File(destination), newContentsLines) + IO.delete(f) + IO.delete(diffFile) + } + + } + } + + }, + Test / sourceGenerators += Def.task { + val name = snapshotsProjectIdentifier.value + val packageName = snapshotsPackageName.value + + val snapshotsDestination = (Test / resourceDirectory).value / name + + val sourceDest = + (Test / managedSourceDirectories).value.head / "Snapshots.scala" + + val tmpDest = + (Test / managedResourceDirectories).value.head / "snapshots-tmp" + + IO.write( + sourceDest, + SnapshotsGenerate(snapshotsDestination, tmpDest, packageName) + ) + + IO.createDirectory(snapshotsDestination) + IO.createDirectory(tmpDest) + + Seq(sourceDest) + } + ) + + def SnapshotsGenerate(path: File, tempPath: File, packageName: String) = + s""" + |package $packageName + |object Snapshots extends proompts.snapshots.Snapshots(location = "$path", tmpLocation = "$tempPath") + """.trim.stripMargin + +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 2d6f912..c9fa2fd 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -16,3 +16,22 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") // Scala.js and Scala Native addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") + +libraryDependencies ++= List( + "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value +) +Compile / unmanagedSourceDirectories += + (ThisBuild / baseDirectory).value.getParentFile / + "modules" / "snapshots-sbt-plugin" / "src" / "main" / "scala" + +Compile / sourceGenerators += Def.task { + val tmpDest = + (Compile / managedResourceDirectories).value.head / "BuildInfo.scala" + + IO.write( + tmpDest, + "package proompts.snapshots.sbtplugin\nobject BuildInfo {def version: String = \"dev\"}" + ) + + Seq(tmpDest) +} From af250e4cfdd6be940944af464152fabc3ce93b2a Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 8 Feb 2024 16:39:07 +0000 Subject: [PATCH 13/32] Remove snapshot diffs if tests are successful --- .../core/src/test/scala/assertSnapshot.scala | 2 ++ .../src/main/scala/Snapshots.scala | 10 ++++++++++ .../src/main/scalajs/Platform.scala | 6 ++++++ .../src/main/scalajvm/Platform.scala | 3 +++ .../src/main/scalanative/Platform.scala | 3 +++ .../src/main/scala/SnapshotsPlugin.scala | 19 +++++++++++++------ 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/modules/core/src/test/scala/assertSnapshot.scala b/modules/core/src/test/scala/assertSnapshot.scala index d54e9fc..1a1d04d 100644 --- a/modules/core/src/test/scala/assertSnapshot.scala +++ b/modules/core/src/test/scala/assertSnapshot.scala @@ -21,3 +21,5 @@ inline def assertSnapshot(inline name: String, contents: String) = val diffReport = diff.createDiffOnlyReport() Snapshots.write(name, contents, diffReport) Assertions.assertNoDiff(contents, value) + else + Snapshots.clear(name) diff --git a/modules/snapshots-runtime/src/main/scala/Snapshots.scala b/modules/snapshots-runtime/src/main/scala/Snapshots.scala index 7cba64b..0006d61 100644 --- a/modules/snapshots-runtime/src/main/scala/Snapshots.scala +++ b/modules/snapshots-runtime/src/main/scala/Snapshots.scala @@ -15,6 +15,16 @@ case class Snapshots(location: String, tmpLocation: String) extends Platform: tmpFileDiff.fileWriteContents(diff) end write + inline def clear(name: String):Unit = + val tmpName = name + "__snap.new" + val tmpDiff = name + "__snap.new.diff" + val tmpFile = tmpLocation.resolve(tmpName) + val tmpFileDiff = tmpLocation.resolve(tmpDiff) + + tmpFileDiff.delete() + tmpFile.delete() + + inline def apply(inline name: String): Option[String] = location.resolve(name).readFileContents() diff --git a/modules/snapshots-runtime/src/main/scalajs/Platform.scala b/modules/snapshots-runtime/src/main/scalajs/Platform.scala index dbc9a1e..8b8b647 100644 --- a/modules/snapshots-runtime/src/main/scalajs/Platform.scala +++ b/modules/snapshots-runtime/src/main/scalajs/Platform.scala @@ -11,6 +11,10 @@ trait Platform: def fileWriteContents(contents: String): Unit = FS.writeFileSync(s, contents) + def delete(): Unit = + FS.rmSync(s, js.Dynamic.literal(force = true)) + + def readFileContents(): Option[String] = val exists = FS.statSync( @@ -28,6 +32,8 @@ end Platform private[snapshots] trait FS extends js.Object: def readFileSync(path: String, options: String | js.Object = ""): String = js.native + def rmSync(path: String, options: js.Object = js.Object()): String = + js.native def writeFileSync( path: String, contents: String, diff --git a/modules/snapshots-runtime/src/main/scalajvm/Platform.scala b/modules/snapshots-runtime/src/main/scalajvm/Platform.scala index 0fab09e..74a979f 100644 --- a/modules/snapshots-runtime/src/main/scalajvm/Platform.scala +++ b/modules/snapshots-runtime/src/main/scalajvm/Platform.scala @@ -14,6 +14,9 @@ private[snapshots] trait Platform: writer.write(contents) } + def delete(): Unit = + new File(s).delete() + def readFileContents(): Option[String] = val file = new File(s) Option.when(file.exists()): diff --git a/modules/snapshots-runtime/src/main/scalanative/Platform.scala b/modules/snapshots-runtime/src/main/scalanative/Platform.scala index 0fab09e..9a9e0ca 100644 --- a/modules/snapshots-runtime/src/main/scalanative/Platform.scala +++ b/modules/snapshots-runtime/src/main/scalanative/Platform.scala @@ -14,6 +14,9 @@ private[snapshots] trait Platform: writer.write(contents) } + def delete(): Unit = + new File(s).delete() + def readFileContents(): Option[String] = val file = new File(s) Option.when(file.exists()): diff --git a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala index a306b1a..3e50b07 100644 --- a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala +++ b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala @@ -10,19 +10,23 @@ object SnapshotsPlugin extends AutoPlugin { val snapshotsProjectIdentifier = settingKey[String]("") val snapshotsPackageName = settingKey[String]("") val snapshotsAddRuntimeDependency = settingKey[Boolean]("") - + val tag = ConcurrentRestrictions.Tag("snapshots-check") val snapshotsCheck = taskKey[Unit]("") } import autoImport.* + override def globalSettings: Seq[Setting[_]] = Seq( + concurrentRestrictions += Tags.limit(tag, 1) + ) + override def projectSettings: Seq[Setting[?]] = Seq( libraryDependencies ++= { if (snapshotsAddRuntimeDependency.value) { - val cross = crossVersion.value match{ + val cross = crossVersion.value match { case b: Binary => b.prefix + scalaBinaryVersion.value - case _ => scalaBinaryVersion.value + case _ => scalaBinaryVersion.value } Seq( @@ -33,7 +37,7 @@ object SnapshotsPlugin extends AutoPlugin { }, snapshotsProjectIdentifier := moduleName.value, snapshotsAddRuntimeDependency := true, - snapshotsCheck := { + snapshotsCheck := Def.task{ val bold = scala.Console.BOLD val reset = scala.Console.RESET val legend = @@ -45,7 +49,7 @@ object SnapshotsPlugin extends AutoPlugin { .toList if (modified.isEmpty) { - System.err.println("No snapshots to check") + System.err.println(s"No snapshots to check in [${snapshotsProjectIdentifier.value}]") } else { modified @@ -62,6 +66,9 @@ object SnapshotsPlugin extends AutoPlugin { val snapshotName :: destination :: newContentsLines = scala.io.Source.fromFile(f).getLines().toList + println( + s"Project ID: ${bold}${snapshotsProjectIdentifier.value}${reset}" + ) println( s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" ) @@ -84,7 +91,7 @@ object SnapshotsPlugin extends AutoPlugin { } } - }, + }.tag(tag).value, Test / sourceGenerators += Def.task { val name = snapshotsProjectIdentifier.value val packageName = snapshotsPackageName.value From a16c90705d8cbf42eba6a3f69daa5ded85e53292 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 9 Feb 2024 09:44:35 +0000 Subject: [PATCH 14/32] Properly separate capabilities --- .../core/src/main/scala/AnsiTerminal.scala | 4 +- modules/core/src/main/scala/Example.scala | 50 ++------ .../core/src/main/scala/InputProvider.scala | 9 +- modules/core/src/main/scala/Terminal.scala | 2 +- .../InputProviderCompanionPlatform.scala | 31 +++++ .../src/main/scalajs/InputProviderImpl.scala | 86 +++++++++++++ .../main/scalajs/InputProviderPlatform.scala | 100 --------------- .../main/scalajvm/InputProviderPlatform.scala | 9 +- .../main/scalanative/InputProviderImpl.scala | 70 +++++++++++ .../scalanative/InputProviderPlatform.scala | 78 ++---------- .../src/main/scala/SnapshotsPlugin.scala | 114 +++++++++--------- 11 files changed, 279 insertions(+), 274 deletions(-) create mode 100644 modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala create mode 100644 modules/core/src/main/scalajs/InputProviderImpl.scala delete mode 100644 modules/core/src/main/scalajs/InputProviderPlatform.scala create mode 100644 modules/core/src/main/scalanative/InputProviderImpl.scala diff --git a/modules/core/src/main/scala/AnsiTerminal.scala b/modules/core/src/main/scala/AnsiTerminal.scala index 2d345ac..a848d4b 100644 --- a/modules/core/src/main/scala/AnsiTerminal.scala +++ b/modules/core/src/main/scala/AnsiTerminal.scala @@ -16,9 +16,11 @@ package com.indoorvivants.proompts -class AnsiTerminal(writer: String => Unit) extends Terminal: +class AnsiTerminal(out: Output) extends Terminal: import AnsiTerminal.{ESC, CSI} + private val writer = (s: String) => out.out(s) + private inline def call(name: Char, inline args: Int*): this.type = writer(s"$CSI${args.mkString(";")}$name") this diff --git a/modules/core/src/main/scala/Example.scala b/modules/core/src/main/scala/Example.scala index e2d15d8..9063378 100644 --- a/modules/core/src/main/scala/Example.scala +++ b/modules/core/src/main/scala/Example.scala @@ -17,51 +17,17 @@ package com.indoorvivants.proompts @main def hello = + val terminal = Terminal.ansi(Output.Std) - def testingProgram( - terminal: Terminal, - events: List[(Event, () => Unit)], - out: Output - ) = - val i = InteractiveAlternatives( - terminal, - Prompt.Alternatives( - "How do you do fellow kids?", - List("killa", "rizza", "flizza") - ), - out, - colors = false - ) - - events.foreach: (ev, callback) => - i.handler(ev) - callback() - end testingProgram + val prompt = Prompt.Alternatives( + "How is your day?", + List("Good", "bad", "excellent", "could've been better") + ) - val term = TracingTerminal(Output.DarkVoid) - val capturing = Output.Delegate(term.writer, s => Output.StdOut.logLn(s)) + val interactive = Interactive(terminal, prompt, Output.Std, true) - val events = - List( - Event.Init, - Event.Key(KeyEvent.DOWN), - Event.Char('r'), - Event.Key(KeyEvent.DELETE), - Event.Char('l'), - Event.Key(KeyEvent.DOWN), - Event.Key(KeyEvent.UP), - Event.Char('i') - ) + val inputProvider = InputProvider(Output.Std) - testingProgram( - term, - events - .map(ev => - ev -> { () => - println(ev); println(term.getPretty()) - } - ), - capturing - ) + val result = inputProvider.evaluateFuture(interactive) end hello diff --git a/modules/core/src/main/scala/InputProvider.scala b/modules/core/src/main/scala/InputProvider.scala index 3b861d1..d0daf0f 100644 --- a/modules/core/src/main/scala/InputProvider.scala +++ b/modules/core/src/main/scala/InputProvider.scala @@ -16,12 +16,13 @@ package com.indoorvivants.proompts -case class Environment(writer: String => Unit) +// case class Environment(writer: String => Unit) abstract class Handler: def apply(ev: Event): Next -trait InputProvider extends AutoCloseable: - def attach(env: Environment => Handler): Completion +abstract class InputProvider(protected val output: Output) + extends AutoCloseable, + InputProviderPlatform -object InputProvider extends InputProviderPlatform +object InputProvider extends InputProviderCompanionPlatform diff --git a/modules/core/src/main/scala/Terminal.scala b/modules/core/src/main/scala/Terminal.scala index 73cf4ef..af141a2 100644 --- a/modules/core/src/main/scala/Terminal.scala +++ b/modules/core/src/main/scala/Terminal.scala @@ -64,4 +64,4 @@ trait Terminal: end Terminal object Terminal: - def ansi(writer: String => Unit) = AnsiTerminal(writer) + def ansi(out: Output) = AnsiTerminal(out) diff --git a/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala b/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala new file mode 100644 index 0000000..e1625d1 --- /dev/null +++ b/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +import scala.concurrent.Future + +trait InputProviderPlatform: + self: InputProvider => + + def evaluateFuture( + f: Interactive + ): Future[Completion] + +trait InputProviderCompanionPlatform: + def apply(o: Output): InputProvider = InputProviderImpl(o) + +end InputProviderCompanionPlatform diff --git a/modules/core/src/main/scalajs/InputProviderImpl.scala b/modules/core/src/main/scalajs/InputProviderImpl.scala new file mode 100644 index 0000000..44221c5 --- /dev/null +++ b/modules/core/src/main/scalajs/InputProviderImpl.scala @@ -0,0 +1,86 @@ +package com.indoorvivants.proompts + +import scala.scalajs.js.annotation.JSGlobal +import scala.scalajs.js.annotation.JSImport + +import com.indoorvivants.proompts.CharCollector.State +import com.indoorvivants.proompts.CharCollector.decode + +import scalajs.js +import scala.concurrent.Future + +private class InputProviderImpl(o: Output) + extends InputProvider(o), + InputProviderPlatform: + override def evaluateFuture( + f: Interactive + ): Future[Completion] = + + val stdin = Process.stdin + if stdin.isTTY.contains(true) then + + stdin.setRawMode(true) + // val env = Environment(writer = s => + // System.err.println(s.getBytes.toList) + // Process.stdout.write(s) + // ) + + val handler = f.handler + + val rl = Readline.createInterface( + js.Dynamic.literal( + input = stdin, + escapeCodeTimeout = 50 + ) + ) + + Readline.emitKeypressEvents(stdin, rl) + + var state = State.Init + + var completion = Completion.Finished + + lazy val keypress: js.Function = (str: js.UndefOr[String], key: Key) => + handle(key) + + def close(res: Completion) = + stdin.setRawMode(false) + rl.close() + stdin.removeListener("keypress", keypress) + completion = res + + def whatNext(n: Next) = + n match + case Next.Continue => + case Next.Stop => close(Completion.Interrupted) + case Next.Error(msg) => close(Completion.Error(msg)) + + def send(ev: Event) = + whatNext(handler(ev)) + + def handle(key: Key) = + if key.name == "c" && key.ctrl then + stdin.setRawMode(false) + rl.close() + stdin.removeListener("keypress", keypress) + else + key.sequence + .getBytes() + .foreach: byte => + val (newState, result) = decode(state, byte) + + state = newState + + result match + case n: Next => whatNext(n) + case e: Event => send(e) + + handler(Event.Init) + stdin.on("keypress", keypress) + + Future.successful(completion) // TODO fix + else Future.successful(Completion.Error("STDIN is not a TTY")) + end if + end evaluateFuture + override def close(): Unit = () +end InputProviderImpl diff --git a/modules/core/src/main/scalajs/InputProviderPlatform.scala b/modules/core/src/main/scalajs/InputProviderPlatform.scala deleted file mode 100644 index 149ff04..0000000 --- a/modules/core/src/main/scalajs/InputProviderPlatform.scala +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2023 Anton Sviridov - * - * 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 com.indoorvivants.proompts - -import scala.scalajs.js.annotation.JSGlobal -import scala.scalajs.js.annotation.JSImport - -import com.indoorvivants.proompts.CharCollector.State -import com.indoorvivants.proompts.CharCollector.decode - -import scalajs.js - -trait InputProviderPlatform: - def apply(): InputProvider = new InputProvider: - override def attach( - f: Environment => Handler - ): Completion = - - val stdin = Process.stdin - if stdin.isTTY.contains(true) then - - stdin.setRawMode(true) - val env = Environment(writer = s => - System.err.println(s.getBytes.toList) - Process.stdout.write(s) - ) - - val handler = f(env) - - val rl = Readline.createInterface( - js.Dynamic.literal( - input = stdin, - escapeCodeTimeout = 50 - ) - ) - - Readline.emitKeypressEvents(stdin, rl) - - var state = State.Init - - var completion = Completion.Finished - - lazy val keypress: js.Function = (str: js.UndefOr[String], key: Key) => - handle(key) - - def close(res: Completion) = - stdin.setRawMode(false) - rl.close() - stdin.removeListener("keypress", keypress) - completion = res - - def whatNext(n: Next) = - n match - case Next.Continue => - case Next.Stop => close(Completion.Interrupted) - case Next.Error(msg) => close(Completion.Error(msg)) - - def send(ev: Event) = - whatNext(handler(ev)) - - def handle(key: Key) = - if key.name == "c" && key.ctrl then - stdin.setRawMode(false) - rl.close() - stdin.removeListener("keypress", keypress) - else - key.sequence - .getBytes() - .foreach: byte => - val (newState, result) = decode(state, byte) - - state = newState - - result match - case n: Next => whatNext(n) - case e: Event => send(e) - - handler(Event.Init) - stdin.on("keypress", keypress) - - completion - else Completion.Error("STDIN is not a TTY") - end if - end attach - override def close(): Unit = () -end InputProviderPlatform diff --git a/modules/core/src/main/scalajvm/InputProviderPlatform.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala index 98908fa..5d55820 100644 --- a/modules/core/src/main/scalajvm/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajvm/InputProviderPlatform.scala @@ -17,4 +17,11 @@ package com.indoorvivants.proompts trait InputProviderPlatform: - def apply(): InputProvider = ??? + self: InputProvider => + +private class InputProviderImpl(o: Output) + extends InputProvider(o), + InputProviderPlatform + +trait InputProviderCompanionPlatform: + def apply(f: Output): InputProvider = InputProviderImpl(f) diff --git a/modules/core/src/main/scalanative/InputProviderImpl.scala b/modules/core/src/main/scalanative/InputProviderImpl.scala new file mode 100644 index 0000000..fd712e5 --- /dev/null +++ b/modules/core/src/main/scalanative/InputProviderImpl.scala @@ -0,0 +1,70 @@ +package com.indoorvivants.proompts + +import scala.util.boundary +import scalanative.libc.stdio.getchar +import scalanative.unsafe.* +import scalanative.posix.termios.* +import boundary.break +import CharCollector.* +import scala.concurrent.Future + +def changemode(dir: Int) = + val oldt = stackalloc[termios]() + val newt = stackalloc[termios]() + val STDIN_FILENO = 0 + if dir == 1 then + tcgetattr(STDIN_FILENO, oldt) + !newt = !oldt + (!newt)._4 = (!newt)._4 & ~(ICANON | ECHO) + tcsetattr(STDIN_FILENO, TCSANOW, newt) + else tcsetattr(STDIN_FILENO, TCSANOW, oldt) +end changemode + +private class InputProviderImpl(o: Output) + extends InputProvider(o), + InputProviderPlatform: + + override def evaluateFuture(f: Interactive) = + Future.successful(evaluate(f)) + + override def evaluate(f: Interactive): Completion = + changemode(1) + + var lastRead = 0 + + inline def read() = + lastRead = getchar() + lastRead + + boundary[Completion]: + + def whatNext(n: Next) = + n match + case Next.Continue => + case Next.Stop => break(Completion.Interrupted) + case Next.Error(msg) => break(Completion.Error(msg)) + + def send(ev: Event) = + whatNext(f.handler(ev)) + + var state = State.Init + + whatNext(f.handler(Event.Init)) + + while read() != 0 do + val (newState, result) = decode(state, lastRead) + + result match + case n: Next => whatNext(n) + case e: Event => + send(e) + + state = newState + + end while + + Completion.Finished + end evaluate + + override def close() = changemode(0) +end InputProviderImpl diff --git a/modules/core/src/main/scalanative/InputProviderPlatform.scala b/modules/core/src/main/scalanative/InputProviderPlatform.scala index edd0048..9a71783 100644 --- a/modules/core/src/main/scalanative/InputProviderPlatform.scala +++ b/modules/core/src/main/scalanative/InputProviderPlatform.scala @@ -1,76 +1,14 @@ package com.indoorvivants.proompts -import scala.util.boundary -import scalanative.libc.stdio.getchar -import scalanative.unsafe.* -import scalanative.posix.termios.* -import boundary.break -import CharCollector.* +import scala.concurrent.Future -class InputProviderPlatform extends InputProvider: - override def attach(f: Environment => Handler): Completion = - changemode(1) +trait InputProviderPlatform: + self: InputProvider => - var lastRead = 0 + def evaluate(f: Interactive): Completion + def evaluateFuture(f: Interactive): Future[Completion] - inline def read() = - lastRead = getchar() - lastRead - - val env = Environment(str => - // System.err.println(str.toCharArray().toList) - System.out.write(str.getBytes()) - System.out.flush() - ) - - val listener = f(env) - - boundary[Completion]: - - def whatNext(n: Next) = - n match - case Next.Continue => - case Next.Stop => break(Completion.Interrupted) - case Next.Error(msg) => break(Completion.Error(msg)) - - def send(ev: Event) = - whatNext(listener(ev)) - - var state = State.Init - - whatNext(listener(Event.Init)) - - while read() != 0 do - - errln("what") - - val (newState, result) = decode(state, lastRead) - - result match - case n: Next => whatNext(n) - case e: Event => - send(e) - - state = newState - - end while - - Completion.Finished - - end attach - - override def close() = changemode(0) -end InputProviderPlatform - -def changemode(dir: Int) = - val oldt = stackalloc[termios]() - val newt = stackalloc[termios]() - val STDIN_FILENO = 0 - if dir == 1 then - tcgetattr(STDIN_FILENO, oldt) - !newt = !oldt - (!newt)._4 = (!newt)._4 & ~(ICANON | ECHO) - tcsetattr(STDIN_FILENO, TCSANOW, newt) - else tcsetattr(STDIN_FILENO, TCSANOW, oldt) -end changemode +trait InputProviderCompanionPlatform: + def apply(o: Output): InputProvider = InputProviderImpl(o) +end InputProviderCompanionPlatform diff --git a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala index 3e50b07..1b4a6a4 100644 --- a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala +++ b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala @@ -10,13 +10,13 @@ object SnapshotsPlugin extends AutoPlugin { val snapshotsProjectIdentifier = settingKey[String]("") val snapshotsPackageName = settingKey[String]("") val snapshotsAddRuntimeDependency = settingKey[Boolean]("") - val tag = ConcurrentRestrictions.Tag("snapshots-check") + val tag = ConcurrentRestrictions.Tag("snapshots-check") val snapshotsCheck = taskKey[Unit]("") } import autoImport.* - override def globalSettings: Seq[Setting[_]] = Seq( + override def globalSettings: Seq[Setting[?]] = Seq( concurrentRestrictions += Tags.limit(tag, 1) ) @@ -30,68 +30,72 @@ object SnapshotsPlugin extends AutoPlugin { } Seq( - // TODO "tech.neander" % s"snapshots-runtime_$cross" % BuildInfo.version ) } else Seq.empty }, snapshotsProjectIdentifier := moduleName.value, snapshotsAddRuntimeDependency := true, - snapshotsCheck := Def.task{ - val bold = scala.Console.BOLD - val reset = scala.Console.RESET - val legend = - s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " - val modified = IO - .listFiles( - (Test / managedResourceDirectories).value.head / "snapshots-tmp" - ) - .toList - - if (modified.isEmpty) { - System.err.println(s"No snapshots to check in [${snapshotsProjectIdentifier.value}]") - } else { - - modified - .filter(_.getName.endsWith("__snap.new")) - .foreach { f => - val diffFile = new File(f.toString() + ".diff") - assert(diffFile.exists(), s"Diff file $diffFile not found") - - val diffContents = scala.io.Source - .fromFile(diffFile) - .getLines() - .mkString(System.lineSeparator()) - - val snapshotName :: destination :: newContentsLines = - scala.io.Source.fromFile(f).getLines().toList - - println( - s"Project ID: ${bold}${snapshotsProjectIdentifier.value}${reset}" - ) - println( - s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" - ) - println( - s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" - ) - println(diffContents) - - println("\n\n") - print(legend) - - val choice = StdIn.readLine().trim - - if (choice == "a") { - IO.writeLines(new File(destination), newContentsLines) - IO.delete(f) - IO.delete(diffFile) + snapshotsCheck := Def + .task { + val bold = scala.Console.BOLD + val reset = scala.Console.RESET + val legend = + s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " + val modified = IO + .listFiles( + (Test / managedResourceDirectories).value.head / "snapshots-tmp" + ) + .toList + + if (modified.isEmpty) { + System.err.println( + s"No snapshots to check in [${snapshotsProjectIdentifier.value}]" + ) + } else { + + modified + .filter(_.getName.endsWith("__snap.new")) + .foreach { f => + val diffFile = new File(f.toString() + ".diff") + assert(diffFile.exists(), s"Diff file $diffFile not found") + + val diffContents = scala.io.Source + .fromFile(diffFile) + .getLines() + .mkString(System.lineSeparator()) + + val snapshotName :: destination :: newContentsLines = + scala.io.Source.fromFile(f).getLines().toList + + println( + s"Project ID: ${bold}${snapshotsProjectIdentifier.value}${reset}" + ) + println( + s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" + ) + println( + s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" + ) + println(diffContents) + + println("\n\n") + print(legend) + + val choice = StdIn.readLine().trim + + if (choice == "a") { + IO.writeLines(new File(destination), newContentsLines) + IO.delete(f) + IO.delete(diffFile) + } + } + } - } } - - }.tag(tag).value, + .tag(tag) + .value, Test / sourceGenerators += Def.task { val name = snapshotsProjectIdentifier.value val packageName = snapshotsPackageName.value From 5b21ac49797cd5fb2985e4c6e7176117f7a0ec58 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 11 Feb 2024 13:38:59 +0000 Subject: [PATCH 15/32] Switch to sbt-snapshots --- build.sbt | 145 +----------------- .../src/main/scalajs/InputProviderImpl.scala | 2 +- .../main/scalajvm/InputProviderPlatform.scala | 3 +- .../main/scalanative/InputProviderImpl.scala | 3 +- .../core/src/test/scala/TerminalTests.scala | 3 +- .../core/src/test/scala/assertSnapshot.scala | 25 --- .../src/main/scala/hello.scala} | 0 .../src/main/scala/Snapshots.scala | 32 ---- .../src/main/scalajs/Platform.scala | 47 ------ .../src/main/scalajvm/Platform.scala | 28 ---- .../src/main/scalanative/Platform.scala | 28 ---- .../src/main/scala/SnapshotsPlugin.scala | 129 ---------------- project/plugins.sbt | 22 +-- 13 files changed, 14 insertions(+), 453 deletions(-) delete mode 100644 modules/core/src/test/scala/assertSnapshot.scala rename modules/{core/src/main/scala/Example.scala => example/src/main/scala/hello.scala} (100%) delete mode 100644 modules/snapshots-runtime/src/main/scala/Snapshots.scala delete mode 100644 modules/snapshots-runtime/src/main/scalajs/Platform.scala delete mode 100644 modules/snapshots-runtime/src/main/scalajvm/Platform.scala delete mode 100644 modules/snapshots-runtime/src/main/scalanative/Platform.scala delete mode 100644 modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala diff --git a/build.sbt b/build.sbt index e7b6cf8..dddee70 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,7 @@ inThisBuild( scalafixScalaBinaryVersion := scalaBinaryVersion.value, organization := "com.indoorvivants", organizationName := "Anton Sviridov", + resolvers ++= Resolver.sonatypeOssRepos("releases"), homepage := Some( url("https://github.com/neandertech/proompts") ), @@ -39,7 +40,6 @@ val Versions = new { // https://github.com/cb372/sbt-explicit-dependencies/issues/27 lazy val disableDependencyChecks = Seq( unusedCompileDependenciesTest := {}, - missinglinkCheck := {}, undeclaredCompileDependenciesTest := {} ) @@ -61,157 +61,21 @@ lazy val core = projectMatrix .settings( name := "core" ) - .dependsOn(snapshotsRuntime % "test->compile") .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) - .enablePlugins(SnapshotsPlugin) .settings( snapshotsPackageName := "proompts", - snapshotsAddRuntimeDependency := false, - snapshotsProjectIdentifier := { - val platformSuffix = - virtualAxes.value.collectFirst { case p: VirtualAxis.PlatformAxis => - p - }.get - - moduleName.value + "-" + platformSuffix.value - - }, + snapshotsIntegrations += SnapshotIntegration.MUnit, scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", nativeConfig ~= (_.withIncrementalCompilation(true)) ) + .enablePlugins(SnapshotsPlugin) -lazy val snapshotsRuntime = projectMatrix - .in(file("modules/snapshots-runtime")) - .defaultAxes(defaults*) - .settings( - name := "snapshots-runtime" - ) - .settings(munitSettings) - .jvmPlatform(Versions.scalaVersions) - .jsPlatform(Versions.scalaVersions, disableDependencyChecks) - .nativePlatform(Versions.scalaVersions, disableDependencyChecks) - .settings( - scalacOptions += "-Wunused:all", - scalaJSUseMainModuleInitializer := true, - scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - nativeConfig ~= (_.withIncrementalCompilation(true)) - ) - -lazy val snapshotsSbtPlugin = project - .in(file("modules/snapshots-sbt-plugin")) - .settings( - sbtPlugin := true, - name := "sbt-proompt-snapshots" - ) - .enablePlugins(BuildInfoPlugin) - .settings( - buildInfoPackage := "proomps.snapshots.sbtplugin", - buildInfoKeys := Seq[BuildInfoKey]( - version, - scalaVersion, - scalaBinaryVersion - ) - ) - -/* -val checkSnapshots = taskKey[Unit]("") - -val withSnapshotTesting = Seq( - checkSnapshots := { - val bold = scala.Console.BOLD - val reset = scala.Console.RESET - val legend = - s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " - val modified = IO - .listFiles( - (Test / managedResourceDirectories).value.head / "snapshots-tmp" - ) - .toList - - if (modified.isEmpty) { - System.err.println("No snapshots to check") - } else { - - modified - .filter(_.getName.endsWith("__snap.new")) - .foreach { f => - val diffFile = new File(f.toString() + ".diff") - assert(diffFile.exists(), s"Diff file $diffFile not found") - - val diffContents = scala.io.Source - .fromFile(diffFile) - .getLines() - .mkString(System.lineSeparator()) - - val snapshotName :: destination :: newContentsLines = - scala.io.Source.fromFile(f).getLines().toList - - println( - s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" - ) - println( - s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" - ) - println(diffContents) - - println("\n\n") - print(legend) - - val choice = StdIn.readLine().trim - - if (choice == "a") { - IO.writeLines(new File(destination), newContentsLines) - IO.delete(f) - IO.delete(diffFile) - } - - } - } - - }, - Test / sourceGenerators += Def.task { - val platformSuffix = - virtualAxes.value.collectFirst { case p: VirtualAxis.PlatformAxis => - p - }.get - - val isNative = platformSuffix.value == "jvm" - val isJS = platformSuffix.value == "js" - val isJVM = !isNative && !isJS - - val name = moduleName.value + "-" + platformSuffix.value - - val snapshotsDestination = (Test / resourceDirectory).value / name - - val sourceDest = - (Test / managedSourceDirectories).value.head / "Snapshots.scala" - - val tmpDest = - (Test / managedResourceDirectories).value.head / "snapshots-tmp" - - IO.write(sourceDest, SnapshotsGenerate(snapshotsDestination, tmpDest)) - - IO.createDirectory(snapshotsDestination) - IO.createDirectory(tmpDest) - - Seq(sourceDest) - } -) - -def SnapshotsGenerate(path: File, tempPath: File) = - """ - |package proompts - |object Snapshots extends proompts.snapshots.Snapshots(location = "PATH", tmpLocation = "TEMP_PATH") - """.trim.stripMargin - .replace("TEMP_PATH", tempPath.toPath().toAbsolutePath().toString) - .replace("PATH", path.toPath().toAbsolutePath().toString) - */ lazy val docs = projectMatrix .in(file("myproject-docs")) .dependsOn(core) @@ -251,8 +115,7 @@ val CICommands = Seq( s"scalafix --check $scalafixRules", "headerCheck", "undeclaredCompileDependenciesTest", - "unusedCompileDependenciesTest", - "missinglinkCheck" + "unusedCompileDependenciesTest" ).mkString(";") val PrepareCICommands = Seq( diff --git a/modules/core/src/main/scalajs/InputProviderImpl.scala b/modules/core/src/main/scalajs/InputProviderImpl.scala index 44221c5..a53e832 100644 --- a/modules/core/src/main/scalajs/InputProviderImpl.scala +++ b/modules/core/src/main/scalajs/InputProviderImpl.scala @@ -1,5 +1,6 @@ package com.indoorvivants.proompts +import scala.concurrent.Future import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport @@ -7,7 +8,6 @@ import com.indoorvivants.proompts.CharCollector.State import com.indoorvivants.proompts.CharCollector.decode import scalajs.js -import scala.concurrent.Future private class InputProviderImpl(o: Output) extends InputProvider(o), diff --git a/modules/core/src/main/scalajvm/InputProviderPlatform.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala index 5d55820..ea5f821 100644 --- a/modules/core/src/main/scalajvm/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajvm/InputProviderPlatform.scala @@ -21,7 +21,8 @@ trait InputProviderPlatform: private class InputProviderImpl(o: Output) extends InputProvider(o), - InputProviderPlatform + InputProviderPlatform: + def close(): Unit = () trait InputProviderCompanionPlatform: def apply(f: Output): InputProvider = InputProviderImpl(f) diff --git a/modules/core/src/main/scalanative/InputProviderImpl.scala b/modules/core/src/main/scalanative/InputProviderImpl.scala index fd712e5..76056e2 100644 --- a/modules/core/src/main/scalanative/InputProviderImpl.scala +++ b/modules/core/src/main/scalanative/InputProviderImpl.scala @@ -1,12 +1,13 @@ package com.indoorvivants.proompts +import scala.concurrent.Future import scala.util.boundary + import scalanative.libc.stdio.getchar import scalanative.unsafe.* import scalanative.posix.termios.* import boundary.break import CharCollector.* -import scala.concurrent.Future def changemode(dir: Int) = val oldt = stackalloc[termios]() diff --git a/modules/core/src/test/scala/TerminalTests.scala b/modules/core/src/test/scala/TerminalTests.scala index 49a7ea0..dac254f 100644 --- a/modules/core/src/test/scala/TerminalTests.scala +++ b/modules/core/src/test/scala/TerminalTests.scala @@ -1,8 +1,9 @@ package proompts import com.indoorvivants.proompts.* +import com.indoorvivants.snapshots.munit_integration.* -trait TerminalTests: +trait TerminalTests extends MunitSnapshotsIntegration: self: munit.FunSuite => def terminalTest( diff --git a/modules/core/src/test/scala/assertSnapshot.scala b/modules/core/src/test/scala/assertSnapshot.scala deleted file mode 100644 index 1a1d04d..0000000 --- a/modules/core/src/test/scala/assertSnapshot.scala +++ /dev/null @@ -1,25 +0,0 @@ -package proompts -import munit.internal.difflib.Diffs -import munit.Assertions - -inline def assertSnapshot(inline name: String, contents: String) = - Snapshots(name) match - case None => - Snapshots.write( - name, - contents, - Diffs.create(contents, "").createDiffOnlyReport() - ) - - Assertions.fail( - s"No snapshot was found for $name, please run checkSnapshots command and accept a snapshot for this test" - ) - - case Some(value) => - val diff = Diffs.create(contents, value) - if !diff.isEmpty then - val diffReport = diff.createDiffOnlyReport() - Snapshots.write(name, contents, diffReport) - Assertions.assertNoDiff(contents, value) - else - Snapshots.clear(name) diff --git a/modules/core/src/main/scala/Example.scala b/modules/example/src/main/scala/hello.scala similarity index 100% rename from modules/core/src/main/scala/Example.scala rename to modules/example/src/main/scala/hello.scala diff --git a/modules/snapshots-runtime/src/main/scala/Snapshots.scala b/modules/snapshots-runtime/src/main/scala/Snapshots.scala deleted file mode 100644 index 0006d61..0000000 --- a/modules/snapshots-runtime/src/main/scala/Snapshots.scala +++ /dev/null @@ -1,32 +0,0 @@ -package proompts.snapshots - -case class Snapshots(location: String, tmpLocation: String) extends Platform: - inline def write(name: String, contents: String, diff: String): Unit = - val tmpName = name + "__snap.new" - val tmpDiff = name + "__snap.new.diff" - val file = location.resolve(name) - val tmpFile = tmpLocation.resolve(tmpName) - val tmpFileDiff = tmpLocation.resolve(tmpDiff) - - val snapContents = - name + "\n" + file + "\n" + contents - - tmpFile.fileWriteContents(snapContents) - tmpFileDiff.fileWriteContents(diff) - end write - - inline def clear(name: String):Unit = - val tmpName = name + "__snap.new" - val tmpDiff = name + "__snap.new.diff" - val tmpFile = tmpLocation.resolve(tmpName) - val tmpFileDiff = tmpLocation.resolve(tmpDiff) - - tmpFileDiff.delete() - tmpFile.delete() - - - inline def apply(inline name: String): Option[String] = - location.resolve(name).readFileContents() - - end apply -end Snapshots diff --git a/modules/snapshots-runtime/src/main/scalajs/Platform.scala b/modules/snapshots-runtime/src/main/scalajs/Platform.scala deleted file mode 100644 index 8b8b647..0000000 --- a/modules/snapshots-runtime/src/main/scalajs/Platform.scala +++ /dev/null @@ -1,47 +0,0 @@ -package proompts.snapshots - -import scala.scalajs.js.annotation.JSImport -import scalajs.js - -trait Platform: - extension (s: String) - def resolve(segment: String): String = - s + "/" + segment - - def fileWriteContents(contents: String): Unit = - FS.writeFileSync(s, contents) - - def delete(): Unit = - FS.rmSync(s, js.Dynamic.literal(force = true)) - - - def readFileContents(): Option[String] = - val exists = - FS.statSync( - s, - js.Dynamic.literal(throwIfNoEntry = false) - ) != js.undefined - Option.when(exists): - FS.readFileSync(s, js.Dynamic.literal(encoding = "utf8")) - end readFileContents - end extension - -end Platform - -@js.native -private[snapshots] trait FS extends js.Object: - def readFileSync(path: String, options: String | js.Object = ""): String = - js.native - def rmSync(path: String, options: js.Object = js.Object()): String = - js.native - def writeFileSync( - path: String, - contents: String, - options: String = "" - ): Unit = js.native - def statSync(path: String, options: js.Any): js.Any = js.native -end FS - -@js.native -@JSImport("node:fs", JSImport.Namespace) -private[snapshots] object FS extends FS diff --git a/modules/snapshots-runtime/src/main/scalajvm/Platform.scala b/modules/snapshots-runtime/src/main/scalajvm/Platform.scala deleted file mode 100644 index 74a979f..0000000 --- a/modules/snapshots-runtime/src/main/scalajvm/Platform.scala +++ /dev/null @@ -1,28 +0,0 @@ -package proompts.snapshots - -import java.nio.file.Paths -import java.io.FileWriter -import java.io.File - -private[snapshots] trait Platform: - extension (s: String) - def resolve(segment: String): String = - Paths.get(s).resolve(segment).toString() - - def fileWriteContents(contents: String): Unit = - scala.util.Using(new FileWriter(new File(s))) { writer => - writer.write(contents) - } - - def delete(): Unit = - new File(s).delete() - - def readFileContents(): Option[String] = - val file = new File(s) - Option.when(file.exists()): - scala.io.Source - .fromFile(file, "utf-8") - .getLines() - .mkString("\n") - end extension -end Platform diff --git a/modules/snapshots-runtime/src/main/scalanative/Platform.scala b/modules/snapshots-runtime/src/main/scalanative/Platform.scala deleted file mode 100644 index 9a9e0ca..0000000 --- a/modules/snapshots-runtime/src/main/scalanative/Platform.scala +++ /dev/null @@ -1,28 +0,0 @@ -package proompts.snapshots - -import java.nio.file.Paths -import java.io.FileWriter -import java.io.File - -private[snapshots] trait Platform: - extension (s: String) - def resolve(segment: String): String = - Paths.get(s).resolve(segment).toString() - - def fileWriteContents(contents: String): Unit = - scala.util.Using(new FileWriter(new File(s))) { writer => - writer.write(contents) - } - - def delete(): Unit = - new File(s).delete() - - def readFileContents(): Option[String] = - val file = new File(s) - Option.when(file.exists()): - scala.io.Source - .fromFile(file, "utf-8") - .getLines() - .mkString("\n") - end extension -end Platform diff --git a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala b/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala deleted file mode 100644 index 1b4a6a4..0000000 --- a/modules/snapshots-sbt-plugin/src/main/scala/SnapshotsPlugin.scala +++ /dev/null @@ -1,129 +0,0 @@ -package proompts.snapshots.sbtplugin - -import sbt.Keys.* -import sbt.nio.Keys.* -import sbt.* -import scala.io.StdIn - -object SnapshotsPlugin extends AutoPlugin { - object autoImport { - val snapshotsProjectIdentifier = settingKey[String]("") - val snapshotsPackageName = settingKey[String]("") - val snapshotsAddRuntimeDependency = settingKey[Boolean]("") - val tag = ConcurrentRestrictions.Tag("snapshots-check") - val snapshotsCheck = taskKey[Unit]("") - } - - import autoImport.* - - override def globalSettings: Seq[Setting[?]] = Seq( - concurrentRestrictions += Tags.limit(tag, 1) - ) - - override def projectSettings: Seq[Setting[?]] = - Seq( - libraryDependencies ++= { - if (snapshotsAddRuntimeDependency.value) { - val cross = crossVersion.value match { - case b: Binary => b.prefix + scalaBinaryVersion.value - case _ => scalaBinaryVersion.value - } - - Seq( - "tech.neander" % s"snapshots-runtime_$cross" % BuildInfo.version - ) - } else Seq.empty - }, - snapshotsProjectIdentifier := moduleName.value, - snapshotsAddRuntimeDependency := true, - snapshotsCheck := Def - .task { - val bold = scala.Console.BOLD - val reset = scala.Console.RESET - val legend = - s"${bold}a${reset} - accept, ${bold}s${reset} - skip\nYour choice: " - val modified = IO - .listFiles( - (Test / managedResourceDirectories).value.head / "snapshots-tmp" - ) - .toList - - if (modified.isEmpty) { - System.err.println( - s"No snapshots to check in [${snapshotsProjectIdentifier.value}]" - ) - } else { - - modified - .filter(_.getName.endsWith("__snap.new")) - .foreach { f => - val diffFile = new File(f.toString() + ".diff") - assert(diffFile.exists(), s"Diff file $diffFile not found") - - val diffContents = scala.io.Source - .fromFile(diffFile) - .getLines() - .mkString(System.lineSeparator()) - - val snapshotName :: destination :: newContentsLines = - scala.io.Source.fromFile(f).getLines().toList - - println( - s"Project ID: ${bold}${snapshotsProjectIdentifier.value}${reset}" - ) - println( - s"Name: ${scala.Console.BOLD}$snapshotName${scala.Console.RESET}" - ) - println( - s"Path: ${scala.Console.BOLD}$destination${scala.Console.RESET}" - ) - println(diffContents) - - println("\n\n") - print(legend) - - val choice = StdIn.readLine().trim - - if (choice == "a") { - IO.writeLines(new File(destination), newContentsLines) - IO.delete(f) - IO.delete(diffFile) - } - - } - } - - } - .tag(tag) - .value, - Test / sourceGenerators += Def.task { - val name = snapshotsProjectIdentifier.value - val packageName = snapshotsPackageName.value - - val snapshotsDestination = (Test / resourceDirectory).value / name - - val sourceDest = - (Test / managedSourceDirectories).value.head / "Snapshots.scala" - - val tmpDest = - (Test / managedResourceDirectories).value.head / "snapshots-tmp" - - IO.write( - sourceDest, - SnapshotsGenerate(snapshotsDestination, tmpDest, packageName) - ) - - IO.createDirectory(snapshotsDestination) - IO.createDirectory(tmpDest) - - Seq(sourceDest) - } - ) - - def SnapshotsGenerate(path: File, tempPath: File, packageName: String) = - s""" - |package $packageName - |object Snapshots extends proompts.snapshots.Snapshots(location = "$path", tmpLocation = "$tempPath") - """.trim.stripMargin - -} diff --git a/project/plugins.sbt b/project/plugins.sbt index c9fa2fd..f0cc4ed 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,10 @@ +resolvers ++= Resolver.sonatypeOssRepos("releases") + addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") // Code quality //addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.2") -addSbtPlugin("ch.epfl.scala" % "sbt-missinglink" % "0.3.6") addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") @@ -17,21 +18,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") -libraryDependencies ++= List( - "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value -) -Compile / unmanagedSourceDirectories += - (ThisBuild / baseDirectory).value.getParentFile / - "modules" / "snapshots-sbt-plugin" / "src" / "main" / "scala" - -Compile / sourceGenerators += Def.task { - val tmpDest = - (Compile / managedResourceDirectories).value.head / "BuildInfo.scala" - - IO.write( - tmpDest, - "package proompts.snapshots.sbtplugin\nobject BuildInfo {def version: String = \"dev\"}" - ) - - Seq(tmpDest) -} +addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.3+1-03c41a8f-SNAPSHOT") From 662c7931b36ae8274509110a7bc89565110046fd Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 11 Feb 2024 13:44:04 +0000 Subject: [PATCH 16/32] Rebuild snapshots --- modules/core/src/test/resources/core-jvm/alternatives.init | 7 ------- .../{core-js => snapshots/core}/alternatives.navigation | 0 .../{core-js => snapshots/core}/alternatives.typing | 0 .../{core-jvm => snapshots/coreJS}/alternatives.navigation | 0 .../{core-jvm => snapshots/coreJS}/alternatives.typing | 0 .../coreNative}/alternatives.navigation | 0 .../coreNative}/alternatives.typing | 0 7 files changed, 7 deletions(-) delete mode 100644 modules/core/src/test/resources/core-jvm/alternatives.init rename modules/core/src/test/resources/{core-js => snapshots/core}/alternatives.navigation (100%) rename modules/core/src/test/resources/{core-js => snapshots/core}/alternatives.typing (100%) rename modules/core/src/test/resources/{core-jvm => snapshots/coreJS}/alternatives.navigation (100%) rename modules/core/src/test/resources/{core-jvm => snapshots/coreJS}/alternatives.typing (100%) rename modules/core/src/test/resources/{core-native => snapshots/coreNative}/alternatives.navigation (100%) rename modules/core/src/test/resources/{core-native => snapshots/coreNative}/alternatives.typing (100%) diff --git a/modules/core/src/test/resources/core-jvm/alternatives.init b/modules/core/src/test/resources/core-jvm/alternatives.init deleted file mode 100644 index 256646b..0000000 --- a/modules/core/src/test/resources/core-jvm/alternatives.init +++ /dev/null @@ -1,7 +0,0 @@ -Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/core-js/alternatives.navigation b/modules/core/src/test/resources/snapshots/core/alternatives.navigation similarity index 100% rename from modules/core/src/test/resources/core-js/alternatives.navigation rename to modules/core/src/test/resources/snapshots/core/alternatives.navigation diff --git a/modules/core/src/test/resources/core-js/alternatives.typing b/modules/core/src/test/resources/snapshots/core/alternatives.typing similarity index 100% rename from modules/core/src/test/resources/core-js/alternatives.typing rename to modules/core/src/test/resources/snapshots/core/alternatives.typing diff --git a/modules/core/src/test/resources/core-jvm/alternatives.navigation b/modules/core/src/test/resources/snapshots/coreJS/alternatives.navigation similarity index 100% rename from modules/core/src/test/resources/core-jvm/alternatives.navigation rename to modules/core/src/test/resources/snapshots/coreJS/alternatives.navigation diff --git a/modules/core/src/test/resources/core-jvm/alternatives.typing b/modules/core/src/test/resources/snapshots/coreJS/alternatives.typing similarity index 100% rename from modules/core/src/test/resources/core-jvm/alternatives.typing rename to modules/core/src/test/resources/snapshots/coreJS/alternatives.typing diff --git a/modules/core/src/test/resources/core-native/alternatives.navigation b/modules/core/src/test/resources/snapshots/coreNative/alternatives.navigation similarity index 100% rename from modules/core/src/test/resources/core-native/alternatives.navigation rename to modules/core/src/test/resources/snapshots/coreNative/alternatives.navigation diff --git a/modules/core/src/test/resources/core-native/alternatives.typing b/modules/core/src/test/resources/snapshots/coreNative/alternatives.typing similarity index 100% rename from modules/core/src/test/resources/core-native/alternatives.typing rename to modules/core/src/test/resources/snapshots/coreNative/alternatives.typing From 0a02afbbae5b269279af31ee8671c4be42440e1b Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 14 Feb 2024 20:12:00 +0000 Subject: [PATCH 17/32] Use Process.stdout on JS --- build.sbt | 27 +++++++++++--- modules/core/src/main/scala/Interactive.scala | 3 +- modules/core/src/main/scala/Output.scala | 23 +++++++++--- .../src/main/scalajs/InputProviderImpl.scala | 36 ++++++++++++++----- .../src/main/scalajs/NodeJSBindings.scala | 1 + .../core/src/main/scalajs/PlatformStd.scala | 22 ++++++++++++ .../main/scalajvm/InputProviderPlatform.scala | 2 +- .../core/src/main/scalajvm/PlatformStd.scala | 21 +++++++++++ .../main/scalanative/InputProviderImpl.scala | 16 +++++++++ .../scalanative/InputProviderPlatform.scala | 16 +++++++++ .../src/main/scalanative/PlatformStd.scala | 21 +++++++++++ modules/example/src/main/scala/hello.scala | 5 +-- project/plugins.sbt | 2 +- 13 files changed, 172 insertions(+), 23 deletions(-) create mode 100644 modules/core/src/main/scalajs/PlatformStd.scala create mode 100644 modules/core/src/main/scalajvm/PlatformStd.scala create mode 100644 modules/core/src/main/scalanative/PlatformStd.scala diff --git a/build.sbt b/build.sbt index dddee70..76b0372 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,6 @@ Global / excludeLintKeys += scalaJSLinkerConfig inThisBuild( List( - scalafixDependencies += "com.github.liancheng" %% "organize-imports" % Versions.organizeImports, semanticdbEnabled := true, semanticdbVersion := scalafixSemanticdb.revision, scalafixScalaBinaryVersion := scalaBinaryVersion.value, @@ -31,10 +30,9 @@ inThisBuild( ) val Versions = new { - val Scala3 = "3.3.1" - val munit = "1.0.0-M7" - val organizeImports = "0.6.0" - val scalaVersions = Seq(Scala3) + val Scala3 = "3.3.1" + val munit = "1.0.0-M11" + val scalaVersions = Seq(Scala3) } // https://github.com/cb372/sbt-explicit-dependencies/issues/27 @@ -76,6 +74,25 @@ lazy val core = projectMatrix ) .enablePlugins(SnapshotsPlugin) +lazy val example = projectMatrix + .dependsOn(core) + .in(file("modules/example")) + .defaultAxes(defaults*) + .settings( + name := "example", + noPublish + ) + .settings(munitSettings) + .jvmPlatform(Versions.scalaVersions) + .jsPlatform(Versions.scalaVersions, disableDependencyChecks) + .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + .settings( + scalacOptions += "-Wunused:all", + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), + nativeConfig ~= (_.withIncrementalCompilation(true)) + ) + lazy val docs = projectMatrix .in(file("myproject-docs")) .dependsOn(core) diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index 7c4d78b..b3effa8 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -26,7 +26,8 @@ class Interactive( ): val handler = prompt match - case p: Prompt.Input => InteractiveTextInput(p, terminal, out, colors).handler + case p: Prompt.Input => + InteractiveTextInput(p, terminal, out, colors).handler case p: Prompt.Alternatives => InteractiveAlternatives(terminal, p, out, colors).handler diff --git a/modules/core/src/main/scala/Output.scala b/modules/core/src/main/scala/Output.scala index fa59ce4..0d00aea 100644 --- a/modules/core/src/main/scala/Output.scala +++ b/modules/core/src/main/scala/Output.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts trait Output: @@ -19,9 +35,9 @@ extension (o: Output) override def out[A: AsString](a: A): Unit = () object Output: - object Std extends Output: - override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) - override def out[A: AsString](a: A): Unit = System.out.print(a.render) + object Std extends PlatformStd + // override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) + // override def out[A: AsString](a: A): Unit = System.out.print(a.render) object StdOut extends Output: override def logLn[A: AsString](a: A): Unit = System.out.println(a.render) @@ -31,7 +47,6 @@ object Output: override def logLn[A: AsString](a: A): Unit = () override def out[A: AsString](a: A): Unit = () - class Delegate(writeOut: String => Unit, writeLog: String => Unit) extends Output: override def logLn[A: AsString](a: A): Unit = writeLog(a.render + "\n") diff --git a/modules/core/src/main/scalajs/InputProviderImpl.scala b/modules/core/src/main/scalajs/InputProviderImpl.scala index a53e832..ccdc9d0 100644 --- a/modules/core/src/main/scalajs/InputProviderImpl.scala +++ b/modules/core/src/main/scalajs/InputProviderImpl.scala @@ -1,6 +1,23 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts import scala.concurrent.Future +import scala.concurrent.Promise import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport @@ -17,13 +34,10 @@ private class InputProviderImpl(o: Output) ): Future[Completion] = val stdin = Process.stdin + if stdin.isTTY.contains(true) then stdin.setRawMode(true) - // val env = Environment(writer = s => - // System.err.println(s.getBytes.toList) - // Process.stdout.write(s) - // ) val handler = f.handler @@ -38,16 +52,20 @@ private class InputProviderImpl(o: Output) var state = State.Init - var completion = Completion.Finished + val completion = Promise[Completion] + val fut = completion.future + + // var completion = Completion.Finished lazy val keypress: js.Function = (str: js.UndefOr[String], key: Key) => handle(key) def close(res: Completion) = - stdin.setRawMode(false) - rl.close() stdin.removeListener("keypress", keypress) - completion = res + if stdin.isTTY.contains(true) then stdin.setRawMode(false) + rl.close() + completion.success(res) + // completion = res def whatNext(n: Next) = n match @@ -78,7 +96,7 @@ private class InputProviderImpl(o: Output) handler(Event.Init) stdin.on("keypress", keypress) - Future.successful(completion) // TODO fix + fut else Future.successful(Completion.Error("STDIN is not a TTY")) end if end evaluateFuture diff --git a/modules/core/src/main/scalajs/NodeJSBindings.scala b/modules/core/src/main/scalajs/NodeJSBindings.scala index 48bba95..6a922d4 100644 --- a/modules/core/src/main/scalajs/NodeJSBindings.scala +++ b/modules/core/src/main/scalajs/NodeJSBindings.scala @@ -40,6 +40,7 @@ trait WriteStream extends js.Object: @js.native trait Process extends js.Object: def stdin: ReadStream = js.native + def stderr: WriteStream = js.native def stdout: WriteStream = js.native @js.native diff --git a/modules/core/src/main/scalajs/PlatformStd.scala b/modules/core/src/main/scalajs/PlatformStd.scala new file mode 100644 index 0000000..e4564b0 --- /dev/null +++ b/modules/core/src/main/scalajs/PlatformStd.scala @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +trait PlatformStd extends Output: + override def logLn[A: AsString](a: A): Unit = + Process.stderr.write(a.render + "\n") + override def out[A: AsString](a: A): Unit = Process.stdout.write(a.render) diff --git a/modules/core/src/main/scalajvm/InputProviderPlatform.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala index ea5f821..b247e28 100644 --- a/modules/core/src/main/scalajvm/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajvm/InputProviderPlatform.scala @@ -22,7 +22,7 @@ trait InputProviderPlatform: private class InputProviderImpl(o: Output) extends InputProvider(o), InputProviderPlatform: - def close(): Unit = () + def close(): Unit = () trait InputProviderCompanionPlatform: def apply(f: Output): InputProvider = InputProviderImpl(f) diff --git a/modules/core/src/main/scalajvm/PlatformStd.scala b/modules/core/src/main/scalajvm/PlatformStd.scala new file mode 100644 index 0000000..747e255 --- /dev/null +++ b/modules/core/src/main/scalajvm/PlatformStd.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +trait PlatformStd extends Output: + override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) + override def out[A: AsString](a: A): Unit = System.out.print(a.render) diff --git a/modules/core/src/main/scalanative/InputProviderImpl.scala b/modules/core/src/main/scalanative/InputProviderImpl.scala index 76056e2..2f0c7a9 100644 --- a/modules/core/src/main/scalanative/InputProviderImpl.scala +++ b/modules/core/src/main/scalanative/InputProviderImpl.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts import scala.concurrent.Future diff --git a/modules/core/src/main/scalanative/InputProviderPlatform.scala b/modules/core/src/main/scalanative/InputProviderPlatform.scala index 9a71783..bfaec31 100644 --- a/modules/core/src/main/scalanative/InputProviderPlatform.scala +++ b/modules/core/src/main/scalanative/InputProviderPlatform.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts import scala.concurrent.Future diff --git a/modules/core/src/main/scalanative/PlatformStd.scala b/modules/core/src/main/scalanative/PlatformStd.scala new file mode 100644 index 0000000..747e255 --- /dev/null +++ b/modules/core/src/main/scalanative/PlatformStd.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 com.indoorvivants.proompts + +trait PlatformStd extends Output: + override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) + override def out[A: AsString](a: A): Unit = System.out.print(a.render) diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 9063378..6c9b8ce 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -15,6 +15,7 @@ */ package com.indoorvivants.proompts +import concurrent.ExecutionContext.Implicits.global @main def hello = val terminal = Terminal.ansi(Output.Std) @@ -28,6 +29,6 @@ package com.indoorvivants.proompts val inputProvider = InputProvider(Output.Std) - val result = inputProvider.evaluateFuture(interactive) - + inputProvider.evaluateFuture(interactive).foreach: value => + println(value) end hello diff --git a/project/plugins.sbt b/project/plugins.sbt index f0cc4ed..ee98429 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -18,4 +18,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") -addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.3+1-03c41a8f-SNAPSHOT") +addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.4") From ab03719d944e8abe70590cfa9f47d75c0741da98 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 14 Feb 2024 21:23:39 +0000 Subject: [PATCH 18/32] Clear and return results --- .../src/main/scala/AlternativesState.scala | 4 +- .../core/src/main/scala/CharCollector.scala | 2 +- modules/core/src/main/scala/Completion.scala | 2 +- .../main/scala/InteractiveAlternatives.scala | 88 +++++++++++-------- modules/core/src/main/scala/Next.scala | 1 + .../src/main/scalajs/InputProviderImpl.scala | 10 +-- .../main/scalanative/InputProviderImpl.scala | 10 ++- ...ves.navigation => alternatives_navigation} | 0 ...lternatives.typing => alternatives_typing} | 0 .../alternatives_navigation} | 7 ++ ...lternatives.typing => alternatives_typing} | 0 .../alternatives_navigation} | 0 ...lternatives.typing => alternatives_typing} | 0 .../core/src/test/scala/ExampleTests.scala | 2 +- modules/example/src/main/scala/hello.scala | 6 +- project/plugins.sbt | 2 +- 16 files changed, 81 insertions(+), 53 deletions(-) rename modules/core/src/test/resources/snapshots/core/{alternatives.navigation => alternatives_navigation} (100%) rename modules/core/src/test/resources/snapshots/core/{alternatives.typing => alternatives_typing} (100%) rename modules/core/src/test/resources/snapshots/{coreNative/alternatives.navigation => coreJS/alternatives_navigation} (66%) rename modules/core/src/test/resources/snapshots/coreJS/{alternatives.typing => alternatives_typing} (100%) rename modules/core/src/test/resources/snapshots/{coreJS/alternatives.navigation => coreNative/alternatives_navigation} (100%) rename modules/core/src/test/resources/snapshots/coreNative/{alternatives.typing => alternatives_typing} (100%) diff --git a/modules/core/src/main/scala/AlternativesState.scala b/modules/core/src/main/scala/AlternativesState.scala index 56047bf..697f402 100644 --- a/modules/core/src/main/scala/AlternativesState.scala +++ b/modules/core/src/main/scala/AlternativesState.scala @@ -18,6 +18,6 @@ package com.indoorvivants.proompts case class AlternativesState( text: String, - selected: Int, - showing: Int + selected: Option[Int], + showing: List[(String, Int)] ) diff --git a/modules/core/src/main/scala/CharCollector.scala b/modules/core/src/main/scala/CharCollector.scala index 380ceae..91facbf 100644 --- a/modules/core/src/main/scala/CharCollector.scala +++ b/modules/core/src/main/scala/CharCollector.scala @@ -45,7 +45,7 @@ object CharCollector: char match case AnsiTerminal.ESC => (State.ESC_Started, Next.Continue) - case 10 => + case 10 | 13 => emit(Event.Key(KeyEvent.ENTER)) case 127 => emit(Event.Key(KeyEvent.DELETE)) diff --git a/modules/core/src/main/scala/Completion.scala b/modules/core/src/main/scala/Completion.scala index 992bad3..209ffaf 100644 --- a/modules/core/src/main/scala/Completion.scala +++ b/modules/core/src/main/scala/Completion.scala @@ -17,6 +17,6 @@ package com.indoorvivants.proompts enum Completion: - case Finished + case Finished(value: String) case Interrupted case Error(msg: String) diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 6c7a387..e3ecbde 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -22,12 +22,20 @@ class InteractiveAlternatives( out: Output, colors: Boolean ): - val lab = prompt.promptLabel - var state = AlternativesState("", 0, prompt.alts.length) + val lab = prompt.promptLabel + val altsWithIndex = prompt.alts.zipWithIndex + var state = AlternativesState("", Some(0), altsWithIndex) def colored(msg: String)(f: String => fansi.Str) = if colors then f(msg).toString else msg + def clear(oldState: AlternativesState, newState: AlternativesState) = + import terminal.* + for _ <- 0 until state.showing.length - newState.showing.length do + moveNextLine(1) + moveHorizontalTo(0) + eraseToEndOfLine() + def printPrompt() = import terminal.* @@ -41,43 +49,47 @@ class InteractiveAlternatives( out.out("\n") val filteredAlts = - prompt.alts.filter( - state.text.isEmpty() || _.toLowerCase().contains( - state.text.toLowerCase() - ) - ) - - val adjustedSelected = - state.selected.min(filteredAlts.length - 1).max(0) - - val newState = - AlternativesState( - state.text, - selected = adjustedSelected, - showing = filteredAlts.length.max(1) - ) + altsWithIndex.filter: (txt, idx) => + state.text.isEmpty() || txt + .toLowerCase() + .contains( + state.text.toLowerCase() + ) if filteredAlts.isEmpty then moveHorizontalTo(0) eraseToEndOfLine() out.out(colored("no matches")(fansi.Underlined.On(_))) + val newState = AlternativesState( + state.text, + selected = None, + showing = Nil + ) + clear(state, newState) + state = newState else - filteredAlts.zipWithIndex.foreach: (alt, idx) => - moveHorizontalTo(0) - eraseToEndOfLine() - val view = - if idx == adjustedSelected then - colored(s"> $alt")(fansi.Color.Green(_)) - else colored(s"· $alt")(fansi.Bold.On(_)) - out.out(view.toString) - if idx != filteredAlts.length - 1 then out.out("\n") - end if + filteredAlts.zipWithIndex.foreach: + case ((alt, originalIdx), idx) => + moveHorizontalTo(0) + eraseToEndOfLine() + val view = + if state.selected.contains(idx) then + colored(s"> $alt")(fansi.Color.Green(_)) + else colored(s"· $alt")(fansi.Bold.On(_)) + out.out(view.toString) + if idx != filteredAlts.length - 1 then out.out("\n") + + val newState = state.copy( + showing = filteredAlts, + selected = + if state.showing == filteredAlts then state.selected + else Some(0) + ) - for _ <- 0 until state.showing - newState.showing do - moveNextLine(1) - moveHorizontalTo(0) - eraseToEndOfLine() - state = newState + clear(state, newState) + state = newState + + end if end printPrompt def handler = new Handler: @@ -96,7 +108,12 @@ class InteractiveAlternatives( Next.Continue case Event.Key(KeyEvent.ENTER) => // enter - Next.Stop + state.selected match + case None => Next.Continue + case Some(value) => + terminal.withRestore: + clear(state, state.copy(showing = Nil)) + Next.Done(state.showing(value)._1) case Event.Key(KeyEvent.DELETE) => // enter trimText() @@ -113,10 +130,11 @@ class InteractiveAlternatives( end match end apply - def selectUp() = state = state.copy(selected = (state.selected - 1).max(0)) + def selectUp() = state = + state.copy(selected = state.selected.map(s => (s - 1).max(0))) def selectDown() = state = - state.copy(selected = (state.selected + 1).min(1000)) + state.copy(selected = state.selected.map(s => (s + 1).min(1000))) def appendText(t: Char) = state = state.copy(text = state.text + t) diff --git a/modules/core/src/main/scala/Next.scala b/modules/core/src/main/scala/Next.scala index 30c4991..d380ff5 100644 --- a/modules/core/src/main/scala/Next.scala +++ b/modules/core/src/main/scala/Next.scala @@ -18,4 +18,5 @@ package com.indoorvivants.proompts enum Next: case Stop, Continue + case Done(value: String) case Error(msg: String) diff --git a/modules/core/src/main/scalajs/InputProviderImpl.scala b/modules/core/src/main/scalajs/InputProviderImpl.scala index ccdc9d0..3ec5e38 100644 --- a/modules/core/src/main/scalajs/InputProviderImpl.scala +++ b/modules/core/src/main/scalajs/InputProviderImpl.scala @@ -55,8 +55,6 @@ private class InputProviderImpl(o: Output) val completion = Promise[Completion] val fut = completion.future - // var completion = Completion.Finished - lazy val keypress: js.Function = (str: js.UndefOr[String], key: Key) => handle(key) @@ -65,13 +63,13 @@ private class InputProviderImpl(o: Output) if stdin.isTTY.contains(true) then stdin.setRawMode(false) rl.close() completion.success(res) - // completion = res def whatNext(n: Next) = n match - case Next.Continue => - case Next.Stop => close(Completion.Interrupted) - case Next.Error(msg) => close(Completion.Error(msg)) + case Next.Continue => + case Next.Done(value) => close(Completion.Finished(value)) + case Next.Stop => close(Completion.Interrupted) + case Next.Error(msg) => close(Completion.Error(msg)) def send(ev: Event) = whatNext(handler(ev)) diff --git a/modules/core/src/main/scalanative/InputProviderImpl.scala b/modules/core/src/main/scalanative/InputProviderImpl.scala index 2f0c7a9..e7e6365 100644 --- a/modules/core/src/main/scalanative/InputProviderImpl.scala +++ b/modules/core/src/main/scalanative/InputProviderImpl.scala @@ -57,9 +57,10 @@ private class InputProviderImpl(o: Output) def whatNext(n: Next) = n match - case Next.Continue => - case Next.Stop => break(Completion.Interrupted) - case Next.Error(msg) => break(Completion.Error(msg)) + case Next.Continue => + case Next.Done(value: String) => break(Completion.Finished(value)) + case Next.Stop => break(Completion.Interrupted) + case Next.Error(msg) => break(Completion.Error(msg)) def send(ev: Event) = whatNext(f.handler(ev)) @@ -80,7 +81,8 @@ private class InputProviderImpl(o: Output) end while - Completion.Finished + Completion.Interrupted + end evaluate override def close() = changemode(0) diff --git a/modules/core/src/test/resources/snapshots/core/alternatives.navigation b/modules/core/src/test/resources/snapshots/core/alternatives_navigation similarity index 100% rename from modules/core/src/test/resources/snapshots/core/alternatives.navigation rename to modules/core/src/test/resources/snapshots/core/alternatives_navigation diff --git a/modules/core/src/test/resources/snapshots/core/alternatives.typing b/modules/core/src/test/resources/snapshots/core/alternatives_typing similarity index 100% rename from modules/core/src/test/resources/snapshots/core/alternatives.typing rename to modules/core/src/test/resources/snapshots/core/alternatives_typing diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives.navigation b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation similarity index 66% rename from modules/core/src/test/resources/snapshots/coreNative/alternatives.navigation rename to modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation index a31c7ba..9586f4e 100644 --- a/modules/core/src/test/resources/snapshots/coreNative/alternatives.navigation +++ b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation @@ -12,3 +12,10 @@ Event.Key(DOWN) ┃> rizza ┃ ┃· flizza ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃· rizza ┃ +┃> flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives.typing b/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing similarity index 100% rename from modules/core/src/test/resources/snapshots/coreJS/alternatives.typing rename to modules/core/src/test/resources/snapshots/coreJS/alternatives_typing diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives.navigation b/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation similarity index 100% rename from modules/core/src/test/resources/snapshots/coreJS/alternatives.navigation rename to modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives.typing b/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing similarity index 100% rename from modules/core/src/test/resources/snapshots/coreNative/alternatives.typing rename to modules/core/src/test/resources/snapshots/coreNative/alternatives_typing diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index 39da5be..553bab6 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -10,7 +10,7 @@ class ExampleTests extends munit.FunSuite, TerminalTests: terminalTest("alternatives.navigation")( prompt, - List(Event.Init, Event.Key(KeyEvent.DOWN)) + List(Event.Init, Event.Key(KeyEvent.DOWN), Event.Key(KeyEvent.DOWN)) ) terminalTest("alternatives.typing")( diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 6c9b8ce..52211a1 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -22,13 +22,15 @@ import concurrent.ExecutionContext.Implicits.global val prompt = Prompt.Alternatives( "How is your day?", - List("Good", "bad", "excellent", "could've been better") + List("Good", "bad", "shexcellent", "could've been better") ) val interactive = Interactive(terminal, prompt, Output.Std, true) val inputProvider = InputProvider(Output.Std) - inputProvider.evaluateFuture(interactive).foreach: value => + inputProvider + .evaluateFuture(interactive) + .foreach: value => println(value) end hello diff --git a/project/plugins.sbt b/project/plugins.sbt index ee98429..15a8dd0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -18,4 +18,4 @@ addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") -addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.4") +addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.5") From 64fe16a7eb4175ad89e6e1ae892f1f329dc03b1f Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 14 Feb 2024 21:33:24 +0000 Subject: [PATCH 19/32] Update snapshots --- .../test/resources/snapshots/core/alternatives_navigation | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/modules/core/src/test/resources/snapshots/core/alternatives_navigation b/modules/core/src/test/resources/snapshots/core/alternatives_navigation index a31c7ba..9586f4e 100644 --- a/modules/core/src/test/resources/snapshots/core/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/core/alternatives_navigation @@ -12,3 +12,10 @@ Event.Key(DOWN) ┃> rizza ┃ ┃· flizza ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃· rizza ┃ +┃> flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ From b5ecb3889b4947a91cda17e463fe956a86d9da7c Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 14 Feb 2024 21:58:00 +0000 Subject: [PATCH 20/32] Correct handling of finished interactive session --- .../main/scala/InteractiveAlternatives.scala | 14 ++++++++++++-- .../src/main/scala/InteractiveTextInput.scala | 2 +- modules/core/src/main/scala/Prompt.scala | 11 ++++++----- .../snapshots/core/alternatives_navigation | 7 +++++++ .../snapshots/core/alternatives_typing | 7 +++++++ .../snapshots/coreJS/alternatives_navigation | 7 +++++++ .../snapshots/coreJS/alternatives_typing | 7 +++++++ .../coreNative/alternatives_navigation | 14 ++++++++++++++ .../snapshots/coreNative/alternatives_typing | 7 +++++++ modules/core/src/test/scala/ExampleTests.scala | 10 ++++++++-- modules/example/src/main/scala/hello.scala | 17 +++++++++++++---- 11 files changed, 89 insertions(+), 14 deletions(-) diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index e3ecbde..ea48362 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -22,7 +22,7 @@ class InteractiveAlternatives( out: Output, colors: Boolean ): - val lab = prompt.promptLabel + val lab = prompt.label + " > " val altsWithIndex = prompt.alts.zipWithIndex var state = AlternativesState("", Some(0), altsWithIndex) @@ -92,6 +92,14 @@ class InteractiveAlternatives( end if end printPrompt + def printFinished(value: String) = + terminal.eraseEntireLine() + terminal.moveBack(lab.length + 2) + out.out(colored("✔ " + lab)(fansi.Color.Cyan(_))) + out.out(colored(value + "\n")(fansi.Bold.On(_))) + + end printFinished + def handler = new Handler: def apply(event: Event): Next = event match @@ -113,7 +121,9 @@ class InteractiveAlternatives( case Some(value) => terminal.withRestore: clear(state, state.copy(showing = Nil)) - Next.Done(state.showing(value)._1) + val stringValue = state.showing(value)._1 + printFinished(stringValue) + Next.Done(stringValue) case Event.Key(KeyEvent.DELETE) => // enter trimText() diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala index ae3aa12..4669473 100644 --- a/modules/core/src/main/scala/InteractiveTextInput.scala +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -22,7 +22,7 @@ class InteractiveTextInput( out: Output, colors: Boolean ): - val lab = prompt.promptLabel + val lab = prompt.label + " > " var state = TextInputState("") def colored(msg: String)(f: String => fansi.Str) = diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 66156a7..3823af6 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -16,9 +16,10 @@ package com.indoorvivants.proompts -enum Prompt(label: String): - case Input(label: String) extends Prompt(label) - case Alternatives(label: String, alts: List[String]) extends Prompt(label) +enum Prompt: + case Input(lab: String) + case Alternatives(lab: String, alts: List[String]) - def promptLabel = - label + " > " + def label = this match + case Input(label) => label + case Alternatives(label, alts) => label diff --git a/modules/core/src/test/resources/snapshots/core/alternatives_navigation b/modules/core/src/test/resources/snapshots/core/alternatives_navigation index 9586f4e..9ae8c21 100644 --- a/modules/core/src/test/resources/snapshots/core/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/core/alternatives_navigation @@ -19,3 +19,10 @@ Event.Key(DOWN) ┃· rizza ┃ ┃> flizza ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/core/alternatives_typing b/modules/core/src/test/resources/snapshots/core/alternatives_typing index cf9b886..1cca425 100644 --- a/modules/core/src/test/resources/snapshots/core/alternatives_typing +++ b/modules/core/src/test/resources/snapshots/core/alternatives_typing @@ -33,3 +33,10 @@ Event.Char('i') ┃ ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation index 9586f4e..9ae8c21 100644 --- a/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation @@ -19,3 +19,10 @@ Event.Key(DOWN) ┃· rizza ┃ ┃> flizza ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing b/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing index cf9b886..1cca425 100644 --- a/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing +++ b/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing @@ -33,3 +33,10 @@ Event.Char('i') ┃ ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation b/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation index a31c7ba..9ae8c21 100644 --- a/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation @@ -12,3 +12,17 @@ Event.Key(DOWN) ┃> rizza ┃ ┃· flizza ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(DOWN) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃How do you do fellow kids? > ┃ +┃· killa ┃ +┃· rizza ┃ +┃> flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing b/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing index cf9b886..1cca425 100644 --- a/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing +++ b/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing @@ -33,3 +33,10 @@ Event.Char('i') ┃ ┃ ┃ ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +Event.Key(ENTER) +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃✔ How do you do fellow kids? > flizza┃ +┃  ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index 553bab6..ec60f82 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -10,7 +10,12 @@ class ExampleTests extends munit.FunSuite, TerminalTests: terminalTest("alternatives.navigation")( prompt, - List(Event.Init, Event.Key(KeyEvent.DOWN), Event.Key(KeyEvent.DOWN)) + List( + Event.Init, + Event.Key(KeyEvent.DOWN), + Event.Key(KeyEvent.DOWN), + Event.Key(KeyEvent.ENTER) + ) ) terminalTest("alternatives.typing")( @@ -20,7 +25,8 @@ class ExampleTests extends munit.FunSuite, TerminalTests: Event.Char('z'), Event.Key(KeyEvent.DELETE), Event.Char('l'), - Event.Char('i') + Event.Char('i'), + Event.Key(KeyEvent.ENTER) ) ) diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 52211a1..58070cb 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -25,12 +25,21 @@ import concurrent.ExecutionContext.Implicits.global List("Good", "bad", "shexcellent", "could've been better") ) - val interactive = Interactive(terminal, prompt, Output.Std, true) + val nextPrompt = Prompt.Alternatives( + "And how was your poop", + List("Strong", "Smelly") + ) + + def interactive(prompt: Prompt) = + Interactive(terminal, prompt, Output.Std, true) val inputProvider = InputProvider(Output.Std) inputProvider - .evaluateFuture(interactive) - .foreach: value => - println(value) + .evaluateFuture(interactive(prompt)) + .collect: + case Completion.Finished(v) => v + .flatMap: v => + inputProvider.evaluateFuture(interactive(nextPrompt)) + end hello From c263a4ebbec61cf7c972603728ad40692ca60824 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Wed, 14 Feb 2024 22:03:37 +0000 Subject: [PATCH 21/32] Better rendering --- modules/core/src/main/scala/InteractiveAlternatives.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index ea48362..501b9f2 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -43,6 +43,7 @@ class InteractiveAlternatives( moveHorizontalTo(0) eraseToEndOfLine() + out.out("· ") out.out(colored(lab + state.text)(fansi.Color.Cyan(_))) withRestore: @@ -74,8 +75,8 @@ class InteractiveAlternatives( eraseToEndOfLine() val view = if state.selected.contains(idx) then - colored(s"> $alt")(fansi.Color.Green(_)) - else colored(s"· $alt")(fansi.Bold.On(_)) + colored(s"‣ $alt")(fansi.Color.Green(_)) + else colored(s" $alt")(fansi.Bold.On(_)) out.out(view.toString) if idx != filteredAlts.length - 1 then out.out("\n") @@ -95,7 +96,8 @@ class InteractiveAlternatives( def printFinished(value: String) = terminal.eraseEntireLine() terminal.moveBack(lab.length + 2) - out.out(colored("✔ " + lab)(fansi.Color.Cyan(_))) + out.out(colored("✔ ")(fansi.Color.Green(_))) + out.out(colored(lab)(fansi.Color.Cyan(_))) out.out(colored(value + "\n")(fansi.Bold.On(_))) end printFinished From ee32fe95b0bfdaccd470507ac11791f2abe0dc0c Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 15 Feb 2024 07:48:37 +0000 Subject: [PATCH 22/32] Rendering improvements --- modules/core/src/main/scala/InteractiveAlternatives.scala | 6 +++--- modules/example/src/main/scala/hello.scala | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 501b9f2..1b9de8f 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -60,7 +60,7 @@ class InteractiveAlternatives( if filteredAlts.isEmpty then moveHorizontalTo(0) eraseToEndOfLine() - out.out(colored("no matches")(fansi.Underlined.On(_))) + out.out(colored(" no matches")(fansi.Underlined.On(_))) val newState = AlternativesState( state.text, selected = None, @@ -75,8 +75,8 @@ class InteractiveAlternatives( eraseToEndOfLine() val view = if state.selected.contains(idx) then - colored(s"‣ $alt")(fansi.Color.Green(_)) - else colored(s" $alt")(fansi.Bold.On(_)) + colored(s" ‣ $alt")(fansi.Color.Green(_)) + else colored(s" $alt")(fansi.Bold.On(_)) out.out(view.toString) if idx != filteredAlts.length - 1 then out.out("\n") diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 58070cb..bc0a5c2 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -22,11 +22,11 @@ import concurrent.ExecutionContext.Implicits.global val prompt = Prompt.Alternatives( "How is your day?", - List("Good", "bad", "shexcellent", "could've been better") + List("great", "okay", "shite") ) - val nextPrompt = Prompt.Alternatives( - "And how was your poop", + def nextPrompt(day: String) = Prompt.Alternatives( + s"So your day has been ${day}. And how was your poop", List("Strong", "Smelly") ) @@ -40,6 +40,6 @@ import concurrent.ExecutionContext.Implicits.global .collect: case Completion.Finished(v) => v .flatMap: v => - inputProvider.evaluateFuture(interactive(nextPrompt)) + inputProvider.evaluateFuture(interactive(nextPrompt(v))) end hello From 862625339b45d8cb18f1dc4843f9942fa01091bc Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Thu, 15 Feb 2024 09:06:40 +0000 Subject: [PATCH 23/32] Introduce prompt chain --- modules/core/src/main/scala/PromptChain.scala | 79 +++++++++++++++++++ modules/example/src/main/scala/hello.scala | 68 +++++++++++----- 2 files changed, 129 insertions(+), 18 deletions(-) create mode 100644 modules/core/src/main/scala/PromptChain.scala diff --git a/modules/core/src/main/scala/PromptChain.scala b/modules/core/src/main/scala/PromptChain.scala new file mode 100644 index 0000000..0f645b3 --- /dev/null +++ b/modules/core/src/main/scala/PromptChain.scala @@ -0,0 +1,79 @@ +package com.indoorvivants.proompts + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext + +type OrError[A] = String | A + +case class PromptChainFuture[A]( + terminal: Terminal, + out: Output, + colors: Boolean, + start: (Prompt, String => Future[A]), + reversedChain: List[(String => Future[Prompt], (A, String) => Future[A])] +): + + def evaluateFuture(using ExecutionContext): Future[A] = + val (startPrompt, startTransform) = start + val chain = reversedChain.reverse + + eval(startPrompt): startResult => + val init = startTransform(startResult) + + chain.foldLeft(init): + case (acc, (nextPrompt, nextValueTransform)) => + acc.flatMap: a => + nextPrompt(startResult).flatMap: prompt => + eval(prompt): nextResult => + nextValueTransform(a, nextResult) + + end evaluateFuture + + def andThen( + nextPrompt: String => Future[Prompt], + updateValue: (A, String) => Future[A] + ) = + copy(reversedChain = (nextPrompt, updateValue) :: reversedChain) + + private def fail(msg: String) = Future.failed(new RuntimeException(msg)) + private def check[T](c: Future[Completion])(v: String => Future[T])(using + ExecutionContext + ): Future[T] = + c.flatMap(check(_)(v)) + + private def eval[T](p: Prompt)(v: String => Future[T])(using + ExecutionContext + ): Future[T] = check( + ip.evaluateFuture(interactive(p)) + )(v) + + private def check[T](c: Completion)(v: String => Future[T]): Future[T] = + c match + case Completion.Interrupted => + fail("interrupted") + case Completion.Error(msg) => + fail(msg) + case Completion.Finished(value) => + v(value) + + private def ip = InputProvider(out) + private def interactive(prompt: Prompt) = + Interactive(terminal, prompt, out, colors) +end PromptChainFuture + +object PromptChain: + def future[A]( + start: Prompt, + createValue: String => Future[A], + terminal: Terminal, + out: Output, + colors: Boolean + ) = + new PromptChainFuture[A]( + terminal, + out, + colors, + start = (start, createValue), + reversedChain = Nil + ) +end PromptChain diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index bc0a5c2..538e618 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -16,30 +16,62 @@ package com.indoorvivants.proompts import concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future @main def hello = - val terminal = Terminal.ansi(Output.Std) + val out = Output.Std + val terminal = Terminal.ansi(out) + val colors = true - val prompt = Prompt.Alternatives( - "How is your day?", - List("great", "okay", "shite") - ) + PromptChain + .future( + Prompt.Alternatives( + "How is your day?", + List("great", "okay", "shite") + ), + s => Future.successful(List(s)), + terminal, + out, + colors + ) + .andThen( + day => + Future.successful( + Prompt.Alternatives( + s"So your day has been ${day}. And how was your poop", + List("Strong", "Smelly") + ) + ), + (cur, poop) => Future.successful(poop :: cur) + ) + .andThen( + poop => + Future.successful( + Prompt.Alternatives( + s"I see... whatcha wanna do", + List("Partay", "sleep") + ) + ), + (cur, doing) => Future.successful(doing :: cur) + ) + .evaluateFuture.foreach: results => + println(results) - def nextPrompt(day: String) = Prompt.Alternatives( - s"So your day has been ${day}. And how was your poop", - List("Strong", "Smelly") - ) + // def nextPrompt(day: String) = Prompt.Alternatives( + // s"So your day has been ${day}. And how was your poop", + // List("Strong", "Smelly") + // ) - def interactive(prompt: Prompt) = - Interactive(terminal, prompt, Output.Std, true) + // def interactive(prompt: Prompt) = + // Interactive(terminal, prompt, Output.Std, true) - val inputProvider = InputProvider(Output.Std) + // val inputProvider = InputProvider(Output.Std) - inputProvider - .evaluateFuture(interactive(prompt)) - .collect: - case Completion.Finished(v) => v - .flatMap: v => - inputProvider.evaluateFuture(interactive(nextPrompt(v))) + // inputProvider + // .evaluateFuture(interactive(prompt)) + // .collect: + // case Completion.Finished(v) => v + // .flatMap: v => + // inputProvider.evaluateFuture(interactive(nextPrompt(v))) end hello From dde28e5ab89163acb573df74b1f653a156e191e2 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 16 Feb 2024 08:26:38 +0000 Subject: [PATCH 24/32] Simpler prompt chains --- modules/core/src/main/scala/PromptChain.scala | 25 +++++++---- .../core/src/main/scala/PromptChains.scala | 21 +++++++++ modules/example/src/main/scala/hello.scala | 43 ++++--------------- 3 files changed, 46 insertions(+), 43 deletions(-) create mode 100644 modules/core/src/main/scala/PromptChains.scala diff --git a/modules/core/src/main/scala/PromptChain.scala b/modules/core/src/main/scala/PromptChain.scala index 0f645b3..19184c1 100644 --- a/modules/core/src/main/scala/PromptChain.scala +++ b/modules/core/src/main/scala/PromptChain.scala @@ -9,29 +9,30 @@ case class PromptChainFuture[A]( terminal: Terminal, out: Output, colors: Boolean, - start: (Prompt, String => Future[A]), - reversedChain: List[(String => Future[Prompt], (A, String) => Future[A])] + start: (Prompt, String => A | Future[A]), + reversedChain: List[ + (String => Prompt | Future[Prompt], (A, String) => A | Future[A]) + ] ): - def evaluateFuture(using ExecutionContext): Future[A] = val (startPrompt, startTransform) = start val chain = reversedChain.reverse eval(startPrompt): startResult => - val init = startTransform(startResult) + val init = lift(startTransform)(startResult) chain.foldLeft(init): case (acc, (nextPrompt, nextValueTransform)) => acc.flatMap: a => - nextPrompt(startResult).flatMap: prompt => + lift(nextPrompt)(startResult).flatMap: prompt => eval(prompt): nextResult => - nextValueTransform(a, nextResult) + lift(nextValueTransform.tupled)(a, nextResult) end evaluateFuture def andThen( - nextPrompt: String => Future[Prompt], - updateValue: (A, String) => Future[A] + nextPrompt: String => Prompt | Future[Prompt], + updateValue: (A, String) => A | Future[A] ) = copy(reversedChain = (nextPrompt, updateValue) :: reversedChain) @@ -41,6 +42,12 @@ case class PromptChainFuture[A]( ): Future[T] = c.flatMap(check(_)(v)) + private def lift[A, B](f: A => B | Future[B]): A => Future[B] = + a => + f(a) match + case f: Future[?] => f.asInstanceOf[Future[B]] + case other => Future.successful(other.asInstanceOf[B]) + private def eval[T](p: Prompt)(v: String => Future[T])(using ExecutionContext ): Future[T] = check( @@ -64,7 +71,7 @@ end PromptChainFuture object PromptChain: def future[A]( start: Prompt, - createValue: String => Future[A], + createValue: String => A | Future[A], terminal: Terminal, out: Output, colors: Boolean diff --git a/modules/core/src/main/scala/PromptChains.scala b/modules/core/src/main/scala/PromptChains.scala new file mode 100644 index 0000000..470c64a --- /dev/null +++ b/modules/core/src/main/scala/PromptChains.scala @@ -0,0 +1,21 @@ +package com.indoorvivants.proompts + +import scala.concurrent.Future + +object PromptChains: + def future[A]( + startPrompt: Prompt, + createValue: String => A | Future[A] + ): PromptChainFuture[A] = + val out = Output.Std + val terminal = Terminal.ansi(out) + val colors = true + PromptChain.future( + start = startPrompt, + createValue = createValue, + terminal = terminal, + out = out, + colors = colors + ) + end future +end PromptChains diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 538e618..0edb419 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -19,30 +19,21 @@ import concurrent.ExecutionContext.Implicits.global import scala.concurrent.Future @main def hello = - val out = Output.Std - val terminal = Terminal.ansi(out) - val colors = true - - PromptChain + PromptChains .future( Prompt.Alternatives( "How is your day?", List("great", "okay", "shite") ), - s => Future.successful(List(s)), - terminal, - out, - colors + s => List(s) ) .andThen( day => - Future.successful( - Prompt.Alternatives( - s"So your day has been ${day}. And how was your poop", - List("Strong", "Smelly") - ) + Prompt.Alternatives( + s"So your day has been ${day}. And how was your poop", + List("Strong", "Smelly") ), - (cur, poop) => Future.successful(poop :: cur) + (cur, poop) => poop :: cur ) .andThen( poop => @@ -52,26 +43,10 @@ import scala.concurrent.Future List("Partay", "sleep") ) ), - (cur, doing) => Future.successful(doing :: cur) + (cur, doing) => doing :: cur ) - .evaluateFuture.foreach: results => + .evaluateFuture + .foreach: results => println(results) - // def nextPrompt(day: String) = Prompt.Alternatives( - // s"So your day has been ${day}. And how was your poop", - // List("Strong", "Smelly") - // ) - - // def interactive(prompt: Prompt) = - // Interactive(terminal, prompt, Output.Std, true) - - // val inputProvider = InputProvider(Output.Std) - - // inputProvider - // .evaluateFuture(interactive(prompt)) - // .collect: - // case Completion.Finished(v) => v - // .flatMap: v => - // inputProvider.evaluateFuture(interactive(nextPrompt(v))) - end hello From 9919f72fa0da8206ebf80bc56bfe94446d286c47 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 16 Feb 2024 13:52:13 +0000 Subject: [PATCH 25/32] Reorg --- .../src/main/scala/AlternativesState.scala | 2 +- .../core/src/main/scala/AnsiTerminal.scala | 2 +- .../core/src/main/scala/CharCollector.scala | 23 +++-- modules/core/src/main/scala/Completion.scala | 6 +- modules/core/src/main/scala/Event.scala | 2 +- .../core/src/main/scala/InputProvider.scala | 8 +- modules/core/src/main/scala/Interactive.scala | 30 +++--- .../main/scala/InteractiveAlternatives.scala | 18 ++-- .../src/main/scala/InteractiveTextInput.scala | 13 +-- modules/core/src/main/scala/Next.scala | 6 +- modules/core/src/main/scala/Output.scala | 2 +- modules/core/src/main/scala/Prompt.scala | 44 +++++++-- modules/core/src/main/scala/PromptChain.scala | 91 +++---------------- .../src/main/scala/PromptChainFuture.scala | 65 +++++++++++++ .../core/src/main/scala/PromptChains.scala | 21 ----- modules/core/src/main/scala/Terminal.scala | 2 +- .../core/src/main/scala/TextInputState.scala | 2 +- .../core/src/main/scala/TracingTerminal.scala | 2 +- .../InputProviderCompanionPlatform.scala | 8 +- .../src/main/scalajs/InputProviderImpl.scala | 29 +++--- .../src/main/scalajs/NodeJSBindings.scala | 2 +- .../core/src/main/scalajs/PlatformStd.scala | 2 +- .../PromptChainCompanionPlatform.scala | 3 + .../main/scalajvm/InputProviderPlatform.scala | 2 +- .../core/src/main/scalajvm/PlatformStd.scala | 2 +- .../main/scalanative/InputProviderImpl.scala | 34 +++---- .../scalanative/InputProviderPlatform.scala | 6 +- .../src/main/scalanative/PlatformStd.scala | 2 +- .../snapshots/coreJS/alternatives_navigation | 36 ++++---- .../snapshots/coreJS/alternatives_typing | 60 ++++++------ .../coreNative/alternatives_navigation | 36 ++++---- .../snapshots/coreNative/alternatives_typing | 60 ++++++------ .../core/src/test/scala/ExampleTests.scala | 4 +- .../core/src/test/scala/TerminalTests.scala | 14 +-- modules/example/src/main/scala/hello.scala | 51 ++++++----- 35 files changed, 356 insertions(+), 334 deletions(-) create mode 100644 modules/core/src/main/scala/PromptChainFuture.scala delete mode 100644 modules/core/src/main/scala/PromptChains.scala create mode 100644 modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala diff --git a/modules/core/src/main/scala/AlternativesState.scala b/modules/core/src/main/scala/AlternativesState.scala index 697f402..698c3e8 100644 --- a/modules/core/src/main/scala/AlternativesState.scala +++ b/modules/core/src/main/scala/AlternativesState.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts case class AlternativesState( text: String, diff --git a/modules/core/src/main/scala/AnsiTerminal.scala b/modules/core/src/main/scala/AnsiTerminal.scala index a848d4b..7def32e 100644 --- a/modules/core/src/main/scala/AnsiTerminal.scala +++ b/modules/core/src/main/scala/AnsiTerminal.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts class AnsiTerminal(out: Output) extends Terminal: import AnsiTerminal.{ESC, CSI} diff --git a/modules/core/src/main/scala/CharCollector.scala b/modules/core/src/main/scala/CharCollector.scala index 91facbf..9a62cfc 100644 --- a/modules/core/src/main/scala/CharCollector.scala +++ b/modules/core/src/main/scala/CharCollector.scala @@ -14,14 +14,23 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts object CharCollector: enum State: case Init, ESC_Started, CSI_Started case CSI_Collecting(bytes: List[Byte]) - def decode(curState: State, char: Int): (State, Next | Event) = + enum DecodeResult: + case Continue + case Error(msg: String) + + def toNext[R]: Next[R] = this match + case Continue => Next.Continue + case Error(msg) => Next.Error(msg) + + + def decode(curState: State, char: Int): (State, DecodeResult | Event) = def isCSIParameterByte(b: Int) = (b >= 0x30 && b <= 0x3f) @@ -32,19 +41,19 @@ object CharCollector: (b >= 0x40 && b <= 0x7e) def error(msg: String) = - (curState, Next.Error(msg)) + (curState, DecodeResult.Error(msg)) def emit(event: Event) = (curState, event) - def toInit(result: Next | Event) = + def toInit(result: DecodeResult | Event) = (State.Init, result) curState match case State.Init => char match case AnsiTerminal.ESC => - (State.ESC_Started, Next.Continue) + (State.ESC_Started, DecodeResult.Continue) case 10 | 13 => emit(Event.Key(KeyEvent.ENTER)) case 127 => @@ -55,7 +64,7 @@ object CharCollector: case State.ESC_Started => char match case '[' => - (State.CSI_Started, Next.Continue) + (State.CSI_Started, DecodeResult.Continue) case _ => error(s"Unexpected symbol ${char} following an ESC sequence") @@ -70,7 +79,7 @@ object CharCollector: if isCSIParameterByte(b) || isCSIIntermediateByte( b ) => - (State.CSI_Collecting(b.toByte :: Nil), Next.Continue) + (State.CSI_Collecting(b.toByte :: Nil), DecodeResult.Continue) case State.CSI_Collecting(bytes) => char match diff --git a/modules/core/src/main/scala/Completion.scala b/modules/core/src/main/scala/Completion.scala index 209ffaf..b02338d 100644 --- a/modules/core/src/main/scala/Completion.scala +++ b/modules/core/src/main/scala/Completion.scala @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts -enum Completion: - case Finished(value: String) +enum Completion[+Result]: + case Finished(value: Result) case Interrupted case Error(msg: String) diff --git a/modules/core/src/main/scala/Event.scala b/modules/core/src/main/scala/Event.scala index 4cec6cc..0958477 100644 --- a/modules/core/src/main/scala/Event.scala +++ b/modules/core/src/main/scala/Event.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts enum Event: case Init diff --git a/modules/core/src/main/scala/InputProvider.scala b/modules/core/src/main/scala/InputProvider.scala index d0daf0f..0ff0714 100644 --- a/modules/core/src/main/scala/InputProvider.scala +++ b/modules/core/src/main/scala/InputProvider.scala @@ -14,12 +14,10 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts -// case class Environment(writer: String => Unit) - -abstract class Handler: - def apply(ev: Event): Next +abstract class Handler[Result]: + def apply(ev: Event): Next[Result] abstract class InputProvider(protected val output: Output) extends AutoCloseable, diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala index b3effa8..ca7b414 100644 --- a/modules/core/src/main/scala/Interactive.scala +++ b/modules/core/src/main/scala/Interactive.scala @@ -14,21 +14,23 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts def errln(o: Any) = System.err.println(o) -class Interactive( - terminal: Terminal, - prompt: Prompt, - out: Output, - colors: Boolean -): - val handler = - prompt match - case p: Prompt.Input => - InteractiveTextInput(p, terminal, out, colors).handler - case p: Prompt.Alternatives => - InteractiveAlternatives(terminal, p, out, colors).handler +// class Interactive[Result]( +// terminal: Terminal, +// prompt: Prompt[Result], +// out: Output, +// colors: Boolean +// ): +// val handler: Handler[Result] = +// prompt match +// case p: Prompt.Input => +// // TODO - reorg the codebase so this instanceOf is not required +// InteractiveTextInput(p, terminal, out, colors).handler.asInstanceOf +// case p: Prompt.Alternatives => +// // TODO - reorg the codebase so this instanceOf is not required +// InteractiveAlternatives(terminal, p, out, colors).handler.asInstanceOf -end Interactive +// end Interactive diff --git a/modules/core/src/main/scala/InteractiveAlternatives.scala b/modules/core/src/main/scala/InteractiveAlternatives.scala index 1b9de8f..53bc15c 100644 --- a/modules/core/src/main/scala/InteractiveAlternatives.scala +++ b/modules/core/src/main/scala/InteractiveAlternatives.scala @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts class InteractiveAlternatives( + prompt: AlternativesPrompt, terminal: Terminal, - prompt: Prompt.Alternatives, out: Output, colors: Boolean ): - val lab = prompt.label + " > " + val lab = prompt.lab + " > " val altsWithIndex = prompt.alts.zipWithIndex var state = AlternativesState("", Some(0), altsWithIndex) @@ -33,7 +33,7 @@ class InteractiveAlternatives( import terminal.* for _ <- 0 until state.showing.length - newState.showing.length do moveNextLine(1) - moveHorizontalTo(0) + moveHorizontalTo(1) eraseToEndOfLine() def printPrompt() = @@ -41,7 +41,7 @@ class InteractiveAlternatives( import terminal.* moveHorizontalTo(0) - eraseToEndOfLine() + eraseEntireLine() out.out("· ") out.out(colored(lab + state.text)(fansi.Color.Cyan(_))) @@ -50,7 +50,7 @@ class InteractiveAlternatives( out.out("\n") val filteredAlts = - altsWithIndex.filter: (txt, idx) => + altsWithIndex.filter: (txt, _) => state.text.isEmpty() || txt .toLowerCase() .contains( @@ -95,15 +95,15 @@ class InteractiveAlternatives( def printFinished(value: String) = terminal.eraseEntireLine() - terminal.moveBack(lab.length + 2) + terminal.moveHorizontalTo(0) out.out(colored("✔ ")(fansi.Color.Green(_))) out.out(colored(lab)(fansi.Color.Cyan(_))) out.out(colored(value + "\n")(fansi.Bold.On(_))) end printFinished - def handler = new Handler: - def apply(event: Event): Next = + val handler = new Handler[String]: + def apply(event: Event): Next[String] = event match case Event.Init => printPrompt() diff --git a/modules/core/src/main/scala/InteractiveTextInput.scala b/modules/core/src/main/scala/InteractiveTextInput.scala index 4669473..ad00e80 100644 --- a/modules/core/src/main/scala/InteractiveTextInput.scala +++ b/modules/core/src/main/scala/InteractiveTextInput.scala @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts class InteractiveTextInput( - prompt: Prompt.Input, + prompt: InputPrompt, terminal: Terminal, out: Output, colors: Boolean ): - val lab = prompt.label + " > " + val lab = prompt.lab + " > " var state = TextInputState("") def colored(msg: String)(f: String => fansi.Str) = @@ -35,14 +35,11 @@ class InteractiveTextInput( moveHorizontalTo(0) eraseToEndOfLine() - errln(prompt) - out.out(colored(lab + state.text)(fansi.Color.Cyan(_))) end printPrompt - val handler = new Handler: - def apply(event: Event): Next = - errln(event) + val handler = new Handler[String]: + def apply(event: Event): Next[String] = event match case Event.Init => printPrompt() diff --git a/modules/core/src/main/scala/Next.scala b/modules/core/src/main/scala/Next.scala index d380ff5..3319ea0 100644 --- a/modules/core/src/main/scala/Next.scala +++ b/modules/core/src/main/scala/Next.scala @@ -14,9 +14,9 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts -enum Next: +enum Next[+Result]: case Stop, Continue - case Done(value: String) + case Done(value: Result) case Error(msg: String) diff --git a/modules/core/src/main/scala/Output.scala b/modules/core/src/main/scala/Output.scala index 0d00aea..3308a87 100644 --- a/modules/core/src/main/scala/Output.scala +++ b/modules/core/src/main/scala/Output.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait Output: def out[A: AsString](a: A): Unit diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 3823af6..0f35047 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -14,12 +14,42 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts -enum Prompt: - case Input(lab: String) - case Alternatives(lab: String, alts: List[String]) +trait Prompt[Result]: + def handler( + terminal: Terminal, + output: Output, + colors: Boolean + ): Handler[Result] - def label = this match - case Input(label) => label - case Alternatives(label, alts) => label +case class InputPrompt(lab: String) extends Prompt[String]: + override def handler( + terminal: Terminal, + output: Output, + colors: Boolean + ): Handler[String] = + val inp = InteractiveTextInput(this, terminal, output, colors) + + inp.handler +end InputPrompt + +case class AlternativesPrompt(lab: String, alts: List[String]) + extends Prompt[String]: + override def handler( + terminal: Terminal, + output: Output, + colors: Boolean + ): Handler[String] = + val inp = InteractiveAlternatives(this, terminal, output, colors) + + inp.handler +end AlternativesPrompt + +// enum Prompt[Result]: +// case Input(lab: String) extends Prompt[String] +// case Alternatives(lab: String, alts: List[String]) extends Prompt[String] + +// def label = this match +// case Input(label) => label +// case Alternatives(label, alts) => label diff --git a/modules/core/src/main/scala/PromptChain.scala b/modules/core/src/main/scala/PromptChain.scala index 19184c1..eddd0eb 100644 --- a/modules/core/src/main/scala/PromptChain.scala +++ b/modules/core/src/main/scala/PromptChain.scala @@ -1,86 +1,17 @@ -package com.indoorvivants.proompts - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext - -type OrError[A] = String | A - -case class PromptChainFuture[A]( - terminal: Terminal, - out: Output, - colors: Boolean, - start: (Prompt, String => A | Future[A]), - reversedChain: List[ - (String => Prompt | Future[Prompt], (A, String) => A | Future[A]) - ] -): - def evaluateFuture(using ExecutionContext): Future[A] = - val (startPrompt, startTransform) = start - val chain = reversedChain.reverse - - eval(startPrompt): startResult => - val init = lift(startTransform)(startResult) - - chain.foldLeft(init): - case (acc, (nextPrompt, nextValueTransform)) => - acc.flatMap: a => - lift(nextPrompt)(startResult).flatMap: prompt => - eval(prompt): nextResult => - lift(nextValueTransform.tupled)(a, nextResult) - - end evaluateFuture - - def andThen( - nextPrompt: String => Prompt | Future[Prompt], - updateValue: (A, String) => A | Future[A] - ) = - copy(reversedChain = (nextPrompt, updateValue) :: reversedChain) - - private def fail(msg: String) = Future.failed(new RuntimeException(msg)) - private def check[T](c: Future[Completion])(v: String => Future[T])(using - ExecutionContext - ): Future[T] = - c.flatMap(check(_)(v)) - - private def lift[A, B](f: A => B | Future[B]): A => Future[B] = - a => - f(a) match - case f: Future[?] => f.asInstanceOf[Future[B]] - case other => Future.successful(other.asInstanceOf[B]) - - private def eval[T](p: Prompt)(v: String => Future[T])(using - ExecutionContext - ): Future[T] = check( - ip.evaluateFuture(interactive(p)) - )(v) - - private def check[T](c: Completion)(v: String => Future[T]): Future[T] = - c match - case Completion.Interrupted => - fail("interrupted") - case Completion.Error(msg) => - fail(msg) - case Completion.Finished(value) => - v(value) - - private def ip = InputProvider(out) - private def interactive(prompt: Prompt) = - Interactive(terminal, prompt, out, colors) -end PromptChainFuture +package proompts object PromptChain: def future[A]( - start: Prompt, - createValue: String => A | Future[A], - terminal: Terminal, - out: Output, - colors: Boolean - ) = + init: A, + terminal: Terminal = Terminal.ansi(Output.Std), + out: Output = Output.Std, + colors: Boolean = true + ): PromptChainFuture[A] = new PromptChainFuture[A]( - terminal, - out, - colors, - start = (start, createValue), - reversedChain = Nil + init = init, + terminal = terminal, + out = out, + colors = colors, + reversedSteps = Nil ) end PromptChain diff --git a/modules/core/src/main/scala/PromptChainFuture.scala b/modules/core/src/main/scala/PromptChainFuture.scala new file mode 100644 index 0000000..1e34bb5 --- /dev/null +++ b/modules/core/src/main/scala/PromptChainFuture.scala @@ -0,0 +1,65 @@ +package proompts + +import scala.concurrent.Future +import scala.concurrent.ExecutionContext + +private[proompts] case class PromptChainFuture[A] private[proompts] ( + init: A, + terminal: Terminal, + out: Output, + colors: Boolean, + reversedSteps: List[ + ExecutionContext ?=> A => Future[A] + ] +): + def prompt[R]( + nextPrompt: A => Prompt[R] | Future[Prompt[R]], + updateValue: (A, R) => A | Future[A] + ) = + val step = (ec: ExecutionContext) ?=> + (a: A) => + lift(nextPrompt)(a).flatMap: prompt => + eval(prompt): nextResult => + lift(updateValue.tupled)(a, nextResult) + + copy(reversedSteps = step :: reversedSteps) + end prompt + + def evaluateFuture(using ExecutionContext): Future[A] = + reversedSteps.reverse.foldLeft(Future.successful(init)): + case (acc, step) => + acc.flatMap(step) + + end evaluateFuture + + private def fail(msg: String) = Future.failed(new RuntimeException(msg)) + private def check[T, R](c: Future[Completion[R]])(v: R => Future[T])(using + ExecutionContext + ): Future[T] = + c.flatMap(check(_)(v)) + + private def lift[A, B](f: A => B | Future[B]): A => Future[B] = + a => + f(a) match + case f: Future[?] => f.asInstanceOf[Future[B]] + case other => Future.successful(other.asInstanceOf[B]) + + private def eval[T, R](p: Prompt[R])(v: R => Future[T])(using + ExecutionContext + ): Future[T] = check( + ip.evaluateFuture(handler(p)) + )(v) + + private def check[T, R](c: Completion[R])(v: R => Future[T]): Future[T] = + c match + case Completion.Interrupted => + fail("interrupted") + case Completion.Error(msg) => + fail(msg) + case Completion.Finished(value) => + v(value) + + private def ip = InputProvider(out) + private def handler[R](prompt: Prompt[R]) = + prompt.handler(terminal, out, colors) +end PromptChainFuture diff --git a/modules/core/src/main/scala/PromptChains.scala b/modules/core/src/main/scala/PromptChains.scala deleted file mode 100644 index 470c64a..0000000 --- a/modules/core/src/main/scala/PromptChains.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.indoorvivants.proompts - -import scala.concurrent.Future - -object PromptChains: - def future[A]( - startPrompt: Prompt, - createValue: String => A | Future[A] - ): PromptChainFuture[A] = - val out = Output.Std - val terminal = Terminal.ansi(out) - val colors = true - PromptChain.future( - start = startPrompt, - createValue = createValue, - terminal = terminal, - out = out, - colors = colors - ) - end future -end PromptChains diff --git a/modules/core/src/main/scala/Terminal.scala b/modules/core/src/main/scala/Terminal.scala index af141a2..1aab718 100644 --- a/modules/core/src/main/scala/Terminal.scala +++ b/modules/core/src/main/scala/Terminal.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait Terminal: self => diff --git a/modules/core/src/main/scala/TextInputState.scala b/modules/core/src/main/scala/TextInputState.scala index c098745..f6c6655 100644 --- a/modules/core/src/main/scala/TextInputState.scala +++ b/modules/core/src/main/scala/TextInputState.scala @@ -14,6 +14,6 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts case class TextInputState(text: String) diff --git a/modules/core/src/main/scala/TracingTerminal.scala b/modules/core/src/main/scala/TracingTerminal.scala index 1125451..f2f7cfb 100644 --- a/modules/core/src/main/scala/TracingTerminal.scala +++ b/modules/core/src/main/scala/TracingTerminal.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.collection.mutable diff --git a/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala b/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala index e1625d1..3fbd56e 100644 --- a/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala +++ b/modules/core/src/main/scalajs/InputProviderCompanionPlatform.scala @@ -14,16 +14,16 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.concurrent.Future trait InputProviderPlatform: self: InputProvider => - def evaluateFuture( - f: Interactive - ): Future[Completion] + def evaluateFuture[Result]( + f: Handler[Result] + ): Future[Completion[Result]] trait InputProviderCompanionPlatform: def apply(o: Output): InputProvider = InputProviderImpl(o) diff --git a/modules/core/src/main/scalajs/InputProviderImpl.scala b/modules/core/src/main/scalajs/InputProviderImpl.scala index 3ec5e38..44b9838 100644 --- a/modules/core/src/main/scalajs/InputProviderImpl.scala +++ b/modules/core/src/main/scalajs/InputProviderImpl.scala @@ -14,24 +14,22 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.concurrent.Future import scala.concurrent.Promise -import scala.scalajs.js.annotation.JSGlobal -import scala.scalajs.js.annotation.JSImport -import com.indoorvivants.proompts.CharCollector.State -import com.indoorvivants.proompts.CharCollector.decode +import proompts.CharCollector.State +import proompts.CharCollector.decode import scalajs.js private class InputProviderImpl(o: Output) extends InputProvider(o), InputProviderPlatform: - override def evaluateFuture( - f: Interactive - ): Future[Completion] = + override def evaluateFuture[Result]( + handler: Handler[Result] + ): Future[Completion[Result]] = val stdin = Process.stdin @@ -39,8 +37,6 @@ private class InputProviderImpl(o: Output) stdin.setRawMode(true) - val handler = f.handler - val rl = Readline.createInterface( js.Dynamic.literal( input = stdin, @@ -52,19 +48,19 @@ private class InputProviderImpl(o: Output) var state = State.Init - val completion = Promise[Completion] + val completion = Promise[Completion[Result]] val fut = completion.future lazy val keypress: js.Function = (str: js.UndefOr[String], key: Key) => handle(key) - def close(res: Completion) = + def close(res: Completion[Result]) = stdin.removeListener("keypress", keypress) if stdin.isTTY.contains(true) then stdin.setRawMode(false) rl.close() completion.success(res) - def whatNext(n: Next) = + def whatNext(n: Next[Result]) = n match case Next.Continue => case Next.Done(value) => close(Completion.Finished(value)) @@ -88,7 +84,12 @@ private class InputProviderImpl(o: Output) state = newState result match - case n: Next => whatNext(n) + case d: CharCollector.DecodeResult => + import CharCollector.DecodeResult.* + d match + case Continue => whatNext(Next.Continue) + case Error(msg) => whatNext(Next.Error(msg)) + case e: Event => send(e) handler(Event.Init) diff --git a/modules/core/src/main/scalajs/NodeJSBindings.scala b/modules/core/src/main/scalajs/NodeJSBindings.scala index 6a922d4..11d6d1a 100644 --- a/modules/core/src/main/scalajs/NodeJSBindings.scala +++ b/modules/core/src/main/scalajs/NodeJSBindings.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.scalajs.js.annotation.JSGlobal import scala.scalajs.js.annotation.JSImport diff --git a/modules/core/src/main/scalajs/PlatformStd.scala b/modules/core/src/main/scalajs/PlatformStd.scala index e4564b0..b5a6ad9 100644 --- a/modules/core/src/main/scalajs/PlatformStd.scala +++ b/modules/core/src/main/scalajs/PlatformStd.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait PlatformStd extends Output: override def logLn[A: AsString](a: A): Unit = diff --git a/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala b/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala new file mode 100644 index 0000000..13fd190 --- /dev/null +++ b/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala @@ -0,0 +1,3 @@ +package proompts + +trait PromptChainCompanionPlatform diff --git a/modules/core/src/main/scalajvm/InputProviderPlatform.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala index b247e28..cc6ea17 100644 --- a/modules/core/src/main/scalajvm/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajvm/InputProviderPlatform.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait InputProviderPlatform: self: InputProvider => diff --git a/modules/core/src/main/scalajvm/PlatformStd.scala b/modules/core/src/main/scalajvm/PlatformStd.scala index 747e255..3155346 100644 --- a/modules/core/src/main/scalajvm/PlatformStd.scala +++ b/modules/core/src/main/scalajvm/PlatformStd.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait PlatformStd extends Output: override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) diff --git a/modules/core/src/main/scalanative/InputProviderImpl.scala b/modules/core/src/main/scalanative/InputProviderImpl.scala index e7e6365..d19162a 100644 --- a/modules/core/src/main/scalanative/InputProviderImpl.scala +++ b/modules/core/src/main/scalanative/InputProviderImpl.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.concurrent.Future import scala.util.boundary @@ -25,11 +25,11 @@ import scalanative.posix.termios.* import boundary.break import CharCollector.* -def changemode(dir: Int) = +def changemode(rawMode: Boolean) = val oldt = stackalloc[termios]() val newt = stackalloc[termios]() val STDIN_FILENO = 0 - if dir == 1 then + if rawMode then tcgetattr(STDIN_FILENO, oldt) !newt = !oldt (!newt)._4 = (!newt)._4 & ~(ICANON | ECHO) @@ -41,11 +41,11 @@ private class InputProviderImpl(o: Output) extends InputProvider(o), InputProviderPlatform: - override def evaluateFuture(f: Interactive) = - Future.successful(evaluate(f)) + override def evaluateFuture[Result](handler: Handler[Result]) = + Future.successful(evaluate(handler)) - override def evaluate(f: Interactive): Completion = - changemode(1) + override def evaluate[Result](handler: Handler[Result]): Completion[Result] = + changemode(rawMode = true) var lastRead = 0 @@ -53,27 +53,27 @@ private class InputProviderImpl(o: Output) lastRead = getchar() lastRead - boundary[Completion]: + boundary[Completion[Result]]: - def whatNext(n: Next) = + def whatNext(n: Next[Result]) = n match - case Next.Continue => - case Next.Done(value: String) => break(Completion.Finished(value)) - case Next.Stop => break(Completion.Interrupted) - case Next.Error(msg) => break(Completion.Error(msg)) + case Next.Continue => + case Next.Done(value) => break(Completion.Finished(value)) + case Next.Stop => break(Completion.Interrupted) + case Next.Error(msg) => break(Completion.Error(msg)) def send(ev: Event) = - whatNext(f.handler(ev)) + whatNext(handler(ev)) var state = State.Init - whatNext(f.handler(Event.Init)) + whatNext(handler(Event.Init)) while read() != 0 do val (newState, result) = decode(state, lastRead) result match - case n: Next => whatNext(n) + case n: DecodeResult => whatNext(n.toNext) case e: Event => send(e) @@ -85,5 +85,5 @@ private class InputProviderImpl(o: Output) end evaluate - override def close() = changemode(0) + override def close() = changemode(rawMode = false) end InputProviderImpl diff --git a/modules/core/src/main/scalanative/InputProviderPlatform.scala b/modules/core/src/main/scalanative/InputProviderPlatform.scala index bfaec31..dc8997c 100644 --- a/modules/core/src/main/scalanative/InputProviderPlatform.scala +++ b/modules/core/src/main/scalanative/InputProviderPlatform.scala @@ -14,15 +14,15 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import scala.concurrent.Future trait InputProviderPlatform: self: InputProvider => - def evaluate(f: Interactive): Completion - def evaluateFuture(f: Interactive): Future[Completion] + def evaluate[Result](f: Handler[Result]): Completion[Result] + def evaluateFuture[Result](f: Handler[Result]): Future[Completion[Result]] trait InputProviderCompanionPlatform: def apply(o: Output): InputProvider = InputProviderImpl(o) diff --git a/modules/core/src/main/scalanative/PlatformStd.scala b/modules/core/src/main/scalanative/PlatformStd.scala index 747e255..3155346 100644 --- a/modules/core/src/main/scalanative/PlatformStd.scala +++ b/modules/core/src/main/scalanative/PlatformStd.scala @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts trait PlatformStd extends Output: override def logLn[A: AsString](a: A): Unit = System.err.println(a.render) diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation index 9ae8c21..c16d972 100644 --- a/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation @@ -1,24 +1,24 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃> rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃· rizza ┃ -┃> flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ rizza ┃ +┃ ‣ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing b/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing index 1cca425..d9ffd2f 100644 --- a/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing +++ b/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing @@ -1,38 +1,38 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('z') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > z┃ -┃> rizza ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > z┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DELETE) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? >  ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? >  ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('l') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > l┃ -┃> killa ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > l┃ +┃ ‣ killa ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('i') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > li┃ -┃> flizza ┃ -┃ ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > li┃ +┃ ‣ flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation b/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation index 9ae8c21..c16d972 100644 --- a/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation +++ b/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation @@ -1,24 +1,24 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃> rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃· rizza ┃ -┃> flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ rizza ┃ +┃ ‣ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing b/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing index 1cca425..d9ffd2f 100644 --- a/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing +++ b/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing @@ -1,38 +1,38 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('z') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > z┃ -┃> rizza ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > z┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DELETE) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? >  ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? >  ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('l') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > l┃ -┃> killa ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > l┃ +┃ ‣ killa ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('i') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > li┃ -┃> flizza ┃ -┃ ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > li┃ +┃ ‣ flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/modules/core/src/test/scala/ExampleTests.scala b/modules/core/src/test/scala/ExampleTests.scala index ec60f82..67d22e9 100644 --- a/modules/core/src/test/scala/ExampleTests.scala +++ b/modules/core/src/test/scala/ExampleTests.scala @@ -1,9 +1,9 @@ package proompts -import com.indoorvivants.proompts.* +import proompts.* class ExampleTests extends munit.FunSuite, TerminalTests: - val prompt = Prompt.Alternatives( + val prompt = AlternativesPrompt( "How do you do fellow kids?", List("killa", "rizza", "flizza") ) diff --git a/modules/core/src/test/scala/TerminalTests.scala b/modules/core/src/test/scala/TerminalTests.scala index dac254f..c8fea06 100644 --- a/modules/core/src/test/scala/TerminalTests.scala +++ b/modules/core/src/test/scala/TerminalTests.scala @@ -1,14 +1,16 @@ package proompts -import com.indoorvivants.proompts.* +import proompts.* import com.indoorvivants.snapshots.munit_integration.* trait TerminalTests extends MunitSnapshotsIntegration: self: munit.FunSuite => - def terminalTest( + def terminalTest[R]( name: String - )(prompt: Prompt, events: List[Event])(implicit loc: munit.Location): Unit = + )(prompt: Prompt[R], events: List[Event])(implicit + loc: munit.Location + ): Unit = test(name) { val result = terminalSession( @@ -19,15 +21,15 @@ trait TerminalTests extends MunitSnapshotsIntegration: assertSnapshot(name, result) } - def terminalSession(name: String, prompt: Prompt, events: List[Event]) = + def terminalSession[R](name: String, prompt: Prompt[R], events: List[Event]) = val sb = new java.lang.StringBuilder val term = TracingTerminal(Output.DarkVoid) val capturing = Output.Delegate(term.writer, s => sb.append(s + "\n")) - val i = Interactive(term, prompt, capturing, colors = false) + val handler = prompt.handler(term, capturing, colors = false) events.foreach: ev => sb.append(ev.toString() + "\n") - i.handler(ev) + handler(ev) sb.append(term.getPretty() + "\n") sb.toString() end terminalSession diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/hello.scala index 0edb419..9857cfc 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/hello.scala @@ -14,36 +14,41 @@ * limitations under the License. */ -package com.indoorvivants.proompts +package proompts import concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future + +case class Info( + day: Option[String] = None, + work: Option[String] = None, + weather: Option[String] = None +) @main def hello = - PromptChains - .future( - Prompt.Alternatives( - "How is your day?", - List("great", "okay", "shite") - ), - s => List(s) + PromptChain + .future(Info()) + .prompt( + _ => + AlternativesPrompt( + "How is your day?", + List("great", "okay", "shite") + ), + (info, day) => info.copy(day = Some(day)) ) - .andThen( - day => - Prompt.Alternatives( - s"So your day has been ${day}. And how was your poop", - List("Strong", "Smelly") + .prompt( + info => + AlternativesPrompt( + s"So your day has been ${info.day.get}. How are things at work?", + List("please go away", "I don't want to talk about it") ), - (cur, poop) => poop :: cur + (info, work) => info.copy(work = Some(work)) ) - .andThen( - poop => - Future.successful( - Prompt.Alternatives( - s"I see... whatcha wanna do", - List("Partay", "sleep") - ) + .prompt( + _ => + AlternativesPrompt( + s"Great! What fantastic weather we're having, right?", + List("please leave me alone", "don't you have actual friends?") ), - (cur, doing) => doing :: cur + (cur, weather) => cur.copy(weather = Some(weather)) ) .evaluateFuture .foreach: results => From 89df196182dc7622325e5de1dbb5f9459bd172ce Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 16 Feb 2024 13:52:48 +0000 Subject: [PATCH 26/32] remove file --- modules/core/src/main/scala/Interactive.scala | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 modules/core/src/main/scala/Interactive.scala diff --git a/modules/core/src/main/scala/Interactive.scala b/modules/core/src/main/scala/Interactive.scala deleted file mode 100644 index ca7b414..0000000 --- a/modules/core/src/main/scala/Interactive.scala +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2023 Anton Sviridov - * - * 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 proompts - -def errln(o: Any) = System.err.println(o) - -// class Interactive[Result]( -// terminal: Terminal, -// prompt: Prompt[Result], -// out: Output, -// colors: Boolean -// ): -// val handler: Handler[Result] = -// prompt match -// case p: Prompt.Input => -// // TODO - reorg the codebase so this instanceOf is not required -// InteractiveTextInput(p, terminal, out, colors).handler.asInstanceOf -// case p: Prompt.Alternatives => -// // TODO - reorg the codebase so this instanceOf is not required -// InteractiveAlternatives(terminal, p, out, colors).handler.asInstanceOf - -// end Interactive From bb0bf9557f232550c298cd15088a75ecf95bd2be Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 23 Feb 2024 12:23:43 +0000 Subject: [PATCH 27/32] Cats Effect module --- build.sbt | 25 ++++++- .../src/main/scala/PromptChainIO.scala | 74 +++++++++++++++++++ .../src/main/scala/PromptChainFuture.scala | 5 +- .../main/scala/{hello.scala => future.scala} | 8 +- modules/example/src/main/scala/io.scala | 58 +++++++++++++++ project/build.properties | 2 +- 6 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 modules/cats-effect/src/main/scala/PromptChainIO.scala rename modules/example/src/main/scala/{hello.scala => future.scala} (96%) create mode 100644 modules/example/src/main/scala/io.scala diff --git a/build.sbt b/build.sbt index 76b0372..3629741 100644 --- a/build.sbt +++ b/build.sbt @@ -74,8 +74,30 @@ lazy val core = projectMatrix ) .enablePlugins(SnapshotsPlugin) -lazy val example = projectMatrix +lazy val catsEffect = projectMatrix + .in(file("modules/cats-effect")) + .defaultAxes(defaults*) + .settings( + name := "cats-effect" + ) .dependsOn(core) + .settings(munitSettings) + .jvmPlatform(Versions.scalaVersions) + .jsPlatform(Versions.scalaVersions, disableDependencyChecks) + .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + .settings( + snapshotsPackageName := "proompts.catseffect", + snapshotsIntegrations += SnapshotIntegration.MUnit, + scalacOptions += "-Wunused:all", + scalaJSUseMainModuleInitializer := true, + scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), + libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.5.3", + nativeConfig ~= (_.withIncrementalCompilation(true)) + ) + .enablePlugins(SnapshotsPlugin) + +lazy val example = projectMatrix + .dependsOn(core, catsEffect) .in(file("modules/example")) .defaultAxes(defaults*) .settings( @@ -89,6 +111,7 @@ lazy val example = projectMatrix .settings( scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, + mainClass := Some("example.io.ioExample"), scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), nativeConfig ~= (_.withIncrementalCompilation(true)) ) diff --git a/modules/cats-effect/src/main/scala/PromptChainIO.scala b/modules/cats-effect/src/main/scala/PromptChainIO.scala new file mode 100644 index 0000000..c16b3dd --- /dev/null +++ b/modules/cats-effect/src/main/scala/PromptChainIO.scala @@ -0,0 +1,74 @@ +package proompts.catseffect + +import cats.effect.* +import proompts.* + +case class PromptChainIO[A] private[catseffect] ( + init: A, + terminal: Terminal, + out: Output, + colors: Boolean, + reversedSteps: List[ + A => IO[A] + ] +): + def prompt[R]( + nextPrompt: A => Prompt[R] | IO[Prompt[R]], + updateValue: (A, R) => A | IO[A] + ) = + val step = + (a: A) => + lift(nextPrompt)(a).flatMap: prompt => + eval(prompt): nextResult => + lift(updateValue.tupled)(a, nextResult) + + copy(reversedSteps = step :: reversedSteps) + end prompt + + def evaluateIO: IO[A] = + reversedSteps.reverse.foldLeft(IO.pure(init)): + case (acc, step) => + acc.flatMap(step) + end evaluateIO + + private def lift[A, B](f: A => B | IO[B]): A => IO[B] = + a => + f(a) match + case f: IO[?] => f.asInstanceOf[IO[B]] + case other => IO.pure(other.asInstanceOf[B]) + + private def eval[T, R](p: Prompt[R])(v: R => IO[T]): IO[T] = + IO.fromFuture(IO(inputProvider.evaluateFuture(handler(p)))) + .flatMap(c => check(c)(v)) + + private def check[T, R](c: Completion[R])(v: R => IO[T]): IO[T] = + c match + case Completion.Interrupted => + fail("interrupted") + case Completion.Error(msg) => + fail(msg) + case Completion.Finished(value) => + v(value) + + private lazy val inputProvider = InputProvider(out) + private def handler[R](prompt: Prompt[R]) = + prompt.handler(terminal, out, colors) + + private def fail(msg: String) = IO.raiseError(new RuntimeException(msg)) + +end PromptChainIO + +extension (p: PromptChain.type) + def io[A]( + init: A, + terminal: Terminal = Terminal.ansi(Output.Std), + out: Output = Output.Std, + colors: Boolean = true + ): PromptChainIO[A] = + new PromptChainIO[A]( + init = init, + terminal = terminal, + out = out, + colors = colors, + reversedSteps = Nil + ) diff --git a/modules/core/src/main/scala/PromptChainFuture.scala b/modules/core/src/main/scala/PromptChainFuture.scala index 1e34bb5..19b4d56 100644 --- a/modules/core/src/main/scala/PromptChainFuture.scala +++ b/modules/core/src/main/scala/PromptChainFuture.scala @@ -29,7 +29,6 @@ private[proompts] case class PromptChainFuture[A] private[proompts] ( reversedSteps.reverse.foldLeft(Future.successful(init)): case (acc, step) => acc.flatMap(step) - end evaluateFuture private def fail(msg: String) = Future.failed(new RuntimeException(msg)) @@ -47,7 +46,7 @@ private[proompts] case class PromptChainFuture[A] private[proompts] ( private def eval[T, R](p: Prompt[R])(v: R => Future[T])(using ExecutionContext ): Future[T] = check( - ip.evaluateFuture(handler(p)) + inputProvider.evaluateFuture(handler(p)) )(v) private def check[T, R](c: Completion[R])(v: R => Future[T]): Future[T] = @@ -59,7 +58,7 @@ private[proompts] case class PromptChainFuture[A] private[proompts] ( case Completion.Finished(value) => v(value) - private def ip = InputProvider(out) + private def inputProvider = InputProvider(out) private def handler[R](prompt: Prompt[R]) = prompt.handler(terminal, out, colors) end PromptChainFuture diff --git a/modules/example/src/main/scala/hello.scala b/modules/example/src/main/scala/future.scala similarity index 96% rename from modules/example/src/main/scala/hello.scala rename to modules/example/src/main/scala/future.scala index 9857cfc..2e81d23 100644 --- a/modules/example/src/main/scala/hello.scala +++ b/modules/example/src/main/scala/future.scala @@ -14,8 +14,10 @@ * limitations under the License. */ -package proompts +package example.future + import concurrent.ExecutionContext.Implicits.global +import proompts.* case class Info( day: Option[String] = None, @@ -23,7 +25,7 @@ case class Info( weather: Option[String] = None ) -@main def hello = +@main def future = PromptChain .future(Info()) .prompt( @@ -53,5 +55,3 @@ case class Info( .evaluateFuture .foreach: results => println(results) - -end hello diff --git a/modules/example/src/main/scala/io.scala b/modules/example/src/main/scala/io.scala new file mode 100644 index 0000000..60beef3 --- /dev/null +++ b/modules/example/src/main/scala/io.scala @@ -0,0 +1,58 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 example.catseffect + +import proompts.*, catseffect.* +import cats.effect.* + +case class Info( + day: Option[String] = None, + work: Option[String] = None, + weather: Option[String] = None +) +object ioExample extends IOApp.Simple: + def run: IO[Unit] = + PromptChain + .io(Info()) + .prompt( + _ => + AlternativesPrompt( + "How is your day?", + List("great", "okay", "shite") + ), + (info, day) => info.copy(day = Some(day)) + ) + .prompt( + info => + AlternativesPrompt( + s"So your day has been ${info.day.get}. How are things at work?", + List("please go away", "I don't want to talk about it") + ), + (info, work) => info.copy(work = Some(work)) + ) + .prompt( + _ => + AlternativesPrompt( + s"Great! What fantastic weather we're having, right?", + List("please leave me alone", "don't you have actual friends?") + ), + (cur, weather) => cur.copy(weather = Some(weather)) + ) + .evaluateIO + .flatMap: results => + IO.println(results) +end ioExample diff --git a/project/build.properties b/project/build.properties index e8a1e24..04267b1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.7 +sbt.version=1.9.9 From 3e79ec05ab66c3c593eef46acd882b8dd16c53d4 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 23 Feb 2024 12:37:28 +0000 Subject: [PATCH 28/32] fix example running --- build.sbt | 2 +- modules/example/src/main/scala/io.scala | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 3629741..eb05ee4 100644 --- a/build.sbt +++ b/build.sbt @@ -111,7 +111,7 @@ lazy val example = projectMatrix .settings( scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, - mainClass := Some("example.io.ioExample"), + Compile / mainClass := Some("example.catseffect.ioExample"), scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), nativeConfig ~= (_.withIncrementalCompilation(true)) ) diff --git a/modules/example/src/main/scala/io.scala b/modules/example/src/main/scala/io.scala index 60beef3..31ff0bb 100644 --- a/modules/example/src/main/scala/io.scala +++ b/modules/example/src/main/scala/io.scala @@ -54,5 +54,5 @@ object ioExample extends IOApp.Simple: ) .evaluateIO .flatMap: results => - IO.println(results) + IO.println(results).replicateA_(5) end ioExample From b9f2bd44e8ec38c41dc70afecbc451dfc3d097b0 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 23 Feb 2024 12:41:12 +0000 Subject: [PATCH 29/32] Remove JVM module --- build.sbt | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/build.sbt b/build.sbt index eb05ee4..78711a5 100644 --- a/build.sbt +++ b/build.sbt @@ -30,7 +30,7 @@ inThisBuild( ) val Versions = new { - val Scala3 = "3.3.1" + val Scala3 = "3.3.2" val munit = "1.0.0-M11" val scalaVersions = Seq(Scala3) } @@ -50,7 +50,8 @@ lazy val munitSettings = Seq( lazy val root = project .in(file(".")) .aggregate(core.projectRefs*) - .aggregate(docs.projectRefs*) + .aggregate(example.projectRefs*) + //.aggregate(docs.projectRefs*) .settings(noPublish) lazy val core = projectMatrix @@ -60,7 +61,7 @@ lazy val core = projectMatrix name := "core" ) .settings(munitSettings) - .jvmPlatform(Versions.scalaVersions) + //.jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( @@ -82,7 +83,7 @@ lazy val catsEffect = projectMatrix ) .dependsOn(core) .settings(munitSettings) - .jvmPlatform(Versions.scalaVersions) + //.jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( @@ -105,7 +106,7 @@ lazy val example = projectMatrix noPublish ) .settings(munitSettings) - .jvmPlatform(Versions.scalaVersions) + //.jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( @@ -116,20 +117,6 @@ lazy val example = projectMatrix nativeConfig ~= (_.withIncrementalCompilation(true)) ) -lazy val docs = projectMatrix - .in(file("myproject-docs")) - .dependsOn(core) - .defaultAxes(defaults*) - .settings( - mdocVariables := Map( - "VERSION" -> version.value - ) - ) - .settings(disableDependencyChecks) - .jvmPlatform(Versions.scalaVersions) - .enablePlugins(MdocPlugin) - .settings(noPublish) - val noPublish = Seq( publish / skip := true, publishLocal / skip := true @@ -149,7 +136,6 @@ val CICommands = Seq( "clean", "compile", "test", - "docs/mdoc", "scalafmtCheckAll", "scalafmtSbtCheck", s"scalafix --check $scalafixRules", From 3bc45d6cf2fbea0944a3ff79050fe89305694e4f Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Fri, 23 Feb 2024 12:46:04 +0000 Subject: [PATCH 30/32] inline inp --- modules/core/src/main/scala/Prompt.scala | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 0f35047..a90ea90 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -29,9 +29,7 @@ case class InputPrompt(lab: String) extends Prompt[String]: output: Output, colors: Boolean ): Handler[String] = - val inp = InteractiveTextInput(this, terminal, output, colors) - - inp.handler + InteractiveTextInput(this, terminal, output, colors).handler end InputPrompt case class AlternativesPrompt(lab: String, alts: List[String]) @@ -41,15 +39,6 @@ case class AlternativesPrompt(lab: String, alts: List[String]) output: Output, colors: Boolean ): Handler[String] = - val inp = InteractiveAlternatives(this, terminal, output, colors) - - inp.handler + InteractiveAlternatives(this, terminal, output, colors).handler end AlternativesPrompt -// enum Prompt[Result]: -// case Input(lab: String) extends Prompt[String] -// case Alternatives(lab: String, alts: List[String]) extends Prompt[String] - -// def label = this match -// case Input(label) => label -// case Alternatives(label, alts) => label From 3e85848183a18386145538da3c3b24b15b644670 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 21 Jul 2024 09:13:45 +0100 Subject: [PATCH 31/32] JVM works! --- .scalafix.conf | 2 + build.sbt | 43 ++++++++--- modules/core/src/main/java/ChangeMode.java | 65 ++++++++++++++++ .../src/main/scalajvm/InputProviderImpl.scala | 75 +++++++++++++++++++ .../main/scalajvm/InputProviderPlatform.scala | 12 +-- .../core}/alternatives_navigation | 0 .../core}/alternatives_typing | 0 .../coreJS}/alternatives_navigation | 0 .../coreJS}/alternatives_typing | 0 .../coreNative}/alternatives_navigation | 36 ++++----- .../coreNative}/alternatives_typing | 60 +++++++-------- project/build.properties | 2 +- project/plugins.sbt | 8 +- 13 files changed, 235 insertions(+), 68 deletions(-) create mode 100644 modules/core/src/main/java/ChangeMode.java create mode 100644 modules/core/src/main/scalajvm/InputProviderImpl.scala rename modules/core/src/{test/resources/snapshots/coreJS => snapshots/core}/alternatives_navigation (100%) rename modules/core/src/{test/resources/snapshots/coreJS => snapshots/core}/alternatives_typing (100%) rename modules/core/src/{test/resources/snapshots/coreNative => snapshots/coreJS}/alternatives_navigation (100%) rename modules/core/src/{test/resources/snapshots/coreNative => snapshots/coreJS}/alternatives_typing (100%) rename modules/core/src/{test/resources/snapshots/core => snapshots/coreNative}/alternatives_navigation (56%) rename modules/core/src/{test/resources/snapshots/core => snapshots/coreNative}/alternatives_typing (52%) diff --git a/.scalafix.conf b/.scalafix.conf index ec9b8ac..01301b7 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -8,3 +8,5 @@ OrganizeImports { importsOrder = Ascii removeUnused = false } +OrganizeImports.targetDialect = Scala3 +OrganizeImports.removeUnused = false \ No newline at end of file diff --git a/build.sbt b/build.sbt index 78711a5..f86cf47 100644 --- a/build.sbt +++ b/build.sbt @@ -30,9 +30,12 @@ inThisBuild( ) val Versions = new { - val Scala3 = "3.3.2" - val munit = "1.0.0-M11" + val Scala3 = "3.3.3" + val munit = "1.0.0" val scalaVersions = Seq(Scala3) + val fansi = "0.5.0" + val jna = "5.14.0" + val catsEffect = "3.5.3" } // https://github.com/cb372/sbt-explicit-dependencies/issues/27 @@ -51,7 +54,7 @@ lazy val root = project .in(file(".")) .aggregate(core.projectRefs*) .aggregate(example.projectRefs*) - //.aggregate(docs.projectRefs*) + // .aggregate(docs.projectRefs*) .settings(noPublish) lazy val core = projectMatrix @@ -61,7 +64,7 @@ lazy val core = projectMatrix name := "core" ) .settings(munitSettings) - //.jvmPlatform(Versions.scalaVersions) + .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( @@ -70,7 +73,24 @@ lazy val core = projectMatrix scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - libraryDependencies += "com.lihaoyi" %%% "fansi" % "0.4.0", + libraryDependencies += "com.lihaoyi" %%% "fansi" % Versions.fansi, + libraryDependencies += + "net.java.dev.jna" % "jna" % Versions.jna, + (Compile / unmanagedSourceDirectories) ++= { + val allCombos = List("js", "jvm", "native").combinations(2).toList + val dis = + virtualAxes.value.collectFirst { case p: VirtualAxis.PlatformAxis => + p.directorySuffix + }.get + + allCombos + .filter(_.contains(dis)) + .map { suff => + val suffixes = "scala" + suff.mkString("-", "-", "") + + (Compile / sourceDirectory).value / suffixes + } + }, nativeConfig ~= (_.withIncrementalCompilation(true)) ) .enablePlugins(SnapshotsPlugin) @@ -83,16 +103,16 @@ lazy val catsEffect = projectMatrix ) .dependsOn(core) .settings(munitSettings) - //.jvmPlatform(Versions.scalaVersions) + .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) - .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + // .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( snapshotsPackageName := "proompts.catseffect", snapshotsIntegrations += SnapshotIntegration.MUnit, scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), - libraryDependencies += "org.typelevel" %%% "cats-effect" % "3.5.3", + libraryDependencies += "org.typelevel" %%% "cats-effect" % Versions.catsEffect, nativeConfig ~= (_.withIncrementalCompilation(true)) ) .enablePlugins(SnapshotsPlugin) @@ -101,18 +121,19 @@ lazy val example = projectMatrix .dependsOn(core, catsEffect) .in(file("modules/example")) .defaultAxes(defaults*) + .enablePlugins(JavaAppPackaging) .settings( name := "example", noPublish ) .settings(munitSettings) - //.jvmPlatform(Versions.scalaVersions) + .jvmPlatform(Versions.scalaVersions) .jsPlatform(Versions.scalaVersions, disableDependencyChecks) - .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + // .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( scalacOptions += "-Wunused:all", scalaJSUseMainModuleInitializer := true, - Compile / mainClass := Some("example.catseffect.ioExample"), + Compile / mainClass := Some("example.catseffect.ioExample"), scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)), nativeConfig ~= (_.withIncrementalCompilation(true)) ) diff --git a/modules/core/src/main/java/ChangeMode.java b/modules/core/src/main/java/ChangeMode.java new file mode 100644 index 0000000..dbeca25 --- /dev/null +++ b/modules/core/src/main/java/ChangeMode.java @@ -0,0 +1,65 @@ +package proompts; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.NativeLong; +import com.sun.jna.Structure; +import java.util.List; +import java.util.Arrays; + +public class ChangeMode { + + // Define the libc interface + public static interface CLibrary extends Library { + CLibrary INSTANCE = Native.load("c", CLibrary.class); + + int tcgetattr(int fd, termios termios); + + int tcsetattr(int fd, int optional_actions, termios termios); + + int getchar(); + } + + // Define the termios structure + @Structure.FieldOrder({ "c_iflag", "c_oflag", "c_cflag", "c_lflag", "c_line", "c_cc", "c_ispeed", "c_ospeed" }) + public static class termios extends Structure { + public NativeLong c_iflag; + public NativeLong c_oflag; + public NativeLong c_cflag; + public NativeLong c_lflag; + public byte c_line; + public byte[] c_cc = new byte[32]; + public NativeLong c_ispeed; + public NativeLong c_ospeed; + } + + // Constants + public static final int STDIN_FILENO = 0; + public static final int TCSANOW = 0; + public static final int ICANON = 256; + public static final int ECHO = 0x0008; + + // Function to change mode + public static termios oldt = new termios(); // store original termios + + public static void changemode(int dir) { + termios newt = new termios(); + + if (dir == 1) { + CLibrary.INSTANCE.tcgetattr(STDIN_FILENO, oldt); // get current terminal attributes + newt.c_iflag = oldt.c_iflag; + newt.c_oflag = oldt.c_oflag; + newt.c_cflag = oldt.c_cflag; + newt.c_lflag = oldt.c_lflag; + newt.c_line = oldt.c_line; + newt.c_cc = oldt.c_cc; + newt.c_ispeed = oldt.c_ispeed; + newt.c_ospeed = oldt.c_ospeed; + + newt.c_lflag.setValue(newt.c_lflag.longValue() & ~(ICANON | ECHO)); // disable canonical mode and echo + CLibrary.INSTANCE.tcsetattr(STDIN_FILENO, TCSANOW, newt); // set new terminal attributes + } else { + CLibrary.INSTANCE.tcsetattr(STDIN_FILENO, TCSANOW, oldt); // restore original terminal attributes + } + } +} diff --git a/modules/core/src/main/scalajvm/InputProviderImpl.scala b/modules/core/src/main/scalajvm/InputProviderImpl.scala new file mode 100644 index 0000000..fbecd95 --- /dev/null +++ b/modules/core/src/main/scalajvm/InputProviderImpl.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 proompts + +import scala.concurrent.Future +import scala.util.boundary + +import boundary.break +import CharCollector.* + +private class InputProviderImpl(o: Output) + extends InputProvider(o), + InputProviderPlatform: + + override def evaluateFuture[Result](handler: Handler[Result]) = + Future.successful(evaluate(handler)) + + override def evaluate[Result](handler: Handler[Result]): Completion[Result] = + proompts.ChangeMode.changemode(1) + + var lastRead = 0 + + inline def read() = + lastRead = proompts.ChangeMode.CLibrary.INSTANCE.getchar() + lastRead + + boundary[Completion[Result]]: + + def whatNext(n: Next[Result]) = + n match + case Next.Continue => + case Next.Done(value) => break(Completion.Finished(value)) + case Next.Stop => break(Completion.Interrupted) + case Next.Error(msg) => break(Completion.Error(msg)) + + def send(ev: Event) = + whatNext(handler(ev)) + + var state = State.Init + + whatNext(handler(Event.Init)) + + while read() != 0 do + val (newState, result) = decode(state, lastRead) + + result match + case n: DecodeResult => whatNext(n.toNext) + case e: Event => + send(e) + + state = newState + + end while + + Completion.Interrupted + + end evaluate + + override def close() = proompts.ChangeMode.changemode(0) + +end InputProviderImpl diff --git a/modules/core/src/main/scalajvm/InputProviderPlatform.scala b/modules/core/src/main/scalajvm/InputProviderPlatform.scala index cc6ea17..dc8997c 100644 --- a/modules/core/src/main/scalajvm/InputProviderPlatform.scala +++ b/modules/core/src/main/scalajvm/InputProviderPlatform.scala @@ -16,13 +16,15 @@ package proompts +import scala.concurrent.Future + trait InputProviderPlatform: self: InputProvider => -private class InputProviderImpl(o: Output) - extends InputProvider(o), - InputProviderPlatform: - def close(): Unit = () + def evaluate[Result](f: Handler[Result]): Completion[Result] + def evaluateFuture[Result](f: Handler[Result]): Future[Completion[Result]] trait InputProviderCompanionPlatform: - def apply(f: Output): InputProvider = InputProviderImpl(f) + def apply(o: Output): InputProvider = InputProviderImpl(o) + +end InputProviderCompanionPlatform diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation b/modules/core/src/snapshots/core/alternatives_navigation similarity index 100% rename from modules/core/src/test/resources/snapshots/coreJS/alternatives_navigation rename to modules/core/src/snapshots/core/alternatives_navigation diff --git a/modules/core/src/test/resources/snapshots/coreJS/alternatives_typing b/modules/core/src/snapshots/core/alternatives_typing similarity index 100% rename from modules/core/src/test/resources/snapshots/coreJS/alternatives_typing rename to modules/core/src/snapshots/core/alternatives_typing diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation b/modules/core/src/snapshots/coreJS/alternatives_navigation similarity index 100% rename from modules/core/src/test/resources/snapshots/coreNative/alternatives_navigation rename to modules/core/src/snapshots/coreJS/alternatives_navigation diff --git a/modules/core/src/test/resources/snapshots/coreNative/alternatives_typing b/modules/core/src/snapshots/coreJS/alternatives_typing similarity index 100% rename from modules/core/src/test/resources/snapshots/coreNative/alternatives_typing rename to modules/core/src/snapshots/coreJS/alternatives_typing diff --git a/modules/core/src/test/resources/snapshots/core/alternatives_navigation b/modules/core/src/snapshots/coreNative/alternatives_navigation similarity index 56% rename from modules/core/src/test/resources/snapshots/core/alternatives_navigation rename to modules/core/src/snapshots/coreNative/alternatives_navigation index 9ae8c21..c16d972 100644 --- a/modules/core/src/test/resources/snapshots/core/alternatives_navigation +++ b/modules/core/src/snapshots/coreNative/alternatives_navigation @@ -1,24 +1,24 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃> rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DOWN) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃· killa ┃ -┃· rizza ┃ -┃> flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ killa ┃ +┃ rizza ┃ +┃ ‣ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/modules/core/src/test/resources/snapshots/core/alternatives_typing b/modules/core/src/snapshots/coreNative/alternatives_typing similarity index 52% rename from modules/core/src/test/resources/snapshots/core/alternatives_typing rename to modules/core/src/snapshots/coreNative/alternatives_typing index 1cca425..d9ffd2f 100644 --- a/modules/core/src/test/resources/snapshots/core/alternatives_typing +++ b/modules/core/src/snapshots/coreNative/alternatives_typing @@ -1,38 +1,38 @@ Event.Init -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('z') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > z┃ -┃> rizza ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > z┃ +┃ ‣ rizza ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(DELETE) -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? >  ┃ -┃> killa ┃ -┃· rizza ┃ -┃· flizza ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? >  ┃ +┃ ‣ killa ┃ +┃ rizza ┃ +┃ flizza ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('l') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > l┃ -┃> killa ┃ -┃· flizza ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > l┃ +┃ ‣ killa ┃ +┃ flizza ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Char('i') -┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -┃How do you do fellow kids? > li┃ -┃> flizza ┃ -┃ ┃ -┃ ┃ -┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +┃· How do you do fellow kids? > li┃ +┃ ‣ flizza ┃ +┃ ┃ +┃ ┃ +┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ Event.Key(ENTER) ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃✔ How do you do fellow kids? > flizza┃ diff --git a/project/build.properties b/project/build.properties index 04267b1..ee4c672 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.1 diff --git a/project/plugins.sbt b/project/plugins.sbt index 15a8dd0..8de35b8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -15,7 +15,9 @@ addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1") // Scala.js and Scala Native -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.14.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.4") -addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.5") +addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.9.16") + +addSbtPlugin("com.indoorvivants.snapshots" % "sbt-snapshots" % "0.0.7") From 59ad63fbfa72e4460742f4235ad5c7a4f2faf360 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 21 Jul 2024 09:44:02 +0100 Subject: [PATCH 32/32] fix --- build.sbt | 21 ++++++------------- modules/core/src/main/java/ChangeMode.java | 16 ++++++++++++++ .../core/src/main/scala/CharCollector.scala | 3 +-- modules/core/src/main/scala/Prompt.scala | 1 - modules/core/src/main/scala/PromptChain.scala | 16 ++++++++++++++ .../src/main/scala/PromptChainFuture.scala | 18 +++++++++++++++- .../PromptChainCompanionPlatform.scala | 16 ++++++++++++++ modules/example/src/main/scala/future.scala | 3 ++- modules/example/src/main/scala/io.scala | 5 ++++- project/plugins.sbt | 10 ++++----- 10 files changed, 82 insertions(+), 27 deletions(-) diff --git a/build.sbt b/build.sbt index f86cf47..b890095 100644 --- a/build.sbt +++ b/build.sbt @@ -38,12 +38,6 @@ val Versions = new { val catsEffect = "3.5.3" } -// https://github.com/cb372/sbt-explicit-dependencies/issues/27 -lazy val disableDependencyChecks = Seq( - unusedCompileDependenciesTest := {}, - undeclaredCompileDependenciesTest := {} -) - lazy val munitSettings = Seq( libraryDependencies += { "org.scalameta" %%% "munit" % Versions.munit % Test @@ -65,8 +59,8 @@ lazy val core = projectMatrix ) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) - .jsPlatform(Versions.scalaVersions, disableDependencyChecks) - .nativePlatform(Versions.scalaVersions, disableDependencyChecks) + .jsPlatform(Versions.scalaVersions) + .nativePlatform(Versions.scalaVersions) .settings( snapshotsPackageName := "proompts", snapshotsIntegrations += SnapshotIntegration.MUnit, @@ -104,7 +98,7 @@ lazy val catsEffect = projectMatrix .dependsOn(core) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) - .jsPlatform(Versions.scalaVersions, disableDependencyChecks) + .jsPlatform(Versions.scalaVersions) // .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( snapshotsPackageName := "proompts.catseffect", @@ -128,7 +122,7 @@ lazy val example = projectMatrix ) .settings(munitSettings) .jvmPlatform(Versions.scalaVersions) - .jsPlatform(Versions.scalaVersions, disableDependencyChecks) + .jsPlatform(Versions.scalaVersions) // .nativePlatform(Versions.scalaVersions, disableDependencyChecks) .settings( scalacOptions += "-Wunused:all", @@ -160,17 +154,14 @@ val CICommands = Seq( "scalafmtCheckAll", "scalafmtSbtCheck", s"scalafix --check $scalafixRules", - "headerCheck", - "undeclaredCompileDependenciesTest", - "unusedCompileDependenciesTest" + "headerCheck" ).mkString(";") val PrepareCICommands = Seq( s"scalafix --rules $scalafixRules", "scalafmtAll", "scalafmtSbt", - "headerCreate", - "undeclaredCompileDependenciesTest" + "headerCreate" ).mkString(";") addCommandAlias("ci", CICommands) diff --git a/modules/core/src/main/java/ChangeMode.java b/modules/core/src/main/java/ChangeMode.java index dbeca25..b2c3fe3 100644 --- a/modules/core/src/main/java/ChangeMode.java +++ b/modules/core/src/main/java/ChangeMode.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 proompts; import com.sun.jna.Library; diff --git a/modules/core/src/main/scala/CharCollector.scala b/modules/core/src/main/scala/CharCollector.scala index 9a62cfc..d60c6d9 100644 --- a/modules/core/src/main/scala/CharCollector.scala +++ b/modules/core/src/main/scala/CharCollector.scala @@ -26,9 +26,8 @@ object CharCollector: case Error(msg: String) def toNext[R]: Next[R] = this match - case Continue => Next.Continue + case Continue => Next.Continue case Error(msg) => Next.Error(msg) - def decode(curState: State, char: Int): (State, DecodeResult | Event) = def isCSIParameterByte(b: Int) = diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index a90ea90..2eac2db 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -41,4 +41,3 @@ case class AlternativesPrompt(lab: String, alts: List[String]) ): Handler[String] = InteractiveAlternatives(this, terminal, output, colors).handler end AlternativesPrompt - diff --git a/modules/core/src/main/scala/PromptChain.scala b/modules/core/src/main/scala/PromptChain.scala index eddd0eb..3862047 100644 --- a/modules/core/src/main/scala/PromptChain.scala +++ b/modules/core/src/main/scala/PromptChain.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 proompts object PromptChain: diff --git a/modules/core/src/main/scala/PromptChainFuture.scala b/modules/core/src/main/scala/PromptChainFuture.scala index 19b4d56..b084982 100644 --- a/modules/core/src/main/scala/PromptChainFuture.scala +++ b/modules/core/src/main/scala/PromptChainFuture.scala @@ -1,7 +1,23 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 proompts -import scala.concurrent.Future import scala.concurrent.ExecutionContext +import scala.concurrent.Future private[proompts] case class PromptChainFuture[A] private[proompts] ( init: A, diff --git a/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala b/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala index 13fd190..ccb998d 100644 --- a/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala +++ b/modules/core/src/main/scalajs/PromptChainCompanionPlatform.scala @@ -1,3 +1,19 @@ +/* + * Copyright 2023 Anton Sviridov + * + * 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 proompts trait PromptChainCompanionPlatform diff --git a/modules/example/src/main/scala/future.scala b/modules/example/src/main/scala/future.scala index 2e81d23..9cabd7f 100644 --- a/modules/example/src/main/scala/future.scala +++ b/modules/example/src/main/scala/future.scala @@ -16,9 +16,10 @@ package example.future -import concurrent.ExecutionContext.Implicits.global import proompts.* +import concurrent.ExecutionContext.Implicits.global + case class Info( day: Option[String] = None, work: Option[String] = None, diff --git a/modules/example/src/main/scala/io.scala b/modules/example/src/main/scala/io.scala index 31ff0bb..f960faa 100644 --- a/modules/example/src/main/scala/io.scala +++ b/modules/example/src/main/scala/io.scala @@ -16,14 +16,17 @@ package example.catseffect -import proompts.*, catseffect.* import cats.effect.* +import proompts.* + +import catseffect.* case class Info( day: Option[String] = None, work: Option[String] = None, weather: Option[String] = None ) + object ioExample extends IOApp.Simple: def run: IO[Unit] = PromptChain diff --git a/project/plugins.sbt b/project/plugins.sbt index 8de35b8..27f8184 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,12 +4,10 @@ addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") // Code quality -//addSbtPlugin("io.github.davidgregory084" % "sbt-tpolecat" % "0.4.2") -addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.3.1") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.1") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.10.0") // Compiled documentation addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.5.1")