From 9e253c5f567065de5c655fca1027fe03f6fcfc97 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sun, 27 Apr 2025 12:04:37 +0100 Subject: [PATCH 1/2] Text splitter --- .../core/src/main/scala/TextSplitter.scala | 36 +++++++++++++++++++ .../src/test/scala/TextSplitterTests.scala | 16 +++++++++ 2 files changed, 52 insertions(+) create mode 100644 modules/core/src/main/scala/TextSplitter.scala create mode 100644 modules/core/src/test/scala/TextSplitterTests.scala diff --git a/modules/core/src/main/scala/TextSplitter.scala b/modules/core/src/main/scala/TextSplitter.scala new file mode 100644 index 0000000..80f8d60 --- /dev/null +++ b/modules/core/src/main/scala/TextSplitter.scala @@ -0,0 +1,36 @@ +package cue4s + +import scala.collection.mutable.ListBuffer + +object TextSplitter: + + def split(s: String, max: Int): List[String] = + + val lines = List.newBuilder[String] + val currentLine = ListBuffer.empty[String] + + val words = s.split(" ") + + inline def recordLine() = + if currentLine.nonEmpty then + lines += currentLine.mkString(" ") + currentLine.clear() + + var curWidth = 0 + words.foreach: word => + if curWidth < max then + currentLine += word + curWidth += word.length + (if currentLine.isEmpty then 0 else 1) + else + recordLine() + currentLine += word + curWidth = word.length + end if + + recordLine() + + lines.result() + + end split + +end TextSplitter diff --git a/modules/core/src/test/scala/TextSplitterTests.scala b/modules/core/src/test/scala/TextSplitterTests.scala new file mode 100644 index 0000000..0743420 --- /dev/null +++ b/modules/core/src/test/scala/TextSplitterTests.scala @@ -0,0 +1,16 @@ +package cue4s + +class TextSplitterTests extends munit.FunSuite: + test("splitting") { + val splitter = TextSplitter + val text = "hello world, I say" + val result = splitter.split(text, 5) + assertEquals(result, List("hello", "world,", "I say")) + + assertEquals(splitter.split(text, text.length), List(text)) + + assertEquals(splitter.split("", 0), List("")) + assertEquals(splitter.split("hello", 5), List("hello")) + assertEquals(splitter.split("hello", 3), List("hello")) + } +end TextSplitterTests From beef50860a274fc33ae0dc3d644efd1d537b8fc8 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 27 Sep 2025 13:22:44 +0100 Subject: [PATCH 2/2] WIP --- .../main/scala/InteractiveSingleChoice.scala | 37 ++++++++++++++++--- modules/core/src/main/scala/Prompt.scala | 2 + .../test/scala/FallbackRenderingTests.scala | 1 - .../src/main/scala-jvm-native/sync.scala | 2 +- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/modules/core/src/main/scala/InteractiveSingleChoice.scala b/modules/core/src/main/scala/InteractiveSingleChoice.scala index f9518d7..878005c 100644 --- a/modules/core/src/main/scala/InteractiveSingleChoice.scala +++ b/modules/core/src/main/scala/InteractiveSingleChoice.scala @@ -23,6 +23,7 @@ private[cue4s] class InteractiveSingleChoice( out: Output, theme: Theme, windowSize: Int, + width: Int, symbols: Symbols, ) extends PromptFramework[String](terminal, out): import InteractiveSingleChoice.* @@ -31,6 +32,8 @@ private[cue4s] class InteractiveSingleChoice( private lazy val altsWithIndex = alts.zipWithIndex private lazy val altMapping = altsWithIndex.map(_.swap).toMap + private lazy val preSplitMapping = + altMapping.view.mapValues(TextSplitter.split(_, width)).toMap override def initialState = State( text = "", @@ -75,6 +78,24 @@ private[cue4s] class InteractiveSingleChoice( import theme.* + def prefixed(lab: (Symbols => String) | String, text: String): String = + lab match + case f: (Symbols => String) => " " + f(symbols) + " " + text + case s: String => " " + s + text + + def prefixed( + lab: (Symbols => String) | String, + text: List[String], + ): List[String] = + var i = 0 + val indent = " " * prefixed(lab, "").length + text.map: l => + i += 1 + if i == 1 then prefixed(lab, l) + else indent + l + + end prefixed + override def renderState( st: State, status: Status, @@ -104,16 +125,20 @@ private[cue4s] class InteractiveSingleChoice( .zipWithIndex .foreach: case (id, idx) => - val alt = altMapping(id) - lines.addOne( - if id == selected then s" $altCursor $alt".focused + val text = preSplitMapping(id) + lines.addAll( + if id == selected then + prefixed( + _.altCursor, + text, + ).map(_.focused) else if st.display.windowStart > 0 && idx == 0 then - s" $pageUpArrow $alt".option + prefixed(_.pageUpArrow, text).map(_.option) else if filtered.size > st.display.windowSize && idx == st.display.windowSize - 1 && filtered.indexOf(id) != filtered.size - 1 - then s" $pageDownArrow $alt".option - else s" $alt".option, + then prefixed(_.pageDownArrow, text).map(_.option) + else prefixed(" ", text).map(_.option), ) end match case Status.Finished(value) => diff --git a/modules/core/src/main/scala/Prompt.scala b/modules/core/src/main/scala/Prompt.scala index 9c9acd2..3866fb3 100644 --- a/modules/core/src/main/scala/Prompt.scala +++ b/modules/core/src/main/scala/Prompt.scala @@ -231,6 +231,7 @@ object Prompt: private val lab: String, private val alts: List[String], private val windowSize: Int = 10, + private val width: Int = 50, ) extends Prompt[String]: def withWindowSize(i: Int) = copy(windowSize = i) @@ -248,6 +249,7 @@ object Prompt: out = output, theme = theme, windowSize = this.windowSize, + width = this.width, symbols = symbols, ) end SingleChoice diff --git a/modules/core/src/test/scala/FallbackRenderingTests.scala b/modules/core/src/test/scala/FallbackRenderingTests.scala index 8464139..72c2062 100644 --- a/modules/core/src/test/scala/FallbackRenderingTests.scala +++ b/modules/core/src/test/scala/FallbackRenderingTests.scala @@ -1,7 +1,6 @@ package cue4s import cue4s.* -import cue4s.Prompt.PasswordInput.Password import KeyEvent.* diff --git a/modules/example/src/main/scala-jvm-native/sync.scala b/modules/example/src/main/scala-jvm-native/sync.scala index 01ebfb0..eb1b6a9 100644 --- a/modules/example/src/main/scala-jvm-native/sync.scala +++ b/modules/example/src/main/scala-jvm-native/sync.scala @@ -34,7 +34,7 @@ import cue4s.Prompt.PasswordInput.Password "relaxing", "stressful", "exhausting", - "challenging", + "challenging challenging challenging challenging challenging challenging challenging challenging challenging challenging challenging", "wonderful", "uneventful", "interesting",