├── project ├── build.properties └── plugins.sbt ├── cli ├── project │ └── build.properties └── src │ ├── main │ ├── g8 │ │ ├── project │ │ │ ├── build.properties │ │ │ └── plugins.sbt │ │ ├── Procfile │ │ ├── .scalafmt.conf │ │ ├── .gitignore │ │ ├── default.properties │ │ ├── deploy.sh │ │ ├── shared │ │ │ └── src │ │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── $package$ │ │ │ │ └── protocol │ │ │ │ └── ExampleService.scala │ │ ├── backend │ │ │ ├── application.conf │ │ │ └── src │ │ │ │ └── main │ │ │ │ └── scala │ │ │ │ └── $package$ │ │ │ │ ├── Config.scala │ │ │ │ └── Backend.scala │ │ ├── package.json │ │ ├── index.html │ │ ├── frontend │ │ │ └── src │ │ │ │ └── main │ │ │ │ ├── scala │ │ │ │ └── $package$ │ │ │ │ │ ├── Component.scala │ │ │ │ │ ├── Frontend.scala │ │ │ │ │ └── Main.scala │ │ │ │ └── static │ │ │ │ └── stylesheets │ │ │ │ └── main.scss │ │ ├── README.md │ │ ├── vite.config.js │ │ ├── build.sbt │ │ └── yarn.lock │ ├── resources │ │ ├── Schema.sql │ │ ├── resource-config.json │ │ └── reflection-config.json │ ├── .DS_Store │ └── scala │ │ ├── .DS_Store │ │ ├── tui │ │ ├── StringSyntax.scala │ │ ├── components │ │ │ ├── LineInput.scala │ │ │ ├── FancyComponent.scala │ │ │ └── Choose.scala │ │ └── TerminalApp.scala │ │ ├── view │ │ ├── Style.scala │ │ ├── Size.scala │ │ ├── Color.scala │ │ ├── KeyEvent.scala │ │ ├── Alignment.scala │ │ ├── Input.scala │ │ ├── TextMap.scala │ │ ├── EscapeCodes.scala │ │ └── View.scala │ │ ├── zio │ │ └── app │ │ │ ├── SbtError.scala │ │ │ ├── internal │ │ │ └── Utils.scala │ │ │ ├── FileSystemService.scala │ │ │ ├── DevMode.scala │ │ │ ├── TemplateGenerator.scala │ │ │ ├── Renamer.scala │ │ │ ├── Main.scala │ │ │ ├── Backend.scala │ │ │ └── SbtManager.scala │ │ └── database │ │ └── ast │ │ └── ParsingApp.scala │ ├── .DS_Store │ └── test │ └── scala │ ├── zio │ └── app │ │ └── RenamerSpec.scala │ └── database │ └── ast │ └── SqlSyntaxSpec.scala ├── examples ├── project │ └── build.properties ├── package.json ├── index.html ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── examples │ │ ├── Backend.scala │ │ └── services │ │ ├── ParameterizedServiceLive.scala │ │ └── ExampleServiceLive.scala ├── vite.config.js ├── shared │ └── src │ │ └── main │ │ └── scala │ │ └── examples │ │ └── ExampleService.scala ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── examples │ │ └── Frontend.scala └── yarn.lock ├── cli-frontend ├── project │ └── build.properties ├── src │ └── main │ │ ├── scala │ │ └── zio │ │ │ └── app │ │ │ └── cli │ │ │ └── frontend │ │ │ ├── fetching.scala │ │ │ ├── Component.scala │ │ │ ├── Main.scala │ │ │ ├── pickleparty.scala │ │ │ ├── SbtOutput.scala │ │ │ └── Frontend.scala │ │ └── static │ │ └── stylesheets │ │ └── main.scss ├── package.json ├── index.html └── vite.config.js ├── .gitignore ├── scripts └── examplesDev.sh ├── core ├── jvm │ └── src │ │ └── main │ │ ├── resources │ │ └── application.conf │ │ └── scala │ │ └── zio │ │ └── app │ │ └── DeriveRoutes.scala ├── shared │ └── src │ │ └── main │ │ └── scala │ │ └── zio │ │ └── app │ │ ├── ClientConfig.scala │ │ └── internal │ │ ├── BackendUtils.scala │ │ └── macros │ │ └── Macros.scala └── js │ └── src │ └── main │ └── scala │ └── zio │ └── app │ ├── DeriveClient.scala │ ├── FrontendUtils.scala │ └── FetchZioBackend.scala ├── release.sh ├── .scalafmt.conf ├── .github └── workflows │ └── release.yml ├── cli-shared └── src │ └── main │ └── scala │ └── zio │ └── app │ └── cli │ └── protocol │ └── ServerCommand.scala └── README.md /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.7.1 -------------------------------------------------------------------------------- /cli/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /examples/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /cli-frontend/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.6.2 2 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/fetching.scala: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cli/src/main/g8/project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.6.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .bsp/ 2 | .idea/ 3 | target/ 4 | node_modules 5 | 6 | .DS_Store -------------------------------------------------------------------------------- /cli/src/main/g8/Procfile: -------------------------------------------------------------------------------- 1 | web: backend/target/universal/stage/bin/backend 2 | -------------------------------------------------------------------------------- /cli/src/main/resources/Schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE user ( 2 | id uuid 3 | ); -------------------------------------------------------------------------------- /cli/src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitlangton/zio-app/HEAD/cli/src/.DS_Store -------------------------------------------------------------------------------- /cli/src/main/g8/.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 2.7.5 2 | align.preset = more 3 | maxColumn = 120 -------------------------------------------------------------------------------- /cli/src/main/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitlangton/zio-app/HEAD/cli/src/main/.DS_Store -------------------------------------------------------------------------------- /cli/src/main/g8/.gitignore: -------------------------------------------------------------------------------- 1 | .bsp/ 2 | .idea/ 3 | node_modules/ 4 | target/ 5 | dist/ 6 | project/project -------------------------------------------------------------------------------- /cli/src/main/scala/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kitlangton/zio-app/HEAD/cli/src/main/scala/.DS_Store -------------------------------------------------------------------------------- /cli/src/main/g8/default.properties: -------------------------------------------------------------------------------- 1 | name=My App 2 | package=app 3 | description=A full-stack Scala application powered by ZIO and Laminar. 4 | -------------------------------------------------------------------------------- /cli/src/main/g8/deploy.sh: -------------------------------------------------------------------------------- 1 | sbt frontend/fullLinkJS 2 | yarn exec vite -- build 3 | cp dist/index.html dist/200.html 4 | surge ./dist '$name$.surge.sh' -------------------------------------------------------------------------------- /scripts/examplesDev.sh: -------------------------------------------------------------------------------- 1 | tmux split -f -p 80 "sbt '~examplesJVM/reStart'" 2 | tmux split -f -p 80 "sleep 2; sbt '~examplesJS/fastLinkJS'" 3 | tmux attach -------------------------------------------------------------------------------- /cli/src/main/g8/shared/src/main/scala/$package$/protocol/ExampleService.scala: -------------------------------------------------------------------------------- 1 | package $package$.protocol 2 | 3 | import zio._ 4 | 5 | trait ExampleService { 6 | def magicNumber: UIO[Int] 7 | } -------------------------------------------------------------------------------- /cli/src/main/g8/backend/application.conf: -------------------------------------------------------------------------------- 1 | # This file is in HOCON format, a superset of JSON 2 | 3 | usernames = [ "Moose" , "Squirrel" , "Flapjack" , "Banana" , "Quetzalcoatal" , "Dungaree" , "Bumpy" , "Snickers" , "Brobostigan"] 4 | -------------------------------------------------------------------------------- /cli/src/main/g8/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.7.0") 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1") 3 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") 4 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "vite": "^2.3.3", 8 | "vite-plugin-html": "^2.0.7" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cli/src/main/resources/resource-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "resources":[ 3 | {"pattern":"dist/assets/index.*js"}, 4 | {"pattern":"dist/assets/index.*css"}, 5 | {"pattern":"\\Qdist/index.html\\E"} 6 | ], 7 | "bundles":[ 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /cli-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "sass": "^1.34.0", 8 | "vite": "^2.3.3", 9 | "vite-plugin-html": "^2.0.7" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cli/src/main/g8/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "$package$", 3 | "version": "0.1.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "devDependencies": { 7 | "sass": "^1.35.1", 8 | "vite": "^2.3.7", 9 | "vite-plugin-html": "^2.0.7" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /core/jvm/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | postgresDB { 2 | dataSourceClassName = org.postgresql.ds.PGSimpleDataSource 3 | dataSource.user = postgres 4 | dataSource.databaseName = kit 5 | dataSource.portNumber = 5432 6 | dataSource.serverName = localhost 7 | connectionTimeout = 30000 8 | } -------------------------------------------------------------------------------- /cli-frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | zio-app 8 | 9 | 10 | 11 |
12 | <%- script %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /cli/src/main/g8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | $name$ 8 | 9 | 10 | 11 |
12 | <%- script %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ZIO App Example 8 | 9 | 10 | 11 |
12 | <%- script %> 13 | 14 | 15 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/zio/app/DeriveRoutes.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio.http.HttpApp 4 | import zio.app.internal.macros.Macros 5 | 6 | import scala.language.experimental.macros 7 | 8 | object DeriveRoutes { 9 | def gen[Service]: HttpApp[Service, Throwable] = macro Macros.routes_impl[Service] 10 | } 11 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | sbt cliFrontend/fullLinkJS 2 | cd cli-frontend 3 | yarn exec -- vite build 4 | cd .. 5 | mv ./cli-frontend/dist ./cli/src/main/resources/dist 6 | 7 | sbt cli/nativeImage 8 | cd cli/target/native-image 9 | mv zio-app-cli zio-app 10 | tar -czf zio-app.gz zio-app 11 | echo $(shasum -a 256 zio-app.gz) 12 | open . 13 | -------------------------------------------------------------------------------- /cli/src/main/scala/tui/StringSyntax.scala: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | object StringSyntax { 4 | val ansiRegex: String = "(\u009b|\u001b\\[)[0-?]*[ -\\/]*[@-~]".r.regex 5 | 6 | implicit final class StringOps(val self: String) extends AnyVal { 7 | def removingAnsiCodes: String = 8 | self.replaceAll(ansiRegex, "") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/app/ClientConfig.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import sttp.model.Uri 4 | 5 | final case class ClientConfig( 6 | authToken: Option[String], 7 | root: Uri.AbsolutePath 8 | ) 9 | 10 | object ClientConfig { 11 | val empty: ClientConfig = 12 | ClientConfig(None, Uri.AbsolutePath(Seq.empty)) 13 | } 14 | -------------------------------------------------------------------------------- /cli/src/main/g8/frontend/src/main/scala/$package$/Component.scala: -------------------------------------------------------------------------------- 1 | package $package$ 2 | 3 | import com.raquo.laminar.api.L._ 4 | 5 | import scala.language.implicitConversions 6 | 7 | trait Component { 8 | def body: HtmlElement 9 | } 10 | 11 | object Component { 12 | implicit def toLaminarElement(component: Component): HtmlElement = component.body 13 | } 14 | -------------------------------------------------------------------------------- /core/js/src/main/scala/zio/app/DeriveClient.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio.app.internal.macros.Macros 4 | import scala.language.experimental.macros 5 | 6 | object DeriveClient { 7 | def gen[Service]: Service = macro Macros.client_impl[Service] 8 | def gen[Service](config: ClientConfig): Service = macro Macros.client_config_impl[Service] 9 | } 10 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/Component.scala: -------------------------------------------------------------------------------- 1 | package zio.app.cli.frontend 2 | 3 | import com.raquo.laminar.api.L._ 4 | 5 | import scala.language.implicitConversions 6 | 7 | trait Component { 8 | def body: HtmlElement 9 | } 10 | 11 | object Component { 12 | implicit def toLaminarElement(component: Component): HtmlElement = component.body 13 | } 14 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/Style.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | sealed abstract class Style(val code: String) 4 | 5 | object Style { 6 | case object Bold extends Style(scala.Console.BOLD) 7 | case object Underlined extends Style(scala.Console.UNDERLINED) 8 | case object Reversed extends Style(scala.Console.REVERSED) 9 | case object Dim extends Style("\u001b[2m") 10 | case object Default extends Style("") 11 | } 12 | -------------------------------------------------------------------------------- /cli/src/main/g8/README.md: -------------------------------------------------------------------------------- 1 | # $name$ 2 | 3 | $description$ 4 | 5 | Created with **[zio-app](https://github.com/kitlangton/zio-app)**. 6 | 7 | ## Run with zio-app 8 | 9 | 1. `zio-app dev` 10 | 2. open `http://localhost:3000` 11 | 12 | ## Run Manually 13 | 14 | 1. `sbt ~frontend/fastLinkJS` in another tab. 15 | 2. `sbt ~backend/reStart` in another tab. 16 | 3. `yarn install` 17 | 4. `yarn exec vite` 18 | 5. open `http://localhost:3000` 19 | 20 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/Size.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | case class Size private (width: Int, height: Int) { self => 4 | def scaled(dx: Int, dy: Int): Size = 5 | Size(width + dx, height + dy) 6 | 7 | def overriding(width: Option[Int] = None, height: Option[Int] = None): Size = 8 | Size(width.getOrElse(self.width), height.getOrElse(self.height)) 9 | } 10 | 11 | object Size { 12 | def apply(width: Int, height: Int) = 13 | new Size(width max 0, height max 0) 14 | } 15 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/SbtError.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | sealed trait SbtError extends Throwable 4 | 5 | object SbtError { 6 | case object WaitingForLock extends SbtError 7 | 8 | case class InvalidCommand(command: String) extends SbtError { 9 | override def getMessage: String = 10 | s"Invalid Sbt Command: $command" 11 | } 12 | 13 | case class SbtErrorMessage(message: String) extends SbtError { 14 | override def getMessage: String = 15 | s"SbtErrorMessage: $message" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/internal/Utils.scala: -------------------------------------------------------------------------------- 1 | package zio.app.internal 2 | 3 | import zio._ 4 | import zio.process.Command 5 | 6 | object Utils { 7 | def launchBrowser(url: String): Unit = { 8 | import java.awt.Desktop 9 | import java.net.URI; 10 | 11 | if (Desktop.isDesktopSupported && Desktop.getDesktop.isSupported(Desktop.Action.BROWSE)) { 12 | Desktop.getDesktop.browse(new URI("http://localhost:3000")); 13 | } 14 | } 15 | 16 | def say(message: String): UIO[Unit] = 17 | Command("say", message).run.ignore 18 | } 19 | -------------------------------------------------------------------------------- /cli/src/test/scala/zio/app/RenamerSpec.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio._ 4 | import zio.test._ 5 | 6 | object RenamerSpec extends ZIOSpecDefault { 7 | 8 | override def spec = suite("RenamerSpec")( 9 | test("Cool") { 10 | for { 11 | tempDir <- TemplateGenerator.cloneRepo 12 | _ <- ZIO.succeed(println(tempDir)) 13 | _ <- Renamer.renameFolders(tempDir) 14 | _ <- Renamer.renameFiles(tempDir, "Funky") 15 | _ <- Renamer.printTree(tempDir) 16 | } yield assertCompletes 17 | } 18 | ).provide(Renamer.live) 19 | } 20 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.0.6" 2 | maxColumn = 120 3 | align.preset = most 4 | align.multiline = false 5 | continuationIndent.defnSite = 2 6 | assumeStandardLibraryStripMargin = true 7 | docstrings.style = Asterisk 8 | docstrings.wrapMaxColumn = 80 9 | lineEndings = preserve 10 | includeCurlyBraceInSelectChains = false 11 | danglingParentheses.preset = true 12 | optIn.annotationNewlines = true 13 | newlines.alwaysBeforeMultilineDef = false 14 | runner.dialect = scala213 15 | rewrite.rules = [RedundantBraces, RedundantParens] 16 | 17 | rewriteTokens = { 18 | "⇒": "=>" 19 | "→": "->" 20 | "←": "<-" 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: [master, main] 5 | tags: ["*"] 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v2.3.4 11 | with: 12 | fetch-depth: 0 13 | - uses: olafurpg/setup-scala@v10 14 | - run: sbt ci-release 15 | env: 16 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 17 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 18 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 19 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 20 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-native-image" % "0.3.2") 2 | addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.8.1") 3 | addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") 4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.1") 5 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.10.1") 6 | addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") 7 | addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7") 8 | addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3") 9 | -------------------------------------------------------------------------------- /cli/src/main/g8/backend/src/main/scala/$package$/Config.scala: -------------------------------------------------------------------------------- 1 | package $package$ 2 | 3 | import zio._ 4 | import zio.config._ 5 | import zio.config.magnolia._ 6 | import zio.config.typesafe._ 7 | 8 | import java.io.File 9 | 10 | case class Config(usernames: List[String]) 11 | 12 | object Config { 13 | val descriptor: ConfigDescriptor[Config] = 14 | DeriveConfigDescriptor.descriptor[Config] 15 | 16 | val live: ZLayer[system.System, Nothing, Has[Config]] = 17 | TypesafeConfig.fromHoconFile(new File("application.conf"), descriptor).orDie 18 | 19 | val service: URIO[Has[Config], Config] = ZIO.service[Config] 20 | } 21 | -------------------------------------------------------------------------------- /cli/src/main/resources/reflection-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name":"java.lang.Thread", 4 | "allPublicMethods":true, 5 | "allPublicFields": true, 6 | "allPublicConstructors": true 7 | }, 8 | { 9 | "name": "java.util.Properties", 10 | "allDeclaredConstructors" : true, 11 | "allPublicConstructors" : true, 12 | "allDeclaredMethods" : true, 13 | "allPublicMethods" : true, 14 | "allDeclaredClasses" : true, 15 | "allPublicClasses" : true 16 | }, 17 | { 18 | "name": "io.netty.channel.socket.nio.NioServerSocketChannel", 19 | "methods": [ 20 | { "name": "", "parameterTypes": [] } 21 | ] 22 | } 23 | ] -------------------------------------------------------------------------------- /cli/src/main/scala/view/Color.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | sealed abstract class Color(val code: String) 4 | 5 | object Color { 6 | case object Black extends Color(scala.Console.BLACK) 7 | case object Blue extends Color(scala.Console.BLUE) 8 | case object Cyan extends Color(scala.Console.CYAN) 9 | case object Green extends Color(scala.Console.GREEN) 10 | case object Magenta extends Color(scala.Console.MAGENTA) 11 | case object Red extends Color(scala.Console.RED) 12 | case object White extends Color(scala.Console.WHITE) 13 | case object Yellow extends Color(scala.Console.YELLOW) 14 | case object Default extends Color("") 15 | } 16 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/Main.scala: -------------------------------------------------------------------------------- 1 | package zio.app.cli.frontend 2 | 3 | import com.raquo.laminar.api.L._ 4 | import org.scalajs.dom 5 | 6 | import scala.scalajs.js 7 | import scala.scalajs.js.annotation.JSImport 8 | 9 | @js.native 10 | @JSImport("stylesheets/main.scss", JSImport.Namespace) 11 | object Css extends js.Any 12 | 13 | object Main { 14 | val css: Css.type = Css 15 | 16 | def main(args: Array[String]): Unit = { 17 | val _ = documentEvents.onDomContentLoaded.foreach { _ => 18 | val appContainer = dom.document.querySelector("#app") 19 | appContainer.innerHTML = "" 20 | val _ = render(appContainer, Frontend.view) 21 | }(unsafeWindowOwner) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/KeyEvent.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import zio.Chunk 4 | 5 | sealed trait KeyEvent 6 | 7 | object KeyEvent { 8 | case class UnSupported(bytes: Chunk[Int]) extends KeyEvent 9 | case class Character(char: Char) extends KeyEvent 10 | case object Up extends KeyEvent 11 | case object Down extends KeyEvent 12 | case object Left extends KeyEvent 13 | case object Right extends KeyEvent 14 | case object Enter extends KeyEvent 15 | case object Delete extends KeyEvent 16 | case object Escape extends KeyEvent 17 | case object Tab extends KeyEvent 18 | case object ShiftTab extends KeyEvent 19 | case object Exit extends KeyEvent 20 | } 21 | -------------------------------------------------------------------------------- /cli/src/main/g8/frontend/src/main/scala/$package$/Frontend.scala: -------------------------------------------------------------------------------- 1 | package $package$ 2 | 3 | import com.raquo.laminar.api.L._ 4 | import $package$.protocol.{ExampleService} 5 | import zio._ 6 | import zio.app.DeriveClient 7 | import animus._ 8 | 9 | object Frontend { 10 | val runtime = Runtime.default 11 | val client = DeriveClient.gen[ExampleService] 12 | 13 | def view: Div = 14 | div( 15 | h3("IMPORTANT WEBSITE"), 16 | debugView("MAGIC NUMBER", client.magicNumber) 17 | ) 18 | 19 | private def debugView[A](name: String, effect: => UIO[A]): Div = { 20 | val output = Var(List.empty[String]) 21 | div( 22 | h3(name), 23 | children <-- output.signal.map { strings => 24 | strings.map(div(_)) 25 | }, 26 | onClick --> { _ => 27 | runtime.unsafeRunAsync_ { 28 | effect.tap { a => ZIO.succeed(output.update(_.prepended(a.toString))) } 29 | } 30 | } 31 | ) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /cli/src/main/g8/backend/src/main/scala/$package$/Backend.scala: -------------------------------------------------------------------------------- 1 | package $package$ 2 | 3 | import $package$.protocol.{ExampleService} 4 | import zio._ 5 | import zio.app.DeriveRoutes 6 | import zio.console._ 7 | import zio.magic._ 8 | 9 | object Backend extends App { 10 | private val httpApp = 11 | DeriveRoutes.gen[ExampleService] 12 | 13 | val program = for { 14 | port <- system.envOrElse("PORT", "8088").map(_.toInt).orElseSucceed(8088) 15 | _ <- zio.service.Server.start(port, httpApp) 16 | } yield () 17 | 18 | override def run(args: List[String]): URIO[zio.ZEnv, ExitCode] = { 19 | program 20 | .injectCustom(ExampleServiceLive.layer) 21 | .exitCode 22 | } 23 | } 24 | 25 | case class ExampleServiceLive(random: zio.random.Random.Service) extends ExampleService { 26 | override def magicNumber: UIO[Int] = random.nextInt 27 | } 28 | 29 | object ExampleServiceLive { 30 | val layer = (ExampleServiceLive.apply _).toLayer[ExampleService] 31 | } 32 | -------------------------------------------------------------------------------- /cli/src/main/scala/tui/components/LineInput.scala: -------------------------------------------------------------------------------- 1 | package tui.components 2 | 3 | import view.View.string2View 4 | import view.{KeyEvent, View} 5 | import tui.{TerminalApp, TerminalEvent} 6 | import tui.TerminalApp.Step 7 | 8 | object LineInput extends TerminalApp[Any, String, String] { 9 | override def render(state: String): View = 10 | View.horizontal("-> ".bold.green, state.bold, View.text(" ").reversed) 11 | 12 | override def update(state: String, event: TerminalEvent[Any]): Step[String, String] = { 13 | event match { 14 | case TerminalEvent.SystemEvent(KeyEvent.Character(c)) => Step.update(state + c) 15 | case TerminalEvent.SystemEvent(KeyEvent.Delete) => Step.update(state.dropRight(1)) 16 | case TerminalEvent.SystemEvent(KeyEvent.Exit) => Step.exit 17 | case TerminalEvent.SystemEvent(KeyEvent.Enter) => Step.succeed(state) 18 | case _ => Step.update(state) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/jvm/src/main/scala/examples/Backend.scala: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import examples.services.{ExampleServiceLive, ParameterizedServiceLive} 4 | import zio.http._ 5 | import zio._ 6 | import zio.app.DeriveRoutes 7 | 8 | case class Person(name: String, age: Int) 9 | case class Dog(name: String, age: Int) 10 | 11 | object Backend extends ZIOAppDefault { 12 | val httpApp: HttpApp[ExampleService with ParameterizedService[Int], Throwable] = 13 | DeriveRoutes.gen[ExampleService] ++ DeriveRoutes.gen[ParameterizedService[Int]] 14 | 15 | override def run = (for { 16 | port <- System.envOrElse("PORT", "8088").map(_.toInt).orElseSucceed(8088) 17 | _ <- Console.printLine(s"STARTING SERVER ON PORT $port") 18 | _ <- Server 19 | .serve(httpApp) 20 | .provideSome[ExampleService with ParameterizedService[Int]]( 21 | ServerConfig.live(ServerConfig.default.port(port)), 22 | Server.live 23 | ) 24 | } yield ()) 25 | .provide( 26 | ExampleServiceLive.layer, 27 | ParameterizedServiceLive.layer 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /cli-shared/src/main/scala/zio/app/cli/protocol/ServerCommand.scala: -------------------------------------------------------------------------------- 1 | package zio.app.cli.protocol 2 | 3 | import zio.Chunk 4 | 5 | case class FileSystemState(pwd: String, dirs: List[String]) 6 | 7 | sealed trait ServerCommand 8 | 9 | object ServerCommand { 10 | case class State( 11 | backendLines: Chunk[Line], 12 | frontendLines: Chunk[Line], 13 | fileSystemState: FileSystemState 14 | ) extends ServerCommand 15 | } 16 | 17 | sealed trait ClientCommand 18 | 19 | object ClientCommand { 20 | case object Subscribe extends ClientCommand 21 | case class ChangeDirectory(path: String) extends ClientCommand 22 | } 23 | 24 | case class Fragment(string: String, attributes: Chunk[Attribute]) 25 | case class Line(fragments: Chunk[Fragment]) 26 | 27 | sealed trait Attribute 28 | 29 | object Attribute { 30 | case object Red extends Attribute 31 | case object Yellow extends Attribute 32 | case object Blue extends Attribute 33 | case object Green extends Attribute 34 | case object Magenta extends Attribute 35 | case object Cyan extends Attribute 36 | case object Bold extends Attribute 37 | } 38 | -------------------------------------------------------------------------------- /examples/jvm/src/main/scala/examples/services/ParameterizedServiceLive.scala: -------------------------------------------------------------------------------- 1 | package examples.services 2 | 3 | import examples.ParameterizedService 4 | import examples.ParameterizedService.{Foo, FooId} 5 | import zio._ 6 | 7 | case class ParameterizedServiceLive(ref: Ref[Map[FooId[Int], Foo[Int]]]) extends ParameterizedService[Int] { 8 | import examples.ParameterizedService._ 9 | 10 | override def getAll: Task[List[Foo[Int]]] = 11 | ref.get.map(_.values.toList) 12 | 13 | override def create(m: CreateFoo): Task[Unit] = { 14 | val fooId = FooId(scala.util.Random.nextInt) 15 | val newFoo = Foo(fooId, m.name, m.content, m.tags) 16 | ref.update(_.updated(fooId, newFoo)) 17 | } 18 | 19 | override def update(m: UpdateFoo[Int]): Task[Unit] = 20 | ref.update(_.updated(m.id, Foo(m.id, m.name, m.content, m.tags))) 21 | 22 | override def delete(id: FooId[Int]): Task[Unit] = 23 | ref.update(_.removed(id)) 24 | } 25 | 26 | object ParameterizedServiceLive { 27 | val layer: ULayer[ParameterizedService[Int]] = 28 | ZLayer { 29 | Ref 30 | .make(Map.empty[FooId[Int], Foo[Int]]) 31 | .map { ref => 32 | ParameterizedServiceLive(ref) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/pickleparty.scala: -------------------------------------------------------------------------------- 1 | package io.laminext.websocket 2 | 3 | import _root_.boopickle.Default._ 4 | import org.scalajs.dom 5 | 6 | import java.nio.ByteBuffer 7 | import scala.scalajs.js.typedarray.{ArrayBuffer, TypedArrayBuffer} 8 | 9 | object PickleSocket { 10 | import scala.scalajs.js.typedarray.TypedArrayBufferOps._ 11 | 12 | implicit class WebSocketReceiveBuilderBooPickleOps(b: WebSocketReceiveBuilder) { 13 | @inline def pickle[Receive, Send](implicit 14 | receiveDecoder: Pickler[Receive], 15 | sendEncoder: Pickler[Send] 16 | ): WebSocketBuilder[Receive, Send] = 17 | new WebSocketBuilder[Receive, Send]( 18 | url = b.url, 19 | protocol = "ws", 20 | initializer = initialize.arraybuffer, 21 | sender = { (webSocket: dom.WebSocket, a: Send) => 22 | val bytes: ByteBuffer = Pickle.intoBytes(a) 23 | val buffer = bytes.arrayBuffer() 24 | send.arraybuffer.apply(webSocket, buffer) 25 | }, 26 | receiver = { msg => 27 | Right(Unpickle[Receive].fromBytes(TypedArrayBuffer.wrap(msg.data.asInstanceOf[ArrayBuffer]))) 28 | } 29 | ) 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /cli/src/main/scala/tui/components/FancyComponent.scala: -------------------------------------------------------------------------------- 1 | package tui.components 2 | 3 | import view.View.string2View 4 | import view.{KeyEvent, View} 5 | import tui.TerminalApp.Step 6 | import tui.{TerminalApp, TerminalEvent} 7 | 8 | object FancyComponent extends TerminalApp[Int, Int, Nothing] { 9 | override def render(state: Int): View = 10 | View 11 | .vertical( 12 | View.horizontal("YOUR NUMBER IS".bold, " ", state.toString.red), 13 | View.horizontal("YOUR NUMBER IS".underlined, " ", state.toString.cyan), 14 | View.horizontal("YOUR NUMBER IS".inverted, " ", state.toString.yellow) 15 | ) 16 | .padding((Math.sin(state.toDouble / 300) * 20).toInt.abs, 0) 17 | .bordered 18 | 19 | override def update(state: Int, event: TerminalEvent[Int]): Step[Int, Nothing] = { 20 | event match { 21 | case TerminalEvent.UserEvent(int) => Step.update(int) 22 | case TerminalEvent.SystemEvent(KeyEvent.Up) => Step.update(state + 1) 23 | case TerminalEvent.SystemEvent(KeyEvent.Down) => Step.update(state - 1) 24 | case TerminalEvent.SystemEvent(KeyEvent.Exit) => Step.exit 25 | case _ => Step.update(state) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cli/src/main/g8/vite.config.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path' 2 | import {minifyHtml, injectHtml} from 'vite-plugin-html' 3 | 4 | const scalaVersion = '2.13' 5 | // const scalaVersion = '3.0.0-RC3' 6 | 7 | // https://vitejs.dev/config/ 8 | export default ({mode}) => { 9 | const mainJS = `./frontend/target/scala-${scalaVersion}/frontend-${mode === 'production' ? 'opt' : 'fastopt'}/main.js` 10 | const script = `` 11 | 12 | return { 13 | server: { 14 | proxy: { 15 | '/api': { 16 | target: 'http://localhost:8088', 17 | changeOrigin: true, 18 | rewrite: (path) => path.replace(/^\/api/, '') 19 | }, 20 | } 21 | }, 22 | publicDir: './src/main/static/public', 23 | plugins: [ 24 | ...(process.env.NODE_ENV === 'production' ? [minifyHtml(),] : []), 25 | injectHtml({ 26 | injectData: { 27 | script 28 | } 29 | }) 30 | ], 31 | resolve: { 32 | alias: { 33 | 'stylesheets': resolve(__dirname, './frontend/src/main/static/stylesheets'), 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /examples/vite.config.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path' 2 | import {minifyHtml, injectHtml} from 'vite-plugin-html' 3 | 4 | const scalaVersion = '2.13' 5 | // const scalaVersion = '3.0.0-RC3' 6 | 7 | 8 | // https://vitejs.dev/config/ 9 | export default ({mode}) => { 10 | const mainJS = `./js/target/scala-${scalaVersion}/zio-app-examples-${mode === 'production' ? 'opt' : 'fastopt'}/main.js` 11 | const script = `` 12 | 13 | return { 14 | server: { 15 | proxy: { 16 | '/api': { 17 | target: 'http://localhost:8088', 18 | changeOrigin: true, 19 | rewrite: (path) => path.replace(/^\/api/, '') 20 | }, 21 | } 22 | }, 23 | // publicDir: './src/main/static/public', 24 | plugins: [ 25 | ...(process.env.NODE_ENV === 'production' ? [minifyHtml(),] : []), 26 | injectHtml({ 27 | injectData: { 28 | script 29 | } 30 | }) 31 | ], 32 | resolve: { 33 | alias: { 34 | 'stylesheets': resolve(__dirname, './frontend/src/main/static/stylesheets'), 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /cli-frontend/vite.config.js: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path' 2 | import {minifyHtml, injectHtml} from 'vite-plugin-html' 3 | 4 | const scalaVersion = '2.13' 5 | // const scalaVersion = '3.0.0-RC3' 6 | 7 | // https://vitejs.dev/config/ 8 | export default ({mode}) => { 9 | const mainJS = `./target/scala-${scalaVersion}/clifrontend-${mode === 'production' ? 'opt' : 'fastopt'}/main.js` 10 | const script = `` 11 | 12 | return { 13 | build: { 14 | outDir: '../cli/src/main/resources/dist', 15 | emptyOutDir: true 16 | }, 17 | server: { 18 | port: 4000, 19 | proxy: { 20 | '/api': { 21 | target: 'http://localhost:9630', 22 | changeOrigin: true, 23 | rewrite: (path) => path.replace(/^\/api/, '') 24 | }, 25 | } 26 | }, 27 | // publicDir: './src/main/static/public', 28 | plugins: [ 29 | ...(process.env.NODE_ENV === 'production' ? [minifyHtml(),] : []), 30 | injectHtml({ 31 | injectData: { 32 | script 33 | } 34 | }) 35 | ], 36 | resolve: { 37 | alias: { 38 | 'stylesheets': resolve(__dirname, './src/main/static/stylesheets'), 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /examples/jvm/src/main/scala/examples/services/ExampleServiceLive.scala: -------------------------------------------------------------------------------- 1 | package examples.services 2 | 3 | import examples.{Event, ExampleService} 4 | import zio._ 5 | import zio.stream.{Stream, ZStream} 6 | 7 | case class ExampleServiceLive() extends ExampleService { 8 | override def magicNumber: UIO[Int] = 9 | for { 10 | int <- Random.nextInt 11 | _ <- Console.printLine(s"GENERATED: $int").orDie 12 | } yield int 13 | 14 | val eventTypes = Vector( 15 | "POST", 16 | "PATCH", 17 | "MURDER", 18 | "MARRY", 19 | "INVERT", 20 | "REASSOCIATE", 21 | "DISASSOCIATE", 22 | "DEFACTOR" 23 | ) 24 | 25 | val randomEventType: UIO[String] = Random.shuffle(eventTypes.toList).map(_.head) 26 | 27 | var i = -9999999 28 | val event = randomEventType 29 | .zipWith(Random.nextInt)(Event(_, _)) 30 | .filterOrFail(_.timestamp % 13 != 0)(9999) 31 | .debug("EVENT") 32 | 33 | override def eventStream: Stream[Int, Event] = 34 | ZStream.fromZIO(event) ++ ZStream.repeatZIO(event.delay(100.millis)) 35 | 36 | override def attemptToProcess(event: Event): IO[String, Int] = { 37 | val int = event.timestamp.toInt 38 | if (int % 2 == 0) ZIO.fail(s"$int WAS EVEN! UNACCEPTABLE") 39 | else ZIO.succeed(int) 40 | } 41 | 42 | override def unit: UIO[Unit] = ZIO.unit 43 | } 44 | 45 | object ExampleServiceLive { 46 | 47 | val layer: ULayer[ExampleService] = ZLayer.succeed(ExampleServiceLive()) 48 | 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/main/g8/frontend/src/main/scala/$package$/Main.scala: -------------------------------------------------------------------------------- 1 | package $package$ 2 | 3 | import com.raquo.laminar.api.L._ 4 | import org.scalajs.dom 5 | 6 | import scala.scalajs.js 7 | import scala.scalajs.js.`import` 8 | import scala.scalajs.js.annotation.JSImport 9 | 10 | @js.native 11 | @JSImport("stylesheets/main.scss", JSImport.Namespace) 12 | object Css extends js.Any 13 | 14 | object Main { 15 | val css: Css.type = Css 16 | 17 | def main(args: Array[String]): Unit = 18 | waitForLoad { 19 | val appContainer = dom.document.querySelector("#app") 20 | appContainer.innerHTML = "" 21 | unmount() 22 | val rootNode = render(appContainer, Frontend.view) 23 | storeUnmount(rootNode) 24 | } 25 | 26 | def waitForLoad(f: => Any): Unit = 27 | if (dom.window.asInstanceOf[js.Dynamic].documentLoaded == null) 28 | documentEvents.onDomContentLoaded.foreach { _ => 29 | dom.window.asInstanceOf[js.Dynamic].documentLoaded = true 30 | f 31 | }(unsafeWindowOwner) 32 | else 33 | f 34 | 35 | def unmount(): Unit = 36 | if (scala.scalajs.LinkingInfo.developmentMode) { 37 | Option(dom.window.asInstanceOf[js.Dynamic].__laminar_root_unmount) 38 | .collect { 39 | case x if !js.isUndefined(x) => 40 | x.asInstanceOf[js.Function0[Unit]] 41 | } 42 | .foreach { _.apply() } 43 | } 44 | 45 | def storeUnmount(rootNode: RootNode): Unit = { 46 | val unmountFunction: js.Function0[Any] = () => {rootNode.unmount()} 47 | dom.window.asInstanceOf[js.Dynamic].__laminar_root_unmount = unmountFunction 48 | } 49 | 50 | if (!js.isUndefined(`import`.meta.hot) && !js.isUndefined(`import`.meta.hot.accept)) { 51 | `import`.meta.hot.accept { (_: Any) => } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/FileSystemService.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio._ 4 | import zio.app.cli.protocol.FileSystemState 5 | import zio.nio.file.{Files, Path} 6 | import zio.stream._ 7 | 8 | trait FileSystemService { 9 | def stateStream: UStream[FileSystemState] 10 | def cd(string: String): UIO[Unit] 11 | } 12 | 13 | object FileSystemService { 14 | def stateStream: ZStream[FileSystemService, Nothing, FileSystemState] = 15 | ZStream.environmentWithStream[FileSystemService](_.get.stateStream) 16 | 17 | def cd(string: String): ZIO[FileSystemService, Nothing, Unit] = 18 | ZIO.serviceWith[FileSystemService](_.cd(string)) 19 | 20 | val live: ZLayer[Any, Throwable, FileSystemService] = ZLayer { 21 | for { 22 | pwd <- System.property("user.dir").map(_.getOrElse(".")) 23 | paths <- Files.list(Path(pwd)).runCollect.orDie 24 | ref <- SubscriptionRef.make(FileSystemState(pwd, paths.map(_.toString).toList)) 25 | } yield FileSystemServiceLive(ref) 26 | } 27 | 28 | case class FileSystemServiceLive( 29 | ref: SubscriptionRef[FileSystemState] 30 | ) extends FileSystemService { 31 | override def stateStream: UStream[FileSystemState] = ref.changes 32 | 33 | override def cd(directory: String): UIO[Unit] = 34 | for { 35 | paths <- Files.list(Path(directory)).runCollect.orDie 36 | _ <- ref.set(FileSystemState(directory, paths.toList.map(_.toString))) 37 | } yield () 38 | } 39 | } 40 | 41 | object FSExample extends ZIOAppDefault { 42 | override def run = 43 | (for { 44 | f <- FileSystemService.stateStream.foreach(state => ZIO.succeed(println(state))).fork 45 | _ <- FileSystemService.cd("zio-app-cli-frontend") 46 | _ <- f.join 47 | } yield ()) 48 | .provide(FileSystemService.live.orDie) 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/main/scala/tui/components/Choose.scala: -------------------------------------------------------------------------------- 1 | package tui.components 2 | 3 | import zio._ 4 | import view.View.string2View 5 | import view.{KeyEvent, View} 6 | import tui.TerminalApp.Step 7 | import tui.{TUI, TerminalApp, TerminalEvent} 8 | 9 | case class Choose[A](renderA: A => View) extends TerminalApp[Any, Choose.State[A], A] { 10 | override def render(state: Choose.State[A]): View = { 11 | val renderedViews = state.options.zipWithIndex.map { case (option, idx) => 12 | val cursor = 13 | if (state.index == idx) "> ".green.bold 14 | else View.text(" ") 15 | View.horizontal(cursor, renderA(option)) 16 | } 17 | 18 | View 19 | .vertical( 20 | ("CHOOSE".green :: renderedViews): _* 21 | ) 22 | 23 | } 24 | 25 | override def update(state: Choose.State[A], event: TerminalEvent[Any]): Step[Choose.State[A], A] = { 26 | event match { 27 | case TerminalEvent.SystemEvent(KeyEvent.Up) => Step.update(state.moveUp) 28 | case TerminalEvent.SystemEvent(KeyEvent.Down) => Step.update(state.moveDown) 29 | case TerminalEvent.SystemEvent(KeyEvent.Enter) => Step.succeed(state.current) 30 | case TerminalEvent.SystemEvent(KeyEvent.Exit) => Step.exit 31 | case _ => Step.update(state) 32 | } 33 | } 34 | } 35 | 36 | object Choose { 37 | def run[A](options: List[A])(render: A => View): RIO[TUI, Option[A]] = { 38 | val value = Choose[A](render) 39 | TUI.run(value)(State(options)) 40 | } 41 | 42 | case class State[A](options: List[A], index: Int = 0) { 43 | def current: A = options(index) 44 | def moveUp: State[A] = changeIndex(-1) 45 | def moveDown: State[A] = changeIndex(1) 46 | 47 | def changeIndex(delta: Int): State[A] = copy(index = (index + delta) max 0 min (options.length - 1)) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/DevMode.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio.process.{Command, ProcessInput} 4 | import zio.stream.{Stream, ZStream} 5 | import zio._ 6 | 7 | object DevMode { 8 | val launchVite = Command("yarn", "exec", "vite") 9 | .stdin(ProcessInput.fromStream(ZStream.empty)) 10 | 11 | val backendLines: Stream[Throwable, String] = 12 | runSbtCommand("~ backend/reStart") 13 | 14 | val frontendLines: Stream[Throwable, String] = 15 | ZStream.succeed("") ++ 16 | ZStream.succeed(ZIO.sleep(350.millis)).drain ++ 17 | runSbtCommand("~ frontend/fastLinkJS") 18 | 19 | def runSbtCommand(command: String): Stream[SbtError, String] = 20 | ZStream 21 | .unwrap( 22 | for { 23 | process <- Command("sbt", command, "--color=always").run 24 | .tap(_.exitCode.fork) 25 | errorStream = ZStream 26 | .fromZIO(process.stderr.lines.flatMap { lines => 27 | val errorString = lines.mkString 28 | if (errorString.contains("waiting for lock")) 29 | ZIO.fail(SbtError.WaitingForLock) 30 | else if (errorString.contains("Invalid commands")) 31 | ZIO.fail(SbtError.InvalidCommand(s"sbt $command")) 32 | else { 33 | println(s"ERRRRRRR ${errorString}") 34 | ZIO.fail(SbtError.SbtErrorMessage(errorString)) 35 | } 36 | }) 37 | } yield ZStream.mergeAllUnbounded()( 38 | ZStream.succeed(s"sbt $command"), 39 | process.stdout.linesStream, 40 | errorStream 41 | ) 42 | ) 43 | .catchSome { case SbtError.WaitingForLock => runSbtCommand(command) } 44 | .refineToOrDie[SbtError] 45 | 46 | } 47 | -------------------------------------------------------------------------------- /examples/shared/src/main/scala/examples/ExampleService.scala: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import examples.ParameterizedService.{CreateFoo, FooId, UpdateFoo} 4 | import zio.{IO, Task, UIO} 5 | import zio.stream._ 6 | 7 | case class Event(description: String, timestamp: Long) 8 | 9 | trait ExampleService { 10 | def magicNumber: UIO[Int] 11 | def attemptToProcess(event: Event): IO[String, Int] 12 | def eventStream: Stream[Int, Event] 13 | // TODO: Support default implementations 14 | def unit: UIO[Unit] // = UIO.unit 15 | } 16 | // HttpApp.collectZIO { 17 | // case Method.GET -> !! / "api" / "examples.ExampleService" / "magicNumber" => 18 | // ZIO.serviceWith[ExampleService].map(_.magicNumber) 19 | // case req @ Method.POST -> !! / "api" / "examples.ExampleService" / "attemptToProcess" => 20 | // ZIO.serviceWith[ExampleService](_.attemptToProcess(req.body.as[Event])) 21 | // 22 | 23 | trait ParameterizedService[T] { 24 | def getAll: Task[List[ParameterizedService.Foo[T]]] 25 | def create(m: CreateFoo): Task[Unit] 26 | def update(m: UpdateFoo[T]): Task[Unit] 27 | def delete(id: FooId[T]): Task[Unit] 28 | } 29 | 30 | // HttpApp.collectZIO { 31 | // case req @ Method.POST -> !! / "api" / "examples.ParameterizedService[Int]" / "attemptToProcess" => 32 | // ZIO.serviceWith[ParameterizedService[Int]](_.delete(req.body.as[FooId[Int]])) 33 | // 34 | 35 | object ParameterizedService { 36 | final case class Foo[T]( 37 | id: FooId[T], 38 | name: String, 39 | content: String, 40 | tags: List[String] 41 | ) 42 | 43 | final case class CreateFoo( 44 | name: String, 45 | content: String, 46 | tags: List[String] 47 | ) 48 | 49 | final case class UpdateFoo[T]( 50 | id: FooId[T], 51 | name: String, 52 | content: String, 53 | tags: List[String] 54 | ) 55 | 56 | final case class FooId[T]( 57 | value: T 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/Alignment.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | case class Coord(x: Int, y: Int) 4 | 5 | case class Alignment(horizontalAlignment: HorizontalAlignment, verticalAlignment: VerticalAlignment) { 6 | 7 | def point(size: Size): Coord = 8 | Coord( 9 | horizontalAlignment.point(size.width), 10 | verticalAlignment.point(size.height) 11 | ) 12 | } 13 | 14 | object Alignment { 15 | val top: Alignment = Alignment(HorizontalAlignment.Center, VerticalAlignment.Top) 16 | val bottom: Alignment = Alignment(HorizontalAlignment.Center, VerticalAlignment.Bottom) 17 | val center: Alignment = Alignment(HorizontalAlignment.Center, VerticalAlignment.Center) 18 | val left: Alignment = Alignment(HorizontalAlignment.Left, VerticalAlignment.Center) 19 | val right: Alignment = Alignment(HorizontalAlignment.Right, VerticalAlignment.Center) 20 | val topLeft: Alignment = Alignment(HorizontalAlignment.Left, VerticalAlignment.Top) 21 | val topRight: Alignment = Alignment(HorizontalAlignment.Right, VerticalAlignment.Top) 22 | val bottomLeft: Alignment = Alignment(HorizontalAlignment.Left, VerticalAlignment.Bottom) 23 | val bottomRight: Alignment = Alignment(HorizontalAlignment.Right, VerticalAlignment.Bottom) 24 | } 25 | 26 | sealed trait HorizontalAlignment { self => 27 | def point(width: Int): Int = self match { 28 | case HorizontalAlignment.Left => 0 29 | case HorizontalAlignment.Center => width / 2 30 | case HorizontalAlignment.Right => width 31 | } 32 | } 33 | 34 | object HorizontalAlignment { 35 | case object Left extends HorizontalAlignment 36 | case object Center extends HorizontalAlignment 37 | case object Right extends HorizontalAlignment 38 | } 39 | 40 | sealed trait VerticalAlignment { self => 41 | def point(height: Int): Int = self match { 42 | case VerticalAlignment.Top => 0 43 | case VerticalAlignment.Center => height / 2 44 | case VerticalAlignment.Bottom => height 45 | } 46 | } 47 | 48 | object VerticalAlignment { 49 | case object Top extends VerticalAlignment 50 | case object Center extends VerticalAlignment 51 | case object Bottom extends VerticalAlignment 52 | } 53 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/TemplateGenerator.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio._ 4 | import zio.nio.file.Files.delete 5 | import zio.nio.file.{Files, Path} 6 | import zio.process.Command 7 | import zio.stream.{ZSink, ZStream} 8 | 9 | import java.io.IOException 10 | import java.nio.file.{Files => JFiles} 11 | import java.util.Comparator 12 | 13 | object TemplateGenerator { 14 | 15 | def newRecursiveDirectoryStream(dir: Path): ZStream[Any, Throwable, Path] = { 16 | val managed = ZIO.fromAutoCloseable( 17 | ZIO 18 | .attemptBlocking(JFiles.walk(dir.toFile.toPath).sorted(Comparator.reverseOrder[java.nio.file.Path]())) 19 | .refineToOrDie[IOException] 20 | ) 21 | ZStream 22 | .scoped(managed) 23 | .mapZIO(dirStream => ZIO.succeed(dirStream.iterator())) 24 | .flatMap(a => ZStream.fromJavaIterator(a)) 25 | .map(Path.fromJava(_)) 26 | } 27 | 28 | def deleteMoreRecursive(path: Path): ZIO[Any, Throwable, Long] = 29 | newRecursiveDirectoryStream(path).mapZIO(delete).run(ZSink.count) 30 | 31 | def cloneRepo: ZIO[Any, Exception, Path] = for { 32 | tempdir <- ZIO.succeed(Path("./.zio-app-g8")) 33 | _ <- deleteMoreRecursive(tempdir).whenZIO(Files.exists(tempdir)).orDie 34 | _ <- Files.createDirectory(tempdir).orDie 35 | _ <- Command("git", "clone", "https://github.com/kitlangton/zio-app") 36 | .workingDirectory(tempdir.toFile) 37 | .successfulExitCode 38 | templateDir = tempdir / "zio-app/cli/src/main/g8" 39 | } yield templateDir 40 | 41 | val DIM = "\u001b[2m" 42 | 43 | def execute: ZIO[Any, Exception, String] = { 44 | for { 45 | cloneFiber <- cloneRepo.fork 46 | _ <- Console.printLine( 47 | scala.Console.BOLD + scala.Console.GREEN + "? " + scala.Console.WHITE + "Project Name" + scala.Console.RESET + DIM + " (example) " + scala.Console.RESET 48 | ) 49 | name <- Console.readLine.filterOrElseWith(_.nonEmpty)(_ => ZIO.succeed("example")).map(_.trim) 50 | templateDir <- cloneFiber.join 51 | _ <- Files.move(templateDir, Path(s"./$name")) 52 | _ <- Renamer.rename(Path(s"./$name"), name).provideLayer(Renamer.live) 53 | kebabCased = name.split(" ").mkString("-").toLowerCase 54 | } yield kebabCased 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cli/src/main/scala/database/ast/ParsingApp.scala: -------------------------------------------------------------------------------- 1 | //package database.ast 2 | // 3 | //import com.sun.nio.file.SensitivityWatchEventModifier 4 | //import zio._ 5 | //import zio.nio.file.{FileSystem, Path, WatchKey, WatchService} 6 | // 7 | //import java.io.IOException 8 | //import java.nio.file.{StandardWatchEventKinds, WatchEvent} 9 | // 10 | //final case class Watcher(private val watchService: WatchService) { 11 | // val allWatchEvents: List[WatchEvent.Kind[_]] = 12 | // List( 13 | // StandardWatchEventKinds.ENTRY_CREATE, 14 | // StandardWatchEventKinds.ENTRY_DELETE, 15 | // StandardWatchEventKinds.ENTRY_MODIFY 16 | // ) 17 | // 18 | // def watch(path: Path): IO[IOException, WatchKey] = 19 | // path.register(watchService, allWatchEvents, SensitivityWatchEventModifier.HIGH) 20 | // 21 | // private val examplePath = 22 | // Path("/Users", "kit/code/zio-app/cli/src/main/resources/".split("/").toList: _*) 23 | // 24 | // println(examplePath) 25 | // 26 | // def start: ZIO[Any, IOException, Unit] = 27 | // for { 28 | // _ <- watch(examplePath) 29 | // _ <- watchService.stream.foreach { watchKey => 30 | // for { 31 | // _ <- ZIO.debug(s"WatchKey: ${watchKey.toString}") 32 | // events <- ZIO.scoped(watchKey.pollEventsScoped) 33 | // _ = events.map { event => println(event.kind) } 34 | // _ <- parse 35 | // } yield () 36 | // } 37 | // } yield () 38 | // 39 | // private def parse: ZIO[Any, IOException, Unit] = 40 | // for { 41 | // content <- ZIO.attemptBlockingIO( 42 | // scala.io.Source.fromFile("/Users/kit/code/zio-app/cli/src/main/resources/Schema.sql").mkString 43 | // ) 44 | // parsed = SqlSyntax.createTable.parseString(content.trim) 45 | // _ <- ZIO.debug(content) 46 | // _ <- ZIO.debug(parsed) 47 | // } yield () 48 | //} 49 | // 50 | //object Watcher { 51 | // 52 | // val live: ZLayer[Any, Nothing, Watcher] = 53 | // ZLayer.scoped(FileSystem.default.newWatchService.orDie) >>> ZLayer.fromFunction(Watcher.apply _) 54 | //} 55 | // 56 | //object ParsingApp extends ZIOAppDefault { 57 | // 58 | // val run = { 59 | // for { 60 | // watcher <- ZIO.service[Watcher] 61 | // _ <- watcher.start 62 | // } yield watcher 63 | // }.provide(Watcher.live) 64 | // 65 | //} 66 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/Renamer.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import zio.nio.file.{Files, Path} 4 | import zio._ 5 | 6 | import java.io.IOException 7 | 8 | trait Renamer { 9 | def renameFolders(path: Path): IO[IOException, Unit] 10 | 11 | def renameFiles(path: Path, name: String): IO[IOException, Unit] 12 | 13 | def renameFile(path: Path, name: String): IO[IOException, Unit] 14 | 15 | def printTree(path: Path): IO[IOException, Unit] 16 | } 17 | 18 | object Renamer { 19 | val live: URLayer[Any, Renamer] = ZLayer.succeed(RenamerLive()) 20 | 21 | def rename(path: Path, name: String): ZIO[Renamer, IOException, Unit] = 22 | renameFolders(path) *> renameFiles(path, name) 23 | 24 | def renameFolders(path: Path): ZIO[Renamer, IOException, Unit] = 25 | ZIO.serviceWith[Renamer](_.renameFolders(path)) 26 | 27 | def renameFiles(path: Path, name: String): ZIO[Renamer, IOException, Unit] = 28 | ZIO.serviceWith[Renamer](_.renameFiles(path, name)) 29 | 30 | def printTree(path: Path): ZIO[Renamer, IOException, Unit] = 31 | ZIO.serviceWith[Renamer](_.printTree(path)) 32 | } 33 | 34 | case class RenamerLive() extends Renamer { 35 | override def renameFolders(path: Path): IO[IOException, Unit] = 36 | Files 37 | .walk(path) 38 | .filterZIO { path => Files.isDirectory(path).map { _ && path.endsWith(Path("$package$")) } } 39 | .runCollect 40 | .flatMap { paths => 41 | ZIO.foreachDiscard(paths) { path => 42 | val newPath = path.toString().replace("$package$", "example") 43 | Files.move(path, Path(newPath)) 44 | } 45 | } 46 | 47 | override def renameFiles(path: Path, name: String): IO[IOException, Unit] = 48 | Files 49 | .walk(path) 50 | .filterZIO(Files.isRegularFile(_)) 51 | .foreach(renameFile(_, name)) 52 | 53 | override def renameFile(path: Path, name: String): IO[IOException, Unit] = (for { 54 | lines <- Files.readAllLines(path).map(_.mkString("\n")) 55 | newLines = lines 56 | .replace("$package$", "example") 57 | .replace("$name$", name) 58 | .replace("$description$", "A full-stack Scala application powered by ZIO and Laminar.") 59 | _ <- Files.writeLines(path, newLines.split("\n")) 60 | } yield ()) 61 | 62 | override def printTree(path: Path): IO[IOException, Unit] = 63 | Files 64 | .walk(path) 65 | .foreach { path => 66 | ZIO.whenZIO(Files.isDirectory(path)) { 67 | ZIO.succeed(println(path)) 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /examples/js/src/main/scala/examples/Frontend.scala: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | import com.raquo.airstream.split.Splittable 4 | import com.raquo.laminar.api.L._ 5 | import examples.ParameterizedService.CreateFoo 6 | import org.scalajs.dom 7 | import zio._ 8 | import zio.app.DeriveClient 9 | 10 | object Frontend { 11 | def main(args: Array[String]): Unit = { 12 | val _ = documentEvents.onDomContentLoaded.foreach { _ => 13 | val appContainer = dom.document.querySelector("#app") 14 | appContainer.innerHTML = "" 15 | val _ = render(appContainer, view) 16 | }(unsafeWindowOwner) 17 | } 18 | 19 | val runtime = zio.Runtime.default 20 | val exampleClient: ExampleService = DeriveClient.gen[ExampleService] 21 | val fooClient: ParameterizedService[Int] = DeriveClient.gen[ParameterizedService[Int]] 22 | 23 | val events: Var[Vector[String]] = Var(Vector.empty) 24 | 25 | def view: Div = 26 | div( 27 | // beginStream, 28 | debugView("Magic Number", exampleClient.magicNumber), 29 | debugView("Unit", exampleClient.unit), 30 | debugView("All Foos", fooClient.getAll), 31 | debugView("Create Foo", fooClient.create(CreateFoo("New Foo", "Some String", List("tag", "another")))), 32 | children <-- events.signal.map(_.zipWithIndex.reverse).split(_._2) { (_, event, _) => 33 | div(event._1) 34 | } 35 | ) 36 | 37 | val beginStream: Modifier[Element] = onMountCallback { _ => 38 | Unsafe.unsafe { implicit u => 39 | runtime.unsafe.fork { 40 | exampleClient.eventStream 41 | .retry(Schedule.spaced(1.second)) 42 | .foreach { event => 43 | println(s"RECEIVED: $event") 44 | ZIO.succeed(events.update(_.appended(event.toString))) 45 | } 46 | } 47 | } 48 | } 49 | 50 | private def debugView[E, A](name: String, effect: => IO[E, A]): Div = { 51 | val output = Var(List.empty[String]) 52 | div( 53 | h3(name), 54 | children <-- output.signal.map { strings => 55 | strings.map(div(_)) 56 | }, 57 | onClick --> { _ => 58 | Unsafe.unsafe { implicit u => 59 | runtime.unsafe.fork { 60 | effect.tap(a => ZIO.succeed(output.update(_.prepended(a.toString)))) 61 | } 62 | } 63 | } 64 | ) 65 | } 66 | 67 | implicit val chunkSplittable: Splittable[Chunk] = new Splittable[Chunk] { 68 | override def map[A, B](inputs: Chunk[A], project: A => B): Chunk[B] = inputs.map(project) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zio-app 2 | 3 | [![Release Artifacts][Badge-SonatypeReleases]][Link-SonatypeReleases] 4 | [![Snapshot Artifacts][Badge-SonatypeSnapshots]][Link-SonatypeSnapshots] 5 | 6 | Quickly create and develop full-stack Scala apps with ZIO and Laminar. 7 | 8 | ## Installation 9 | 10 | **Via Homebrew** 11 | 12 | ```sh 13 | brew tap kitlangton/zio-app 14 | brew install zio-app 15 | ``` 16 | 17 | **Via Source** 18 | 19 | ```sh 20 | git clone https://github.com/kitlangton/zio-app.git 21 | cd zio-app 22 | sbt cli/nativeImage 23 | ``` 24 | 25 | ## Usage 26 | 27 | 1. Create a new project. 28 | 29 | ```sh 30 | zio-app new 31 | 32 | # Configure your new ZIO app. 33 | # ? Project Name (example) zio-app-example 34 | 35 | cd zio-app-example 36 | ``` 37 | 38 | 2. Launch file-watching compilation and hot-reloading dev server: 39 | 40 | ```sh 41 | zio-app dev 42 | 43 | # Launches: 44 | ┌───────────────────────────────────────────────────────────┐ 45 | │ zio-app running at http://localhost:3000 │ 46 | └───────────────────────────INFO────────────────────────────┘ 47 | ┌────────────────────────────┐┌─────────────────────────────┐ 48 | │ ││ │ 49 | │ ││ │ 50 | │[info] welcome to sbt 1.5.2 ││[info] welcome to sbt 1.5.2 (│ 51 | │[info] loading global plugin││[info] loading global plugins│ 52 | │[info] loading settings for ││[info] loading settings for p│ 53 | │[info] loading project defin││[info] loading project defini│ 54 | │[info] loading settings for ││[info] loading settings for p│ 55 | │[info] set current project t││[info] set current project to│ 56 | │[warn] sbt server could not ││[warn] sbt server could not s│ 57 | │[warn] Running multiple inst││[warn] Running multiple insta│ 58 | │[info] compiling 6 Scala sou││[info] compiling 6 Scala sour│ 59 | │[info] done compiling ││[info] done compiling │ 60 | │[info] compiling 12 Scala so││[info] compiling 3 Scala sour│ 61 | └──────────FRONTEND──────────┘└───────────BACKEND───────────┘ 62 | ``` 63 | 64 | ---- 65 | 66 | [Badge-SonatypeReleases]: https://img.shields.io/nexus/r/https/oss.sonatype.org/io.github.kitlangton/zio-app_2.13.svg "Sonatype Releases" 67 | [Badge-SonatypeSnapshots]: https://img.shields.io/nexus/s/https/oss.sonatype.org/io.github.kitlangton/zio-app_2.13.svg "Sonatype Snapshots" 68 | [Link-SonatypeSnapshots]: https://oss.sonatype.org/content/repositories/snapshots/io/github/kitlangton/zio-app_2.13/ "Sonatype Snapshots" 69 | [Link-SonatypeReleases]: https://oss.sonatype.org/content/repositories/releases/io/github/kitlangton/zio-app_2.13/ "Sonatype Releases" 70 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/Main.scala: -------------------------------------------------------------------------------- 1 | //package zio.app 2 | // 3 | //import view.View._ 4 | //import view._ 5 | //import zio._ 6 | //import zio.process.{Command, CommandError} 7 | // 8 | //import java.io.File 9 | // 10 | //object Main extends ZIOAppDefault { 11 | // def print(string: String): UIO[Unit] = ZIO.succeed(println(string)) 12 | // 13 | // def run = { 14 | // getArgs.flatMap { args => 15 | // if (args.headOption.contains("new")) { 16 | // createTemplateProject 17 | // } else if (args.headOption.contains("dev")) { 18 | // val view = vertical( 19 | // "Running Dev Mode", 20 | // "http://localhost:9630".blue 21 | // ) 22 | // println(view.renderNow) 23 | // for { 24 | // fiber <- Console.readLine.fork 25 | // result <- Backend.run raceFirst fiber.await.exitCode 26 | // } yield result 27 | // } else { 28 | // renderHelp 29 | // } 30 | // } 31 | // } 32 | // 33 | // private val createTemplateProject: ZIO[Any, Throwable, Unit] = for { 34 | // _ <- print("Configure your new ZIO app.".cyan.renderNow) 35 | // name <- TemplateGenerator.execute 36 | // pwd <- System.property("user.dir").someOrFail(new Error("Can't get PWD")) 37 | // dir = new File(new File(pwd), name) 38 | // _ <- runYarnInstall(dir) 39 | // view = vertical( 40 | // horizontal("Created ", name.yellow).bordered, 41 | // "Run the following commands to get started:", 42 | // s"cd $name".yellow, 43 | // "zio-app dev".yellow 44 | // ) 45 | // _ <- print(view.renderNow) 46 | // } yield () 47 | // 48 | // private def renderInvalidCommandError(command: String) = { 49 | // val view = 50 | // vertical( 51 | // vertical( 52 | // horizontal( 53 | // "Invalid Command:".red, 54 | // s" $command" 55 | // ) 56 | // ).bordered 57 | // .overlay( 58 | // "ERROR".red.reversed.padding(2, 0), 59 | // Alignment.topLeft 60 | // ), 61 | // horizontal( 62 | // "Are you're sure you're running ", 63 | // "zio-app dev".cyan, 64 | // " in a directory created using ", 65 | // "zio-app new".cyan, 66 | // "?" 67 | // ).bordered 68 | // ) 69 | // print("") *> 70 | // print(view.renderNow) 71 | // } 72 | // 73 | // private val renderHelp: UIO[Unit] = { 74 | // val view = 75 | // vertical( 76 | // horizontal("new".cyan, " Create a new zio-app"), 77 | // horizontal("dev".cyan, " Activate live-reloading dev mode") 78 | // ).bordered 79 | // .overlay( 80 | // text("commands", Color.Yellow).paddingH(2), 81 | // Alignment.topRight 82 | // ) 83 | // 84 | // print(view.renderNow) 85 | // } 86 | // 87 | // private def runYarnInstall(dir: File): ZIO[Any, CommandError, Unit] = 88 | // Command("yarn", "install") 89 | // .workingDirectory(dir) 90 | // .linesStream 91 | // .foreach(print) 92 | // .tapError { 93 | // case err if err.getMessage.contains("""Cannot run program "yarn"""") => 94 | // Command("npm", "i", "-g", "yarn").successfulExitCode 95 | // case _ => 96 | // ZIO.unit 97 | // } 98 | // .retryN(1) 99 | //} 100 | -------------------------------------------------------------------------------- /core/js/src/main/scala/zio/app/FrontendUtils.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import boopickle.Default._ 4 | import boopickle.{CompositePickler, UnpickleState} 5 | import org.scalajs.dom.RequestMode 6 | import sttp.client3._ 7 | import sttp.model.Uri 8 | import zio._ 9 | import zio.stream._ 10 | 11 | import java.nio.{ByteBuffer, ByteOrder} 12 | import scala.util.Try 13 | 14 | object FrontendUtils { 15 | implicit val exPickler: CompositePickler[Throwable] = exceptionPickler 16 | 17 | private val sttpBackend = 18 | FetchZioBackend(fetchOptions = FetchOptions(credentials = None, mode = Some(RequestMode.`same-origin`))) 19 | 20 | def apiUri(config: ClientConfig): Uri = 21 | Uri(org.scalajs.dom.document.location.hostname) 22 | .scheme(org.scalajs.dom.document.location.protocol.replaceAll(":", "")) 23 | .port(org.scalajs.dom.document.location.port.toIntOption) 24 | .addPathSegments(config.root.add("api").segments) 25 | 26 | def fetch[A: Pickler](service: String, method: String, config: ClientConfig): UIO[A] = 27 | fetchRequest[A](bytesRequest.get(apiUri(config).addPath(service, method)), config) 28 | 29 | def fetch[A: Pickler]( 30 | service: String, 31 | method: String, 32 | value: ByteBuffer, 33 | config: ClientConfig, 34 | ): UIO[A] = 35 | fetchRequest[A](bytesRequest.post(apiUri(config).addPath(service, method)).body(value), config) 36 | 37 | def fetchRequest[A: Pickler](request: Request[Array[Byte], Any], config: ClientConfig): UIO[A] = 38 | sttpBackend 39 | .send(request.header("authorization", config.authToken.map("Bearer " + _))) 40 | .orDie 41 | .flatMap { response => 42 | Unpickle[A].fromBytes(ByteBuffer.wrap(response.body)) match { 43 | case value => 44 | ZIO.succeed(value) 45 | } 46 | } 47 | 48 | def fetchStream[A: Pickler](service: String, method: String, config: ClientConfig): Stream[Nothing, A] = 49 | fetchStreamRequest[A](basicRequest.get(apiUri(config).addPath(service, method))) 50 | 51 | def fetchStream[A: Pickler]( 52 | service: String, 53 | method: String, 54 | value: ByteBuffer, 55 | config: ClientConfig, 56 | ): Stream[Nothing, A] = 57 | fetchStreamRequest[A](basicRequest.post(apiUri(config).addPath(service, method)).body(value)) 58 | 59 | def fetchStreamRequest[A: Pickler](request: Request[Either[String, String], Any]): Stream[Nothing, A] = 60 | ZStream.unwrap { 61 | request 62 | .response(asStreamAlwaysUnsafe(ZioStreams)) 63 | .send(sttpBackend) 64 | .orDie 65 | .map(resp => transformZioResponseStream[A](resp.body)) 66 | } 67 | 68 | private def transformZioResponseStream[A: Pickler](stream: ZioStreams.BinaryStream) = 69 | stream 70 | .catchAll(ZStream.die(_)) 71 | .mapConcatChunk(a => unpickleMany[A](a)) 72 | 73 | private val bytesRequest = 74 | RequestT[Empty, Array[Byte], Any]( 75 | None, 76 | None, 77 | NoBody, 78 | Vector(), 79 | asByteArrayAlways, 80 | RequestOptions( 81 | followRedirects = true, 82 | DefaultReadTimeout, 83 | 10, 84 | redirectToGet = true, 85 | ), 86 | Map(), 87 | ) 88 | 89 | def unpickleMany[A: Pickler](bytes: Array[Byte]): Chunk[A] = { 90 | val unpickleState = UnpickleState(ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)) 91 | def unpickle: Option[A] = Try(Unpickle[A].fromState(unpickleState)).toOption 92 | Chunk.unfold(unpickle)(_.map(_ -> unpickle)) 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/Input.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import org.jline.keymap.{BindingReader, KeyMap} 4 | import org.jline.terminal.Terminal.{Signal, SignalHandler} 5 | import org.jline.terminal.{Attributes, Terminal, TerminalBuilder} 6 | import org.jline.utils.InfoCmp.Capability 7 | import zio._ 8 | import zio.stream.ZStream 9 | 10 | object Input { 11 | lazy val ec = new EscapeCodes(java.lang.System.out) 12 | 13 | private lazy val terminal: org.jline.terminal.Terminal = 14 | TerminalBuilder 15 | .builder() 16 | .jna(true) 17 | .system(true) 18 | .nativeSignals(true) 19 | .signalHandler(Terminal.SignalHandler.SIG_IGN) 20 | .build(); 21 | 22 | def rawModeScoped(fullscreen: Boolean = true): ZIO[Scope, Nothing, Attributes] = ZIO.acquireRelease { 23 | for { 24 | originalAttrs <- ZIO.attemptBlocking(terminal.enterRawMode()).orDie 25 | _ <- ZIO.attemptBlocking { 26 | if (fullscreen) { 27 | terminal.puts(Capability.enter_ca_mode) 28 | terminal.puts(Capability.keypad_xmit) 29 | terminal.puts(Capability.clear_screen) 30 | ec.alternateBuffer() 31 | ec.clear() 32 | } 33 | ec.hideCursor() 34 | }.orDie 35 | } yield originalAttrs 36 | } { originalAttrs => 37 | (for { 38 | _ <- ZIO.attemptBlocking { 39 | terminal.setAttributes(originalAttrs) 40 | terminal.puts(Capability.exit_ca_mode) 41 | terminal.puts(Capability.keypad_local) 42 | terminal.puts(Capability.cursor_visible) 43 | ec.normalBuffer() 44 | ec.showCursor() 45 | } 46 | } yield ()).orDie 47 | } 48 | 49 | lazy val terminalSizeStream: ZStream[Any, Nothing, (Int, Int)] = 50 | ZStream.fromZIO(ZIO.blocking(ZIO.succeed(terminalSize))) ++ 51 | ZStream.async { register => addResizeHandler(size => register(ZIO.succeed(Chunk(size)))) } 52 | 53 | private def addResizeHandler(f: ((Int, Int)) => Unit): SignalHandler = 54 | terminal.handle(Signal.WINCH, _ => { f(terminalSize) }) 55 | 56 | def terminalSize: (Int, Int) = { 57 | val size = Input.terminal.getSize 58 | val width = size.getColumns 59 | val height = size.getRows 60 | (width, height) 61 | } 62 | 63 | def withRawMode[R, E, A](zio: ZIO[R, E, A]): ZIO[R, E, A] = 64 | ZIO.scoped[R] { 65 | rawModeScoped(true) *> zio 66 | } 67 | 68 | lazy val keyMap: KeyMap[KeyEvent] = { 69 | val keyMap = new KeyMap[KeyEvent] 70 | 71 | for (i <- 32 to 256) { 72 | val str = Character.toString(i.toChar) 73 | keyMap.bind(KeyEvent.Character(i.toChar), str); 74 | } 75 | 76 | keyMap.bind(KeyEvent.Exit, KeyMap.key(terminal, Capability.key_exit), Character.toString(3.toChar)) 77 | keyMap.bind(KeyEvent.Up, KeyMap.key(terminal, Capability.key_up), "[A") 78 | keyMap.bind(KeyEvent.Left, KeyMap.key(terminal, Capability.key_down), "[D") 79 | keyMap.bind(KeyEvent.Down, KeyMap.key(terminal, Capability.key_left), "[B") 80 | keyMap.bind(KeyEvent.Right, KeyMap.key(terminal, Capability.key_right), "[C") 81 | keyMap.bind(KeyEvent.Delete, KeyMap.key(terminal, Capability.key_backspace), KeyMap.del()) 82 | keyMap.bind(KeyEvent.Enter, KeyMap.key(terminal, Capability.carriage_return), "\n") 83 | 84 | keyMap 85 | } 86 | 87 | private lazy val bindingReader = new BindingReader(terminal.reader()) 88 | 89 | private val readBinding: RIO[Any, KeyEvent] = 90 | ZIO.attemptBlockingInterrupt(bindingReader.readBinding(keyMap)) 91 | 92 | val keyEventStream: ZStream[Any, Throwable, KeyEvent] = 93 | ZStream.repeatZIO(readBinding) merge 94 | ZStream.async[Any, Nothing, KeyEvent](register => 95 | terminal.handle( 96 | Signal.INT, 97 | _ => register(ZIO.succeed(Chunk(KeyEvent.Exit))) 98 | ) 99 | ) 100 | } 101 | -------------------------------------------------------------------------------- /cli/src/main/g8/build.sbt: -------------------------------------------------------------------------------- 1 | name := "$name$" 2 | description := "$description$" 3 | version := "0.0.1" 4 | 5 | val animusVersion = "0.1.9" 6 | val boopickleVersion = "1.4.0" 7 | val laminarVersion = "0.13.1" 8 | val laminextVersion = "0.13.10" 9 | val postgresVersion = "42.2.23" 10 | val quillZioVersion = "3.9.0" 11 | val scalaJavaTimeVersion = "2.3.0" 12 | val sttpVersion = "3.3.13" 13 | val zioAppVersion = "0.2.5" 14 | val zioConfigVersion = "1.0.6" 15 | val zioHttpVersion = "1.0.0.0-RC17" 16 | val zioJsonVersion = "0.1.5" 17 | val zioMagicVersion = "0.3.8" 18 | val zioVersion = "1.0.11" 19 | 20 | val sharedSettings = Seq( 21 | addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.0" cross CrossVersion.full), 22 | addCompilerPlugin("com.olegpy" %% "better-monadic-for" % "0.3.1"), 23 | scalacOptions ++= Seq("-Xfatal-warnings"), 24 | resolvers ++= Seq( 25 | "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots", 26 | "Sonatype OSS Snapshots s01" at "https://s01.oss.sonatype.org/content/repositories/snapshots" 27 | ), 28 | libraryDependencies ++= Seq( 29 | "io.github.kitlangton" %% "zio-app" % zioAppVersion, 30 | "io.suzaku" %%% "boopickle" % boopickleVersion, 31 | "dev.zio" %%% "zio" % zioVersion, 32 | "dev.zio" %%% "zio-streams" % zioVersion, 33 | "dev.zio" %%% "zio-macros" % zioVersion, 34 | "dev.zio" %%% "zio-test" % zioVersion % Test, 35 | "dev.zio" %%% "zio-json" % zioJsonVersion, 36 | "io.github.kitlangton" %%% "zio-app" % zioAppVersion, 37 | "com.softwaremill.sttp.client3" %%% "core" % sttpVersion 38 | ), 39 | scalacOptions ++= Seq("-Ymacro-annotations", "-Xfatal-warnings", "-deprecation"), 40 | scalaVersion := "2.13.6", 41 | testFrameworks += new TestFramework("zio.test.sbt.ZTestFramework") 42 | ) 43 | 44 | lazy val backend = project 45 | .in(file("backend")) 46 | .enablePlugins(JavaAppPackaging) 47 | .settings( 48 | sharedSettings, 49 | Compile / run / mainClass := Some("$package$.Backend"), 50 | libraryDependencies ++= Seq( 51 | "io.github.kitlangton" %% "zio-magic" % zioMagicVersion, 52 | "dev.zio" %% "zio-config" % zioConfigVersion, 53 | "dev.zio" %% "zio-config-typesafe" % zioConfigVersion, 54 | "dev.zio" %% "zio-config-magnolia" % zioConfigVersion, 55 | "io.d11" %% "zio" % zioHttpVersion, 56 | "com.softwaremill.sttp.client3" %% "httpclient-backend-zio" % sttpVersion, 57 | "org.postgresql" % "postgresql" % "42.2.8", 58 | "io.getquill" %% "quill-jdbc-zio" % quillZioVersion 59 | ) 60 | ) 61 | .dependsOn(shared) 62 | 63 | lazy val frontend = project 64 | .in(file("frontend")) 65 | .enablePlugins(ScalaJSPlugin) 66 | .settings( 67 | scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }, 68 | scalaJSLinkerConfig ~= { _.withSourceMap(false) }, 69 | scalaJSUseMainModuleInitializer := true, 70 | libraryDependencies ++= Seq( 71 | "io.github.kitlangton" %%% "animus" % animusVersion, 72 | "com.raquo" %%% "laminar" % laminarVersion, 73 | "io.github.cquiroz" %%% "scala-java-time" % scalaJavaTimeVersion, 74 | "io.laminext" %%% "websocket" % laminextVersion 75 | ) 76 | ) 77 | .settings(sharedSettings) 78 | .dependsOn(shared) 79 | 80 | lazy val shared = project 81 | .enablePlugins(ScalaJSPlugin) 82 | .in(file("shared")) 83 | .settings( 84 | sharedSettings, 85 | scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }, 86 | scalaJSLinkerConfig ~= { _.withSourceMap(false) } 87 | ) 88 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/Backend.scala: -------------------------------------------------------------------------------- 1 | //package zio.app 2 | // 3 | //import boopickle.Default._ 4 | //import boopickle.Pickler 5 | //import io.netty.buffer.Unpooled 6 | //import io.netty.handler.codec.http.{HttpHeaderNames, HttpHeaderValues} 7 | //import zio.http._ 8 | //import zio.service.{ChannelEvent, Server} 9 | //import zio.socket._ 10 | //import zio._ 11 | //import zio.app.cli.protocol.{ClientCommand, ServerCommand} 12 | //import zio.process.{Command, CommandError} 13 | //import zio.stream.ZStream 14 | // 15 | //import java.nio.ByteBuffer 16 | //import scala.io.Source 17 | //import scala.util.{Failure, Success, Try} 18 | // 19 | //object Backend extends ZIOAppDefault { 20 | // def appSocket: SocketApp[FileSystemService with SbtManager] = 21 | // pickleSocket { (command: ClientCommand) => 22 | // command match { 23 | // case ClientCommand.ChangeDirectory(path) => 24 | // FileSystemService.cd(path) 25 | // 26 | // case ClientCommand.Subscribe => 27 | // SbtManager.launchVite merge 28 | // SbtManager.backendSbtStream 29 | // .zipWithLatest(SbtManager.frontendSbtStream)(_ -> _) 30 | // .zipWithLatest(FileSystemService.stateStream)(_ -> _) 31 | // .map { case ((b, f), fs) => 32 | // val command: ServerCommand = ServerCommand.State(b, f, fs) 33 | // val byteBuf = Pickle.intoBytes(command) 34 | // WebSocketFrame.binary(Chunk.fromArray(byteBuf.array())) 35 | // } 36 | // } 37 | // } 38 | // 39 | // implicit def chunkPickler[A](implicit aPickler: Pickler[A]): Pickler[Chunk[A]] = 40 | // transformPickler[Chunk[A], List[A]](as => Chunk.from(as))(_.toList) 41 | // 42 | // private def app: HttpApp[FileSystemService with SbtManager, Throwable] = 43 | // Http.collectZIO { 44 | // case Method.GET -> !! / "ws" => 45 | // Response.fromSocket(appSocket) 46 | // 47 | // case Method.GET -> !! / "assets" / file => 48 | // val source = Source.fromResource(s"dist/assets/$file").getLines().mkString("\n") 49 | // 50 | // val contentTypeHtml: Header = 51 | // if (file.endsWith(".js")) (HttpHeaderNames.CONTENT_TYPE, "text/javascript") 52 | // else (HttpHeaderNames.CONTENT_TYPE, "text/css") 53 | // 54 | // ZIO.succeed { 55 | // Response( 56 | // headers = Headers(contentTypeHtml), 57 | // data = HttpData.fromChunk(Chunk.fromArray(source.getBytes(HTTP_CHARSET))), 58 | // ) 59 | // } 60 | // 61 | // case Method.GET -> !! => 62 | // val html = Source.fromResource(s"dist/index.html").getLines().mkString("\n") 63 | // 64 | // val contentTypeHtml: Header = (HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_HTML) 65 | // ZIO.succeed { 66 | // Response( 67 | // data = HttpData.fromChunk(Chunk.fromArray(html.getBytes(HTTP_CHARSET))), 68 | // headers = Headers(contentTypeHtml), 69 | // ) 70 | // } 71 | // 72 | // case other => 73 | // println(s"RECEIVED NOT FOUND: $other") 74 | // ZIO.succeed(Response.status(Status.NotFound)) 75 | // } 76 | // 77 | // lazy val program = for { 78 | // port <- System.envOrElse("PORT", "9630").map(_.toInt).orElseSucceed(9630) 79 | // _ <- Console.printLine(s"STARTING SERVER ON PORT $port") 80 | // _ <- openBrowser.delay(1.second).fork 81 | // _ <- Server.start(port, app) 82 | // } yield () 83 | // 84 | // private def openBrowser: ZIO[Any, CommandError, ExitCode] = 85 | // Command("open", "http://localhost:9630").exitCode 86 | // 87 | // override def run = 88 | // program 89 | // .provide(SbtManager.live, FileSystemService.live) 90 | // 91 | // private def pickleSocket[R, E, A: Pickler]( 92 | // f: A => ZIO[R, E, Any], 93 | // ) = 94 | // Http.collectZIO[WebSocketChannelEvent] { 95 | // 96 | // case ChannelEvent(ch, ChannelEvent.ChannelRead(WebSocketFrame.Binary(bytes))) => 97 | // Try(Unpickle[A].fromBytes(ByteBuffer.wrap(bytes.toArray))) match { 98 | // case Failure(error) => 99 | // Console.printLineError(s"Decoding Error: $error").orDie 100 | // case Success(command) => 101 | // f(command) 102 | // } 103 | // } 104 | // case other => 105 | // Console.printLineError(s"UNEXPECTED SOCKET EVENT $other") 106 | // } 107 | //} 108 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/SbtOutput.scala: -------------------------------------------------------------------------------- 1 | package zio.app.cli.frontend 2 | 3 | import animus._ 4 | import com.raquo.laminar.api.L._ 5 | import zio.Chunk 6 | import zio.app.cli.protocol 7 | import zio.app.cli.protocol.Line 8 | 9 | case class SbtOutput( 10 | $lines: Signal[Chunk[Line]], 11 | $isOpen: Signal[Boolean], 12 | toggleVisible: () => Unit, 13 | title: String 14 | ) extends Component { 15 | 16 | val scrollTopVar = Var(0.0) 17 | val scrollOverride = Var(false) 18 | 19 | def collapse = { 20 | val $deg = $isOpen.map { b => 21 | if (!b) 90.0 22 | else 180.0 23 | } 24 | 25 | div( 26 | cls("hover-button"), 27 | cursor.pointer, 28 | div( 29 | "^", 30 | transform <-- $deg.spring.map { deg => s"rotate(${deg}deg)" } 31 | ), 32 | onClick --> { _ => toggleVisible() } 33 | ) 34 | } 35 | 36 | def scrollLocking = { 37 | val $deg = scrollOverride.signal.map { b => 38 | if (!b) 90.0 39 | else 180.0 40 | } 41 | 42 | div( 43 | cls("hover-button"), 44 | cursor.pointer, 45 | div( 46 | "^", 47 | transform <-- $deg.spring.map { deg => s"rotate(${deg}deg)" } 48 | ), 49 | onClick --> { _ => scrollOverride.update(!_) } 50 | ) 51 | } 52 | 53 | val $headerPadding = $isOpen.map { if (_) 12.0 else 0.0 }.spring.map { p => s"${p}px 12px" } 54 | 55 | def body: HtmlElement = 56 | div( 57 | cls("sbt-output"), 58 | cls.toggle("disabled") <-- $isOpen.map(!_), 59 | div( 60 | cls("sbt-output-title"), 61 | zIndex(10), 62 | div( 63 | title, 64 | opacity <-- $isOpen.map { if (_) 1.0 else 0.5 }.spring 65 | ), 66 | div( 67 | display.flex, 68 | collapse, 69 | div(width("12px")), 70 | scrollLocking 71 | ) 72 | ), 73 | pre( 74 | cls("sbt-output-body"), 75 | visibilityStyles, 76 | div( 77 | children <-- $rendered, 78 | opacity <-- $opacity.spring 79 | ), 80 | scrollEvents 81 | ) 82 | ) 83 | 84 | lazy val scrollEvents: Modifier[HtmlElement] = Seq( 85 | inContext { (el: HtmlElement) => 86 | val diffBus = new EventBus[Double] 87 | val ref = el.ref 88 | Seq( 89 | $lines.changes.delay(0).combineWith(EventStream.periodic(100)) --> { _ => 90 | if (!scrollOverride.now()) { 91 | scrollTopVar.set( 92 | ref.scrollHeight.toDouble - ref.getBoundingClientRect().height 93 | ) 94 | } 95 | }, 96 | onWheel --> { _ => 97 | val diff: Double = ref.scrollHeight - (ref.scrollTop + ref.getBoundingClientRect().height) 98 | diffBus.writer.onNext(diff) 99 | scrollOverride.set(true) 100 | scrollTopVar.set(ref.scrollTop) 101 | }, 102 | diffBus.events.debounce(100) --> { diff => 103 | scrollOverride.set(diff > 0) 104 | }, 105 | // TODO: Fix `step` method in animus. Make sure to check that animating is still true. 106 | scrollTopVar.signal.spring --> { scrollTop => 107 | if (!scrollOverride.now()) { 108 | ref.scrollTop = scrollTop 109 | } 110 | } 111 | ) 112 | } 113 | ) 114 | 115 | def attrStyles(attribute: protocol.Attribute): Mod[HtmlElement] = attribute match { 116 | case protocol.Attribute.Red => cls("console-red") 117 | case protocol.Attribute.Yellow => cls("console-yellow") 118 | case protocol.Attribute.Blue => cls("console-blue") 119 | case protocol.Attribute.Green => cls("console-green") 120 | case protocol.Attribute.Cyan => cls("console-cyan") 121 | case protocol.Attribute.Magenta => cls("console-magenta") 122 | case protocol.Attribute.Bold => fontWeight.bold 123 | } 124 | 125 | val $rendered = $lines.map(_.zipWithIndex.toVector).split(_._2) { (_, value, _) => 126 | div( 127 | value._1.fragments.map { fragment => 128 | span(fragment.attributes.map(attrStyles), fragment.string) 129 | } 130 | ) 131 | } 132 | 133 | private val $height = $isOpen.map { if (_) 200.0 else 0.0 } 134 | private val $padding = $isOpen.map { if (_) 12.0 else 0.0 } 135 | private val $opacity = $isOpen.map { if (_) 1.0 else 0.3 } 136 | 137 | private val visibilityStyles: Mod[HtmlElement] = Seq( 138 | // height <-- $height.spring.px, 139 | paddingTop <-- $padding.spring.px, 140 | paddingBottom <-- $padding.spring.px 141 | ) 142 | } 143 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/app/internal/BackendUtils.scala: -------------------------------------------------------------------------------- 1 | package zio.app.internal 2 | 3 | import boopickle.CompositePickler 4 | import boopickle.Default._ 5 | import io.netty.buffer.Unpooled 6 | import io.netty.handler.codec.http.{HttpHeaderNames, HttpHeaderValues} 7 | import zio.http._ 8 | import zio.http.model._ 9 | import zio._ 10 | import zio.stream.{UStream, ZStream} 11 | 12 | import java.nio.ByteBuffer 13 | import java.time.Instant 14 | 15 | object BackendUtils { 16 | implicit val exPickler: CompositePickler[Throwable] = exceptionPickler 17 | 18 | private val bytesContent = (HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.BYTES) 19 | 20 | private def urlEncode(s: String): String = 21 | java.net.URLEncoder.encode(s, "UTF-8") 22 | 23 | def makeRoute[R, E: Pickler, A: Pickler, B: Pickler]( 24 | service: String, 25 | method: String, 26 | call: A => ZIO[R, E, B] 27 | ): HttpApp[R, Throwable] = { 28 | val service0 = urlEncode(service) 29 | val method0 = method 30 | Http.collectZIO { case post @ Method.POST -> !! / `service0` / `method0` => 31 | post.body.asArray.orDie.flatMap { body => 32 | val byteBuffer = ByteBuffer.wrap(body) 33 | val unpickled = Unpickle[A].fromBytes(byteBuffer) 34 | call(unpickled) 35 | .map(pickle[B](_)) 36 | .catchAll { 37 | case t: Throwable => 38 | ZIO.fail(t) 39 | case other => 40 | ZIO.fail(new Exception(s"Route Failed ${service}.${method}: ${other.toString}")) 41 | } 42 | } 43 | } 44 | } 45 | 46 | def makeRouteNullary[R, E: Pickler, A: Pickler]( 47 | service: String, 48 | method: String, 49 | call: ZIO[R, E, A] 50 | ): HttpApp[R, Throwable] = { 51 | val service0 = urlEncode(service) 52 | val method0 = method 53 | Http.collectZIO { case Method.GET -> !! / `service0` / `method0` => 54 | call 55 | .map(pickle[A](_)) 56 | .catchAll { 57 | case t: Throwable => 58 | ZIO.fail(t) 59 | case other => 60 | ZIO.fail(new Exception(s"Route Failed ${service}.${method}: ${other.toString}")) 61 | } 62 | } 63 | } 64 | 65 | def makeRouteStream[R, E: Pickler, A: Pickler, B: Pickler]( 66 | service: String, 67 | method: String, 68 | call: A => ZStream[R, E, B] 69 | ): HttpApp[R, Nothing] = { 70 | val service0 = service 71 | val method0 = method 72 | Http.collectZIO { case post @ Method.POST -> !! / `service0` / `method0` => 73 | post.body.asArray.orDie.flatMap { body => 74 | val byteBuffer = ByteBuffer.wrap(body) 75 | val unpickled = Unpickle[A].fromBytes(byteBuffer) 76 | ZIO.environment[R].map { env => 77 | makeStreamResponse(call(unpickled), env) 78 | } 79 | } 80 | } 81 | } 82 | 83 | def makeRouteNullaryStream[R, E: Pickler, A: Pickler]( 84 | service: String, 85 | method: String, 86 | call: ZStream[R, E, A] 87 | ): HttpApp[R, Nothing] = { 88 | val service0 = service 89 | val method0 = method 90 | Http.collectZIO { case Method.GET -> !! / `service0` / `method0` => 91 | ZIO.environment[R].map { env => 92 | makeStreamResponse(call, env) 93 | } 94 | } 95 | } 96 | 97 | private def pickle[A: Pickler](value: A): Response = { 98 | val bytes: ByteBuffer = Pickle.intoBytes(value) 99 | val byteBuf = Unpooled.wrappedBuffer(bytes) 100 | val body = Body.fromByteBuf(byteBuf) 101 | 102 | Response(status = Status.Ok, headers = Headers(bytesContent), body = body) 103 | } 104 | 105 | private def makeStreamResponse[A: Pickler, E: Pickler, R]( 106 | stream: ZStream[R, E, A], 107 | env: ZEnvironment[R] 108 | ): Response = { 109 | val responseStream: ZStream[Any, Throwable, Byte] = 110 | stream.mapConcatChunk { a => 111 | Chunk.fromByteBuffer(Pickle.intoBytes(a)) 112 | }.mapError { 113 | case t: Throwable => t 114 | case other => new Exception(s"Stream Failed: ${other.toString}") 115 | 116 | } 117 | .provideEnvironment(env) 118 | 119 | Response(body = Body.fromStream(responseStream)) 120 | } 121 | 122 | } 123 | 124 | object CustomPicklers { 125 | implicit val nothingPickler: Pickler[Nothing] = new Pickler[Nothing] { 126 | override def pickle(obj: Nothing)(implicit state: PickleState): Unit = throw new Error("IMPOSSIBLE") 127 | override def unpickle(implicit state: UnpickleState): Nothing = throw new Error("IMPOSSIBLE") 128 | } 129 | 130 | implicit val datePickler: Pickler[Instant] = 131 | transformPickler((t: Long) => Instant.ofEpochMilli(t))(_.toEpochMilli) 132 | 133 | // local date time 134 | implicit val localDateTimePickler: Pickler[java.time.LocalDateTime] = 135 | transformPickler((t: Long) => 136 | java.time.LocalDateTime.ofInstant(Instant.ofEpochMilli(t), java.time.ZoneId.of("UTC")) 137 | )(_.toInstant(java.time.ZoneOffset.UTC).toEpochMilli) 138 | 139 | } 140 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/TextMap.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import tui.StringSyntax.StringOps 4 | 5 | class RenderContext(val textMap: TextMap, var x: Int, var y: Int) { 6 | 7 | def align(childSize: Size, parentSize: Size, alignment: Alignment): Unit = { 8 | val parentPoint = alignment.point(parentSize) 9 | val childPoint = alignment.point(childSize) 10 | translateBy(parentPoint.x - childPoint.x, parentPoint.y - childPoint.y) 11 | } 12 | 13 | def insert(string: String, color: Color = Color.Default, style: Style = Style.Default): Unit = 14 | textMap.insert(string, x, y, color, style) 15 | 16 | def translateBy(dx: Int, dy: Int): Unit = { 17 | x += dx 18 | y += dy 19 | } 20 | 21 | def scratch(f: => Unit): Unit = { 22 | val x0 = x 23 | val y0 = y 24 | f 25 | x = x0 26 | y = y0 27 | } 28 | } 29 | 30 | class TextMap( 31 | text: Array[Array[String]], 32 | colors: Array[Array[Color]], 33 | styles: Array[Array[Style]], 34 | private val width: Int, 35 | private val height: Int 36 | ) { self => 37 | 38 | def apply(x: Int, y: Int): String = 39 | if (0 <= x && x < width && 0 <= y && y < height) 40 | text(y)(x) 41 | else "" 42 | 43 | def setColor(x: Int, y: Int, color: Color): Unit = 44 | if (0 <= x && x < width && 0 <= y && y < height) 45 | colors(y)(x) = color 46 | 47 | def setStyle(x: Int, y: Int, style: Style): Unit = 48 | if (0 <= x && x < width && 0 <= y && y < height) 49 | styles(y)(x) = style 50 | 51 | def getColor(x: Int, y: Int): Color = 52 | if (0 <= x && x < width && 0 <= y && y < height) 53 | colors(y)(x) 54 | else Color.Default 55 | 56 | def getStyle(x: Int, y: Int): Style = 57 | if (0 <= x && x < width && 0 <= y && y < height) 58 | styles(y)(x) 59 | else Style.Default 60 | 61 | def update(x: Int, y: Int, string: String): Unit = 62 | if (0 <= x && x < width && 0 <= y && y < height) 63 | text(y)(x) = string 64 | 65 | def add(char: Char, x: Int, y: Int, color: Color = Color.Default, style: Style = Style.Default): Unit = { 66 | self(x, y) = char.toString 67 | setColor(x, y, color) 68 | setStyle(x, y, style) 69 | } 70 | 71 | def insert(string: String, x: Int, y: Int, color: Color = Color.Default, style: Style = Style.Default): Unit = { 72 | var currentX = x 73 | string.foreach { char => 74 | add(char, currentX, y, color, style) 75 | currentX += 1 76 | } 77 | } 78 | 79 | override def toString: String = { 80 | val builder = new StringBuilder() 81 | var color: Color = Color.Default 82 | var style: Style = Style.Default 83 | var y = 0 84 | text.foreach { line => 85 | var x = 0 86 | line.foreach { char => 87 | val newColor = colors(y)(x) 88 | val newStyle = styles(y)(x) 89 | 90 | // TODO: Clean up this nonsense. Actually model the terminal styling domain. 91 | if (newColor != color || newStyle != style) { 92 | color = newColor 93 | style = newStyle 94 | builder.addAll(scala.Console.RESET) 95 | builder.addAll(color.code) 96 | builder.addAll(style.code) 97 | } 98 | builder.addAll(char) 99 | x += 1 100 | } 101 | if (y < height - 1) { 102 | y += 1 103 | builder.addOne('\n') 104 | } 105 | } 106 | builder.toString() 107 | } 108 | } 109 | 110 | object TextMap { 111 | def ofDim(width: Int, height: Int, empty: String = " "): TextMap = 112 | new TextMap( 113 | Array.fill(height, width)(empty), 114 | Array.fill(height, width)(Color.Default), 115 | Array.fill(height, width)(Style.Default), 116 | width, 117 | height 118 | ) 119 | 120 | def diff(oldMap: TextMap, newMap: TextMap, width: Int, height: Int): String = { 121 | val result = new StringBuilder() 122 | result.addAll(moveCursor(0, 0)) 123 | for (x <- 0 until width; y <- 0 until height) { 124 | val oldChar = oldMap(x, y) 125 | val newChar = newMap(x, y) 126 | val oldColor = oldMap.getColor(x, y) 127 | val newColor = newMap.getColor(x, y) 128 | val oldStyle = oldMap.getStyle(x, y) 129 | val newStyle = newMap.getStyle(x, y) 130 | 131 | if (oldChar != newChar || oldColor != newColor || oldStyle != newStyle) { 132 | result.addAll(moveCursor(x, y)) 133 | result.addAll(newColor.code) 134 | result.addAll(newStyle.code) 135 | result.addAll(newChar) 136 | result.addAll(scala.Console.RESET) 137 | } 138 | 139 | } 140 | 141 | result.addAll(moveCursor(width, height) + scala.Console.RESET) 142 | result.toString() 143 | } 144 | 145 | private def moveCursor(x: Int, y: Int): String = s"\u001b[${y + 1};${x + 1}H" 146 | 147 | def main(args: Array[String]): Unit = { 148 | val oldMap = TextMap.ofDim(8, 8) 149 | val newMap = TextMap.ofDim(8, 8) 150 | oldMap.insert("cool", 4, 4) 151 | newMap.insert("cool", 2, 4) 152 | val result = diff(oldMap, newMap, 8, 8) 153 | val start = diff(TextMap.ofDim(0, 0), oldMap, 8, 8) 154 | println(start) 155 | println(result) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /cli/src/main/scala/zio/app/SbtManager.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import fansi.{Attr, Bold, Category, Str} 4 | import zio.app.DevMode.{backendLines, frontendLines} 5 | import zio.app.cli.protocol.{Attribute, Fragment, Line} 6 | import zio.stream._ 7 | import zio._ 8 | 9 | trait SbtManager { 10 | def backendSbtStream: Stream[Throwable, Chunk[Line]] 11 | def frontendSbtStream: Stream[Throwable, Chunk[Line]] 12 | def launchVite: Stream[Throwable, Nothing] 13 | } 14 | 15 | object SbtManager { 16 | val live: ULayer[SbtManager] = 17 | ZLayer.succeed(SbtManagerLive()) 18 | 19 | val backendSbtStream: ZStream[SbtManager, Throwable, Chunk[Line]] = 20 | ZStream.environmentWithStream[SbtManager](_.get.backendSbtStream) 21 | 22 | val frontendSbtStream: ZStream[SbtManager, Throwable, Chunk[Line]] = 23 | ZStream.environmentWithStream[SbtManager](_.get.frontendSbtStream) 24 | 25 | val launchVite: ZStream[SbtManager, Throwable, Nothing] = 26 | ZStream.environmentWithStream[SbtManager](_.get.launchVite) 27 | } 28 | 29 | case class SbtManagerLive() extends SbtManager { 30 | override def backendSbtStream: Stream[Throwable, Chunk[Line]] = 31 | backendLines 32 | .map { s => 33 | val str = scala.util.Try(Str(s)) 34 | str.map(renderDom).map(Chunk(_)).getOrElse(Chunk.empty) 35 | } 36 | .scan[Chunk[Line]](Chunk.empty)(_ ++ _) 37 | 38 | override def frontendSbtStream: Stream[Throwable, Chunk[Line]] = 39 | frontendLines 40 | .map { s => 41 | val str = scala.util.Try(Str(s)) 42 | str.map(renderDom).map(Chunk(_)).getOrElse(Chunk.empty) 43 | } 44 | .scan[Chunk[Line]](Chunk.empty)(_ ++ _) 45 | 46 | override def launchVite: Stream[Throwable, Nothing] = 47 | ZStream.fromZIO(DevMode.launchVite.exitCode).drain 48 | 49 | def renderDom(str: Str): Line = { 50 | val chars = str.getChars 51 | val colors = str.getColors 52 | 53 | // Pre-size StringBuilder with approximate size (ansi colors tend 54 | // to be about 5 chars long) to avoid re-allocations during growth 55 | val output = new StringBuilder(chars.length + colors.length * 5) 56 | 57 | var section = new StringBuilder() 58 | var attrs: Chunk[Attribute] = Chunk.empty 59 | val builder = ChunkBuilder.make[Fragment]() 60 | 61 | var currentState: Str.State = 0 62 | 63 | // Make a local array copy of the immutable Vector, for maximum performance 64 | // since the Vector is small and we'll be looking it up over & over & over 65 | val categoryArray = Attr.categories.toArray 66 | 67 | var i = 0 68 | while (i < colors.length) { 69 | // Emit ANSI escapes to change colors where necessary 70 | // fast-path optimization to check for integer equality first before 71 | // going through the whole `enableDiff` rigmarole 72 | if (colors(i) != currentState) { 73 | emitAnsi(currentState, colors(i), output, categoryArray) match { 74 | case Some(newAttrs) if section.nonEmpty => 75 | builder += Fragment(section.toString, attrs) 76 | attrs = newAttrs 77 | section = new StringBuilder() 78 | case _ => 79 | () 80 | } 81 | currentState = colors(i) 82 | } 83 | output.append(chars(i)) 84 | section.append(chars(i)) 85 | i += 1 86 | } 87 | 88 | builder += Fragment(section.toString, attrs) 89 | 90 | Line(builder.result()) 91 | } 92 | 93 | def emitAnsi( 94 | currentState: Str.State, 95 | nextState: Str.State, 96 | output: StringBuilder, 97 | categoryArray: Array[Category] 98 | ): Option[Chunk[Attribute]] = { 99 | if (currentState != nextState) { 100 | val builder = ChunkBuilder.make[Attribute]() 101 | val hardOffMask = Bold.mask 102 | 103 | val currentState2 = 104 | if ((currentState & ~nextState & hardOffMask) != 0) { 105 | output.append(scala.Console.RESET) 106 | 0L 107 | } else { 108 | currentState 109 | } 110 | 111 | var categoryIndex = 0 112 | while (categoryIndex < categoryArray.length) { 113 | val cat = categoryArray(categoryIndex) 114 | if ((cat.mask & currentState2) != (cat.mask & nextState)) { 115 | val attr = cat.lookupAttr(nextState & cat.mask) 116 | attr.name match { 117 | case "Color.Red" => builder += Attribute.Red 118 | case "Color.Yellow" => builder += Attribute.Yellow 119 | case "Color.Blue" => builder += Attribute.Blue 120 | case "Color.Green" => builder += Attribute.Green 121 | case "Color.Magenta" => builder += Attribute.Magenta 122 | case "Color.Cyan" => builder += Attribute.Cyan 123 | case "Bold.On" => builder += Attribute.Bold 124 | case "Color.Reset" => () 125 | case _ => println(attr.name) 126 | } 127 | val escape = cat.lookupEscape(nextState & cat.mask) 128 | output.append(escape) 129 | } 130 | categoryIndex += 1 131 | } 132 | 133 | Some(builder.result()) 134 | } else 135 | None 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/EscapeCodes.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import java.io.OutputStream 4 | 5 | // TODO: Rewrite all this nonsense copy pasted from elsewhere :) 6 | class EscapeCodes(out: OutputStream) { 7 | // Output an Escape Sequence 8 | private def ESC(command: Char): Unit = { out.write(("\u001b" + command).getBytes) } 9 | // Output a Control Sequence Inroducer 10 | private def CSI(sequence: String): Unit = { 11 | out.write(("\u001b[" + sequence).getBytes) 12 | } 13 | // Execute commands 14 | private def CSI(command: Char): Unit = { CSI(s"$command") } 15 | private def CSI(n: Int, command: Char): Unit = { CSI(s"$n$command") } 16 | private def CSI(n: Int, m: Int, command: Char): Unit = { CSI(s"$n;$m$command") } 17 | private def CSI(n: Int, m: Int, o: Int, command: Char): Unit = { 18 | CSI(s"$n;$m;$o$command") 19 | } 20 | // Execute commands in private modes 21 | private def CSI(mode: Char, command: Char): Unit = { CSI(s"$mode$command") } 22 | private def CSI(mode: Char, n: Int, command: Char): Unit = { 23 | CSI(s"$mode$n;$command") 24 | } 25 | 26 | /* DSR */ 27 | def status(): Unit = { CSI(5, 'n') } 28 | 29 | // Cursor movement 30 | /* CUU */ 31 | def moveUp(n: Int = 1): Unit = { CSI(n, 'A') } 32 | /* CUD */ 33 | def moveDown(n: Int = 1): Unit = { CSI(n, 'B') } 34 | /* CUF */ 35 | def moveRight(n: Int = 1): Unit = { CSI(n, 'C') } 36 | /* CUB */ 37 | def moveLeft(n: Int = 1): Unit = { CSI(n, 'D') } 38 | /* CUP */ 39 | def move(y: Int, x: Int): Unit = { CSI(x + 1, y + 1, 'H') } 40 | 41 | // Cursor management 42 | /* DECTCEM */ 43 | def hideCursor(): Unit = { CSI('?', 25, 'l') } 44 | /* DECTCEM */ 45 | def showCursor(): Unit = { CSI('?', 25, 'h') } 46 | /* DECSC */ 47 | def saveCursor(): Unit = { ESC('7') } 48 | /* DECRC */ 49 | def restoreCursor(): Unit = { ESC('8') } 50 | // Somehow this fails when the window has a height of 30-39: 51 | /* CPR */ 52 | def cursorPosition(): (Int, Int) = { 53 | val r = getReport(() => CSI(6, 'n'), 2, 'R'); (r(1), r(0)) 54 | } 55 | 56 | // Screen management 57 | /* ED */ 58 | def clear(): Unit = { CSI(2, 'J') } 59 | /* ED */ 60 | def clearToEnd(): Unit = { CSI(0, 'J') } 61 | /* ED */ 62 | def clearLine(): Unit = { CSI(2, 'K') } 63 | /* DECSET */ 64 | def alternateBuffer(): Unit = { CSI('?', 47, 'h') } 65 | /* DECRST */ 66 | def normalBuffer(): Unit = { CSI('?', 47, 'l') } 67 | /* RIS */ 68 | def fullReset(): Unit = { ESC('c') } 69 | /* dtterm */ 70 | def resizeScreen(w: Int, h: Int): Unit = { CSI(8, w, h, 't') } 71 | /* dtterm */ 72 | def screenSize(): (Int, Int) = { 73 | val r = getReport(() => CSI(18, 't'), 3, 't'); (r(2), r(1)) 74 | } 75 | 76 | // Window management 77 | /* dtterm */ 78 | def unminimizeWindow(): Unit = { CSI(1, 't') } 79 | /* dtterm */ 80 | def minimizeWindow(): Unit = { CSI(2, 't') } 81 | /* dtterm */ 82 | def moveWindow(x: Int, y: Int): Unit = { CSI(3, x, y, 't') } 83 | /* dtterm */ 84 | def resizeWindow(w: Int, h: Int): Unit = { CSI(4, w, h, 't') } 85 | /* dtterm */ 86 | def moveToTop(): Unit = { CSI(5, 't') } 87 | /* dtterm */ 88 | def moveToBottom(): Unit = { CSI(6, 't') } 89 | /* dtterm */ 90 | def restoreWindow(): Unit = { CSI(9, 0, 't') } 91 | /* dtterm */ 92 | def maximizeWindow(): Unit = { CSI(9, 1, 't') } 93 | /* dtterm */ 94 | def windowPosition(): (Int, Int) = { 95 | val r = getReport(() => CSI(13, 't'), 3, 't'); (r(2), r(1)) 96 | } 97 | /* dtterm */ 98 | def windowSize(): (Int, Int) = { 99 | val r = getReport(() => CSI(14, 't'), 3, 't'); (r(2), r(1)) 100 | } 101 | 102 | // Color management 103 | /* ISO-8613-3 */ 104 | def setForeground(color: Int): Unit = { CSI(38, 5, color, 'm') } 105 | /* ISO-8613-3 */ 106 | def setBackground(color: Int): Unit = { CSI(48, 5, color, 'm') } 107 | /* SGR */ 108 | def startBold(): Unit = { CSI(1, 'm') } 109 | /* SGR */ 110 | def startUnderline(): Unit = { CSI(4, 'm') } 111 | /* SGR */ 112 | def startBlink(): Unit = { CSI(5, 'm') } 113 | /* SGR */ 114 | def startReverse(): Unit = { CSI(7, 'm') } 115 | /* SGR */ 116 | def stopBold(): Unit = { CSI(22, 'm') } 117 | /* SGR */ 118 | def stopUnderline(): Unit = { CSI(24, 'm') } 119 | /* SGR */ 120 | def stopBlink(): Unit = { CSI(25, 'm') } 121 | /* SGR */ 122 | def stopReverse(): Unit = { CSI(27, 'm') } 123 | /* SGR */ 124 | def stopForeground(): Unit = { CSI(39, 'm') } 125 | /* SGR */ 126 | def stopBackground(): Unit = { CSI(49, 'm') } 127 | /* SGR */ 128 | def resetColors(): Unit = { CSI(0, 'm') } 129 | 130 | /** Executes a request and parses the response report. 131 | * Usually, they would start with a CSI but JLine seems to ignore them. 132 | * @param csi CSI to execute 133 | * @param args How many arguments are expected 134 | * @param terminator Terminator character of the report 135 | * @return Sequence of parsed integers 136 | */ 137 | def getReport(csi: () => Unit, args: Int, terminator: Char): Array[Int] = { 138 | // Send the CSI 139 | csi() 140 | out.flush() 141 | 142 | val results = Array.fill(args)("") 143 | val separators = Array.fill(args - 1)(';') :+ terminator 144 | // Parse CSI 145 | System.in.read() 146 | System.in.read() 147 | // Parse each Ps 148 | for (i <- 0 until args) { 149 | var n = System.in.read() 150 | while (n != separators(i).toInt) { 151 | results(i) += n.toChar 152 | n = System.in.read() 153 | } 154 | } 155 | results.map(_.toInt) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /core/js/src/main/scala/zio/app/FetchZioBackend.scala: -------------------------------------------------------------------------------- 1 | package zio.app 2 | 3 | import org.scalajs.dom 4 | import org.scalajs.dom.{BodyInit, Request => FetchRequest} 5 | import sttp.capabilities.{Streams, WebSockets} 6 | import sttp.client3.internal.ConvertFromFuture 7 | //import sttp.client3.testing.SttpBackendStub 8 | import sttp.client3.{AbstractFetchBackend, FetchOptions, SttpBackend} 9 | import sttp.monad.{Canceler, MonadAsyncError} 10 | import sttp.ws.{WebSocket, WebSocketClosed, WebSocketFrame} 11 | import zio._ 12 | import zio.stream._ 13 | 14 | import scala.concurrent.Future 15 | import scala.scalajs.js 16 | import scala.scalajs.js.typedarray.{AB2TA, Int8Array} 17 | 18 | trait ZioStreams extends Streams[ZioStreams] { 19 | override type BinaryStream = Stream[Throwable, Array[Byte]] 20 | override type Pipe[A, B] = Stream[Throwable, A] => Stream[Throwable, B] 21 | } 22 | 23 | object ZioStreams extends ZioStreams 24 | 25 | object ZioWebsockets { 26 | def compilePipe( 27 | ws: WebSocket[Task], 28 | pipe: Stream[Throwable, WebSocketFrame.Data[_]] => Stream[Throwable, WebSocketFrame], 29 | ): Task[Unit] = 30 | Promise.make[Throwable, Unit].flatMap { wsClosed => 31 | val onClose = ZIO.attempt(wsClosed.succeed(())).as(None) 32 | pipe( 33 | ZStream 34 | .repeatZIO(ws.receive().flatMap { 35 | case WebSocketFrame.Close(_, _) => onClose 36 | case WebSocketFrame.Ping(payload) => ws.send(WebSocketFrame.Pong(payload)).as(None) 37 | case WebSocketFrame.Pong(_) => ZIO.succeedNow(None) 38 | case in: WebSocketFrame.Data[_] => ZIO.succeedNow(Some(in)) 39 | }) 40 | .catchSome { case _: WebSocketClosed => ZStream.fromZIO(onClose) } 41 | .interruptWhen(wsClosed) 42 | .flatMap { 43 | case None => ZStream.empty 44 | case Some(f) => ZStream.succeed(f) 45 | }, 46 | ) 47 | .map(ws.send(_)) 48 | .runDrain 49 | .ensuring(ZIO.succeed(ws.close())) 50 | } 51 | } 52 | 53 | class FetchZioBackend private (fetchOptions: FetchOptions, customizeRequest: FetchRequest => FetchRequest) 54 | extends AbstractFetchBackend[Task, ZioStreams, ZioStreams with WebSockets]( 55 | fetchOptions, 56 | customizeRequest, 57 | ZioTaskMonadAsyncError, 58 | ) { 59 | 60 | override val streams: ZioStreams = ZioStreams 61 | 62 | override protected def addCancelTimeoutHook[T](result: Task[T], cancel: () => Unit): Task[T] = 63 | result.ensuring(ZIO.succeed(cancel())) 64 | 65 | override protected def handleStreamBody(s: Stream[Throwable, Array[Byte]]): Task[js.UndefOr[BodyInit]] = { 66 | // as no browsers support a ReadableStream request body yet we need to create an in memory array 67 | // see: https://stackoverflow.com/a/41222366/4094860 68 | val bytes = s.runFold(Array.emptyByteArray) { case (data, item) => data ++ item } 69 | bytes.map(_.toTypedArray.asInstanceOf[BodyInit]) 70 | } 71 | 72 | implicit final class ZIOOps(private val self: ZIO.type) { 73 | def fromPromiseJS[A](promise: js.Promise[A]): ZIO[Any, Throwable, A] = 74 | ??? 75 | } 76 | 77 | override protected def handleResponseAsStream( 78 | response: dom.Response, 79 | ): Task[(Stream[Throwable, Array[Byte]], () => Task[Unit])] = 80 | ZIO.attempt { 81 | lazy val reader = response.body.getReader() 82 | val read = ZIO.fromPromiseJS(reader.read()) 83 | 84 | def go(): Stream[Throwable, Array[Byte]] = 85 | ZStream.fromZIO(read).flatMap { chunk => 86 | if (chunk.done) ZStream.empty 87 | else ZStream(new Int8Array(chunk.value.buffer).toArray) ++ go() 88 | } 89 | 90 | val cancel = ZIO.succeed(reader.cancel("Response body reader cancelled")).unit 91 | (go().ensuring(cancel), () => cancel) 92 | } 93 | 94 | override protected def compileWebSocketPipe( 95 | ws: WebSocket[Task], 96 | pipe: Stream[Throwable, WebSocketFrame.Data[_]] => Stream[Throwable, WebSocketFrame], 97 | ): Task[Unit] = 98 | ZioWebsockets.compilePipe(ws, pipe) 99 | 100 | override implicit def convertFromFuture: ConvertFromFuture[Task] = new ConvertFromFuture[Task] { 101 | override def apply[T](f: Future[T]): Task[T] = ZIO.fromFuture(_ => f) 102 | } 103 | } 104 | 105 | object FetchZioBackend { 106 | def apply( 107 | fetchOptions: FetchOptions = FetchOptions.Default, 108 | customizeRequest: FetchRequest => FetchRequest = identity, 109 | ): SttpBackend[Task, ZioStreams with WebSockets] = 110 | new FetchZioBackend(fetchOptions, customizeRequest) 111 | } 112 | 113 | object ZioTaskMonadAsyncError extends MonadAsyncError[Task] { 114 | override def unit[T](t: T): Task[T] = ZIO.succeedNow(t) 115 | 116 | override def map[T, T2](fa: Task[T])(f: T => T2): Task[T2] = fa.map(f) 117 | 118 | override def flatMap[T, T2](fa: Task[T])(f: T => Task[T2]): Task[T2] = 119 | fa.flatMap(f) 120 | 121 | override def async[T](register: (Either[Throwable, T] => Unit) => Canceler): Task[T] = 122 | ZIO.async { cb => 123 | val canceler = register { 124 | case Left(t) => cb(ZIO.fail(t)) 125 | case Right(t) => cb(ZIO.succeed(t)) 126 | } 127 | ZIO.attempt(canceler.cancel()) 128 | } 129 | 130 | override def error[T](t: Throwable): Task[T] = ZIO.fail(t) 131 | 132 | override protected def handleWrappedError[T](rt: Task[T])(h: PartialFunction[Throwable, Task[T]]): Task[T] = 133 | rt.catchSome(h) 134 | 135 | override def eval[T](t: => T): Task[T] = ZIO.attempt(t) 136 | 137 | override def suspend[T](t: => Task[T]): Task[T] = ZIO.suspend(t) 138 | 139 | override def flatten[T](ffa: Task[Task[T]]): Task[T] = ffa.flatten 140 | 141 | override def ensure[T](f: Task[T], e: => Task[Unit]): Task[T] = f.ensuring(e.orDie) 142 | } 143 | -------------------------------------------------------------------------------- /cli/src/main/scala/tui/TerminalApp.scala: -------------------------------------------------------------------------------- 1 | package tui 2 | 3 | import tui.TerminalApp.Step 4 | import view.View.string2View 5 | import view.{View, _} 6 | import tui.components.{Choose, FancyComponent, LineInput} 7 | import zio.stream._ 8 | import zio._ 9 | 10 | trait TerminalApp[-I, S, +A] { self => 11 | def run(initialState: S): RIO[TUI, A] = 12 | runOption(initialState).map(_.get) 13 | 14 | def runOption(initialState: S): RIO[TUI, Option[A]] = 15 | TUI.run(self)(initialState) 16 | 17 | def render(state: S): View 18 | 19 | def update(state: S, event: TerminalEvent[I]): Step[S, A] 20 | } 21 | 22 | object TerminalApp { 23 | sealed trait Step[+S, +A] 24 | 25 | object Step { 26 | def update[S](state: S): Step[S, Nothing] = Update(state) 27 | def succeed[A](result: A): Step[Nothing, A] = Done(result) 28 | def exit: Step[Nothing, Nothing] = Exit 29 | 30 | private[tui] case class Update[S](state: S) extends Step[S, Nothing] 31 | private[tui] case class Done[A](result: A) extends Step[Nothing, A] 32 | private[tui] case object Exit extends Step[Nothing, Nothing] 33 | } 34 | } 35 | 36 | sealed trait TerminalEvent[+I] 37 | 38 | trait TUI { 39 | def run[I, S, A]( 40 | terminalApp: TerminalApp[I, S, A], 41 | events: ZStream[Any, Throwable, I], 42 | initialState: S 43 | ): Task[Option[A]] 44 | } 45 | 46 | object TUI { 47 | 48 | def live(fullScreen: Boolean): ZLayer[Any, Nothing, TUI] = 49 | ZLayer.succeed(TUILive(fullScreen)) 50 | 51 | def run[I, S, A](terminalApp: TerminalApp[I, S, A])(initialState: S): RIO[TUI, Option[A]] = 52 | ZIO.serviceWithZIO[TUI](_.run(terminalApp, ZStream.never, initialState)) 53 | 54 | def runWithEvents[I, S, A]( 55 | terminalApp: TerminalApp[I, S, A] 56 | )(events: ZStream[Any, Throwable, I], initialState: S): RIO[TUI, Option[A]] = 57 | ZIO.serviceWithZIO[TUI](_.run(terminalApp, events, initialState)) 58 | } 59 | 60 | case class TUILive(fullScreen: Boolean) extends TUI { 61 | var lastSize: Size = Size(0, 0) 62 | 63 | def run[I, S, A]( 64 | terminalApp: TerminalApp[I, S, A], 65 | events: ZStream[Any, Throwable, I], 66 | initialState: S 67 | ): Task[Option[A]] = 68 | ZIO.scoped { 69 | Input 70 | .rawModeScoped(fullScreen) 71 | .flatMap { _ => 72 | for { 73 | stateRef <- SubscriptionRef.make(initialState) 74 | resultPromise <- Promise.make[Nothing, Option[A]] 75 | oldMap: Ref[TextMap] <- Ref.make(TextMap.ofDim(0, 0)) 76 | 77 | _ <- (for { 78 | _ <- ZIO.succeed(Input.ec.clear()) 79 | (width, height) <- ZIO.succeed(Input.terminalSize) 80 | _ <- renderFullScreen(oldMap, terminalApp, initialState, width, height) 81 | } yield ()).when(fullScreen) 82 | 83 | renderStream = 84 | stateRef.changes 85 | .zipLatestWith(Input.terminalSizeStream)((_, _)) 86 | .tap { case (state, (width, height)) => 87 | if (fullScreen) renderFullScreen(oldMap, terminalApp, state, width, height) 88 | else renderTerminal(terminalApp, state) 89 | } 90 | 91 | updateStream = Input.keyEventStream.mergeEither(events).tap { keyEvent => 92 | val event = keyEvent match { 93 | case Left(value) => TerminalEvent.SystemEvent(value) 94 | case Right(value) => TerminalEvent.UserEvent(value) 95 | } 96 | 97 | stateRef.updateZIO { state => 98 | terminalApp.update(state, event) match { 99 | case Step.Update(state) => ZIO.succeed(state) 100 | case Step.Done(result) => resultPromise.succeed(Some(result)).as(state) 101 | case Step.Exit => resultPromise.succeed(None).as(state) 102 | } 103 | } 104 | } 105 | 106 | _ <- ZStream.mergeAllUnbounded()(renderStream, updateStream).interruptWhen(resultPromise.await).runDrain 107 | result <- resultPromise.await 108 | } yield result 109 | } 110 | } 111 | 112 | var lastHeight = 0 113 | var lastWidth = 0 114 | 115 | def renderFullScreen[I, S, A]( 116 | oldMap: Ref[TextMap], 117 | terminalApp: TerminalApp[I, S, A], 118 | state: S, 119 | width: Int, 120 | height: Int 121 | ): UIO[Unit] = 122 | oldMap.update { oldMap => 123 | if (lastWidth != width || lastHeight != height) { 124 | lastHeight = height 125 | lastWidth = width 126 | val map = terminalApp.render(state).center.textMap(width, height) 127 | print(map.toString) 128 | map 129 | } else { 130 | val map = terminalApp.render(state).center.textMap(width, height) 131 | val diff = TextMap.diff(oldMap, map, width, height) 132 | print(diff) 133 | map 134 | } 135 | } 136 | 137 | private def renderTerminal[I, S, A](terminalApp: TerminalApp[I, S, A], state: S): UIO[Unit] = 138 | ZIO.succeed { 139 | val (size, rendered) = terminalApp.render(state).renderNowWithSize 140 | 141 | Input.ec.moveUp(lastSize.height) 142 | Input.ec.clearToEnd() 143 | lastSize = size 144 | println(scala.Console.RESET + rendered + scala.Console.RESET) 145 | } 146 | } 147 | 148 | object TerminalAppExample extends ZIOAppDefault { 149 | override def run = 150 | (for { 151 | number <- Choose.run(List(1, 2, 3, 4, 5, 6))(_.toString.red.bold) 152 | line <- LineInput.run("") 153 | _ <- FancyComponent.run(number.get + line.toIntOption.getOrElse(0)) 154 | } yield ()) 155 | .provide(TUI.live(false)) 156 | } 157 | 158 | object TerminalEvent { 159 | case class UserEvent[+I](event: I) extends TerminalEvent[I] 160 | case class SystemEvent(keyEvent: KeyEvent) extends TerminalEvent[Nothing] 161 | } 162 | -------------------------------------------------------------------------------- /cli-frontend/src/main/scala/zio/app/cli/frontend/Frontend.scala: -------------------------------------------------------------------------------- 1 | package zio.app.cli.frontend 2 | 3 | import animus._ 4 | import boopickle.Default._ 5 | import com.raquo.laminar.api.L._ 6 | import io.laminext.websocket.PickleSocket.WebSocketReceiveBuilderBooPickleOps 7 | import io.laminext.websocket.WebSocket 8 | import org.scalajs.dom.window 9 | import zio.Chunk 10 | import zio.app.cli.protocol._ 11 | 12 | sealed trait ConnectionStatus 13 | 14 | object ConnectionStatus { 15 | case object Online extends ConnectionStatus 16 | case object Connecting extends ConnectionStatus 17 | case object Offline extends ConnectionStatus 18 | } 19 | 20 | object Frontend { 21 | val ws: WebSocket[ServerCommand, ClientCommand] = 22 | WebSocket 23 | .url("ws://localhost:9630/ws") 24 | .pickle[ServerCommand, ClientCommand] 25 | .build(reconnectRetries = Int.MaxValue) 26 | 27 | val $connectionStatus: Signal[ConnectionStatus] = 28 | ws.isConnecting.combineWith(ws.isConnected).map { 29 | case (_, true) => ConnectionStatus.Online 30 | case (true, _) => ConnectionStatus.Connecting 31 | case (false, _) => ConnectionStatus.Offline 32 | } 33 | 34 | val connectionIndicator = div( 35 | cls("status-indicator"), 36 | width("8px"), 37 | height("8px"), 38 | borderRadius("8px"), 39 | backgroundColor <-- $connectionStatus.map { 40 | case ConnectionStatus.Online => "green" 41 | case ConnectionStatus.Connecting => "yellow" 42 | case ConnectionStatus.Offline => "red" 43 | } 44 | ) 45 | 46 | val stateVar = Var(ServerCommand.State(Chunk.empty, Chunk.empty, FileSystemState("", List.empty))) 47 | val inputVar = Var("") 48 | 49 | sealed trait Focus 50 | 51 | object Focus { 52 | case object Frontend extends Focus 53 | case object Backend extends Focus 54 | case object None extends Focus 55 | } 56 | 57 | case class AppState(focus: Focus) { 58 | def focusFrontend: AppState = 59 | copy(focus = focus match { 60 | case Focus.Frontend => Focus.None 61 | case _ => Focus.Frontend 62 | }) 63 | 64 | def focusBackend: AppState = 65 | copy(focus = focus match { 66 | case Focus.Backend => Focus.None 67 | case _ => Focus.Backend 68 | }) 69 | } 70 | 71 | val appStateVar = Var(AppState(Focus.None)) 72 | 73 | case class Rect(x: Double, y: Double, width: Double, height: Double) {} 74 | 75 | object Rect { 76 | def styles(rect: Signal[Rect]): Mod[HtmlElement] = Seq( 77 | left <-- rect.map(_.x).spring.px, 78 | top <-- rect.map(_.y).spring.px, 79 | width <-- rect.map(_.width).spring.px, 80 | height <-- rect.map(_.height).spring.px 81 | ) 82 | } 83 | 84 | case class Grid(backend: HtmlElement, frontend: HtmlElement) extends Component { 85 | val rectVar = Var(Rect(0.0, 0.0, window.innerWidth, window.innerHeight)) 86 | 87 | val $width = rectVar.signal.map(_.width / 2).spring.px 88 | 89 | case class Layout(backendRect: Rect, frontendRect: Rect) 90 | 91 | def layout(available: Rect, focus: Focus): Layout = { 92 | val width = available.width / 2 93 | if (width < 500) { 94 | val backendHeight = focus match { 95 | case Focus.Frontend => 30.0 96 | case Focus.Backend => available.height - 30.0 97 | case Focus.None => available.height / 2.0 98 | } 99 | 100 | val backend = Rect(0, 0, available.width, backendHeight) 101 | val frontend = Rect(0, backendHeight, available.width, available.height - backendHeight) 102 | Layout(backend, frontend) 103 | } else { 104 | // Horizontal Layout 105 | val backendWidth = focus match { 106 | case Focus.Frontend => 30.0 107 | case Focus.Backend => available.width - 30.0 108 | case Focus.None => available.width / 2.0 109 | } 110 | 111 | val backend = Rect(0, 0, backendWidth, available.height) 112 | val frontend = Rect(backendWidth, 0, available.width - backendWidth, available.height) 113 | Layout(backend, frontend) 114 | } 115 | } 116 | 117 | val $layout = rectVar.signal.combineWithFn(appStateVar.signal.map(_.focus))(layout) 118 | 119 | def body: HtmlElement = 120 | div( 121 | cls("main-grid"), 122 | overflow.hidden, 123 | position.relative, 124 | onMountBind { el => 125 | EventStream.periodic(100) --> { _ => 126 | val rect = el.thisNode.ref.getBoundingClientRect() 127 | val rect1 = Rect(rect.left, rect.top, rect.width, rect.height) 128 | rectVar.set(rect1) 129 | } 130 | }, 131 | div( 132 | backend, 133 | position.absolute, 134 | Rect.styles($layout.map(_.backendRect)) 135 | ), 136 | div( 137 | frontend, 138 | position.absolute, 139 | Rect.styles($layout.map(_.frontendRect)) 140 | ) 141 | ) 142 | } 143 | 144 | private def sbtOutputs = 145 | Grid( 146 | SbtOutput( 147 | stateVar.signal.map(_.backendLines), 148 | appStateVar.signal.map(_.focus != Focus.Frontend), 149 | () => appStateVar.update(_.focusBackend), 150 | "BACKEND" 151 | ), 152 | SbtOutput( 153 | stateVar.signal.map(_.frontendLines), 154 | appStateVar.signal.map(_.focus != Focus.Backend), 155 | () => appStateVar.update(_.focusFrontend), 156 | "FRONTEND" 157 | ) 158 | ) 159 | 160 | def header: Div = div( 161 | cls("top-header"), 162 | s"zio-app", 163 | div( 164 | display.flex, 165 | alignItems.center, 166 | a( 167 | fontSize("12px"), 168 | "http://localhost:3000", 169 | href("http://localhost:3000"), 170 | target("_blank"), 171 | textDecoration.none, 172 | color("white"), 173 | padding("4px 6px"), 174 | borderRadius("2px"), 175 | background("rgb(25,25,25") 176 | ), 177 | div(width("12px")), 178 | connectionIndicator 179 | ) 180 | ) 181 | 182 | val $fileSystem = stateVar.signal.map(_.fileSystemState) 183 | 184 | def fileSystem: Div = { 185 | div( 186 | fontSize("16px"), 187 | child.text <-- $fileSystem.map(_.pwd), 188 | children <-- $fileSystem.map(_.dirs).splitTransition(identity) { (_, path, _, transition) => 189 | div( 190 | onClick --> { _ => 191 | ws.sendOne(ClientCommand.ChangeDirectory(path)) 192 | }, 193 | div( 194 | cls("fs-item"), 195 | cursor.pointer, 196 | path 197 | ), 198 | transition.height, 199 | transition.opacity 200 | ) 201 | } 202 | ) 203 | } 204 | 205 | def view: Div = 206 | div( 207 | cls("container"), 208 | header, 209 | sbtOutputs, 210 | windowEvents.onKeyDown --> { 211 | _.key match { 212 | case "f" => appStateVar.update(_.focusFrontend) 213 | case "b" => appStateVar.update(_.focusBackend) 214 | case _ => () 215 | } 216 | }, 217 | ws.connect, 218 | ws.connected --> { _ => 219 | ws.sendOne(ClientCommand.Subscribe) 220 | }, 221 | ws.received --> { (command: ServerCommand) => 222 | command match { 223 | case state: ServerCommand.State => 224 | stateVar.set(state) 225 | } 226 | } 227 | ) 228 | 229 | } 230 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/zio/app/internal/macros/Macros.scala: -------------------------------------------------------------------------------- 1 | package zio.app.internal.macros 2 | 3 | import zio.http._ 4 | import zio.app.ClientConfig 5 | import zio.stream.ZStream 6 | 7 | import scala.language.experimental.macros 8 | import scala.reflect.macros.blackbox 9 | 10 | private[app] class Macros(val c: blackbox.Context) { 11 | import c.universe._ 12 | 13 | // Frontend Macro for deriving Clients 14 | 15 | def client_impl[Service: c.WeakTypeTag]: c.Tree = 16 | client_config_impl[Service](c.Expr[ClientConfig](q"_root_.zio.app.ClientConfig.empty")) 17 | 18 | def client_config_impl[Service: c.WeakTypeTag](config: c.Expr[ClientConfig]): c.Tree = { 19 | val serviceType = c.weakTypeOf[Service] 20 | assertValidMethods(serviceType) 21 | 22 | val appliedTypes = getAppliedTypes(serviceType) 23 | def applyType(tpe: Type): Type = tpe.map(tpe => appliedTypes.getOrElse(tpe.typeSymbol, tpe)) 24 | 25 | val methodDefs = serviceType.decls.collect { case method: MethodSymbol => 26 | val methodName = method.name 27 | 28 | val valDefs = method.paramLists.map { 29 | _.map { param => 30 | val applied = applyType(param.typeSignature) 31 | ValDef(Modifiers(Flag.PARAM), TermName(param.name.toString), tq"$applied", EmptyTree) 32 | } 33 | } 34 | 35 | val params = method.paramLists.flatten.map(param => TermName(param.name.toString)) 36 | val tupleConstructor = TermName(s"Tuple${params.length}") 37 | val pickleType = q"$tupleConstructor(..$params)" 38 | 39 | // 0 1 2 <-- Accesses the return type of the ZIO 40 | // ZIO[R, E, A] 41 | val returnType = applyType(method.returnType.dealias.typeArgs(2)) 42 | val isStream = 43 | method.returnType.dealias.typeConstructor <:< weakTypeOf[ZStream[Any, Nothing, Any]].typeConstructor 44 | 45 | val request = 46 | if (isStream) { 47 | if (params.isEmpty) 48 | q"_root_.zio.app.FrontendUtils.fetchStream[$returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, $config)" 49 | else 50 | q"_root_.zio.app.FrontendUtils.fetchStream[$returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, Pickle.intoBytes($pickleType), $config)" 51 | } else { 52 | if (params.isEmpty) 53 | q"_root_.zio.app.FrontendUtils.fetch[$returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, $config)" 54 | else 55 | q"_root_.zio.app.FrontendUtils.fetch[$returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, Pickle.intoBytes($pickleType), $config)" 56 | } 57 | 58 | q"def $methodName(...$valDefs): ${applyType(method.returnType)} = $request" 59 | } 60 | 61 | val result = q""" 62 | new ${serviceType.finalResultType} { 63 | import _root_.java.nio.ByteBuffer 64 | import _root_.boopickle.Default._ 65 | import _root_.zio.app.internal.CustomPicklers._ 66 | import _root_.zio.app.FrontendUtils.exPickler 67 | 68 | ..$methodDefs 69 | } 70 | """ 71 | result 72 | } 73 | 74 | // Backend Macro for deriving Routes 75 | 76 | // Type -> c.WeakTypeTag 77 | // TypeRepr -> c.Type 78 | def routes_impl[Service: c.WeakTypeTag]: c.Expr[HttpApp[Service, Throwable]] = { 79 | val serviceType = c.weakTypeOf[Service] 80 | assertValidMethods(serviceType) 81 | 82 | val appliedTypes = getAppliedTypes(serviceType) 83 | def applyType(tpe: Type): Type = tpe.map(tpe => appliedTypes.getOrElse(tpe.typeSymbol, tpe)) 84 | 85 | val blocks = serviceType.decls.collect { case method: MethodSymbol => 86 | val methodName = method.name 87 | 88 | // (Int, String) 89 | val argsType = method.paramLists.flatten.collect { 90 | case param: TermSymbol if !param.isImplicit => applyType(param.typeSignature) 91 | } match { 92 | case Nil => tq"Unit" 93 | case a :: Nil => tq"Tuple1[$a]" 94 | case as => tq"(..$as)" 95 | } 96 | 97 | val callMethod = callServiceMethod(serviceType, method) 98 | val isStream = 99 | method.returnType.dealias.typeConstructor <:< weakTypeOf[ZStream[Any, Nothing, Any]].typeConstructor 100 | 101 | // 0 1 2 <-- Accesses the return type of the ZIO 102 | // ZIO[R, E, A] 103 | val errorType = applyType(method.returnType.dealias.typeArgs(1)) 104 | val returnType = applyType(method.returnType.dealias.typeArgs(2)) 105 | 106 | val block = 107 | if (isStream) { 108 | if (method.paramLists.flatten.isEmpty) 109 | q"""_root_.zio.app.internal.BackendUtils.makeRouteNullaryStream[$serviceType, $errorType, $returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, { $callMethod })""" 110 | else 111 | q"""_root_.zio.app.internal.BackendUtils.makeRouteStream[$serviceType, $errorType, $argsType, $returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, { (unpickled: $argsType) => $callMethod })""" 112 | } else { 113 | if (method.paramLists.flatten.isEmpty) 114 | q"""_root_.zio.app.internal.BackendUtils.makeRouteNullary[$serviceType, $errorType, $returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, { $callMethod })""" 115 | else 116 | q"""_root_.zio.app.internal.BackendUtils.makeRoute[$serviceType, $errorType, $argsType, $returnType](${serviceType.finalResultType.toString}, ${methodName.toString}, { (unpickled: $argsType) => $callMethod })""" 117 | } 118 | 119 | block 120 | } 121 | 122 | val block = blocks.reduce((a, b) => q"$a ++ $b") 123 | 124 | val result = c.Expr[HttpApp[Service, Throwable]](q""" 125 | import _root_.zio.http._ 126 | import _root_.boopickle.Default._ 127 | import _root_.zio.app.internal.CustomPicklers._ 128 | import _root_.zio.app.internal.BackendUtils.exPickler 129 | 130 | $block 131 | """) 132 | 133 | result 134 | } 135 | 136 | private def callServiceMethod(service: Type, method: MethodSymbol): c.Tree = { 137 | var idx = 0 138 | 139 | val params = method.paramLists.map { paramList => 140 | paramList.map { _ => 141 | idx += 1 142 | q"unpickled.${TermName("_" + idx)}" 143 | } 144 | } 145 | 146 | if (method.returnType.dealias.typeConstructor <:< typeOf[ZStream[Any, Nothing, Any]].typeConstructor) 147 | q"_root_.zio.stream.ZStream.environmentWithStream[$service](_.get.${method.name}(...$params))" 148 | else 149 | q"_root_.zio.ZIO.serviceWithZIO[$service](_.${method.name}(...$params))" 150 | } 151 | 152 | private def hasTypeParameters(t: Type): Boolean = t match { 153 | case _: PolyType => true 154 | case _ => false 155 | } 156 | 157 | /** 158 | * Assures the given trait's declarations contain no type parameters. 159 | */ 160 | private def assertValidMethods(t: Type): Unit = { 161 | val methods = t.decls.filter(m => hasTypeParameters(m.typeSignature)) 162 | if (methods.nonEmpty) { 163 | c.abort(c.enclosingPosition, s"Invalid methods:\n - ${methods.map(_.name).mkString("\n - ")}") 164 | } 165 | } 166 | 167 | // Symbol -> Type 168 | // Service[A] -> Service[Int] 169 | // Map(A -> Int) 170 | private def getAppliedTypes[Service: c.WeakTypeTag](serviceType: c.Type): Map[c.universe.Symbol, c.universe.Type] = 171 | (serviceType.typeConstructor.typeParams zip serviceType.typeArgs).toMap 172 | } 173 | -------------------------------------------------------------------------------- /cli/src/test/scala/database/ast/SqlSyntaxSpec.scala: -------------------------------------------------------------------------------- 1 | //package database.ast 2 | // 3 | //import zio.Chunk 4 | //import zio.app.database.ast.SQL 5 | //import zio.app.database.ast.SQL.Constraint.PrimaryKey 6 | //import zio.app.database.ast.SQL.{Column, Constraint, CreateTable, SqlType} 7 | //import zio.parser.Syntax 8 | //import zio.test._ 9 | // 10 | //object SqlParsingSpec extends ZIOSpecDefault { 11 | // 12 | // def spec = 13 | // suite("SqlParsingSpec")( 14 | // columnSuite, 15 | // createTableSuite, 16 | // alterTableSuite, 17 | // ) 18 | // 19 | // def assertSyntaxEquality[A](string: String, value: A)( 20 | // syntax: Syntax[String, Char, Char, A, A], 21 | // )(implicit trace: ZTraceElement): Assert = { 22 | // val parsed = syntax.parseString(string) 23 | // val printed = syntax.print(value).map(_.mkString) 24 | // assertTrue( 25 | // printed.toOption.get.trim == string, 26 | // parsed.toOption.get == value, 27 | // ) 28 | // } 29 | // 30 | // def assertSyntaxEquality[A](value: A)( 31 | // syntax: Syntax[String, Char, Char, A, A], 32 | // )(implicit trace: ZTraceElement): Assert = { 33 | // val printed = syntax.print(value).map(_.mkString) 34 | // println(trace) 35 | // println(printed) 36 | // val parsed = syntax.parseString(printed.toOption.get.trim) 37 | // assertTrue( 38 | // value == parsed.toOption.get, 39 | // ) 40 | // } 41 | // 42 | // // Fixtures 43 | // 44 | // lazy val columnSuite = 45 | // suite("Column")( 46 | // test("idColumn") { 47 | // assertSyntaxEquality( 48 | // "id VARCHAR(255) DEFAULT gen_random_uuid() NOT NULL CONSTRAINT question_pk PRIMARY KEY", 49 | // Column( 50 | // name = "id", 51 | // columnType = SqlType.VarChar(255), 52 | // constraints = Chunk( 53 | // Constraint.Default("gen_random_uuid()"), 54 | // Constraint.NotNull, 55 | // Constraint.PrimaryKey(Some("question_pk")), 56 | // ), 57 | // ), 58 | // )(SqlSyntax.column) 59 | // }, 60 | // test("nameColumn") { 61 | // assertSyntaxEquality( 62 | // "name VARCHAR(255)", 63 | // Column( 64 | // name = "name", 65 | // columnType = SqlType.VarChar(255), 66 | // constraints = Chunk(), 67 | // ), 68 | // )(SqlSyntax.column) 69 | // }, 70 | // test("primary key without name") { 71 | // assertSyntaxEquality( 72 | // "name TEXT PRIMARY KEY", 73 | // Column( 74 | // name = "name", 75 | // columnType = SqlType.Text, 76 | // constraints = Chunk( 77 | // PrimaryKey(None), 78 | // ), 79 | // ), 80 | // )(SqlSyntax.column) 81 | // }, 82 | // ) 83 | // 84 | // lazy val createTableSuite = 85 | // suite("CREATE TABLE")( 86 | // test("user table") { 87 | // assertSyntaxEquality( 88 | // "CREATE TABLE user (id VARCHAR(255) DEFAULT gen_random_uuid() NOT NULL CONSTRAINT question_pk PRIMARY KEY, name VARCHAR(255));", 89 | // CreateTable( 90 | // "user", 91 | // Chunk( 92 | // Column( 93 | // name = "id", 94 | // columnType = SqlType.VarChar(255), 95 | // constraints = Chunk( 96 | // Constraint.Default("gen_random_uuid()"), 97 | // Constraint.NotNull, 98 | // Constraint.PrimaryKey(Some("question_pk")), 99 | // ), 100 | // ), 101 | // Column( 102 | // name = "name", 103 | // columnType = SqlType.VarChar(255), 104 | // constraints = Chunk( 105 | // ), 106 | // ), 107 | // ), 108 | // ifNotExists = false, 109 | // ), 110 | // )(SqlSyntax.createTable) 111 | // }, 112 | // test("IF NOT EXISTS") { 113 | // assertSyntaxEquality( 114 | // "CREATE TABLE IF NOT EXISTS user (id UUID);", 115 | // CreateTable( 116 | // name = "user", 117 | // columns = Chunk( 118 | // Column("id", SqlType.UUID, Chunk()), 119 | // ), 120 | // ifNotExists = true, 121 | // ), 122 | // )(SqlSyntax.createTable) 123 | // }, 124 | // test("parsing") { 125 | // val input = 126 | // """ 127 | //CREATE TABLE question( 128 | // id uuid default gen_random_uuid() NOT NULL 129 | // CONSTRAINT question_pk 130 | // PRIMARY KEY, 131 | // question TEXT NOT NULL, 132 | // author VARCHAR(255) NOT NULL 133 | //); 134 | //""".trim 135 | // 136 | // val result = SqlSyntax.createTable.parseString(input) 137 | // val createTable = 138 | // CreateTable( 139 | // name = "question", 140 | // columns = Chunk( 141 | // Column( 142 | // name = "id", 143 | // columnType = SqlType.UUID, 144 | // constraints = Chunk( 145 | // Constraint.Default("gen_random_uuid()"), 146 | // Constraint.NotNull, 147 | // PrimaryKey( 148 | // name = Some("question_pk"), 149 | // ), 150 | // ), 151 | // ), 152 | // Column( 153 | // name = "question", 154 | // columnType = SqlType.Text, 155 | // constraints = Chunk( 156 | // Constraint.NotNull, 157 | // ), 158 | // ), 159 | // Column( 160 | // name = "author", 161 | // columnType = SqlType.VarChar(size = 255), 162 | // constraints = Chunk( 163 | // Constraint.NotNull, 164 | // ), 165 | // ), 166 | // ), 167 | // ifNotExists = false, 168 | // ) 169 | // 170 | // val printed = SqlSyntax.createTable.print(createTable).map(_.mkString) 171 | // 172 | // val expected = 173 | // "CREATE TABLE question (id UUID DEFAULT gen_random_uuid() NOT NULL CONSTRAINT question_pk PRIMARY KEY, question TEXT NOT NULL, author VARCHAR(255) NOT NULL);" 174 | // 175 | // assertTrue( 176 | // result.toOption.get == createTable, 177 | // printed.toOption.get == expected, 178 | // ) 179 | // }, 180 | // ) 181 | // 182 | // lazy val alterTableSuite = 183 | // suite("ALTER TABLE")( 184 | // suite("ADD COLUMN")( 185 | // test("simple") { 186 | // assertSyntaxEquality( 187 | // """ 188 | //ALTER TABLE user 189 | // ADD COLUMN name VARCHAR(255); 190 | //""".trim, 191 | // SQL.AlterTable( 192 | // name = "user", 193 | // actions = Chunk( 194 | // SQL.AlterTable.Action.AddColumn( 195 | // column = Column( 196 | // name = "name", 197 | // columnType = SqlType.VarChar(255), 198 | // constraints = Chunk(), 199 | // ), 200 | // ifNotExists = false, 201 | // ), 202 | // ), 203 | // ifExists = false, 204 | // ), 205 | // )(SqlSyntax.alterTable) 206 | // }, 207 | // test("multiple columns") { 208 | // assertSyntaxEquality( 209 | // """ 210 | //ALTER TABLE user 211 | // ADD COLUMN name VARCHAR(255), 212 | // ADD COLUMN IF NOT EXISTS age INTEGER, 213 | // ALTER COLUMN id SET DEFAULT gen_random_uuid(), 214 | // ALTER COLUMN name SET NOT NULL, 215 | // ALTER COLUMN name DROP NOT NULL, 216 | // DROP COLUMN cruft, 217 | // DROP COLUMN IF EXISTS power, 218 | // DROP COLUMN IF EXISTS power CASCADE, 219 | // DROP COLUMN power RESTRICT; 220 | //""".trim, 221 | // SQL.AlterTable( 222 | // name = "user", 223 | // actions = Chunk( 224 | // SQL.AlterTable.Action.AddColumn( 225 | // column = Column( 226 | // name = "name", 227 | // columnType = SqlType.VarChar(255), 228 | // constraints = Chunk(), 229 | // ), 230 | // ifNotExists = false, 231 | // ), 232 | // SQL.AlterTable.Action.AddColumn( 233 | // column = Column( 234 | // name = "age", 235 | // columnType = SqlType.Integer, 236 | // constraints = Chunk(), 237 | // ), 238 | // ifNotExists = true, 239 | // ), 240 | // SQL.AlterTable.Action.SetColumnDefault( 241 | // name = "id", 242 | // expression = "gen_random_uuid()", 243 | // ), 244 | // SQL.AlterTable.Action.SetColumnNotNull( 245 | // name = "name", 246 | // isNotNull = true, 247 | // ), 248 | // SQL.AlterTable.Action.SetColumnNotNull( 249 | // name = "name", 250 | // isNotNull = false, 251 | // ), 252 | // SQL.AlterTable.Action.DropColumn( 253 | // name = "cruft", 254 | // ifExists = false, 255 | // cascade = false, 256 | // restrict = false, 257 | // ), 258 | // SQL.AlterTable.Action.DropColumn( 259 | // name = "power", 260 | // ifExists = true, 261 | // cascade = false, 262 | // restrict = false, 263 | // ), 264 | // SQL.AlterTable.Action.DropColumn( 265 | // name = "power", 266 | // ifExists = true, 267 | // cascade = true, 268 | // restrict = false, 269 | // ), 270 | // SQL.AlterTable.Action.DropColumn( 271 | // name = "power", 272 | // ifExists = false, 273 | // cascade = false, 274 | // restrict = true, 275 | // ), 276 | // ), 277 | // ifExists = false, 278 | // ), 279 | // )(SqlSyntax.alterTable) 280 | // }, 281 | // ), 282 | // ) 283 | //} 284 | // 285 | //object TabularExample { 286 | // 287 | // val example = 288 | // """ 289 | //CREATE TABLE question( 290 | // id uuid default gen_random_uuid() NOT NULL 291 | // CONSTRAINT question_pk 292 | // PRIMARY KEY, 293 | // question TEXT NOT NULL, 294 | // author VARCHAR(255) NOT NULL, 295 | // author_id uuid REFERENCES user(id), 296 | // age INTEGER 297 | //); 298 | //""".trim 299 | // 300 | // def main(args: Array[String]): Unit = { 301 | // val sql = SqlSyntax.createTable.parseString(example).toOption.get 302 | // println(sql) 303 | // } 304 | // 305 | //} 306 | -------------------------------------------------------------------------------- /cli/src/main/g8/frontend/src/main/static/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,900&display=swap'); 2 | 3 | 4 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 5 | 6 | /* Document 7 | ========================================================================== */ 8 | 9 | /** 10 | * 1. Correct the line height in all browsers. 11 | * 2. Prevent adjustments of font size after orientation changes in iOS. 12 | */ 13 | 14 | html { 15 | line-height: 1.15; /* 1 */ 16 | -webkit-text-size-adjust: 100%; /* 2 */ 17 | } 18 | 19 | /* Sections 20 | ========================================================================== */ 21 | 22 | /** 23 | * Remove the margin in all browsers. 24 | */ 25 | 26 | body { 27 | font-family: "Source Code Pro", BlinkMacSystemFont, sans-serif; 28 | font-size: 28px; 29 | background: #333 !important; 30 | color: white; 31 | box-sizing: border-box; 32 | margin: 40px; 33 | } 34 | 35 | 36 | button { 37 | cursor: pointer; 38 | border: 1px solid #555; 39 | border-radius: 4px; 40 | color: rgba(255,255,255,0.9); 41 | font-size: 18px !important; 42 | outline: none; 43 | background: #223; 44 | width: 100%; 45 | font-weight: bold; 46 | 47 | &.cancel { 48 | background: #222; 49 | } 50 | } 51 | 52 | textarea { 53 | background: #222; 54 | margin-bottom: 12px !important; 55 | border: 1px solid #333; 56 | border-radius: 4px; 57 | padding: 8px; 58 | outline: none; 59 | color: white; 60 | width: 100%; 61 | resize: none; 62 | font-size: 22px !important; 63 | } 64 | 65 | form { 66 | padding: 20px; 67 | border-radius: 4px; 68 | background: #111; 69 | border: 1px solid #222; 70 | } 71 | 72 | .all-questions { 73 | max-height: 300px; 74 | overflow-y: scroll; 75 | } 76 | 77 | .vote-vote { 78 | transition: width 0.5s; 79 | } 80 | 81 | 82 | .slide { 83 | transition: all 0.3s 0.0s; 84 | position: relative; 85 | } 86 | 87 | 88 | .panel-visible { 89 | transition: all 0.5s 0.0s; 90 | opacity: 1; 91 | position: relative; 92 | transform: translateY(0px); 93 | } 94 | 95 | .panel-hidden { 96 | transition: all 0.3s 0.0s; 97 | opacity: 0; 98 | position: relative; 99 | transform: translateY(50px); 100 | } 101 | 102 | .slide-next, .slide-previous { 103 | opacity: 0; 104 | transform: rotateY(90deg); 105 | } 106 | 107 | .slide-app { 108 | transition: all 0.4s; 109 | border-radius: 0px; 110 | border: 1px solid #000; 111 | margin: 0px; 112 | } 113 | 114 | .slide-app-shrink { 115 | transition: all 0.8s; 116 | border-radius: 8px; 117 | border: 1px solid #555; 118 | margin: 24px; 119 | } 120 | 121 | 122 | .hidden { 123 | transition: all 0.4s; 124 | opacity: 0; 125 | } 126 | 127 | .visible { 128 | opacity: 1; 129 | transition: all 0.4s; 130 | } 131 | 132 | .slide-current { 133 | left: 0px; 134 | z-index: 10; 135 | transition: all 0.4s; 136 | transform: rotateY(0deg); 137 | } 138 | 139 | .slide-next { 140 | left: -80px; 141 | // transform: rotateZ(-4deg); 142 | transform: rotateY(-90deg); 143 | } 144 | 145 | .slide-previous { 146 | left: 80px; 147 | } 148 | 149 | .input-group { 150 | + .input-group { 151 | margin-top: 24px; 152 | } 153 | 154 | input { 155 | font-size: 22px; 156 | padding: 8px; 157 | } 158 | } 159 | 160 | 161 | label { 162 | text-transform: uppercase; 163 | font-size: 14px; 164 | opacity: 0.8; 165 | display: block; 166 | margin-bottom: 8px; 167 | } 168 | 169 | input { 170 | background: #222; 171 | border: 1px solid #333; 172 | border-radius: 4px; 173 | margin-bottom: 16px; 174 | color: white; 175 | 176 | } 177 | 178 | ::placeholder { 179 | color: rgba(255,255,255,0.25); 180 | } 181 | 182 | /** 183 | * Render the `main` element consistently in IE. 184 | */ 185 | 186 | main { 187 | display: block; 188 | } 189 | 190 | /** 191 | * Correct the font size and margin on `h1` elements within `section` and 192 | * `article` contexts in Chrome, Firefox, and Safari. 193 | */ 194 | 195 | h1 { 196 | font-size: 2em; 197 | margin: 0.67em 0; 198 | } 199 | 200 | /* Grouping content 201 | ========================================================================== */ 202 | 203 | /** 204 | * 1. Add the correct box sizing in Firefox. 205 | * 2. Show the overflow in Edge and IE. 206 | */ 207 | 208 | hr { 209 | box-sizing: content-box; /* 1 */ 210 | height: 0; /* 1 */ 211 | overflow: visible; /* 2 */ 212 | } 213 | 214 | /** 215 | * 1. Correct the inheritance and scaling of font size in all browsers. 216 | * 2. Correct the odd `em` font sizing in all browsers. 217 | */ 218 | 219 | pre { 220 | font-family: "Source Code Pro", monospace, monospace; /* 1 */ 221 | font-size: 1em; /* 2 */ 222 | } 223 | 224 | /* Text-level semantics 225 | ========================================================================== */ 226 | 227 | /** 228 | * Remove the gray background on active links in IE 10. 229 | */ 230 | 231 | a { 232 | background-color: transparent; 233 | } 234 | 235 | /** 236 | * 1. Remove the bottom border in Chrome 57- 237 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 238 | */ 239 | 240 | abbr[title] { 241 | border-bottom: none; /* 1 */ 242 | text-decoration: underline; /* 2 */ 243 | text-decoration: underline dotted; /* 2 */ 244 | } 245 | 246 | /** 247 | * Add the correct font weight in Chrome, Edge, and Safari. 248 | */ 249 | 250 | b, 251 | strong { 252 | font-weight: bolder; 253 | } 254 | 255 | /** 256 | * 1. Correct the inheritance and scaling of font size in all browsers. 257 | * 2. Correct the odd `em` font sizing in all browsers. 258 | */ 259 | 260 | code, 261 | kbd, 262 | samp { 263 | font-family: monospace, monospace; /* 1 */ 264 | font-size: 1em; /* 2 */ 265 | } 266 | 267 | /** 268 | * Add the correct font size in all browsers. 269 | */ 270 | 271 | small { 272 | font-size: 80%; 273 | } 274 | 275 | /** 276 | * Prevent `sub` and `sup` elements from affecting the line height in 277 | * all browsers. 278 | */ 279 | 280 | sub, 281 | sup { 282 | font-size: 75%; 283 | line-height: 0; 284 | position: relative; 285 | vertical-align: baseline; 286 | } 287 | 288 | sub { 289 | bottom: -0.25em; 290 | } 291 | 292 | sup { 293 | top: -0.5em; 294 | } 295 | 296 | /* Embedded content 297 | ========================================================================== */ 298 | 299 | /** 300 | * Remove the border on images inside links in IE 10. 301 | */ 302 | 303 | img { 304 | border-style: none; 305 | } 306 | 307 | /* Forms 308 | ========================================================================== */ 309 | 310 | /** 311 | * 1. Change the font styles in all browsers. 312 | * 2. Remove the margin in Firefox and Safari. 313 | */ 314 | 315 | button, 316 | input, 317 | optgroup, 318 | select, 319 | textarea { 320 | font-family: inherit; /* 1 */ 321 | font-size: 100%; /* 1 */ 322 | line-height: 1.15; /* 1 */ 323 | margin: 0; /* 2 */ 324 | } 325 | 326 | /** 327 | * Show the overflow in IE. 328 | * 1. Show the overflow in Edge. 329 | */ 330 | 331 | button, 332 | input { /* 1 */ 333 | overflow: visible; 334 | } 335 | 336 | /** 337 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 338 | * 1. Remove the inheritance of text transform in Firefox. 339 | */ 340 | 341 | button, 342 | select { /* 1 */ 343 | text-transform: none; 344 | } 345 | 346 | /** 347 | * Correct the inability to style clickable types in iOS and Safari. 348 | */ 349 | 350 | button, 351 | [type="button"], 352 | [type="reset"], 353 | [type="submit"] { 354 | -webkit-appearance: button; 355 | } 356 | 357 | /** 358 | * Remove the inner border and padding in Firefox. 359 | */ 360 | 361 | button::-moz-focus-inner, 362 | [type="button"]::-moz-focus-inner, 363 | [type="reset"]::-moz-focus-inner, 364 | [type="submit"]::-moz-focus-inner { 365 | border-style: none; 366 | padding: 0; 367 | } 368 | 369 | /** 370 | * Restore the focus styles unset by the previous rule. 371 | */ 372 | 373 | button:-moz-focusring, 374 | [type="button"]:-moz-focusring, 375 | [type="reset"]:-moz-focusring, 376 | [type="submit"]:-moz-focusring { 377 | outline: 1px dotted ButtonText; 378 | } 379 | 380 | /** 381 | * Correct the padding in Firefox. 382 | */ 383 | 384 | fieldset { 385 | padding: 0.35em 0.75em 0.625em; 386 | } 387 | 388 | /** 389 | * 1. Correct the text wrapping in Edge and IE. 390 | * 2. Correct the color inheritance from `fieldset` elements in IE. 391 | * 3. Remove the padding so developers are not caught out when they zero out 392 | * `fieldset` elements in all browsers. 393 | */ 394 | 395 | legend { 396 | box-sizing: border-box; /* 1 */ 397 | color: inherit; /* 2 */ 398 | display: table; /* 1 */ 399 | max-width: 100%; /* 1 */ 400 | padding: 0; /* 3 */ 401 | white-space: normal; /* 1 */ 402 | } 403 | 404 | /** 405 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 406 | */ 407 | 408 | progress { 409 | vertical-align: baseline; 410 | } 411 | 412 | /** 413 | * Remove the default vertical scrollbar in IE 10+. 414 | */ 415 | 416 | textarea { 417 | overflow: auto; 418 | } 419 | 420 | /** 421 | * 1. Add the correct box sizing in IE 10. 422 | * 2. Remove the padding in IE 10. 423 | */ 424 | 425 | [type="checkbox"], 426 | [type="radio"] { 427 | box-sizing: border-box; /* 1 */ 428 | padding: 0; /* 2 */ 429 | } 430 | 431 | /** 432 | * Correct the cursor style of increment and decrement buttons in Chrome. 433 | */ 434 | 435 | [type="number"]::-webkit-inner-spin-button, 436 | [type="number"]::-webkit-outer-spin-button { 437 | height: auto; 438 | } 439 | 440 | /** 441 | * 1. Correct the odd appearance in Chrome and Safari. 442 | * 2. Correct the outline style in Safari. 443 | */ 444 | 445 | [type="search"] { 446 | -webkit-appearance: textfield; /* 1 */ 447 | outline-offset: -2px; /* 2 */ 448 | } 449 | 450 | /** 451 | * Remove the inner padding in Chrome and Safari on macOS. 452 | */ 453 | 454 | [type="search"]::-webkit-search-decoration { 455 | -webkit-appearance: none; 456 | } 457 | 458 | /** 459 | * 1. Correct the inability to style clickable types in iOS and Safari. 460 | * 2. Change font properties to `inherit` in Safari. 461 | */ 462 | 463 | ::-webkit-file-upload-button { 464 | -webkit-appearance: button; /* 1 */ 465 | font: inherit; /* 2 */ 466 | } 467 | 468 | /* Interactive 469 | ========================================================================== */ 470 | 471 | /* 472 | * Add the correct display in Edge, IE 10+, and Firefox. 473 | */ 474 | 475 | details { 476 | display: block; 477 | } 478 | 479 | /* 480 | * Add the correct display in all browsers. 481 | */ 482 | 483 | summary { 484 | display: list-item; 485 | } 486 | 487 | /* Misc 488 | ========================================================================== */ 489 | 490 | /** 491 | * Add the correct display in IE 10+. 492 | */ 493 | 494 | template { 495 | display: none; 496 | } 497 | 498 | /** 499 | * Add the correct display in IE 10. 500 | */ 501 | 502 | [hidden] { 503 | display: none; 504 | } -------------------------------------------------------------------------------- /cli-frontend/src/main/static/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200;0,300;0,400;0,500;0,600;0,700;0,900;1,200;1,300;1,400;1,500;1,600;1,700;1,900&display=swap'); 2 | 3 | 4 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 5 | 6 | /* Document 7 | ========================================================================== */ 8 | 9 | /** 10 | * 1. Correct the line height in all browsers. 11 | * 2. Prevent adjustments of font size after orientation changes in iOS. 12 | */ 13 | 14 | html { 15 | line-height: 1.15; /* 1 */ 16 | -webkit-text-size-adjust: 100%; /* 2 */ 17 | } 18 | 19 | .top-header { 20 | font-size: 16px; 21 | padding: 12px; 22 | background: #222; 23 | border-bottom: 1px solid #333; 24 | display: flex; 25 | justify-content: space-between; 26 | align-items: center; 27 | } 28 | 29 | .status-indicator { 30 | transition: background-color 0.6s; 31 | } 32 | 33 | .main { 34 | padding: 20px; 35 | } 36 | 37 | .console-red { 38 | color: rgb(255, 0, 0) 39 | } 40 | 41 | .console-green { 42 | color: rgb(87, 238, 87) 43 | } 44 | 45 | .console-cyan { 46 | color: cyan 47 | } 48 | 49 | .console-magenta { 50 | color: magenta 51 | } 52 | 53 | .console-blue { 54 | color: rgb(78, 189, 255) 55 | } 56 | 57 | .console-yellow { 58 | color: rgb(255, 190, 78) 59 | } 60 | 61 | .container { 62 | display: flex; 63 | flex-direction: column; 64 | height: 100vh; 65 | } 66 | 67 | .fs-item { 68 | padding: 8px; 69 | border-top: 1px solid #333; 70 | background: #222; 71 | border-radius: 2px; 72 | 73 | &:hover { 74 | background: rgb(40, 40, 40); 75 | } 76 | } 77 | 78 | .main-grid { 79 | height: 100%; 80 | //display: grid; 81 | //grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); 82 | //grid-gap: 12px; 83 | } 84 | 85 | .sbt-output { 86 | background: rgb(30, 30, 30); 87 | height: 100%; 88 | font-size: 16px; 89 | border: 1px solid #333; 90 | position: relative; 91 | 92 | &:after { 93 | content: ''; 94 | position: absolute; 95 | top: 0; 96 | left: 0; 97 | right: 0; 98 | bottom: 0; 99 | pointer-events: none; 100 | transition: background-color 0.5s; 101 | background: rgba(0, 0, 0, 0.0); 102 | } 103 | 104 | &.disabled { 105 | &:after { 106 | background: rgba(0, 0, 0, 0.3); 107 | } 108 | } 109 | 110 | .sbt-output-title { 111 | font-size: 14px; 112 | color: #EEE; 113 | background: rgb(40, 40, 40); 114 | display: flex; 115 | justify-content: space-between; 116 | align-items: center; 117 | padding: 6px 0 6px 12px; 118 | border-bottom: 1px solid #333; 119 | } 120 | 121 | .sbt-output-body { 122 | height: calc(100% - 52px); 123 | padding: 12px; 124 | line-height: 1.4em; 125 | overflow: scroll; 126 | } 127 | } 128 | 129 | .hover-button { 130 | display: flex; 131 | justify-content: center; 132 | padding: 8px; 133 | width: 12px; 134 | height: 12px; 135 | background: rgb(25, 25, 25); 136 | 137 | &:hover { 138 | background: rgb(20, 20, 20); 139 | } 140 | 141 | } 142 | 143 | 144 | /* Sections 145 | ========================================================================== */ 146 | 147 | /** 148 | * Remove the margin in all browsers. 149 | */ 150 | 151 | body { 152 | font-family: "Source Code Pro", BlinkMacSystemFont, sans-serif; 153 | font-size: 28px; 154 | background: #111 !important; 155 | color: white; 156 | box-sizing: border-box; 157 | margin: 0; 158 | padding: 0; 159 | height: 100%; 160 | } 161 | 162 | div { 163 | box-sizing: border-box; 164 | } 165 | 166 | 167 | button { 168 | cursor: pointer; 169 | border: 1px solid #555; 170 | border-radius: 4px; 171 | color: rgba(255, 255, 255, 0.9); 172 | font-size: 18px !important; 173 | outline: none; 174 | background: #223; 175 | width: 100%; 176 | font-weight: bold; 177 | 178 | &.cancel { 179 | background: #222; 180 | } 181 | } 182 | 183 | textarea { 184 | background: #222; 185 | margin-bottom: 12px !important; 186 | border: 1px solid #333; 187 | border-radius: 4px; 188 | padding: 8px; 189 | outline: none; 190 | color: white; 191 | width: 100%; 192 | resize: none; 193 | font-size: 22px !important; 194 | } 195 | 196 | form { 197 | padding: 20px; 198 | border-radius: 4px; 199 | background: #111; 200 | border: 1px solid #222; 201 | } 202 | 203 | .all-questions { 204 | max-height: 300px; 205 | overflow-y: scroll; 206 | } 207 | 208 | .vote-vote { 209 | transition: width 0.5s; 210 | } 211 | 212 | 213 | .slide { 214 | transition: all 0.3s 0.0s; 215 | position: relative; 216 | } 217 | 218 | 219 | .panel-visible { 220 | transition: all 0.5s 0.0s; 221 | opacity: 1; 222 | position: relative; 223 | transform: translateY(0px); 224 | } 225 | 226 | .panel-hidden { 227 | transition: all 0.3s 0.0s; 228 | opacity: 0; 229 | position: relative; 230 | transform: translateY(50px); 231 | } 232 | 233 | .slide-next, .slide-previous { 234 | opacity: 0; 235 | transform: rotateY(90deg); 236 | } 237 | 238 | .slide-app { 239 | transition: all 0.4s; 240 | border-radius: 0px; 241 | border: 1px solid #000; 242 | margin: 0px; 243 | } 244 | 245 | .slide-app-shrink { 246 | transition: all 0.8s; 247 | border-radius: 8px; 248 | border: 1px solid #555; 249 | margin: 24px; 250 | } 251 | 252 | 253 | .hidden { 254 | transition: all 0.4s; 255 | opacity: 0; 256 | } 257 | 258 | .visible { 259 | opacity: 1; 260 | transition: all 0.4s; 261 | } 262 | 263 | .slide-current { 264 | left: 0px; 265 | z-index: 10; 266 | transition: all 0.4s; 267 | transform: rotateY(0deg); 268 | } 269 | 270 | .slide-next { 271 | left: -80px; 272 | // transform: rotateZ(-4deg); 273 | transform: rotateY(-90deg); 274 | } 275 | 276 | .slide-previous { 277 | left: 80px; 278 | } 279 | 280 | .input-group { 281 | + .input-group { 282 | margin-top: 24px; 283 | } 284 | 285 | input { 286 | font-size: 22px; 287 | padding: 8px; 288 | } 289 | } 290 | 291 | 292 | label { 293 | text-transform: uppercase; 294 | font-size: 14px; 295 | opacity: 0.8; 296 | display: block; 297 | margin-bottom: 8px; 298 | } 299 | 300 | input { 301 | background: #222; 302 | border: 1px solid #333; 303 | border-radius: 4px; 304 | margin-bottom: 16px; 305 | color: white; 306 | 307 | } 308 | 309 | ::placeholder { 310 | color: rgba(255, 255, 255, 0.25); 311 | } 312 | 313 | /** 314 | * Render the `main` element consistently in IE. 315 | */ 316 | 317 | main { 318 | display: block; 319 | } 320 | 321 | /** 322 | * Correct the font size and margin on `h1` elements within `section` and 323 | * `article` contexts in Chrome, Firefox, and Safari. 324 | */ 325 | 326 | h1 { 327 | font-size: 2em; 328 | margin: 0.67em 0; 329 | } 330 | 331 | /* Grouping content 332 | ========================================================================== */ 333 | 334 | /** 335 | * 1. Add the correct box sizing in Firefox. 336 | * 2. Show the overflow in Edge and IE. 337 | */ 338 | 339 | hr { 340 | box-sizing: border-box; /* 1 */ 341 | height: 0; /* 1 */ 342 | overflow: visible; /* 2 */ 343 | } 344 | 345 | /** 346 | * 1. Correct the inheritance and scaling of font size in all browsers. 347 | * 2. Correct the odd `em` font sizing in all browsers. 348 | */ 349 | 350 | pre { 351 | margin: 0; 352 | font-family: "Source Code Pro", monospace, monospace; /* 1 */ 353 | font-size: 1em; /* 2 */ 354 | } 355 | 356 | /* Text-level semantics 357 | ========================================================================== */ 358 | 359 | /** 360 | * Remove the gray background on active links in IE 10. 361 | */ 362 | 363 | a { 364 | background-color: transparent; 365 | } 366 | 367 | /** 368 | * 1. Remove the bottom border in Chrome 57- 369 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 370 | */ 371 | 372 | abbr[title] { 373 | border-bottom: none; /* 1 */ 374 | text-decoration: underline; /* 2 */ 375 | text-decoration: underline dotted; /* 2 */ 376 | } 377 | 378 | /** 379 | * Add the correct font weight in Chrome, Edge, and Safari. 380 | */ 381 | 382 | b, 383 | strong { 384 | font-weight: bolder; 385 | } 386 | 387 | /** 388 | * 1. Correct the inheritance and scaling of font size in all browsers. 389 | * 2. Correct the odd `em` font sizing in all browsers. 390 | */ 391 | 392 | code, 393 | kbd, 394 | samp { 395 | font-family: monospace, monospace; /* 1 */ 396 | font-size: 1em; /* 2 */ 397 | } 398 | 399 | /** 400 | * Add the correct font size in all browsers. 401 | */ 402 | 403 | small { 404 | font-size: 80%; 405 | } 406 | 407 | /** 408 | * Prevent `sub` and `sup` elements from affecting the line height in 409 | * all browsers. 410 | */ 411 | 412 | sub, 413 | sup { 414 | font-size: 75%; 415 | line-height: 0; 416 | position: relative; 417 | vertical-align: baseline; 418 | } 419 | 420 | sub { 421 | bottom: -0.25em; 422 | } 423 | 424 | sup { 425 | top: -0.5em; 426 | } 427 | 428 | /* Embedded content 429 | ========================================================================== */ 430 | 431 | /** 432 | * Remove the border on images inside links in IE 10. 433 | */ 434 | 435 | img { 436 | border-style: none; 437 | } 438 | 439 | /* Forms 440 | ========================================================================== */ 441 | 442 | /** 443 | * 1. Change the font styles in all browsers. 444 | * 2. Remove the margin in Firefox and Safari. 445 | */ 446 | 447 | button, 448 | input, 449 | optgroup, 450 | select, 451 | textarea { 452 | font-family: inherit; /* 1 */ 453 | font-size: 100%; /* 1 */ 454 | line-height: 1.15; /* 1 */ 455 | margin: 0; /* 2 */ 456 | } 457 | 458 | /** 459 | * Show the overflow in IE. 460 | * 1. Show the overflow in Edge. 461 | */ 462 | 463 | button, 464 | input { /* 1 */ 465 | overflow: visible; 466 | } 467 | 468 | /** 469 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 470 | * 1. Remove the inheritance of text transform in Firefox. 471 | */ 472 | 473 | button, 474 | select { /* 1 */ 475 | text-transform: none; 476 | } 477 | 478 | /** 479 | * Correct the inability to style clickable types in iOS and Safari. 480 | */ 481 | 482 | button, 483 | [type="button"], 484 | [type="reset"], 485 | [type="submit"] { 486 | -webkit-appearance: button; 487 | } 488 | 489 | /** 490 | * Remove the inner border and padding in Firefox. 491 | */ 492 | 493 | button::-moz-focus-inner, 494 | [type="button"]::-moz-focus-inner, 495 | [type="reset"]::-moz-focus-inner, 496 | [type="submit"]::-moz-focus-inner { 497 | border-style: none; 498 | padding: 0; 499 | } 500 | 501 | /** 502 | * Restore the focus styles unset by the previous rule. 503 | */ 504 | 505 | button:-moz-focusring, 506 | [type="button"]:-moz-focusring, 507 | [type="reset"]:-moz-focusring, 508 | [type="submit"]:-moz-focusring { 509 | outline: 1px dotted ButtonText; 510 | } 511 | 512 | /** 513 | * Correct the padding in Firefox. 514 | */ 515 | 516 | fieldset { 517 | padding: 0.35em 0.75em 0.625em; 518 | } 519 | 520 | /** 521 | * 1. Correct the text wrapping in Edge and IE. 522 | * 2. Correct the color inheritance from `fieldset` elements in IE. 523 | * 3. Remove the padding so developers are not caught out when they zero out 524 | * `fieldset` elements in all browsers. 525 | */ 526 | 527 | legend { 528 | box-sizing: border-box; /* 1 */ 529 | color: inherit; /* 2 */ 530 | display: table; /* 1 */ 531 | max-width: 100%; /* 1 */ 532 | padding: 0; /* 3 */ 533 | white-space: normal; /* 1 */ 534 | } 535 | 536 | /** 537 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 538 | */ 539 | 540 | progress { 541 | vertical-align: baseline; 542 | } 543 | 544 | /** 545 | * Remove the default vertical scrollbar in IE 10+. 546 | */ 547 | 548 | textarea { 549 | overflow: auto; 550 | } 551 | 552 | /** 553 | * 1. Add the correct box sizing in IE 10. 554 | * 2. Remove the padding in IE 10. 555 | */ 556 | 557 | [type="checkbox"], 558 | [type="radio"] { 559 | box-sizing: border-box; /* 1 */ 560 | padding: 0; /* 2 */ 561 | } 562 | 563 | /** 564 | * Correct the cursor style of increment and decrement buttons in Chrome. 565 | */ 566 | 567 | [type="number"]::-webkit-inner-spin-button, 568 | [type="number"]::-webkit-outer-spin-button { 569 | height: auto; 570 | } 571 | 572 | /** 573 | * 1. Correct the odd appearance in Chrome and Safari. 574 | * 2. Correct the outline style in Safari. 575 | */ 576 | 577 | [type="search"] { 578 | -webkit-appearance: textfield; /* 1 */ 579 | outline-offset: -2px; /* 2 */ 580 | } 581 | 582 | /** 583 | * Remove the inner padding in Chrome and Safari on macOS. 584 | */ 585 | 586 | [type="search"]::-webkit-search-decoration { 587 | -webkit-appearance: none; 588 | } 589 | 590 | /** 591 | * 1. Correct the inability to style clickable types in iOS and Safari. 592 | * 2. Change font properties to `inherit` in Safari. 593 | */ 594 | 595 | ::-webkit-file-upload-button { 596 | -webkit-appearance: button; /* 1 */ 597 | font: inherit; /* 2 */ 598 | } 599 | 600 | /* Interactive 601 | ========================================================================== */ 602 | 603 | /* 604 | * Add the correct display in Edge, IE 10+, and Firefox. 605 | */ 606 | 607 | details { 608 | display: block; 609 | } 610 | 611 | /* 612 | * Add the correct display in all browsers. 613 | */ 614 | 615 | summary { 616 | display: list-item; 617 | } 618 | 619 | /* Misc 620 | ========================================================================== */ 621 | 622 | /** 623 | * Add the correct display in IE 10+. 624 | */ 625 | 626 | template { 627 | display: none; 628 | } 629 | 630 | /** 631 | * Add the correct display in IE 10. 632 | */ 633 | 634 | [hidden] { 635 | display: none; 636 | } -------------------------------------------------------------------------------- /examples/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ansi-styles@^3.2.1: 6 | version "3.2.1" 7 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 8 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 9 | dependencies: 10 | color-convert "^1.9.0" 11 | 12 | async@0.9.x: 13 | version "0.9.2" 14 | resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" 15 | integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= 16 | 17 | at-least-node@^1.0.0: 18 | version "1.0.0" 19 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 20 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 21 | 22 | balanced-match@^1.0.0: 23 | version "1.0.2" 24 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 25 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 26 | 27 | brace-expansion@^1.1.7: 28 | version "1.1.11" 29 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 30 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 31 | dependencies: 32 | balanced-match "^1.0.0" 33 | concat-map "0.0.1" 34 | 35 | buffer-from@^1.0.0: 36 | version "1.1.1" 37 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 38 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 39 | 40 | camel-case@^4.1.1: 41 | version "4.1.2" 42 | resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" 43 | integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== 44 | dependencies: 45 | pascal-case "^3.1.2" 46 | tslib "^2.0.3" 47 | 48 | chalk@^2.4.2: 49 | version "2.4.2" 50 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 51 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 52 | dependencies: 53 | ansi-styles "^3.2.1" 54 | escape-string-regexp "^1.0.5" 55 | supports-color "^5.3.0" 56 | 57 | clean-css@^4.2.3: 58 | version "4.2.3" 59 | resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" 60 | integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== 61 | dependencies: 62 | source-map "~0.6.0" 63 | 64 | color-convert@^1.9.0: 65 | version "1.9.3" 66 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 67 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 68 | dependencies: 69 | color-name "1.1.3" 70 | 71 | color-name@1.1.3: 72 | version "1.1.3" 73 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 74 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 75 | 76 | colorette@^1.2.2: 77 | version "1.2.2" 78 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" 79 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== 80 | 81 | commander@^2.20.0: 82 | version "2.20.3" 83 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 84 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 85 | 86 | commander@^4.1.1: 87 | version "4.1.1" 88 | resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" 89 | integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== 90 | 91 | concat-map@0.0.1: 92 | version "0.0.1" 93 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 94 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 95 | 96 | dot-case@^3.0.4: 97 | version "3.0.4" 98 | resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" 99 | integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== 100 | dependencies: 101 | no-case "^3.0.4" 102 | tslib "^2.0.3" 103 | 104 | ejs@^3.1.6: 105 | version "3.1.6" 106 | resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" 107 | integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== 108 | dependencies: 109 | jake "^10.6.1" 110 | 111 | esbuild@^0.11.23: 112 | version "0.11.23" 113 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.11.23.tgz#c42534f632e165120671d64db67883634333b4b8" 114 | integrity sha512-iaiZZ9vUF5wJV8ob1tl+5aJTrwDczlvGP0JoMmnpC2B0ppiMCu8n8gmy5ZTGl5bcG081XBVn+U+jP+mPFm5T5Q== 115 | 116 | escape-string-regexp@^1.0.5: 117 | version "1.0.5" 118 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 119 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 120 | 121 | filelist@^1.0.1: 122 | version "1.0.2" 123 | resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" 124 | integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== 125 | dependencies: 126 | minimatch "^3.0.4" 127 | 128 | fs-extra@^9.1.0: 129 | version "9.1.0" 130 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" 131 | integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== 132 | dependencies: 133 | at-least-node "^1.0.0" 134 | graceful-fs "^4.2.0" 135 | jsonfile "^6.0.1" 136 | universalify "^2.0.0" 137 | 138 | fsevents@~2.3.1: 139 | version "2.3.2" 140 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 141 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 142 | 143 | function-bind@^1.1.1: 144 | version "1.1.1" 145 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 146 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 147 | 148 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 149 | version "4.2.6" 150 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" 151 | integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== 152 | 153 | has-flag@^3.0.0: 154 | version "3.0.0" 155 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 156 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 157 | 158 | has@^1.0.3: 159 | version "1.0.3" 160 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 161 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 162 | dependencies: 163 | function-bind "^1.1.1" 164 | 165 | he@^1.2.0: 166 | version "1.2.0" 167 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 168 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 169 | 170 | html-minifier-terser@^5.1.1: 171 | version "5.1.1" 172 | resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" 173 | integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== 174 | dependencies: 175 | camel-case "^4.1.1" 176 | clean-css "^4.2.3" 177 | commander "^4.1.1" 178 | he "^1.2.0" 179 | param-case "^3.0.3" 180 | relateurl "^0.2.7" 181 | terser "^4.6.3" 182 | 183 | is-core-module@^2.2.0: 184 | version "2.4.0" 185 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" 186 | integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== 187 | dependencies: 188 | has "^1.0.3" 189 | 190 | jake@^10.6.1: 191 | version "10.8.2" 192 | resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" 193 | integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== 194 | dependencies: 195 | async "0.9.x" 196 | chalk "^2.4.2" 197 | filelist "^1.0.1" 198 | minimatch "^3.0.4" 199 | 200 | jsonfile@^6.0.1: 201 | version "6.1.0" 202 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" 203 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== 204 | dependencies: 205 | universalify "^2.0.0" 206 | optionalDependencies: 207 | graceful-fs "^4.1.6" 208 | 209 | lower-case@^2.0.2: 210 | version "2.0.2" 211 | resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" 212 | integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== 213 | dependencies: 214 | tslib "^2.0.3" 215 | 216 | minimatch@^3.0.4: 217 | version "3.0.4" 218 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 219 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 220 | dependencies: 221 | brace-expansion "^1.1.7" 222 | 223 | nanoid@^3.1.23: 224 | version "3.1.23" 225 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81" 226 | integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw== 227 | 228 | no-case@^3.0.4: 229 | version "3.0.4" 230 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" 231 | integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== 232 | dependencies: 233 | lower-case "^2.0.2" 234 | tslib "^2.0.3" 235 | 236 | param-case@^3.0.3: 237 | version "3.0.4" 238 | resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" 239 | integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== 240 | dependencies: 241 | dot-case "^3.0.4" 242 | tslib "^2.0.3" 243 | 244 | pascal-case@^3.1.2: 245 | version "3.1.2" 246 | resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" 247 | integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== 248 | dependencies: 249 | no-case "^3.0.4" 250 | tslib "^2.0.3" 251 | 252 | path-parse@^1.0.6: 253 | version "1.0.6" 254 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 255 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 256 | 257 | postcss@^8.2.10: 258 | version "8.2.15" 259 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.15.tgz#9e66ccf07292817d226fc315cbbf9bc148fbca65" 260 | integrity sha512-2zO3b26eJD/8rb106Qu2o7Qgg52ND5HPjcyQiK2B98O388h43A448LCslC0dI2P97wCAQRJsFvwTRcXxTKds+Q== 261 | dependencies: 262 | colorette "^1.2.2" 263 | nanoid "^3.1.23" 264 | source-map "^0.6.1" 265 | 266 | relateurl@^0.2.7: 267 | version "0.2.7" 268 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" 269 | integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= 270 | 271 | resolve@^1.19.0: 272 | version "1.20.0" 273 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" 274 | integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== 275 | dependencies: 276 | is-core-module "^2.2.0" 277 | path-parse "^1.0.6" 278 | 279 | rollup@^2.38.5: 280 | version "2.48.0" 281 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.48.0.tgz#fceb01ed771f991f29f7bd2ff7838146e55acb74" 282 | integrity sha512-wl9ZSSSsi5579oscSDYSzGn092tCS076YB+TQrzsGuSfYyJeep8eEWj0eaRjuC5McuMNmcnR8icBqiE/FWNB1A== 283 | optionalDependencies: 284 | fsevents "~2.3.1" 285 | 286 | source-map-support@~0.5.12: 287 | version "0.5.19" 288 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 289 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 290 | dependencies: 291 | buffer-from "^1.0.0" 292 | source-map "^0.6.0" 293 | 294 | source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: 295 | version "0.6.1" 296 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 297 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 298 | 299 | supports-color@^5.3.0: 300 | version "5.5.0" 301 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 302 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 303 | dependencies: 304 | has-flag "^3.0.0" 305 | 306 | terser@^4.6.3: 307 | version "4.8.0" 308 | resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" 309 | integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== 310 | dependencies: 311 | commander "^2.20.0" 312 | source-map "~0.6.1" 313 | source-map-support "~0.5.12" 314 | 315 | tslib@^2.0.3: 316 | version "2.2.0" 317 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c" 318 | integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== 319 | 320 | universalify@^2.0.0: 321 | version "2.0.0" 322 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" 323 | integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== 324 | 325 | vite-plugin-html@^2.0.7: 326 | version "2.0.7" 327 | resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-2.0.7.tgz#55139ff8a843ba3b3d5cd48610ca6f6afdf7c23d" 328 | integrity sha512-c2fFBxRqP+jbJX/uMZ6pTXcOWZHEtjcki+u609+KIOIjhR+nDY6zBbRvCy29n9Sc0X+CBhFcj2pPLLAtS5ERJQ== 329 | dependencies: 330 | ejs "^3.1.6" 331 | fs-extra "^9.1.0" 332 | html-minifier-terser "^5.1.1" 333 | 334 | vite@^2.3.3: 335 | version "2.3.3" 336 | resolved "https://registry.yarnpkg.com/vite/-/vite-2.3.3.tgz#7e88a71abd03985c647789938d784cce0ee3b0fd" 337 | integrity sha512-eO1iwRbn3/BfkNVMNJDeANAFCZ5NobYOFPu7IqfY7DcI7I9nFGjJIZid0EViTmLDGwwSUPmRAq3cRBbO3+DsMA== 338 | dependencies: 339 | esbuild "^0.11.23" 340 | postcss "^8.2.10" 341 | resolve "^1.19.0" 342 | rollup "^2.38.5" 343 | optionalDependencies: 344 | fsevents "~2.3.1" 345 | -------------------------------------------------------------------------------- /cli/src/main/scala/view/View.scala: -------------------------------------------------------------------------------- 1 | package view 2 | 3 | import view.View.string2View 4 | import zio.Chunk 5 | import tui.StringSyntax.StringOps 6 | 7 | import scala.language.implicitConversions 8 | 9 | sealed trait View { self => 10 | 11 | def renderNow: String = { 12 | val termSize = Input.terminalSize 13 | val size = self.size(Size(termSize._1, termSize._2)) 14 | render(size.width, size.height) 15 | } 16 | 17 | def renderNowWithSize: (Size, String) = { 18 | val termSize = Input.terminalSize 19 | val size = self.size(Size(termSize._1, termSize._2)) 20 | size -> render(size.width, size.height) 21 | } 22 | 23 | def renderNowWithTextMap: (Size, TextMap) = { 24 | val termSize = Input.terminalSize 25 | val size = self.size(Size(termSize._1, termSize._2)) 26 | size -> textMap(size.width, size.height) 27 | } 28 | 29 | def bordered: View = 30 | View.Border(self.padding(1, 0)) 31 | 32 | def borderedTight: View = 33 | View.Border(self) 34 | 35 | def center: View = 36 | flex(maxWidth = Some(Int.MaxValue), maxHeight = Some(Int.MaxValue)) 37 | 38 | def top: View = 39 | flex(maxWidth = Some(Int.MaxValue), maxHeight = Some(Int.MaxValue), alignment = Alignment.top) 40 | 41 | def centerH: View = 42 | flex(maxWidth = Some(Int.MaxValue)) 43 | 44 | def right: View = 45 | flex(maxWidth = Some(Int.MaxValue), alignment = Alignment.right) 46 | 47 | def left: View = 48 | flex(maxWidth = Some(Int.MaxValue), alignment = Alignment.left) 49 | 50 | def bottomLeft: View = 51 | flex(maxWidth = Some(Int.MaxValue), maxHeight = Some(Int.MaxValue), alignment = Alignment.bottomLeft) 52 | 53 | def bottomRight: View = 54 | flex(maxWidth = Some(Int.MaxValue), maxHeight = Some(Int.MaxValue), alignment = Alignment.bottomRight) 55 | 56 | def centerV: View = 57 | flex(maxHeight = Some(Int.MaxValue)) 58 | 59 | def padding(amount: Int): View = padding(horizontal = amount, vertical = amount) 60 | 61 | def paddingH(amount: Int): View = padding(amount, 0) 62 | 63 | def paddingV(amount: Int): View = padding(0, amount) 64 | 65 | def padding(horizontal: Int, vertical: Int): View = 66 | View.Padding(self, vertical, vertical, horizontal, horizontal) 67 | 68 | def padding(left: Int = 0, top: Int = 0, right: Int = 0, bottom: Int = 0): View = 69 | View.Padding(self, top, bottom, left, right) 70 | 71 | def overlay(view: View, alignment: Alignment = Alignment.center): View = View.Overlay(self, view, alignment) 72 | 73 | def frame(width: Int, height: Int, alignment: Alignment = Alignment.center): View = 74 | View.FixedFrame(self, Some(width), Some(height), alignment) 75 | 76 | def flex( 77 | minWidth: Option[Int] = None, 78 | maxWidth: Option[Int] = None, 79 | minHeight: Option[Int] = None, 80 | maxHeight: Option[Int] = None, 81 | alignment: Alignment = Alignment.center 82 | ): View = 83 | View.FlexibleFrame(self, minWidth, maxWidth, minHeight, maxHeight, alignment) 84 | 85 | def size(proposed: Size): Size 86 | 87 | def render(context: RenderContext, size: Size): Unit 88 | 89 | def blue: View = color(Color.Blue) 90 | def cyan: View = color(Color.Cyan) 91 | def green: View = color(Color.Green) 92 | def magenta: View = color(Color.Magenta) 93 | def red: View = color(Color.Red) 94 | def white: View = color(Color.White) 95 | def yellow: View = color(Color.Yellow) 96 | 97 | def color(color: Color): View = 98 | transform { case View.Text(string, None, style) => View.Text(string, Some(color), style) } 99 | 100 | def bold: View = style(Style.Bold) 101 | def dim: View = style(Style.Dim) 102 | def underlined: View = style(Style.Underlined) 103 | def inverted: View = style(Style.Reversed) 104 | def reversed: View = style(Style.Reversed) 105 | 106 | def style(style: Style): View = 107 | transform { case View.Text(string, color, None) => View.Text(string, color, Some(style)) } 108 | 109 | def transform(pf: PartialFunction[View, View]): View = { 110 | pf.lift(self).getOrElse(self) match { 111 | case text: View.Text => 112 | text 113 | case View.Padding(view, top, bottom, left, right) => 114 | View.Padding(view.transform(pf), top, bottom, left, right) 115 | case View.Horizontal(views, spacing, alignment) => 116 | View.Horizontal(views.map(_.transform(pf)), spacing, alignment) 117 | case View.Vertical(views, spacing, alignment) => 118 | View.Vertical(views.map(_.transform(pf)), spacing, alignment) 119 | case View.Border(view) => 120 | View.Border(view.transform(pf)) 121 | case View.Overlay(view, overlay, alignment) => 122 | View.Overlay(view.transform(pf), overlay, alignment) 123 | case View.FlexibleFrame(view, minWidth, maxWidth, minHeight, maxHeight, alignment) => 124 | View.FlexibleFrame(view.transform(pf), minWidth, maxWidth, minHeight, maxHeight, alignment) 125 | case View.FixedFrame(view, width, height, alignment) => 126 | View.FixedFrame(view.transform(pf), width, height, alignment) 127 | case _ => 128 | throw new Error("OH NO") 129 | } 130 | } 131 | 132 | def render(width: Int, height: Int): String = { 133 | val context = new RenderContext(TextMap.ofDim(width, height), 0, 0) 134 | self.render(context, Size(width, height)) 135 | context.textMap.toString 136 | } 137 | 138 | def textMap(width: Int, height: Int): TextMap = { 139 | val context = new RenderContext(TextMap.ofDim(width, height), 0, 0) 140 | self.render(context, Size(width, height)) 141 | context.textMap 142 | } 143 | 144 | } 145 | 146 | object View { 147 | def withSize(f: Size => View): View = View.WithSize(f) 148 | 149 | def text(string: String): View = View.Text( 150 | string.removingAnsiCodes 151 | .replaceAll("\\n", "") 152 | .replaceAll("\\t", " "), 153 | None, 154 | None 155 | ) 156 | 157 | def text(string: String, color: Color): View = View.Text( 158 | string.removingAnsiCodes 159 | .replaceAll("\\n", "") 160 | .replaceAll("\\t", " "), 161 | Some(color), 162 | None 163 | ) 164 | 165 | def horizontal(views: View*): View = 166 | View.Horizontal(Chunk.fromIterable(views)) 167 | 168 | def horizontal(spacing: Int)(views: View*): View = 169 | View.Horizontal(Chunk.fromIterable(views), spacing) 170 | 171 | def vertical(views: View*): View = 172 | View.Vertical(Chunk.fromIterable(views), alignment = HorizontalAlignment.Left) 173 | 174 | implicit def string2View(string: String): View = text(string) 175 | 176 | case class Padding(view: View, topP: Int, bottomP: Int, leftP: Int, rightP: Int) extends View { 177 | lazy val horizontal: Int = leftP + rightP 178 | lazy val vertical: Int = topP + bottomP 179 | 180 | override def size(proposed: Size): Size = 181 | view 182 | .size(proposed.scaled(horizontal * -1, vertical * -1)) 183 | .scaled(horizontal, vertical) 184 | 185 | override def render(context: RenderContext, size: Size): Unit = { 186 | val childSize = view.size(size.scaled(horizontal * -1, vertical * -1)) 187 | context.scratch { 188 | context.translateBy(leftP, topP) 189 | view.render(context, childSize) 190 | } 191 | } 192 | } 193 | 194 | case class Horizontal(views: Chunk[View], spacing: Int = 1, alignment: VerticalAlignment = VerticalAlignment.Center) 195 | extends View { 196 | override def size(proposed: Size): Size = { 197 | val sizes = layout(proposed) 198 | Size(sizes.map(_.width).sum, sizes.map(_.height).maxOption.getOrElse(0)) 199 | } 200 | 201 | override def render(context: RenderContext, size: Size): Unit = { 202 | val selfY = alignment.point(size.height) 203 | val sizes = layout(size) 204 | var currentX = 0 205 | views.zipWith(sizes) { (view, childSize) => 206 | context.scratch { 207 | val childY = alignment.point(childSize.height) 208 | context.translateBy(currentX, selfY - childY) 209 | view.render(context, childSize) 210 | } 211 | currentX += childSize.width + spacing 212 | } 213 | } 214 | 215 | private def layout(proposed: Size): Chunk[Size] = { 216 | val sizes: Array[Size] = Array.ofDim(views.length) 217 | 218 | val viewsWithFlex = views.zipWithIndex 219 | .map { case (view, idx) => 220 | val lower = view.size(Size(0, proposed.height)).width 221 | val upper = view.size(Size(Int.MaxValue, proposed.height)).width 222 | (idx, view, upper - lower) 223 | } 224 | .sortBy(_._3) 225 | 226 | val total = views.length 227 | var remaining = proposed.width 228 | var idx = 0 229 | 230 | viewsWithFlex.foreach { case (i, view, _) => 231 | val width = remaining / (total - idx) 232 | val childSize = view.size(Size(width, proposed.height)) 233 | idx += 1 234 | remaining -= childSize.width 235 | sizes(i) = childSize 236 | } 237 | 238 | val result = Chunk.fromArray(sizes) 239 | result 240 | } 241 | 242 | } 243 | 244 | case class Vertical(views: Chunk[View], spacing: Int = 0, alignment: HorizontalAlignment = HorizontalAlignment.Center) 245 | extends View { 246 | override def size(proposed: Size): Size = { 247 | val sizes = layout(proposed) 248 | val result = Size(sizes.map(_.width).maxOption.getOrElse(0), sizes.map(_.height).sum) 249 | result 250 | } 251 | 252 | override def render(context: RenderContext, size: Size): Unit = { 253 | val sizes = layout(size) 254 | var currentY = 0 255 | views.zipWith(sizes) { (view, childSize) => 256 | context.scratch { 257 | context.translateBy(0, currentY) 258 | context.align(childSize, Size(size.width, childSize.height), Alignment(alignment, VerticalAlignment.Center)) 259 | view.render(context, childSize) 260 | } 261 | currentY += childSize.height + spacing 262 | } 263 | } 264 | 265 | private def layout(proposed: Size): Chunk[Size] = { 266 | val total = views.length 267 | var remaining = proposed.height - (spacing * (total - 1)) 268 | var idx = 0 269 | val sizes = views.flatMap { view => 270 | if (remaining <= 0) None 271 | else { 272 | val childSize = view.size(Size(proposed.width, remaining / (total - idx))) 273 | idx += 1 274 | remaining -= childSize.height 275 | Some(childSize) 276 | } 277 | } 278 | sizes 279 | } 280 | 281 | } 282 | 283 | case class Text(string: String, color: Option[Color], style: Option[Style]) extends View { 284 | lazy val length: Int = string.length 285 | 286 | override def size(proposed: Size): Size = 287 | Size(width = string.length min proposed.width, height = 1) 288 | 289 | override def render(context: RenderContext, size: Size): Unit = { 290 | val taken = string.take(size.width) 291 | context.insert(taken, color.getOrElse(Color.Default), style.getOrElse(Style.Default)) 292 | } 293 | } 294 | 295 | case class Border(view: View) extends View { 296 | override def size(proposed: Size): Size = { 297 | view.size(proposed.scaled(-2, -2)).scaled(2, 2) 298 | } 299 | 300 | override def render(context: RenderContext, size: Size): Unit = { 301 | val childSize = view.size(size.scaled(-2, -2)) 302 | 303 | val top = "┌" + ("─" * childSize.width) + "┐" 304 | val bottom = "└" + ("─" * childSize.width) + "┘" 305 | 306 | context.scratch { 307 | context.translateBy(1, 1) 308 | view.render(context, childSize) 309 | } 310 | 311 | context.insert(top, style = Style.Dim) 312 | context.textMap.insert(bottom, context.x, context.y + childSize.height + 1, style = Style.Dim) 313 | (1 to childSize.height).foreach { dy => 314 | context.textMap.add('│', context.x, context.y + dy, style = Style.Dim) 315 | context.textMap.add('│', context.x + childSize.width + 1, context.y + dy, style = Style.Dim) 316 | } 317 | } 318 | } 319 | 320 | case class Overlay(view: View, overlay: View, alignment: Alignment) extends View { self => 321 | override def size(proposed: Size): Size = view.size(proposed) 322 | 323 | override def render(context: RenderContext, size: Size): Unit = { 324 | view.render(context, size) 325 | context.scratch { 326 | val childSize = overlay.size(size) 327 | context.align(childSize, size, alignment) 328 | overlay.render(context, childSize) 329 | } 330 | } 331 | } 332 | 333 | case class FlexibleFrame( 334 | view: View, 335 | minWidth: Option[Int], 336 | maxWidth: Option[Int], 337 | minHeight: Option[Int], 338 | maxHeight: Option[Int], 339 | alignment: Alignment = Alignment.center 340 | ) extends View { 341 | override def size(proposed0: Size): Size = { 342 | var proposed = proposed0 343 | proposed = proposed.overriding(width = minWidth.filter(_ > proposed.width)) 344 | proposed = proposed.overriding(width = maxWidth.filter(_ < proposed.width)) 345 | proposed = proposed.overriding(height = minHeight.filter(_ > proposed.height)) 346 | proposed = proposed.overriding(height = maxHeight.filter(_ < proposed.height)) 347 | var result = view.size(proposed) 348 | minWidth.foreach { m => 349 | result = result.copy(width = m.max(result.width.min(proposed.width))) 350 | } 351 | maxWidth.foreach { m => 352 | result = result.copy(width = m.min(result.width.max(proposed.width))) 353 | } 354 | minHeight.foreach { m => 355 | result = result.copy(height = m.max(result.height.min(proposed.height))) 356 | } 357 | maxHeight.foreach { m => 358 | result = result.copy(height = m.min(result.height.max(proposed.height))) 359 | } 360 | result 361 | } 362 | 363 | override def render(context: RenderContext, size: Size): Unit = { 364 | context.scratch { 365 | val childSize = view.size(size) 366 | context.align(childSize, size, alignment) 367 | view.render(context, childSize) 368 | } 369 | } 370 | } 371 | 372 | case class FixedFrame(view: View, width: Option[Int], height: Option[Int], alignment: Alignment) extends View { 373 | override def size(proposed: Size): Size = { 374 | lazy val childSize = view.size(proposed.overriding(width, height)) 375 | Size(width.getOrElse(childSize.width), height.getOrElse(childSize.height)) 376 | } 377 | 378 | override def render(context: RenderContext, size: Size): Unit = { 379 | val childSize = view.size(size) 380 | context.scratch { 381 | context.align(childSize, size, alignment) 382 | view.render(context, childSize) 383 | } 384 | } 385 | } 386 | 387 | case class WithSize(f: Size => View) extends View { 388 | override def size(proposed: Size): Size = { 389 | f(proposed).size(proposed) 390 | } 391 | 392 | override def render(context: RenderContext, size: Size): Unit = { 393 | f(size).render(context, size) 394 | } 395 | } 396 | } 397 | 398 | object FrameExamples { 399 | def main(args: Array[String]): Unit = { 400 | println( 401 | View 402 | .horizontal( 403 | View 404 | .text("zio-app") 405 | .center 406 | .bordered 407 | .yellow 408 | .underlined, 409 | View 410 | .text("zio-app") 411 | .center 412 | .bordered 413 | .reversed 414 | .red 415 | ) 416 | .padding(bottom = 1) 417 | // .renderNow 418 | .render(42, 7) 419 | ) 420 | 421 | println( 422 | View 423 | .horizontal( 424 | View 425 | .text("zio-app") 426 | .center 427 | .bordered 428 | .yellow 429 | .underlined, 430 | View 431 | .text("zio-app") 432 | .center 433 | .bordered 434 | .reversed 435 | .red 436 | ) 437 | .padding(bottom = 2) 438 | // .renderNow 439 | .render(42, 7) 440 | ) 441 | } 442 | 443 | } 444 | -------------------------------------------------------------------------------- /cli/src/main/g8/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | ansi-styles@^3.2.1: 6 | version "3.2.1" 7 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 8 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 9 | dependencies: 10 | color-convert "^1.9.0" 11 | 12 | anymatch@~3.1.1: 13 | version "3.1.1" 14 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" 15 | integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== 16 | dependencies: 17 | normalize-path "^3.0.0" 18 | picomatch "^2.0.4" 19 | 20 | async@0.9.x: 21 | version "0.9.2" 22 | resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" 23 | integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= 24 | 25 | at-least-node@^1.0.0: 26 | version "1.0.0" 27 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 28 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 29 | 30 | balanced-match@^1.0.0: 31 | version "1.0.0" 32 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 33 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 34 | 35 | binary-extensions@^2.0.0: 36 | version "2.2.0" 37 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 38 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 39 | 40 | brace-expansion@^1.1.7: 41 | version "1.1.11" 42 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 43 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 44 | dependencies: 45 | balanced-match "^1.0.0" 46 | concat-map "0.0.1" 47 | 48 | braces@~3.0.2: 49 | version "3.0.2" 50 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 51 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 52 | dependencies: 53 | fill-range "^7.0.1" 54 | 55 | buffer-from@^1.0.0: 56 | version "1.1.1" 57 | resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" 58 | integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== 59 | 60 | camel-case@^4.1.1: 61 | version "4.1.2" 62 | resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" 63 | integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== 64 | dependencies: 65 | pascal-case "^3.1.2" 66 | tslib "^2.0.3" 67 | 68 | chalk@^2.4.2: 69 | version "2.4.2" 70 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" 71 | integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== 72 | dependencies: 73 | ansi-styles "^3.2.1" 74 | escape-string-regexp "^1.0.5" 75 | supports-color "^5.3.0" 76 | 77 | "chokidar@>=2.0.0 <4.0.0": 78 | version "3.5.1" 79 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" 80 | integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw== 81 | dependencies: 82 | anymatch "~3.1.1" 83 | braces "~3.0.2" 84 | glob-parent "~5.1.0" 85 | is-binary-path "~2.1.0" 86 | is-glob "~4.0.1" 87 | normalize-path "~3.0.0" 88 | readdirp "~3.5.0" 89 | optionalDependencies: 90 | fsevents "~2.3.1" 91 | 92 | clean-css@^4.2.3: 93 | version "4.2.3" 94 | resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" 95 | integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== 96 | dependencies: 97 | source-map "~0.6.0" 98 | 99 | color-convert@^1.9.0: 100 | version "1.9.3" 101 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 102 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 103 | dependencies: 104 | color-name "1.1.3" 105 | 106 | color-name@1.1.3: 107 | version "1.1.3" 108 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 109 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 110 | 111 | colorette@^1.2.2: 112 | version "1.2.2" 113 | resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" 114 | integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== 115 | 116 | commander@^2.20.0: 117 | version "2.20.3" 118 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 119 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 120 | 121 | commander@^4.1.1: 122 | version "4.1.1" 123 | resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" 124 | integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== 125 | 126 | concat-map@0.0.1: 127 | version "0.0.1" 128 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 129 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 130 | 131 | dot-case@^3.0.4: 132 | version "3.0.4" 133 | resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" 134 | integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== 135 | dependencies: 136 | no-case "^3.0.4" 137 | tslib "^2.0.3" 138 | 139 | ejs@^3.1.6: 140 | version "3.1.6" 141 | resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.6.tgz#5bfd0a0689743bb5268b3550cceeebbc1702822a" 142 | integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== 143 | dependencies: 144 | jake "^10.6.1" 145 | 146 | esbuild@^0.9.3: 147 | version "0.9.7" 148 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.9.7.tgz#ea0d639cbe4b88ec25fbed4d6ff00c8d788ef70b" 149 | integrity sha512-VtUf6aQ89VTmMLKrWHYG50uByMF4JQlVysb8dmg6cOgW8JnFCipmz7p+HNBl+RR3LLCuBxFGVauAe2wfnF9bLg== 150 | 151 | escape-string-regexp@^1.0.5: 152 | version "1.0.5" 153 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 154 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 155 | 156 | filelist@^1.0.1: 157 | version "1.0.2" 158 | resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.2.tgz#80202f21462d4d1c2e214119b1807c1bc0380e5b" 159 | integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== 160 | dependencies: 161 | minimatch "^3.0.4" 162 | 163 | fill-range@^7.0.1: 164 | version "7.0.1" 165 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 166 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 167 | dependencies: 168 | to-regex-range "^5.0.1" 169 | 170 | fs-extra@^9.1.0: 171 | version "9.1.0" 172 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" 173 | integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== 174 | dependencies: 175 | at-least-node "^1.0.0" 176 | graceful-fs "^4.2.0" 177 | jsonfile "^6.0.1" 178 | universalify "^2.0.0" 179 | 180 | fsevents@~2.3.1: 181 | version "2.3.2" 182 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 183 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 184 | 185 | function-bind@^1.1.1: 186 | version "1.1.1" 187 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 188 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 189 | 190 | glob-parent@~5.1.0: 191 | version "5.1.2" 192 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 193 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 194 | dependencies: 195 | is-glob "^4.0.1" 196 | 197 | graceful-fs@^4.1.6, graceful-fs@^4.2.0: 198 | version "4.2.6" 199 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" 200 | integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== 201 | 202 | has-flag@^3.0.0: 203 | version "3.0.0" 204 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 205 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 206 | 207 | has@^1.0.3: 208 | version "1.0.3" 209 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 210 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 211 | dependencies: 212 | function-bind "^1.1.1" 213 | 214 | he@^1.2.0: 215 | version "1.2.0" 216 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 217 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 218 | 219 | html-minifier-terser@^5.1.1: 220 | version "5.1.1" 221 | resolved "https://registry.yarnpkg.com/html-minifier-terser/-/html-minifier-terser-5.1.1.tgz#922e96f1f3bb60832c2634b79884096389b1f054" 222 | integrity sha512-ZPr5MNObqnV/T9akshPKbVgyOqLmy+Bxo7juKCfTfnjNniTAMdy4hz21YQqoofMBJD2kdREaqPPdThoR78Tgxg== 223 | dependencies: 224 | camel-case "^4.1.1" 225 | clean-css "^4.2.3" 226 | commander "^4.1.1" 227 | he "^1.2.0" 228 | param-case "^3.0.3" 229 | relateurl "^0.2.7" 230 | terser "^4.6.3" 231 | 232 | is-binary-path@~2.1.0: 233 | version "2.1.0" 234 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 235 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 236 | dependencies: 237 | binary-extensions "^2.0.0" 238 | 239 | is-core-module@^2.2.0: 240 | version "2.2.0" 241 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.2.0.tgz#97037ef3d52224d85163f5597b2b63d9afed981a" 242 | integrity sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ== 243 | dependencies: 244 | has "^1.0.3" 245 | 246 | is-extglob@^2.1.1: 247 | version "2.1.1" 248 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 249 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 250 | 251 | is-glob@^4.0.1, is-glob@~4.0.1: 252 | version "4.0.1" 253 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 254 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 255 | dependencies: 256 | is-extglob "^2.1.1" 257 | 258 | is-number@^7.0.0: 259 | version "7.0.0" 260 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 261 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 262 | 263 | jake@^10.6.1: 264 | version "10.8.2" 265 | resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.2.tgz#ebc9de8558160a66d82d0eadc6a2e58fbc500a7b" 266 | integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== 267 | dependencies: 268 | async "0.9.x" 269 | chalk "^2.4.2" 270 | filelist "^1.0.1" 271 | minimatch "^3.0.4" 272 | 273 | jsonfile@^6.0.1: 274 | version "6.1.0" 275 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" 276 | integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== 277 | dependencies: 278 | universalify "^2.0.0" 279 | optionalDependencies: 280 | graceful-fs "^4.1.6" 281 | 282 | lower-case@^2.0.2: 283 | version "2.0.2" 284 | resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" 285 | integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== 286 | dependencies: 287 | tslib "^2.0.3" 288 | 289 | minimatch@^3.0.4: 290 | version "3.0.4" 291 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 292 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 293 | dependencies: 294 | brace-expansion "^1.1.7" 295 | 296 | nanoid@^3.1.22: 297 | version "3.1.22" 298 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.22.tgz#b35f8fb7d151990a8aebd5aa5015c03cf726f844" 299 | integrity sha512-/2ZUaJX2ANuLtTvqTlgqBQNJoQO398KyJgZloL0PZkC0dpysjncRUPsFe3DUPzz/y3h+u7C46np8RMuvF3jsSQ== 300 | 301 | no-case@^3.0.4: 302 | version "3.0.4" 303 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" 304 | integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== 305 | dependencies: 306 | lower-case "^2.0.2" 307 | tslib "^2.0.3" 308 | 309 | normalize-path@^3.0.0, normalize-path@~3.0.0: 310 | version "3.0.0" 311 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 312 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 313 | 314 | param-case@^3.0.3: 315 | version "3.0.4" 316 | resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" 317 | integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== 318 | dependencies: 319 | dot-case "^3.0.4" 320 | tslib "^2.0.3" 321 | 322 | pascal-case@^3.1.2: 323 | version "3.1.2" 324 | resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" 325 | integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== 326 | dependencies: 327 | no-case "^3.0.4" 328 | tslib "^2.0.3" 329 | 330 | path-parse@^1.0.6: 331 | version "1.0.6" 332 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" 333 | integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== 334 | 335 | picomatch@^2.0.4, picomatch@^2.2.1: 336 | version "2.2.2" 337 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" 338 | integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== 339 | 340 | postcss@^8.2.1: 341 | version "8.2.10" 342 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.2.10.tgz#ca7a042aa8aff494b334d0ff3e9e77079f6f702b" 343 | integrity sha512-b/h7CPV7QEdrqIxtAf2j31U5ef05uBDuvoXv6L51Q4rcS1jdlXAVKJv+atCFdUXYl9dyTHGyoMzIepwowRJjFw== 344 | dependencies: 345 | colorette "^1.2.2" 346 | nanoid "^3.1.22" 347 | source-map "^0.6.1" 348 | 349 | readdirp@~3.5.0: 350 | version "3.5.0" 351 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e" 352 | integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ== 353 | dependencies: 354 | picomatch "^2.2.1" 355 | 356 | relateurl@^0.2.7: 357 | version "0.2.7" 358 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" 359 | integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= 360 | 361 | resolve@^1.19.0: 362 | version "1.20.0" 363 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" 364 | integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== 365 | dependencies: 366 | is-core-module "^2.2.0" 367 | path-parse "^1.0.6" 368 | 369 | rollup@^2.38.5: 370 | version "2.45.2" 371 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.45.2.tgz#8fb85917c9f35605720e92328f3ccbfba6f78b48" 372 | integrity sha512-kRRU7wXzFHUzBIv0GfoFFIN3m9oteY4uAsKllIpQDId5cfnkWF2J130l+27dzDju0E6MScKiV0ZM5Bw8m4blYQ== 373 | optionalDependencies: 374 | fsevents "~2.3.1" 375 | 376 | sass@^1.32.8: 377 | version "1.32.8" 378 | resolved "https://registry.yarnpkg.com/sass/-/sass-1.32.8.tgz#f16a9abd8dc530add8834e506878a2808c037bdc" 379 | integrity sha512-Sl6mIeGpzjIUZqvKnKETfMf0iDAswD9TNlv13A7aAF3XZlRPMq4VvJWBC2N2DXbp94MQVdNSFG6LfF/iOXrPHQ== 380 | dependencies: 381 | chokidar ">=2.0.0 <4.0.0" 382 | 383 | source-map-support@~0.5.12: 384 | version "0.5.19" 385 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" 386 | integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== 387 | dependencies: 388 | buffer-from "^1.0.0" 389 | source-map "^0.6.0" 390 | 391 | source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: 392 | version "0.6.1" 393 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 394 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 395 | 396 | supports-color@^5.3.0: 397 | version "5.5.0" 398 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 399 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 400 | dependencies: 401 | has-flag "^3.0.0" 402 | 403 | terser@^4.6.3: 404 | version "4.8.0" 405 | resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.0.tgz#63056343d7c70bb29f3af665865a46fe03a0df17" 406 | integrity sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw== 407 | dependencies: 408 | commander "^2.20.0" 409 | source-map "~0.6.1" 410 | source-map-support "~0.5.12" 411 | 412 | to-regex-range@^5.0.1: 413 | version "5.0.1" 414 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 415 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 416 | dependencies: 417 | is-number "^7.0.0" 418 | 419 | tslib@^2.0.3: 420 | version "2.1.0" 421 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.1.0.tgz#da60860f1c2ecaa5703ab7d39bc05b6bf988b97a" 422 | integrity sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== 423 | 424 | universalify@^2.0.0: 425 | version "2.0.0" 426 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" 427 | integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== 428 | 429 | vite-plugin-html@^2.0.3: 430 | version "2.0.3" 431 | resolved "https://registry.yarnpkg.com/vite-plugin-html/-/vite-plugin-html-2.0.3.tgz#9c610042b4181e95ec6c7d4b4125f3c00cbbd84b" 432 | integrity sha512-1+vFAXc8G1h5NRPNsV0e3GbD8KJL71nv2N8w5y4wdt6VwwAEe6zE2WI66PBVneRBJKOw56LH7C5WJvkMxec92g== 433 | dependencies: 434 | ejs "^3.1.6" 435 | fs-extra "^9.1.0" 436 | html-minifier-terser "^5.1.1" 437 | 438 | vite@^2.1.3: 439 | version "2.1.5" 440 | resolved "https://registry.yarnpkg.com/vite/-/vite-2.1.5.tgz#4857da441c62f7982c83cbd5f42a00330f20c9c1" 441 | integrity sha512-tYU5iaYeUgQYvK/CNNz3tiJ8vYqPWfCE9IQ7K0iuzYovWw7lzty7KRYGWwV3CQPh0NKxWjOczAqiJsCL0Xb+Og== 442 | dependencies: 443 | esbuild "^0.9.3" 444 | postcss "^8.2.1" 445 | resolve "^1.19.0" 446 | rollup "^2.38.5" 447 | optionalDependencies: 448 | fsevents "~2.3.1" 449 | --------------------------------------------------------------------------------