├── .envrc ├── .git-blame-ignore-revs ├── .github ├── crosscompile ├── dependabot.yml ├── release-drafter.yml ├── renovate.json ├── update-hash └── workflows │ ├── release-drafter.yml │ ├── release.yml │ ├── steward.yml │ ├── test.yml │ └── update-flake.yml ├── .gitignore ├── .jvmopts ├── .scalafmt.conf ├── LICENSE ├── README.md ├── VERSION ├── build.sbt ├── compat.nix ├── default.nix ├── flake.lock ├── flake.nix ├── images └── screenshot1.png ├── jitpack.yml ├── jvm ├── graal-config.json └── src │ └── main │ ├── c │ └── constants.scala.c │ └── scala │ └── de │ └── bley │ └── scalals │ ├── Core.scala │ └── package.scala ├── native └── src │ └── main │ ├── c │ └── include │ │ └── sys │ │ └── posix_sem.h │ ├── resources │ └── scala-native │ │ └── terminal.c │ └── scala │ └── de │ └── bley │ └── scalals │ ├── Core.scala │ ├── Terminal.scala │ └── package.scala ├── project ├── CNumber.scala ├── Ninja.scala ├── build.properties └── plugins.sbt ├── shared └── src │ ├── main │ └── scala │ │ └── de │ │ └── bley │ │ └── scalals │ │ ├── Aliases.scala │ │ ├── Core.scala │ │ ├── CoreConfig.scala │ │ ├── FileInfo.scala │ │ ├── Files.scala │ │ ├── Main.scala │ │ ├── Particles.scala │ │ ├── Size.scala │ │ └── Utils.scala │ └── test │ └── scala │ └── de │ └── bley │ └── scalals │ └── UtilsTest.scala └── shell.nix /.envrc: -------------------------------------------------------------------------------- 1 | 2 | use_flake() { 3 | watch_file flake.nix 4 | watch_file flake.lock 5 | eval "$(nix print-dev-env --profile "$(direnv_layout_dir)/flake-profile")" 6 | } 7 | 8 | if nix show-config | grep -q 'experimental-features =.*flake'; then 9 | use flake 10 | elif has nix; then 11 | use nix 12 | 13 | watch_file nix/default.nix nix/sources.nix nix/sources.json 14 | fi 15 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Enable trailing commas for multiple argument expressions 2 | 4df8d0e5351edf7ffe6e32dc795a4ba8dc4c7fa6 3 | # Reformat with optional braces removed 4 | b918b7ac6be54ddc7586b71dc315150a2c1374cf 5 | # Add end markers for expressions spanning at least 7 lines 6 | 2a249972e648f7ff1423a20b36039a1d07b97c7a 7 | # reformat with scalafmt 8 | 713d7b1729c457c5d390bd20098c32957819db58 9 | 10 | -------------------------------------------------------------------------------- /.github/crosscompile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | declare -ar TARGETS=( 6 | x86_64-unknown-linux-musl 7 | x86_64-apple-darwin-none 8 | aarch64-unknown-linux-musl 9 | aarch64-apple-darwin-none 10 | riscv64-unknown-linux-musl 11 | ) 12 | 13 | echo '::group::configure' 14 | sbt -client 'project scalalsNative' 15 | echo '::endgroup::' 16 | 17 | # Mode: {debug, release-fast, release-full, release-size} 18 | 19 | declare -a binaries 20 | 21 | for target in "${TARGETS[@]}"; do 22 | # LTO: {none, full, thin} 23 | case "$target" in 24 | # zig ld does not yet support LTO for mach-O see https://github.com/ziglang/zig/issues/8680 25 | *darwin*) LTO=none ;; 26 | *) LTO=full ;; 27 | esac 28 | 29 | echo "::group::build $target" 30 | 31 | sbt -client "set nativeConfig ~= { _.withLTO(scala.scalanative.build.LTO.$LTO) }" 32 | sbt -client "set targetTriplet:= Some(\"$target\")" 33 | sbt -client "ninja" 34 | sbt -client "ninjaCompile" 35 | 36 | ninja -f native/target/build.ninja 37 | 38 | binaries+=( "$( ninja -f native/target/build.ninja -t targets rule exe )" ) 39 | 40 | echo '::endgroup::' 41 | done 42 | 43 | mv -vt . "${binaries[@]}" 44 | 45 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: github-actions 5 | directory: "/" 6 | schedule: 7 | interval: daily 8 | time: '00:00' 9 | timezone: UTC 10 | open-pull-requests-limit: 10 11 | commit-message: 12 | prefix: "chore" 13 | include: "scope" 14 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: 'Version $NEXT_PATCH_VERSION' 2 | tag-template: 'v$NEXT_PATCH_VERSION' 3 | categories: 4 | - title: '🚀 Features' 5 | labels: 6 | - 'feature' 7 | - 'enhancement' 8 | - title: '🐛 Bug Fixes' 9 | labels: 10 | - 'fix' 11 | - 'bugfix' 12 | - 'bug' 13 | - title: '🧰 Maintenance' 14 | labels: 15 | - 'chore' 16 | - 'dependencies' 17 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 18 | template: | 19 | ## Changes 20 | 21 | $CHANGES 22 | autolabeler: 23 | - label: 'chore' 24 | files: 25 | - '*.md' 26 | branch: 27 | - '/docs{0,1}\/.+/' 28 | - '/update\/.+/' 29 | - label: 'bug' 30 | branch: 31 | - '/fix\/.+/' 32 | title: 33 | - '/fix/i' 34 | - label: 'enhancement' 35 | branch: 36 | - '/feature\/.+/' 37 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "addLabels": ["dependencies"], 4 | "extends": [ 5 | "config:recommended", 6 | "helpers:pinGitHubActionDigests" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.github/update-hash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -eo pipefail 4 | 5 | if [ $# -ne 1 ]; then 6 | echo 'error: wrong number of arguments' >&2 7 | echo "usage: $0 ATTR" >&2 8 | exit 1 9 | fi 10 | 11 | attr="$1" 12 | 13 | if [[ -n "$( git status --porcelain -- flake.nix )" ]]; then 14 | echo "flake.nix is not clean. cowardly refusing to continue." >&2 15 | exit 2 16 | fi 17 | 18 | outputHash=$( nix eval --raw ".#${attr}.outputHash" ) 19 | 20 | sed -i'' -e "s,$outputHash,," flake.nix 21 | 22 | if output=$( nix build --no-link --print-build-logs ".#${attr}" 2>&1 ); then 23 | echo "expected the build to fail after replacing the hash" >&2 24 | exit 1 25 | fi 26 | 27 | if [[ "$output" != *'error: hash mismatch in fixed-output derivation'* ]]; then 28 | echo "build problem: $output" >&2 ; exit 1 29 | fi 30 | 31 | git restore flake.nix 32 | 33 | got=$( sed -ne 's,.*got: *\([^ ]*\),\1,p' <<< "$output" ) 34 | 35 | if [[ "$outputHash" = "$got" ]]; then 36 | echo "hash for $attr is the same" >&2 37 | else 38 | echo "got new hash: $got" >&2 39 | sed -i'' -e "s,$outputHash,$got," flake.nix 40 | fi 41 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | # pull_request event is required only for autolabeler 5 | pull_request: 6 | # Only following types are handled by the action, but one can default to all as well 7 | types: [opened, reopened, synchronize] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | update_release_draft: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | # write permission is required for autolabeler 17 | # otherwise, read permission is required at least 18 | pull-requests: write 19 | steps: 20 | # Drafts your next Release notes as Pull Requests are merged into "main" 21 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 22 | # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml 23 | with: 24 | # only label PRs 25 | disable-releaser: true 26 | # config-name: my-config.yml 27 | # disable-autolabeler: true 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Trigger build on jitpack 12 | run: | 13 | curl -L https://jitpack.io/com/github/avdv/scalals/${{ github.ref_name }}/scalals-${{ github.ref_name }}.pom 14 | -------------------------------------------------------------------------------- /.github/workflows/steward.yml: -------------------------------------------------------------------------------- 1 | name: Update deps hash 2 | 3 | on: 4 | pull_request_target: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | update-hash: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.event.pull_request.head.repo.owner.login == 'scala-steward' }} 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 14 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 15 | - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 16 | with: 17 | name: cbley 18 | extraPullNames: pre-commit-hooks 19 | - name: Update fixed output hash 20 | run: | 21 | outputHash=$( nix eval --raw '.#scalals.dependencies.outputHash' ) 22 | sed -i'' -e "s,$outputHash,," flake.nix 23 | if output=$( nix build --no-link --print-build-logs '.#scalals.dependencies' 2>&1 ); then 24 | echo "command succeeded unexpectedly." >&2 25 | exit 1 26 | fi 27 | if [[ "$output" != *'error: hash mismatch in fixed-output derivation'* ]]; then 28 | echo "build problem: $output" >&2 ; exit 1 29 | fi 30 | git restore flake.nix 31 | got=$( sed -ne 's,.*got: *\([^ ]*\),\1,p' <<< "$output" ) 32 | sed -i'' -e "s,$outputHash,$got," flake.nix 33 | git diff 34 | - name: Push changes 35 | uses: devops-infra/action-commit-push@b8c990ac36bac67f71133ad7ec3da1d7abf4d57e # v0.10.0 36 | with: 37 | amend: true 38 | force: true 39 | github_token: "${{ secrets.PR_TOKEN }}" 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | pull_request: 4 | branches: 5 | - main 6 | push: 7 | branches: 8 | - main 9 | 10 | jobs: 11 | graalvm: 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest, macos-13, macos-14] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 18 | - name: Cache SBT 19 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 20 | with: 21 | path: | 22 | ~/.cache/coursier 23 | ~/.ivy2/cache 24 | ~/.ivy2/local 25 | ~/.sbt/boot 26 | ~/.sbt/launchers 27 | key: ${{ runner.os }}-sbt-${{ hashFiles('build.sbt', 'project/plugins.sbt') }} 28 | - name: "Install Nix ❄" 29 | uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 30 | with: 31 | extra_nix_config: | 32 | experimental-features = nix-command flakes 33 | - name: "Install Cachix ❄" 34 | uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 35 | with: 36 | name: cbley 37 | extraPullNames: pre-commit-hooks 38 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 39 | pushFilter: '[-](source|nixpkgs-src)$' 40 | - run: git branch PR-${{ github.event.number }} 41 | - run: nix develop '.#graalVM' --command sbt 'scalalsJVM / GraalVMNativeImage / packageBin' 42 | - run: jvm/target/graalvm-native-image/scalals 43 | 44 | package: 45 | name: Nix ❄ 46 | strategy: 47 | matrix: 48 | os: [ubuntu-latest, macos-13, macos-14] 49 | runs-on: ${{ matrix.os }} 50 | steps: 51 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 52 | with: 53 | fetch-depth: 2 54 | persist-credentials: false 55 | - name: "Install Nix ❄" 56 | uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 57 | - name: "Install Cachix ❄" 58 | uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 59 | with: 60 | name: cbley 61 | extraPullNames: pre-commit-hooks 62 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 63 | pushFilter: '[-](source|nixpkgs-src)$' 64 | - name: Update scalals.dependencies hash 65 | id: hash-update 66 | if: github.event_name == 'pull_request' && github.repository == 'avdv/scalals' 67 | run: |- 68 | nix develop --command .github/update-hash scalals.dependencies 69 | echo exit="$( git diff --quiet flake.nix ; echo $? )" >> "$GITHUB_OUTPUT" 70 | - name: Set author identity and push url 71 | run: |- 72 | git config user.email '${{ github.actor }}@users.noreply.github.com' 73 | git config user.name '${{ github.actor }}' 74 | git remote set-url --push origin https://x-access-token:${{ secrets.PR_TOKEN }}@github.com/${{ github.repository }} 75 | - name: Push changes to PR 76 | if: steps.hash-update.outputs.exit != '0' 77 | env: 78 | GH_HEAD_REF: ${{ github.head_ref }} 79 | run: |- 80 | git switch -c pr-branch ${{ github.event.pull_request.head.sha }} 81 | git commit -m 'Update scalals.dependencies hash' flake.nix 82 | git push origin "HEAD:$GH_HEAD_REF" 83 | exit 1 84 | - run: nix flake check 85 | - run: nix build --print-build-logs 86 | - run: nix run 87 | 88 | tests: 89 | runs-on: ubuntu-latest 90 | steps: 91 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 92 | - name: Cache SBT 93 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 94 | with: 95 | path: | 96 | ~/.cache/coursier 97 | ~/.ivy2/cache 98 | ~/.ivy2/local 99 | ~/.sbt/boot 100 | ~/.sbt/launchers 101 | key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt', '.travis.yml') }} 102 | - name: "Install Nix ❄" 103 | uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 104 | with: 105 | #install_url: https://nixos-nix-install-tests.cachix.org/serve/i6laym9jw3wg9mw6ncyrk6gjx4l34vvx/install 106 | #install_options: '--tarball-url-prefix https://nixos-nix-install-tests.cachix.org/serve' 107 | extra_nix_config: | 108 | experimental-features = nix-command flakes 109 | - name: "Install Cachix ❄" 110 | uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 111 | with: 112 | name: cbley 113 | extraPullNames: pre-commit-hooks 114 | signingKey: '${{ secrets.CACHIX_SIGNING_KEY }}' 115 | pushFilter: '[-](source|nixpkgs-src)$' 116 | # Only needed for private caches 117 | #authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' 118 | - name: Determine zig env 119 | run: | 120 | nix develop --ignore-environment --keep HOME --command bash -c \ 121 | "( echo 'env<> '$GITHUB_OUTPUT'" 122 | id: nix-zig 123 | - name: Cache zig ${{ fromJson(steps.nix-zig.outputs.env).version }} 124 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 125 | with: 126 | path: | 127 | ${{ fromJson(steps.nix-zig.outputs.env).global_cache_dir }} 128 | key: zig-${{ fromJson(steps.nix-zig.outputs.env).zig_exe }} 129 | restore-keys: | 130 | zig- 131 | - run: git branch PR-${{ github.event.number }} 132 | - name: Test 133 | run: nix develop --ignore-environment --keep HOME --command sbt tpolecatCiMode 'scalalsNative / test' 134 | - name: Cross Compile 135 | run: nix develop --ignore-environment --keep HOME --keep SCALANATIVE_MODE --command '.github/crosscompile' 136 | env: 137 | SCALANATIVE_MODE: ${{ github.ref == 'refs/heads/main' && 'release-full' || 'debug' }} 138 | - name: qemu-aarch64 scalals 139 | run: nix shell --inputs-from . 'nixpkgs#qemu' --command qemu-aarch64 scalals-linux-aarch64 140 | - name: qemu-risc64 scalals 141 | run: nix shell --inputs-from . 'nixpkgs#qemu' --command qemu-riscv64 scalals-linux-riscv64 142 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 143 | with: 144 | path: scalals-* 145 | 146 | run: 147 | name: Run 148 | strategy: 149 | matrix: 150 | os: 151 | - ubuntu-latest 152 | - macos-14 153 | needs: 154 | - tests 155 | runs-on: 156 | ${{ matrix.os }} 157 | steps: 158 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 159 | id: download 160 | - run: | 161 | ls -lh 162 | chmod +x scalals-* 163 | working-directory: ${{steps.download.outputs.download-path}}/artifact 164 | - run: ./scalals-${{ runner.os == 'Linux' && 'linux' || 'darwin' }}-x86_64 165 | working-directory: ${{steps.download.outputs.download-path}}/artifact 166 | - run: ./scalals-${{ runner.os == 'Linux' && 'linux' || 'darwin' }}-aarch64 167 | if: ${{ runner.arch == 'ARM64' }} 168 | working-directory: ${{steps.download.outputs.download-path}}/artifact 169 | 170 | release: 171 | name: Prepare release 172 | if: github.ref == 'refs/heads/main' 173 | needs: 174 | - run 175 | runs-on: ubuntu-latest 176 | permissions: 177 | # write permission is required to create a github release 178 | contents: write 179 | steps: 180 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 181 | - name: Rename binaries 182 | run: | 183 | mv artifact/scalals-linux-aarch64 scalals-arm64-linux 184 | mv artifact/scalals-linux-riscv64 scalals-riscv64-linux 185 | mv artifact/scalals-linux-x86_64 scalals-x86_64-linux 186 | mv artifact/scalals-darwin-aarch64 scalals-arm64-darwin 187 | mv artifact/scalals-darwin-x86_64 scalals-x86_64-darwin 188 | # Drafts your next Release notes as Pull Requests are merged into "main" 189 | - uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 190 | id: release 191 | env: 192 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 193 | - name: Upload binaries 194 | run: | 195 | gh release upload '${{ steps.release.outputs.tag_name }}' \ 196 | scalals-arm64-linux \ 197 | scalals-arm64-darwin \ 198 | scalals-riscv64-linux \ 199 | scalals-x86_64-linux \ 200 | scalals-x86_64-darwin \ 201 | --clobber --repo ${{ github.repository }} 202 | env: 203 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 204 | 205 | benchmark: 206 | name: Benchmark 207 | if: github.ref == 'refs/heads/main' 208 | needs: 209 | - run 210 | runs-on: ubuntu-latest 211 | permissions: 212 | checks: write 213 | steps: 214 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 215 | - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 216 | - name: ⚙ Install hyperfine 217 | run: |- 218 | sudo apt-get update 219 | sudo apt-get install -y hyperfine 220 | - run: |- 221 | mv artifact/scalals-linux-x86_64 scalals-x86_64-linux 222 | chmod +x scalals-x86_64-linux 223 | - uses: bencherdev/bencher@main 224 | - name: Track base branch benchmarks with Bencher 225 | env: 226 | BRANCH: ${{ github.head_ref || github.ref_name }} 227 | run: | 228 | bencher run \ 229 | --project scalals \ 230 | --token '${{ secrets.BENCHER_API_TOKEN }}' \ 231 | --branch "$BRANCH" \ 232 | --testbed ubuntu-latest \ 233 | --threshold-measure latency \ 234 | --threshold-test t_test \ 235 | --threshold-max-sample-size 64 \ 236 | --threshold-upper-boundary 0.99 \ 237 | --thresholds-reset \ 238 | --err \ 239 | --adapter shell_hyperfine \ 240 | --file ../results.json \ 241 | --github-actions '${{ secrets.GITHUB_TOKEN }}' \ 242 | "hyperfine -w 2 --export-json ../results.json './scalals-x86_64-linux --tree --color'" 243 | -------------------------------------------------------------------------------- /.github/workflows/update-flake.yml: -------------------------------------------------------------------------------- 1 | name: Update flake ❄ 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | # every wednesday at 3pm UTC 7 | - cron: '0 15 * * wed' 8 | 9 | jobs: 10 | update-dependencies: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | steps: 16 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 17 | - uses: cachix/install-nix-action@17fe5fb4a23ad6cbbe47d6b3f359611ad276644c # v31 18 | - uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 19 | with: 20 | name: cbley 21 | extraPullNames: pre-commit-hooks 22 | - name: Set author identity 23 | run: | 24 | git config user.email '${{ github.actor }}@users.noreply.github.com' 25 | git config user.name '${{ github.actor }}' 26 | - run: nix flake update --commit-lock-file 27 | - name: Create Pull Request 28 | id: create-pr 29 | uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7 30 | with: 31 | commit-message: "[automation] flake update" 32 | title: "[automation] flake update" 33 | branch: "automation/update-flake-inputs" 34 | labels: "dependencies" 35 | token: "${{ secrets.PR_TOKEN }}" 36 | - name: Enable Pull Request Automerge 37 | run: gh pr merge --rebase --auto "${{ steps.create-pr.outputs.pull-request-number }}" 38 | env: 39 | GH_TOKEN: ${{ secrets.PR_TOKEN }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # handled by pre-commit-hooks.nix 3 | /.pre-commit-config.yaml 4 | 5 | /.bsp/ 6 | 7 | ### Bloop ### 8 | /.bloop 9 | /project/.bloop/ 10 | 11 | ### Metals ### 12 | .metals/ 13 | /project/**/metals.sbt 14 | 15 | ### sbt ### 16 | /target/ 17 | /jvm/target/ 18 | /native/target/ 19 | /project/target/ 20 | /project/boot/ 21 | /shared/target/ 22 | 23 | ### direnv ### 24 | .direnv 25 | -------------------------------------------------------------------------------- /.jvmopts: -------------------------------------------------------------------------------- 1 | -Xms512m 2 | -Xmx2G 3 | -Xss2m 4 | -XX:MaxInlineLevel=18 5 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.5" 2 | 3 | maxColumn = 120 4 | 5 | spaces.inImportCurlyBraces = true 6 | 7 | rewrite.rules = [PreferCurlyFors, SortImports, SortModifiers] 8 | 9 | # remove unicode arrows, deprecated since Scala 2.13 10 | rewriteTokens = { 11 | "→" = "->" 12 | "⇒" = "=>" 13 | "←" = "<-" 14 | } 15 | 16 | runner.dialect = scala3 17 | 18 | rewrite.scala3.convertToNewSyntax = yes 19 | rewrite.scala3.removeOptionalBraces = yes 20 | rewrite.scala3.insertEndMarkerMinLines = 7 21 | rewrite.trailingCommas.style = multiple 22 | 23 | fileOverride { 24 | ".sbt" { runner.dialect = sbt1 } 25 | 26 | "glob:**/project/*.scala" { runner.dialect = sbt1 } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Claudio Bley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test](https://github.com/avdv/scalals/actions/workflows/test.yml/badge.svg)](https://github.com/avdv/scalals/actions/workflows/test.yml) 2 | 3 | # About 4 | 5 | This is yet another [colorls](https://github.com/athityakumar/colorls) clone. 6 | 7 | ![screenshot](images/screenshot1.png) 8 | 9 | # Features 10 | 11 | 1. fast (compiled to native code) 12 | 2. aims to be a drop-in replacement to GNU ls 13 | 3. supports `LS_COLORS` 14 | 15 | # Install 16 | 17 | _Note_: scalals binaries are currently available for Linux and MacOS on amd64 and arm64. 18 | 19 | ## Using [coursier](https://get-coursier.io/) 20 | 21 | 1. run `cs install --contrib scalals` 22 | 23 | (run this again to install the latest version) 24 | 25 | ## With nix flakes 26 | 27 | 1. `cachix use cbley` (optional, avoids re-building) 28 | 2. run `nix profile install github:avdv/scalals` 29 | 30 | ## Manually 31 | 32 | 1. download a pre-built [binary](https://github.com/avdv/scalals/releases/latest) for your platform 33 | 2. ensure it is found in your `PATH` 34 | 3. run `chmod +x path/to/scalals` 35 | 36 | # Setup 37 | 38 | 1. install a Nerd Font from [here](https://www.nerdfonts.com/font-downloads) and use it in your terminal emulator 39 | 2. set up your dircolors (see https://www.nordtheme.com/ports/dircolors for example) 40 | 41 | # Building 42 | 43 | 1. run `cachix use cbley` (optional) 44 | 2. run `nix-build` or `nix build` (flake) 45 | 3. binary is in `result/bin/` 46 | 47 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.1.10 2 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import scala.util.matching.Regex 2 | import Regex.Groups 3 | import scala.sys.process._ 4 | import java.nio.file.{ Files, Paths } 5 | import java.io.File 6 | import scala.scalanative.build.NativeConfig 7 | import org.typelevel.scalacoptions.{ ScalacOption, ScalacOptions } 8 | import org.typelevel.scalacoptions.ScalaVersion 9 | import org.typelevel.scalacoptions.ScalaVersion.V3_0_0 10 | import scala.Ordering.Implicits._ 11 | 12 | Global / onChangedBuildSource := ReloadOnSourceChanges 13 | 14 | ThisBuild / scalaVersion := "3.6.4" 15 | 16 | val sharedSettings = Seq( 17 | publish / skip := true, 18 | tpolecatDevModeOptions ++= Set( 19 | ScalacOption("-rewrite", _ => true), 20 | // TODO use ScalacOptions.newSyntax instead 21 | ScalacOption("-new-syntax", List.empty, _ >= V3_0_0), 22 | // TODO use ScalaVersion.V3_4_0 instead 23 | ScalacOptions.scala3Source("3.4-migration", _ >= ScalaVersion(3, 4, 0)), 24 | ), 25 | ) 26 | 27 | def generateConstants(base: File): File = { 28 | base.mkdirs() 29 | val outputFile = base / "constants.scala" 30 | val cc = sys.env.getOrElse("CC", "clang") 31 | val output = scala.io.Source.fromString(s"$cc -E -P jvm/src/main/c/constants.scala.c".!!) 32 | val definition = "_(S_[^ ]+) = ((?:0[xX])?[0-9]+)".r 33 | val shift = "[(] *((?:0[xX])?[0-9]+) *>> *((?:0[xX])?[0-9]+) *[)]".r 34 | 35 | def evalShifts(s: String): String = { 36 | val replaced = shift.replaceAllIn( 37 | s, 38 | _ match { 39 | case Groups(CNumber(lhs), CNumber(rhs)) => (lhs >> rhs).toString 40 | }, 41 | ) 42 | if (replaced == s) s 43 | else evalShifts(replaced) 44 | } 45 | 46 | val constants = for { 47 | line <- output.getLines() 48 | Groups(name, CNumber(value)) <- definition.findFirstMatchIn(evalShifts(line)) 49 | } yield f"val $name%s: Int = $value%#x" 50 | 51 | io.IO.write( 52 | outputFile, 53 | s"""package de.bley.scalals 54 | | 55 | |object UnixConstants { 56 | | ${constants.mkString("\n ")} 57 | |} 58 | |""".stripMargin, 59 | ) 60 | outputFile 61 | } 62 | 63 | lazy val targetTriplet = settingKey[Option[String]]("describes target platform for native compilation") 64 | 65 | lazy val scalals = 66 | // select supported platforms 67 | crossProject(JVMPlatform, NativePlatform) 68 | .crossType(CrossType.Full) 69 | .in(file(".")) 70 | .enablePlugins(BuildInfoPlugin) 71 | .settings(sharedSettings) 72 | .settings( 73 | buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion), 74 | buildInfoPackage := "de.bley.scalals", 75 | libraryDependencies ++= Seq( 76 | "com.github.scopt" %%% "scopt" % "4.1.0", 77 | "org.scalameta" %%% "munit" % "1.1.1" % Test, 78 | ), 79 | ) 80 | // configure JVM settings 81 | .jvmEnablePlugins(GraalVMNativeImagePlugin) 82 | .jvmSettings( 83 | Compile / sourceGenerators += Def.task { 84 | Seq(generateConstants((Compile / sourceManaged).value / "de" / "bley" / "scalals")) 85 | }.taskValue, 86 | Compile / run / fork := true, 87 | Compile / run / javaOptions += "--add-opens=java.base/sun.nio.fs=ALL-UNNAMED", 88 | Compile / packageSrc / mappings ~= { mappings => 89 | mappings.map { case mapping @ (from, to) => 90 | if (from.name == "Core.scala" && from.getPath.contains("/shared/")) 91 | from -> to.replace("Core.scala", "CoreShared.scala") 92 | else 93 | mapping 94 | } 95 | }, 96 | graalVMNativeImageOptions ++= Seq( 97 | "--no-fallback", 98 | "-H:-CheckToolchain", 99 | "--add-opens=java.base/sun.nio.fs=ALL-UNNAMED", 100 | s"-H:ReflectionConfigurationFiles=${baseDirectory.value / "graal-config.json" absolutePath}", 101 | ), 102 | ) 103 | // configure Scala-Native settings 104 | .nativeSettings( 105 | targetTriplet := None, 106 | libraryDependencies += "io.github.cquiroz" %%% "scala-java-time" % "2.6.0", 107 | nativeConfig := { 108 | val config = nativeConfig.value 109 | val nixCFlagsCompile = for { 110 | flags <- sys.env.get("NIX_CFLAGS_COMPILE").toList 111 | flag <- flags.split(" +") if flag.nonEmpty 112 | } yield flag 113 | 114 | val nixCFlagsLink = for { 115 | flags <- sys.env.get("NIX_CFLAGS_LINK").toList 116 | flag <- flags.split(" +") if flag.nonEmpty 117 | } yield flag 118 | 119 | config 120 | .withCompileOptions("-Wall" :: nixCFlagsCompile ++ config.compileOptions) 121 | .withBaseName { 122 | val target = targetTriplet.value.fold("") { t => 123 | val Array(arch, _, os, _) = t.split("-", 4) 124 | s"-$os-$arch" 125 | } 126 | "scalals" + target 127 | } 128 | .withLinkingOptions(nixCFlagsLink) 129 | .withMultithreading(Some(false)) 130 | .withTargetTriple(targetTriplet.value) 131 | }, 132 | ) 133 | -------------------------------------------------------------------------------- /compat.nix: -------------------------------------------------------------------------------- 1 | import ( 2 | let 3 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 4 | in 5 | fetchTarball { 6 | url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; 7 | sha256 = lock.nodes.flake-compat.locked.narHash; 8 | } 9 | ) { src = ./.; } 10 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | (import ./compat.nix).defaultNix 2 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1747046372, 7 | "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-compat_2": { 20 | "flake": false, 21 | "locked": { 22 | "lastModified": 1696426674, 23 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 24 | "owner": "edolstra", 25 | "repo": "flake-compat", 26 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 27 | "type": "github" 28 | }, 29 | "original": { 30 | "owner": "edolstra", 31 | "repo": "flake-compat", 32 | "type": "github" 33 | } 34 | }, 35 | "flake-utils": { 36 | "inputs": { 37 | "systems": "systems" 38 | }, 39 | "locked": { 40 | "lastModified": 1731533236, 41 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 42 | "owner": "numtide", 43 | "repo": "flake-utils", 44 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 45 | "type": "github" 46 | }, 47 | "original": { 48 | "owner": "numtide", 49 | "repo": "flake-utils", 50 | "type": "github" 51 | } 52 | }, 53 | "git-hooks": { 54 | "inputs": { 55 | "flake-compat": "flake-compat_2", 56 | "gitignore": "gitignore", 57 | "nixpkgs": [ 58 | "nixpkgs" 59 | ] 60 | }, 61 | "locked": { 62 | "lastModified": 1747372754, 63 | "narHash": "sha256-2Y53NGIX2vxfie1rOW0Qb86vjRZ7ngizoo+bnXU9D9k=", 64 | "owner": "cachix", 65 | "repo": "git-hooks.nix", 66 | "rev": "80479b6ec16fefd9c1db3ea13aeb038c60530f46", 67 | "type": "github" 68 | }, 69 | "original": { 70 | "owner": "cachix", 71 | "repo": "git-hooks.nix", 72 | "type": "github" 73 | } 74 | }, 75 | "gitignore": { 76 | "inputs": { 77 | "nixpkgs": [ 78 | "git-hooks", 79 | "nixpkgs" 80 | ] 81 | }, 82 | "locked": { 83 | "lastModified": 1709087332, 84 | "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", 85 | "owner": "hercules-ci", 86 | "repo": "gitignore.nix", 87 | "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", 88 | "type": "github" 89 | }, 90 | "original": { 91 | "owner": "hercules-ci", 92 | "repo": "gitignore.nix", 93 | "type": "github" 94 | } 95 | }, 96 | "nix-filter": { 97 | "locked": { 98 | "lastModified": 1731533336, 99 | "narHash": "sha256-oRam5PS1vcrr5UPgALW0eo1m/5/pls27Z/pabHNy2Ms=", 100 | "owner": "numtide", 101 | "repo": "nix-filter", 102 | "rev": "f7653272fd234696ae94229839a99b73c9ab7de0", 103 | "type": "github" 104 | }, 105 | "original": { 106 | "owner": "numtide", 107 | "repo": "nix-filter", 108 | "type": "github" 109 | } 110 | }, 111 | "nixpkgs": { 112 | "locked": { 113 | "lastModified": 1748995628, 114 | "narHash": "sha256-bFufQGSAEYQgjtc4wMrobS5HWN0hDP+ZX+zthYcml9U=", 115 | "owner": "NixOS", 116 | "repo": "nixpkgs", 117 | "rev": "8eb3b6a2366a7095939cd22f0dc0e9991313294b", 118 | "type": "github" 119 | }, 120 | "original": { 121 | "owner": "NixOS", 122 | "ref": "nixos-24.11", 123 | "repo": "nixpkgs", 124 | "type": "github" 125 | } 126 | }, 127 | "root": { 128 | "inputs": { 129 | "flake-compat": "flake-compat", 130 | "flake-utils": "flake-utils", 131 | "git-hooks": "git-hooks", 132 | "nix-filter": "nix-filter", 133 | "nixpkgs": "nixpkgs", 134 | "sbt": "sbt" 135 | } 136 | }, 137 | "sbt": { 138 | "inputs": { 139 | "flake-utils": [ 140 | "flake-utils" 141 | ], 142 | "nixpkgs": [ 143 | "nixpkgs" 144 | ] 145 | }, 146 | "locked": { 147 | "lastModified": 1698464090, 148 | "narHash": "sha256-Pnej7WZIPomYWg8f/CZ65sfW85IfIUjYhphMMg7/LT0=", 149 | "owner": "zaninime", 150 | "repo": "sbt-derivation", 151 | "rev": "6762cf2c31de50efd9ff905cbcc87239995a4ef9", 152 | "type": "github" 153 | }, 154 | "original": { 155 | "owner": "zaninime", 156 | "repo": "sbt-derivation", 157 | "type": "github" 158 | } 159 | }, 160 | "systems": { 161 | "locked": { 162 | "lastModified": 1681028828, 163 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 164 | "owner": "nix-systems", 165 | "repo": "default", 166 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 167 | "type": "github" 168 | }, 169 | "original": { 170 | "owner": "nix-systems", 171 | "repo": "default", 172 | "type": "github" 173 | } 174 | } 175 | }, 176 | "root": "root", 177 | "version": 7 178 | } 179 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "scalals"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11"; 6 | nix-filter.url = "github:numtide/nix-filter"; 7 | flake-compat = { 8 | url = "github:edolstra/flake-compat"; 9 | flake = false; 10 | }; 11 | flake-utils.url = "github:numtide/flake-utils"; 12 | git-hooks = { 13 | inputs = { 14 | nixpkgs.follows = "nixpkgs"; 15 | }; 16 | url = "github:cachix/git-hooks.nix"; 17 | }; 18 | sbt = { 19 | inputs = { 20 | nixpkgs.follows = "nixpkgs"; 21 | flake-utils.follows = "flake-utils"; 22 | }; 23 | url = "github:zaninime/sbt-derivation"; 24 | }; 25 | }; 26 | 27 | outputs = 28 | { 29 | self, 30 | nixpkgs, 31 | nix-filter, 32 | flake-utils, 33 | git-hooks, 34 | sbt, 35 | ... 36 | }: 37 | flake-utils.lib.eachSystem 38 | [ 39 | "aarch64-linux" 40 | "aarch64-darwin" 41 | "x86_64-darwin" 42 | "x86_64-linux" 43 | ] 44 | ( 45 | system: 46 | let 47 | filter = nix-filter.lib; 48 | 49 | jreHeadlessOverlay = _: prev: { jre = prev.openjdk11_headless; }; 50 | 51 | scalafmtOverlay = final: prev: { 52 | scalafmt = prev.scalafmt.overrideAttrs ( 53 | old: 54 | let 55 | version = builtins.head ( 56 | builtins.match ''[ \n]*version *= *"([^ \n]+)".*'' (builtins.readFile ./.scalafmt.conf) 57 | ); 58 | outputHash = "sha256-6pK5CIjOnB3z6o1R/PlhOtjr6JEI72/xEk9+x5GtJFo="; 59 | in 60 | { 61 | inherit version; 62 | passthru = { 63 | inherit outputHash; 64 | }; 65 | buildInputs = [ 66 | (prev.stdenv.mkDerivation { 67 | name = "scalafmt-deps-${version}"; 68 | buildCommand = '' 69 | export COURSIER_CACHE=$(pwd) 70 | ${prev.coursier}/bin/cs fetch org.scalameta:scalafmt-cli_2.13:${version} > deps 71 | mkdir -p $out/share/java 72 | cp $(< deps) $out/share/java/ 73 | ''; 74 | outputHashMode = "recursive"; 75 | inherit outputHash; 76 | outputHashAlgo = if outputHash == "" then "sha256" else null; 77 | }) 78 | ]; 79 | } 80 | ); 81 | }; 82 | 83 | pkgs = import nixpkgs { 84 | inherit system; 85 | overlays = [ 86 | jreHeadlessOverlay 87 | scalafmtOverlay 88 | ]; 89 | }; 90 | 91 | inherit (pkgs) lib stdenvNoCC; 92 | 93 | zig = pkgs.zig_0_13; 94 | 95 | mkShell = pkgs.mkShell.override { stdenv = stdenvNoCC; }; 96 | 97 | clang = pkgs.writeScriptBin "clang" '' 98 | #!${pkgs.bash}/bin/bash 99 | 100 | declare -a args 101 | for arg; do 102 | arg="''${arg/-unknown-/-}" 103 | arg="''${arg/-apple-darwin-none/-macos-none}" 104 | args+=( "$arg" ) 105 | done 106 | exec ${zig}/bin/zig cc "''${args[@]}" 107 | ''; 108 | 109 | clangpp = pkgs.writeShellApplication { 110 | name = "clang++"; 111 | runtimeInputs = [ pkgs.gnused ]; 112 | text = '' 113 | declare -a args=() 114 | declare -a tmpfiles=() 115 | trap '[[ "''${#tmpfiles[@]}" -gt 0 ]] && rm -v "''${tmpfiles[@]}"' EXIT 116 | for arg; do 117 | case "$arg" in 118 | @* ) 119 | infile="''${arg:1}" 120 | resp=$( mktemp ) 121 | tmpfiles+=( "$resp" ) 122 | sed -e 's,-unknown-,-,' -e 's,-apple-darwin-none,-macos-none,' "$infile" >> "$resp" 123 | args+=( "@$resp" ) 124 | ;; 125 | * ) 126 | arg="''${arg/-unknown-/-}" 127 | arg="''${arg/-apple-darwin-none/-macos-none}" 128 | args+=( "$arg" ) 129 | esac 130 | done 131 | ${zig}/bin/zig c++ "''${args[@]}" 132 | ''; 133 | }; 134 | 135 | buildInputs = lib.optional (system == "x86_64-darwin") pkgs.apple-sdk_11; 136 | 137 | nativeBuildInputs = with pkgs; [ 138 | git 139 | ninja 140 | zig 141 | which 142 | clang 143 | clangpp 144 | ]; 145 | in 146 | { 147 | formatter = pkgs.nixfmt-rfc-style; 148 | 149 | packages = rec { 150 | inherit (pkgs) scalafmt; 151 | 152 | scalals = sbt.lib.mkSbtDerivation rec { 153 | inherit pkgs buildInputs nativeBuildInputs; 154 | 155 | overrides = { 156 | stdenv = stdenvNoCC; 157 | }; 158 | 159 | pname = "scalals-native"; 160 | 161 | # read the first non-empty string from the VERSION file 162 | version = builtins.head (builtins.match "[ \n]*([^ \n]+).*" (builtins.readFile ./VERSION)); 163 | 164 | depsSha256 = "sha256-9w1WCUtLjufksU61HCGKzgt/rDFw9wHqYwgjT3H+Z1w="; 165 | 166 | src = filter { 167 | root = self; 168 | include = [ 169 | "native" 170 | "project" 171 | "shared" 172 | ./.jvmopts 173 | ./build.sbt 174 | ]; 175 | }; 176 | 177 | # explicitly override version from sbt-dynver which does not work within a nix build 178 | patchPhase = '' 179 | echo 'ThisBuild / version := "${version}"' > version.sbt 180 | ''; 181 | 182 | env = { 183 | SCALANATIVE_MODE = "release-full"; # {debug, release-fast, release-full} 184 | SCALANATIVE_LTO = if stdenvNoCC.isLinux then "thin" else "none"; # {none, full, thin} 185 | XDG_CACHE_HOME = "xdg_cache"; # needed by zig cc for a writable directory 186 | 187 | NIX_CFLAGS_COMPILE = 188 | lib.optionalString (stdenvNoCC.isLinux && stdenvNoCC.isx86_64) 189 | # zig uses -march=native by default 190 | "-march=sandybridge"; 191 | }; 192 | 193 | buildPhase = '' 194 | sbt tpolecatReleaseMode 'project scalalsNative' 'show nativeConfig' ninjaCompile ninja 195 | ninja -f native/target/build.ninja 196 | ''; 197 | 198 | dontPatchELF = true; 199 | 200 | depsWarmupCommand = "sbt tpolecatDevMode scalalsNative/compile"; 201 | 202 | installPhase = '' 203 | mkdir --parents $out/bin 204 | cp "$(ninja -f native/target/build.ninja -t targets rule exe)" $out/bin/scalals 205 | ''; 206 | }; 207 | default = scalals; 208 | }; 209 | 210 | apps.default = flake-utils.lib.mkApp { 211 | drv = self.packages.${system}.default; 212 | exePath = "/bin/scalals"; 213 | }; 214 | 215 | checks = { 216 | pre-commit-check = git-hooks.lib.${system}.run { 217 | src = ./.; 218 | hooks = { 219 | actionlint.enable = true; 220 | nix-fmt = { 221 | enable = true; 222 | name = "nix fmt"; 223 | entry = "${pkgs.nixfmt-rfc-style}/bin/nixfmt"; 224 | types = [ "nix" ]; 225 | }; 226 | scalafmt = { 227 | enable = true; 228 | name = "scalafmt"; 229 | entry = "${pkgs.scalafmt}/bin/scalafmt --respect-project-filters"; 230 | types = [ "scala" ]; 231 | }; 232 | }; 233 | }; 234 | }; 235 | 236 | devShells = 237 | { 238 | default = mkShell { 239 | name = "scalals"; 240 | 241 | env.SBT_TPOLECAT_DEV = "1"; 242 | 243 | inherit (self.checks.${system}.pre-commit-check) shellHook; 244 | inherit buildInputs; 245 | 246 | packages = [ pkgs.metals ]; 247 | nativeBuildInputs = nativeBuildInputs ++ [ pkgs.sbt ]; 248 | }; 249 | 250 | graalVM = pkgs.mkShell { 251 | name = "scalals / graalvm"; 252 | 253 | inherit (self.checks.${system}.pre-commit-check) shellHook; 254 | inherit buildInputs; 255 | 256 | nativeBuildInputs = [ 257 | pkgs.graalvm-ce 258 | pkgs.sbt 259 | ]; 260 | }; 261 | } 262 | // (lib.optionalAttrs (system == "x86_64-linux") ( 263 | let 264 | inherit (pkgs.pkgsCross.aarch64-multiplatform-musl) llvmPackages_13 mkShell; 265 | llvm-bin = llvmPackages_13.libcxxStdenv.mkDerivation { 266 | name = "clang-llvm-bin"; 267 | dontUnpack = true; 268 | dontConfigure = true; 269 | dontBuild = true; 270 | installPhase = '' 271 | mkdir $out 272 | ln -sT $NIX_CC/bin/$CC $out/clang 273 | ln -sT $NIX_CC/bin/$CXX $out/clang++ 274 | ''; 275 | }; 276 | in 277 | { 278 | aarch64-cross = mkShell.override { stdenv = llvmPackages_13.libcxxStdenv; } { 279 | name = "scalals-arm64"; 280 | 281 | # scala-native uses $LLVM_BIN to resolve clang and clang++ 282 | env.LLVM_BIN = llvm-bin; 283 | env.NIX_CFLAGS_LINK = "-static"; 284 | 285 | nativeBuildInputs = [ 286 | pkgs.lld_13 287 | pkgs.git 288 | pkgs.ninja 289 | pkgs.which 290 | pkgs.sbt 291 | ]; 292 | }; 293 | } 294 | )); 295 | 296 | # compatibility for nix < 2.7.0 297 | defaultApp = self.apps.${system}.default; 298 | defaultPackage = self.packages.${system}.default; 299 | devShell = self.devShells.${system}.default; 300 | } 301 | ); 302 | } 303 | -------------------------------------------------------------------------------- /images/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/avdv/scalals/6814bb7bbe786dbf3133b351988c107ab31042ec/images/screenshot1.png -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /jvm/graal-config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "sun.misc.Unsafe", 4 | "allDeclaredFields": true 5 | }, 6 | { 7 | "name": "scopt.OParser$", 8 | "fields": [ 9 | { 10 | "name": "0bitmap$1" 11 | } 12 | ] 13 | } 14 | ] 15 | -------------------------------------------------------------------------------- /jvm/src/main/c/constants.scala.c: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | #define constant(LC) _ ## LC = LC 5 | 6 | constant(S_ISUID) 7 | constant(S_ISGID) 8 | constant(S_ISVTX) 9 | constant(S_IRUSR) 10 | constant(S_IRGRP) 11 | constant(S_IROTH) 12 | constant(S_IWUSR) 13 | constant(S_IWGRP) 14 | constant(S_IWOTH) 15 | constant(S_IXUSR) 16 | constant(S_IXGRP) 17 | constant(S_IXOTH) 18 | 19 | constant(S_IFSOCK) 20 | constant(S_IFIFO) 21 | constant(S_IFBLK) 22 | constant(S_IFCHR) 23 | 24 | constant(S_IFMT) 25 | -------------------------------------------------------------------------------- /jvm/src/main/scala/de/bley/scalals/Core.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import java.nio.file.attribute.* 4 | import java.nio.file.{ Files, LinkOption, Path } 5 | import java.time.Instant 6 | import java.text.Collator 7 | 8 | import scala.jdk.CollectionConverters.* 9 | import scala.util.chaining.* 10 | import scala.util.Try 11 | import scala.collection.mutable 12 | 13 | object Core extends generic.Core: 14 | private val collator = Collator.getInstance 15 | 16 | protected val orderByName = Ordering 17 | .fromLessThan((a: FileInfo, b: FileInfo) => collator.compare(a.name, b.name) < 0) 18 | .asInstanceOf[Ordering[generic.FileInfo]] 19 | 20 | private val sb = new StringBuilder(3 * 3) 21 | 22 | def permissionString(mode: Int): String = 23 | import UnixConstants.* 24 | 25 | sb.clear() 26 | format((mode & S_IRUSR).toInt, (mode & S_IWUSR).toInt, (mode & S_IXUSR).toInt, (mode & S_ISUID).toInt != 0, 's', sb) 27 | format((mode & S_IRGRP).toInt, (mode & S_IWGRP).toInt, (mode & S_IXGRP).toInt, (mode & S_ISGID).toInt != 0, 's', sb) 28 | format((mode & S_IROTH).toInt, (mode & S_IWOTH).toInt, (mode & S_IXOTH).toInt, (mode & S_ISVTX).toInt != 0, 't', sb) 29 | sb.toString() 30 | end permissionString 31 | end Core 32 | 33 | object Terminal: 34 | import sys.process.* 35 | 36 | val isTTYOutput: Boolean = System.console != null 37 | val width: Int = 38 | sys.env 39 | .get("COLUMNS") 40 | .orElse(Try("tput cols" !! ProcessLogger(_ => ())).toOption) 41 | .flatMap(_.toIntOption) 42 | .getOrElse(80) 43 | end Terminal 44 | 45 | object FileInfo: 46 | 47 | private val executableBits = 48 | Set(PosixFilePermission.GROUP_EXECUTE, PosixFilePermission.OWNER_EXECUTE, PosixFilePermission.OTHERS_EXECUTE) 49 | 50 | private val modeField = 51 | Class.forName("sun.nio.fs.UnixFileAttributes").getDeclaredField("st_mode").tap(_.setAccessible(true)) 52 | 53 | private def mode(attr: PosixFileAttributes) = modeField.getInt(attr) 54 | 55 | def apply(path: Path, dereference: Boolean): FileInfo = new FileInfo(path, dereference) 56 | end FileInfo 57 | 58 | final class FileInfo private (val path: Path, dereference: Boolean) extends generic.FileInfo: 59 | import UnixConstants.* 60 | 61 | private val attributes = 62 | if dereference then Files.readAttributes(path, classOf[PosixFileAttributes]) 63 | else Files.readAttributes(path, classOf[PosixFileAttributes], LinkOption.NOFOLLOW_LINKS) 64 | 65 | val name = path.getFileName.toString 66 | 67 | @inline def isDirectory: Boolean = attributes.isDirectory 68 | @inline def isRegularFile: Boolean = attributes.isRegularFile 69 | @inline def isSymlink: Boolean = attributes.isSymbolicLink 70 | @inline def group: String = attributes.group().getName 71 | @inline def owner: String = attributes.owner().getName 72 | @inline def permissions: Int = FileInfo.mode(attributes) 73 | @inline def size: Long = attributes.size() 74 | @inline def lastModifiedTime: Instant = attributes.lastModifiedTime().toInstant 75 | @inline def lastAccessTime: Instant = attributes.lastAccessTime().toInstant 76 | @inline def creationTime: Instant = attributes.creationTime().toInstant 77 | @inline def isExecutable = attributes.permissions().asScala.exists(FileInfo.executableBits) 78 | @inline def isBlockDev: Boolean = (permissions & S_IFMT) == S_IFBLK 79 | @inline def isCharDev: Boolean = (permissions & S_IFMT) == S_IFCHR 80 | @inline def isPipe: Boolean = (permissions & S_IFMT) == S_IFIFO 81 | @inline def isSocket: Boolean = (permissions & S_IFMT) == S_IFSOCK 82 | end FileInfo 83 | -------------------------------------------------------------------------------- /jvm/src/main/scala/de/bley/scalals/package.scala: -------------------------------------------------------------------------------- 1 | package de.bley 2 | 3 | package scalals: 4 | sealed trait Env 5 | 6 | object Env extends Env: 7 | inline def apply[T](inline f: Env ?=> T): T = f(using this) 8 | -------------------------------------------------------------------------------- /native/src/main/c/include/sys/posix_sem.h: -------------------------------------------------------------------------------- 1 | 2 | #define PSEMNAMLEN 31 3 | 4 | -------------------------------------------------------------------------------- /native/src/main/resources/scala-native/terminal.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | long int scalanative_tiocgwinsize() { 6 | return TIOCGWINSZ; 7 | } 8 | 9 | bool scalanative_isatty(int fd) { 10 | return isatty(fd); 11 | } 12 | -------------------------------------------------------------------------------- /native/src/main/scala/de/bley/scalals/Core.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import java.time.Instant 4 | 5 | import scala.collection.mutable 6 | import java.io.IOException 7 | 8 | import scala.scalanative.libc.errno 9 | import scala.scalanative.libc.locale 10 | import scala.scalanative.libc.string.strcoll 11 | import scala.scalanative.unsafe.* 12 | import scala.scalanative.posix.time.timespec 13 | import scala.scalanative.posix.errno.* 14 | import scala.scalanative.posix.sys.stat 15 | import scala.scalanative.posix.sys.statOps.statOps 16 | import java.nio.file.Path 17 | 18 | object FileInfo: 19 | // FIXME: crashes with a scala.scalanative.runtime.UndefinedBehaviorError 20 | // val lookupService = FileSystems.getDefault.getUserPrincipalLookupService 21 | 22 | def apply(path: Path, dereference: Boolean)(using e: Env) = 23 | val info = 24 | val buf = alloc[stat.stat]() 25 | val err = 26 | if dereference then stat.stat(toCString(path.toString), buf) 27 | else stat.lstat(toCString(path.toString), buf) 28 | 29 | if err == 0 then buf 30 | else 31 | errno.errno match 32 | case e if e == ENOENT => throw new IOException("No such file or directory") 33 | case e if e == EACCES => throw new IOException("Permission denied") 34 | case _ => throw new IOException("I/O error") 35 | end if 36 | end info 37 | new FileInfo(path, toCString(path.getFileName.toString), info) 38 | end apply 39 | end FileInfo 40 | 41 | final class FileInfo private (val path: Path, val cstr: CString, private val info: Ptr[stat.stat]) 42 | extends generic.FileInfo: 43 | import scala.scalanative.posix.{ grp, pwd } 44 | import scala.scalanative.posix.timeOps.* 45 | import scala.scalanative.libc.errno 46 | 47 | val name = fromCString(cstr) 48 | 49 | @inline def isDirectory: Boolean = stat.S_ISDIR(info._13) != 0 50 | @inline def isRegularFile: Boolean = stat.S_ISREG(info._13) != 0 51 | @inline def isSymlink: Boolean = stat.S_ISLNK(info._13) != 0 52 | @inline def isPipe: Boolean = stat.S_ISFIFO(info._13) != 0 53 | @inline def isSocket: Boolean = stat.S_ISSOCK(info._13) != 0 54 | @inline def isCharDev: Boolean = stat.S_ISCHR(info._13) != 0 55 | @inline def isBlockDev: Boolean = stat.S_ISBLK(info._13) != 0 56 | @inline def group: String = 57 | val buf = stackalloc[grp.group]() 58 | errno.errno = 0 59 | val err = grp.getgrgid(info._5, buf) 60 | if err == 0 then fromCString(buf._1) 61 | else if errno.errno == 0 then info._5.toString 62 | else throw new IOException(s"$path: ${errno.errno}") 63 | end group 64 | @inline def owner: String = 65 | val buf = stackalloc[pwd.passwd]() 66 | errno.errno = 0 67 | val err = pwd.getpwuid(info._4, buf) 68 | if err == 0 then fromCString(buf._1) 69 | else if errno.errno == 0 then info._4.toString 70 | else throw new IOException(s"$path: ${errno.errno}") 71 | end owner 72 | @inline def permissions: Int = info._13.toInt 73 | @inline def size: Long = info._6 74 | @inline def lastModifiedTime: Instant = timespecToInstant(info.st_mtimespec) 75 | @inline def lastAccessTime: Instant = timespecToInstant(info.st_atimespec) 76 | @inline def creationTime: Instant = timespecToInstant(info.st_ctimespec) 77 | @inline def isExecutable = 78 | import scala.scalanative.unsigned.* 79 | (info._13 & (stat.S_IXGRP | stat.S_IXOTH | stat.S_IXUSR)) != 0.toUInt 80 | 81 | private def timespecToInstant(time: timespec) = 82 | val tptr = time.toPtr 83 | val secs: Long = tptr.tv_sec.toLong 84 | val nsecs: Long = tptr.tv_nsec.toLong 85 | Instant.ofEpochSecond(secs, nsecs) 86 | end FileInfo 87 | 88 | object Core extends generic.Core: 89 | import scala.scalanative.posix.sys.stat.* 90 | import scala.scalanative.unsafe.* 91 | 92 | if locale.setlocale(locale.LC_ALL, c"") == null then Console.err.println("setlocale: LC_ALL: cannot change locale") 93 | 94 | private val sb = new StringBuilder(3 * 3) 95 | 96 | protected val orderByName = Ordering 97 | .fromLessThan[FileInfo]((a, b) => strcoll(a.cstr, b.cstr) < 0) 98 | .asInstanceOf[Ordering[generic.FileInfo]] 99 | 100 | final override def permissionString(imode: Int): String = 101 | import scala.scalanative.unsigned.* 102 | val mode = imode.toUInt 103 | 104 | sb.clear() 105 | format((mode & S_IRUSR).toInt, (mode & S_IWUSR).toInt, (mode & S_IXUSR).toInt, (mode & S_ISUID).toInt != 0, 's', sb) 106 | format((mode & S_IRGRP).toInt, (mode & S_IWGRP).toInt, (mode & S_IXGRP).toInt, (mode & S_ISGID).toInt != 0, 's', sb) 107 | format((mode & S_IROTH).toInt, (mode & S_IWOTH).toInt, (mode & S_IXOTH).toInt, (mode & S_ISVTX).toInt != 0, 't', sb) 108 | sb.toString() 109 | end permissionString 110 | 111 | @inline def timing[T](marker: String)(body: => T): T = 112 | // val start = System.nanoTime 113 | val r = body 114 | // val end = System.nanoTime 115 | // Console.err.println(marker + " " + (end - start).toString) 116 | r 117 | end timing 118 | end Core 119 | -------------------------------------------------------------------------------- /native/src/main/scala/de/bley/scalals/Terminal.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import scala.scalanative.libc.stdio.perror 4 | import scala.scalanative.unsigned.* 5 | import scala.scalanative.unsafe.* 6 | import scala.scalanative.posix.unistd.STDOUT_FILENO 7 | import scala.scalanative.posix.fcntl.* 8 | import scala.scalanative.posix.unistd.close 9 | import scala.scalanative.posix.sys.ioctl.* 10 | 11 | @extern 12 | object termios: 13 | @name("scalanative_tiocgwinsize") 14 | def TIOCGWINSZ: CLongInt = extern 15 | 16 | @extern 17 | object xunistd: 18 | @name("scalanative_isatty") 19 | def isatty(fileno: CInt): Boolean = extern 20 | 21 | object types: 22 | /* 23 | struct winsize { 24 | unsigned short ws_row; 25 | unsigned short ws_col; 26 | unsigned short ws_xpixel; /* unused */ 27 | unsigned short ws_ypixel; /* unused */ 28 | }; 29 | */ 30 | type winsize = CStruct4[UShort, UShort, UShort, UShort] 31 | end types 32 | 33 | object Terminal: 34 | def isTTYOutput: Boolean = xunistd.isatty(STDOUT_FILENO) 35 | 36 | def width: Int = 37 | val winsz = stackalloc[types.winsize]() 38 | 39 | var tty = open(c"/dev/tty", O_RDWR, 0.toUInt) 40 | try 41 | if tty == -1 then tty = STDOUT_FILENO 42 | 43 | if ioctl(tty, termios.TIOCGWINSZ, winsz.asInstanceOf[Ptr[Byte]]) >= 0 then (winsz._2).toInt 44 | else sys.env.get("COLUMNS").map(_.toInt).getOrElse(80) 45 | finally 46 | if tty != STDOUT_FILENO then 47 | val ret = close(tty) 48 | if ret != 0 then perror(c"close tty") 49 | end try 50 | end width 51 | end Terminal 52 | -------------------------------------------------------------------------------- /native/src/main/scala/de/bley/scalals/package.scala: -------------------------------------------------------------------------------- 1 | package de.bley 2 | 3 | package object scalals: 4 | type Env = scalanative.unsafe.Zone 5 | def Env = scalanative.unsafe.Zone 6 | -------------------------------------------------------------------------------- /project/CNumber.scala: -------------------------------------------------------------------------------- 1 | object CNumber { 2 | def getRadix(s: String) = 3 | if (s.startsWith("0x") || s.startsWith("0X")) 16 4 | else if (s.startsWith("0")) 8 5 | else 10 6 | 7 | // might be an octal, hexadecimal or decimal integer literal 8 | def readCNumber(s: String): Int = Integer.parseInt(s, getRadix(s)) 9 | 10 | def unapply(s: String): Option[Int] = Some(readCNumber(s)) 11 | } 12 | -------------------------------------------------------------------------------- /project/Ninja.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Drop this into the `project` directory of your scala-native project (needs version 0.4.9+) 3 | * 4 | * Now, you can build your project with ninja: `sbt runNinja` 5 | * 6 | * Note: this only compiles the .ll and .c(pp) files to .o files and produces a resulting binary, 7 | * the heavy lifting of generating .ll files is still done by the sbt scala-native plugin 8 | * especially including re-generating the .ll files each time 9 | */ 10 | 11 | package scala.scalanative 12 | package build 13 | 14 | import scala.scalanative.build._ 15 | import scala.scalanative.sbtplugin.ScalaNativePlugin.autoImport._ 16 | import scala.scalanative.sbtplugin.Utilities._ 17 | import scala.scalanative.linker.ReachabilityAnalysis 18 | 19 | import sbt._ 20 | import Keys._ 21 | import java.nio.file.{ Files, Path, Paths } 22 | import scala.scalanative.sbtplugin.ScalaNativePlugin 23 | import scala.sys.process._ 24 | import scala.concurrent._ 25 | import scala.concurrent.ExecutionContext.Implicits.global 26 | import scala.concurrent.duration.Duration 27 | import java.nio.file.StandardOpenOption 28 | import java.io.Writer 29 | 30 | /** Internal utilities to interact with Ninja. */ 31 | object Ninja extends AutoPlugin { 32 | implicit private val sharedScope = scala.scalanative.util.Scope.unsafe() 33 | 34 | override def requires = ScalaNativePlugin 35 | 36 | implicit class RichPath(val path: Path) extends AnyVal { 37 | def abs: String = path.toAbsolutePath.toString 38 | } 39 | 40 | override def trigger = allRequirements 41 | 42 | object autoImport { 43 | val ninja = taskKey[Path]("build ninja file") 44 | val runNinja = taskKey[Unit]("run ninja") 45 | val ninjaCompileFile = settingKey[Path]("file with ninja build rules") 46 | val ninjaCompile = taskKey[Unit]("generate ninja compile file") 47 | } 48 | 49 | import autoImport._ 50 | 51 | override lazy val projectSettings = Seq( 52 | ninja := ninjaTask.value, 53 | ninjaCompile := ninjaCompileTask.value, 54 | ninjaCompileFile := target.value.toPath / "compile.ninja", 55 | runNinja := runNinjaTask.value, 56 | ) 57 | 58 | lazy val runNinjaTask = 59 | Def.task { 60 | val ninjaFile = ninja.value 61 | 62 | Process(Seq("ninja", "-f", ninjaFile.abs)).! 63 | } 64 | 65 | import scala.reflect.runtime.{ universe => ru } 66 | 67 | private val m = ru.runtimeMirror(getClass.getClassLoader) 68 | private val nativeLibMod = ru.typeOf[NativeLib.type].termSymbol.asModule 69 | private val mm = m.reflectModule(nativeLibMod) 70 | private val im = m.reflect(mm.instance) 71 | 72 | private def unpackNativeCode(nativelib: NativeLib): Path = { 73 | unpackNativeCodeMethod(nativelib).asInstanceOf[Path] 74 | } 75 | 76 | private val unpackNativeCodeMethod = { 77 | val nlm = ru.typeOf[NativeLib.type].decl(ru.TermName("unpackNativeCode")).asMethod 78 | im.reflectMethod(nlm) 79 | } 80 | 81 | private def findNativePaths(destPath: Path): Seq[Path] = { 82 | findNativePathsMethod(destPath).asInstanceOf[Seq[Path]] 83 | } 84 | 85 | private val findNativePathsMethod = { 86 | val nlm = ru.typeOf[NativeLib.type].decl(ru.TermName("findNativePaths")).asMethod 87 | im.reflectMethod(nlm) 88 | } 89 | 90 | private def configureNativeLibrary( 91 | initialConfig: Config, 92 | analysis: ReachabilityAnalysis.Result, 93 | destPath: Path, 94 | ): Config = { 95 | configureNativeLibraryMethod(initialConfig, analysis, destPath).asInstanceOf[Config] 96 | } 97 | 98 | private val configureNativeLibraryMethod = { 99 | val nlm = ru.typeOf[NativeLib.type].decl(ru.TermName("configureNativeLibrary")).asMethod 100 | im.reflectMethod(nlm) 101 | } 102 | 103 | lazy val ninjaCompileTask = 104 | Def.task { 105 | val outfile = ninjaCompileFile.value 106 | val logger = streams.value.log.toLogger 107 | 108 | val config = { 109 | val mainClass = (Compile / selectMainClass).value 110 | val classpath = (Compile / fullClasspath).value.map(_.data.toPath) 111 | val baseDir = crossTarget.value 112 | 113 | scala.scalanative.build.Config.empty 114 | .withLogger(logger) 115 | .withMainClass(mainClass) 116 | .withClassPath(classpath) 117 | .withCompilerConfig(nativeConfig.value) 118 | .withBaseDir(baseDir.toPath()) 119 | } 120 | 121 | val writer = Files.newBufferedWriter(outfile, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE) 122 | logger.info(s"generating $outfile") 123 | 124 | val fclasspath = NativeLib.filterClasspath(config.classPath) 125 | val fconfig = config.withClassPath(fclasspath) 126 | 127 | // create optimized code and generate ll 128 | val entries = ScalaNative.entries(fconfig) 129 | 130 | val result = for { 131 | linked <- ScalaNative.link(fconfig, entries) 132 | _ = ScalaNative.logLinked(fconfig, linked, "ninja") 133 | optimized <- ScalaNative.optimize(fconfig, linked) 134 | codegen <- ScalaNative.codegen(fconfig, optimized) 135 | generated <- Future.sequence(codegen) 136 | } yield { 137 | // find native libs 138 | val nativelibs = NativeLib.findNativeLibs(fconfig) 139 | 140 | // compile all libs 141 | val objectPaths = { 142 | val libObjectPaths = nativelibs 143 | .map { unpackNativeCode } 144 | .map { destPath => 145 | val paths = findNativePaths(destPath) 146 | val projConfig = configureNativeLibrary(config, optimized, destPath) 147 | addBuildStatements(writer, projConfig, paths) 148 | } 149 | .flatten 150 | 151 | // compile generated ll 152 | val llObjectPaths = addBuildStatements(writer, fconfig, generated) 153 | 154 | libObjectPaths ++ llObjectPaths 155 | } 156 | 157 | writer.write(addExe(objectPaths)) 158 | writer.write(addDefaultTarget("$program")) 159 | writer.close() 160 | } 161 | Await.result(result, Duration.Inf) 162 | } 163 | 164 | lazy val ninjaTask = 165 | Def.task { 166 | val logger = streams.value.log.toLogger 167 | val baseDir = crossTarget.value 168 | val config = { 169 | val mainClass = (Compile / selectMainClass).value 170 | val classpath = (Compile / fullClasspath).value.map(_.data.toPath) 171 | 172 | scala.scalanative.build.Config.empty 173 | .withLogger(logger) 174 | .withBaseDir(baseDir.toPath) 175 | .withMainClass(mainClass) 176 | .withClassPath(classpath) 177 | .withCompilerConfig(nativeConfig.value) 178 | } 179 | val outpath = config.artifactPath 180 | val ninjaBuild = (target.value / "build.ninja").toPath 181 | 182 | val fclasspath = NativeLib.filterClasspath(config.classPath) 183 | val fconfig = config.withClassPath(fclasspath) 184 | val incdir = sourceDirectory.value / "main" / "c" / "include" 185 | 186 | val entries = ScalaNative.entries(fconfig) 187 | 188 | val linkResult = ScalaNative.link(fconfig, entries).map { linked => 189 | ScalaNative.logLinked(fconfig, linked, "ninja") 190 | 191 | val writer = 192 | Files.newBufferedWriter(ninjaBuild, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE) 193 | logger.info(s"generating $ninjaBuild") 194 | 195 | writer.write(addRules(config, linked, outpath, incdir.toPath)) 196 | 197 | writer.write(s"include ${ninjaCompileFile.value}\n\n") 198 | writer.close() 199 | 200 | ninjaBuild 201 | } 202 | 203 | Await.result(linkResult, Duration.Inf) 204 | } 205 | 206 | def addExe(objectsPaths: Seq[Path]): String = { 207 | val paths = for { 208 | p <- objectsPaths 209 | if !p.toString.contains("/scala-native/windows/") 210 | } yield p.abs 211 | 212 | s"build $$program: exe ${paths.mkString(" ")}\n\n" 213 | } 214 | 215 | def addDefaultTarget(name: String): String = s"default $name\n\n" 216 | 217 | def addRules(config: Config, linkerResult: ReachabilityAnalysis.Result, outpath: Path, incdir: Path): String = { 218 | val configFlags = { 219 | val multithreadingEnabled = 220 | if (config.compilerConfig.multithreadingSupport) 221 | Seq("-DSCALANATIVE_MULTITHREADING_ENABLED") 222 | else Nil 223 | val usingCppExceptions = 224 | if (config.usingCppExceptions) 225 | Seq("-DSCALANATIVE_USING_CPP_EXCEPTIONS") 226 | else Nil 227 | val allowTargetOverrrides = 228 | config.compilerConfig.targetTriple.map(_ => s"-Wno-override-module") 229 | multithreadingEnabled ++ usingCppExceptions ++ allowTargetOverrrides 230 | } 231 | val cflags = opt(config) ++: flto(config) ++: ninja_target(config) ++: configFlags :+ "-fvisibility=hidden" 232 | 233 | val links = { 234 | val srclinks = linkerResult.links.map(_.name) 235 | val gclinks = config.gc.links 236 | // We need extra linking dependencies for: 237 | // * libdl for our vendored libunwind implementation. 238 | // * libpthread for process APIs and parallel garbage collection. 239 | "pthread" +: "dl" +: srclinks ++: gclinks 240 | } 241 | val linkopts = linkOpts(config) ++ links.map("-l" + _) 242 | val linkflags = flto(config) ++ ninja_target(config) 243 | val ltoName = lto(config).getOrElse("none") 244 | 245 | s"""|clang = ${config.clang.abs} 246 | |clangpp = ${config.clangPP.abs} 247 | | 248 | |cflags = ${cflags.mkString(" ")} 249 | | 250 | |ldflags = ${linkflags.mkString(" ")} 251 | |ldopts = ${linkopts.mkString(" ")} 252 | | 253 | |program = $outpath 254 | | 255 | |rule cc 256 | | command = $$clang -std=gnu11 $$cflags $$auxflags -isystem $incdir -c $$in -o $$out 257 | | description = compile object file (${config.gc.name} gc, $ltoName lto) 258 | | 259 | |rule ll 260 | | command = $$clang $$cflags -c $$in -o $$out 261 | | description = compile ll file (${config.gc.name} gc, $ltoName lto) 262 | | 263 | |rule cpp 264 | | command = $$clangpp -std=c++11 $$cflags $$auxflags -isystem $incdir -c $$in -o $$out 265 | | description = compile c++ file (${config.gc.name} gc, $ltoName lto) 266 | | 267 | |rule exe 268 | | command = $$clangpp $$ldflags $$in -o $$out $$ldopts 269 | | description = link exe (${config.gc.name} gc, $ltoName lto) 270 | | 271 | |""".stripMargin 272 | } 273 | 274 | def addBuildStatements(writer: Writer, config: Config, files: Seq[Path]): Seq[Path] = { 275 | files.map { path => 276 | val inpath = path.abs 277 | val outpath = inpath + oExt 278 | val isCpp = inpath.endsWith(cppExt) 279 | val isLl = inpath.endsWith(llExt) 280 | val objPath = Paths.get(outpath) 281 | 282 | val rule = if (isCpp) "cpp" else if (isLl) "ll" else "cc" 283 | 284 | writer.write(s"build $objPath: $rule $inpath\n") 285 | 286 | if (config.compileOptions.nonEmpty) { 287 | writer.write(s" auxflags = ${config.compileOptions.mkString(" ")}\n") 288 | } 289 | writer.write('\n') 290 | 291 | objPath 292 | } 293 | } 294 | 295 | /** Object file extension: ".o" */ 296 | val oExt = ".o" 297 | 298 | /** C++ file extension: ".cpp" */ 299 | val cppExt = ".cpp" 300 | 301 | /** LLVM intermediate file extension: ".ll" */ 302 | val llExt = ".ll" 303 | 304 | /** List of source patterns used: ".c, .cpp, .S" */ 305 | val srcExtensions = Seq(".c", cppExt, ".S") 306 | 307 | private def linkOpts(config: Config): Seq[String] = 308 | config.linkingOptions ++ { 309 | config.mode match { 310 | // disable UB sanitizer in debug mode 311 | case Mode.Debug => Seq("-fno-sanitize=all") 312 | case Mode.ReleaseFull => Seq("-s") 313 | case _ => Seq.empty 314 | } 315 | } 316 | 317 | private def lto(config: Config): Option[String] = 318 | (config.mode, config.LTO) match { 319 | case (Mode.Debug, _) => None 320 | case (_: Mode.Release, LTO.None) => None 321 | case (_: Mode.Release, lto) => Some(lto.name) 322 | } 323 | 324 | private def flto(config: Config): Seq[String] = 325 | lto(config).fold[Seq[String]] { 326 | Seq() 327 | } { name => Seq(s"-flto=$name") } 328 | 329 | private def ninja_target(config: Config): Seq[String] = 330 | config.compilerConfig.targetTriple match { 331 | case Some(tt) => Seq("-target", tt) 332 | case None => Seq("-Wno-override-module") 333 | } 334 | 335 | private def opt(config: Config): Seq[String] = 336 | config.mode match { 337 | // disable UB sanitizer in debug mode 338 | case Mode.Debug => List("-O0", "-fno-sanitize=all") 339 | case Mode.ReleaseFast => List("-O2", "-Xclang", "-O2") 340 | case Mode.ReleaseFull => List("-O3", "-Xclang", "-O3") 341 | case Mode.ReleaseSize => List("-Os", "-Xclang", "-Oz") 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.10.11 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val scalaNativeVersion = "0.5.8" 2 | 3 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % scalaNativeVersion) 4 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 5 | addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.0") 6 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 7 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 8 | addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.11.1") 9 | addSbtPlugin("org.typelevel" % "sbt-tpolecat" % "0.5.2") 10 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Aliases.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | object SAliases: 4 | val map = Map( 5 | "erb" -> "rubydoc", 6 | "yaml" -> "yml", 7 | "rdoc" -> "md", 8 | "xlsx" -> "xls", 9 | "webp" -> "image", 10 | "rdata" -> "r", 11 | "jpg" -> "image", 12 | "rspec" -> "rb", 13 | "svg" -> "image", 14 | "gitconfig" -> "git", 15 | "bat" -> "windows", 16 | "rspec_parallel" -> "rb", 17 | "png" -> "image", 18 | "ttf" -> "font", 19 | "dockerfile" -> "docker", 20 | "pyc" -> "py", 21 | "mov" -> "video", 22 | "ini" -> "windows", 23 | "rds" -> "r", 24 | "ogg" -> "audio", 25 | "woff" -> "font", 26 | "editorconfig" -> "conf", 27 | "ogv" -> "video", 28 | "eot" -> "font", 29 | "apk" -> "android", 30 | "gemspec" -> "rb", 31 | "scss" -> "css", 32 | "bash_profile" -> "shell", 33 | "csv" -> "xls", 34 | "wav" -> "audio", 35 | "mp4" -> "video", 36 | "flv" -> "video", 37 | "xul" -> "xml", 38 | "slim" -> "rubydoc", 39 | "lock" -> "rb", 40 | "webm" -> "video", 41 | "pptx" -> "ppt", 42 | "pxm" -> "image", 43 | "jpeg" -> "image", 44 | "woff2" -> "font", 45 | "bmp" -> "image", 46 | "gemfile" -> "rb", 47 | "rakefile" -> "rb", 48 | "lhs" -> "hs", 49 | "license" -> "md", 50 | "guardfile" -> "rb", 51 | "zsh-theme" -> "shell", 52 | "ru" -> "rb", 53 | "ico" -> "image", 54 | "properties" -> "json", 55 | "gradle" -> "android", 56 | "zsh" -> "shell", 57 | "zshrc" -> "shell", 58 | "mkv" -> "video", 59 | "rspec_status" -> "rb", 60 | "gif" -> "image", 61 | "gdoc" -> "doc", 62 | "fish" -> "shell", 63 | "rar" -> "zip", 64 | "gz" -> "zip", 65 | "gitignore_global" -> "git", 66 | "cls" -> "tex", 67 | "gsheet" -> "xls", 68 | "gitignore" -> "git", 69 | "m4a" -> "audio", 70 | "mobi" -> "ebook", 71 | "readme" -> "md", 72 | "tsx" -> "jsx", 73 | "avi" -> "video", 74 | "bashrc" -> "shell", 75 | "flac" -> "audio", 76 | "docx" -> "doc", 77 | "localized" -> "apple", 78 | "bash_history" -> "shell", 79 | "otf" -> "font", 80 | "ds_store" -> "apple", 81 | "procfile" -> "rb", 82 | "tiff" -> "image", 83 | "mkd" -> "md", 84 | "jar" -> "java", 85 | "tar" -> "zip", 86 | "ipynb" -> "py", 87 | "mp3" -> "audio", 88 | "bash" -> "shell", 89 | "markdown" -> "md", 90 | "exe" -> "windows", 91 | "gslides" -> "ppt", 92 | "stylus" -> "styl", 93 | "sh" -> "shell", 94 | ) 95 | end SAliases 96 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Core.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | package generic 3 | 4 | import java.nio.file.Files 5 | import java.io.IOException 6 | import java.nio.file.{ Path, Paths } 7 | import java.nio.file.LinkOption 8 | import java.nio.file.{ AccessDeniedException, NoSuchFileException } 9 | import java.time.{ Instant, ZoneId } 10 | import java.time.format.DateTimeFormatter 11 | 12 | import scala.annotation.unused 13 | import scala.collection.mutable 14 | import scala.jdk.CollectionConverters.* 15 | import scala.util.Using 16 | 17 | trait Core: 18 | protected def format(r: Int, w: Int, x: Int, special: Boolean, ch: Char, builder: StringBuilder): Unit = 19 | val _ = builder 20 | .append(if r == 0 then '-' else 'r') 21 | .append(if w == 0 then '-' else 'w') 22 | .append( 23 | if special then if x == 0 then ch.toUpper else ch 24 | else if x == 0 then '-' 25 | else 'x' 26 | ) 27 | end format 28 | 29 | protected def orderByName: Ordering[FileInfo] 30 | 31 | def permissionString(imode: Int): String 32 | 33 | def ls(config: Config) = Env { 34 | val items = if config.paths.isEmpty then List(Paths.get(".")) else config.paths 35 | 36 | if config.tree then tree(config, items) 37 | else lsNormal(config, items) 38 | } 39 | 40 | private def lsNormal(config: Config, items: List[Path]) = Env { 41 | val decorators = layout(config) 42 | 43 | if config.listDirectories then 44 | val linkOptions = 45 | Option.unless(config.dereference || config.dereferenceArgs || config.dereferenceArgsToDirectory)( 46 | LinkOption.NOFOLLOW_LINKS 47 | ) 48 | val (dirPaths, filePaths) = items.partition(Files.isDirectory(_, linkOptions.toSeq*)) 49 | 50 | listAll(list(filePaths, config), config, decorators) 51 | 52 | val showPrefix = config.recursive || dirPaths.lengthCompare(1) > 0 || filePaths.nonEmpty 53 | 54 | val dirStack = mutable.Queue(dirPaths*) 55 | 56 | while dirStack.nonEmpty do 57 | val dir = dirStack.dequeue 58 | 59 | if showPrefix then println(s"\uf115 $dir:") 60 | 61 | Using(Files.newDirectoryStream(dir)) { dirstream => 62 | val entries = for path <- dirstream.asScala if config.showAll || !Files.isHidden(path) 63 | yield 64 | if config.recursive && Files.isDirectory(path, linkOptions.toSeq*) then dirStack += path 65 | 66 | path 67 | 68 | listAll(list(entries, config), config, decorators) 69 | }.failed.foreach: 70 | case e: NoSuchFileException => 71 | Console.err.println(s"scalals: no such file or directory: '${e.getMessage}'") 72 | case e: AccessDeniedException => 73 | Console.err.println(s"scalals: access denied: '${e.getMessage()}'") 74 | case e => 75 | Console.err.println(s"scalals: error $e") 76 | 77 | println() 78 | end while 79 | else listAll(list(items, config), config, decorators) 80 | end if 81 | } 82 | 83 | protected def traverse( 84 | fileInfo: FileInfo, 85 | config: Config, 86 | prefix: String = "", 87 | subdirPrefix: String = "", 88 | depth: Int = 0, 89 | )(using @unused z: Env): Unit = 90 | val keepGoing = !config.maxDepth.exists(depth > _) 91 | 92 | if keepGoing then 93 | val decorators = layout(config) 94 | val builder = StringBuilder(prefix) 95 | val columns = decorators.size 96 | 97 | for (decorator, idx) <- decorators.zipWithIndex 98 | do 99 | val _ = decorator.decorate(fileInfo, builder) 100 | if idx < columns - 1 then builder += ' ' 101 | 102 | println(builder) 103 | 104 | if fileInfo.isDirectory then 105 | Using(Files.newDirectoryStream(fileInfo.path)) { dirstream => 106 | val entries = for path <- dirstream.asScala if config.showAll || !Files.isHidden(path) 107 | yield path 108 | 109 | val items = list(entries, config) 110 | 111 | if items.nonEmpty then 112 | for fileInfo <- items.init 113 | do traverse(fileInfo, config, subdirPrefix + " ├── ", subdirPrefix + " │ ", depth + 1) 114 | 115 | traverse(items.last, config, subdirPrefix + " └── ", subdirPrefix + " ", depth + 1) 116 | end if 117 | }.failed.foreach: 118 | case e: NoSuchFileException => 119 | Console.err.println(s"scalals: no such file or directory: '${e.getMessage}'") 120 | case e: AccessDeniedException => 121 | Console.err.println(s"scalals: access denied: '${e.getMessage()}'") 122 | case e => 123 | Console.err.println(s"scalals: ${fileInfo.path}: error $e - ${e.getCause}") 124 | end if 125 | end if 126 | end traverse 127 | 128 | protected def tree(config: Config, items: List[Path])(using @unused z: Env) = 129 | val dereference = config.dereference || config.dereferenceArgs 130 | for path <- items 131 | do 132 | try 133 | val dereferenceItem = dereference || (config.dereferenceArgsToDirectory && Files.isDirectory(path)) 134 | traverse(FileInfo(path, dereferenceItem), config) 135 | catch case e: IOException => Console.err.println(s"scalals: cannot access '$path': ${e.getMessage}") 136 | end tree 137 | 138 | protected def orderingFor(config: Config) = 139 | val orderBy = config.sort match 140 | case SortMode.size => Ordering.by((f: generic.FileInfo) => (-f.size, f.name)) 141 | case SortMode.time => Ordering.by((f: generic.FileInfo) => (-f.lastModifiedTime.toEpochMilli(), f.name)) 142 | case SortMode.extension => 143 | Ordering.by { (f: generic.FileInfo) => 144 | val e = f.name.dropWhile(_ == '.') 145 | val dot = e.lastIndexOf('.') 146 | if dot > 0 then e.splitAt(dot).swap 147 | else ("", f.name) 148 | } 149 | case _ => orderByName 150 | 151 | val orderDirection = if config.reverse then orderBy.reverse else orderBy 152 | 153 | if config.groupDirectoriesFirst then groupDirsFirst(orderDirection) else orderDirection 154 | end orderingFor 155 | 156 | protected def list(items: IterableOnce[Path], config: Config)(using @unused z: Env) = 157 | given Ordering[FileInfo] = orderingFor(config) 158 | 159 | val listingBuffer = scala.collection.mutable.TreeSet.empty[generic.FileInfo] 160 | 161 | for path <- items.iterator 162 | do 163 | try listingBuffer += FileInfo(path, config.dereference) 164 | catch case e: IOException => Console.err.println(s"scalals: cannot access '$path': ${e.getMessage}") 165 | 166 | listingBuffer 167 | end list 168 | 169 | protected def groupDirsFirst(underlying: Ordering[generic.FileInfo]): Ordering[generic.FileInfo] = 170 | new Ordering[FileInfo]: 171 | override def compare(a: generic.FileInfo, b: generic.FileInfo): Int = 172 | if a.isDirectory == b.isDirectory then underlying.compare(a, b) 173 | else if a.isDirectory then -1 174 | else 1 175 | 176 | def listAll( 177 | listingBuffer: scala.collection.mutable.Set[FileInfo], 178 | config: Config, 179 | decorators: Vector[Decorator], 180 | ): Unit = 181 | // import java.util.{ List => JList } 182 | // import java.util.function.Supplier 183 | // scala.collection.mutable.ArrayBuffer.empty[FileInfo] 184 | 185 | // val supplier: Supplier[JList[FileInfo]] = () => listingBuffer.asJava 186 | // new ArrayList[FileInfo](100) 187 | // timing("collect"){ 188 | 189 | // .collect(Collectors.toCollection[FileInfo, JList[FileInfo]](supplier)) 190 | // Collectors.toList()) 191 | 192 | // timing("sort")(listing.sort(if (config.reverse) comparator.reversed() else comparator)) 193 | // val sorted = timing("sort")(listingBuffer.sortWith(if (config.reverse) { (a: FileInfo, b: FileInfo) => !comparator(a, b) } else comparator)) 194 | 195 | if listingBuffer.nonEmpty then 196 | val output = 197 | for 198 | fileInfo <- listingBuffer.toVector 199 | decorator <- decorators 200 | builder = StringBuilder() 201 | yield decorator.decorate(fileInfo, builder) -> builder 202 | 203 | val sizes = output.map(_._1.abs) 204 | // val minlen = sizes.min 205 | val columns = 206 | if config.long || config.longWithoutGroup || config.oneLine then decorators.size 207 | else 208 | val terminalMax = Terminal.width 209 | (terminalMax / (sizes.max + 1)) max 1 210 | val maxColSize = 211 | val g = sizes.grouped(columns).toList 212 | val h = 213 | if sizes.size > columns then g.init :+ (g.last ++ List.fill(columns - g.last.size)(0)) 214 | else g 215 | h.transpose.map(_.max) 216 | 217 | // Console.err.println(s"$columns") 218 | // Console.err.println(maxColSize.mkString(", ")) 219 | for record <- output.grouped(columns) do 220 | for 221 | ((width, builder), i) <- record.zipWithIndex 222 | colSize = maxColSize(i) 223 | padding = " " * (colSize - width.abs + 1) 224 | do 225 | if width < 0 then print(padding) 226 | 227 | print(builder) 228 | 229 | if i < record.size - 1 then print(if width >= 0 then padding else " ") 230 | else println() 231 | end for 232 | end for 233 | 234 | final def date: Decorator = new Decorator: 235 | private val halfayear: Long = 31556952L / 2 236 | private val recentLimit = Instant.now.minusSeconds(halfayear).toEpochMilli() 237 | private val cache = mutable.LongMap.empty[String] 238 | private val recentFormat = DateTimeFormatter.ofPattern("MMM ppd hh:mm").withZone(ZoneId.systemDefault()) 239 | private val dateFormat = DateTimeFormatter.ofPattern("MMM ppd yyyy").withZone(ZoneId.systemDefault()) 240 | 241 | override def decorate(file: generic.FileInfo, builder: StringBuilder): Int = 242 | val instant = file.lastModifiedTime.toEpochMilli() 243 | 244 | val date = cache.getOrElseUpdate( 245 | instant / 1000, { 246 | val format = if instant > recentLimit then recentFormat else dateFormat 247 | 248 | format.format(file.lastModifiedTime) 249 | }, 250 | ) 251 | builder.append(date) 252 | date.length 253 | end decorate 254 | end date 255 | 256 | def layout(config: Config): Vector[Decorator] = 257 | // val ext = file.name.dropWhile(_ == '.').replaceFirst(".*[.]", "").toLowerCase(Locale.ENGLISH) 258 | // 259 | // val key = if (files.contains(ext)) ext else aliases.getOrElse(ext, "file")v1 260 | // val symbol = files.getOrElse(key, ' ') 261 | 262 | // val code = Colors.colorFor(file) 263 | // 264 | // import Console.RESET 265 | 266 | // val output = Seq(IconDecorator & ColorDecorator, IndicatorDecorator).foldLeft(Decorated(0, "")){ 267 | // case (d, action) ⇒ d |+| action(file) 268 | // } 269 | 270 | // val decorated = if (config.hyperlink) hyperlink(file.path.toUri.toURL.toString, file.name) else file.name 271 | val decorator: Decorator = 272 | val d = Decorator( 273 | IconDecorator, 274 | config.hyperlink match 275 | case When.never => Decorator.name 276 | case When.auto if !Terminal.isTTYOutput => Decorator.name 277 | case _ => HyperlinkDecorator(Decorator.name), 278 | ).colored(config.colorMode) 279 | .cond(config.indicatorStyle `ne` IndicatorStyle.none)( 280 | IndicatorDecorator(config.indicatorStyle) 281 | ) 282 | 283 | if config.showGitStatus then GitDecorator + d else d 284 | end decorator 285 | 286 | if config.long || config.longWithoutGroup then 287 | val perms = new Decorator: 288 | override def decorate(file: FileInfo, builder: StringBuilder): Int = 289 | val firstChar = 290 | if file.isBlockDev then 'b' 291 | else if file.isCharDev then 'c' 292 | else if file.isDirectory then 'd' 293 | else if file.isSymlink then 'l' 294 | else if file.isPipe then 'p' 295 | else if file.isSocket then 's' 296 | else '-' 297 | 298 | val _ = builder.append(firstChar).append(permissionString(file.permissions)) 299 | 300 | 3 * 3 + 1 301 | end decorate 302 | 303 | val fileAndLink = new Decorator: 304 | override def decorate(file: FileInfo, builder: StringBuilder): Int = 305 | val n = decorator.decorate(file, builder) 306 | 307 | if file.isSymlink then 308 | val t = Files.readSymbolicLink(file.path) 309 | val target = s" → $t" 310 | 311 | builder.append(target) 312 | 313 | n + target.length() 314 | else n 315 | end if 316 | end decorate 317 | 318 | val user = new Decorator: 319 | override def decorate(file: FileInfo, builder: StringBuilder): Int = 320 | val owner = 321 | try file.owner 322 | catch case e: IOException => "-" 323 | 324 | builder.append(owner) 325 | owner.length() 326 | end decorate 327 | 328 | val group: Decorator = (file: FileInfo, builder: StringBuilder) => 329 | val group = 330 | try 331 | // val group = file.group 332 | // principalCache.getOrElseUpdate(group, group.getName) 333 | file.group 334 | catch case e: IOException => "-" 335 | builder.append(group) 336 | group.length() 337 | 338 | val sizeDecorator = config.humanReadable.fold(SizeDecorator(config.blockSize))(HumanSizeDecorator(_)) 339 | 340 | Vector(perms, user) ++ (if config.long then Vector(group) else Vector.empty) ++ Vector( 341 | ToggleAlignment(sizeDecorator), 342 | date, 343 | fileAndLink, 344 | ) 345 | else if config.printSize then 346 | val sizeDecorator = config.humanReadable.fold(SizeDecorator(config.blockSize))(HumanSizeDecorator(_)) 347 | Vector(sizeDecorator + decorator) 348 | else Vector(decorator) 349 | end if 350 | end layout 351 | end Core 352 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/CoreConfig.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import java.nio.file.Files 4 | 5 | object CoreConfig: 6 | val files: Map[String, Char] = SFiles.map 7 | // val p = new Properties() 8 | // p.load(this.getClass.getClassLoader.getResourceAsStream("files.properties")) 9 | // p.load(new FileInputStream("files.properties")) 10 | // p.asScala.mapValues(_.charAt(0)).toMap 11 | // } 12 | val aliases: Map[String, String] = SAliases.map 13 | // val p = new Properties() 14 | end CoreConfig 15 | // p.load(this.getClass.getClassLoader.getResourceAsStream("aliases.properties")) 16 | // p.load(new FileInputStream("aliases.properties")) 17 | // p.asScala.toMap 18 | // } 19 | 20 | sealed trait FileType 21 | case object RegularFile extends FileType 22 | case object Directory extends FileType 23 | case object Socket extends FileType 24 | case object Pipe extends FileType 25 | case object Special extends FileType 26 | case object Symlink extends FileType 27 | case object Orphan extends FileType 28 | case object Executable extends FileType 29 | 30 | class Extension(val suffix: String) extends FileType: 31 | override def equals(other: Any): Boolean = other match 32 | case s: String if s != null => s.endsWith(suffix) 33 | case e: Extension if e != null => suffix == e.suffix 34 | case _ => false 35 | override def hashCode: Int = suffix.## 36 | end Extension 37 | 38 | object Extension: 39 | def apply(glob: String): Extension = new Extension(glob.stripPrefix("*.")) 40 | 41 | object Colors: 42 | def ansiColor(str: String): String = "\u001b[" + str + "m" 43 | 44 | val getType: PartialFunction[String, FileType] = 45 | case "fi" => RegularFile 46 | case "di" => Directory 47 | case "ln" => Symlink 48 | case "or" => Orphan 49 | case "ex" => Executable 50 | case "so" => Socket 51 | case "pi" => Pipe 52 | case "do" | "bd" | "cd" => Special // door, block device, char device 53 | // case "mh" => multi hardlink 54 | case s if s.startsWith("*.") => Extension(s) 55 | end getType 56 | 57 | lazy val getColors: Map[AnyRef, String] = { 58 | val Assign = raw"([^=]+)=([\d;]+|target)".r 59 | for 60 | definition <- sys.env.get("LS_COLORS").filterNot(_.isEmpty).toList 61 | assign <- definition.split(':') 62 | (lhs, rhs) <- assign match 63 | case Assign(lhs, rhs) => Some(lhs -> rhs) 64 | case _ => None 65 | if getType.isDefinedAt(lhs) 66 | fileType = getType(lhs) 67 | yield fileType -> { if rhs == "target" then "" else ansiColor(rhs) } 68 | end for 69 | }.toMap[AnyRef, String].withDefaultValue(ansiColor("00")) 70 | 71 | def colorFor(file: generic.FileInfo) = 72 | val color = 73 | if file.isDirectory then Directory 74 | else if file.isSymlink then 75 | if Files.notExists(file.path) && getColors.contains(Orphan) then Orphan 76 | else Symlink 77 | else if file.isPipe then Pipe 78 | else if file.isSocket then Socket 79 | else if file.isRegularFile then 80 | if file.isExecutable then Executable 81 | else RegularFile 82 | else Special 83 | 84 | // println("colors: #" + getColors.size.toString) 85 | // getColors.get(color) 86 | val fileColor = 87 | if color eq RegularFile then 88 | getColors.collectFirst: 89 | case (k, v) if k == file.name => v 90 | else None 91 | 92 | fileColor.getOrElse(getColors(color)) 93 | end colorFor 94 | end Colors 95 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/FileInfo.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | package generic 3 | 4 | import java.time.Instant 5 | import java.nio.file.Path 6 | 7 | trait FileInfo: 8 | def name: String 9 | def path: Path 10 | def isDirectory: Boolean 11 | def isRegularFile: Boolean 12 | def isSymlink: Boolean 13 | def isPipe: Boolean 14 | def isSocket: Boolean 15 | def isCharDev: Boolean 16 | def isBlockDev: Boolean 17 | def group: String 18 | def owner: String 19 | def permissions: Int 20 | def size: Long 21 | def lastModifiedTime: Instant 22 | def lastAccessTime: Instant 23 | def creationTime: Instant 24 | def isExecutable: Boolean 25 | end FileInfo 26 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Files.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | object SFiles: 4 | val map = Map( 5 | "rss" -> '', 6 | "video" -> '', 7 | "image" -> '', 8 | "rdb" -> '', 9 | "iml" -> '', 10 | "shell" -> '', 11 | "styl" -> '', 12 | "doc" -> '', 13 | "apple" -> '', 14 | "env" -> '', 15 | "py" -> '', 16 | "clj" -> '', 17 | "txt" -> '', 18 | "log" -> '', 19 | "js" -> '', 20 | "go" -> '', 21 | "sqlite3" -> '', 22 | "sass" -> '', 23 | "ai" -> '', 24 | "ppt" -> '', 25 | "android" -> '', 26 | "nix" -> '', 27 | "yml" -> '', 28 | "epub" -> '', 29 | "lua" -> '', 30 | "pl" -> '', 31 | "pdf" -> '', 32 | "docker" -> '', 33 | "db" -> '', 34 | "tex" -> '', 35 | "md" -> '', 36 | "ebook" -> '', 37 | "xml" -> '', 38 | "dart" -> '', 39 | "file" -> '', 40 | "zip" -> '', 41 | "psd" -> '', 42 | "audio" -> '', 43 | "gform" -> '', 44 | "twig" -> '', 45 | "windows" -> '', 46 | "diff" -> '', 47 | "html" -> '', 48 | "git" -> '', 49 | "xls" -> '', 50 | "yarn.lock" -> '', 51 | "scala" -> '', 52 | "rubydoc" -> '', 53 | "gruntfile.js" -> '', 54 | "java" -> '', 55 | "coffee" -> '', 56 | "rb" -> '', 57 | "mustache" -> '', 58 | "avro" -> '', 59 | "r" -> '', 60 | "css" -> '', 61 | "cpp" -> '', 62 | "json" -> '', 63 | "conf" -> '', 64 | "hs" -> '', 65 | "less" -> '', 66 | "font" -> '', 67 | "php" -> '', 68 | "vim" -> '', 69 | "ts" -> '', 70 | "npmignore" -> '', 71 | "jsx" -> '', 72 | "d" -> '', 73 | "c" -> '', 74 | "erl" -> '', 75 | ) 76 | end SFiles 77 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Main.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import java.nio.file.{ Path, Paths } 4 | 5 | //import scala.io.AnsiColor 6 | 7 | enum SortMode: 8 | // TODO: version (-v) 9 | case name, none, size, time, extension 10 | 11 | enum When: 12 | case always, auto, never 13 | 14 | enum IndicatorStyle: 15 | case none, slash, classify, `file-type` 16 | 17 | given scopt.Read[Path] = scopt.Read.reads(Paths.get(_: String)) 18 | 19 | given scopt.Read[SortMode] = scopt.Read.reads(SortMode `valueOf` _) 20 | 21 | given scopt.Read[When] = scopt.Read.reads[When](When `valueOf` _) 22 | 23 | given scopt.Read[IndicatorStyle] = scopt.Read.reads(IndicatorStyle `valueOf` _) 24 | 25 | final case class Config( 26 | blockSize: Long = 1, 27 | sort: SortMode = SortMode.name, 28 | showAll: Boolean = false, 29 | listDirectories: Boolean = true, 30 | groupDirectoriesFirst: Boolean = false, 31 | humanReadable: Option[Int] = None, 32 | hyperlink: When = When.never, 33 | dereference: Boolean = false, 34 | dereferenceArgs: Boolean = false, 35 | dereferenceArgsToDirectory: Boolean = false, 36 | indicatorStyle: IndicatorStyle = IndicatorStyle.none, 37 | long: Boolean = false, 38 | longWithoutGroup: Boolean = false, 39 | oneLine: Boolean = false, 40 | showGitStatus: Boolean = false, 41 | tree: Boolean = false, 42 | maxDepth: Option[Int] = None, 43 | printSize: Boolean = false, 44 | paths: List[Path] = List.empty, 45 | colorMode: When = When.auto, 46 | reverse: Boolean = false, 47 | recursive: Boolean = false, 48 | ) 49 | 50 | object Main: 51 | import scopt.OParser 52 | val builder = OParser.builder[Config] 53 | val parser = 54 | import builder.* 55 | OParser.sequence( 56 | programName("scalals"), 57 | head("scalals", BuildInfo.version), 58 | opt[SortMode]("sort") 59 | .unbounded() 60 | .text("sort by WORD instead of name: none (-U), size (-S), time (-t), extension (-X)") 61 | .valueName("WORD") 62 | .action((m, c) => c.copy(sort = m)), 63 | opt[Option[When]]('F', "classify") 64 | .unbounded() 65 | .text("append indicator (one of */=>@|) to entries") 66 | .valueName("[WHEN]") 67 | .action((_, c) => c.copy(indicatorStyle = IndicatorStyle.classify)), 68 | opt[Unit]('H', "dereference-command-line") 69 | .unbounded() 70 | .text("follow symbolic links listed on the command line") 71 | .action((_, c) => c.copy(dereferenceArgs = true)), 72 | opt[Unit]("dereference-command-line-symlink-to-dir") 73 | .unbounded() 74 | .text("follow each command line symbolic link that points to a directory") 75 | .action((_, c) => c.copy(dereferenceArgsToDirectory = true)), 76 | opt[Unit]("file-type") 77 | .unbounded() 78 | .text("likewise, except do not append '*'") 79 | .action((_, c) => c.copy(indicatorStyle = IndicatorStyle.`file-type`)), 80 | opt[IndicatorStyle]("indicator-style") 81 | .unbounded() 82 | .text( 83 | "append indicator with style WORD to entry names: none (default), slash (-p), file-type (--file-type), classify (-F)" 84 | ) 85 | .valueName("STYLE") 86 | .action((s, c) => c.copy(indicatorStyle = s)), 87 | opt[Unit]('p', "indicator-style=slash") 88 | .unbounded() 89 | .text("append / indicator to directories") 90 | .action((_, c) => c.copy(indicatorStyle = IndicatorStyle.slash)), 91 | opt[Unit]('l', "long") 92 | .unbounded() 93 | .text("use a long listing format") 94 | .action((_, c) => c.copy(long = true)), 95 | opt[Unit]('o', "long-without-group-info") 96 | .unbounded() 97 | .text("like -l, but do not list group information") 98 | .action((_, c) => c.copy(longWithoutGroup = true)), 99 | opt[Long]("block-size") // TODO: parse unit 100 | .unbounded() 101 | .text("scale sizes by SIZE when printing them") 102 | .action((factor, c) => c.copy(blockSize = factor)), 103 | opt[Unit]('L', "dereference") 104 | .unbounded() 105 | .text("deference symbolic links") 106 | .action((_, c) => c.copy(dereference = true)), 107 | opt[Unit]('r', "reverse") 108 | .unbounded() 109 | .text("reverse order while sorting") 110 | .action((_, c) => c.copy(reverse = true)), 111 | opt[Unit]('R', "recursive") 112 | .unbounded() 113 | .text("list subdirectories recursively") 114 | .action((_, c) => c.copy(recursive = true)), 115 | opt[Option[Int]]("tree") 116 | .unbounded() 117 | .text("show tree") 118 | .valueName("[DEPTH]") 119 | .action((depth, c) => c.copy(tree = true, maxDepth = depth)), 120 | opt[Option[When]]("hyperlink") 121 | .unbounded() 122 | .text("hyperlink file names") 123 | .valueName("[WHEN]") 124 | .action((when, c) => c.copy(hyperlink = when.getOrElse(When.always))), 125 | opt[Unit]('d', "directory") 126 | .unbounded() 127 | .text("list directories themselves, not their contents") 128 | .action((_, c) => c.copy(listDirectories = false)), 129 | opt[Unit]('a', "all") 130 | .unbounded() 131 | .text("do not ignore hidden files") 132 | .action((_, c) => c.copy(showAll = true)), 133 | opt[Unit]('A', "almost-all") 134 | .unbounded() 135 | .text("do not list . and ..") 136 | .action((_, c) => c), 137 | opt[Option[When]]("color") 138 | .unbounded() 139 | .text("colorize the output") 140 | .valueName("[WHEN]") 141 | .action((when, c) => c.copy(colorMode = when.getOrElse(When.always))), 142 | opt[Unit]("git-status") 143 | .unbounded() 144 | .text("show git status for each file") 145 | .action((_, c) => c.copy(showGitStatus = true)), 146 | opt[Unit]("group-directories-first") 147 | .unbounded() 148 | .text("group directories before files") 149 | .action((_, c) => c.copy(groupDirectoriesFirst = true)), 150 | opt[Unit]('h', "human-readable") 151 | .unbounded() 152 | .text("print sizes in human readable format") 153 | .action((_, c) => c.copy(humanReadable = Some(1024))), 154 | opt[Unit]("si") 155 | .unbounded() 156 | .text("likewise, but use powers of 1000 not 1024") 157 | .action((_, c) => c.copy(humanReadable = Some(1000))), 158 | opt[Unit]('s', "size") 159 | .unbounded() 160 | .text("print size of each file, in blocks") 161 | .action((_, c) => c.copy(printSize = true)), 162 | opt[Unit]('S', "sort-by-size") 163 | .unbounded() 164 | .text("sort by file size") 165 | .action((_, c) => c.copy(sort = SortMode.size)), 166 | opt[Unit]('t', "sort-by-time") 167 | .unbounded() 168 | .text("sort by last modification time") 169 | .action((_, c) => c.copy(sort = SortMode.time)), 170 | opt[Unit]('U', "do-not-sort") 171 | .unbounded() 172 | .text("do not sort; display files in directory order") 173 | .action((_, c) => c.copy(sort = SortMode.none)), 174 | opt[Unit]('X', "sort-by-extension") 175 | .unbounded() 176 | .text("sort alphabetically by entry extension") 177 | .action((_, c) => c.copy(sort = SortMode.extension)), 178 | opt[Unit]('1', "one-line") 179 | .unbounded() 180 | .text("list one file per line") 181 | .action((_, c) => c.copy(oneLine = true)), 182 | help("help").text("show this help and exit"), 183 | version("version").text("show version information"), 184 | note("""| 185 | |Exit status: 186 | | 0 if OK, 187 | | 1 if minor problems (e.g., cannot access subdirectory), 188 | | 2 if serious trouble (e.g., parsing command line options). 189 | |""".stripMargin), 190 | arg[Path]("...") 191 | .hidden() 192 | .unbounded() 193 | .optional() 194 | .action { (x, c) => 195 | c.copy(paths = c.paths :+ x) 196 | }, 197 | ) 198 | end parser 199 | 200 | def main(args: Array[String]): Unit = 201 | // scopt does not support options with optional parameters (see https://github.com/scopt/scopt/pull/273) 202 | // it always requires a `=` after the option name with an empty string. 203 | // Fix the options missing the `=` here, before passing them to scopts. 204 | val (options, rest) = args.span(_ != "--") 205 | val fixed = options.map { arg => 206 | arg match 207 | case "--hyperlink" | "--color" | "--classify" | "--tree" => arg + '=' 208 | case _ => arg 209 | } 210 | OParser 211 | .parse(parser, fixed ++ rest, Config()) 212 | .fold(sys.exit(2)): 213 | Core.ls 214 | end main 215 | end Main 216 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Particles.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import java.util.concurrent.TimeUnit 4 | import java.nio.file.{ Path, Paths } 5 | 6 | import de.bley.scalals.CoreConfig.{ aliases, files } 7 | 8 | import scala.collection.mutable 9 | import scala.io.Source 10 | import scala.util.Try 11 | import scala.annotation.unused 12 | 13 | sealed trait FileSizeMode 14 | final case class ScaleSize(factor: Int) 15 | 16 | // TODO: implement file size mode 17 | class FileSize(@unused mode: FileSizeMode): 18 | def format(fileInfo: FileInfo) = 19 | fileInfo.size.toString 20 | 21 | // more decorators 22 | 23 | trait Decorator: 24 | def decorate(subject: generic.FileInfo, builder: StringBuilder): Int 25 | 26 | def +(other: Decorator): Decorator = 27 | val self = this 28 | new Decorator: 29 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 30 | self.decorate(subject, builder) + other.decorate(subject, builder) 31 | 32 | // TODO: 33 | // def colored(color: String): Decorator = ColorDecorator(color, this) 34 | def colored(mode: When): Decorator = mode match 35 | case When.never => this 36 | case When.auto if !Terminal.isTTYOutput => this 37 | case _ => ColorDecorator(this) 38 | 39 | def cond(p: Boolean)(d: => Decorator): Decorator = 40 | if p then this + d else this 41 | end Decorator 42 | 43 | object Decorator: 44 | object name extends Decorator: 45 | override def decorate(file: generic.FileInfo, builder: StringBuilder): Int = 46 | builder.append(file.name) 47 | file.name.length 48 | 49 | def apply(d: Decorator, ds: Decorator*): Decorator = ds.foldLeft(d) { case (lhs, rhs) => 50 | lhs + rhs 51 | } 52 | end Decorator 53 | 54 | object ColorDecorator: 55 | import scala.io.AnsiColor.RESET 56 | 57 | def apply(d: Decorator): Decorator = new Decorator: 58 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 59 | val code = Colors.colorFor(subject) 60 | val o = d.decorate(subject, builder.append(code)) 61 | builder.append(RESET) 62 | // inner.copy(text = s"$code${inner.text}$RESET") 63 | o 64 | end ColorDecorator 65 | 66 | final case class IndicatorDecorator(style: IndicatorStyle) extends Decorator: 67 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 68 | 69 | val indicator = style match 70 | case IndicatorStyle.slash => if subject.isDirectory then "/" else "" 71 | case IndicatorStyle.none => "" 72 | case IndicatorStyle.classify | IndicatorStyle.`file-type` => 73 | if subject.isDirectory then "/" 74 | else if subject.isSymlink then "@" 75 | else if subject.isSocket then "=" 76 | else if subject.isPipe then // FIFO 77 | "|" 78 | else if subject.isExecutable && style == IndicatorStyle.classify then "*" 79 | else "" 80 | builder.append(indicator) 81 | indicator.length 82 | end decorate 83 | end IndicatorDecorator 84 | 85 | object HyperlinkDecorator: 86 | def apply(d: Decorator): Decorator = new Decorator: 87 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 88 | val _ = builder 89 | .append("\u001b]8;;file://") 90 | .append(subject.path.toString) 91 | .append('\u0007') 92 | val o = d.decorate(subject, builder) 93 | 94 | builder.append("\u001b]8;;\u0007") 95 | 96 | o 97 | end decorate 98 | end HyperlinkDecorator 99 | 100 | object GitDecorator extends Decorator: 101 | import scala.io.AnsiColor.* 102 | 103 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 104 | GitDecorator(subject.path).toOption.fold(0) { info => 105 | val _ = info.get(subject.name).fold(builder.append(s" $GREEN✓$RESET ")) { modes => 106 | val m = modes.map: 107 | case 'M' => BLUE + 'M' + RESET 108 | case 'D' => RED + 'D' + RESET 109 | case 'A' => YELLOW + 'A' + RESET 110 | case '?' => MAGENTA + '?' + RESET 111 | case '!' => " " 112 | case c => c.toString 113 | builder 114 | .append(" " * (3 - modes.size)) 115 | .append(m.mkString) 116 | .append(' ') 117 | } 118 | 4 119 | } 120 | 121 | private val gitStatus = mutable.HashMap.empty[Path, Try[Map[String, Set[Char]]]] 122 | 123 | private def apply(path: Path): Try[Map[String, Set[Char]]] = 124 | val parent = path.toAbsolutePath.getParent() 125 | 126 | gitStatus.getOrElseUpdate(parent, getStatus(parent)) 127 | 128 | private def getStatus(path: Path) = 129 | val proc = new ProcessBuilder("git", "-C", path.toAbsolutePath.toString, "rev-parse", "--show-prefix").start() 130 | val status = for 131 | prefix <- Try(Source.fromInputStream(proc.getInputStream()).mkString.trim) 132 | if { 133 | while !proc.waitFor(10, TimeUnit.MILLISECONDS) do () 134 | proc.exitValue() == 0 135 | } 136 | gitStatus = new ProcessBuilder( 137 | "git", 138 | "-C", 139 | path.toAbsolutePath.toString, 140 | "status", 141 | "--porcelain", 142 | "-z", 143 | "-unormal", 144 | "--ignored", 145 | ".", 146 | ).start() 147 | out <- Try(Source.fromInputStream(gitStatus.getInputStream())) 148 | gitInfo = mutable.HashMap.empty[String, mutable.HashSet[Char]].withDefault(m => mutable.HashSet.empty[Char]) 149 | sb = new StringBuilder 150 | iter = out.iter.buffered 151 | yield 152 | def getc(): Boolean = 153 | val ch = iter.next() 154 | if ch == '\u0000' then false 155 | else 156 | sb append ch 157 | true 158 | 159 | while iter.hasNext do 160 | sb.clear() 161 | while getc() do {} 162 | val line = sb.toString 163 | val mode = line.substring(0, 2).trim 164 | val file = line.substring(3) 165 | 166 | // skip next line for renames 167 | if mode.contains('R') then while iter.hasNext && iter.next() != '\u0000' do {} 168 | 169 | val f = Paths.get(file.stripPrefix(prefix)).subpath(0, 1).toString 170 | 171 | gitInfo(f) = gitInfo(f) ++= mode 172 | end while 173 | gitInfo.view.mapValues(_.toSet).toMap 174 | status 175 | end getStatus 176 | end GitDecorator 177 | 178 | final class ToggleAlignment(other: Decorator) extends Decorator: 179 | // *hacky* negative size indicates right alignment 180 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 181 | -other.decorate(subject, builder) 182 | 183 | final class HumanSizeDecorator(power: Long) extends Decorator: 184 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 185 | val output = Size.render(subject.size, power) 186 | builder.append(output) 187 | output.length 188 | 189 | final case class SizeDecorator(scale: Long = 1L) extends Decorator: 190 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 191 | val output = (subject.size.toDouble / scale).round.toString 192 | builder.append(output) 193 | output.length 194 | 195 | object IconDecorator extends Decorator: 196 | override def decorate(subject: generic.FileInfo, builder: StringBuilder): Int = 197 | val ext = 198 | val e = subject.name.dropWhile(_ == '.') 199 | val dot = e.lastIndexOf('.') 200 | if dot > 0 then e.substring(dot + 1).toLowerCase() // FIXME: Locale.ENGLISH 201 | else "" 202 | // val key = if (files.contains(ext)) ext else aliases.getOrElse(ext, ext) 203 | // val symbol = files.getOrElse(key, ) // 204 | val symbol = files.getOrElse( 205 | ext, { 206 | aliases.get(ext).fold(if subject.isDirectory then '\uf115' else '\uf15b')(files.getOrElse(_, ' ')) 207 | }, 208 | ) 209 | val _ = builder.append(' ').append(symbol).append(" ") 210 | 211 | 4 212 | end decorate 213 | end IconDecorator 214 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Size.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | object Size: 4 | private val prefixes = Array("", "k", "M", "G", "T", "P") 5 | 6 | /// render human readable size 7 | def render(value: Long, powers: Long): String = 8 | var prefix = 0 9 | var scaled = value.toDouble 10 | 11 | while scaled > powers && prefix < prefixes.size do 12 | scaled /= powers 13 | prefix += 1 14 | 15 | if prefix != 0 && scaled < 10 then f"$scaled%.1f${prefixes(prefix)}" 16 | else f"${scaled.round}${prefixes(prefix)}" 17 | end render 18 | end Size 19 | -------------------------------------------------------------------------------- /shared/src/main/scala/de/bley/scalals/Utils.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | import scala.annotation.switch 4 | 5 | object Utils: 6 | // fast globbing implementation, see https://research.swtch.com/glob 7 | def glob(pattern: String, string: String): Boolean = 8 | var px, nx, nextPx, nextNx = 0 9 | var mismatch = false 10 | 11 | while !mismatch && (px < pattern.length || nx < string.length) do 12 | var handled = false 13 | 14 | if px < pattern.length then 15 | (pattern(px): @switch) match 16 | case '?' => 17 | if nx < string.length then 18 | px += 1 19 | nx += 1 20 | handled = true 21 | 22 | case '*' => 23 | // Try to match at nx. 24 | // If that doesn't work out, restart at nx+1 next. 25 | nextPx = px 26 | nextNx = nx + 1 27 | px += 1 28 | handled = true 29 | 30 | case c => 31 | if nx < string.length && string(nx) == c then 32 | px += 1 33 | nx += 1 34 | handled = true 35 | end if 36 | 37 | if handled then { 38 | // continue 39 | } else if 0 < nextNx && nextNx <= string.length then 40 | // Mismatch. Maybe restart. 41 | px = nextPx 42 | nx = nextNx 43 | else mismatch = true 44 | end while 45 | 46 | !mismatch 47 | end glob 48 | end Utils 49 | -------------------------------------------------------------------------------- /shared/src/test/scala/de/bley/scalals/UtilsTest.scala: -------------------------------------------------------------------------------- 1 | package de.bley.scalals 2 | 3 | class UtilsTests extends munit.FunSuite: 4 | import Utils.glob 5 | 6 | test("glob - simple") { 7 | assert(glob("abc", "abc")) 8 | } 9 | end UtilsTests 10 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import ./compat.nix).shellNix 2 | --------------------------------------------------------------------------------