├── 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 |
--------------------------------------------------------------------------------