package example.akkawschat.cli import akka.actor.ActorSystem import akka.stream.scaladsl.{ Flow, Source } import akka.http.scaladsl.model.Uri import shared.Protocol import scala.util.{ Failure, Success } object ChatCLI extends App { def promptForName(): String = { Console.out.print("What's your name? ") Console.out.flush() Console.in.readLine() } val endpointBase = "ws://localhost:8080/chat" val name = promptForName() val endpoint = Uri(endpointBase).withQuery(Uri.Query("name" -> name)) implicit val system = ActorSystem() import system.dispatcher import Console._ def formatCurrentMembers(members: Seq[String]): String = s"(${members.size} people chatting: ${members.map(m ⇒ s"$YELLOW$m$RESET").mkString(", ")})" object ChatApp extends ConsoleDSL[String] { type State = Seq[String] // current chat members def initialState: Seq[String] = Nil def run(): Unit = { lazy val initialCommands = Command.PrintLine("Welcome to the Chat!") ~ readLineAndEmitLoop val inputFlow = Flow[Protocol.Message] .map { case Protocol.ChatMessage(sender, message) ⇒ Command.PrintLine(s"$YELLOW$sender$RESET: $message") case Protocol.Joined(member, all) ⇒ Command.PrintLine(s"$YELLOW$member$RESET ${GREEN}joined!$RESET ${formatCurrentMembers(all)}") ~ Command.SetState(all) case Protocol.Left(member, all) ⇒ Command.PrintLine(s"$YELLOW$member$RESET ${RED}left!$RESET ${formatCurrentMembers(all)}") ~ Command.SetState(all) } // inject initial commands before the commands generated by the server .prepend(Source.single(initialCommands)) val appFlow = inputFlow .via(consoleHandler) .filterNot(_.trim.isEmpty) .watchTermination()((_, f) => f onComplete { case Success(_) => println("\nFinishing...") system.terminate() case Failure(e) ⇒ println(s"Connection to $endpoint failed because of '${e.getMessage}'") system.terminate() }) println("Connecting... (Use Ctrl-D to exit.)") ChatClient.connect(endpoint, appFlow) } val basePrompt = s"($name) >" lazy val readLineAndEmitLoop: Command = readWithParticipantNameCompletion { line ⇒ Command.Emit(line) ~ readLineAndEmitLoop } def readWithParticipantNameCompletion(andThen: String ⇒ Command): Command = { import Command._ /** Base mode: just collect characters */ def simpleMode(line: String): Command = SetPrompt(s"$basePrompt $line") ~ SetInputHandler { case '\r' ⇒ andThen(line) case '@' ⇒ mentionMode(line, "") case x if x >= 0x20 && x < 0x7e ⇒ simpleMode(line + x) case 127 /* backspace */ ⇒ // TODO: if backspacing to the end of a mention, enable mention mode simpleMode(line.dropRight(1)) } /** Mention mode: collect characters and try to make progress towards one of the current candidates */ def mentionMode(prefix: String, namePrefix: String): Command = { def candidates(state: State) = state.filter(_.toLowerCase startsWith namePrefix.toLowerCase).filterNot(_ == name) def fullLine = s"$prefix@$namePrefix" StatefulPrompt { state ⇒ val cs = candidates(state) val completion = cs.size match { case 1 ⇒ cs.head.drop(namePrefix.size) case 0 ⇒ " (no one there with this name)" case n if n < 5 ⇒ s" (${cs.mkString(", ")})" case n ⇒ s" (${cs.size} chatters)" } val left = if (completion.nonEmpty) TTY.cursorLeft(completion.length) else "" s"$basePrompt $prefix${TTY.GRAY}@$namePrefix$RESET${YELLOW}$completion$left$RESET" } ~ SetStatefulInputHandler { state ⇒ { //case '\r' ⇒ andThen(fullLine) case ' ' ⇒ simpleMode(fullLine + " ") case '\t' ⇒ val cs = candidates(state) if (cs.size != 1) mentionMode(prefix, namePrefix) // ignore else simpleMode(prefix + "@" + cs.head + " ") case x if x >= 0x20 && x < 0x7e ⇒ mentionMode(prefix, namePrefix + x) case 127 /* backspace */ ⇒ if (namePrefix.isEmpty) simpleMode(prefix) else mentionMode(prefix, namePrefix.dropRight(1)) } } } simpleMode("") } } ChatApp.run() }