From 8d57ce21f710533c25740209a787f628203cf889 Mon Sep 17 00:00:00 2001 From: Noel Welsh Date: Sat, 21 Dec 2024 20:46:55 +0000 Subject: [PATCH] Initial commit --- .github/workflows/ci.yml | 271 ++++++++++++++++++ .github/workflows/clean.yml | 59 ++++ .gitignore | 148 ++++++++++ .scalafix.conf | 6 + .scalafmt.conf | 3 + LICENSE.txt | 176 ++++++++++++ README.md | 3 + build.sbt | 128 +++++++++ .../main/scala/terminus/JLineTerminal.scala | 37 +++ .../src/main/scala/terminus/Terminal.scala | 27 ++ .../src/main/scala/terminus/Color.scala | 177 ++++++++++++ .../src/main/scala/terminus/Reader.scala | 24 ++ .../src/main/scala/terminus/Writer.scala | 33 +++ .../scala/terminus/interpreter/Color.scala | 123 ++++++++ .../main/scala/terminus/interpreter/Csi.scala | 18 ++ .../scala/terminus/interpreter/Cursor.scala | 40 +++ .../scala/terminus/interpreter/Erase.scala | 21 ++ .../terminus/interpreter/Interpreter.scala | 6 + .../scala/terminus/interpreter/Reader.scala | 24 ++ .../scala/terminus/interpreter/Scroll.scala | 16 ++ .../scala/terminus/interpreter/Writer.scala | 30 ++ .../main/scala/terminus/syntax/Color.scala | 63 ++++ .../src/main/scala/terminus/syntax/all.scala | 3 + docs/src/pages/README.md | 3 + project/Dependencies.scala | 45 +++ project/build.properties | 1 + project/plugins.sbt | 9 + project/project/plugins.sbt | 1 + 28 files changed, 1495 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/clean.yml create mode 100644 .gitignore create mode 100644 .scalafix.conf create mode 100644 .scalafmt.conf create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 build.sbt create mode 100644 core/jvm/src/main/scala/terminus/JLineTerminal.scala create mode 100644 core/jvm/src/main/scala/terminus/Terminal.scala create mode 100644 core/shared/src/main/scala/terminus/Color.scala create mode 100644 core/shared/src/main/scala/terminus/Reader.scala create mode 100644 core/shared/src/main/scala/terminus/Writer.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Color.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Csi.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Cursor.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Erase.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Interpreter.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Reader.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Scroll.scala create mode 100644 core/shared/src/main/scala/terminus/interpreter/Writer.scala create mode 100644 core/shared/src/main/scala/terminus/syntax/Color.scala create mode 100644 core/shared/src/main/scala/terminus/syntax/all.scala create mode 100644 docs/src/pages/README.md create mode 100644 project/Dependencies.scala create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 project/project/plugins.sbt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9acab49 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,271 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Continuous Integration + +on: + pull_request: + branches: ['**', '!update/**', '!pr/**'] + push: + branches: ['**', '!update/**', '!pr/**'] + tags: [v*] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test + strategy: + matrix: + os: [ubuntu-latest] + scala: [3] + java: [temurin@8] + project: [rootJS, rootJVM, rootNative] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Install sbt + uses: sbt/setup-sbt@v1 + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Check that workflows are up to date + run: sbt githubWorkflowCheck + + - name: Check headers and formatting + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck + + - name: scalaJSLink + if: matrix.project == 'rootJS' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/scalaJSLinkerResult + + - name: nativeLink + if: matrix.project == 'rootNative' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' Test/nativeLink + + - name: Test + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' test + + - name: Check binary compatibility + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' mimaReportBinaryIssues + + - name: Generate API documentation + if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-latest' + run: sbt 'project ${{ matrix.project }}' '++ ${{ matrix.scala }}' doc + + - name: Make target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: mkdir -p unidocs/target core/native/target core/js/target core/jvm/target project/target + + - name: Compress target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + run: tar cf targets.tar unidocs/target core/native/target core/js/target core/jvm/target project/target + + - name: Upload target directories + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + uses: actions/upload-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.project }} + path: targets.tar + + publish: + name: Publish Artifacts + needs: [build] + if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') + strategy: + matrix: + os: [ubuntu-latest] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Install sbt + uses: sbt/setup-sbt@v1 + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Download target directories (3, rootJS) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJS + + - name: Inflate target directories (3, rootJS) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3, rootJVM) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootJVM + + - name: Inflate target directories (3, rootJVM) + run: | + tar xf targets.tar + rm targets.tar + + - name: Download target directories (3, rootNative) + uses: actions/download-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-3-rootNative + + - name: Inflate target directories (3, rootNative) + run: | + tar xf targets.tar + rm targets.tar + + - name: Import signing key + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: echo $PGP_SECRET | base64 -d -i - | gpg --import + + - name: Import signing key and strip passphrase + if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' + env: + PGP_SECRET: ${{ secrets.PGP_SECRET }} + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + run: | + echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg + echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg + (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) + + - name: Publish + env: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} + run: sbt tlCiRelease + + dependency-submission: + name: Submit Dependencies + if: github.event.repository.fork == false && github.event_name != 'pull_request' + strategy: + matrix: + os: [ubuntu-latest] + java: [temurin@8] + runs-on: ${{ matrix.os }} + steps: + - name: Install sbt + uses: sbt/setup-sbt@v1 + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Submit Dependencies + uses: scalacenter/sbt-dependency-submission@v2 + with: + modules-ignore: rootjs_3 docs_3 rootjvm_3 rootnative_3 + configs-ignore: test scala-tool scala-doc-tool test-internal + + site: + name: Generate Site + strategy: + matrix: + os: [ubuntu-latest] + java: [temurin@11] + runs-on: ${{ matrix.os }} + steps: + - name: Install sbt + uses: sbt/setup-sbt@v1 + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (temurin@8) + id: setup-java-temurin-8 + if: matrix.java == 'temurin@8' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' + run: sbt +update + + - name: Setup Java (temurin@11) + id: setup-java-temurin-11 + if: matrix.java == 'temurin@11' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 11 + cache: sbt + + - name: sbt update + if: matrix.java == 'temurin@11' && steps.setup-java-temurin-11.outputs.cache-hit == 'false' + run: sbt +update + + - name: Generate site + run: sbt docs/tlSite + + - name: Publish site + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + uses: peaceiris/actions-gh-pages@v4.0.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: docs/target/docs/site + keep_files: true diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 0000000..547aaa4 --- /dev/null +++ b/.github/workflows/clean.yml @@ -0,0 +1,59 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Clean + +on: push + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Delete artifacts + run: | + # Customize those three lines with your repository and credentials: + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} + + # A shortcut to call GitHub API. + ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } + + # A temporary file which receives HTTP response headers. + TMPFILE=/tmp/tmp.$$ + + # An associative array, key: artifact name, value: number of artifacts of that name. + declare -A ARTCOUNT + + # Process all artifacts on this repository, loop on returned "pages". + URL=$REPO/actions/artifacts + while [[ -n "$URL" ]]; do + + # Get current page, get response headers in a temporary file. + JSON=$(ghapi --dump-header $TMPFILE "$URL") + + # Get URL of next page. Will be empty if we are at the last page. + URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + rm -f $TMPFILE + + # Number of artifacts on this page: + COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) + + # Loop on all artifacts on this page. + for ((i=0; $i < $COUNT; i++)); do + + # Get name of artifact and count instances of this name. + name=$(jq <<<$JSON -r ".artifacts[$i].name?") + ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) + + id=$(jq <<<$JSON -r ".artifacts[$i].id?") + size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) + printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size + ghapi -X DELETE $REPO/actions/artifacts/$id + done + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd4b1fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,148 @@ +# Created by https://www.gitignore.io/api/scala,intellij,eclipse,sbt + +### Scala ### +*.class +*.log + +# sbt specific +.cache +.cache-main +.history +.lib/ +dist/* +target/ +lib_managed/ +src_managed/ +project/boot/ +project/plugins/project/ +project/target +project/project/metals.sbt +project/project/project + +# Scala-IDE specific +.scala_dependencies +.worksheet + +# Documentation intermediate files +docs/src/main/paradox +docs/src/main/mdoc/api +docs/src/pages/**/*.png + + + +### SublimeText ### +doodle.sublime-project +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + + +### Intellij ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio + +*.iml + +## Directory-based project format: +.idea/ +# if you remove the above rule, at least ignore the following: + +# User-specific stuff: +# .idea/workspace.xml +# .idea/tasks.xml +# .idea/dictionaries + +# Sensitive or high-churn files: +# .idea/dataSources.ids +# .idea/dataSources.xml +# .idea/sqlDataSources.xml +# .idea/dynamic.xml +# .idea/uiDesigner.xml + +# Gradle: +# .idea/gradle.xml +# .idea/libraries + +# Mongo Explorer plugin: +# .idea/mongoSettings.xml + +## File-based project format: +*.ipr +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties + + +### Eclipse ### +*.pydevproject +.metadata +.gradle +bin/ +tmp/ +*.tmp +*.bak +*.swp +*~.nib +local.properties +.settings/ +.loadpath + +# Eclipse Core +.project + +# External tool builders +.externalToolBuilders/ + +# Locally stored "Eclipse launch configurations" +*.launch + +# CDT-specific +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific +.buildpath + +# sbteclipse plugin +.target + +# TeXlipse plugin +.texlipse + +# Metals and Bloop +.bsp +.metals +.bloop +project/metals.sbt +.vscode + +.sbt-hydra-history diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 0000000..9ba6d29 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,6 @@ +rules = [ + OrganizeImports +] +OrganizeImports.removeUnused = false +OrganizeImports.coalesceToWildcardImportThreshold = 5 +OrganizeImports.targetDialect = Scala3 \ No newline at end of file diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..352c57d --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,3 @@ +version = "3.8.1" +runner.dialect = scala3 +rewrite.scala3.convertToNewSyntax = true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d1a9fa --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Terminus + +Terminus provides terminal interaction for Scala 3. [Read more](http://www.creativescala.org/terminus). diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..9d35652 --- /dev/null +++ b/build.sbt @@ -0,0 +1,128 @@ +/* + * Copyright 2015-2020 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import scala.sys.process.* +import laika.config.LinkConfig +import laika.config.ApiLinks +import laika.theme.Theme + +ThisBuild / tlBaseVersion := "0.27" // your current series x.y + +Global / onChangedBuildSource := ReloadOnSourceChanges + +ThisBuild / organization := "org.creativescala" +ThisBuild / organizationName := "Creative Scala" +ThisBuild / startYear := Some(2024) +ThisBuild / licenses := Seq(License.Apache2) +ThisBuild / developers := List( + // your GitHub handle and name + tlGitHubDev("noelwelsh", "Noel Welsh") +) + +ThisBuild / sonatypeCredentialHost := xerial.sbt.Sonatype.sonatypeLegacy + +lazy val scala3 = "3.3.4" + +ThisBuild / crossScalaVersions := List(scala3) +ThisBuild / scalaVersion := scala3 +ThisBuild / useSuperShell := false +ThisBuild / semanticdbEnabled := true +ThisBuild / semanticdbVersion := scalafixSemanticdb.revision +ThisBuild / tlSitePublishBranch := Some("main") + +// Run this (build) to do everything involved in building the project +commands += Command.command("build") { state => + "clean" :: + "compile" :: + "test" :: + "scalafixAll" :: + "scalafmtAll" :: + "scalafmtSbt" :: + "headerCreateAll" :: + "githubWorkflowGenerate" :: + "dependencyUpdates" :: + "reload plugins; dependencyUpdates; reload return" :: + "docs / tlSite" :: + state +} + +lazy val commonSettings = Seq( + // This is needed when running examples + Compile / run / fork := true, + libraryDependencies ++= Seq( + Dependencies.munit.value, + Dependencies.munitScalaCheck.value + ), + startYear := Some(2024), + licenses := List( + "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0.txt") + ) +) + +lazy val root = tlCrossRootProject.aggregate(core) + +lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) + .in(file("core")) + .settings( + commonSettings, + libraryDependencies ++= Seq( + Dependencies.catsCore.value + ), + name := "terminus-core" + ) + .jvmSettings(libraryDependencies += Dependencies.jline.value) + +lazy val docs = + project + .in(file("docs")) + .settings( + laikaConfig := laikaConfig.value.withConfigValue( + LinkConfig.empty + .addApiLinks( + ApiLinks(baseUri = + "https://javadoc.io/doc/org.creativescala/terminus-docs_3/latest/" + ) + ) + ), + mdocIn := file("docs/src/pages"), + Laika / sourceDirectories ++= Seq(), + laikaTheme := CreativeScalaTheme.empty + .addJs(laika.ast.Path.Root / "main.js") + .build, + laikaExtensions ++= Seq( + laika.format.Markdown.GitHubFlavor, + laika.config.SyntaxHighlighting + ), + tlSite := Def + .sequential( + mdoc.toTask(""), + laikaSite + ) + .value + ) + .enablePlugins(TypelevelSitePlugin) + .dependsOn(core.jvm) + +lazy val unidocs = project + .in(file("unidocs")) + .enablePlugins(TypelevelUnidocPlugin) // also enables the ScalaUnidocPlugin + .settings( + name := "terminus-docs", + ScalaUnidoc / unidoc / unidocProjectFilter := + inAnyProject -- inProjects( + docs, + core.js + ) + ) diff --git a/core/jvm/src/main/scala/terminus/JLineTerminal.scala b/core/jvm/src/main/scala/terminus/JLineTerminal.scala new file mode 100644 index 0000000..96947e1 --- /dev/null +++ b/core/jvm/src/main/scala/terminus/JLineTerminal.scala @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +import org.jline.terminal.Terminal as JTerminal +import terminus.interpreter.Eof + +class JLineTerminal(terminal: JTerminal) extends Terminal { + private val reader = terminal.reader() + private val writer = terminal.writer() + + def read(): Eof | Char = + reader.read() match { + case -1 => Eof + case char => char.toChar + } + + def flush(): Unit = writer.flush() + + def write(char: Char): Unit = writer.write(char) + + def write(string: String): Unit = writer.write(string) +} diff --git a/core/jvm/src/main/scala/terminus/Terminal.scala b/core/jvm/src/main/scala/terminus/Terminal.scala new file mode 100644 index 0000000..4a3ebe4 --- /dev/null +++ b/core/jvm/src/main/scala/terminus/Terminal.scala @@ -0,0 +1,27 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +import org.jline.terminal.TerminalBuilder + +trait Terminal + extends interpreter.Reader, + interpreter.Writer, + interpreter.Color[Terminal] +object Terminal extends Reader, Writer, Color { + def apply: Terminal = JLineTerminal(TerminalBuilder.builder().build()) +} diff --git a/core/shared/src/main/scala/terminus/Color.scala b/core/shared/src/main/scala/terminus/Color.scala new file mode 100644 index 0000000..4f3e779 --- /dev/null +++ b/core/shared/src/main/scala/terminus/Color.scala @@ -0,0 +1,177 @@ +package terminus + +trait Color { + object foreground { + def default[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.default(f) + + def black[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.black(f) + + def red[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.red(f) + + def green[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.green(f) + + def yellow[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.yellow(f) + + def blue[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.blue(f) + + def magenta[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.magenta(f) + + def cyan[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.cyan(f) + + def white[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.white(f) + + def brightBlack[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightBlack(f) + + def brightRed[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightRed(f) + + def brightGreen[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightGreen(f) + + def brightYellow[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightYellow(f) + + def brightBlue[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightBlue(f) + + def brightMagenta[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightMagenta(f) + + def brightCyan[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightCyan(f) + + def brightWhite[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightWhite(f) + } + + object background { + def default[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.default(f) + + def black[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.black(f) + + def red[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.red(f) + + def green[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.green(f) + + def yellow[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.yellow(f) + + def blue[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.blue(f) + + def magenta[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.magenta(f) + + def cyan[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.cyan(f) + + def white[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.white(f) + + def brightBlack[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightBlack(f) + + def brightRed[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightRed(f) + + def brightGreen[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightGreen(f) + + def brightYellow[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightYellow(f) + + def brightBlue[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightBlue(f) + + def brightMagenta[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightMagenta(f) + + def brightCyan[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightCyan(f) + + def brightWhite[F <: interpreter.Writer, A]( + f: F ?=> A + ): (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.background.brightWhite(f) + } +} diff --git a/core/shared/src/main/scala/terminus/Reader.scala b/core/shared/src/main/scala/terminus/Reader.scala new file mode 100644 index 0000000..0fa225e --- /dev/null +++ b/core/shared/src/main/scala/terminus/Reader.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +import terminus.interpreter.Eof + +trait Reader { + def read(): interpreter.Reader ?=> Eof | Char = + interpreter ?=> interpreter.read() +} diff --git a/core/shared/src/main/scala/terminus/Writer.scala b/core/shared/src/main/scala/terminus/Writer.scala new file mode 100644 index 0000000..eb72014 --- /dev/null +++ b/core/shared/src/main/scala/terminus/Writer.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus + +/** Interface for writing to a console. */ +trait Writer { + + /** Write a character to the console. */ + def write(char: Char): interpreter.Writer ?=> Unit = + interpreter ?=> interpreter.write(char) + + /** Write a string to the console. */ + def write(string: String): interpreter.Writer ?=> Unit = + interpreter ?=> interpreter.write(string) + + /** Flush the current output, causing it to be shown on the console. */ + def flush(): interpreter.Writer ?=> Unit = + interpreter ?=> interpreter.flush() +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Color.scala b/core/shared/src/main/scala/terminus/interpreter/Color.scala new file mode 100644 index 0000000..021ae83 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Color.scala @@ -0,0 +1,123 @@ +package terminus.interpreter + +trait Color[F <: Writer] extends Csi { self: F => + object foreground { + def default[A](f: F ?=> A): A = + withColor("39")(f) + + def black[A](f: F ?=> A): A = + withColor("30")(f) + + def red[A](f: F ?=> A): A = + withColor("31")(f) + + def green[A](f: F ?=> A): A = + withColor("32")(f) + + def yellow[A](f: F ?=> A): A = + withColor("33")(f) + + def blue[A](f: F ?=> A): A = + withColor("34")(f) + + def magenta[A](f: F ?=> A): A = + withColor("35")(f) + + def cyan[A](f: F ?=> A): A = + withColor("36")(f) + + def white[A](f: F ?=> A): A = + withColor("37")(f) + + def brightBlack[A](f: F ?=> A): A = + withColor("90")(f) + + def brightRed[A](f: F ?=> A): A = + withColor("91")(f) + + def brightGreen[A](f: F ?=> A): A = + withColor("92")(f) + + def brightYellow[A](f: F ?=> A): A = + withColor("93")(f) + + def brightBlue[A](f: F ?=> A): A = + withColor("94")(f) + def brightMagenta[A](f: F ?=> A): A = + withColor("95")(f) + + def brightCyan[A](f: F ?=> A): A = + withColor("96")(f) + + def brightWhite[A](f: F ?=> A): A = + withColor("97")(f) + } + + object background { + def default[A](f: F ?=> A): A = + withColor("49")(f) + + def black[A](f: F ?=> A): A = + withColor("40")(f) + + def red[A](f: F ?=> A): A = + withColor("41")(f) + + def green[A](f: F ?=> A): A = + withColor("42")(f) + + def yellow[A](f: F ?=> A): A = + withColor("43")(f) + + def blue[A](f: F ?=> A): A = + withColor("44")(f) + + def magenta[A](f: F ?=> A): A = + withColor("45")(f) + + def cyan[A](f: F ?=> A): A = + withColor("46")(f) + + def white[A](f: F ?=> A): A = + withColor("47")(f) + + def brightBlack[A](f: F ?=> A): A = + withColor("100")(f) + + def brightRed[A](f: F ?=> A): A = + withColor("101")(f) + + def brightGreen[A](f: F ?=> A): A = + withColor("102")(f) + + def brightYellow[A](f: F ?=> A): A = + withColor("103")(f) + + def brightBlue[A](f: F ?=> A): A = + withColor("104")(f) + + def brightMagenta[A](f: F ?=> A): A = + withColor("105")(f) + + def brightCyan[A](f: F ?=> A): A = + withColor("106")(f) + + def brightWhite[A](f: F ?=> A): A = + withColor("107")(f) + } + + private def withColor[A](code: String)(f: F ?=> A): A = { + sgr(code) + val result = f(using this) + reset() + + result + } + + private def reset(): Unit = + sgr("0") + + private def sgr(n: String): Unit = { + csi("m", n) + } +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Csi.scala b/core/shared/src/main/scala/terminus/interpreter/Csi.scala new file mode 100644 index 0000000..974c124 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Csi.scala @@ -0,0 +1,18 @@ +package terminus.interpreter + +/** Provides a utility for writing a CSI escape code. Intended to be extended by + * other interpreters that use this escape code. + */ +trait Csi extends Writer { + private val csiString = "\u001b[" + + /** Write a CSI escape code. The terminator must be specifed first, followed + * by zero or more arguments. The arguments will printed semi-colon separated + * before the terminator. + */ + protected def csi(terminator: String, args: String*): Unit = { + write(csiString) + write(args.mkString(";")) + write(terminator) + } +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Cursor.scala b/core/shared/src/main/scala/terminus/interpreter/Cursor.scala new file mode 100644 index 0000000..472d01c --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Cursor.scala @@ -0,0 +1,40 @@ +package terminus.interpreter + +/** Functionality for manipulating the terminal's cursor. */ +trait Cursor extends Csi { + object cursor { + + /** Move cursor to given column. The left-most column is 1, and coordinates + * increase to the right. + */ + def column(x: Int = 1): Unit = + csi("G", x.toString) + + /** Move absolute cursor position, where (1, 1) is the top left corner and + * coordinates increase to the right and down. + */ + def to(x: Int, y: Int): Unit = + csi("H", x.toString, y.toString) + + /** Move the cursor position relative to the current position. Coordinates + * are given in characters / cells. + */ + def move(x: Int, y: Int): Unit = { + if x < 0 then csi("D", (-x).toString) + else csi("C", x.toString) + + if y < 0 then csi("A", (-y).toString) + else csi("B", y.toString) + } + + /** Move the cursor up the given number of rows. The column of the cursor + * remains unchanged. Defaults to 1 row. + */ + def up(lines: Int = 1): Unit = + csi("A", lines.toString) + + /** Move the cursor down the given number of rows. Defaults to 1 row. */ + def down(lines: Int = 1): Unit = + csi("B", lines.toString) + } +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Erase.scala b/core/shared/src/main/scala/terminus/interpreter/Erase.scala new file mode 100644 index 0000000..4007230 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Erase.scala @@ -0,0 +1,21 @@ +package terminus.interpreter + +/** Functionality for clearing contents on the terminal. */ +trait Erase extends Csi { + + /** Erase the entire screen and move the cursor to the top-left. */ + def screen(): Unit = + csi("J", "2") + + /** Erase from current cursor position to the end of the screen. */ + def down(): Unit = + csi("J", "0") + + /** Erase from current cursor position to the start of the screen. */ + def up(): Unit = + csi("J", "1") + + /** Erase the current line. */ + def line(): Unit = + csi("K", "2") +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Interpreter.scala b/core/shared/src/main/scala/terminus/interpreter/Interpreter.scala new file mode 100644 index 0000000..04aecb2 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Interpreter.scala @@ -0,0 +1,6 @@ +package terminus.interpreter + +/** Marker trait for Terminus interpreters. All interpreters should have this as + * a super-trait. + */ +trait Interpreter diff --git a/core/shared/src/main/scala/terminus/interpreter/Reader.scala b/core/shared/src/main/scala/terminus/interpreter/Reader.scala new file mode 100644 index 0000000..30e8930 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Reader.scala @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus.interpreter + +sealed trait Eof +object Eof extends Eof + +trait Reader extends Interpreter { + def read(): Eof | Char +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Scroll.scala b/core/shared/src/main/scala/terminus/interpreter/Scroll.scala new file mode 100644 index 0000000..02c2197 --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Scroll.scala @@ -0,0 +1,16 @@ +package terminus.interpreter + +/** Functionality for scrolling the terminal. */ +trait Scroll extends Csi { + object scroll { + + /** Scroll the display up the given number of rows. Defaults to 1 row. */ + def up(lines: Int = 1): Unit = + (0.until(lines)).foreach(_ => csi("S")) + + /** Scroll the display down the given number of rows. Defaults to 1 row. */ + def down(lines: Int = 1): Unit = + (0.until(lines)).foreach(_ => csi("T")) + } + +} diff --git a/core/shared/src/main/scala/terminus/interpreter/Writer.scala b/core/shared/src/main/scala/terminus/interpreter/Writer.scala new file mode 100644 index 0000000..4c9abef --- /dev/null +++ b/core/shared/src/main/scala/terminus/interpreter/Writer.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Creative Scala + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package terminus.interpreter + +/** Interface for writing to a console. */ +trait Writer extends Interpreter { + + /** Write a character to the console. */ + def write(char: Char): Unit + + /** Write a string to the console. */ + def write(string: String): Unit + + /** Flush the current output, causing it to be shown on the console. */ + def flush(): Unit +} diff --git a/core/shared/src/main/scala/terminus/syntax/Color.scala b/core/shared/src/main/scala/terminus/syntax/Color.scala new file mode 100644 index 0000000..d79e9e0 --- /dev/null +++ b/core/shared/src/main/scala/terminus/syntax/Color.scala @@ -0,0 +1,63 @@ +package terminus.syntax + +import terminus.interpreter + +trait ColorSyntax { + class ForegroundColorSyntax[F <: interpreter.Writer, A](f: F ?=> A) { + def default: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.default(f) + + def black: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.black(f) + + def red: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.red(f) + + def green: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.green(f) + + def yellow: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.yellow(f) + + def blue: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.blue(f) + + def magenta: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.magenta(f) + + def cyan: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.cyan(f) + + def white: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.white(f) + + def brightBlack: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightBlack(f) + + def brightRed: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightRed(f) + + def brightGreen: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightGreen(f) + + def brightYellow: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightYellow(f) + + def brightBlue: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightBlue(f) + + def brightMagenta: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightMagenta(f) + + def brightCyan: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightCyan(f) + + def brightWhite: (F & interpreter.Color[F]) ?=> A = + interpreter ?=> interpreter.foreground.brightWhite(f) + + } + + extension [F <: interpreter.Writer, A](f: F ?=> A) { + def foreground = ForegroundColorSyntax(f) + } +} diff --git a/core/shared/src/main/scala/terminus/syntax/all.scala b/core/shared/src/main/scala/terminus/syntax/all.scala new file mode 100644 index 0000000..a408d8d --- /dev/null +++ b/core/shared/src/main/scala/terminus/syntax/all.scala @@ -0,0 +1,3 @@ +package terminus.syntax + +object all extends ColorSyntax diff --git a/docs/src/pages/README.md b/docs/src/pages/README.md new file mode 100644 index 0000000..2f5b9ad --- /dev/null +++ b/docs/src/pages/README.md @@ -0,0 +1,3 @@ +# Terminus + +Terminus is a functional Scala library for working with the terminal. diff --git a/project/Dependencies.scala b/project/Dependencies.scala new file mode 100644 index 0000000..69e6417 --- /dev/null +++ b/project/Dependencies.scala @@ -0,0 +1,45 @@ +import sbt.* +import org.scalajs.sbtplugin.ScalaJSPlugin +import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport.* +import org.portablescala.sbtplatformdeps.PlatformDepsPlugin.autoImport.* + +object Dependencies { + // Library Versions + val catsVersion = "2.12.0" + val catsEffectVersion = "3.5.7" + val fs2Version = "3.11.0" + + val scalatagsVersion = "0.13.1" + val scalajsDomVersion = "2.8.0" + + val jlineVersion = "3.28.0" + + val scalaCheckVersion = "1.15.4" + val munitVersion = "1.0.3" + val munitScalacheckVersion = "1.0.0" + val munitCatsEffectVersion = "2.0.0" + + // Libraries + val catsEffect = + Def.setting("org.typelevel" %%% "cats-effect" % catsEffectVersion) + val catsCore = Def.setting("org.typelevel" %%% "cats-core" % catsVersion) + val catsFree = Def.setting("org.typelevel" %%% "cats-free" % catsVersion) + val fs2 = Def.setting("co.fs2" %%% "fs2-core" % fs2Version) + + val scalatags = Def.setting("com.lihaoyi" %%% "scalatags" % scalatagsVersion) + val scalajsDom = + Def.setting("org.scala-js" %%% "scalajs-dom" % scalajsDomVersion) + + val jline = + Def.setting("org.jline" % "jline" % jlineVersion) + + val munit = Def.setting("org.scalameta" %%% "munit" % munitVersion % "test") + val munitScalaCheck = + Def.setting( + "org.scalameta" %%% "munit-scalacheck" % munitScalacheckVersion % "test" + ) + val munitCatsEffect = + Def.setting( + "org.typelevel" %%% "munit-cats-effect" % munitCatsEffectVersion % Test + ) +} diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..e88a0d8 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.10.6 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..1db444c --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,9 @@ +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.17.0") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("com.github.sbt" % "sbt-unidoc" % "0.5.0") +addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.13.0") +addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.7.4") +addSbtPlugin("org.typelevel" % "sbt-typelevel-site" % "0.7.4") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.6") +addSbtPlugin("org.creativescala" % "creative-scala-theme" % "0.2.1") diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt new file mode 100644 index 0000000..6ddcba5 --- /dev/null +++ b/project/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.4")