Skip to content

Commit

Permalink
JS impl
Browse files Browse the repository at this point in the history
  • Loading branch information
keynmol committed Nov 17, 2023
1 parent 5cc25c7 commit 2442215
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 152 deletions.
6 changes: 3 additions & 3 deletions modules/core/src/main/scala/ANSI.scala
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ private[proompts] object ANSI:
inline def restore() =
call('u')

inline def withRestore[A](inline f: => A) =
print(save())
inline def withRestore[A](writer: String => Unit)(inline f: => A) =
writer(save())
f
print(restore())
writer(restore())

end ANSI
1 change: 1 addition & 0 deletions modules/core/src/main/scala/Event.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.indoorvivants.proompts

enum Event:
case Init
case Key(which: KeyEvent)
case Char(which: Int)
case CSICode(bytes: List[Byte])
Expand Down
136 changes: 5 additions & 131 deletions modules/core/src/main/scala/Example.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,142 +2,16 @@ package com.indoorvivants.proompts

import ANSI.*

enum Prompt(label: String):
case Input(label: String, state: InputState) extends Prompt(label)
case Alternatives(label: String, alts: List[String], state: AlternativesState)
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))

case class InputState(text: String)
case class AlternativesState(
text: String,
selected: Int,
showing: Int
)

@main def hello =

var prompt = Prompt.Alternatives(
"How would you describe yourself?",
List("Sexylicious", "Shmexy", "Pexying")
)

def printPrompt() =
val lab = prompt.promptLabel
val lines = 0
print(move.horizontalTo(0))
print(erase.line.toEndOfLine())
prompt match
case Prompt.Input(label, state) =>
print(Console.CYAN + lab + Console.RESET + state.text)
case Prompt.Alternatives(label, alts, state) =>
print(Console.CYAN + lab + Console.RESET + state.text)
withRestore:
print("\n")

val filteredAlts =
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.min(1)
)

if filteredAlts.isEmpty then
print(move.horizontalTo(0))
print(erase.line.toEndOfLine())
print(fansi.Underlined.On("no matches"))
else
filteredAlts.zipWithIndex.foreach: (alt, idx) =>
print(move.horizontalTo(0))
print(erase.line.toEndOfLine())

val view =
if idx == adjustedSelected then fansi.Color.Green("> " + alt)
else fansi.Bold.On("· " + alt)
print(view)
if idx != filteredAlts.length - 1 then print("\n")
end if

for _ <- 0 until state.showing - newState.showing do
print(move.nextLine(1))
print(move.horizontalTo(0))
print(erase.line.toEndOfLine())
end match
end printPrompt

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() =
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))
)

InputProvider().attach:
case Event.Key(KeyEvent.UP) =>
selectUp()
printPrompt()
Next.Continue
case Event.Key(KeyEvent.DOWN) =>
selectDown()
printPrompt()
Next.Continue

case Event.Key(KeyEvent.ENTER) => // enter
println("booyah!")
Next.Stop

case Event.Key(KeyEvent.DELETE) => // enter
trimText()
printPrompt()
Next.Continue

case Event.Char(which) =>
appendText(which.toChar)
printPrompt()
Next.Continue
val result =
InputProvider().attach(env => Interactive(prompt, env.writer).handler)

case _ =>
Next.Continue
// Process.stdout.write("how do you do")
// Process.stdout.write(move.back(5))
// Process.stdout.write(erase.line.toEndOfLine())
end hello
9 changes: 7 additions & 2 deletions modules/core/src/main/scala/InputProvider.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,15 @@ enum Completion:
case Error(msg: String)

enum Next:
case Stop, Continue
case Stop, Continue
case Error(msg: String)

case class Environment(writer: String => Unit)

abstract class Handler:
def apply(ev: Event): Next

trait InputProvider extends AutoCloseable:
def attach(handler: Event => Next): Completion
def attach(env: Environment => Handler): Completion

object InputProvider extends InputProviderPlatform
142 changes: 142 additions & 0 deletions modules/core/src/main/scala/Interactive.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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() =
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))
)

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 AlternativesState(
text: String,
selected: Int,
showing: Int
)

16 changes: 16 additions & 0 deletions modules/core/src/main/scala/Prompt.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.indoorvivants.proompts

import ANSI.*

enum Prompt(label: String):
case Input(label: String, state: InputState) extends Prompt(label)
case Alternatives(label: String, alts: List[String], state: AlternativesState)
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))
Loading

0 comments on commit 2442215

Please sign in to comment.