diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index d717c75f8..000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,164 +0,0 @@ -name: Docs - -on: - push: - branches: - - main - pull_request: - branches: - - main - schedule: - - cron: "0 2 * * *" - -jobs: - build: - name: Build website artifacts - runs-on: ubuntu-latest - steps: - - name: Checkout uutils.github.io Repository - uses: actions/checkout@v4 - with: - repository: uutils/uutils.github.io - path: './uutils.github.io' - fetch-depth: 0 - - - name: Checkout Coreutils Repository - uses: actions/checkout@v4 - with: - repository: uutils/coreutils - path: './coreutils' - fetch-depth: 0 - - - name: Checkout Findutils Repository - uses: actions/checkout@v4 - with: - repository: uutils/findutils - path: './findutils' - fetch-depth: 0 - - - name: Install `rust` toolchain - uses: dtolnay/rust-toolchain@stable - - - name: Install `libacl` - run: | - sudo apt install libacl1-dev - - - name: Download tldr archive - run: | - curl https://tldr.sh/assets/tldr.zip --output coreutils/docs/tldr.zip - - - name: Install necessary tools (mdbook and mdbook-toc) - uses: taiki-e/install-action@v2 - with: - tool: mdbook,mdbook-toc - - - name: Build Coreutils Docs - run: | - cd coreutils - cargo run --bin uudoc --all-features - cd docs - mdbook build - - - name: Build Findutils Docs - run: | - cd findutils - cd docs - mdbook build - - - name: Run Zola - uses: shalzz/zola-deploy-action@v0.18.0 - env: - BUILD_DIR: uutils.github.io - BUILD_ONLY: true - - - name: Collect results into `public` folder - run: | - cp -r uutils.github.io/public public - cp -r coreutils/docs/book public/coreutils/docs - cp -r findutils/docs/book public/findutils/docs - - - name: Upload artifact for checking the output - uses: actions/upload-artifact@v4 - with: - path: ./public - - - name: Upload artifact for pages - uses: actions/upload-pages-artifact@v3 - with: - path: ./public - - # dev-docs: - # name: generate the dev doc - # runs-on: ubuntu-latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - # with: - # repository: uutils/coreutils - # path: './coreutils' - # fetch-depth: 0 - - # - name: Install `rust` toolchain - # uses: actions-rs/toolchain@v1 - # with: - # toolchain: stable - # default: true - # profile: minimal - - # - name: Build dev documentation - # run: | - # cd coreutils - # cargo doc --no-deps --all-features --workspace - - # - name: Deploy Docs - # uses: peaceiris/actions-gh-pages@v3 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # publish_dir: ./coreutils/target/doc - # destination_dir: coreutils/dev/ - - # build-report: - # name: generate the build report - # runs-on: ubuntu-latest - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - # with: - # repository: uutils/coreutils - # path: './coreutils' - # fetch-depth: 0 - - # - name: Install `rust` toolchain - # uses: actions-rs/toolchain@v1 - # with: - # toolchain: nightly - # default: true - # profile: minimal - - # - name: Build report - # run: | - # cd coreutils - # cargo +nightly build --timings=html -Zunstable-options - - # - name: Deploy build report - # uses: peaceiris/actions-gh-pages@v3 - # with: - # github_token: ${{ secrets.GITHUB_TOKEN }} - # publish_dir: ./coreutils/target/cargo-timings/ - # destination_dir: cargo-timings/ - - # Deployment job - deploy: - permissions: - contents: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: ubuntu-latest - needs: build - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 000000000..3e578d0b0 --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,443 @@ +name: Docs + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: "0 2 * * *" + +jobs: + # The WASM modules compile in parallel: coreutils has its own job (it needs + # l10n + feat_wasm parsing), while grep/find/diffutils share the matrix job + # below. Each leg uploads its `.wasm` plus small fragments (command names and + # a version block) that the `build` job stitches together into the + # playground's commands.js / version.js. Fragment files are numbered so the + # merge order is deterministic (coreutils base list first, then the rest). + wasm-coreutils: + name: Build uutils (coreutils) WASM + runs-on: ubuntu-latest + steps: + - name: Checkout Coreutils Repository + uses: actions/checkout@v7 + with: + repository: uutils/coreutils + path: './coreutils' + fetch-depth: 0 + + - name: Checkout Coreutils L10n Repository + uses: actions/checkout@v7 + with: + repository: uutils/coreutils-l10n + path: './coreutils-l10n' + fetch-depth: 0 + + - name: Install `rust` toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: coreutils + + - name: Copy l10n locales into coreutils + run: | + # Only copy locales into utilities that already exist in coreutils + # Exclude en-US.ftl to preserve the coreutils English originals + for util_dir in coreutils-l10n/src/uu/*/; do + util=$(basename "$util_dir") + if [ -d "coreutils/src/uu/$util/locales" ]; then + for ftl in "$util_dir"/locales/*.ftl; do + [ -f "$ftl" ] || continue + [ "$(basename "$ftl")" = "en-US.ftl" ] && continue + cp "$ftl" "coreutils/src/uu/$util/locales/" + done + fi + done + # Copy uucore l10n files, excluding en-US.ftl + if [ -d "coreutils-l10n/src/uucore/locales" ]; then + for ftl in coreutils-l10n/src/uucore/locales/*.ftl; do + [ -f "$ftl" ] || continue + [ "$(basename "$ftl")" = "en-US.ftl" ] && continue + cp "$ftl" "coreutils/src/uucore/locales/" + done + fi + + - name: Build uutils WASM binary + run: | + cd coreutils + # Build the multicall binary for WASI target + # Use --no-default-features to avoid platform-specific dependencies + cargo build --release --target wasm32-wasip1 -p coreutils --no-default-features --features feat_wasm + test -f target/wasm32-wasip1/release/coreutils.wasm + mkdir -p ../wasm-out + cp target/wasm32-wasip1/release/coreutils.wasm ../wasm-out/uutils.wasm + # Optimize WASM size if wasm-opt is available + if command -v wasm-opt &> /dev/null; then + wasm-opt -Oz ../wasm-out/uutils.wasm -o ../wasm-out/uutils.wasm + fi + echo "WASM binary size: $(du -h ../wasm-out/uutils.wasm | cut -f1)" + # Generate the list of available locales from .ftl files + locales=$(find src/uu/*/locales src/uucore/locales -name '*.ftl' 2>/dev/null \ + | sed 's|.*/||; s|\.ftl$||' | sort -u | paste -sd, -) + echo "const WASM_LOCALES = [$(echo "$locales" | sed 's/[^,]*/\"&\"/g')];" \ + > ../wasm-out/locales.js + echo "Available locales: $locales" + # Generate the list of available commands from the feat_wasm feature + # in Cargo.toml so the playground stays in sync with the WASM build. + # HIDDEN_CMDS: utilities that ship in the binary but we don't want to + # advertise in the playground (e.g. `yes` produces infinite output + # that just spams the terminal). + HIDDEN_CMDS='^(false|true|yes)$' + commands=$(sed -n '/^feat_wasm = \[/,/^\]/p' Cargo.toml \ + | grep -oE '"[a-zA-Z0-9_]+"' | tr -d '"' \ + | sort -u | grep -vE "$HIDDEN_CMDS" | paste -sd, -) + echo "$commands" | sed 's/[^,]*/"&"/g' > ../wasm-out/commands-00-coreutils.txt + echo "Available commands: $commands" + # Record the coreutils commit used to build the WASM binary + commit_hash=$(git rev-parse HEAD) + commit_short=$(git rev-parse --short HEAD) + commit_date=$(git show -s --format=%cI HEAD) + { + echo "const UUTILS_WASM_VERSION = {" + echo " commit: \"${commit_hash}\"," + echo " short: \"${commit_short}\"," + echo " date: \"${commit_date}\"" + echo "};" + } > ../wasm-out/version-00-coreutils.js + echo "uutils WASM build: ${commit_short} (${commit_date})" + + - name: Upload uutils WASM artifact + uses: actions/upload-artifact@v7 + with: + name: wasm-coreutils + path: wasm-out + + # grep, findutils, diffutils and sed ship standalone binaries built the same + # way, so they share one matrix job. Each entry lists its `bins` (findutils + # provides three: find, locate, updatedb); every bin emits its own .wasm. + # grep/findutils pull in the Oniguruma C library (onig_sys) and need a WASI + # sysroot to compile its bundled C sources; diffutils and sed are pure Rust and + # set needs_wasi: false. The `order` field keeps the merged command/version + # lists in a stable sequence in `build`. + wasm-standalone: + name: Build ${{ matrix.name }} WASM + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - name: grep + repo: uutils/grep + bins: grep + needs_wasi: true + commands: '"grep"' + version_const: UUTILS_GREP_VERSION + order: 10 + # findutils ships several standalone binaries; build find, locate and + # updatedb so the playground can offer them. (xargs is omitted: it must + # spawn child processes, which the browser WASI sandbox can't do.) + - name: find + repo: uutils/findutils + bins: find locate updatedb + needs_wasi: true + commands: '"find","locate","updatedb"' + version_const: UUTILS_FINDUTILS_VERSION + order: 20 + - name: diffutils + repo: uutils/diffutils + bins: diffutils + needs_wasi: false + commands: '"diff","cmp"' + version_const: UUTILS_DIFFUTILS_VERSION + order: 30 + - name: sed + repo: uutils/sed + bins: sed + needs_wasi: false + commands: '"sed"' + version_const: UUTILS_SED_VERSION + order: 40 + steps: + - name: Checkout ${{ matrix.name }} Repository + uses: actions/checkout@v7 + with: + repository: ${{ matrix.repo }} + path: ${{ matrix.name }} + fetch-depth: 0 + + - name: Install `rust` toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasip1 + + - name: Cache cargo build + uses: Swatinem/rust-cache@v2 + with: + workspaces: ${{ matrix.name }} + + - name: Build ${{ matrix.name }} WASM binary + run: | + if [ "${{ matrix.needs_wasi }}" = "true" ]; then + # Build a WASI sysroot so the onig_sys C sources can be compiled. + WASI_SDK_VERSION=25 + WASI_SDK_DIR="wasi-sdk-${WASI_SDK_VERSION}.0-x86_64-linux" + curl -sL "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/${WASI_SDK_DIR}.tar.gz" | tar xz + export WASI_SDK_PATH="$PWD/${WASI_SDK_DIR}" + export CC_wasm32_wasip1="$WASI_SDK_PATH/bin/clang" + export CFLAGS_wasm32_wasip1="--sysroot=$WASI_SDK_PATH/share/wasi-sysroot" + fi + cd ${{ matrix.name }} + mkdir -p ../wasm-out + # A module may ship several standalone binaries (findutils → find, + # locate, updatedb); build each and emit its own .wasm file. + for bin in ${{ matrix.bins }}; do + wasm="${bin}.wasm" + cargo build --release --target wasm32-wasip1 --bin "${bin}" + test -f "target/wasm32-wasip1/release/${wasm}" + cp "target/wasm32-wasip1/release/${wasm}" "../wasm-out/${wasm}" + # Optimize WASM size if wasm-opt is available + if command -v wasm-opt &> /dev/null; then + wasm-opt -Oz "../wasm-out/${wasm}" -o "../wasm-out/${wasm}" + fi + echo "${bin} WASM binary size: $(du -h ../wasm-out/${wasm} | cut -f1)" + done + # Advertise this module's commands in the playground's command list. + echo '${{ matrix.commands }}' > ../wasm-out/commands-${{ matrix.order }}-${{ matrix.name }}.txt + # Record the commit used to build this WASM module. + h=$(git rev-parse HEAD) + s=$(git rev-parse --short HEAD) + d=$(git show -s --format=%cI HEAD) + { + echo "const ${{ matrix.version_const }} = {" + echo " commit: \"${h}\"," + echo " short: \"${s}\"," + echo " date: \"${d}\"" + echo "};" + } > ../wasm-out/version-${{ matrix.order }}-${{ matrix.name }}.js + echo "${{ matrix.name }} WASM build: ${s} (${d})" + + - name: Upload ${{ matrix.name }} WASM artifact + uses: actions/upload-artifact@v7 + with: + name: wasm-${{ matrix.name }} + path: wasm-out + + lint-js: + name: Lint playground JavaScript + runs-on: ubuntu-latest + steps: + - name: Checkout uutils.github.io Repository + uses: actions/checkout@v7 + + - name: Set up Node.js + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Install dependencies + run: npm install + + - name: Run ESLint + run: npm run lint + + build: + name: Build website artifacts + runs-on: ubuntu-latest + needs: [wasm-coreutils, wasm-standalone] + steps: + - name: Checkout uutils.github.io Repository + uses: actions/checkout@v7 + with: + repository: uutils/uutils.github.io + path: './uutils.github.io' + fetch-depth: 0 + + - name: Checkout Coreutils Repository + uses: actions/checkout@v7 + with: + repository: uutils/coreutils + path: './coreutils' + fetch-depth: 0 + + - name: Checkout Coreutils L10n Repository + uses: actions/checkout@v7 + with: + repository: uutils/coreutils-l10n + path: './coreutils-l10n' + fetch-depth: 0 + + - name: Checkout Findutils Repository + uses: actions/checkout@v7 + with: + repository: uutils/findutils + path: './findutils' + fetch-depth: 0 + + - name: Install `rust` toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install system deps + run: | + sudo apt update + sudo apt install libacl1-dev libselinux1-dev libsystemd-dev + pip install babel + + - name: Install necessary tools (mdbook and mdbook-toc) + uses: taiki-e/install-action@v2 + with: + tool: mdbook@0.5.0,mdbook-toc@0.15.3 + + - name: Copy l10n locales into coreutils + run: | + # Only copy locales into utilities that already exist in coreutils + # Exclude en-US.ftl to preserve the coreutils English originals + for util_dir in coreutils-l10n/src/uu/*/; do + util=$(basename "$util_dir") + if [ -d "coreutils/src/uu/$util/locales" ]; then + for ftl in "$util_dir"/locales/*.ftl; do + [ -f "$ftl" ] || continue + [ "$(basename "$ftl")" = "en-US.ftl" ] && continue + cp "$ftl" "coreutils/src/uu/$util/locales/" + done + fi + done + # Copy uucore l10n files, excluding en-US.ftl + if [ -d "coreutils-l10n/src/uucore/locales" ]; then + for ftl in coreutils-l10n/src/uucore/locales/*.ftl; do + [ -f "$ftl" ] || continue + [ "$(basename "$ftl")" = "en-US.ftl" ] && continue + cp "$ftl" "coreutils/src/uucore/locales/" + done + fi + + - name: Patch mdbook theme with language selector + run: | + uutils.github.io/scripts/patch-mdbook-theme.sh coreutils/docs coreutils-l10n + + - name: Build Coreutils Docs + run: | + cd coreutils + # Download and repack tldr zip: uudoc expects pages/common/ prefix + curl -sfL https://github.com/tldr-pages/tldr/releases/download/v2.3/tldr-pages.zip -o /tmp/tldr-raw.zip || true + if [ -f /tmp/tldr-raw.zip ]; then + mkdir -p /tmp/tldr-repack/pages + cd /tmp/tldr-repack && unzip -o /tmp/tldr-raw.zip -d pages/ > /dev/null + zip -r "$OLDPWD/docs/tldr.zip" pages/ > /dev/null + cd "$OLDPWD" + fi + cargo run --bin uudoc --all-features + cd docs + # Remove deprecated 'multilingual' field unsupported by newer mdbook + sed -i '/^multilingual/d' book.toml + # Strip the legacy FA4 `fa` class from uudoc-generated platform icons + # so mdbook 0.5's Font Awesome parser picks up the `fa-brands` family + # instead of defaulting to `regular` and warning about linux/windows/apple. + find src/utils -name '*.md' -exec sed -i 's|class="fa fa-brands |class="fa-brands |g' {} + + mdbook build + + - name: Build Coreutils Docs (translations) + run: | + uutils.github.io/scripts/build-docs-l10n.sh coreutils + + - name: Build Findutils Docs + run: | + cd findutils/docs + # Remove deprecated 'multilingual' field unsupported by newer mdbook + sed -i '/^multilingual/d' book.toml + mdbook build + + - name: Download WASM artifacts + uses: actions/download-artifact@v8 + with: + pattern: wasm-* + path: wasm-artifacts + merge-multiple: true + + - name: Assemble WASM playground assets + run: | + # The four wasm-* artifacts each carry their .wasm plus numbered + # command/version fragments. Stitch them into the single commands.js / + # version.js the playground loads. SITE_VERSION is generated here since + # it describes the uutils.github.io repo, not any of the tool repos. + mkdir -p uutils.github.io/static/wasm + cp wasm-artifacts/*.wasm uutils.github.io/static/wasm/ + cp wasm-artifacts/locales.js uutils.github.io/static/wasm/ + + # Merge the command-name fragments in numeric (filename) order. + commands=$(cat $(ls wasm-artifacts/commands-*.txt | sort) | paste -sd, -) + echo "const WASM_COMMANDS = [${commands}];" \ + > uutils.github.io/static/wasm/commands.js + echo "Playground commands: ${commands}" + + # Record the uutils.github.io commit the site was built from. + site_hash=$(git -C uutils.github.io rev-parse HEAD) + site_short=$(git -C uutils.github.io rev-parse --short HEAD) + site_date=$(git -C uutils.github.io show -s --format=%cI HEAD) + { + cat $(ls wasm-artifacts/version-*.js | sort) + echo "const SITE_VERSION = {" + echo " commit: \"${site_hash}\"," + echo " short: \"${site_short}\"," + echo " date: \"${site_date}\"" + echo "};" + } > uutils.github.io/static/wasm/version.js + echo "site build: ${site_short} (${site_date})" + + - name: Run Zola + uses: shalzz/zola-deploy-action@v0.22.1 + env: + BUILD_DIR: uutils.github.io + BUILD_ONLY: true + + - name: Collect results into `public` folder + run: | + cp -r uutils.github.io/public public + cp -r coreutils/docs/book public/coreutils/docs + cp -r findutils/docs/book public/findutils/docs + # Copy translated docs to /coreutils/docs-{lang}/ + for lang_dir in coreutils/docs/book-*/; do + [ -d "$lang_dir" ] || continue + lang=$(basename "$lang_dir" | sed 's/^book-//') + cp -r "$lang_dir" "public/coreutils/docs-${lang}" + done + + - name: Run playground JS tests + run: | + npm install puppeteer@24 + node uutils.github.io/scripts/run-tests.js --dir public --port 8080 + + - name: Remove test files from deploy output + run: rm -f public/js/wasm-terminal.test.html + + - name: Upload artifact for checking the output + uses: actions/upload-artifact@v7 + with: + path: ./public + + - name: Upload artifact for pages + uses: actions/upload-pages-artifact@v5 + with: + path: ./public + + # Deployment job + deploy: + permissions: + contents: read + pages: write + id-token: write + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + if: (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'schedule' + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v5 diff --git a/.gitignore b/.gitignore index 15ce475e7..08f5bed39 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ -/public/ \ No newline at end of file +/public/ +/static/syntax-* +/static/wasm/ +/node_modules/ diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 000000000..b2d6e5eff --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,104 @@ +# Development + +## Prerequisites + +- [Zola](https://www.getzola.org/) (static site generator) +- [Rust toolchain](https://rustup.rs/) with `wasm32-wasip1` target +- [mdbook](https://rust-lang.github.io/mdBook/) and mdbook-toc +- Node.js (for running tests) +- Python 3 (for `babel`, used in l10n) + +## Building the site locally + +```sh +zola build +``` + +The output is in `public/`. + +To run a local dev server with live reload: + +```sh +zola serve +``` + +## Documentation generation + +The coreutils man pages and documentation are generated from the coreutils source code, not maintained in this repository. The CI workflow handles this automatically, but here is how it works: + +1. **uudoc** generates the mdbook source from the coreutils Rust source: + ```sh + cd coreutils + cargo run --bin uudoc --all-features + ``` + +2. **TLDR examples** are downloaded and bundled into the docs: + ```sh + curl -sfL https://github.com/tldr-pages/tldr/releases/download/v2.3/tldr-pages.zip -o tldr.zip + ``` + +3. **mdbook** builds the HTML documentation: + ```sh + cd coreutils/docs + mdbook build + ``` + +4. **Translations** are built by `scripts/build-docs-l10n.sh`, which swaps locale `.ftl` files and rebuilds uudoc + mdbook for each language. + +5. **Language selector** is patched into the mdbook theme by `scripts/patch-mdbook-theme.sh`. + +The generated docs are deployed at `/coreutils/docs/` and `/findutils/docs/` on the site. + +## WASM Playground + +The playground lets visitors run uutils coreutils commands in the browser via WebAssembly. + +### Building the WASM binary + +```sh +rustup target add wasm32-wasip1 +cd /path/to/coreutils +cargo build --release --target wasm32-wasip1 -p coreutils --no-default-features --features feat_wasm +cp target/wasm32-wasip1/release/coreutils.wasm /path/to/uutils.github.io/static/wasm/uutils.wasm +``` + +Not all coreutils compile to WASI — the `feat_wasm` feature set includes the compatible subset. Commands like `ls`, `sort`, `head`, `tail`, and `tr` are excluded because they depend on platform-specific syscalls. + +### How it works + +- `static/js/wasm-terminal.js` — main playground runtime (xterm.js + `@bjorn3/browser_wasi_shim`) +- `static/js/wasm-example.js` — inline "Run" button handler for shortcodes +- `templates/shortcodes/wasm_example.html` — Zola shortcode for embedding runnable examples +- `content/playground.md` — the playground page + +When the WASM binary is not available, the playground falls back to JavaScript implementations of common commands. + +### Running the tests + +The playground has unit tests in `static/js/wasm-terminal.test.html`. To run them: + +```sh +npm install puppeteer # one-time setup +node scripts/run-tests.js --dir static +``` + +Or after a full site build: + +```sh +node scripts/run-tests.js --dir public +``` + +The test runner starts a local HTTP server, opens the test page in headless Chrome via Puppeteer, and reports pass/fail counts. This is the same script used in CI. + +## CI workflow + +The GitHub Actions workflow (`.github/workflows/website.yml`) does the following: + +1. Checks out `uutils.github.io`, `coreutils`, `coreutils-l10n`, and `findutils` +2. Copies l10n locale files into the coreutils tree +3. Builds coreutils documentation (English + translations) with uudoc + mdbook +4. Builds findutils documentation with mdbook +5. Builds the WASM binary (`feat_wasm` feature set) +6. Builds the Zola site +7. Runs the playground JS tests +8. Deploys to GitHub Pages (on push to main or scheduled builds) diff --git a/README.md b/README.md index 2baf0d1c4..2e900e85e 100644 --- a/README.md +++ b/README.md @@ -5,28 +5,36 @@ This repository generates the user and dev documentations of https://github.com/ ## User documentation It is available on: -https://uutils.github.io/user/ +https://uutils.org/coreutils/docs/ Can be generated with: -``` +```bash cargo run --bin uudoc --all-features cd docs mdbook build ``` +## Playground + +An in-browser playground runs the uutils coreutils WASM build directly in your browser: +https://uutils.org/playground/ + +See also how it works: https://uutils.org/playground-how-it-works/ + ## Developer documentation: It is available on: -https://uutils.github.io/dev/coreutils/ +https://uutils.org/dev/coreutils/ Can be generated with: -``` +```bash cargo doc --no-deps --all-features --workspace ``` -The pages are committed daily into the gh-pages branch. +The website CI builds this on each `main`/scheduled run and publishes it +under `/dev/coreutils/` (and `/dev/findutils/`) as part of the deploy. ## Build timing Generated by cargo, the build timings can be seen on: -https://uutils.github.io/cargo-timings/cargo-timing.html +https://uutils.org/cargo-timings/cargo-timing.html diff --git a/config.toml b/config.toml index be03294fa..3ed6d8901 100644 --- a/config.toml +++ b/config.toml @@ -1,16 +1,16 @@ # The URL the site will be built for -base_url = "https://uutils.github.io" +base_url = "https://uutils.org" title = "uutils" -description = "" +description = "uutils — cross-platform Rust reimplementations of essential Unix command-line utilities" default_language = "en" -paginate_by = 10 - -generate_feed = true +generate_feeds = true [markdown] -highlight_code = true -highlight_theme = "OneHalfLight" +[markdown.highlighting] +style = "class" +light_theme = "one-light" +dark_theme = "one-dark-pro" diff --git a/content/_index.md b/content/_index.md index f25cb1b4b..d265b57e0 100644 --- a/content/_index.md +++ b/content/_index.md @@ -3,36 +3,76 @@ title = "Home" template = "index.html" +++ -
- -
uutils
+
+
+ uutils --about + + + +
+
+
+ uutils logo +
user@machine:~$ uutils --about
+
uutils_
+

Cross-platform Rust reimplementations of the command-line tools you use every day, with full GNU compatibility.

+ +
+
The uutils project reimplements ubiquitous command line utilities in Rust. Our goal is to modernize the utils, while retaining full compatibility with the existing utilities. +We are planning to replace all essential Linux tools. + +# Why? + +C has served us well for decades, but it is time to move on. For new generations of developers, Rust is more appealing — and it brings real, substantive benefits: + +- **Memory safety** — eliminates entire categories of bugs without sacrificing performance +- **Cross-platform portability** — the same code runs on Linux, macOS, Windows, Android, FreeBSD, NetBSD, OpenBSD, Solaris, Illumos, Fuchsia, Redox and even [WebAssembly](/playground) +- **Modern tooling** — Cargo, crates.io and a rich ecosystem mean we don't have to reinvent the wheel +- **Easy parallelism** — fearless concurrency built into the language +- **Terrific performance** — on par with or better than C implementations +- **Modern engineering practices** — excellent CI coverage (build on many OS/archs, GNU test suite execution), a smooth contributor experience, and continuous fuzzing (including differential fuzzing) at the review phase to catch edge cases early + +This is not about fighting the GNU project. It is not primarily about security (GNU coreutils only had [17 CVEs since 2003](https://security-tracker.debian.org/tracker/source-package/coreutils)) or about license debates. It is about **modernizing foundational software** so it can be maintained and improved by the next generation of contributors. + +Ubuntu is already [carefully but purposefully adopting](https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995) uutils coreutils, Debian might follow the same path, and Microsoft offers it as well. + # Projects -
- -

coreutils

-

- The commands you use everyday: ls, cp, etc. -

-
- -

findutils

-

- Finding what you need: find, locate, updatedb & xargs. -

-
- -

diffutils

-

- Comparing text and files: diff, cmp, diff3, sdiff. -

-
+ # Crates @@ -41,14 +81,24 @@ We maintain a variety of public crates to support our projects, which are published on [crates.io](https://crates.io/). - [`ansi-width`](https://github.com/uutils/ansi-width) +- [`num-prime`](https://github.com/uutils/num-prime) - [`parse_datetime`](https://github.com/uutils/parse_datetime) - [`platform-info`](https://github.com/uutils/platform-info) - [`uutils-term-grid`](https://github.com/uutils/uutils-term-grid) +# Who we are + +uutils is a community-driven, open-source effort maintained by volunteers around the world. There is no company behind it, just contributors who care about the future of foundational command-line tools. + +Everything happens in the open on [GitHub](https://github.com/uutils), and newcomers are genuinely welcome. Many of our contributors landed their first-ever open-source patch on a uutils project, and we are happy to help you do the same. + +[Meet the team](/team) behind uutils. + # Contributing You can help us out by: +- [Tackling a good first issue](https://github.com/search?q=org%3Auutils+is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22&type=issues&s=created&o=desc) across any of our projects (the easiest way to get started) - Contributing code - Contributing documentation - Reporting bugs (e.g. incompatibilities with GNU utilities) @@ -57,6 +107,10 @@ You can help us out by: You can join our [Discord server](https://discord.gg/wQVJbvJ) to discuss or ask anything concerning uutils. We're happy to help you get started with contributing! +# Talks & Media + +We did a few talks about this project, see [our talks & media page](/talks). + # Friends of uutils We collaborate with and build upon many other projects in the Rust diff --git a/content/acl.md b/content/acl.md new file mode 100644 index 000000000..36a7d3151 --- /dev/null +++ b/content/acl.md @@ -0,0 +1,29 @@ ++++ + +title = "acl" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils acl is a work-in-progress Rust reimplementation of the acl utilities, which are used to manage POSIX Access Control Lists. + +This project aims to provide memory-safe replacements for tools such as `getfacl` and `setfacl`. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from the original acl tools. + +# Goals + +Implement a memory-safe set of ACL utilities compatible with the original command-line interfaces. + +# Contributing + +To contribute to uutils acl, please see [CONTRIBUTING](https://github.com/uutils/acl/blob/main/CONTRIBUTING.md). + +# License + +uutils acl is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/acl/blob/main/LICENSE) file for details. + +The acl package is licensed under the GPL 2.0 or later. diff --git a/content/awk.md b/content/awk.md new file mode 100644 index 000000000..812afd253 --- /dev/null +++ b/content/awk.md @@ -0,0 +1,25 @@ ++++ + +title = "awk" +template = "project.html" + +[extra] +wip = true + ++++ + +Rust implementation of `awk`, the pattern scanning and text-processing language. + +This project aims to be a drop-in replacement of the original command. + +# Goals + +This project aims to be a drop-in replacement for GNU awk (gawk). Differences with the original are treated as bugs. + +# Contributing + +To contribute to uutils awk, please see [CONTRIBUTING](https://github.com/uutils/coreutils/blob/main/CONTRIBUTING.md). + +# License + +uutils awk is licensed under the MIT License or the Apache License 2.0, at your option - see the [LICENSE-MIT](https://github.com/uutils/awk/blob/main/LICENSE-MIT), [LICENSE-APACHE](https://github.com/uutils/awk/blob/main/LICENSE-APACHE) file for details. diff --git a/content/blog/2024-02-extending.md b/content/blog/2024-02-extending.md deleted file mode 100644 index 74b05dab8..000000000 --- a/content/blog/2024-02-extending.md +++ /dev/null @@ -1,75 +0,0 @@ -+++ -title = "Extending the Coreutils project - Rewriting base tools in Rust " -draft = true -date = 2024-03-26 -authors = ["Sylvestre Ledru", "Terts Diepraam"] -+++ - -Over the last 4 years, we have been working at reimplementing some of the key Linux tools in Rust. We started with the [Coreutils](https://github.com/uutils/coreutils) and [findutils](https://github.com/uutils/findutils). - -As we approach feature parity with the GNU Coreutils implementation, and as its adoption in production environments continues to expand, we have been thinking about what is next. - -Given the overwhelming positive feedback around this initiative, we are going to extend our efforts to rewrite other parts of the modern Linux/Unix/Mac stack in Rust (still with Windows support in mind when relevant). -We also noticed a lot of contributions on these projects coming from a lot of different hackers (475 different contributors on Coreutils!). -With the growing enthusiasm for Rust and the eagerness to learn it, now is the best time to push this project. We think that rewriting in Rust will help with the longer term maintenance of the ecosystem, ensuring it stays robust, safe and welcoming for new generations of contributors. - -For the next projects, we are using the same approach: dropped-in replacement of the GNU C implementation. -For now, we are going to focus on: -* [diffutils](https://github.com/uutils/diffutils) (transferred by Michael Howell) - [Good first issues](https://github.com/uutils/diffutils/labels/good%20first%20issue) - Almost ready -* [procps](https://github.com/uutils/procps) - [Good first issues](https://github.com/uutils/procps/labels/good%20first%20issue) - 5 programs started -* [acl](https://github.com/uutils/acl) - [Good first issues](https://github.com/uutils/acl/labels/good%20first%20issue) - The 3 started -* [util-linux](https://github.com/uutils/util-linux) - [Good first issues](https://github.com/uutils/util-linux/labels/good%20first%20issue) - A couple programs started -* [bsdutils](https://github.com/uutils/bsdutils) - [Good first issues](https://github.com/uutils/bsdutils/labels/good%20first%20issue) - One program started -* [login](https://github.com/uutils/login/) - [Good first issues](https://github.com/uutils/login/labels/good%20first%20issue) - Just the skeleton -* [hostname](https://github.com/uutils/hostname/) - [Good first issues](https://github.com/uutils/hostname/labels/good%20first%20issue) - Almost empty - - -These packages are part of the essential list on Debian & Ubuntu. - - -Sylvestre Ledru, Terts Diepraam and Daniel Hofstetter - -FAQ -### - -How long is it going to take? ------------------------------ - -Some programs like diff, acl or hostname should be completed quickly. -Others will take years to be completed. - -Do you hate the GNU project? ----------------------------- - -Not at all: we often contribute to the upstream implementations when we identify issues, add missing tests, etc. - - -Why the MIT License? --------------------- - -For consistency purposes and not interested by a license debate, we are going to use the MIT license just like the Coreutils. - - -The binaries are too big? -------------------------- - -Does it really matter? Yes, Rust binaries are usually bigger than GNU but we don't think it is a blocker, even for embedded devices. - - -Is it really safer? -------------------- - -While Rust is clearly a better language than C/C++ for security, these programs are often very safe. It isn't a key argument for us for -this rewrite. - -What about performances? ------------------------- - -Although we haven't entered the optimization phase, Rust's modern design inherently facilitates some straightforward performance improvements. diff --git a/content/blog/2025-02-extending.md b/content/blog/2025-02-extending.md new file mode 100644 index 000000000..b1a66784a --- /dev/null +++ b/content/blog/2025-02-extending.md @@ -0,0 +1,90 @@ ++++ +title = "Extending the Coreutils project - Rewriting base tools in Rust" +date = 2025-02-01 +page_template = "post.html" +authors = ["Sylvestre Ledru"] ++++ + +As I [just presented at FOSDEM](https://sylvestre.ledru.info/coreutils-fosdem-2025/), we are moving into the next phase of the uutils project. + +Over the last five years, we have been working on reimplementing some of the key Linux tools in Rust. We started with the [Coreutils](https://github.com/uutils/coreutils) and [findutils](https://github.com/uutils/findutils). + +As we approach feature parity with the GNU Coreutils implementation, and as its adoption in production environments continues to expand, we have been thinking about what is next. + +Given the very positive feedback around this initiative, we are going to extend our efforts to rewrite other parts of the modern Linux/Unix/Mac stack in Rust (still with Windows support in mind when relevant). These efforts will be managed under the [uutils umbrella](https://github.com/uutils/), which will allow us to maintain a cohesive and unified approach across various utilities. + +We also noticed a lot of contributions on these projects coming from a diverse group of hackers (530 different contributors on Coreutils alone!). With the growing enthusiasm for Rust and the eagerness to learn it, now is the best time to push this project forward. Rewriting in Rust will help with the long-term maintenance of the ecosystem, ensuring it stays robust, safe, and welcoming for new generations of contributors. + +### Vision for the Future + +As we expand the scope of the project, we are working on a future where Rust becomes the backbone of essential Linux, Unix, and potentially macOS system tools. Our focus will be on improving security, ensuring long-term maintainability, and optimizing performance for both modern and legacy systems. Through the uutils umbrella, we aim to create a more secure, efficient, and scalable alternative to the traditional tools, replacing software developed in older, less secure programming languages like C. + +This project is also an investment for the future. C is becoming less popular with the next generation of developers. Just like how the Linux kernel continues to evolve and includes Rust, it is our responsibility to upgrade the core utilities of the system to a modern, safer programming language. By doing so, we ensure that the essential tools used in Linux/Unix systems remain relevant, maintainable, and accessible to future contributors. + +### Specific Benefits of Rust + +Rust offers several advantages over C/C++, particularly in terms of memory safety, concurrency, and performance. By eliminating common security issues such as null pointer dereferences and buffer overflows, Rust allows us to build tools that are inherently safer. Its modern compiler tooling also enables us to optimize performance without sacrificing code safety, ensuring that our utilities can handle the demands of modern computing environments while remaining easy to maintain. + +### Challenges and Opportunities + +As with any large-scale reimplementation, there are challenges to overcome. Porting complex utilities from C to Rust requires careful consideration of edge cases and platform-specific behaviors. Additionally, ensuring full compatibility across Linux, Unix, and macOS environments presents unique challenges. However, these challenges also offer opportunities for growth and innovation, allowing us to extend and refine core system utilities for modern needs. Rust's modern design gives us the chance to improve both security and performance while maintaining cross-platform compatibility. + +### Next Steps + +For the next phase of the project, we are adopting the same approach: a drop-in replacement of the C implementations. Here's what's coming next: + +| Name | Description | Status | +|----------------------------------------------------|----------------------------------------------------|-----------------------------| +| [acl](https://github.com/uutils/acl) | Access control list utilities | | +| [bsdutils](https://github.com/uutils/bsdutils) | Basic utilities for BSD compatibility | | +| [coreutils](https://github.com/uutils/coreutils) | Basic utilities for the system | Production level | +| [diffutils](https://github.com/uutils/diffutils) | File comparison utilities | Almost ready | +| [findutils](https://github.com/uutils/findutils) | Utilities for finding files | Getting close to completion | +| [hostname](https://github.com/uutils/hostname) | Utility to show or set system hostname | | +| [login](https://github.com/uutils/login) | User login management utilities | | +| [procps](https://github.com/uutils/procps) | Utilities for monitoring and controlling processes | | +| [util-linux](https://github.com/uutils/util-linux) | Utilities essential for Linux systems | | + +These packages are part of the essential list for Debian and Ubuntu, and we're excited to push their Rust reimplementation further. + +### GSoC 2024 Participation + +In 2024, we had the pleasure of mentoring three students during Google Summer of Code (GSoC): + +1. **Sreehari Prasad** worked on improving the support of Rust CCoreutils. His focus was on making uutils compatible with the GNU coreutils test suite. Sreehari resolved most of the failing tests for the `cp`, `mv`, and `ls` utilities and significantly enhanced compatibility. + +2. **Hanbings** tackled the implementation of key GNU `findutils` utilities like `xargs` and `find` in Rust. His work focused on improving compatibility with the GNU suite while enhancing performance, resulting in major progress on missing features and test code. + +3. **Krysztal Huang** worked on implementing the `procps` suite, which includes utilities like `slabtop`, `top`, `pgrep`, `pidof`, `ps`, `pidwait`, and `snice`. This project involved implementing some of these commands. + +### Call to Action for Contributors + +Contributors who are passionate about system-level programming and Rust are always welcome. Whether you're experienced with GNU utilities or just learning Rust, your contributions will be invaluable to this +project. You can get involved by picking up good-first issues, reviewing code, or even helping us test new features across various platforms. The [uutils GitHub organization](https://github.com/uutils) has all the information you need to get started. +You can also sponsor the project through [GitHub Sponsors](https://github.com/sponsors/uutils). + +### FAQ + +**How long is it going to take?** + +Some programs, like `diff`, `acl`, or `hostname`, could be completed quickly, while others will take years to finish. + +**Do you hate the GNU project?** + +Not at all. We often contribute to the upstream implementations when we identify issues, add missing tests, and so on. + +**Why the MIT License?** + +For consistency purposes. We're not interested in a license debate and will continue to use the MIT license, as we did with Coreutils. + +**The binaries are too big. Does it really matter?** + +Yes, Rust binaries are generally bigger than GNU's, but we don't think it's a blocker, even for embedded devices. + +**Is it really safer?** + +While Rust is better than C/C++ for security, these programs are often already very safe. Security is not the key argument for us in this rewrite even if Rust provides some automatic improvements. + +**What about performance?** + +Although we haven't started optimizing yet, Rust's design facilitates performance improvements naturally. We are confident that, in time, these tools will match or exceed the performance of their GNU counterparts. diff --git a/content/blog/2025-04-playground.md b/content/blog/2025-04-playground.md new file mode 100644 index 000000000..96d880a89 --- /dev/null +++ b/content/blog/2025-04-playground.md @@ -0,0 +1,22 @@ ++++ +title = "Try Uutils Coreutils directly in your browser" +date = 2025-04-04 +page_template = "post.html" +authors = ["Sylvestre Ledru"] ++++ + +We are happy to announce the [Uutils playground](/playground) - an interactive terminal that lets you run real Rust Uutils Coreutils directly in your browser, with no installation, no server, and no network round-trips after the initial page load. + +Just open the page and start typing commands like `ls`, `sort`, `cat`, `head`, `factor`, or any of the 60+ utilities available. You can pipe commands together (`sort fruits.txt | uniq -c | sort -rn`), use redirections (`echo hello > file.txt`, `cat < file.txt`), explore the virtual filesystem, and even switch between 30+ languages using the locale dropdown. + +### How it works + +The Uutils Coreutils are compiled to WebAssembly (targeting `wasm32-wasip1`) as a single multicall binary - similar to BusyBox. When you type a command, a minimal JavaScript shell parses your input, sets up pipes between stages, and executes each one by instantiating the WASM binary with the right arguments. A virtual in-memory filesystem backed by [browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim) provides file I/O. Everything runs client-side - once loaded, the playground works entirely offline. + +An important distinction worth noting: **coreutils only provides individual commands** (`sort`, `cat`, `ls`, etc.). Features like `if`/`then`/`else`, `while` loops, variable expansion (`$VAR`), and globbing (`*.txt`) are shell features provided by Bash, Zsh, or similar - not by coreutils. The playground's JavaScript shell is intentionally minimal, keeping the focus on the coreutils themselves. + +For a deeper look at the architecture - including diagrams, the multicall dispatch mechanism, the `feat_wasm` feature gate, and how localization is handled - check out the [full technical deep-dive](/playground-how-it-works). + +### Try it, break it, improve it + +Give the [playground](/playground) a spin and let us know what you think! Contributions are welcome - whether it is improving WASI compatibility to enable more commands, adding translations via [Weblate](https://hosted.weblate.org/projects/rust-coreutils/), or enhancing the playground shell itself. diff --git a/content/blog/_index.md b/content/blog/_index.md index f784a32f4..e16b5390b 100644 --- a/content/blog/_index.md +++ b/content/blog/_index.md @@ -5,4 +5,4 @@ page_template = "post.html" title = "Blog" +++ -You can subscribe to this blog with this [atom feed](/atom.xml). \ No newline at end of file +You can subscribe to this blog with this [atom feed](/atom.xml). diff --git a/content/bsdutils.md b/content/bsdutils.md new file mode 100644 index 000000000..1a2182b33 --- /dev/null +++ b/content/bsdutils.md @@ -0,0 +1,25 @@ ++++ + +title = "bsdutils" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils bsdutils is a work-in-progress Rust reimplementation of the BSD utilities, a collection of standard Unix tools originating from BSD systems. + +> **Note:** This project is still in early development. Many features are not yet implemented. + +# Goals + +Implement a cross-platform, memory-safe set of BSD utilities compatible with their original command-line interfaces. + +# Contributing + +To contribute to uutils bsdutils, please see [CONTRIBUTING](https://github.com/uutils/bsdutils/blob/main/CONTRIBUTING.md). + +# License + +uutils bsdutils is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/bsdutils/blob/main/LICENSE) file for details. diff --git a/content/coreutils.md b/content/coreutils.md index 4d38ef14c..72480cb0c 100644 --- a/content/coreutils.md +++ b/content/coreutils.md @@ -3,10 +3,15 @@ title = "coreutils" template = "project.html" +[extra] +status = "ready" + +++ uutils coreutils is a cross-platform reimplementation of the GNU coreutils in Rust. While all programs have been implemented, some options might be missing or different behavior might be experienced. +[![GNU test results](https://raw.githubusercontent.com/uutils/coreutils-tracking/refs/heads/main/gnu-results.svg)](https://github.com/uutils/coreutils-tracking) + # Goals This project aims to be a drop-in replacement for the GNU utils. Differences with GNU are treated as bugs. diff --git a/content/diffutils.md b/content/diffutils.md index fbcc854d2..58d1434bc 100644 --- a/content/diffutils.md +++ b/content/diffutils.md @@ -3,14 +3,25 @@ title = "diffutils" template = "project.html" +[extra] +status = "beta" + +++ Rust implementation of GNU diffutils: `diff`, `cmp`, `diff3` and `sdiff`. This project aims to be a drop-in replacement of the original commands. +# Goals + +This project aims to be a drop-in replacement for the GNU diffutils. Differences with GNU are treated as bugs. + +# Contributing + +To contribute to uutils diffutils, please see [CONTRIBUTING](https://github.com/uutils/diffutils/blob/main/CONTRIBUTING.md). + # License -uutils diffutils is licensed under the MIT or Apache License - see the [LICENSE-MIT](https://github.com/uutils/diffutils/blob/main/LICENSE-MIT) and [LICENSE-APACHE](https://github.com/uutils/diffutils/blob/main/LICENSE-MIT) file for details. +uutils diffutils is licensed under the MIT or Apache License - see the [LICENSE-MIT](https://github.com/uutils/diffutils/blob/main/LICENSE-MIT) and [LICENSE-APACHE](https://github.com/uutils/diffutils/blob/main/LICENSE-APACHE) file for details. GNU diffutils is licensed under the GPL 3.0 or later. diff --git a/content/findutils.md b/content/findutils.md index 28a0796c2..0760aff58 100644 --- a/content/findutils.md +++ b/content/findutils.md @@ -3,12 +3,21 @@ title = "findutils" template = "project.html" +[extra] +status = "beta" + +++ Rust implementation of GNU findutils: `xargs`, `find`, `locate` and `updatedb`. This project aims to be a drop-in replacement of the original commands. +# Goals + +This project aims to be a drop-in replacement for the GNU findutils. Differences with GNU are treated as bugs. + +[![BFS test results](https://raw.githubusercontent.com/uutils/findutils-tracking/refs/heads/main/bfs-results.svg)](https://github.com/uutils/findutils-tracking) + # Contributing To contribute to uutils findutils, please see [CONTRIBUTING](https://github.com/uutils/findutils/blob/main/CONTRIBUTING.md). diff --git a/content/grep.md b/content/grep.md new file mode 100644 index 000000000..e3fe37f04 --- /dev/null +++ b/content/grep.md @@ -0,0 +1,29 @@ ++++ + +title = "grep" +template = "project.html" + +[extra] +status = "beta" + ++++ + +Rust implementation of GNU grep: `grep`, `egrep` and `fgrep`. + +This project aims to be a drop-in replacement of the original commands. + +[![GNU test results](https://raw.githubusercontent.com/uutils/grep-tracking/refs/heads/main/gnu-results.svg)](https://github.com/uutils/grep-tracking) + +# Goals + +This project aims to be a drop-in replacement for GNU grep. Differences with GNU are treated as bugs. + +# Contributing + +To contribute to uutils grep, please see [CONTRIBUTING](https://github.com/uutils/grep/blob/main/CONTRIBUTING.md). + +# License + +uutils grep is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/grep/blob/main/LICENSE) file for details. + +GNU grep is licensed under the GPL 3.0 or later. diff --git a/content/gsoc.md b/content/gsoc.md index de386e3d5..b6f26ff62 100644 --- a/content/gsoc.md +++ b/content/gsoc.md @@ -2,12 +2,18 @@ title = "Uutils at GSOC" +++ -Google summer of code is: - -> Google Summer of Code is a global, online program focused on bringing -> new contributors into open source software development. GSoC -> Contributors work with an open source organization on a 12+ week -> programming project under the guidance of mentors. +
+
+ uutils gsoc --help + + + +
+
+
$ uutils gsoc --help
+

Google Summer of Code is a global, online program focused on bringing new contributors into open source software development. GSoC contributors work with an open source organization on a 12+ week programming project under the guidance of mentors.

+
+
If you want to know more about how it works, check out the links below. @@ -18,7 +24,7 @@ If you want to know more about how it works, check out the links below. # What is it about? -The [uutils project](https://github.com/uutils/) is aiming at rewriting key Linux utilities in Rust, targeting [coreutils](https://github.com/uutils/coreutils), [findutils](https://github.com/uutils/findutils), [diffutils](https://github.com/uutils/diffutils), [procps](https://github.com/uutils/procps), [util-linux](https://github.com/uutils/util-linux), and [bsdutils](https://github.com/uutils/bsdutils). Their goal is to create fully compatible, high-performance drop-in replacements, ensuring reliability through upstream test suites. Significant progress has been made with coreutils, diffutils, and findutils, while the other utilities are in the early stages of development. +The [uutils project](https://github.com/uutils/) is aiming at rewriting key Linux utilities in Rust, targeting [coreutils](https://github.com/uutils/coreutils), [findutils](https://github.com/uutils/findutils), [diffutils](https://github.com/uutils/diffutils), [sed](https://github.com/uutils/sed), [grep](https://github.com/uutils/grep), [awk](https://github.com/uutils/awk), [procps](https://github.com/uutils/procps), [util-linux](https://github.com/uutils/util-linux), and [bsdutils](https://github.com/uutils/bsdutils). Their goal is to create fully compatible, high-performance drop-in replacements, ensuring reliability through upstream test suites. coreutils is already production-ready and shipping in distributions; findutils, diffutils and grep are well advanced; while sed, awk and the system utilities (procps, util-linux, bsdutils) are at earlier stages of development. # How to get started @@ -31,8 +37,8 @@ with uutils. coreutils and the other tools. 1. **Reach out to us!** We are happy to discuss potential projects and help you find a meaningful project for uutils. Tell us what interests you about the project and what experience you have and we can find a suitable project together. You can talk to the uutils maintainers on the [Discord server](https://discord.gg/wQVJbvJ). In particular, you can contact: * Sylvestre Ledru (@sylvestre on GitHub and Discord) - * Terts Diepraam (@tertsdiepraam on GitHub and @terts on Discord) -2. **Get comfortable with uutils.** To find a good project you need to understand the codebase. We recommend that you take a look at the code, the issue tracker and maybe try to tackle some [good-first-issues](https://github.com/uutils/coreutils/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22). Also take a look at our [contributor guidelines](https://github.com/uutils/coreutils/blob/main/CONTRIBUTING.md). + +2. **Get comfortable with uutils.** To find a good project you need to understand the codebase. We recommend that you take a look at the code, the issue tracker and maybe try to tackle some [good-first-issues](https://github.com/search?q=org%3Auutils+is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22&type=issues&s=created&o=desc) across any of our projects. Also take a look at our [contributor guidelines](https://github.com/uutils/coreutils/blob/main/CONTRIBUTING.md). 3. **Find a project and a mentor.** We have a [list of potential projects](https://github.com/uutils/coreutils/wiki/GSOC-Project-Ideas) you can adapt or use as inspiration. Make sure discuss your ideas with the maintainers! Some project ideas below have suggested mentors you could contact. 4. **Write the application.** You can do this with your mentor. The application has to go through Google, so make sure to follow all the advice in Google's [Contributor Guide](https://google.github.io/gsocguides/student/writing-a-proposal). Please make sure you include your prior contributions to uutils in your application. @@ -44,278 +50,273 @@ with uutils. # Project Ideas -This page contains project ideas for the Google Summer of Code for -uutils. Feel free to suggest project ideas of your own. - -[Guidelines for the project list](https://google.github.io/gsocguides/mentor/defining-a-project-ideas-list) - -Summarizing that page, each project should include: -- Title -- Description -- Expected outputs -- Skills required/preferred -- Possible mentors -- Size (either ~175 or ~350 hours) -- Difficulty (easy, medium or hard) - -## Complete the `ls` GNU compatibility - -Most of the features in `ls` have been implemented by now. -However, a bunch of work remains on the color side for a full GNU compat. Other tests are failing. -We have 12 remaining failing tests. - -To get the list of failing tests, run: -``` -$ ./util/remaining-gnu-error.py |grep "/ls/" -``` - -- Difficulty: Medium -- Size: 175 -- Mentors: Sylvestre -- Required skills: - - Rust - - Basic knowledge about the terminal - -## Complete the `cp` GNU compatibility - -Most of the features in `cp` have been implemented by now. -However, some corner cases needs to be implemented. We have 16 remaining failing tests. - -To get the list of failing tests, run: -``` -$ ./util/remaining-gnu-error.py |grep "/cp/" -``` - -- Difficulty: Medium -- Size: 175 -- Mentors: Sylvestre -- Required skills: - - Rust - - Basic knowledge about the terminal - -## Complete the `mv` GNU compatibility - -Most of the features in `mv` have been implemented by now. -However, some corner cases needs to be implemented. We have 10 remaining failing tests. - -To get the list of failing tests, run: -``` -$ ./util/remaining-gnu-error.py |grep "/mv/" -``` - -- Difficulty: Medium -- Size: 175 -- Mentors: Sylvestre -- Required skills: - - Rust - - Basic knowledge about the terminal - -## Improve `stty` -The `stty` utility is currently only partially implemented and should be expanded. - -See issues: [#3859](https://github.com/uutils/coreutils/issues/3859), [#3860](https://github.com/uutils/coreutils/issues/3860), [#3861](https://github.com/uutils/coreutils/issues/3861), [#3862](https://github.com/uutils/coreutils/issues/3862), [#3863](https://github.com/uutils/coreutils/issues/3863). - -- Difficulty: Medium -- Size: 175 -- Mentors: Terts Diepraam -- Required skills: - - Rust - - Basic knowledge about the terminal - -## Improve findutils coverage - -[More than half](https://github.com/uutils/findutils-tracking/) of the findutils GNU & BFS are passing. The goal of this project is to improve the compatibility of uutils/findutils with regard to GNU's implementation. - -See [https://github.com/uutils/findutils](https://github.com/uutils/findutils) - - -To achieve this, we should invest in fuzzing findutils: -Just like we are [doing with some](https://github.com/uutils/coreutils/tree/main/fuzz/fuzz_targets) [Coreutils programs](https://github.com/uutils/coreutils/blob/main/.github/workflows/fuzzing.yml), we should: -* fuzz findutils -* do some differential testing with GNU's implementation (and potentially others) - -- Difficulty: Medium -- Size: 175 -- Mentors: Sylvestre -- Required skills: - - Rust - - Basic knowledge about the terminal usage - -## Localization -Support for localization for formatting, quoting & sorting in various utilities, like `date`, `ls` and `sort`. For this project, we need to figure out how to deal with locale data. The first option is to use the all-Rust `icu4x` library, which has a different format than what distributions usually provide. In this case a solution _could_ be to write a custom `localedef`-like command. The second option is to use a wrapper around the C `icu` library, which comes with the downside of being a C dependency. - -This is described in detail in [issue #3997](https://github.com/uutils/coreutils/issues/3997). - -And was also discussed in [#1919](https://github.com/uutils/coreutils/issues/1919#issuecomment-846471073), [#3584](https://github.com/uutils/coreutils/issues/3584). - -- Difficulty: Hard -- Size: TBD -- Mentors: TBD -- Required skills: - - Rust - -## `procps`: Development of Process Management and Information Tools in Rust - -This project focuses on creating Rust-based implementations of process management and information tools: `ps`, `pgrep`, `pidwait`, `pkill`, `skill`, and `snice`. The goal is to ensure full compatibility with all options and successful passing of GNU tests, maintaining the functionality and reliability of these essential tools. - -- **Description:** Develop Rust-based versions of key process management and information tools, ensuring compatibility with all options and GNU tests. -- **Expected Outputs:** Efficient, reliable tools with full option compatibility and passing GNU tests. -- **Skills Required/Preferred:** Proficiency in Rust, understanding of Linux process management, experience with GNU testing methodologies. -- **Possible Mentors:** [To be determined] -- **Size:** ~350 hours. -- **Difficulty:** Medium. - -## `procps`: Development of System Monitoring and Statistics Tools in Rust - -This project involves the Rust-based development of system monitoring and statistics tools: `top`, `vmstat`, `tload`, `w`, and `watch`. The objective is to achieve full compatibility with all options and to pass GNU tests, ensuring these tools provide accurate and reliable system insights. - -- **Description:**: Create Rust versions of system monitoring and statistics tools, with a focus on full option compatibility and passing GNU tests. -- **Expected Outputs:**: Robust tools for system monitoring and statistics, fully compatible with existing options and verified by GNU tests. -- **Skills Required/Preferred:**: Rust expertise, knowledge of system performance metrics, familiarity with GNU testing frameworks. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~350 hours. -- **Difficulty:**: Medium. - -## `procps`: Development of Memory and Resource Analysis Tools in Rust - -The aim of this project is to develop Rust-based versions of memory and resource analysis tools: `pmap` and `slabtop`. The project will focus on ensuring full compatibility with all options and passing GNU tests, providing in-depth and reliable analysis of memory usage and kernel resources. - -- **Description:**: Implement Rust versions of memory and resource analysis tools, with emphasis on option compatibility and passing GNU tests. -- **Expected Outputs:**: Advanced tools for memory and resource analysis, fully compatible with existing options and validated by GNU tests. -- **Skills Required/Preferred:**: Proficiency in Rust, deep understanding of memory management and kernel resources, experience with GNU testing methodologies. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~175 hours. -- **Difficulty:**: Medium. - -## `util-linux`: Reimplementation of essential system utilities in Rust - -The objective of this project is to reimplement essential system utilities from the util-linux package in Rust. This initiative will include the development of Rust-based versions of various utilities, such as `dmesg`, `lscpu`, `lsipc`, `lslocks`, `lsmem`, and `lsns`. The primary focus will be on ensuring that these Rust implementations provide full compatibility with existing options and pass GNU tests, delivering reliable and efficient system utilities for Linux users. - -- **Description:**: Reimplement essential system utilities, including `dmesg`, `lscpu`, `lsipc`, `lslocks`, `lsmem`, and `lsns`, using Rust while emphasizing compatibility with existing options and GNU test validation. -- **Expected Outputs:**: Rust-based system utilities that mirror the functionality of their counterparts, ensuring full compatibility and reliability, validated by GNU tests. -- **Skills Required/Preferred:**: Proficiency in Rust programming, knowledge of system utilities and their functionality, experience with GNU testing methodologies. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~175 hours. -- **Difficulty:**: Medium. - -## `util-linux`: Process and Resource Management: Reimplementation in Rust - -This project focuses on the reimplementations of crucial Linux utilities related to process and resource management in the Rust programming language. The target utilities include `runuser`, `sulogin`, `chrt`, `ionice`, `kill`, `renice`, `prlimit`, `taskset`, and `uclampset`. The primary goal is to create Rust-based versions of these utilities, ensuring compatibility with their original counterparts, and validating their functionality with GNU tests. - -- **Description:**: Reimplement key Linux utilities for process and resource management, such as `runuser`, `sulogin`, `chrt`, `ionice`, `kill`, `renice`, `prlimit`, `taskset`, and `uclampset`, in the Rust programming language. The emphasis is on maintaining compatibility with the original utilities and validating their functionality using GNU tests. -- **Expected Outputs:**: Rust-based versions of the specified utilities that seamlessly integrate into Linux systems, providing the same functionality and passing GNU tests for reliability. -- **Skills Required/Preferred:**: Proficiency in Rust programming, understanding of process and resource management on Linux, experience with GNU testing methodologies. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~175 hours. -- **Difficulty:**: Medium. - -## `util-linux`: User and Session Management: Reimplementation in Rust - -This project focuses on the reimplementations of essential Linux utilities related to user and session management in the Rust programming language. The target utilities include `su`, `agetty`, `ctrlaltdel`, `pivot_root`, `switch_root`, `last`, `lslogins`, `mesg`, `setsid`, and `setterm`. The primary goal is to create Rust-based versions of these utilities, ensuring compatibility with their original counterparts, and validating their functionality with GNU tests. - -- **Description:**: Reimplement essential Linux utilities for user and session management, such as `su`, `agetty`, `ctrlaltdel`, `pivot_root`, `switch_root`, `last`, `lslogins`, `mesg`, `setsid`, and `setterm`, in the Rust programming language. The emphasis is on maintaining compatibility with the original utilities and validating their functionality using GNU tests. -- **Expected Outputs:**: Rust-based versions of the specified utilities that seamlessly integrate into Linux systems, providing the same functionality and passing GNU tests for reliability. -- **Skills Required/Preferred:**: Proficiency in Rust programming, understanding of user and session management on Linux, experience with GNU testing methodologies. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~175 hours. -- **Difficulty:**: Medium. - -This project aims to modernize and enhance critical Linux utilities related to user and session management, ensuring they remain efficient, reliable, and fully compatible with existing systems. - -## Code refactoring for `procps`, `util-linux`, and `bsdutils` - -Refactoring the Rust-based versions of procps, util-linux, and bsdutils to reduce code duplication. - -- **Title:**: Code Optimization and Refactoring for procps, util-linux, and bsdutils in Rust -- **Description:**: This project involves optimizing and refactoring the Rust-based versions of procps, util-linux, and bsdutils. The focus will be on eliminating duplicated code across these utilities, particularly in areas like uudoc, the test framework, and support for single/multicall binaries. -- **Expected outputs:**: A streamlined codebase with reduced duplication, improved maintainability for procps, util-linux, and bsdutils. -- **Skills required/preferred:**: Proficiency in Rust programming, understanding of Linux utilities, experience with code optimization and refactoring. -- **Possible mentors:**: Sylvestre -- **Size:**: 175 hours -- **Difficulty:**: Medium - -## A multicall binary and core library for `findutils` - -`findutils` currently exists of a few unconnected binaries. It would be nice to have a multicall binary (like -`coreutils`) and a library of shared functions (like `uucore`). - -This also might require thinking about sharing code between coreutils and findutils. - -- **Difficulty**: Medium -- **Size**: 175 hours -- **Mentors**: TBD -- Required skills: - - Rust - -## Implementation of GNU Test Execution for `procps`, `util-linux`, `diffutils`, and `bsdutils` - -The project aims at integrating the GNU test suite execution using the Rust-based versions of `procps`, `util-linux`, `diffutils`, and `bsdutils`, ensuring compatibility, crucial for seamless drop-in replacement integration. We have been doing such operation successfully for the Coreutils using [GitHub Actions](https://github.com/uutils/coreutils/blob/main/.github/workflows/GnuTests.yml), a [build script](https://github.com/uutils/coreutils/blob/main/util/build-gnu.sh) and a [run script](https://github.com/uutils/coreutils/blob/main/util/run-gnu-test.sh). - -- **Description:**: Run the GNU test suite on the Rust-based versions of procps, util-linux, diffutils, and bsdutils -- **Expected Outputs:**: The GNU test suite execution for each utility, ensuring functionality meets expected standards -- **Skills Required/Preferred:**: GitHub action understanding, Proficiency in Rust, experience with GNU testing methodologies, familiarity with Linux system utilities, and understanding of software testing principles. -- **Possible Mentors:**: Sylvestre -- **Size:**: ~175 hours -- **Difficulty:**: Medium - -## Refactoring `factor` - -The uutils `factor` is currently significantly slower than -GNU `factor` and only supports numbers up to 2^64-1. See [issue 1559](https://github.com/uutils/coreutils/issues/1559) -and [issue 1456](https://github.com/uutils/coreutils/issues/1456) for more information. - -- Difficulty: Hard -- Size: 175 hours -- Mentors: TBD -- Required skills: - - Rust - - Optimization techniques - - Mathematics - -## Symbolic/Fuzz Testing and Formal Verification of Tool Grammars - -See [Using Lightweight Formal Methods to Validate a Key Value Storage Node In Amazon S3](https://www.amazon.science/publications/using-lightweight-formal-methods-to-validate-a-key-value-storage-node-in-amazon-s3). - -Most KLEE scaffolding was done for [KLEE 2021](https://project-oak.github.io/rust-verification-tools/2021/07/14/coreutils.html). - -Start with `wc`, formalize the command line grammar. Get it working under AFL++ and Klee. Add several proofs of resource use and correctness - especially proofs about operating system calls and memory/cache usage. Generalize to other tools. Try to unify the seeds for the fuzzer and KLEE so they can help each other find new paths. Use QEMU to test several operating systems and architectures. Automate detection of performance regressions - try to hunt for [accidentally quadratic](https://accidentallyquadratic.tumblr.com) behavior. - -Specific to `wc` - formalize the inner loop over a UTF-8 buffer into a finite state automata with counters that can generalize into SIMD width operations like [simdjson](https://simdjson.org). Further generalize into a monoid so K processors can combine results. - -- Difficulty: Mixed -- Size: Mixed -- Mentors: TBD - informally @chadbrewbaker -- Required skills: - - Rust - - KLEE - - Fuzzers like AFL++ - - Grammar testing frameworks like [LARK](https://github.com/ligurio/lark-grammars/tree/master/lark_grammars/grammars) - - /usr/bin/time -v (and similar tools for Widows/OSX). - - Alloy, TLA+, [P](https://github.com/p-org/P) - - System call tracing with [strace](https://jvns.ca/blog/2014/02/17/spying-on-ssh-with-strace/), [uftrace](https://github.com/namhyung/uftrace) etc. - - SMT solvers like [Z3](https://www.philipzucker.com/programming-and-interactive-proving-with-z3py/) and CVC5 for superoptimization and proofs of automata equivalence. - - [SOUPER](https://github.com/google/souper) and [CompilerExplorer](https://godbolt.org) - - Basic statistics on quantiles (histograms) for outlier detection. The math is simple as generalizing from one to k medians but the formal notation is [complex](https://aakinshin.net/posts/thdqe-hdi/). - - [MPI-IO](https://wgropp.cs.illinois.edu/courses/cs598-s16/lectures/lecture32.pdf), just enough to read a file into k parts and combine "wc" outputs to understand multicore scaling. - -## Development of advanced terminal session recording and replay tools in Rust - -This project involves creating Rust-based implementations of `/usr/bin/script`, `/usr/bin/scriptlive`, and `/usr/bin/scriptreplay`. The `/usr/bin/script` command will record terminal sessions, `/usr/bin/scriptlive` will offer real-time recording features, and `/usr/bin/scriptreplay` will be used to replay recorded sessions. - -The work will happen in https://github.com/uutils/bsdutils. - -- **Description:**: Develop Rust-based versions of `/usr/bin/script`, `/usr/bin/scriptlive`, and `/usr/bin/scriptreplay` for terminal session recording and replaying. -- **Expected Outputs:**: Robust and cross-platform terminal session recording and replay tools, with real-time features in `scriptlive`. -- **Skills Required/Preferred:**: Proficiency in Rust, understanding of terminal emulation, experience with cross-platform development. -- **Possible Mentors:**: [To be determined] -- **Size:**: ~175 hours -- **Difficulty:**: Medium - -## Official Redox support -We want to support the Redox operating system, but are not actively testing against it. Since the last round of fixes in [#2550](https://github.com/uutils/coreutils/pull/2550), many changes have probably been introduced that break Redox support. This project would involve setting up Redox in the CI and fixing any issues that arise and porting features over. - -- Difficulty: Medium -- Size: 175 hours -- Mentors: TBD -- Required skills: - - Rust +These are starting points for a Google Summer of Code project with uutils. Feel free to adapt one or propose your own, and see the [guidelines for the project list](https://google.github.io/gsocguides/mentor/defining-a-project-ideas-list). Each idea lists its difficulty, an estimated size (~175 or ~350 hours) and a suggested mentor where one is available. + +
+ +
+

Performance optimization for coreutils

+
+ Medium + ~175h + Mentor: TBD +
+

uutils/coreutils has strong GNU compatibility, but some utilities can still be made faster. Systematically profile, benchmark and optimize the hot paths so they match or beat GNU coreutils.

+
    +
  • Profile utilities with perf, flamegraph and criterion
  • +
  • Build a benchmark suite comparing against GNU coreutils
  • +
  • Optimize hot paths in cat, cut, sort, uniq, wc, etc.
  • +
  • Reduce allocations, improve buffering, use SIMD where it helps
  • +
+

Skills: Rust, performance profiling, systems/I/O optimization; SIMD a plus.

+
+ +
+

Expand differential fuzzing for coreutils

+
+ Medium + ~175h + Mentor: TBD +
+

coreutils has some fuzzing infrastructure, but many utilities lack coverage. Expand differential fuzzing that compares uutils against GNU to catch discrepancies automatically.

+
    +
  • Add fuzz targets for utilities that currently lack them
  • +
  • Build differential harnesses comparing uutils vs GNU output
  • +
  • Run campaigns with AFL++ and libFuzzer, wire them into CI
  • +
  • Triage and fix the bugs the fuzzers find
  • +
+

Skills: Rust, fuzzing tools (AFL++, libFuzzer, cargo-fuzz), differential testing.

+
+ +
+

Complete findutils GNU compatibility

+
+ Medium + ~175h + Mentor: Sylvestre +
+

uutils/findutils already passes more than half of the GNU findutils and BFS tests. Finish the remaining work to reach full compatibility and production readiness.

+
    +
  • Implement missing options and predicates for find
  • +
  • Fix edge cases in traversal and symlink handling
  • +
  • Complete xargs argument handling
  • +
  • Pass the remaining GNU tests; add differential fuzzing
  • +
+

Skills: Rust, filesystem operations, find/xargs usage; fuzzing a plus.

+
+ +
+

Complete diffutils GNU compatibility

+
+ Medium + ~175h + Mentor: TBD +
+

uutils/diffutils implements diff, diff3, cmp and sdiff. Complete the remaining features and edge cases so it passes the GNU test suite.

+
    +
  • Implement missing options and output formats for diff
  • +
  • Improve algorithm efficiency for large files
  • +
  • Complete diff3 three-way merges
  • +
  • Pass the GNU diffutils tests; add differential fuzzing
  • +
+

Skills: Rust, diff algorithms (Myers, Patience), text processing.

+
+ +
+

Complete the Rust implementation of sed

+
+ Medium + ~175h + Mentor: TBD +
+

uutils/sed has been started but needs significant work to fully match GNU sed and POSIX. Implement the missing commands and edge cases and make it pass the GNU test suite.

+
    +
  • Implement missing commands and addressing flags
  • +
  • Handle complex regex, backreferences and multi-line pattern space
  • +
  • Implement hold-space operations correctly
  • +
  • Pass the GNU sed tests; add differential fuzzing
  • +
+

Skills: Rust, regular expressions, sed scripting, text processing.

+
+ +
+

Rust implementation of grep

+
+ Hard + ~350h + Mentor: TBD +
+

Build a high-performance, feature-complete drop-in replacement for GNU grep - full command-line interface, output modes and edge-case behavior, with the performance Rust can provide.

+
    +
  • Implement the full POSIX/GNU grep CLI
  • +
  • Support BRE, ERE and PCRE patterns
  • +
  • Handle context lines, recursive search, binary and compressed files
  • +
  • Pass the GNU grep tests; benchmark against GNU grep
  • +
+

Skills: Rust, regex engines, performance optimization, I/O.

+
+ +
+

Rust implementation of awk

+
+ Hard + ~350h + Mentor: TBD +
+

Implement awk, a complete programming language for pattern scanning and text processing, targeting POSIX awk and GNU awk (gawk) extensions.

+
    +
  • Build the lexer, parser and pattern-action execution engine
  • +
  • Support built-in variables (NR, NF, FS, RS) and functions
  • +
  • Implement field splitting, arrays, user functions and control flow
  • +
  • Set up GNU test suite execution for validation
  • +
+

Skills: Rust, lexers/parsers/interpreters, regex; language implementation a plus.

+
+ +
+

Complete procps implementation

+
+ Medium + ~350h + Mentor: TBD +
+

uutils/procps reimplements the process and system monitoring tools. Complete the core utilities and reach production readiness with full GNU compatibility. Scope can be focused on one of the groups below.

+
    +
  • Process management & info: ps, pgrep, pidwait, pkill, skill, snice
  • +
  • System monitoring & statistics: top, vmstat, tload, w, watch
  • +
  • Memory & resource analysis: pmap, slabtop, free, uptime
  • +
  • Robust /proc parsing across kernels; run the GNU procps tests
  • +
+

Skills: Rust, Linux /proc filesystem, process management and monitoring.

+
+ +
+

Complete util-linux implementation

+
+ Medium + ~350h + Mentor: TBD +
+

uutils/util-linux reimplements essential system utilities. Complete the most commonly used tools and reach production-ready status with full compatibility. Scope can be focused on one of the groups below.

+
    +
  • Essential system utilities: dmesg, lscpu, lsipc, lslocks, lsmem, lsns, mount, umount
  • +
  • Process & resource management: chrt, ionice, kill, renice, prlimit, taskset, runuser
  • +
  • User & session management: su, agetty, last, lslogins, mesg, setsid, setterm
  • +
  • Run the GNU util-linux tests; man-page compatibility
  • +
+

Skills: Rust, Linux system calls and kernel interfaces, system administration.

+
+ +
+

Complete bsdutils implementation

+
+ Medium + ~175h + Mentor: TBD +
+

uutils/bsdutils reimplements BSD-origin utilities found on Linux. Complete the core tools with compatibility across BSD and GNU/Linux variants.

+
    +
  • Complete logger, column, hexdump, look and friends
  • +
  • Terminal session recording: script, scriptlive, scriptreplay
  • +
  • Handle cross-platform differences and portability
  • +
  • Set up test suites for both BSD and GNU variants
  • +
+

Skills: Rust, BSD and Linux environments, terminal emulation, cross-platform development.

+
+ +
+

Localization

+
+ Hard + Size: TBD + Mentor: TBD +
+

Support localization for formatting, quoting and sorting in utilities like date, ls and sort. The core question is how to deal with locale data: the all-Rust icu4x library (possibly with a custom localedef-like command) versus a wrapper around the C icu library.

+ +

Skills: Rust, Unicode/locale handling.

+
+ +
+

Code refactoring for procps, util-linux & bsdutils

+
+ Medium + ~175h + Mentor: Sylvestre +
+

Refactor the Rust versions of procps, util-linux and bsdutils to reduce duplication, particularly around uudoc, the test framework, and single/multicall binary support.

+
    +
  • Eliminate duplicated code across the three projects
  • +
  • Unify the documentation and test scaffolding
  • +
  • Improve maintainability and shared infrastructure
  • +
+

Skills: Rust, Linux utilities, code optimization and refactoring.

+
+ +
+

A multicall binary and core library for findutils

+
+ Medium + ~175h + Mentor: TBD +
+

findutils currently consists of a few unconnected binaries. Build a multicall binary (like coreutils) and a library of shared functions (like uucore).

+
    +
  • Design a unified multicall entry point
  • +
  • Extract shared functionality into a core library
  • +
  • Consider sharing code between coreutils and findutils
  • +
+

Skills: Rust, library and CLI design.

+
+ +
+

GNU test execution for procps, util-linux, diffutils & bsdutils

+
+ Medium + ~175h + Mentor: Sylvestre +
+

Integrate the upstream test suites against the Rust versions of procps, util-linux, diffutils and bsdutils - crucial for proving drop-in compatibility. We already do this for coreutils via GitHub Actions, a build script and a run script.

+
    +
  • Adapt the coreutils CI approach to each project
  • +
  • Wire the test suites into GitHub Actions
  • +
  • Track and report compatibility over time
  • +
+

Skills: GitHub Actions, Rust, GNU testing methodologies, Linux utilities.

+
+ +
+

Symbolic/fuzz testing and formal verification of tool grammars

+
+ Mixed + Size: Mixed + Mentor: TBD (informally @chadbrewbaker) +
+

Inspired by lightweight formal methods at AWS; most KLEE scaffolding was done for KLEE 2021. Start with wc, formalize its command-line grammar, run it under AFL++ and KLEE, then generalize.

+
    +
  • Add proofs of resource use and correctness, especially around syscalls and memory
  • +
  • Unify fuzzer and KLEE seeds so they help each other find paths
  • +
  • Formalize the wc UTF-8 inner loop into a SIMD-friendly automaton / monoid
  • +
  • Automate detection of accidentally quadratic behavior
  • +
+

Skills: Rust, KLEE, AFL++, SMT solvers (Z3, CVC5), TLA+/Alloy, grammar testing.

+
+ +
+

Official Redox support

+
+ Medium + ~175h + Mentor: TBD +
+

We want to support the Redox operating system but are not actively testing against it. Since the last round of fixes in #2550, regressions have likely crept in.

+
    +
  • Set up Redox in CI
  • +
  • Fix the issues that arise and port missing features
  • +
+

Skills: Rust, cross-platform/OS development.

+
+ +
diff --git a/content/hostname.md b/content/hostname.md new file mode 100644 index 000000000..7f201f3f2 --- /dev/null +++ b/content/hostname.md @@ -0,0 +1,27 @@ ++++ + +title = "hostname" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils hostname is a work-in-progress Rust reimplementation of the `hostname` utility, used to get or set the system's hostname. + +This project aims to be a drop-in replacement for the standard hostname command across Linux, macOS, Windows, and other platforms. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ across platforms. + +# Goals + +Implement a cross-platform, memory-safe hostname utility compatible with existing command-line interfaces. + +# Contributing + +To contribute to uutils hostname, please see [CONTRIBUTING](https://github.com/uutils/hostname/blob/main/CONTRIBUTING.md). + +# License + +uutils hostname is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/hostname/blob/main/LICENSE) file for details. diff --git a/content/login.md b/content/login.md new file mode 100644 index 000000000..502a0b68a --- /dev/null +++ b/content/login.md @@ -0,0 +1,27 @@ ++++ + +title = "login" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils login is a work-in-progress Rust reimplementation of the login utilities, which handle user authentication and session management on Unix-like systems. + +This project aims to provide a memory-safe replacement for tools such as `login`, `newgrp`, `su`, and related utilities. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from the original login utilities. + +# Goals + +Implement a memory-safe set of login utilities compatible with the original command-line interfaces. + +# Contributing + +To contribute to uutils login, please see [CONTRIBUTING](https://github.com/uutils/login/blob/main/CONTRIBUTING.md). + +# License + +uutils login is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/login/blob/main/LICENSE) file for details. diff --git a/content/playground-how-it-works.md b/content/playground-how-it-works.md new file mode 100644 index 000000000..0b8533b89 --- /dev/null +++ b/content/playground-how-it-works.md @@ -0,0 +1,290 @@ ++++ +title = "Playground: How It Works" +template = "page.html" ++++ + + + + +The [uutils playground](/playground) lets you run real Rust coreutils directly in your browser, with no server and no installation. This page explains the architecture behind it. + +## High-Level Overview + +
+flowchart LR
+    A["Browser"] -->|"1. Load page"| B["Zola static site"]
+    B -->|"2. Fetch core binary"| C["uutils.wasm
(coreutils multicall)"] + A -->|"3. User types command"| D["JavaScript shell"] + D -->|"4a. On-demand fetch
(grep, find, diff, sed…)"| F["Standalone
WASM modules"] + D -->|"4b. Execute via WASI"| E["WebAssembly runtime"] + F --> E + E -->|"5. Output"| A +
+ +Everything runs **client-side**. The page loads only the **coreutils multicall binary** up front; the optional standalone modules (`grep`, `find`/`locate`/`updatedb`, `diff`/`cmp`, `sed`) are fetched **on demand** the first time you use them — either by clicking their "Load" button or simply by running the command. Once a module is downloaded, it works entirely offline. + +## Architecture + +
+flowchart TB
+    subgraph Browser
+        direction TB
+        UI["xterm.js terminal emulator"]
+        Shell["JavaScript shell layer
(parseCommandLine, builtins)"] + WASI["WASI shim
(@bjorn3/browser_wasi_shim)"] + WASM["uutils coreutils
compiled to WebAssembly"] + VFS["Virtual filesystem
(in-memory)"] + + UI -->|"user input"| Shell + Shell -->|"argv + stdin"| WASI + WASI -->|"WASI syscalls"| WASM + WASM -->|"fd_write stdout/stderr"| WASI + WASI -->|"output text"| UI + WASM <-->|"fd_read / fd_write"| VFS + Shell <-->|"builtins: cd, help, clear, locale"| UI + end +
+ +### Components + +| Component | Role | +|-----------|------| +| **[xterm.js](https://xtermjs.org/)** | Terminal emulator rendered in the browser. Handles cursor, colors, input, scrollback. | +| **JavaScript shell** | Parses command lines, manages pipes, handles builtins (`help`, `clear`, `cd`, `locale`), and dispatches to WASM. | +| **[browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim)** | Implements the WASI (WebAssembly System Interface) in JavaScript so that uutils can perform I/O operations. | +| **uutils.wasm** | The Rust coreutils, compiled with the `feat_wasm` feature to a single multicall WASM binary containing 60+ commands. This is the only binary loaded eagerly at page load. | +| **Standalone modules** | Separate uutils projects shipped as their own WASM binaries, **loaded on demand**: `grep.wasm` ([uutils/grep](https://github.com/uutils/grep)), `find.wasm`/`locate.wasm`/`updatedb.wasm` ([uutils/findutils](https://github.com/uutils/findutils)), `diffutils.wasm` providing `diff`/`cmp` ([uutils/diffutils](https://github.com/uutils/diffutils)), and `sed.wasm` ([uutils/sed](https://github.com/uutils/sed)). | +| **Virtual filesystem** | An in-memory filesystem backed by WASI shim `PreopenDirectory`, pre-populated with sample files. Persists across commands within a session. | + +## Lifecycle of a Command + +Here's what happens when you type `sort fruits.txt | uniq -c | sort -rn` and press Enter: + +
+sequenceDiagram
+    participant User
+    participant Terminal as xterm.js
+    participant Shell as JS Shell
+    participant WASI as WASI Shim
+    participant WASM as uutils.wasm
+    participant VFS as Virtual FS
+
+    User->>Terminal: types "sort fruits.txt | uniq -c | sort -rn" + Enter
+    Terminal->>Shell: raw input string
+
+    Note over Shell: parseCommandLine() splits
into 3 pipeline stages + + Shell->>WASI: argv=["coreutils","sort","fruits.txt"], stdin="" + WASI->>WASM: instantiate + start() + WASM->>VFS: fd_read("fruits.txt") + VFS-->>WASM: file contents + WASM->>WASI: fd_write(stdout, sorted data) + WASI-->>Shell: stdout captured + + Shell->>WASI: argv=["coreutils","uniq","-c"], stdin=sorted data + WASI->>WASM: instantiate + start() + WASM->>WASI: fd_write(stdout, counted data) + WASI-->>Shell: stdout captured + + Shell->>WASI: argv=["coreutils","sort","-rn"], stdin=counted data + WASI->>WASM: instantiate + start() + WASM->>WASI: fd_write(stdout, final result) + WASI-->>Shell: stdout captured + + Shell->>Terminal: display final output + Terminal->>User: rendered result +
+ +Key details: + +- **Pipeline execution**: each pipe stage is a fresh WASM instantiation. The stdout of one stage becomes the stdin of the next. +- **Command dispatch**: coreutils commands go through `["coreutils", command, ...args]` - the core WASM binary is a multicall binary, similar to BusyBox. Standalone modules (`grep`, `find`, `diff`, `sed`…) are invoked **directly by their own name** as `argv[0]`, since each is its own binary rather than a multicall entry. +- **On-demand loading**: if a command lives in a standalone module that hasn't been fetched yet, the shell loads that module first (printing a `loading … done` notice), then runs the command. +- **Path resolution**: relative paths are resolved against a virtual `cwd` maintained by the JS shell. + +## WASM Loading & Initialization + +The playground splits its WASM into one eagerly-loaded core binary and several optional modules fetched lazily, keeping the initial page download small. + +
+flowchart TB
+    A["Page load"] --> B{"SharedArrayBuffer
available?"} + B -->|No| C["Install polyfill stub"] + B -->|Yes| D["Continue"] + C --> D + D --> E["Load xterm.js + fit addon
(from CDN with SRI)"] + E --> F["Render terminal + banner"] + F --> G["Parallel fetch"] + G --> H["browser_wasi_shim
(dynamic import)"] + G --> I["uutils.wasm
(compileStreaming)"] + H --> J["Ready"] + I --> J + J --> K{"?cmd= parameter?"} + K -->|Yes| L["Auto-run commands"] + K -->|No| M["Show prompt"] +
+ +- Only the **coreutils multicall binary** loads eagerly. The standalone modules (`grep`, `find`/`locate`/`updatedb`, `diffutils`, `sed`) are **not** part of this startup fetch. +- WASM binaries are compiled with `WebAssembly.compileStreaming()` for best performance, with a fallback to `arrayBuffer()` if the server doesn't set the `application/wasm` content-type. +- Commands are disabled until the core binary finishes loading. The terminal shows a loading message and a prompt appears once it's ready. +- The `SharedArrayBuffer` polyfill stub prevents `ReferenceError` in browsers without cross-origin isolation headers. + +### On-Demand Loading of Standalone Modules + +
+flowchart TB
+    A["User runs grep/find/diff/sed
(or clicks its Load button)"] --> B{"Module already
compiled?"} + B -->|Yes| F["Run command"] + B -->|No| C{"Fetch in flight?"} + C -->|Yes| D["Share the existing
in-flight fetch"] + C -->|No| E["fetch + compileStreaming
/wasm/<module>.wasm"] + D --> G["Dispatch
uutils:program-loaded"] + E --> G + G --> F +
+ +- Each module is fetched **once**, the first time it's needed. Concurrent callers share a single in-flight fetch, and the compiled module is cached for the rest of the session. +- A single module can back several commands: `diffutils.wasm` provides both `diff` and `cmp`, and the **"Load" button for `find`** brings in `find`, `locate` and `updatedb` together. +- On success the page dispatches a `uutils:program-loaded` event, which the "Load" buttons listen for so their label flips to `✓ loaded` — whether the module was loaded by the button or by running the command. +- If a module's binary isn't present (e.g. local dev without a CI build), the command reports that it's unavailable instead of breaking the terminal. + +## Command Parsing & Pipes + +The shell implements a simple but functional parser: + +
+flowchart LR
+    Input["sort fruits.txt | uniq -c"] --> Tokenizer
+    Tokenizer --> |"handles quotes, escapes,
pipes, redirections"| Pipeline + + subgraph Pipeline + direction LR + S1["Stage 1
['sort', 'fruits.txt']"] + S2["Stage 2
['uniq', '-c']"] + S1 --> |stdout → stdin| S2 + end +
+ +Supported shell features: +- **Pipes** (`|`) - chain commands together +- **Redirections** (`>`, `>>`, `<`) - write output to files, append, or read input from files +- **Single quotes** (`'...'`) - literal strings, no escaping +- **Double quotes** (`"..."`) - literal strings with backslash escaping +- **Backslash escaping** (`\|`, `\ `) - escape special characters +- **Tab completion** - commands and filenames +- **Keyboard shortcuts** - Ctrl+C (cancel), Ctrl+L (clear), Ctrl+U (clear line), arrows (history/cursor) + +**Not** supported (by design, to keep it simple): variables (`$VAR`), subshells, `&&`/`||`, globbing. + +### Shell vs. Coreutils: Who Does What? + +It's important to understand that **coreutils only provides individual commands** like `sort`, `cat`, `ls`, etc. Features like `if`/`then`/`else`, `while` loops, `for` loops, variable expansion (`$VAR`), and globbing (`*.txt`) are all **shell features** - they are provided by a shell such as Bash or Zsh, not by coreutils. + +Since the playground implements only a minimal shell (pipes, redirections, quoting, and a few builtins), these shell constructs are not available. This isn't a limitation of uutils itself - it's simply because the playground's JavaScript shell is intentionally lightweight and doesn't include a full shell language interpreter. + +## The Rust Side: Building Coreutils for WebAssembly + +### Compilation Target + +The uutils coreutils are compiled to **`wasm32-wasip1`** (WebAssembly System Interface Preview 1) using the standard Rust toolchain: + +```bash +cargo build --target wasm32-wasip1 --features feat_wasm +``` + +This produces a single `uutils.wasm` binary - a **multicall binary** similar to BusyBox, where all 60+ utilities are bundled into one executable. + +### The `feat_wasm` Feature Gate + +Not every coreutil can run in a WASM sandbox. The [`feat_wasm` feature](https://github.com/uutils/coreutils/blob/main/Cargo.toml) in `Cargo.toml` defines the curated set of utilities that are compatible with WASI: + +
+flowchart LR
+    subgraph "feat_wasm (included)"
+        direction LR
+        A["Text: cat, head, tail,
sort, uniq, cut, tr, wc, fmt"] + B["Files: cp, mv, rm, mkdir,
touch, link, ln, ls"] + C["Math: seq, factor, shuf,
numfmt, expr"] + D["Checksum: md5sum, sha*sum,
b2sum, cksum"] + E["Encoding: base32, base64,
basenc"] + F["Other: date, uname, arch,
nproc, sleep, echo, printf"] + end + + subgraph "Excluded from WASM" + direction LR + X["dd, df, du, env, mktemp,
more, tac, test, stty,
chcon, runcon, chown, kill..."] + end +
+ +Utilities are excluded when they depend on OS-level syscalls not available in WASI - for example, `df` needs filesystem stats, `du` needs directory traversal with metadata, and `chown`/`chcon` need permission and SELinux APIs. + +> **Note:** `grep`, `find`/`locate`/`updatedb`, `diff`/`cmp` and `sed` are **not** part of the coreutils `feat_wasm` set — they live in separate uutils projects ([grep](https://github.com/uutils/grep), [findutils](https://github.com/uutils/findutils), [diffutils](https://github.com/uutils/diffutils), [sed](https://github.com/uutils/sed)) and are compiled to their own WASM modules, loaded on demand as described above. (`xargs` is intentionally absent: it must spawn child processes, which the browser WASI sandbox can't do.) + +### Multicall Binary: How Command Dispatch Works + +
+flowchart TB
+    A["WASI runtime calls _start()"] --> B["main() in coreutils.rs"]
+    B --> C["Read argv[0] = 'coreutils'"]
+    C --> D["Read argv[1] = utility name
e.g. 'sort'"] + D --> E["Look up in util_map()"] + E --> F["Call sort::uumain(args)"] + F --> G["Return exit code"] +
+ +At **build time**, `build.rs` scans all enabled Cargo features and generates a `uutils_map.rs` file containing a [PHF](https://crates.io/crates/phf) (perfect hash function) map: + +```rust +// Auto-generated at build time +type UtilityMap = phf::OrderedMap< + &'static str, + (fn(T) -> i32, fn() -> Command) +>; +``` + +Each entry maps a utility name (e.g. `"sort"`) to a pair of functions: +- **`uumain`** - the utility's entry point, taking argument iterators and returning an exit code +- **`uu_app`** - returns the `clap::Command` definition for argument parsing and help + +At **runtime**, the multicall binary reads `argv` to determine which utility to invoke. In the browser, the JavaScript shell calls the WASM binary as `["coreutils", "sort", "-rn"]`, so `argv[1]` becomes the dispatch key. + +### WASI Platform Adaptations + +Individual utilities use conditional compilation to handle WASI's limitations: + +```rust +// In cp: symlinks are not supported on WASI +#[cfg(target_os = "wasi")] +return Err(Error::Unsupported); + +// In tail: no inotify/kqueue file watching +#[cfg(target_os = "wasi")] +fn follow() { /* no-op stub */ } + +// In ls: hostname crate excluded for WASI +#[cfg(not(target_os = "wasi"))] +use hostname::get; +``` + +These stubs mean the utilities gracefully degrade rather than crash - `tail -f` simply won't follow, `cp` won't create symlinks, and `ls` won't show hostname information. + +### Localization: Embedding All Translations + +A key difference for the WASM build is how locale files are handled: + +
+flowchart TB
+    subgraph "Normal build (Linux, macOS...)"
+        N1["build.rs detects user's LANG"]
+        N1 --> N2["Embed only matching .ftl file
+ English fallback"] + end + + subgraph "WASI build" + W1["build.rs detects target_os = wasi"] + W1 --> W2["Embed ALL .ftl locale files
(30+ languages)"] + W2 --> W3["Runtime locale switching
via LANG env variable"] + end +
+ +On native platforms, `uucore`'s build script embeds only the [Fluent](https://projectfluent.org/) (`.ftl`) translation files matching the user's `LANG` environment variable, to keep the binary small. For WASI builds, **all locale files are embedded**, because the target locale isn't known at compile time - the playground user can switch languages at runtime via the locale dropdown or the `locale` command. + diff --git a/content/playground.md b/content/playground.md new file mode 100644 index 000000000..cf71e40d8 --- /dev/null +++ b/content/playground.md @@ -0,0 +1,111 @@ ++++ +title = "Playground" +template = "page.html" ++++ + +

Try uutils coreutils directly in your browser! This interactive terminal runs Rust coreutils via WebAssembly - no installation needed.

+ +
+ + + Missing a translation or your language? Help translate on Weblate +
+ +
+ Extra programs: +
+ +
+
+ user@uutils: ~ + + + +
+
+
+ +
+ + Run a command, then copy a link that reruns it for anyone you share it with. +
+ +

+ + + + + + + + + +## Try it out + +Click an example to run it in the terminal: + +
+ + + + + + + + + + + + + + + + + + + + + +
+ +## Sharing commands via URL + +After running a command, click **🔗 Copy share link** below the terminal to copy a link that reruns it. You can also build one by hand with the `?cmd=` URL parameter. The command runs automatically when the page loads - great for sharing examples or linking from documentation. + +**Examples:** + +- [`?cmd=date`](/playground?cmd=date) - show the current date +- [`?cmd=seq 1 10 | factor`](/playground?cmd=seq%201%2010%20|%20factor) - factorize numbers 1–10 +- [`?cmd=echo 'Hello, world!' | sha256sum`](/playground?cmd=echo%20%27Hello%2C%20world!%27%20|%20sha256sum) - hash a string +- [`?cmd=sort fruits.txt | uniq -c | sort -rn`](/playground?cmd=sort%20fruits.txt%20|%20uniq%20-c%20|%20sort%20-rn) - count and rank fruit + +Multiple commands can be run in sequence, separated either by `;` on one line or by newlines (`%0A` in the URL): + +- [`?cmd=echo hello%0Aecho world`](/playground?cmd=echo%20hello%0Aecho%20world) - run two commands in sequence +- [`?cmd=updatedb; locate names`](/playground?cmd=updatedb%3B%20locate%20names) - build the locate database, then search it + +## Available commands + +The following commands run as **real Rust coreutils compiled to WebAssembly**: + +

Loading the list of available commands…

+ +The following are **shell builtins** implemented in JavaScript: + +- `help` - list available commands and examples +- `clear` - clear the terminal screen +- `cd` - change the current working directory +- `locale` - show or change the current locale + +Some commands (e.g. `chcon`, `runcon`, etc.) are not yet available in the WASM build because they +depend on platform-specific syscalls not fully supported by WebAssembly/WASI. + +We are actively working on adding more commands as we improve WASI compatibility in uutils coreutils. + +Curious about how this all works under the hood? Read the [technical deep-dive on the playground architecture](/playground-how-it-works). + +The source code for this website (including the playground) is available on [GitHub](https://github.com/uutils/uutils.github.io/). diff --git a/content/procps.md b/content/procps.md new file mode 100644 index 000000000..142ab0b38 --- /dev/null +++ b/content/procps.md @@ -0,0 +1,29 @@ ++++ + +title = "procps" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils procps is a work-in-progress Rust reimplementation of procps, the package that provides utilities for monitoring and interacting with processes. + +This project aims to provide memory-safe replacements for tools such as `ps`, `top`, `kill`, `free`, `vmstat`, `uptime`, and others found in procps. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from the original procps tools. + +# Goals + +Implement a cross-platform, memory-safe set of process monitoring utilities compatible with the procps command-line interfaces. + +# Contributing + +To contribute to uutils procps, please see [CONTRIBUTING](https://github.com/uutils/procps/blob/main/CONTRIBUTING.md). + +# License + +uutils procps is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/procps/blob/main/LICENSE) file for details. + +procps is licensed under the GPL 2.0 or later. diff --git a/content/reviews.md b/content/reviews.md new file mode 100644 index 000000000..943c5363e --- /dev/null +++ b/content/reviews.md @@ -0,0 +1,155 @@ ++++ +title = "Review Guidelines" +template = "page.html" ++++ + +This page describes what we expect from pull requests across the uutils +projects ([coreutils](https://github.com/uutils/coreutils), +[findutils](https://github.com/uutils/findutils) and the others) and how reviews +are carried out. It is meant for both **contributors** who want to know what a +reviewer looks for, and **reviewers** who want a shared checklist. Each +project's `CONTRIBUTING.md` links here; the rules below apply to all of them. + +## The one rule that cannot be bent + +> uutils is **original** code. We **cannot** accept any change based on the GNU +> source code, and you **must not even link to** the GNU source in an issue or +> PR. A reviewer will reject a contribution that appears to be derived from GNU +> (or any other strongly-licensed implementation such as GPL/LGPL code). + +It is fine to look at permissively-licensed implementations +([Apple's file_cmds](https://github.com/apple-oss-distributions/file_cmds/), +[OpenBSD](https://github.com/openbsd/src/tree/master/bin)) and to read the GNU +*manuals* and man pages - just never the GNU *source*. + +## What a reviewer expects before merging + +A pull request is ready to be merged when it meets all of the following. If you +check these before requesting a review, your PR will move much faster. + +- **It passes CI.** The test suite is green (allowing for intermittently + failing tests), `rustfmt` is satisfied, and there are no `clippy` warnings. +- **It compiles without warnings on every CI platform.** Use `#[cfg(...)]` for + platform-specific code rather than breaking other targets. +- **It is small and self-contained.** A series of small PRs gets merged far + faster than one large one. Unrelated changes belong in separate PRs. +- **It has a descriptive title.** Describe the problem solved, e.g. + `ls: fix version sort order`, not `Fix #1234`. Prefix with the utility name + when relevant. +- **New behavior comes with tests.** Our test suite is fast; regressions should + be caught by a test. Code coverage should not regress. +- **If possible, it was discussed first.** For anything non-trivial, open (or comment on) an + issue *before* writing the code, so effort isn't wasted on a change we can't + merge. +- **It follows GNU behavior** for options and output, verified against the GNU + manual or output - never the GNU source. +- **GNU compatibility does not regress.** The GNU test suite (`run-gnu-test.sh`, + tracked with `remaining-gnu-error.py`) should not go backwards; a + compatibility fix should ideally add the now-passing test. Error messages and + exit codes should match GNU, checked with `LANG=C` (except for locale bugs). +- If the GNU test suite passes but you still found a behavior difference, that is + a gap in their coverage: please contribute the missing test upstream to GNU to + improve their test suite. +- **It updates docs, help and translations.** A new option or behavior should + also update the `--help` text, the documentation, the generated man pages, and + at least the English `locales/en-US.ftl` strings. +- **It stays focused.** Keep formatting-only changes, unrelated refactors, and + dependency or lockfile bumps out of a feature or fix PR. +- **It is safe with untrusted input.** Utilities process arbitrary file contents + and arguments, so avoid unbounded allocations, integer overflow, and + path-traversal foot-guns. +- **It does not regress performance.** Runtime should not get more than **3%** + slower than the current `main`. Increased memory usage is acceptable when it is + justified (e.g. it buys a meaningful speed-up or is needed for correctness). +- **It does not regress binary size.** The compiled binary should not grow more + than **3%** compared to the current `main`, unless the increase is justified by + the change. +- **New dependencies must be discussed and justified.** Adding a crate is not + free - it affects build time, binary size, the audit surface, and licensing. + Raise it first, explain why it's needed and why an existing dependency or a + small amount of in-tree code won't do, and make sure its license is compatible + (see Licensing in each project's `CONTRIBUTING.md`). + +### Commit hygiene reviewers care about + +- Small, atomic commits with a clean history. +- Informative messages annotated with the component, e.g. + `cp: do not overwrite with -i` or `uucore: add support for FreeBSD`. +- Don't move code around unnecessarily - it makes diffs hard to review. If a + move is needed, do it in its own commit. + +### Coding expectations reviewers check against + +- **No `panic!`** - avoid `.unwrap()`, `panic!` and stray `println!`. A + justified `unreachable!` needs a comment. +- **No `exit`** - utilities must be embeddable, so avoid `std::process::exit` + and friends. +- **Minimal `unsafe`** - generally only for FFI, always with a `// SAFETY:` + comment. Performance is rarely a good enough reason. +- **`OsStr`/`Path` over `str`/`String`** for paths, since paths may not be valid + UTF-8. +- **Macros sparingly**, and **comments that explain *why***, kept up to date. +- **Don't silence clippy with `#[allow(...)]`.** In particular, we don't want to + see `#[allow(dead_code)]`; fix the underlying issue (or remove the unused code) + instead of suppressing the lint. + +## For contributors: getting your PR reviewed + +- You don't need to ping a maintainer the moment you open a PR. +- If you get no response within a few days, it's fine to request a review. +- If after a week there's still no review, ping the maintainers on + [Discord](https://discord.gg/wQVJbvJ) (`#coreutils-chat` for coreutils). +- You know your code best - please resolve merge conflicts on your branch + yourself (`git merge main` or `git rebase main`, your choice). Ask for help if + you get stuck. +- When you address review feedback, fold the fixes into the relevant commits + (`git commit --fixup` / `git absorb`) to keep history clean. + +## For reviewers: how we review + +- **Double-check a human's work.** A reviewer is there to verify a contributor's + reasoning, not to launder unreviewed machine output. Expect the author to be + able to explain and justify every line. +- **Watch for GNU/GPL provenance.** Be especially careful with AI-assisted + patches: assistants are trained on GPL sources and can reproduce them + verbatim, which we cannot accept. +- **Keep comments short and actionable.** Prefer simple, one-line comments on + the specific line, so the author knows exactly what to change. +- **Push back on** long-winded code, duplication, needless complexity, and + changes that arrive without tests. +- **Confirm the basics** from the checklist above (CI, scope, title, tests, + style) rather than re-deriving them each time. +- **Disregard machine-generated discussion.** Review comments and replies should + come from a person. Maintainers may hide or ignore comments that read as + AI-generated. + +## AI-assisted contributions + +AI-assisted contributions are allowed, but the same standards apply as for any +other patch. If you use an AI tool, **you** are responsible for the result: you +should understand every line, be able to justify it in review, and make sure the +output is not derived from GNU or other GPL code. Keep patches small and +self-review the diff carefully before opening the PR. Commit messages and PR +descriptions should describe the change, not the tooling used to produce it. + +AI is a tool for writing *code*. Issue reports, pull request descriptions, and +replies to reviewers should be written in **your own words**. The whole point of +review discussion is to confirm that a human understands the change; generated +prose defeats that. + +This is not about English fluency: if you are not a native speaker, using a tool +to fix grammar or translate your own words is fine. The point is that the ideas +and reasoning should be yours, not an AI's. + +This section is inspired by the +[astral-sh AI policy](https://github.com/astral-sh/.github/blob/main/AI_POLICY.md) +and Mozilla's +[AI coding guidance](https://firefox-source-docs.mozilla.org/contributing/ai-coding.html). + +## See also + +- The full `CONTRIBUTING.md` in each repository + ([coreutils](https://github.com/uutils/coreutils/blob/main/CONTRIBUTING.md), + [findutils](https://github.com/uutils/findutils/blob/main/CONTRIBUTING.md)) +- `DEVELOPMENT.md` for setting up your environment +- Our [Code of Conduct](https://github.com/uutils/coreutils/blob/main/CODE_OF_CONDUCT.md) diff --git a/content/sed.md b/content/sed.md new file mode 100644 index 000000000..9267e4cac --- /dev/null +++ b/content/sed.md @@ -0,0 +1,32 @@ ++++ + +title = "sed" +template = "project.html" + +[extra] +wip = true +status = "alpha" + ++++ + +uutils sed is a work-in-progress Rust reimplementation of GNU sed, the stream editor for filtering and transforming text. + +This project aims to be a drop-in replacement for GNU sed, supporting the full range of sed scripts and command-line options. + +[![GNU test results](https://raw.githubusercontent.com/uutils/sed-tracking/refs/heads/main/gnu-results.svg)](https://github.com/uutils/sed-tracking) + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from GNU sed. + +# Goals + +Implement a cross-platform, memory-safe sed that is compatible with GNU sed scripts and command-line interface. + +# Contributing + +To contribute to uutils sed, please see [CONTRIBUTING](https://github.com/uutils/sed/blob/main/CONTRIBUTING.md). + +# License + +uutils sed is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/sed/blob/main/LICENSE) file for details. + +GNU sed is licensed under the GPL 3.0 or later. diff --git a/content/shadow.md b/content/shadow.md new file mode 100644 index 000000000..1235bfc9b --- /dev/null +++ b/content/shadow.md @@ -0,0 +1,33 @@ ++++ + +title = "shadow" +template = "project.html" +aliases = ["/shadow-rs"] + +[extra] +wip = true + ++++ + +Memory-safe Rust reimplementation of Linux shadow-utils: `useradd`, `userdel`, `usermod`, `passwd`, `pwck`, `chpasswd`, `chage`, `groupadd`, `groupdel`, `groupmod`, `grpck`, `chfn`, `chsh`, and `newgrp`. + +This project aims to be a drop-in replacement for the original commands, with the same flags, exit codes, and output format as GNU shadow-utils. + +# Goals + +This project aims to be a drop-in replacement for the GNU shadow-utils, with a focus on: + +- **Drop-in compatibility** - same flags, same exit codes, same output format as GNU shadow-utils. +- **Memory safety** - eliminating buffer overflows and use-after-free vulnerabilities through Rust's type system. +- **Security hardening** - Landlock sandboxing and privilege dropping. +- **Comprehensive testing** - unit tests, property-based tests, integration tests, and fuzz targets. + +# Contributing + +To contribute to uutils shadow, please see [CONTRIBUTING](https://github.com/uutils/shadow/blob/main/CONTRIBUTING.md). + +# License + +uutils shadow is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/shadow/blob/main/LICENSE) file for details. + +GNU shadow-utils is licensed under the BSD 3-Clause License. diff --git a/content/talks.md b/content/talks.md new file mode 100644 index 000000000..717122f48 --- /dev/null +++ b/content/talks.md @@ -0,0 +1,63 @@ ++++ + +title = "Talks & Media" +template = "page.html" + ++++ + +# Talks + +## FOSDEM 2026 — Rust Coreutils in Ubuntu + +*Sylvestre Ledru — February 1, 2026* + +Yes, we rewrote `/bin/true` in Rust — Here's what really happened. Which obscure behaviours scripts secretly depend on, how packaging Essential tools can turn one missing corner-case into a boot failure, and what we learned shipping Rust coreutils in Ubuntu. + +- [Schedule](https://fosdem.org/2026/schedule/event/DTYYL9-rust-coreutils/) +- [Slides](https://sylvestre.ledru.info/presentations/coreutils-fosdem-2026/) + +## FOSDEM 2025 — Rewriting the future of the Linux essential packages in Rust? + +*Sylvestre Ledru — February 1, 2025* + +The Linux ecosystem depends on core utilities written decades ago in C, but modern needs demand safer, more maintainable tools. This talk covers the uutils project's effort to reimagine essential Linux tools in Rust, including coreutils, findutils, and diffutils. + +- [Schedule](https://archive.fosdem.org/2025/schedule/event/fosdem-2025-6196-rewriting-the-future-of-the-linux-essential-packages-in-rust-/) +- [Slides](https://sylvestre.ledru.info/coreutils-fosdem-2025/) + +## FOSDEM 2023 — Reimplementing the Coreutils in a modern language (Rust) + +*Sylvestre Ledru — February 5, 2023* + +How a community spawned to reimplement the coreutils in Rust, enabling achievements like booting Debian and compiling Firefox and the Linux kernel with these reimplementations. + +- [Schedule](https://archive.fosdem.org/2023/schedule/event/rust_coreutils/) +- [Slides](https://sylvestre.ledru.info/presentations/coreutils-fosdem-2023/) + +# Podcasts + +## Open Source Security Podcast — Rust coreutils with Sylvestre Ledru + +*March 2, 2026* + +Josh interviews Sylvestre Ledru about rewriting GNU coreutils in Rust — modernizing legacy code, attracting new contributors, the positive relationship with GNU coreutils developers, Ubuntu's adoption plans, and future directions. + +- [Listen](https://opensourcesecuritypodcast.libsyn.com/rust-coreutils-with-sylvestre-ledru) + +## FLOSS Weekly Episode 792 — Rust Coreutils + +*July 17, 2024* + +Jonathan Bennett and Jeff Massie interview Sylvestre Ledru about the Rust Coreutils project — why developers are reimplementing classic Unix utilities in Rust, the practical advantages, and the perspective of the original coreutils maintainers. + +- [Listen](https://hackaday.com/2024/07/17/floss-weekly-episode-792-rust-coreutils/) + +# Videos + +## Rust at TU Delft — Rewriting the GNU Coreutils in Rust + +*Terts Diepraam* + +An overview of the uutils coreutils project, its goals, challenges, and the path toward replacing GNU coreutils in Linux distributions. + +- [Watch on YouTube](https://www.youtube.com/watch?v=fu2sxy6OR54) diff --git a/content/tar.md b/content/tar.md new file mode 100644 index 000000000..b8d77e825 --- /dev/null +++ b/content/tar.md @@ -0,0 +1,29 @@ ++++ + +title = "tar" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils tar is a work-in-progress Rust reimplementation of GNU tar, the ubiquitous archiving utility. + +This project aims to be a drop-in replacement for GNU tar, supporting the creation, extraction, and manipulation of tar archives across platforms. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from GNU tar. + +# Goals + +Implement a cross-platform, memory-safe tar that is compatible with GNU tar archives and command-line interface. + +# Contributing + +To contribute to uutils tar, please see [CONTRIBUTING](https://github.com/uutils/tar/blob/main/CONTRIBUTING.md). + +# License + +uutils tar is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/tar/blob/main/LICENSE) file for details. + +GNU tar is licensed under the GPL 3.0 or later. diff --git a/content/team.md b/content/team.md new file mode 100644 index 000000000..8a06568a9 --- /dev/null +++ b/content/team.md @@ -0,0 +1,35 @@ ++++ + +title = "Who we are" +template = "page.html" + ++++ + +uutils is a community-driven, open-source effort maintained by volunteers around the world. There is no company behind it, just contributors who care about the future of foundational command-line tools. + +Everything happens in the open on [GitHub](https://github.com/uutils), and newcomers are genuinely welcome. Many of our contributors landed their first-ever open-source patch on a uutils project, and we are happy to help you do the same. + + + +uutils is built by far more people than this list. See the full roster of contributors on each project's [GitHub repository](https://github.com/uutils). diff --git a/content/users.md b/content/users.md new file mode 100644 index 000000000..e37604eaa --- /dev/null +++ b/content/users.md @@ -0,0 +1,249 @@ ++++ +title = "Who uses uutils" +template = "page.html" ++++ + +uutils is no longer just a development experiment - it is running in production +at some of the largest software organisations in the world. This page documents +known adopters and how they use our tools. + +--- + +## Canonical / Ubuntu + +Canonical is progressively replacing GNU coreutils with uutils across Ubuntu. +Canonical laid out the roadmap in March 2025 in +[*Carefully But Purposefully Oxidising Ubuntu*](https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995). + +The rollout: + +| Release | Status | +|---|---| +| Ubuntu 25.10 | uutils coreutils ships as default - real-world testing before the LTS | +| Ubuntu 26.04 LTS | rust-coreutils 0.8.0 included; `cp`, `mv`, `rm` remain GNU pending 8 TOCTOU fixes found in audit | +| Ubuntu 26.10 | Target for 100% rust-coreutils | + +To support this transition, Canonical commissioned a two-phase security audit +with [Zellic](https://zellic.io) (December 2025 – March 2026). The audit found +and resolved 113 issues in total, all reported back to the uutils upstream. +The full report is available at +[github.com/Zellic/publications](https://github.com/Zellic/publications/blob/master/uutils%20coreutils%20-%20Zellic%20Audit%20Report.pdf). + +A Canonical engineer also created +[**oxidizr**](https://github.com/jnsgruk/oxidizr), a tool that lets users +on Ubuntu 24.04+ switch to uutils today - replacing GNU binaries with uutils +symlinks, reversibly, in one command. + +**Links:** +- [Discourse: Carefully But Purposefully Oxidising Ubuntu](https://discourse.ubuntu.com/t/carefully-but-purposefully-oxidising-ubuntu/56995) +- [Discourse: Migration to rust-coreutils in 25.10](https://discourse.ubuntu.com/t/migration-to-rust-coreutils-in-25-10/59708) +- [Discourse: An update on rust-coreutils (April 2026)](https://discourse.ubuntu.com/t/an-update-on-rust-coreutils/80773) +- [github.com/jnsgruk/oxidizr](https://github.com/jnsgruk/oxidizr) +- [LWN.net coverage](https://lwn.net/Articles/1014002/) + +--- + +## Microsoft + +Microsoft ships uutils coreutils as **Coreutils for Windows**, a native +Windows distribution of uutils/coreutils, uutils/findutils, and a Microsoft +fork of uutils/grep. It was announced at **Microsoft Build 2026** (June 2, 2026) +as part of Windows becoming a first-class development platform. + +The distribution is available today via: + +``` +winget install Microsoft.Coreutils +``` + + +The stated goal is to make moving between Linux, macOS, WSL, containers, and +Windows frictionless: the same commands, flags, and pipelines work the same +way, so existing scripts carry over without translation. + +Microsoft maintains the downstream packaging at +[github.com/microsoft/coreutils](https://github.com/microsoft/coreutils). +The uutils/coreutils project is the upstream - Microsoft builds from it +directly and does not rewrite the tools. + +**Links:** +- [Windows Developer Blog - Build 2026 announcement](https://blogs.windows.com/windowsdeveloper/2026/06/02/build-2026-furthering-windows-as-the-trusted-platform-for-development/) +- [Microsoft Learn - Coreutils for Windows overview](https://learn.microsoft.com/en-us/windows/core-utils/overview) +- [github.com/microsoft/coreutils](https://github.com/microsoft/coreutils) + +--- + +## Snap Inc. (SPECS AR glasses) + +[Snap Inc.](https://snap.com/) ships **Snap OS** - the proprietary Linux-based +operating system powering the [SPECS augmented reality glasses](https://newsroom.snap.com/introducing-specs-augmented-reality-glasses) +announced at Augmented World Expo 2026 - built on **Yocto/OpenEmbedded**. + +A Snap engineer, Etienne Cordonnier, has been the primary contributor and +maintainer of the `uutils-coreutils` recipe in the **meta-openembedded** +(`meta-oe`) layer since Yocto 4.1 (langdale). The recipe uses `PROVIDES = "coreutils"` so uutils +acts as a transparent drop-in replacement for GNU coreutils in embedded images. + +**Links:** +- [SPECS AR glasses announcement](https://newsroom.snap.com/introducing-specs-augmented-reality-glasses) +- [uutils-coreutils recipe on OpenEmbedded Layer Index](https://layers.openembedded.org/rrs/recipedetail/meta-openembedded/3417/) +- [Patches on the Yocto mailing list](https://lists.yoctoproject.org/g/poky/topic/bitbake_recipe_for/92508454) + +--- + +## Debian + +Debian has packaged uutils coreutils since **Debian 12 (Bookworm)** and is +actively tracking upstream releases. + +A **Google Summer of Code 2024** project - *"Improve support of the Rust +coreutils in Debian"* - was mentored to accelerate the potential +integration. The package is also inherited by downstream Debian-based +distributions including Raspbian, Kali Linux, Parrot OS, PureOS, and deepin 23. + +There has been further progress in **Debian experimental**: a virtual +`coreutils` package now dispatches between providers (GNU, uutils, busybox, +toybox), and installing `coreutils` there pulls in +[`coreutils-from-uutils`](https://tracker.debian.org/pkg/coreutils-from-uutils) +and [`rust-coreutils`](https://tracker.debian.org/pkg/rust-coreutils), making +uutils the default in experimental. + +**Links:** +- [Debian package tracker: rust-coreutils](https://tracker.debian.org/pkg/rust-coreutils) +- [Debian package tracker: coreutils-from-uutils](https://tracker.debian.org/pkg/coreutils-from-uutils) +- [GSoC 2024 project](https://wiki.debian.org/SummerOfCode2024/ApprovedProjects) + +--- + +## Alpine Linux + +Alpine Linux packages uutils coreutils in its **community repository** (Alpine +3.19+, now at 0.9.0 in Alpine Edge / 3.24). What makes Alpine particularly +notable is the depth of adoption: **29 downstream Alpine packages already +declare a dependency on uutils-coreutils**, including lvm2, netdata, dracut, +Pi-hole, and openvas-scanner. + +When both `uutils-coreutils` and `coreutils` are installed, Alpine's package +manager automatically purges the GNU binaries and replaces them with uutils +symlinks. + +**Links:** +- [Alpine package: uutils-cutils](https://pkgs.alpinelinux.org/package/edge/community/x86_64/uutils-coreutils) + +--- + +## Redox OS + +[Redox OS](https://www.redox-os.org/), the microkernel operating system written +entirely in Rust, uses uutils as its coreutils layer. The +[Redox Book](https://doc.redox-os.org/book/system-tools.html) states it plainly: +"Redox uses the Rust implementation of the GNU Coreutils, uutils." Redox is also +listed as an officially supported platform in the uutils codebase. + +--- + +## VS Code for the Web + +**Microsoft VS Code for the Web** (vscode.dev) uses uutils coreutils compiled +to **WebAssembly/WASI** to power the shell commands (`ls`, `cat`, `date`, etc.) +available in the browser-based terminal. This was documented in the official +VS Code blog in June 2023 and represents one of the first large-scale +production deployments of uutils in a WASM context. + +**Links:** +- [VS Code blog: WebAssembly shell](https://code.visualstudio.com/blogs/2023/06/05/vscode-wasm-wasi) + +--- + +## Buildroot + +[Buildroot](https://buildroot.org/), the widely-used embedded Linux build +system, ships an official `uutils-coreutils` package since April 2025. + +This brings uutils into the reach of a vast ecosystem of IoT, industrial, and +embedded devices built with Buildroot. + +--- + +## Apertus + +[Apertus](https://www.apertus.org/) builds the **AXIOM Beta**, an open-source +professional cinema camera. Their firmware build system runs on Ubuntu hosts +and was updated in December 2025 to support uutils coreutils as the host +toolchain. + +This makes Apertus one of the first hardware projects to explicitly track and +maintain uutils compatibility in their build system. + +**Links:** +- [apertus.org](https://www.apertus.org/) +- [axiom-firmware on GitHub](https://github.com/apertus-open-source-cinema/axiom-firmware) + +--- + +## Fedora / RHEL / EPEL + +uutils coreutils is packaged in **Fedora** since F39/F40 (as `rust-coreutils`, +renamed to `uutils-coreutils` in Fedora 42). It is available in Fedora +42, 43, 44 and Rawhide at version 0.7.0, and in **EPEL 9 and EPEL 10** - making +it available to Red Hat Enterprise Linux users. Fedora is not planning to make +it the default in the near term but maintains the package actively. + +uutils is also packaged in **OpenMandriva** (all branches) and **Apertis** +(the Debian-based embedded automotive Linux distro, v2023–v2027). + +**Links:** +- [Fedora package: uutils-coreutils](https://packages.fedoraproject.org/pkgs/uutils-coreutils/) +- [EPEL package](https://packages.fedoraproject.org/pkgs/uutils-coreutils/) + +--- + +## NixOS + +uutils coreutils is available in **nixpkgs** as `uutils-coreutils` and +`uutils-coreutils-noprefix` (the latter installs commands without the `uu-` +prefix, as drop-in replacements). + +**Links:** +- [NixOS Wiki: Uutils](https://wiki.nixos.org/wiki/Uutils) + +--- + +## macOS + +uutils is available on macOS via two package managers: + +- **Homebrew**: `brew install uutils-coreutils` +- **MacPorts**: `port install coreutils-uutils` + +This makes uutils a practical cross-platform development tool: scripts written +with uutils on Linux run identically on macOS developer machines without +depending on GNU coreutils via Homebrew. + +--- + +## Windows (community) + +Beyond Microsoft's official distribution, uutils is available on Windows via: + +- **MSYS2** (all variants): 0.9.0 +- **Scoop**: `scoop install uutils-coreutils` +- **Chocolatey**: `choco install uutils-coreutils` + +--- + +## Termux (Android) and ChromeOS + +- **[Termux User Repository (TUR)](https://github.com/termux-user-repository/tur/blob/master/tur/uutils-coreutils/build.sh#L2)**: + uutils-coreutils 0.9.0 is available for Android terminals via Termux. +- **[Chromebrew](https://github.com/chromebrew/chromebrew/blob/master/packages/uutils_coreutils.rb#L6)**: ChromeOS users can + install uutils-coreutils via the Chromebrew package manager. + +--- + +## Are you using uutils? + +If your project or organisation uses uutils tools, we would love to hear from +you. Open an issue or pull request on +[github.com/uutils/uutils.github.io](https://github.com/uutils/uutils.github.io) +to add your entry to this page. diff --git a/content/util-linux.md b/content/util-linux.md new file mode 100644 index 000000000..5e3291e1a --- /dev/null +++ b/content/util-linux.md @@ -0,0 +1,29 @@ ++++ + +title = "util-linux" +template = "project.html" + +[extra] +wip = true + ++++ + +uutils util-linux is a work-in-progress Rust reimplementation of the util-linux suite, a collection of essential Linux system utilities. + +This project aims to provide memory-safe, cross-platform replacements for tools such as `mount`, `umount`, `lsblk`, `fdisk`, `kill`, and many others found in util-linux. + +> **Note:** This project is still in early development. Many features are not yet implemented and behavior may differ from the original util-linux tools. + +# Goals + +Implement a cross-platform, memory-safe set of utilities compatible with the util-linux command-line interfaces. + +# Contributing + +To contribute to uutils util-linux, please see [CONTRIBUTING](https://github.com/uutils/util-linux/blob/main/CONTRIBUTING.md). + +# License + +uutils util-linux is licensed under the MIT License - see the [LICENSE](https://github.com/uutils/util-linux/blob/main/LICENSE) file for details. + +util-linux is licensed under the GPL 2.0 or later. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..5477a2bb2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,78 @@ +// Flat ESLint config for the playground's browser-side JavaScript. +// +// These files run in the browser (no bundler, no module system) and lean on a +// handful of globals provided by the page or by third-party scripts loaded via +// +ENDSCRIPT + +# Inject the dynamically-built language list and generation date +GENDATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) +sed -i "s|LANGS_PLACEHOLDER|[${LANGS_JSON}]|" "$HEAD_HBS" +sed -i "s|GENDATE_PLACEHOLDER|'${GENDATE}'|" "$HEAD_HBS" + +echo "Patched $HEAD_HBS with language selector and utility links" diff --git a/scripts/run-tests.js b/scripts/run-tests.js new file mode 100644 index 000000000..b10cb0dab --- /dev/null +++ b/scripts/run-tests.js @@ -0,0 +1,120 @@ +#!/usr/bin/env node +/** + * Run the wasm-terminal unit tests headlessly using Puppeteer. + * + * Usage: + * npm install puppeteer # one-time setup + * node scripts/run-tests.js [--port 8080] [--dir public] + * + * The script starts a local HTTP server, opens the test page in headless + * Chrome, waits for the results, and exits with code 1 on failure. + */ + +const http = require("http"); +const fs = require("fs"); +const path = require("path"); + +// Parse CLI args +const args = process.argv.slice(2); +let port = 8080; +let dir = "public"; + +for (let i = 0; i < args.length; i++) { + if (args[i] === "--port" && args[i + 1]) port = parseInt(args[++i]); + if (args[i] === "--dir" && args[i + 1]) dir = args[++i]; +} + +// Resolve serve directory — fall back to static/ if public/ doesn't exist +// (public/ is the zola build output; static/ works for running without a build) +const serveDir = fs.existsSync(path.resolve(dir)) + ? path.resolve(dir) + : path.resolve("static"); + +const MIME_TYPES = { + ".html": "text/html", + ".js": "application/javascript", + ".css": "text/css", + ".wasm": "application/wasm", + ".json": "application/json", + ".svg": "image/svg+xml", + ".png": "image/png", +}; + +function startServer() { + return new Promise((resolve) => { + const server = http.createServer((req, res) => { + const url = req.url.split("?")[0]; + let filePath = path.join(serveDir, url === "/" ? "index.html" : url); + + // Prevent path traversal outside the serve directory + if (!path.resolve(filePath).startsWith(serveDir + path.sep) && path.resolve(filePath) !== serveDir) { + res.writeHead(403); + res.end("Forbidden"); + return; + } + + if (!fs.existsSync(filePath)) { + res.writeHead(404); + res.end("Not found"); + return; + } + + if (fs.statSync(filePath).isDirectory()) { + filePath = path.join(filePath, "index.html"); + } + + const ext = path.extname(filePath); + const mime = MIME_TYPES[ext] || "application/octet-stream"; + + res.writeHead(200, { "Content-Type": mime }); + fs.createReadStream(filePath).pipe(res); + }); + + server.listen(port, "127.0.0.1", () => { + console.log(`Serving ${serveDir} on http://127.0.0.1:${port}`); + resolve(server); + }); + }); +} + +async function runTests(server) { + let puppeteer; + try { + puppeteer = require("puppeteer"); + } catch { + console.error("Puppeteer not found. Install it with: npm install puppeteer"); + process.exit(1); + } + + const browser = await puppeteer.launch({ + headless: true, + args: ["--no-sandbox", "--disable-setuid-sandbox"], + }); + + const page = await browser.newPage(); + page.on("console", (msg) => console.log("BROWSER:", msg.text())); + page.on("pageerror", (err) => console.error("PAGE ERROR:", err.message)); + + const testUrl = `http://127.0.0.1:${port}/js/wasm-terminal.test.html`; + console.log(`Opening ${testUrl}`); + + await page.goto(testUrl, { waitUntil: "networkidle0", timeout: 30000 }); + await page.waitForFunction("window.__testsFailed !== undefined", { + timeout: 30000, + }); + + const failed = await page.evaluate(() => window.__testsFailed); + const passed = await page.evaluate(() => window.__testsPassed); + + console.log(`\nResults: ${passed} passed, ${failed} failed`); + + await browser.close(); + server.close(); + + process.exit(failed > 0 ? 1 : 0); +} + +(async () => { + const server = await startServer(); + await runTests(server); +})(); diff --git a/static/CNAME b/static/CNAME new file mode 100644 index 000000000..fbd5bb9e2 --- /dev/null +++ b/static/CNAME @@ -0,0 +1 @@ +uutils.org diff --git a/static/fonts/JetBrainsMono-400.woff2 b/static/fonts/JetBrainsMono-400.woff2 new file mode 100644 index 000000000..585887339 Binary files /dev/null and b/static/fonts/JetBrainsMono-400.woff2 differ diff --git a/static/fonts/JetBrainsMono-500.woff2 b/static/fonts/JetBrainsMono-500.woff2 new file mode 100644 index 000000000..be878e68f Binary files /dev/null and b/static/fonts/JetBrainsMono-500.woff2 differ diff --git a/static/fonts/JetBrainsMono-700.woff2 b/static/fonts/JetBrainsMono-700.woff2 new file mode 100644 index 000000000..3a4e333fc Binary files /dev/null and b/static/fonts/JetBrainsMono-700.woff2 differ diff --git a/static/giallo-dark.css b/static/giallo-dark.css new file mode 100644 index 000000000..9195290e4 --- /dev/null +++ b/static/giallo-dark.css @@ -0,0 +1,477 @@ +/* + * theme "one-dark-pro" generated by giallo + */ + +.z-code { + color: #ABB2BF; + background-color: #282C34; +} + +.z-hl { + background-color: #2C313C; +} + +.giallo-ln { + color: #495162; +} + +.z-keyword { color: #C678DD; } +.z-variable { color: #E06C75; } +.z-storage { color: #C678DD; } +.z-string { color: #98C379; } +.z-constant { color: #D19A66; } +.z-rgb-value { color: #56B6C2; } +.z-comment { color: #7F848E; font-style: italic; } +.z-md { color: #C678DD; } +.z-punctuation { color: #E06C75; } +.z-rgb-value { color: #D19A66; } +.z-rgb-value { color: #D19A66; } +.z-punctuation { color: #98C379; } +.z-punctuation { color: #98C379; } +.z-meta.z-embedded { color: #ABB2BF; } +.z-source.z-java { color: #E06C75; } +.z-keyword.z-operator { color: #ABB2BF; } +.z-variable.z-c { color: #ABB2BF; } +.z-variable.z-language { color: #E5C07B; } +.z-token.z-package { color: #ABB2BF; } +.z-meta.z-require { color: #61AFEF; } +.z-variable.z-function { color: #61AFEF; } +.z-support.z-class { color: #E5C07B; } +.z-keyword.z-control { color: #C678DD; } +.z-control.z-elements { color: #D19A66; } +.z-token.z-storage { color: #C678DD; } +.z-support.z-function { color: #56B6C2; } +.z-meta.z-tag { color: #ABB2BF; } +.z-constant.z-numeric { color: #D19A66; } +.z-meta.z-selector { color: #C678DD; } +.z-markup.z-heading { color: #E06C75; } +.z-markup.z-bold { color: #D19A66; } +.z-todo.z-bold { color: #D19A66; } +.z-markup.z-italic { color: #C678DD; } +.z-todo.z-emphasis { color: #C678DD; } +.z-string.z-regexp { color: #56B6C2; } +.z-variable.z-interpolation { color: #E06C75; } +.z-invalid.z-illegal { color: #FFFFFF; } +.z-invalid.z-broken { color: #FFFFFF; } +.z-invalid.z-deprecated { color: #FFFFFF; } +.z-invalid.z-unimplemented { color: #FFFFFF; } +.z-function.z-parameter { color: #ABB2BF; } +.z-function.z-brace { color: #ABB2BF; } +.z-selector.z-sass { color: #E06C75; } +.z-token.z-info-token { color: #61AFEF; } +.z-token.z-warn-token { color: #D19A66; } +.z-token.z-error-token { color: #F44747; } +.z-token.z-debug-token { color: #C678DD; } +.z-source.z-ini { color: #98C379; } +.z-source.z-makefile { color: #E5C07B; } +.z-text.z-variable { color: #E06C75; } +.z-text.z-bracketed { color: #E06C75; } +.z-invalid.z-xi { color: #ABB2BF; } +.z-accent.z-xi { color: #61AFEF; } +.z-wikiword.z-xi { color: #D19A66; } +.z-markup.z-deleted { color: #E06C75; } +.z-markup.z-inserted { color: #98C379; } +.z-markup.z-underline { text-decoration: underline; } +.z-string.z-regexp { color: #E06C75; } +.z-log.z-info { color: #98C379; } +.z-log.z-warning { color: #E5C07B; } +.z-log.z-error { color: #E06C75; } +.z-variable.z-parameter { font-style: italic; } +.z-markup.z-link { color: #5C6370; } +.z-punctuation.z-string { color: #E06C75; } +.z-storage.z-type.z-haskell { color: #D19A66; } +.z-variable.z-language.z-rust { color: #E06C75; } +.z-support.z-constant.z-edge { color: #C678DD; } +.z-keyword.z-operator.z-word { color: #C678DD; } +.z-variable.z-parameter.z-function { color: #ABB2BF; } +.z-markup.z-changed.z-diff { color: #E5C07B; } +.z-markup.z-inserted.z-diff { color: #98C379; } +.z-markup.z-deleted.z-diff { color: #E06C75; } +.z-meta.z-function.z-c { color: #E06C75; } +.z-meta.z-function.z-cpp { color: #E06C75; } +.z-punctuation.z-separator.z-key-value { color: #ABB2BF; } +.z-support.z-constant.z-math { color: #E5C07B; } +.z-variable.z-other.z-constant { color: #E5C07B; } +.z-punctuation.z-terminator.z-java { color: #ABB2BF; } +.z-meta.z-method-call.z-java { color: #ABB2BF; } +.z-meta.z-method.z-java { color: #61AFEF; } +.z-storage.z-type.z-java { color: #E5C07B; } +.z-keyword.z-operator.z-logical { color: #56B6C2; } +.z-keyword.z-operator.z-bitwise { color: #56B6C2; } +.z-keyword.z-operator.z-channel { color: #56B6C2; } +.z-keyword.z-operator.z-css { color: #56B6C2; } +.z-keyword.z-operator.z-scss { color: #56B6C2; } +.z-keyword.z-operator.z-less { color: #56B6C2; } +.z-support.z-module.z-node { color: #E5C07B; } +.z-support.z-module.z-node { color: #E5C07B; } +.z-variable.z-other.z-readwrite { color: #E06C75; } +.z-meta.z-object-literal.z-key { color: #E06C75; } +.z-support.z-variable.z-property { color: #E06C75; } +.z-support.z-constant.z-json { color: #D19A66; } +.z-keyword.z-operator.z-new { color: #C678DD; } +.z-keyword.z-operator.z-ternary { color: #C678DD; } +.z-keyword.z-operator.z-optional { color: #C678DD; } +.z-entity.z-name.z-function { color: #61AFEF; } +.z-support.z-function.z-console { color: #61AFEF; } +.z-keyword.z-operator.z-delete { color: #C678DD; } +.z-support.z-variable.z-dom { color: #E06C75; } +.z-keyword.z-operator.z-arithmetic { color: #56B6C2; } +.z-keyword.z-operator.z-comparison { color: #56B6C2; } +.z-keyword.z-operator.z-decrement { color: #56B6C2; } +.z-keyword.z-operator.z-increment { color: #56B6C2; } +.z-keyword.z-operator.z-relational { color: #56B6C2; } +.z-keyword.z-operator.z-c { color: #C678DD; } +.z-keyword.z-operator.z-cpp { color: #C678DD; } +.z-punctuation.z-separator.z-delimiter { color: #ABB2BF; } +.z-punctuation.z-separator.z-c { color: #C678DD; } +.z-punctuation.z-separator.z-cpp { color: #C678DD; } +.z-support.z-type.z-python { color: #56B6C2; } +.z-entity.z-name.z-namespace { color: #E5C07B; } +.z-import.z-storage.z-java { color: #E5C07B; } +.z-token.z-package.z-keyword { color: #C678DD; } +.z-entity.z-name.z-function { color: #61AFEF; } +.z-support.z-function.z-any-method { color: #61AFEF; } +.z-entity.z-name.z-class { color: #E5C07B; } +.z-entity.z-name.z-type { color: #E5C07B; } +.z-keyword.z-operator.z-less { color: #D19A66; } +.z-keyword.z-other.z-special-method { color: #61AFEF; } +.z-keyword.z-operator.z-new { color: #C678DD; } +.z-support.z-type.z-property-name { color: #ABB2BF; } +.z-support.z-constant.z-property-value { color: #ABB2BF; } +.z-support.z-constant.z-font-name { color: #D19A66; } +.z-constant.z-other.z-symbol { color: #56B6C2; } +.z-punctuation.z-definition.z-constant { color: #D19A66; } +.z-entity.z-name.z-tag { color: #E06C75; } +.z-entity.z-other.z-attribute-name { color: #D19A66; } +.z-entity.z-name.z-section { color: #61AFEF; } +.z-keyword.z-other.z-unit { color: #E06C75; } +.z-punctuation.z-definition.z-bold { color: #E5C07B; } +.z-punctuation.z-definition.z-italic { color: #C678DD; } +.z-markup.z-heading.z-setext { color: #ABB2BF; } +.z-punctuation.z-definition.z-asciidoc { color: #E5C07B; } +.z-markup.z-list.z-asciidoc { color: #E5C07B; } +.z-markup.z-link.z-asciidoc { color: #C678DD; } +.z-string.z-unquoted.z-asciidoc { color: #61AFEF; } +.z-punctuation.z-section.z-embedded { color: #E06C75; } +.z-meta.z-interface.z-php { color: #E5C07B; } +.z-storage.z-type.z-php { color: #E5C07B; } +.z-meta.z-function-call.z-php { color: #61AFEF; } +.z-support.z-other.z-php { color: #61AFEF; } +.z-function.z-parameter.z-ruby { color: #ABB2BF; } +.z-function.z-parameter.z-cs { color: #ABB2BF; } +.z-block.z-scope.z-end { color: #ABB2BF; } +.z-block.z-scope.z-begin { color: #ABB2BF; } +.z-storage.z-type.z-cs { color: #E5C07B; } +.z-punctuation.z-section.z-embedded { color: #C678DD; } +.z-meta.z-template.z-expression { color: #ABB2BF; } +.z-keyword.z-operator.z-module { color: #C678DD; } +.z-support.z-type.z-primitive { color: #E5C07B; } +.z-meta.z-property.z-object { color: #E06C75; } +.z-keyword.z-operator.z-assignment { color: #56B6C2; } +.z-support.z-constant.z-elm { color: #D19A66; } +.z-punctuation.z-quasi.z-element { color: #C678DD; } +.z-constant.z-character.z-entity { color: #E06C75; } +.z-entity.z-global.z-clojure { color: #E5C07B; } +.z-meta.z-symbol.z-clojure { color: #E06C75; } +.z-constant.z-keyword.z-clojure { color: #56B6C2; } +.z-meta.z-arguments.z-coffee { color: #E06C75; } +.z-meta.z-method.z-groovy { color: #61AFEF; } +.z-support.z-type.z-swift { color: #E5C07B; } +.z-constant.z-regexp.z-xi { color: #C678DD; } +.z-keyword.z-control.z-xi { color: #56B6C2; } +.z-constant.z-character.z-xi { color: #61AFEF; } +.z-meta.z-brace.z-square { color: #ABB2BF; } +.z-punctuation.z-definition.z-comment { color: #7F848E; font-style: italic; } +.z-markup.z-quote.z-markdown { color: #5C6370; } +.z-variable.z-other.z-object { color: #E5C07B; } +.z-entity.z-other.z-inherited-class { color: #E5C07B; } +.z-constant.z-other.z-php { color: #ABB2BF; } +.z-constant.z-character.z-escape { color: #56B6C2; } +.z-entity.z-name.z-label { color: #E06C75; } +.z-variable.z-language.z-super { font-style: italic; } +.z-comment.z-line.z-double-slash { font-style: italic; } +.z-comment.z-block.z-documentation { font-style: italic; } +.z-markup.z-italic.z-markdown { font-style: italic; } +.z-punctuation.z-definition.z-heading { color: #61AFEF; } +.z-string.z-quoted.z-json { color: #E06C75; } +.z-constant.z-language.z-json { color: #56B6C2; } +.z-constant.z-language.z-json { color: #56B6C2; } +.z-support.z-constant.z-laravel-blade { color: #C678DD; } +.z-string.z-quoted.z-json { color: #98C379; } +.z-string.z-quoted.z-json { color: #98C379; } +.z-punctuation.z-definition.z-delayed.z-unison { color: #E06C75; } +.z-punctuation.z-separator.z-pipe.z-unison { color: #E06C75; } +.z-punctuation.z-separator.z-delimiter.z-unison { color: #E06C75; } +.z-punctuation.z-definition.z-hash.z-unison { color: #E06C75; } +.z-variable.z-other.z-generic-type.z-haskell { color: #C678DD; } +.z-support.z-variable.z-magic.z-python { color: #E06C75; } +.z-punctuation.z-separator.z-period.z-python { color: #ABB2BF; } +.z-punctuation.z-separator.z-element.z-python { color: #ABB2BF; } +.z-punctuation.z-parenthesis.z-begin.z-python { color: #ABB2BF; } +.z-punctuation.z-parenthesis.z-end.z-python { color: #ABB2BF; } +.z-storage.z-modifier.z-lifetime.z-rust { color: #ABB2BF; } +.z-support.z-function.z-std.z-rust { color: #61AFEF; } +.z-entity.z-name.z-lifetime.z-rust { color: #E5C07B; } +.z-constant.z-other.z-character-class.z-regexp { color: #E06C75; } +.z-keyword.z-operator.z-quantifier.z-regexp { color: #D19A66; } +.z-meta.z-diff.z-header.z-from-file { color: #61AFEF; } +.z-meta.z-diff.z-header.z-to-file { color: #61AFEF; } +.z-punctuation.z-definition.z-from-file.z-diff { color: #61AFEF; } +.z-punctuation.z-definition.z-to-file.z-diff { color: #61AFEF; } +.z-punctuation.z-terminator.z-statement.z-c { color: #ABB2BF; } +.z-keyword.z-operator.z-expression.z-import { color: #61AFEF; } +.z-support.z-constant.z-property.z-math { color: #D19A66; } +.z-storage.z-type.z-annotation.z-java { color: #E5C07B; } +.z-meta.z-method.z-identifier.z-java { color: #ABB2BF; } +.z-punctuation.z-separator.z-period.z-java { color: #ABB2BF; } +.z-punctuation.z-bracket.z-angle.z-java { color: #ABB2BF; } +.z-punctuation.z-definition.z-annotation.z-java { color: #ABB2BF; } +.z-meta.z-method.z-body.z-java { color: #ABB2BF; } +.z-storage.z-modifier.z-import.z-java { color: #E5C07B; } +.z-storage.z-type.z-generic.z-java { color: #E5C07B; } +.z-keyword.z-operator.z-instanceof.z-java { color: #C678DD; } +.z-support.z-constant.z-property-value.z-scss { color: #D19A66; } +.z-support.z-constant.z-property-value.z-css { color: #D19A66; } +.z-support.z-type.z-object.z-module { color: #E5C07B; } +.z-entity.z-name.z-type.z-module { color: #E5C07B; } +.z-support.z-variable.z-object.z-process { color: #E06C75; } +.z-support.z-variable.z-object.z-node { color: #E06C75; } +.z-keyword.z-operator.z-expression.z-instanceof { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-keyof { color: #C678DD; } +.z-support.z-type.z-object.z-console { color: #E06C75; } +.z-support.z-variable.z-property.z-process { color: #D19A66; } +.z-keyword.z-operator.z-misc.z-rust { color: #ABB2BF; } +.z-keyword.z-operator.z-sigil.z-rust { color: #C678DD; } +.z-support.z-type.z-object.z-dom { color: #56B6C2; } +.z-support.z-variable.z-property.z-dom { color: #E06C75; } +.z-keyword.z-operator.z-assignment.z-c { color: #C678DD; } +.z-keyword.z-operator.z-comparison.z-c { color: #C678DD; } +.z-keyword.z-operator.z-increment.z-c { color: #C678DD; } +.z-keyword.z-operator.z-decrement.z-c { color: #C678DD; } +.z-keyword.z-operator.z-assignment.z-cpp { color: #C678DD; } +.z-keyword.z-operator.z-comparison.z-cpp { color: #C678DD; } +.z-keyword.z-operator.z-increment.z-cpp { color: #C678DD; } +.z-keyword.z-operator.z-decrement.z-cpp { color: #C678DD; } +.z-support.z-type.z-posix-reserved.z-c { color: #56B6C2; } +.z-support.z-type.z-posix-reserved.z-cpp { color: #56B6C2; } +.z-keyword.z-operator.z-sizeof.z-c { color: #C678DD; } +.z-keyword.z-operator.z-sizeof.z-cpp { color: #C678DD; } +.z-keyword.z-operator.z-logical.z-python { color: #C678DD; } +.z-variable.z-parameter.z-function.z-python { color: #D19A66; } +.z-punctuation.z-separator.z-arguments.z-python { color: #ABB2BF; } +.z-meta.z-function-call.z-generic.z-python { color: #61AFEF; } +.z-keyword.z-operator.z-assignment.z-compound { color: #C678DD; } +.z-token.z-variable.z-parameter.z-java { color: #ABB2BF; } +.z-entity.z-name.z-type.z-namespace { color: #E5C07B; } +.z-entity.z-name.z-type.z-class { color: #E5C07B; } +.z-variable.z-other.z-class.z-js { color: #E5C07B; } +.z-variable.z-other.z-class.z-ts { color: #E5C07B; } +.z-variable.z-other.z-class.z-php { color: #E06C75; } +.z-keyword.z-operator.z-expression.z-delete { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-in { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-of { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-instanceof { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-typeof { color: #C678DD; } +.z-keyword.z-operator.z-expression.z-void { color: #C678DD; } +.z-token.z-storage.z-type.z-java { color: #E5C07B; } +.z-support.z-type.z-property-name.z-toml { color: #E06C75; } +.z-entity.z-other.z-attribute-name.z-id { color: #61AFEF; } +.z-entity.z-name.z-section.z-markdown { color: #E06C75; } +.z-punctuation.z-definition.z-heading.z-markdown { color: #E06C75; } +.z-punctuation.z-definition.z-bold.z-markdown { color: #D19A66; } +.z-markup.z-inline.z-raw.z-markdown { color: #98C379; } +.z-punctuation.z-definition.z-raw.z-markdown { color: #E5C07B; } +.z-punctuation.z-definition.z-list.z-markdown { color: #E5C07B; } +.z-punctuation.z-definition.z-metadata.z-markdown { color: #E06C75; } +.z-punctuation.z-definition.z-metadata.z-markdown { color: #E06C75; } +.z-markup.z-underline.z-link.z-markdown { color: #C678DD; } +.z-markup.z-raw.z-monospace.z-asciidoc { color: #98C379; } +.z-markup.z-other.z-url.z-asciidoc { color: #C678DD; } +.z-markup.z-other.z-url.z-asciidoc { color: #61AFEF; } +.z-punctuation.z-section.z-embedded.z-begin { color: #C678DD; } +.z-punctuation.z-section.z-embedded.z-end { color: #C678DD; } +.z-invalid.z-illegal.z-bad-ampersand.z-html { color: #ABB2BF; } +.z-invalid.z-illegal.z-unrecognized-tag.z-html { color: #E06C75; } +.z-support.z-type.z-property-name.z-json { color: #E06C75; } +.z-entity.z-other.z-alias.z-php { color: #E5C07B; } +.z-keyword.z-operator.z-error-control.z-php { color: #C678DD; } +.z-keyword.z-operator.z-type.z-php { color: #C678DD; } +.z-invalid.z-illegal.z-non-null-typehinted.z-php { color: #F44747; } +.z-keyword.z-other.z-type.z-php { color: #E5C07B; } +.z-meta.z-function-call.z-object.z-php { color: #61AFEF; } +.z-meta.z-function-call.z-static.z-php { color: #61AFEF; } +.z-punctuation.z-separator.z-delimiter.z-php { color: #ABB2BF; } +.z-punctuation.z-terminator.z-expression.z-php { color: #ABB2BF; } +.z-support.z-constant.z-core.z-rust { color: #D19A66; } +.z-support.z-constant.z-ext.z-php { color: #D19A66; } +.z-support.z-constant.z-std.z-php { color: #D19A66; } +.z-support.z-constant.z-core.z-php { color: #D19A66; } +.z-support.z-constant.z-parser-token.z-php { color: #D19A66; } +.z-entity.z-name.z-goto-label.z-php { color: #61AFEF; } +.z-keyword.z-operator.z-logical.z-php { color: #56B6C2; } +.z-keyword.z-operator.z-bitwise.z-php { color: #56B6C2; } +.z-keyword.z-operator.z-arithmetic.z-php { color: #56B6C2; } +.z-keyword.z-operator.z-regexp.z-php { color: #C678DD; } +.z-keyword.z-operator.z-comparison.z-php { color: #56B6C2; } +.z-keyword.z-operator.z-heredoc.z-php { color: #C678DD; } +.z-keyword.z-operator.z-nowdoc.z-php { color: #C678DD; } +.z-meta.z-function.z-decorator.z-python { color: #61AFEF; } +.z-support.z-token.z-decorator.z-python { color: #56B6C2; } +.z-constant.z-language.z-symbol.z-ruby { color: #56B6C2; } +.z-support.z-type.z-primitive.z-ts { color: #E5C07B; } +.z-support.z-type.z-builtin.z-ts { color: #E5C07B; } +.z-support.z-type.z-primitive.z-tsx { color: #E5C07B; } +.z-support.z-type.z-builtin.z-tsx { color: #E5C07B; } +.z-punctuation.z-definition.z-template-expression.z-begin { color: #C678DD; } +.z-punctuation.z-definition.z-template-expression.z-end { color: #C678DD; } +.z-support.z-type.z-type.z-flowtype { color: #61AFEF; } +.z-variable.z-parameter.z-function.z-js { color: #E06C75; } +.z-keyword.z-other.z-template.z-begin { color: #98C379; } +.z-keyword.z-other.z-template.z-end { color: #98C379; } +.z-keyword.z-other.z-substitution.z-begin { color: #98C379; } +.z-keyword.z-other.z-substitution.z-end { color: #98C379; } +.z-keyword.z-operator.z-assignment.z-go { color: #E5C07B; } +.z-keyword.z-operator.z-arithmetic.z-go { color: #C678DD; } +.z-keyword.z-operator.z-address.z-go { color: #C678DD; } +.z-keyword.z-operator.z-arithmetic.z-c { color: #C678DD; } +.z-keyword.z-operator.z-arithmetic.z-cpp { color: #C678DD; } +.z-entity.z-name.z-package.z-go { color: #E5C07B; } +.z-support.z-type.z-prelude.z-elm { color: #56B6C2; } +.z-entity.z-other.z-attribute-name.z-pseudo-element { color: #56B6C2; } +.z-entity.z-other.z-attribute-name.z-pseudo-class { color: #56B6C2; } +.z-variable.z-parameter.z-function.z-coffee { color: #E06C75; } +.z-meta.z-scope.z-prerequisites.z-makefile { color: #E06C75; } +.z-storage.z-modifier.z-import.z-groovy { color: #E5C07B; } +.z-support.z-variable.z-semantic.z-hlsl { color: #E5C07B; } +.z-support.z-type.z-texture.z-hlsl { color: #C678DD; } +.z-support.z-type.z-sampler.z-hlsl { color: #C678DD; } +.z-support.z-type.z-object.z-hlsl { color: #C678DD; } +.z-support.z-type.z-fx.z-hlsl { color: #C678DD; } +.z-support.z-type.z-object.z-hlsl { color: #C678DD; } +.z-support.z-type.z-vb.z-asp { color: #E5C07B; } +.z-entity.z-name.z-function.z-xi { color: #E5C07B; } +.z-entity.z-name.z-class.z-xi { color: #56B6C2; } +.z-punctuation.z-definition.z-tag.z-xi { color: #5C6370; } +.z-entity.z-name.z-label.z-cs { color: #E5C07B; } +.z-entity.z-name.z-label.z-cs { color: #E06C75; } +.z-constant.z-language.z-symbol.z-elixir { color: #56B6C2; } +.z-support.z-other.z-namespace.z-php { color: #ABB2BF; } +.z-variable.z-parameter.z-function.z-latex { color: #E06C75; } +.z-variable.z-other.z-constant.z-property { color: #E06C75; } +.z-variable.z-other.z-readwrite.z-c { color: #E06C75; } +.z-punctuation.z-separator.z-colon.z-php { color: #ABB2BF; } +.z-support.z-other.z-parenthesis.z-regexp { color: #D19A66; } +.z-keyword.z-operator.z-expression.z-is { color: #C678DD; } +.z-constant.z-character.z-math.z-tex { color: #98C379; } +.z-entity.z-other.z-attribute-name.z-js { font-style: italic; } +.z-entity.z-other.z-attribute-name.z-ts { font-style: italic; } +.z-entity.z-other.z-attribute-name.z-jsx { font-style: italic; } +.z-entity.z-other.z-attribute-name.z-tsx { font-style: italic; } +.z-entity.z-name.z-tag.z-laravel-blade { color: #C678DD; } +.z-punctuation.z-definition.z-list.z-begin.z-unison { color: #E06C75; } +.z-punctuation.z-definition.z-list.z-end.z-unison { color: #E06C75; } +.z-punctuation.z-definition.z-ability.z-begin.z-unison { color: #E06C75; } +.z-punctuation.z-definition.z-ability.z-end.z-unison { color: #E06C75; } +.z-punctuation.z-operator.z-assignment.z-as.z-unison { color: #E06C75; } +.z-storage.z-type.z-object.z-array.z-java { color: #E5C07B; } +.z-punctuation.z-section.z-block.z-begin.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-block.z-end.z-java { color: #ABB2BF; } +.z-punctuation.z-definition.z-method-parameters.z-begin.z-java { color: #ABB2BF; } +.z-punctuation.z-definition.z-method-parameters.z-end.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-method.z-begin.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-method.z-end.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-class.z-begin.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-class.z-end.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-inner-class.z-begin.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-inner-class.z-end.z-java { color: #ABB2BF; } +.z-meta.z-definition.z-variable.z-name.z-java { color: #E06C75; } +.z-support.z-constant.z-color.z-w3c-standard-color-name.z-css { color: #D19A66; } +.z-support.z-constant.z-color.z-w3c-standard-color-name.z-scss { color: #D19A66; } +.z-punctuation.z-separator.z-list.z-comma.z-css { color: #ABB2BF; } +.z-support.z-constant.z-color.z-w3c-standard-color-name.z-css { color: #D19A66; } +.z-support.z-type.z-vendored.z-property-name.z-css { color: #56B6C2; } +.z-keyword.z-operator.z-bitwise.z-shift.z-c { color: #C678DD; } +.z-keyword.z-operator.z-bitwise.z-shift.z-cpp { color: #C678DD; } +.z-variable.z-parameter.z-function.z-language.z-python { color: #D19A66; } +.z-punctuation.z-definition.z-arguments.z-begin.z-python { color: #ABB2BF; } +.z-punctuation.z-definition.z-arguments.z-end.z-python { color: #ABB2BF; } +.z-punctuation.z-definition.z-list.z-begin.z-python { color: #ABB2BF; } +.z-punctuation.z-definition.z-list.z-end.z-python { color: #ABB2BF; } +.z-keyword.z-operator.z-assignment.z-compound.z-js { color: #56B6C2; } +.z-keyword.z-operator.z-assignment.z-compound.z-ts { color: #56B6C2; } +.z-support.z-type.z-property-name.z-table.z-toml { color: #E06C75; } +.z-support.z-type.z-property-name.z-array.z-toml { color: #E06C75; } +.z-entity.z-other.z-attribute-name.z-class.z-css { color: #D19A66; } +.z-punctuation.z-definition.z-list.z-begin.z-markdown { color: #E5C07B; } +.z-markup.z-inline.z-raw.z-string.z-markdown { color: #98C379; } +.z-punctuation.z-definition.z-string.z-begin.z-markdown { color: #E06C75; } +.z-punctuation.z-definition.z-string.z-end.z-markdown { color: #E06C75; } +.z-beginning.z-punctuation.z-definition.z-list.z-markdown { color: #E06C75; } +.z-markup.z-underline.z-link.z-image.z-markdown { color: #C678DD; } +.z-string.z-other.z-link.z-title.z-markdown { color: #61AFEF; } +.z-string.z-other.z-link.z-description.z-markdown { color: #61AFEF; } +.z-support.z-other.z-namespace.z-use.z-php { color: #E5C07B; } +.z-support.z-other.z-namespace.z-use-as.z-php { color: #E5C07B; } +.z-punctuation.z-section.z-array.z-begin.z-php { color: #ABB2BF; } +.z-punctuation.z-section.z-array.z-end.z-php { color: #ABB2BF; } +.z-meta.z-other.z-type.z-phpdoc.z-php { color: #E5C07B; } +.z-keyword.z-other.z-array.z-phpdoc.z-php { color: #E5C07B; } +.z-punctuation.z-section.z-scope.z-begin.z-php { color: #ABB2BF; } +.z-punctuation.z-section.z-scope.z-end.z-php { color: #ABB2BF; } +.z-meta.z-function.z-decorator.z-identifier.z-python { color: #56B6C2; } +.z-constant.z-language.z-symbol.z-hashkey.z-ruby { color: #56B6C2; } +.z-entity.z-name.z-variable.z-local.z-cs { color: #E06C75; } +.z-meta.z-definition.z-variable.z-name.z-groovy { color: #E06C75; } +.z-support.z-type.z-object.z-rw.z-hlsl { color: #C678DD; } +.z-constant.z-character.z-character-class.z-regexp.z-xi { color: #E06C75; } +.z-constant.z-other.z-color.z-rgb-value.z-xi { color: #FFFFFF; } +.z-entity.z-name.z-scope-resolution.z-function.z-call { color: #E5C07B; } +.z-entity.z-name.z-scope-resolution.z-function.z-definition { color: #E5C07B; } +.z-markup.z-heading.z-setext.z-\31 .z-markdown { color: #E06C75; } +.z-markup.z-heading.z-setext.z-\32 .z-markdown { color: #E06C75; } +.z-constant.z-language.z-symbol.z-double-quoted.z-elixir { color: #56B6C2; } +.z-entity.z-name.z-variable.z-parameter.z-cs { color: #E5C07B; } +.z-entity.z-name.z-variable.z-field.z-cs { color: #E06C75; } +.z-punctuation.z-section.z-embedded.z-begin.z-php { color: #BE5046; } +.z-punctuation.z-section.z-embedded.z-end.z-php { color: #BE5046; } +.z-entity.z-name.z-variable.z-parameter.z-php { color: #ABB2BF; } +.z-constant.z-numeric.z-decimal.z-asm.z-x86_64 { color: #C678DD; } +.z-constant.z-other.z-general.z-math.z-tex { color: #61AFEF; } +.z-constant.z-character.z-format.z-placeholder.z-other.z-python { color: #D19A66; } +.z-entity.z-name.z-class.z-identifier.z-namespace.z-type { color: #E5C07B; } +.z-invalid.z-deprecated.z-entity.z-other.z-attribute-name.z-html { color: #D19A66; } +.z-punctuation.z-definition.z-begin.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-end.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-begin.z-bracket.z-curly.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-end.z-bracket.z-curly.z-php { color: #ABB2BF; } +.z-meta.z-definition.z-class.z-inherited.z-classes.z-groovy { color: #98C379; } +.z-beginning.z-punctuation.z-definition.z-quote.z-markdown.z-xi { color: #98C379; } +.z-beginning.z-punctuation.z-definition.z-list.z-markdown.z-xi { color: #7F848E; } +.z-punctuation.z-definition.z-block.z-sequence.z-item.z-yaml { color: #ABB2BF; } +.z-support.z-class.z-math.z-block.z-environment.z-latex { color: #61AFEF; } +.z-variable.z-parameter.z-function.z-language.z-special.z-self.z-python { color: #E5C07B; } +.z-variable.z-parameter.z-function.z-language.z-special.z-cls.z-python { color: #E5C07B; } +.z-punctuation.z-section.z-block.z-begin.z-bracket.z-curly.z-cpp { color: #ABB2BF; } +.z-punctuation.z-section.z-block.z-end.z-bracket.z-curly.z-cpp { color: #ABB2BF; } +.z-punctuation.z-section.z-block.z-begin.z-bracket.z-curly.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-block.z-end.z-bracket.z-curly.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-parens.z-begin.z-bracket.z-round.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-parens.z-end.z-bracket.z-round.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-parameters.z-begin.z-bracket.z-round.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-parameters.z-end.z-bracket.z-round.z-c { color: #ABB2BF; } +.z-punctuation.z-section.z-class.z-begin.z-bracket.z-curly.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-class.z-end.z-bracket.z-curly.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-method.z-begin.z-bracket.z-curly.z-java { color: #ABB2BF; } +.z-punctuation.z-section.z-method.z-end.z-bracket.z-curly.z-java { color: #ABB2BF; } +.z-punctuation.z-definition.z-parameters.z-begin.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-parameters.z-end.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-arguments.z-begin.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-arguments.z-end.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-storage-type.z-begin.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-storage-type.z-end.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-array.z-begin.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-array.z-end.z-bracket.z-round.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-section.z-switch-block.z-end.z-bracket.z-curly.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-section.z-switch-block.z-start.z-bracket.z-curly.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-section.z-switch-block.z-begin.z-bracket.z-curly.z-php { color: #ABB2BF; } +.z-punctuation.z-definition.z-section.z-switch-block.z-end.z-bracket.z-curly.z-php { color: #ABB2BF; } diff --git a/static/giallo-light.css b/static/giallo-light.css new file mode 100644 index 000000000..ed0726407 --- /dev/null +++ b/static/giallo-light.css @@ -0,0 +1,370 @@ +/* + * theme "one-light" generated by giallo + */ + +.z-code { + color: #383A42; + background-color: #FAFAFA; +} + +.z-hl { + background-color: #383A420C; +} + +.giallo-ln { + color: #9D9D9F; +} + +.z-comment { color: #A0A1A7; font-style: italic; } +.z-keyword { color: #A626A4; } +.z-storage { color: #A626A4; } +.z-constant { color: #986801; } +.z-variable { color: #E45649; } +.z-string { color: #50A14F; } +.z-none { color: #383A42; } +.z-customrule { color: #383A42; } +.z-source { color: #383A42; } +.z-embedded { color: #383A42; } +.z-property-name { color: #696C77; } +.z-property-value { color: #696C77; } +.z-quoted { color: #50A14F; } +.z-separator { color: #986801; } +.z-array { color: #696C77; } +.z-scope { color: #696C77; } +.z-section { color: #696C77; } +.z-function { color: #E45649; } +.z-bool { color: #986801; } +.z-bracket { color: #383A42; } +.z-attribute-name { color: #C18401; } +.z-string { color: #A626A4; } +.z-punctuation { color: #50A14F; } +.z-punctuation { color: #E45649; } +.z-variable { color: #383A42; } +.z-variable { color: #383A42; } +.z-variable { color: #383A42; } +.z-variable { color: #383A42; } +.z-variable { color: #C18401; } +.z-variable { color: #C18401; } +.z-variable { color: #C18401; } +.z-variable { color: #C18401; } +.z-constant { color: #986801; } +.z-storage { color: #986801; } +.z-punctuation { color: #E45649; } +.z-entity { color: #4078F2; } +.z-punctuation { color: #383A42; } +.z-punctuation { color: #50A14F; } +.z-punctuation { color: #50A14F; } +.z-keyword.z-control { color: #A626A4; } +.z-keyword.z-operator { color: #383A42; } +.z-constant.z-variable { color: #986801; } +.z-constant.z-numeric { color: #986801; } +.z-variable.z-interpolation { color: #CA1243; } +.z-variable.z-parameter { color: #383A42; } +.z-string.z-regexp { color: #0184BC; } +.z-support.z-class { color: #C18401; } +.z-support.z-type { color: #0184BC; } +.z-support.z-function { color: #0184BC; } +.z-meta.z-class { color: #C18401; } +.z-meta.z-method-call { color: #383A42; } +.z-meta.z-method { color: #383A42; } +.z-meta.z-link { color: #986801; } +.z-meta.z-require { color: #4078F2; } +.z-meta.z-selector { color: #A626A4; } +.z-meta.z-separator { color: #383A42; } +.z-meta.z-tag { color: #383A42; } +.z-invalid.z-deprecated { color: #000000; background-color: #F2A60D; } +.z-invalid.z-illegal { color: #FFFFFF; background-color: #FF1414; } +.z-markup.z-bold { color: #986801; font-weight: bold; } +.z-markup.z-changed { color: #A626A4; } +.z-markup.z-deleted { color: #E45649; } +.z-markup.z-italic { color: #A626A4; font-style: italic; } +.z-markup.z-heading { color: #E45649; } +.z-markup.z-link { color: #0184BC; } +.z-markup.z-inserted { color: #50A14F; } +.z-markup.z-quote { color: #986801; } +.z-markup.z-raw { color: #50A14F; } +.z-ng.z-interpolation { color: #E45649; } +.z-ng.z-pipe { color: #383A42; } +.z-ng.z-operator { color: #383A42; } +.z-ng.z-tag { color: #0184BC; } +.z-support.z-constant { color: #986801; } +.z-markup.z-link { color: #A0A1A7; } +.z-keyword.z-operator { color: #A626A4; } +.z-keyword.z-operator { color: #A626A4; } +.z-keyword.z-operator { color: #A626A4; } +.z-property-name.z-support { color: #383A42; } +.z-property-value.z-support { color: #383A42; } +.z-constant.z-language { color: #4078F2; } +.z-constant.z-numeric { color: #4078F2; } +.z-constant.z-definition { color: #4078F2; } +.z-variable.z-definition { color: #A626A4; } +.z-variable.z-anonymous { color: #A626A4; } +.z-keyword.z-special-method { color: #E45649; } +.z-embedded.z-section { color: #E45649; } +.z-regexp.z-section { color: #CA1243; } +.z-regexp.z-string { color: #CA1243; } +.z-keyword.z-operator { color: #986801; } +.z-variable.z-constant { color: #C18401; } +.z-storage.z-type { color: #C18401; } +.z-meta.z-key-pair { color: #E45649; } +.z-keyword.z-operator { color: #0184BC; } +.z-keyword.z-operator { color: #0184BC; } +.z-keyword.z-operator { color: #0184BC; } +.z-function.z-begin { color: #4078F2; } +.z-function.z-end { color: #4078F2; } +.z-string.z-begin { color: #383A42; } +.z-string.z-end { color: #383A42; } +.z-class.z-bracket { color: #383A42; } +.z-variable.z-parameter { color: #986801; } +.z-keyword.z-operator { color: #0184BC; } +.z-keyword.z-operator { color: #0184BC; } +.z-punctuation.z-definition { color: #CA1243; } +.z-punctuation.z-definition { color: #CA1243; } +.z-punctuation.z-accessor { color: #383A42; } +.z-punctuation.z-accessor { color: #383A42; } +.z-punctuation.z-accessor { color: #383A42; } +.z-support.z-variable { color: #E45649; } +.z-support.z-variable { color: #E45649; } +.z-support.z-variable { color: #E45649; } +.z-keyword.z-operator { color: #0184BC; } +.z-keyword.z-operator { color: #0184BC; } +.z-support.z-type { color: #383A42; } +.z-support.z-type { color: #383A42; } +.z-support.z-type { color: #383A42; } +.z-support.z-type { color: #383A42; } +.z-constant.z-other { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-punctuation.z-accessor { color: #50A14F; } +.z-punctuation.z-accessor { color: #50A14F; } +.z-punctuation.z-accessor { color: #50A14F; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-keyword.z-operator { color: #383A42; } +.z-punctuation.z-definition { color: #50A14F; } +.z-punctuation.z-definition { color: #50A14F; } +.z-punctuation.z-definition { color: #50A14F; } +.z-punctuation.z-definition { color: #50A14F; } +.z-punctuation.z-string { color: #E45649; } +.z-entity.z-name.z-type { color: #C18401; } +.z-entity.z-other.z-inherited-class { color: #C18401; } +.z-keyword.z-other.z-special-method { color: #4078F2; } +.z-keyword.z-other.z-unit { color: #986801; } +.z-storage.z-type.z-annotation { color: #A626A4; } +.z-storage.z-type.z-primitive { color: #A626A4; } +.z-storage.z-modifier.z-package { color: #383A42; } +.z-storage.z-modifier.z-import { color: #383A42; } +.z-constant.z-character.z-escape { color: #0184BC; } +.z-constant.z-other.z-color { color: #0184BC; } +.z-constant.z-other.z-symbol { color: #0184BC; } +.z-string.z-other.z-link { color: #E45649; } +.z-punctuation.z-definition.z-comment { color: #A0A1A7; } +.z-punctuation.z-definition.z-method-parameters { color: #383A42; } +.z-punctuation.z-definition.z-function-parameters { color: #383A42; } +.z-punctuation.z-definition.z-parameters { color: #383A42; } +.z-punctuation.z-definition.z-separator { color: #383A42; } +.z-punctuation.z-definition.z-seperator { color: #383A42; } +.z-punctuation.z-definition.z-array { color: #383A42; } +.z-punctuation.z-definition.z-heading { color: #4078F2; } +.z-punctuation.z-definition.z-identity { color: #4078F2; } +.z-punctuation.z-definition.z-bold { color: #C18401; font-weight: bold; } +.z-punctuation.z-definition.z-italic { color: #A626A4; font-style: italic; } +.z-punctuation.z-section.z-embedded { color: #CA1243; } +.z-punctuation.z-section.z-method { color: #383A42; } +.z-punctuation.z-section.z-class { color: #383A42; } +.z-punctuation.z-section.z-inner-class { color: #383A42; } +.z-support.z-function.z-any-method { color: #4078F2; } +.z-entity.z-name.z-function { color: #4078F2; } +.z-entity.z-name.z-class { color: #C18401; } +.z-entity.z-name.z-section { color: #4078F2; } +.z-entity.z-name.z-tag { color: #E45649; } +.z-entity.z-other.z-attribute-name { color: #986801; } +.z-meta.z-class.z-body { color: #383A42; } +.z-meta.z-definition.z-variable { color: #E45649; } +.z-ng.z-interpolation.z-begin { color: #4078F2; } +.z-ng.z-interpolation.z-end { color: #4078F2; } +.z-support.z-type.z-property-name { color: #383A42; } +.z-meta.z-array-binding-pattern-variable.z-js { color: #383A42; } +.z-meta.z-array-binding-pattern-variable.z-ts { color: #383A42; } +.z-meta.z-array-binding-pattern-variable.z-tsx { color: #383A42; } +.z-meta.z-class.z-js { color: #383A42; } +.z-meta.z-class.z-ts { color: #383A42; } +.z-meta.z-class.z-jsx { color: #383A42; } +.z-meta.z-class.z-tsx { color: #383A42; } +.z-constant.z-language.z-json { color: #0184BC; } +.z-storage.z-type.z-cs { color: #C18401; } +.z-meta.z-attribute.z-rust { color: #986801; } +.z-markup.z-heading.z-setext { color: #383A42; } +.z-markup.z-quote.z-markdown { color: #A0A1A7; font-style: italic; } +.z-meta.z-class.z-php { color: #383A42; } +.z-source.z-ruby.z-embedded { color: #C18401; } +.z-punctuation.z-definition.z-heading { color: #4078F2; } +.z-source.z-embedded.z-source { color: #383A42; } +.z-parameter.z-variable.z-function { color: #986801; font-style: italic; } +.z-embedded.z-source.z-empty { color: #E45649; } +.z-storage.z-type.z-string { color: #A626A4; } +.z-storage.z-modifier.z-import { color: #C18401; } +.z-keyword.z-operator.z-instanceof { color: #A626A4; } +.z-keyword.z-operator.z-delete { color: #A626A4; } +.z-keyword.z-operator.z-in { color: #A626A4; } +.z-keyword.z-operator.z-of { color: #A626A4; } +.z-keyword.z-operator.z-instanceof { color: #A626A4; } +.z-keyword.z-operator.z-new { color: #A626A4; } +.z-keyword.z-operator.z-typeof { color: #A626A4; } +.z-keyword.z-operator.z-void { color: #A626A4; } +.z-punctuation.z-definition.z-typeparameters { color: #383A42; } +.z-punctuation.z-definition.z-typeparameters { color: #383A42; } +.z-punctuation.z-definition.z-typeparameters { color: #383A42; } +.z-punctuation.z-definition.z-block { color: #383A42; } +.z-punctuation.z-definition.z-block { color: #383A42; } +.z-punctuation.z-definition.z-block { color: #383A42; } +.z-punctuation.z-separator.z-comma { color: #383A42; } +.z-punctuation.z-separator.z-comma { color: #383A42; } +.z-punctuation.z-separator.z-comma { color: #383A42; } +.z-punctuation.z-section.z-embedded { color: #383A42; } +.z-punctuation.z-section.z-embedded { color: #383A42; } +.z-punctuation.z-section.z-embedded { color: #383A42; } +.z-punctuation.z-section.z-embedded { color: #383A42; } +.z-string.z-quoted.z-json { color: #E45649; } +.z-constant.z-language.z-json { color: #0184BC; } +.z-constant.z-language.z-json { color: #0184BC; } +.z-string.z-quoted.z-json { color: #50A14F; } +.z-string.z-quoted.z-json { color: #50A14F; } +.z-entity.z-name.z-type.z-class { color: #C18401; } +.z-entity.z-other.z-attribute-name.z-id { color: #4078F2; } +.z-support.z-type.z-property-name.z-json { color: #E45649; } +.z-punctuation.z-separator.z-key-value.z-ts { color: #0184BC; } +.z-punctuation.z-separator.z-key-value.z-js { color: #0184BC; } +.z-punctuation.z-separator.z-key-value.z-tsx { color: #0184BC; } +.z-variable.z-other.z-readwrite.z-js { color: #383A42; } +.z-variable.z-other.z-readwrite.z-ts { color: #383A42; } +.z-variable.z-other.z-readwrite.z-tsx { color: #383A42; } +.z-support.z-variable.z-dom.z-js { color: #E45649; } +.z-support.z-variable.z-dom.z-ts { color: #E45649; } +.z-support.z-variable.z-property.z-js { color: #E45649; } +.z-support.z-variable.z-property.z-ts { color: #E45649; } +.z-support.z-variable.z-property.z-tsx { color: #E45649; } +.z-keyword.z-control.z-default.z-js { color: #E45649; } +.z-keyword.z-control.z-default.z-ts { color: #E45649; } +.z-keyword.z-control.z-default.z-tsx { color: #E45649; } +.z-meta.z-brace.z-round.z-js { color: #383A42; } +.z-meta.z-brace.z-square.z-js { color: #383A42; } +.z-meta.z-brace.z-round.z-ts { color: #383A42; } +.z-meta.z-brace.z-square.z-ts { color: #383A42; } +.z-meta.z-brace.z-round.z-tsx { color: #383A42; } +.z-meta.z-brace.z-square.z-tsx { color: #383A42; } +.z-punctuation.z-terminator.z-statement.z-js { color: #383A42; } +.z-punctuation.z-terminator.z-statement.z-ts { color: #383A42; } +.z-punctuation.z-terminator.z-statement.z-tsx { color: #383A42; } +.z-keyword.z-operator.z-new.z-ts { color: #A626A4; } +.z-keyword.z-operator.z-new.z-j { color: #A626A4; } +.z-keyword.z-operator.z-new.z-tsx { color: #A626A4; } +.z-punctuation.z-separator.z-parameter.z-js { color: #383A42; } +.z-punctuation.z-separator.z-parameter.z-ts { color: #383A42; } +.z-punctuation.z-separator.z-parameter.z-tsx { color: #383A42; } +.z-constant.z-language.z-import-export-all.z-js { color: #E45649; } +.z-constant.z-language.z-import-export-all.z-ts { color: #E45649; } +.z-constant.z-language.z-import-export-all.z-jsx { color: #0184BC; } +.z-constant.z-language.z-import-export-all.z-tsx { color: #0184BC; } +.z-keyword.z-control.z-as.z-js { color: #383A42; } +.z-keyword.z-control.z-as.z-ts { color: #383A42; } +.z-keyword.z-control.z-as.z-jsx { color: #383A42; } +.z-keyword.z-control.z-as.z-tsx { color: #383A42; } +.z-variable.z-other.z-constant.z-js { color: #986801; } +.z-variable.z-other.z-constant.z-ts { color: #986801; } +.z-variable.z-other.z-constant.z-jsx { color: #986801; } +.z-variable.z-other.z-constant.z-tsx { color: #986801; } +.z-storage.z-type.z-property.z-js { color: #0184BC; } +.z-storage.z-type.z-property.z-jsx { color: #0184BC; } +.z-storage.z-type.z-property.z-ts { color: #0184BC; } +.z-storage.z-type.z-property.z-tsx { color: #0184BC; } +.z-variable.z-other.z-object.z-js { color: #383A42; } +.z-variable.z-other.z-object.z-ts { color: #383A42; } +.z-meta.z-object-literal.z-key.z-js { color: #E45649; } +.z-meta.z-object-literal.z-key.z-ts { color: #E45649; } +.z-support.z-variable.z-magic.z-python { color: #E45649; } +.z-meta.z-function.z-parameters.z-python { color: #986801; } +.z-punctuation.z-separator.z-annotation.z-python { color: #383A42; } +.z-punctuation.z-separator.z-parameters.z-python { color: #383A42; } +.z-variable.z-other.z-readwrite.z-cs { color: #383A42; } +.z-variable.z-other.z-object.z-cs { color: #383A42; } +.z-keyword.z-other.z-unsafe.z-rust { color: #A626A4; } +.z-entity.z-name.z-type.z-rust { color: #0184BC; } +.z-storage.z-modifier.z-lifetime.z-rust { color: #383A42; } +.z-entity.z-name.z-lifetime.z-rust { color: #986801; } +.z-storage.z-type.z-core.z-rust { color: #0184BC; } +.z-storage.z-class.z-std.z-rust { color: #0184BC; } +.z-markup.z-raw.z-block.z-markdown { color: #383A42; } +.z-punctuation.z-definition.z-variable.z-shell { color: #E45649; } +.z-support.z-constant.z-property-value.z-css { color: #383A42; } +.z-punctuation.z-definition.z-constant.z-css { color: #986801; } +.z-punctuation.z-separator.z-key-value.z-scss { color: #E45649; } +.z-punctuation.z-definition.z-constant.z-scss { color: #986801; } +.z-entity.z-name.z-section.z-markdown { color: #E45649; } +.z-punctuation.z-definition.z-heading.z-markdown { color: #E45649; } +.z-punctuation.z-definition.z-bold.z-markdown { color: #986801; } +.z-markup.z-inline.z-raw.z-markdown { color: #50A14F; } +.z-punctuation.z-definition.z-metadata.z-markdown { color: #383A42; } +.z-punctuation.z-definition.z-metadata.z-markdown { color: #A626A4; } +.z-markup.z-underline.z-link.z-markdown { color: #A626A4; } +.z-punctuation.z-separator.z-variable.z-ruby { color: #E45649; } +.z-variable.z-other.z-constant.z-ruby { color: #986801; } +.z-keyword.z-operator.z-other.z-ruby { color: #50A14F; } +.z-punctuation.z-definition.z-variable.z-php { color: #E45649; } +.z-keyword.z-other.z-definition.z-ini { color: #E45649; } +.z-keyword.z-operator.z-logical.z-python { color: #A626A4; } +.z-variable.z-other.z-readwrite.z-js { color: #986801; } +.z-variable.z-other.z-readwrite.z-ts { color: #986801; } +.z-variable.z-other.z-readwrite.z-tsx { color: #986801; } +.z-variable.z-other.z-readwrite.z-js { color: #E45649; } +.z-variable.z-other.z-readwrite.z-ts { color: #E45649; } +.z-punctuation.z-separator.z-key-value.z-scss { color: #383A42; } +.z-punctuation.z-definition.z-string.z-template { color: #50A14F; } +.z-punctuation.z-definition.z-string.z-template { color: #50A14F; } +.z-punctuation.z-definition.z-string.z-template { color: #50A14F; } +.z-punctuation.z-definition.z-string.z-template { color: #50A14F; } +.z-support.z-variable.z-property.z-dom.z-js { color: #E45649; } +.z-support.z-variable.z-property.z-dom.z-ts { color: #E45649; } +.z-keyword.z-operator.z-expression.z-instanceof.z-js { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-instanceof.z-ts { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-instanceof.z-tsx { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-of.z-js { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-of.z-ts { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-of.z-tsx { color: #A626A4; } +.z-variable.z-other.z-constant.z-property.z-js { color: #986801; } +.z-variable.z-other.z-constant.z-property.z-ts { color: #986801; } +.z-variable.z-other.z-constant.z-property.z-tsx { color: #986801; } +.z-variable.z-other.z-readwrite.z-alias.z-js { color: #E45649; } +.z-variable.z-other.z-readwrite.z-alias.z-ts { color: #E45649; } +.z-variable.z-other.z-readwrite.z-alias.z-jsx { color: #E45649; } +.z-variable.z-other.z-readwrite.z-alias.z-tsx { color: #E45649; } +.z-entity.z-name.z-type.z-module.z-js { color: #50A14F; } +.z-entity.z-name.z-type.z-module.z-ts { color: #50A14F; } +.z-entity.z-name.z-type.z-module.z-jsx { color: #50A14F; } +.z-entity.z-name.z-type.z-module.z-tsx { color: #50A14F; } +.z-support.z-type.z-object.z-module.z-js { color: #E45649; } +.z-support.z-type.z-object.z-module.z-jsx { color: #E45649; } +.z-support.z-type.z-object.z-module.z-ts { color: #E45649; } +.z-support.z-type.z-object.z-module.z-tsx { color: #E45649; } +.z-variable.z-other.z-constant.z-object.z-js { color: #986801; } +.z-variable.z-other.z-constant.z-object.z-jsx { color: #986801; } +.z-variable.z-other.z-constant.z-object.z-ts { color: #986801; } +.z-variable.z-other.z-constant.z-object.z-tsx { color: #986801; } +.z-keyword.z-operator.z-expression.z-in.z-js { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-in.z-jsx { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-in.z-ts { color: #A626A4; } +.z-keyword.z-operator.z-expression.z-in.z-tsx { color: #A626A4; } +.z-entity.z-name.z-variable.z-field.z-cs { color: #E45649; } +.z-variable.z-other.z-object.z-property.z-cs { color: #383A42; } +.z-entity.z-name.z-variable.z-property.z-cs { color: #4078F2; } +.z-storage.z-type.z-primitive.z-array.z-java { color: #C18401; } +.z-beginning.z-punctuation.z-definition.z-list.z-markdown { color: #E45649; } +.z-punctuation.z-definition.z-string.z-begin.z-markdown { color: #383A42; } +.z-punctuation.z-definition.z-string.z-end.z-markdown { color: #383A42; } +.z-markup.z-underline.z-link.z-image.z-markdown { color: #A626A4; } +.z-string.z-other.z-link.z-title.z-markdown { color: #4078F2; } +.z-string.z-other.z-link.z-description.z-markdown { color: #4078F2; } diff --git a/static/js/playground.js b/static/js/playground.js new file mode 100644 index 000000000..c67591b61 --- /dev/null +++ b/static/js/playground.js @@ -0,0 +1,175 @@ +/** + * Page wiring for the playground (/playground). + * + * wasm-terminal.js provides the terminal engine and the window.* helpers + * (initPlayground, runInTerminal, getLastCommand, …); this file connects them + * to the page chrome: the on-demand "Load" buttons, the "Copy share link" + * button, the locale dropdown, the available-commands list, the build-version + * footer and the clickable examples. + */ +document.addEventListener("DOMContentLoaded", function() { + initPlayground("wasm-playground"); + + // Build a "Load" button per optional standalone group (grep, find, + // diffutils, sed). These ship as their own WASM modules and load on demand + // to keep the initial page download light; running a command auto-loads its + // module too (e.g. diff/cmp both come from the diffutils module, and find + // loads find/locate/updatedb together). + var loaderBar = document.getElementById("playground-loaders"); + if (loaderBar && Array.isArray(window.uutilsPrograms)) { + window.uutilsPrograms.forEach(function(prog) { + var btn = document.createElement("button"); + btn.className = "playground-loader"; + var markLoaded = function() { + btn.disabled = true; + btn.classList.add("loaded"); + btn.textContent = "✓ " + prog + " loaded"; + }; + var setIdleLabel = function(size) { + btn.textContent = "Load " + prog + (size ? " (" + (size / 1024 / 1024).toFixed(1) + " MB)" : ""); + }; + setIdleLabel(0); + window.programSize(prog).then(function(size) { + if (!btn.classList.contains("loaded") && !btn.disabled) setIdleLabel(size); + }); + btn.addEventListener("click", function() { + if (btn.disabled) return; + btn.disabled = true; + btn.textContent = "Loading " + prog + "…"; + window.loadProgram(prog).then(function(mod) { + if (mod) { + markLoaded(); + } else { + btn.disabled = false; + btn.textContent = prog + " unavailable"; + } + }); + }); + // Keep the button in sync once every module it covers is loaded by + // running a command (a group like "find" backs find/locate/updatedb). + document.addEventListener("uutils:program-loaded", function(e) { + if (e.detail && window.isProgramLoaded(prog)) markLoaded(); + }); + if (window.isProgramLoaded(prog)) markLoaded(); + loaderBar.appendChild(btn); + }); + } + + // "Copy share link" button: builds a ?cmd= URL to the most recent command + // so it can be shared - the link reruns that command on page load. + var shareBtn = document.getElementById("playground-share-btn"); + var shareHint = document.getElementById("playground-share-hint"); + if (shareBtn) { + var shareResetTimer = null; + var buildShareUrl = function(cmd) { + var url = new URL(window.location.href); + url.search = ""; + url.hash = ""; + url.searchParams.set("cmd", cmd); + return url.toString(); + }; + // Enable the button as soon as a command has been run. + document.addEventListener("uutils:command-run", function(e) { + shareBtn.disabled = false; + if (shareHint && !shareBtn.classList.contains("copied")) { + shareHint.textContent = "Shares: " + e.detail.command; + } + }); + shareBtn.addEventListener("click", function() { + var cmd = window.getLastCommand ? window.getLastCommand() : ""; + if (!cmd) return; + var link = buildShareUrl(cmd); + navigator.clipboard.writeText(link).then(function() { + shareBtn.classList.add("copied"); + shareBtn.textContent = "✓ Link copied!"; + if (shareHint) shareHint.textContent = link; + if (shareResetTimer) clearTimeout(shareResetTimer); + shareResetTimer = setTimeout(function() { + shareBtn.classList.remove("copied"); + shareBtn.textContent = "🔗 Copy share link"; + if (shareHint) shareHint.textContent = "Shares: " + cmd; + }, 2000); + }); + }); + } + + // Populate the locale dropdown from the build-generated list + if (typeof WASM_LOCALES !== "undefined") { + var sel = document.getElementById("locale-select"); + WASM_LOCALES.forEach(function(loc) { + if (loc === "en-US") return; // already the default option + var opt = document.createElement("option"); + opt.value = loc; + opt.textContent = loc; + sel.appendChild(opt); + }); + } + + // Populate the "Available commands" list from the build-generated list + if (typeof WASM_COMMANDS !== "undefined" && Array.isArray(WASM_COMMANDS)) { + var listEl = document.getElementById("wasm-commands-list"); + if (listEl) { + listEl.innerHTML = WASM_COMMANDS.slice().sort().map(function(c) { + return "" + c + ""; + }).join(" "); + } + } + + // Show the uutils commit that was used to build the WASM binary, + // and the uutils.github.io commit the site itself was built from. + var el = document.getElementById("playground-version"); + if (el) { + var parts = []; + if (typeof UUTILS_WASM_VERSION !== "undefined") { + var date = UUTILS_WASM_VERSION.date.split("T")[0]; + var url = "https://github.com/uutils/coreutils/commit/" + UUTILS_WASM_VERSION.commit; + parts.push('Built from uutils/coreutils ' + + UUTILS_WASM_VERSION.short + ' (' + date + ')'); + } + if (typeof UUTILS_GREP_VERSION !== "undefined") { + var grepDate = UUTILS_GREP_VERSION.date.split("T")[0]; + var grepUrl = "https://github.com/uutils/grep/commit/" + UUTILS_GREP_VERSION.commit; + parts.push('grep ' + + UUTILS_GREP_VERSION.short + ' (' + grepDate + ')'); + } + if (typeof UUTILS_FINDUTILS_VERSION !== "undefined") { + var findDate = UUTILS_FINDUTILS_VERSION.date.split("T")[0]; + var findUrl = "https://github.com/uutils/findutils/commit/" + UUTILS_FINDUTILS_VERSION.commit; + parts.push('findutils ' + + UUTILS_FINDUTILS_VERSION.short + ' (' + findDate + ')'); + } + if (typeof UUTILS_DIFFUTILS_VERSION !== "undefined") { + var diffDate = UUTILS_DIFFUTILS_VERSION.date.split("T")[0]; + var diffUrl = "https://github.com/uutils/diffutils/commit/" + UUTILS_DIFFUTILS_VERSION.commit; + parts.push('diffutils ' + + UUTILS_DIFFUTILS_VERSION.short + ' (' + diffDate + ')'); + } + if (typeof UUTILS_SED_VERSION !== "undefined") { + var sedDate = UUTILS_SED_VERSION.date.split("T")[0]; + var sedUrl = "https://github.com/uutils/sed/commit/" + UUTILS_SED_VERSION.commit; + parts.push('sed ' + + UUTILS_SED_VERSION.short + ' (' + sedDate + ')'); + } + if (typeof SITE_VERSION !== "undefined") { + var siteDate = SITE_VERSION.date.split("T")[0]; + var siteUrl = "https://github.com/uutils/uutils.github.io/commit/" + SITE_VERSION.commit; + parts.push('site ' + + SITE_VERSION.short + ' (' + siteDate + ')'); + } + el.innerHTML = parts.join(' · '); + } + + // Clickable examples: just run the command in the terminal. We deliberately + // leave the address bar alone — running an example shouldn't rewrite ?cmd= + // out from under the user. The "Copy share link" button is the way to turn + // the last command into a shareable ?cmd= URL. + document.querySelectorAll('.playground-example').forEach(function(btn) { + btn.addEventListener('click', function() { + var cmd = btn.textContent; + document.getElementById('wasm-playground').scrollIntoView({ behavior: 'smooth' }); + if (window.runInTerminal) { + window.runInTerminal(cmd); + } + }); + }); +}); diff --git a/static/js/wasm-example.js b/static/js/wasm-example.js new file mode 100644 index 000000000..cc172c6e5 --- /dev/null +++ b/static/js/wasm-example.js @@ -0,0 +1,56 @@ +/** + * Inline "Run" button handler for uutils code examples. + * Lightweight alternative to the full terminal playground. + */ + +(function () { + let runtimePromise = null; + + function ensureRuntime() { + if (runtimePromise) return runtimePromise; + runtimePromise = new Promise((resolve, reject) => { + if (document.querySelector('script[src="/js/wasm-terminal.js"]')) { + resolve(); + return; + } + const script = document.createElement("script"); + script.src = "/js/wasm-terminal.js"; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + return runtimePromise; + } + + async function runExample(button) { + const container = button.closest(".wasm-example"); + if (!container) return; + + const command = container.dataset.command; + const outputEl = container.querySelector(".wasm-example-output"); + const preEl = outputEl.querySelector("pre"); + + // Show loading state + button.disabled = true; + button.textContent = "Running..."; + outputEl.style.display = "block"; + preEl.textContent = "Loading..."; + + try { + await ensureRuntime(); + const output = await window.uutilsExecute(command); + preEl.textContent = output || "(no output)"; + } catch (e) { + preEl.textContent = "Error: " + e.message; + } + + button.disabled = false; + button.textContent = "Run"; + } + + // Use event delegation instead of inline onclick for CSP compatibility + document.addEventListener("click", function (e) { + const btn = e.target.closest(".wasm-run-btn"); + if (btn) runExample(btn); + }); +})(); diff --git a/static/js/wasm-terminal.js b/static/js/wasm-terminal.js new file mode 100644 index 000000000..f82434d9e --- /dev/null +++ b/static/js/wasm-terminal.js @@ -0,0 +1,1205 @@ +/** + * WASM Terminal Playground for uutils coreutils. + * + * Lazy-loads xterm.js and @bjorn3/browser_wasi_shim, then provides an + * interactive terminal where visitors can run uutils commands in the browser. + */ + +// The WASI shim checks `instanceof SharedArrayBuffer` in random_get. +// Without cross-origin isolation headers, SharedArrayBuffer is undefined +// in most browsers. Define a stub so the instanceof check returns false +// instead of throwing a ReferenceError. +if (typeof SharedArrayBuffer === "undefined") { + globalThis.SharedArrayBuffer = /** @type {any} */ (function () {}); +} + +const WASM_URL = "/wasm/uutils.wasm"; +// Some utilities ship as their own standalone WASM modules rather than as part +// of the coreutils multicall binary (grep lives in uutils/grep, find/locate/ +// updatedb in uutils/findutils, diff and cmp in uutils/diffutils, sed in +// uutils/sed). Each module is loaded on demand and is optional — see +// loadStandalone. A single module can provide several commands (diffutils → +// diff, cmp), which the binary dispatches on argv[0], so each command is invoked +// directly by its own name. findutils ships separate binaries, so each is its +// own module. (xargs is intentionally absent: it must spawn child processes, +// which the browser WASI sandbox can't do.) +const STANDALONE_MODULES = { + grep: { url: "/wasm/grep.wasm", commands: ["grep"] }, + find: { url: "/wasm/find.wasm", commands: ["find"] }, + locate: { url: "/wasm/locate.wasm", commands: ["locate"] }, + updatedb: { url: "/wasm/updatedb.wasm", commands: ["updatedb"] }, + diffutils: { url: "/wasm/diffutils.wasm", commands: ["diff", "cmp"] }, + sed: { url: "/wasm/sed.wasm", commands: ["sed"] }, +}; +// "Load" buttons present standalone modules under one label. findutils ships +// find, locate and updatedb as separate binaries but loads them together as a +// single "find" entry, so one click brings up all of findutils. +const STANDALONE_GROUPS = { + grep: ["grep"], + find: ["find", "locate", "updatedb"], + diffutils: ["diffutils"], + sed: ["sed"], +}; +// Shared locate database path inside the virtual FS (see the updatedb/locate +// handling in executeCommandLine). +const LOCATE_DB = "locatedb"; +// Map each command to the module that provides it (e.g. diff -> "diffutils"). +const STANDALONE_COMMAND_MODULE = Object.fromEntries( + Object.entries(STANDALONE_MODULES).flatMap( + ([mod, def]) => def.commands.map(cmd => [cmd, mod]) + ) +); +const XTERM_CSS = "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/css/xterm.min.css"; +const XTERM_CSS_INTEGRITY = "sha384-tStR1zLfWgsiXCF3IgfB3lBa8KmBe/lG287CL9WCeKgQYcp1bjb4/+mwN6oti4Co"; +const XTERM_JS = "https://cdn.jsdelivr.net/npm/@xterm/xterm@5.5.0/lib/xterm.min.js"; +const XTERM_JS_INTEGRITY = "sha384-J4qzUjBl1FxyLsl/kQPQIOeINsmp17OHYXDOMpMxlKX53ZfYsL+aWHpgArvOuof9"; +const XTERM_FIT_JS = "https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0.11.0/lib/addon-fit.min.js"; +const XTERM_FIT_JS_INTEGRITY = "sha384-UwMkGaBqfOcrTjPjXdAPWrGQkhpxTJ21vKtTwLb6wBpBM8HQXKAiUuwVJfgY0Yw6"; +// NOTE: dynamic import() does not support SRI integrity checks. +// Pin the exact version to reduce supply-chain risk. +const WASI_SHIM_URL = "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.0/+esm"; + +// Sample files for the virtual filesystem +const SAMPLE_FILES = { + "names.txt": "Alice\nBob\nCharlie\nDiana\nEve\nFrank\nGrace\nHeidi\n", + "numbers.txt": "42\n7\n13\n99\n1\n56\n23\n8\n100\n3\n", + "fruits.txt": "banana\napple\ncherry\ndate\napple\nbanana\ncherry\napple\n", + "csv.txt": "name,age,city\nAlice,30,Paris\nBob,25,London\nCharlie,35,Berlin\nDiana,28,Tokyo\n", + "words.txt": "hello world\nfoo bar baz\nthe quick brown fox\njumps over the lazy dog\n", + // Emoji-named files so `find` has something fun (and Unicode!) to match. + "🍎.md": "# Apple\n", + "🍌.md": "# Banana\n", + "🍒.md": "# Cherry\n", + "🥝.md": "# Kiwi\n", + // Two near-identical lists so `diff`/`cmp` have a small, readable change to show. + "shopping-old.txt": "milk\neggs\nbread\nbutter\napples\n", + "shopping-new.txt": "milk\neggs\nyogurt\nbread\nhoney\napples\n", +}; + +// Commands available in the feat_wasm build. +// The workflow generates /wasm/commands.js from coreutils' Cargo.toml feat_wasm +// list and exposes it as `WASM_COMMANDS`. The hardcoded list below is a fallback +// for local dev or when the generated file isn't available. +const FALLBACK_COMMANDS = [ + "arch", "b2sum", "base32", "base64", "basenc", "basename", "cat", "cksum", + "comm", "cp", "csplit", "cut", "date", "dd", "dir", "dircolors", "dirname", + "echo", "expand", "expr", "factor", "false", "fmt", "fold", "head", + "join", "link", "ln", "ls", "md5sum", "mkdir", "mv", "nl", "numfmt", + "nproc", "od", "paste", "pathchk", "printenv", "printf", "pr", "ptx", "pwd", + "readlink", "realpath", "rm", "rmdir", + "seq", "sort", "split", "tail", "touch", "tr", "tsort", + "sha1sum", "sha224sum", "sha256sum", "sha384sum", "sha512sum", + "shred", "shuf", "sleep", "sum", "tee", "true", "truncate", + "uname", "unexpand", "uniq", "unlink", "vdir", "wc", + "grep", "find", "locate", "updatedb", "diff", "cmp", "sed", +]; +const AVAILABLE_COMMANDS = + (typeof WASM_COMMANDS !== "undefined" && Array.isArray(WASM_COMMANDS) && WASM_COMMANDS.length > 0) + ? WASM_COMMANDS + : FALLBACK_COMMANDS; + +// Shortcut mappings for locale command (e.g. "fr" -> "fr-FR") +const LOCALE_SHORTCUTS = { + en: "en-US", fr: "fr-FR", de: "de-DE", es: "es-ES", it: "it-IT", + pt: "pt-BR", ja: "ja-JP", zh: "zh-CN", ko: "ko-KR", ru: "ru-RU", + pl: "pl-PL", nl: "nl-NL", sv: "sv-SE", da: "da-DK", fi: "fi-FI", + nb: "nb-NO", uk: "uk-UA", cs: "cs-CZ", ro: "ro-RO", hu: "hu-HU", +}; + +let wasmModule = null; +// Compiled standalone modules, keyed by module name (e.g. "grep", "diffutils"). +// A key is present only once its module has loaded successfully. +const standaloneModules = {}; +// In-flight loads, keyed by module name, so a button click and a command that +// both trigger a load (or two rapid clicks) share one fetch instead of racing. +const standaloneLoading = {}; +let wasiShim = null; +let terminal = null; +let inputBuffer = ""; +let cursorPos = 0; +let history = []; +let historyIndex = -1; +let wasmReady = false; +let wasmSize = 0; // downloaded binary size in bytes +let persistentDir = null; +let cwd = ""; // virtual current working directory (relative to preopened root) +let currentLocale = "en-US"; // current locale for l10n +let lastCommand = ""; // most recent command line run, for the "Share" button + +function loadScript(src, integrity) { + return new Promise((resolve, reject) => { + if (document.querySelector(`script[src="${src}"]`)) { + resolve(); + return; + } + const s = document.createElement("script"); + s.src = src; + if (integrity) { + s.integrity = integrity; + s.crossOrigin = "anonymous"; + } + s.onload = resolve; + s.onerror = reject; + document.head.appendChild(s); + }); +} + +function loadCSS(href, integrity) { + if (document.querySelector(`link[href="${href}"]`)) return; + const link = document.createElement("link"); + link.rel = "stylesheet"; + if (integrity) { + link.integrity = integrity; + link.crossOrigin = "anonymous"; + } + link.href = href; + document.head.appendChild(link); +} + +async function loadWasiShim() { + if (wasiShim) return wasiShim; + wasiShim = await import(WASI_SHIM_URL); + return wasiShim; +} + +/** + * Fetch and compile a WASM module from the given URL. + * Returns { module, size } where size is the downloaded byte length (0 if the + * server didn't send a content-length header). + */ +async function compileWasmModule(url) { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to fetch WASM binary: ${response.status}`); + } + const contentLength = response.headers.get("content-length"); + const size = contentLength ? parseInt(contentLength, 10) : 0; + // compileStreaming requires application/wasm content-type; fall back if not set. + // Clone the response so the fallback path can read the body without re-fetching. + const cloned = response.clone(); + let module; + try { + if (WebAssembly.compileStreaming) { + module = await WebAssembly.compileStreaming(response); + } else { + module = await WebAssembly.compile(await response.arrayBuffer()); + } + } catch (e) { + // Some servers don't set proper MIME type, compile from the cloned response + console.warn("WASM compileStreaming failed, falling back to arrayBuffer:", e.message); + module = await WebAssembly.compile(await cloned.arrayBuffer()); + } + return { module, size }; +} + +async function loadWasm() { + if (wasmModule) return wasmModule; + const { module, size } = await compileWasmModule(WASM_URL); + wasmModule = module; + wasmSize = size; + return wasmModule; +} + +/** + * Load one optional standalone module (grep, find, …) on demand. These are not + * part of the eager startup download — they're fetched the first time the user + * runs the command or clicks its "Load" button, keeping the initial page load + * to just the coreutils multicall binary. + * + * Concurrent callers share a single in-flight fetch. Returns the compiled + * module, or null if it's unknown or its binary isn't present (e.g. local dev + * without a CI build) — in which case the command reports it's unavailable + * rather than breaking the terminal. + * + * When `announce` is set and a terminal exists, a "loading … done" + * notice is printed live; the button UI instead reacts to the + * `uutils:program-loaded` event dispatched on success. + * + * Keyed by module name (e.g. "diffutils"), so commands that share a module + * (diff, cmp) trigger a single fetch. + */ +function loadStandalone(mod, { announce = false } = {}) { + if (standaloneModules[mod]) return Promise.resolve(standaloneModules[mod]); + if (standaloneLoading[mod]) return standaloneLoading[mod]; + const url = STANDALONE_MODULES[mod] && STANDALONE_MODULES[mod].url; + if (!url) return Promise.resolve(null); + const notify = announce && terminal; + if (notify) terminal.write(`loading ${mod}… `); + standaloneLoading[mod] = (async () => { + try { + const { module, size } = await compileWasmModule(url); + standaloneModules[mod] = module; + wasmSize += size; + if (notify) terminal.write(`done (${(size / 1024 / 1024).toFixed(1)} MB)\r\n`); + if (typeof document !== "undefined") { + document.dispatchEvent(new CustomEvent("uutils:program-loaded", { detail: { module: mod, size } })); + } + return module; + } catch (e) { + console.warn(`${mod} WASM unavailable:`, e.message); + if (notify) terminal.write("unavailable\r\n"); + return null; + } finally { + delete standaloneLoading[mod]; + } + })(); + return standaloneLoading[mod]; +} + +async function initWasm() { + if (wasmReady) return; + try { + // Only the coreutils multicall binary loads eagerly; the standalone + // modules (grep, find, locate, updatedb, diffutils, sed) are fetched + // on demand — see loadStandalone. + await Promise.all([loadWasiShim(), loadWasm()]); + wasmReady = true; + } catch (e) { + // Will fall back to JS implementations + console.warn("WASM init failed:", e); + } +} + +/** + * Get or create the persistent virtual filesystem directory. + * This directory is shared across all command invocations so that + * files created by one command (e.g. mkdir, cp) are visible to the next. + */ +function getPersistentDir() { + if (persistentDir) return persistentDir; + const encoder = new TextEncoder(); + // WASI nanosecond timestamp for "now" + const nowNs = BigInt(Date.now()) * 1_000_000n; + const fileEntries = Object.entries(SAMPLE_FILES).map( + ([name, content]) => { + const file = new wasiShim.File(encoder.encode(content)); + // browser_wasi_shim leaves timestamps at 0 (epoch 1970); + // patch them so ls shows a realistic date. + const origStat = file.stat.bind(file); + file.stat = () => { const s = origStat(); s.atim = nowNs; s.mtim = nowNs; s.ctim = nowNs; return s; }; + return [name, file]; + } + ); + persistentDir = new wasiShim.PreopenDirectory(".", fileEntries); + // Also patch the root directory stat + const origDirStat = persistentDir.dir.stat.bind(persistentDir.dir); + persistentDir.dir.stat = () => { const s = origDirStat(); s.atim = nowNs; s.mtim = nowNs; s.ctim = nowNs; return s; }; + return persistentDir; +} + +/** + * Resolve a path relative to cwd. Returns a normalized path relative to the + * preopened root directory. + */ +function resolvePath(p) { + if (!p || p.startsWith("-")) return p; + // Absolute paths (starting with /) stay as-is - WASI preopened dir is "." + // so absolute paths won't resolve anyway, but don't mangle them. + if (p.startsWith("/")) return p; + const base = cwd ? cwd.split("/") : []; + const parts = [...base, ...p.split("/")]; + const resolved = []; + for (const part of parts) { + if (part === "." || part === "") continue; + if (part === "..") { resolved.pop(); continue; } + resolved.push(part); + } + return resolved.join("/") || "."; +} + +/** + * Look up a directory in the virtual filesystem. + * Returns the Directory object if found, or null. + */ +function lookupDir(path) { + if (!wasiShim) return null; + const dir = getPersistentDir(); + if (!path || path === ".") return dir.dir; + const parts = path.split("/").filter(Boolean); + let current = dir.dir; // the Directory inside PreopenDirectory + for (const part of parts) { + if (part === ".") continue; + if (part === "..") return null; // can't go above root + const entry = current.contents.get(part); + if (!entry || !entry.contents) return null; // not a directory + current = entry; + } + return current; +} + +/** + * Run a single uutils command via the WASM module using browser_wasi_shim. + * Returns { stdout: string, stderr: string, exitCode: number } + */ +async function runCommand(argv, stdinData = "", module = wasmModule) { + if (!wasmReady) throw new Error("WASM not loaded"); + if (!module) throw new Error("WASM module not loaded"); + + const encoder = new TextEncoder(); + + // Collect raw bytes for stdout/stderr to avoid mid-character UTF-8 splits. + // ConsoleStdout.lineBuffered uses TextDecoder in streaming mode which can + // split multibyte UTF-8 characters across fd_write calls. Instead, we + // use ConsoleStdout with a raw byte callback and decode once at the end. + let stdoutBytes = []; + let stderrBytes = []; + + const stdoutFd = new wasiShim.ConsoleStdout((bytes) => { + stdoutBytes.push(new Uint8Array(bytes)); + }); + + const stderrFd = new wasiShim.ConsoleStdout((bytes) => { + stderrBytes.push(new Uint8Array(bytes)); + }); + + // stdin as an OpenFile wrapping a File + const stdinFile = new wasiShim.OpenFile( + new wasiShim.File(encoder.encode(stdinData)) + ); + + const preopenDir = getPersistentDir(); + + const fds = [ + stdinFile, // fd 0: stdin + stdoutFd, // fd 1: stdout + stderrFd, // fd 2: stderr + preopenDir, // fd 3: preopened directory + ]; + + const env = [`LANG=${currentLocale}.UTF-8`, "TERM=xterm-256color"]; + const wasi = new wasiShim.WASI(argv, env, fds); + + // Workaround for https://github.com/bjorn3/browser_wasi_shim/issues/109 + // @bjorn3/browser_wasi_shim args_sizes_get and environ_sizes_get use + // JavaScript's String.length (UTF-16 code units) to report the buffer + // size needed for argv/environ strings. However, WASI expects UTF-8 + // byte lengths. For ASCII this is identical, but for multibyte characters + // (CJK, emoji, Arabic, etc.) the UTF-8 encoding is longer than the + // UTF-16 code unit count, causing the WASM runtime to allocate a buffer + // that is too small. The result is truncated arguments and garbled output + // (e.g. "こんにちは世界" → "こんにち�3"). + // Fix: override both functions to use TextEncoder for correct byte counts. + wasi.wasiImport.args_sizes_get = function(argcPtr, argvBufSizePtr) { + const mem = () => new DataView(wasi.inst.exports.memory.buffer); + mem().setUint32(argcPtr, argv.length, true); + let bufSize = 0; + for (const arg of argv) bufSize += encoder.encode(arg).length + 1; + mem().setUint32(argvBufSizePtr, bufSize, true); + return 0; + }; + + wasi.wasiImport.environ_sizes_get = function(countPtr, sizePtr) { + const mem = () => new DataView(wasi.inst.exports.memory.buffer); + mem().setUint32(countPtr, env.length, true); + let bufSize = 0; + for (const e of env) bufSize += encoder.encode(e).length + 1; + mem().setUint32(sizePtr, bufSize, true); + return 0; + }; + + // Workaround: browser_wasi_shim Filestat.write_bytes() only writes the + // defined fields and skips padding bytes (e.g. bytes 17-23 between the + // 1-byte filetype and the 8-byte nlink). Rust allocates the stat buffer + // with MaybeUninit (uninitialized stack memory), so those padding bytes + // contain garbage that corrupts the struct when Rust reads it back. + // Fix: patch write_bytes to zero padding before writing fields. + // Monkey-patch write_bytes to also write the padding/nlink fields that + // the original skips. The original writes filetype as a single byte at + // ptr+16 but leaves bytes 17-23 untouched. On WASM those bytes come from + // uninitialized stack memory (MaybeUninit) and corrupt the struct. + wasiShim.wasi.Filestat.prototype.write_bytes = function(view, ptr) { + view.setBigUint64(ptr, this.dev, true); + view.setBigUint64(ptr + 8, this.ino, true); + view.setBigUint64(ptr + 16, BigInt(this.filetype), true); // zero-extends, clearing padding + view.setBigUint64(ptr + 24, this.nlink, true); + view.setBigUint64(ptr + 32, this.size, true); + view.setBigUint64(ptr + 40, this.atim, true); + view.setBigUint64(ptr + 48, this.mtim, true); + view.setBigUint64(ptr + 56, this.ctim, true); + }; + + let exitCode = 0; + try { + const result = await WebAssembly.instantiate(module, { + wasi_snapshot_preview1: wasi.wasiImport, + }); + // instantiate(Module) returns Instance; instantiate(Buffer) returns {instance} + const instance = result.instance || result; + wasi.start(instance); + } catch (e) { + if (e instanceof wasiShim.WASIProcExit) { + exitCode = e.code; + } else { + return { stdout: "", stderr: e.message + "\n", exitCode: 1 }; + } + } + + const decoder = new TextDecoder(); + const concatBytes = (chunks) => { + const total = chunks.reduce((s, c) => s + c.length, 0); + const buf = new Uint8Array(total); + let offset = 0; + for (const chunk of chunks) { buf.set(chunk, offset); offset += chunk.length; } + return decoder.decode(buf); + }; + + return { + stdout: concatBytes(stdoutBytes), + stderr: concatBytes(stderrBytes), + exitCode, + }; +} + +/** + * Split a command line into sequentially-run commands on top-level ";". + * Semicolons inside single/double quotes or escaped are left untouched, so + * `echo 'a;b'` stays one command. Empty segments (e.g. a trailing ";") are + * dropped. Returns an array of command strings, each run in turn like a shell. + */ +function splitCommands(line) { + const commands = []; + let current = ""; + let inSingle = false; + let inDouble = false; + let escape = false; + + for (const ch of line) { + if (escape) { current += ch; escape = false; continue; } + if (ch === "\\" && !inSingle) { current += ch; escape = true; continue; } + if (ch === "'" && !inDouble) { inSingle = !inSingle; current += ch; continue; } + if (ch === '"' && !inSingle) { inDouble = !inDouble; current += ch; continue; } + if (ch === ";" && !inSingle && !inDouble) { + if (current.trim()) commands.push(current.trim()); + current = ""; + continue; + } + current += ch; + } + if (current.trim()) commands.push(current.trim()); + return commands; +} + +/** + * Parse a command line into a pipeline of commands. + * Supports pipes (|), input redirection (<), and output redirection (>, >>). + * Returns an array of stages: { args: string[], stdin: string|null, stdout: string|null, append: boolean } + */ +function parseCommandLine(line) { + // First, tokenize respecting quotes + const tokens = []; + let current = ""; + let inSingle = false; + let inDouble = false; + let escape = false; + + for (const ch of line) { + if (escape) { current += ch; escape = false; continue; } + if (ch === "\\" && !inSingle) { escape = true; continue; } + if (ch === "'" && !inDouble) { inSingle = !inSingle; continue; } + if (ch === '"' && !inSingle) { inDouble = !inDouble; continue; } + if (!inSingle && !inDouble && (ch === "|" || ch === "<" || ch === ">" || ch === " ")) { + if (current) { tokens.push(current); current = ""; } + if (ch !== " ") tokens.push(ch); + continue; + } + current += ch; + } + if (current) tokens.push(current); + + // Merge >> into a single token + for (let i = 0; i < tokens.length - 1; i++) { + if (tokens[i] === ">" && tokens[i + 1] === ">") { + tokens.splice(i, 2, ">>"); + } + } + + // Split into pipeline stages and extract redirections + const pipeline = []; + let stage = { args: [], stdin: null, stdout: null, append: false }; + + for (let i = 0; i < tokens.length; i++) { + const tok = tokens[i]; + if (tok === "|") { + pipeline.push(stage); + stage = { args: [], stdin: null, stdout: null, append: false }; + } else if (tok === "<" && i + 1 < tokens.length) { + stage.stdin = tokens[++i]; + } else if ((tok === ">" || tok === ">>") && i + 1 < tokens.length) { + stage.append = tok === ">>"; + stage.stdout = tokens[++i]; + } else { + stage.args.push(tok); + } + } + pipeline.push(stage); + return pipeline; +} + +// Upper bound on the length of a command supplied via the ?cmd= URL parameter. +// Long enough for any real example, short enough that a pathological value +// can't be handed to the WASM runtime wholesale (issue #53). +const MAX_URL_COMMAND_LENGTH = 2048; + +/** + * Sanitize a command coming from the ?cmd= URL parameter before it is executed. + * + * Strips control / unprintable characters that cannot be typed at the prompt - + * keeping only newline (used to separate multiple commands) and tab - so that + * an invisible byte like NUL (%00) no longer reaches the parser and produces a + * baffling "command not found:" with nothing after it (issue #52). Also caps + * the length so an oversized value degrades gracefully (issue #53). + */ +function sanitizeUrlCommand(raw) { + if (!raw) return ""; + // Remove C0 controls (0x00-0x1F) except tab (0x09) and newline (0x0A), plus DEL (0x7F). + // eslint-disable-next-line no-control-regex -- stripping control chars is the whole point + let cmd = raw.replace(/[\u0000-\u0008\u000b-\u001f\u007f]/g, ""); + if (cmd.length > MAX_URL_COMMAND_LENGTH) cmd = cmd.slice(0, MAX_URL_COMMAND_LENGTH); + return cmd; +} + +/** + * Read a file from the virtual filesystem. Returns its content as a string, + * or null if not found. + */ +function readVirtualFile(name) { + const dir = getPersistentDir(); + const resolved = resolvePath(name); + const parts = resolved.split("/").filter(Boolean); + let current = dir.dir; + for (let i = 0; i < parts.length - 1; i++) { + const entry = current.contents.get(parts[i]); + if (!entry || !entry.contents) return null; + current = entry; + } + const file = current.contents.get(parts[parts.length - 1] || name); + if (!file || !file.data) return null; + return new TextDecoder().decode(file.data); +} + +/** + * Stamp a virtual file's timestamps with "now". browser_wasi_shim creates + * files written by a WASM command (e.g. updatedb's database) with epoch (1970) + * timestamps; this patches stat() the same way getPersistentDir does for the + * sample files, so age-sensitive readers (locate) don't see a decades-old file. + */ +function touchVirtualFileNow(name) { + const dir = getPersistentDir(); + const file = dir.dir.contents.get(resolvePath(name)); + if (!file || typeof file.stat !== "function") return; + const nowNs = BigInt(Date.now()) * 1_000_000n; + const orig = file.stat.bind(file); + file.stat = () => { const s = orig(); s.atim = nowNs; s.mtim = nowNs; s.ctim = nowNs; return s; }; +} + +/** + * Write a file to the virtual filesystem. + */ +function writeVirtualFile(name, content, append) { + const dir = getPersistentDir(); + const resolved = resolvePath(name); + const encoder = new TextEncoder(); + const existing = dir.dir.contents.get(resolved); + let data; + if (append && existing && existing.data) { + const prev = existing.data; + const added = encoder.encode(content); + data = new Uint8Array(prev.length + added.length); + data.set(prev); + data.set(added, prev.length); + } else { + data = encoder.encode(content); + } + const file = new wasiShim.File(data); + dir.dir.contents.set(resolved, file); +} + +/** + * Execute a command line, splitting on top-level ";" so several commands run + * in sequence (like a shell), e.g. `updatedb; locate names`. Each segment is + * executed in turn and their outputs are concatenated. State changes from one + * segment (cwd via cd, the locate db built by updatedb, files written via >) + * carry into the next, since they share the module-global virtual FS and cwd. + */ +async function executeCommandLine(line) { + const commands = splitCommands(line); + if (commands.length <= 1) return executeSingleCommandLine(line); + let output = ""; + for (const cmd of commands) { + output += await executeSingleCommandLine(cmd); + } + return output; +} + +/** + * Execute a single command line, handling pipes and builtins. + */ +async function executeSingleCommandLine(line) { + line = line.trim(); + if (!line) return ""; + + // Builtin: help + if (line === "help") { + return ( + "Available commands:\n" + + AVAILABLE_COMMANDS.join(", ") + + "\n\nBuiltins: help, clear, cd, locale\n" + + "Sample files: names.txt, numbers.txt, fruits.txt, csv.txt, words.txt\n" + + "\nExamples:\n" + + " echo 'hello world'\n" + + " cat numbers.txt\n" + + " echo '5 3 1 4 2' | fmt -w1 | sort -n\n" + + " wc -l fruits.txt\n" + + " seq 1 10 | factor\n" + + " grep -i alice names.txt\n" + + " find . -name '*.md'\n" + + " diff -u shopping-old.txt shopping-new.txt\n" + + " echo 'hello world' | sed 's/world/there/'\n" + + " basename /usr/local/bin/rustc\n" + + " date\n" + + " uname -a\n" + ); + } + + // Builtin: clear + if (line === "clear") { + if (terminal) terminal.clear(); + return ""; + } + + // Builtin: cd + if (line === "cd" || line.startsWith("cd ")) { + const target = line === "cd" ? "" : line.slice(3).trim(); + if (!target || target === "/" || target === "~") { + cwd = ""; + return ""; + } + const resolved = resolvePath(target); + if (resolved === ".") { + cwd = ""; + return ""; + } + const dir = lookupDir(resolved); + if (!dir) { + return `cd: ${target}: No such directory\n`; + } + cwd = resolved; + return ""; + } + + // Builtin: locale - show or set the current locale + if (line === "locale" || line.startsWith("locale ")) { + const arg = line === "locale" ? "" : line.slice(7).trim(); + if (!arg) { + return `LANG=${currentLocale}.UTF-8\n`; + } + // Normalize: "fr" -> "fr-FR", "en" -> "en-US", or accept full form + const normalized = arg.includes("-") ? arg : LOCALE_SHORTCUTS[arg.toLowerCase()] || arg; + currentLocale = normalized; + return `Locale set to ${currentLocale}\n`; + } + + // Builtin: pwd (return virtual cwd) + if (line === "pwd") { + return "/" + cwd + "\n"; + } + + const pipeline = parseCommandLine(line); + let stdinData = ""; + + for (const stage of pipeline) { + const { args, stdin: stdinFile, stdout: stdoutFile, append } = stage; + if (args.length === 0) continue; + + // Handle input redirection: < file + if (stdinFile) { + const content = readVirtualFile(stdinFile); + if (content === null) return `${stdinFile}: No such file\n`; + stdinData = content; + } + + const cmd = args[0]; + + if (!wasmReady) { + return "WASM binary is still loading, please wait...\n"; + } + + if (!AVAILABLE_COMMANDS.includes(cmd)) { + return `uutils: command not found: ${cmd}\nType 'help' for available commands.\n`; + } + + // Some utilities (grep, find, locate, updatedb, diff, cmp, sed) are + // separate WASM modules rather than part of the coreutils multicall binary, + // and are loaded on demand. + // Fetch the module the first time one of its commands is used (no-op once + // cached; diff and cmp share the single diffutils module). + const moduleName = STANDALONE_COMMAND_MODULE[cmd]; + const isStandalone = !!moduleName; + if (isStandalone && !standaloneModules[moduleName]) { + const mod = await loadStandalone(moduleName, { announce: true }); + if (!mod) return `${cmd} is not available in this build.\n`; + } + + try { + // sed's script argument is a program, not a path — it must NOT go through + // resolvePath, which normalizes path segments and would strip a trailing + // delimiter (e.g. `s/world/there/` -> `s/world/there`, breaking the `s` + // command). Collect the indices of any sed script arguments to skip. + const sedScriptIndices = new Set(); + if (cmd === "sed") { + let scriptFromFlag = false; + for (let i = 1; i < args.length; i++) { + const a = args[i]; + if (a === "-e" || a === "-f") { sedScriptIndices.add(i + 1); scriptFromFlag = true; i++; continue; } + if (a.startsWith("-e") || a.startsWith("-f")) { scriptFromFlag = true; } // combined form, e.g. -e's/a/b/' + } + // Without -e/-f, the script is the first non-option argument. + if (!scriptFromFlag) { + for (let i = 1; i < args.length; i++) { + if (!args[i].startsWith("-")) { sedScriptIndices.add(i); break; } + } + } + } + // Resolve relative paths using the virtual cwd + const resolvedArgs = args.map((arg, i) => { + if (i === 0) return arg; // command name + if (arg.startsWith("-")) return arg; // flag + if (sedScriptIndices.has(i)) return arg; // sed script, not a path + return resolvePath(arg); + }); + // If the command takes a default path (like ls) and no path args + // were given, add the cwd so it lists the right directory + const hasPathArg = resolvedArgs.slice(1).some(a => !a.startsWith("-")); + if (!hasPathArg && cwd && ["ls", "dir"].includes(cmd)) { + resolvedArgs.push(cwd); + } + // find takes its starting paths *before* the expression. When the user + // gives none (e.g. `find -type f`), GNU find defaults to "."; mirror that + // but use the virtual cwd so `cd subdir; find` searches the right place. + if (cmd === "find") { + const hasStartPath = resolvedArgs.length > 1 && !resolvedArgs[1].startsWith("-"); + if (!hasStartPath) resolvedArgs.splice(1, 0, cwd || "."); + } + // updatedb/locate default to /usr/local/var/locatedb and updatedb scans + // "/", neither of which exists in the browser's WASI filesystem (only the + // virtual cwd "." is preopened). Point both at a writable db file in the + // virtual root and have updatedb index the playground's files, so a bare + // `updatedb` then `locate ` works out of the box. + if (cmd === "updatedb") { + if (!resolvedArgs.some(a => a === "--output" || a.startsWith("--output="))) + resolvedArgs.push(`--output=${LOCATE_DB}`); + if (!resolvedArgs.some(a => a === "--localpaths" || a.startsWith("--localpaths="))) + resolvedArgs.push(`--localpaths=${cwd || "."}`); + } + if (cmd === "locate") { + const hasDb = resolvedArgs.some(a => + a === "--database" || a.startsWith("--database=") || a === "-d" || (a.startsWith("-d") && a.length > 2)); + if (!hasDb) resolvedArgs.splice(1, 0, `--database=${LOCATE_DB}`); + } + // Standalone utilities are invoked directly (argv[0] = the command name); + // coreutils utilities go through the multicall dispatcher + // (argv = ["coreutils", , ...]). + let dispatchArgs = resolvedArgs; + if (cmd === "grep") { + // browser_wasi_shim reports stdout as a TTY, so grep would emit GNU + // match-highlight escape codes by default. That looks fine in the + // terminal but corrupts piped/redirected output (e.g. `grep x | wc`), + // so default to no color unless the user asks for it explicitly. + const hasColorFlag = resolvedArgs.some(a => a === "--color" || a.startsWith("--color=")); + dispatchArgs = hasColorFlag + ? resolvedArgs + : [resolvedArgs[0], "--color=never", ...resolvedArgs.slice(1)]; + } + const wasmArgs = isStandalone ? dispatchArgs : ["coreutils", ...resolvedArgs]; + const result = await runCommand(wasmArgs, stdinData, isStandalone ? standaloneModules[moduleName] : wasmModule); + // updatedb writes its database with epoch (1970) timestamps in the virtual + // FS, so a later `locate` would warn the db is decades old (and that stderr + // warning would break `locate … | …` pipes). Stamp the freshly-built db + // with "now" to keep locate quiet. + if (cmd === "updatedb" && result.exitCode === 0) { + const outArg = resolvedArgs.find(a => a.startsWith("--output=")); + touchVirtualFileNow(outArg ? outArg.slice("--output=".length) : LOCATE_DB); + } + if (result.stderr) { + return result.stderr + result.stdout; + } + stdinData = result.stdout; + } catch (e) { + return `Error running '${cmd}': ${e.message}\n`; + } + + // Handle output redirection: > file or >> file + if (stdoutFile) { + writeVirtualFile(stdoutFile, stdinData, append); + stdinData = ""; + } + } + + return stdinData; +} + + +/** + * Record a command line that was run, whether typed at the prompt or triggered + * by clicking an example. Adds it to the up-arrow history (skipping consecutive + * duplicates), remembers it for the page's "Share" button, and notifies any + * listeners. Builtins that only affect the local view (clear) aren't worth + * sharing, so they're skipped. + */ +function recordCommand(line) { + line = (line || "").trim(); + if (!line) return; + if (history[history.length - 1] !== line) history.push(line); + historyIndex = -1; + // clear only wipes the local view, so it isn't worth sharing. + if (line === "clear") return; + lastCommand = line; + if (typeof document !== "undefined") { + document.dispatchEvent(new CustomEvent("uutils:command-run", { detail: { command: line } })); + } +} + +function writeToTerminal(text) { + if (!terminal) return; + const lines = text.split("\n"); + for (let i = 0; i < lines.length; i++) { + terminal.write(lines[i]); + if (i < lines.length - 1) terminal.write("\r\n"); + } +} + +function promptStr() { + const dir = cwd ? `\x1b[1;34m${cwd}\x1b[0m ` : ""; + return `\x1b[1;38;5;166muutils\x1b[0m ${dir}\x1b[1;38;5;166m$\x1b[0m `; +} + +function prompt() { + if (!terminal) return; + terminal.write("\r\n" + promptStr()); +} + +function redrawInput() { + terminal.write("\r" + promptStr() + inputBuffer + "\x1b[K"); + const moveBack = inputBuffer.length - cursorPos; + if (moveBack > 0) terminal.write(`\x1b[${moveBack}D`); +} + +/** + * Compute tab-completion result (pure function, no terminal side-effects). + * Returns { buffer, cursor, completed, candidates }. + */ +function tabComplete(buffer, cursor) { + const before = buffer.slice(0, cursor); + const after = buffer.slice(cursor); + const parts = before.split(/\s+/); + const word = parts[parts.length - 1]; + const isCommand = parts.length <= 1; + + const builtins = ["help", "clear", "cd", "locale"]; + const candidates = isCommand + ? AVAILABLE_COMMANDS.concat(builtins).filter(c => c.startsWith(word)) + : Object.keys(SAMPLE_FILES).filter(f => f.startsWith(word)); + + if (candidates.length === 1) { + const suffix = candidates[0].slice(word.length) + " "; + return { buffer: before + suffix + after, cursor: cursor + suffix.length, completed: true, candidates }; + } + if (candidates.length > 1) { + let common = candidates[0]; + for (const c of candidates) { + while (!c.startsWith(common)) common = common.slice(0, -1); + } + if (common.length > word.length) { + const suffix = common.slice(word.length); + return { buffer: before + suffix + after, cursor: cursor + suffix.length, completed: true, candidates }; + } + return { buffer, cursor, completed: false, candidates }; + } + return { buffer, cursor, completed: false, candidates: [] }; +} + +async function handleInput(data) { + for (let i = 0; i < data.length; i++) { + const ch = data[i]; + const code = ch.charCodeAt(0); + + // Handle escape sequences + if (ch === "\x1b" && i + 2 < data.length) { + if (data[i + 1] === "[") { + const seq = data[i + 2]; + if (seq === "A") { // Up arrow + if (history.length > 0 && historyIndex < history.length - 1) { + historyIndex++; + inputBuffer = history[history.length - 1 - historyIndex]; + cursorPos = inputBuffer.length; + redrawInput(); + } + i += 2; continue; + } + if (seq === "B") { // Down arrow + if (historyIndex > 0) { + historyIndex--; + inputBuffer = history[history.length - 1 - historyIndex]; + cursorPos = inputBuffer.length; + redrawInput(); + } else if (historyIndex === 0) { + historyIndex = -1; + inputBuffer = ""; + cursorPos = 0; + redrawInput(); + } + i += 2; continue; + } + if (seq === "C") { // Right arrow + if (cursorPos < inputBuffer.length) { cursorPos++; terminal.write("\x1b[C"); } + i += 2; continue; + } + if (seq === "D") { // Left arrow + if (cursorPos > 0) { cursorPos--; terminal.write("\x1b[D"); } + i += 2; continue; + } + } + continue; + } + + if (code === 13) { // Enter + terminal.write("\r\n"); + const line = inputBuffer.trim(); + if (line) { + recordCommand(line); + const output = await executeCommandLine(line); + if (output) writeToTerminal(output); + } + inputBuffer = ""; + cursorPos = 0; + prompt(); + continue; + } + + if (code === 127 || code === 8) { // Backspace + if (cursorPos > 0) { + inputBuffer = inputBuffer.slice(0, cursorPos - 1) + inputBuffer.slice(cursorPos); + cursorPos--; + redrawInput(); + } + continue; + } + + if (code === 3) { // Ctrl+C + inputBuffer = ""; + cursorPos = 0; + terminal.write("^C"); + prompt(); + continue; + } + + if (code === 12) { // Ctrl+L + terminal.clear(); + prompt(); + terminal.write(inputBuffer); + continue; + } + + if (code === 9) { // Tab - completion + const result = tabComplete(inputBuffer, cursorPos); + if (result.completed) { + inputBuffer = result.buffer; + cursorPos = result.cursor; + redrawInput(); + } else if (result.candidates.length > 1) { + terminal.write("\r\n" + result.candidates.join(" ") + "\r\n"); + prompt(); + terminal.write(inputBuffer); + const back = inputBuffer.length - cursorPos; + if (back > 0) terminal.write(`\x1b[${back}D`); + } + continue; + } + + if (code === 21) { // Ctrl+U + inputBuffer = ""; + cursorPos = 0; + redrawInput(); + continue; + } + + if (code >= 32) { // Regular character + inputBuffer = inputBuffer.slice(0, cursorPos) + ch + inputBuffer.slice(cursorPos); + cursorPos++; + redrawInput(); + } + } +} + +/** + * Initialize the playground terminal in the given container element. + */ +async function initPlayground(containerId) { + const container = document.getElementById(containerId); + if (!container) return; + + container.innerHTML = '
Loading terminal...
'; + + loadCSS(XTERM_CSS, XTERM_CSS_INTEGRITY); + await loadScript(XTERM_JS, XTERM_JS_INTEGRITY); + await loadScript(XTERM_FIT_JS, XTERM_FIT_JS_INTEGRITY); + + container.innerHTML = ""; + + terminal = new window.Terminal({ + cursorBlink: true, + fontSize: 14, + fontFamily: '"Fira Code", "Cascadia Code", Menlo, Monaco, monospace', + theme: { + background: "#1e1e2e", + foreground: "#cdd6f4", + cursor: "#f5e0dc", + selectionBackground: "#585b7066", + black: "#45475a", + red: "#f38ba8", + green: "#a6e3a1", + yellow: "#f9e2af", + blue: "#89b4fa", + magenta: "#f5c2e7", + cyan: "#94e2d5", + white: "#bac2de", + }, + rows: 24, + cols: 80, + }); + + const fitAddon = new window.FitAddon.FitAddon(); + terminal.loadAddon(fitAddon); + terminal.open(container); + fitAddon.fit(); + + window.addEventListener("resize", () => fitAddon.fit()); + + terminal.writeln("\x1b[1;38;5;166m _ _ _ _ _ _ _\x1b[0m"); + terminal.writeln("\x1b[1;38;5;166m | | | | | | | |_(_) |___\x1b[0m"); + terminal.writeln("\x1b[1;38;5;166m | |_| | |_| | _| | (_-<\x1b[0m"); + terminal.writeln("\x1b[1;38;5;166m \\___/ \\___/ \\__|_|_/__/\x1b[0m"); + terminal.writeln(""); + terminal.writeln("Welcome to the \x1b[1muutils coreutils\x1b[0m playground!"); + terminal.writeln("Run Rust coreutils directly in your browser via WebAssembly."); + terminal.writeln(""); + terminal.write("Loading WASM binary..."); + + terminal.onData(handleInput); + + // Load WASM in background + try { + await initWasm(); + const sizeStr = wasmSize > 0 ? ` (${(wasmSize / 1024 / 1024).toFixed(1)} MB)` : ""; + terminal.writeln(` \x1b[1;32mdone!\x1b[0m${sizeStr}`); + terminal.writeln(""); + terminal.writeln("Type \x1b[1;32mhelp\x1b[0m for available commands."); + terminal.writeln("Sample data files: names.txt, numbers.txt, fruits.txt, csv.txt, words.txt"); + terminal.writeln("\x1b[2mgrep, find/locate/updatedb, sed and diff/cmp load on demand - just run them, or use the buttons above.\x1b[0m"); + } catch { + terminal.writeln(" \x1b[1;31mfailed\x1b[0m"); + terminal.writeln("Failed to load WASM binary. Commands are not available."); + terminal.writeln("Try reloading the page."); + } + + // Run command(s) from URL ?cmd= parameter if present + const urlCmd = sanitizeUrlCommand(new URLSearchParams(window.location.search).get("cmd")); + if (urlCmd) { + for (const cmd of urlCmd.split("\n")) { + if (cmd.trim()) await runInTerminal(cmd.trim()); + } + } else { + prompt(); + } +} + +/** + * Run a command in the terminal, displaying it as if the user typed it. + */ +async function runInTerminal(cmd) { + if (!terminal) return; + // Show the command on the prompt line + terminal.write(cmd); + terminal.write("\r\n"); + recordCommand(cmd); + const output = await executeCommandLine(cmd); + if (output) writeToTerminal(output); + prompt(); +} + +/** + * Set the locale and optionally update the terminal. + */ +function setLocale(locale) { + currentLocale = locale; + if (terminal) { + terminal.writeln(`\r\nLocale set to ${currentLocale}`); + prompt(); + } +} + +// Expose globally +window.initPlayground = initPlayground; +window.uutilsExecute = executeCommandLine; +window.runInTerminal = runInTerminal; +window.setLocale = setLocale; +window.getLastCommand = () => lastCommand; + +// On-demand loading of the optional standalone modules, used by the "Load" +// buttons on the playground page. Buttons operate on groups (see +// STANDALONE_GROUPS): "find" loads find + locate + updatedb together, while the +// others map one-to-one. A group with an unknown name falls back to a same-named +// single module. +const groupModules = (group) => STANDALONE_GROUPS[group] || [group]; +window.uutilsPrograms = Object.keys(STANDALONE_GROUPS); +window.loadProgram = (group) => + Promise.all(groupModules(group).map(m => loadStandalone(m))) + .then(mods => mods.every(Boolean) ? mods : null); +window.isProgramLoaded = (group) => + groupModules(group).every(m => !!standaloneModules[m]); +// Best-effort byte size of a group, summed across its modules, for the button +// label (0 if the server doesn't report Content-Length or a binary is missing). +window.programSize = async (group) => { + const sizes = await Promise.all(groupModules(group).map(async (m) => { + const url = STANDALONE_MODULES[m] && STANDALONE_MODULES[m].url; + if (!url) return 0; + try { + const r = await fetch(url, { method: "HEAD" }); + const cl = r.ok ? r.headers.get("content-length") : null; + return cl ? parseInt(cl, 10) : 0; + } catch { + return 0; + } + })); + return sizes.reduce((a, b) => a + b, 0); +}; + +// Expose internals for testing +window._uutilsTestInternals = { + parseCommandLine, + splitCommands, + sanitizeUrlCommand, + executeCommandLine, + resolvePath, + lookupDir, + getPersistentDir, + readVirtualFile, + writeVirtualFile, + get cwd() { return cwd; }, + set cwd(v) { cwd = v; }, + get locale() { return currentLocale; }, + set locale(v) { currentLocale = v; }, + get wasmReady() { return wasmReady; }, + get grepReady() { return !!standaloneModules.grep; }, + get findReady() { return !!standaloneModules.find; }, + get locateReady() { return !!standaloneModules.locate; }, + get updatedbReady() { return !!standaloneModules.updatedb; }, + get diffutilsReady() { return !!standaloneModules.diffutils; }, + get sedReady() { return !!standaloneModules.sed; }, + initWasm, + loadStandalone, + LOCALE_SHORTCUTS, + SAMPLE_FILES, + AVAILABLE_COMMANDS, + tabComplete, +}; diff --git a/static/js/wasm-terminal.test.html b/static/js/wasm-terminal.test.html new file mode 100644 index 000000000..2cdafb446 --- /dev/null +++ b/static/js/wasm-terminal.test.html @@ -0,0 +1,786 @@ + + + + + wasm-terminal unit tests + + + +

wasm-terminal unit tests

+
+
+ + + + + diff --git a/static/logo-dark.svg b/static/logo-dark.svg new file mode 100644 index 000000000..dee38a04f --- /dev/null +++ b/static/logo-dark.svg @@ -0,0 +1,61 @@ + diff --git a/static/style.css b/static/style.css index 135b7fb4e..ec4816566 100644 --- a/static/style.css +++ b/static/style.css @@ -1,20 +1,118 @@ +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/fonts/JetBrainsMono-400.woff2") format("woff2"); +} +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("/fonts/JetBrainsMono-500.woff2") format("woff2"); +} +@font-face { + font-family: "JetBrains Mono"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/fonts/JetBrainsMono-700.woff2") format("woff2"); +} + :root { + /* Light theme colors (default) */ + + --accent-color: #c04828; + --accent-hover: #d45530; + + --font-mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; + --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, + "Helvetica Neue", Arial, "Noto Sans", sans-serif; + + /* Terminal window palette (kept dark in both themes so it reads as a terminal) */ + --term-head: #2b2b2d; + --term-head-fg: #cfcfd6; + --term-comment: #8a8a93; + --term-blue: #6cb6ff; + --st-ready: #7ee2a0; + --st-beta: #f0c870; + --st-alpha: #f0a868; + --st-wip: #8aa0b8; + + --radius: 0.4rem; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06), 0 1px 3px rgba(0, 0, 0, 0.08); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.1); + + --card-bg: #fafafa; + --card-border: #e5e5e5; + --muted-fg-color: #595959; + --terminal-bg-color: #1e1e2e; + --terminal-fg-color: #cdd6f4; + + --badge-ready-bg: #e6f4ea; + --badge-ready-fg: #1e7a3c; + --badge-beta-bg: #fff4e0; + --badge-beta-fg: #9a6400; + --badge-alpha-bg: #fdeadb; + --badge-alpha-fg: #b5500a; + --badge-wip-bg: #eef1f5; + --badge-wip-fg: #4a5568; + --dark-fg-color: #fff; --light-fg-color: #141414; --light-bg-color: var(--dark-fg-color); --dark-bg-color: var(--light-fg-color); --fg-color: var(--light-fg-color); --bg-color: var(--light-bg-color); - --light-link-color: #c04828; - --dark-link-color: #c04828; - --link-color: var(--light-link-color); + --link-color: var(--accent-color); --light-highlight-bg-color: #ededed; --light-highlight-fg-color: #595959; --dark-highlight-bg-color: #27272a; --dark-highlight-fg-color: #ededed; --highlight-fg-color: var(--light-highlight-fg-color); --highlight-bg-color: var(--light-highlight-bg-color); - --font-face: "Fira Sans", sans-serif; + --link-text-color: var(--accent-color); + --header-border-color: #ddd; + --post-bg-color: #e5e5e5; + --font-face: var(--font-sans); + + --github-icon: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); + --github-icon-black: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='000' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); +} + +@media (prefers-color-scheme: dark) { + :root { + --fg-color: #d7dce5; + --bg-color: #0e0f13; + --link-color: var(--accent-color); + --light-highlight-bg-color: #161a22; + --light-highlight-fg-color: #ffffff; + --dark-highlight-bg-color: #27272a; + --dark-highlight-fg-color: #ededed; + --highlight-fg-color: var(--light-highlight-fg-color); + --highlight-bg-color: var(--light-highlight-bg-color); + --link-text-color: var(--accent-color); + --header-border-color: #232a36; + --post-bg-color: #151922; + --card-bg: #151922; + --card-border: #232a36; + --muted-fg-color: #9aa0ab; + + --badge-ready-bg: #16321f; + --badge-ready-fg: #7ee2a0; + --badge-beta-bg: #3a2e12; + --badge-beta-fg: #f0c870; + --badge-alpha-bg: #3a2a16; + --badge-alpha-fg: #f0a868; + --badge-wip-bg: #2c313a; + --badge-wip-fg: #b6c0cf; + } +} + +body { + background-color: var(--bg-color); + color: var(--fg-color); } *, @@ -55,6 +153,23 @@ a { text-decoration: inherit; } +a:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 2px; + border-radius: 2px; +} + +.btn:focus-visible, +.btn-primary:focus-visible, +.btn-ghost:focus-visible, +.playground-toolbar select:focus-visible, +.wasm-run-btn:focus-visible, +#playground-share-btn:focus-visible, +.playground-loader:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + /* PAGE LAYOUT */ html, body { @@ -89,12 +204,46 @@ header { justify-items: center; padding: 0.5rem 2rem; width: 100%; - border-bottom: 1px solid #ddd; - font-size: 1.2rem; + border-bottom: 1px solid var(--header-border-color); + font-size: 1.05rem; + font-family: var(--font-mono); + position: sticky; + top: 0; + z-index: 100; + background-color: color-mix(in srgb, var(--bg-color) 85%, transparent); + backdrop-filter: saturate(160%) blur(10px); +} + +.brand-name { + margin-left: 0.5rem; + font-family: var(--font-mono); + font-weight: 700; + font-size: 1.4rem; + letter-spacing: -0.04em; +} + +@keyframes cursor-blink { + 50% { + opacity: 0; + } +} + +.brand-cursor, +.hero-cursor { + color: var(--accent-color); + font-weight: 400; + animation: cursor-blink 1.1s steps(1) infinite; +} + +@media (prefers-reduced-motion: reduce) { + .brand-cursor, + .hero-cursor { + animation: none; + } } header .home { - color: black; + color: var(--fg-color); font-size: 2rem; font-weight: bold; } @@ -111,13 +260,12 @@ header .home svg { } header a { - color: black; + color: var(--fg-color); border-bottom: 2px solid transparent; } header a:hover:not(.home) { - color: black; - border-bottom: 2px solid #c04828; + border-bottom: 2px solid var(--link-text-color); } .navigation-block { @@ -126,6 +274,73 @@ header a:hover:not(.home) { gap: 1rem; } +.dropdown { + position: relative; +} + +.dropdown-toggle { + cursor: pointer; + color: var(--fg-color); + background: none; + border: none; + border-bottom: 2px solid transparent; + padding: 0 0 0.1em; + font: inherit; +} + +.dropdown:hover .dropdown-toggle, +.dropdown.open .dropdown-toggle { + border-bottom: 2px solid var(--link-text-color); +} + +.dropdown-toggle:focus-visible { + outline: 2px solid var(--accent-color); + outline-offset: 2px; +} + +.dropdown-menu::before { + content: ""; + position: absolute; + bottom: 100%; + left: 0; + right: 0; + height: 0.75rem; +} + +.dropdown-menu { + display: none; + position: absolute; + top: calc(100% + 0.75rem); + left: 50%; + transform: translateX(-50%); + background-color: var(--bg-color); + border: 1px solid var(--header-border-color); + border-radius: 0.3em; + padding: 0.4em 0; + z-index: 100; + min-width: 10rem; + flex-direction: column; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.dropdown:hover .dropdown-menu, +.dropdown.open .dropdown-menu { + display: flex; +} + +.dropdown-menu a { + padding: 0.4em 1.2em; + color: var(--fg-color); + border-bottom: none !important; + white-space: nowrap; +} + +.dropdown-menu a:hover { + color: var(--link-text-color); + background-color: var(--highlight-bg-color); + border-bottom: none !important; +} + header .icon { display: flex; align-items: center; @@ -149,13 +364,13 @@ header .icon { #mobile-open-navigation button div { margin: 0.5em 0; width: 2em; - border-bottom: 0.25em solid black; + border-bottom: 0.25em solid var(--fg-color); } header { grid-template-columns: max-content 1fr; } - + .spacer { display: none; } @@ -165,39 +380,139 @@ header .icon { grid-column: auto / span 2; justify-self: left; } - + header:not(.open) .spacer, header:not(.open) .navigation-block { display: none; } + + .dropdown-menu { + position: static; + transform: none; + display: flex; + box-shadow: none; + border: none; + padding: 0 0 0 1em; + } + + .dropdown-toggle { + display: none; + } } /* HERO */ .hero { - margin: 2rem 0; + margin: 2.5rem 0 3rem; + text-align: center; +} + +/* Hero wrapped in the terminal window: the body is always dark, so the + logo, text and spacing are tuned for the dark terminal background. */ +.term-hero .term-body { + padding: 2.5rem 1.5rem 3rem; +} + +.term-hero .hero { + margin: 0; +} + +.term-hero .hero-title { + color: var(--terminal-fg-color); +} + +.term-hero .hero-tagline { + color: var(--term-head-fg); } .hero img { display: block; - height: 14em; - margin: auto; + height: 11em; + margin: 0 auto 1.2rem; } -.hero div { - font-size: 3.75rem; - line-height: 1; - text-align: center; - padding-bottom: 0.5rem; - font-weight: bold; +.hero-prompt { + font-family: var(--font-mono); + font-size: 0.95rem; + color: var(--muted-fg-color); + margin-bottom: 0.7rem; +} + +.term-hero .hero-prompt { + color: var(--term-comment); +} + +.hero-prompt .pr { + color: var(--accent-color); +} + +.hero-title { + font-family: var(--font-mono); + font-size: 3rem; + line-height: 1.05; + letter-spacing: -0.04em; + font-weight: 700; +} + +.hero-tagline { + max-width: 40rem; + margin: 1rem auto 2rem; + font-size: 1.2rem; + line-height: 1.55; + color: var(--muted-fg-color); +} + +.hero-cta { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 1rem; } @media (min-width: 640px) { - .hero div { - font-size: 6rem; - line-height: 1; + .hero-title { + font-size: 4.5rem; + line-height: 1.02; } } +/* BUTTONS */ +.btn { + display: inline-block; + padding: 0.6em 1.4em; + border-radius: var(--radius); + border: 1px solid var(--accent-color); + font-family: var(--font-mono); + font-size: 1rem; + font-weight: 600; + text-align: center; + cursor: pointer; + transition: background-color 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s; +} + +.btn-primary { + background-color: var(--accent-color); + color: var(--dark-fg-color); +} + +.btn-primary:hover { + background-color: var(--accent-hover); + border-color: var(--accent-hover); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +.btn-ghost { + background-color: transparent; + color: var(--link-text-color); +} + +.btn-ghost:hover { + background-color: var(--accent-color); + color: var(--dark-fg-color); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + /* MAIN */ main { margin: 1rem auto; @@ -211,17 +526,22 @@ main { } } +h1, +h2, +h3 { + font-family: var(--font-mono); + letter-spacing: -0.02em; +} + h1 { font-size: 1.5rem; - font-weight: 900; - line-height: 2.25rem; + font-weight: 700; line-height: 1.25; margin-bottom: 2rem; } p { font-size: 1rem; - line-height: 1.5rem; line-height: 1.625; margin-bottom: 2rem; } @@ -241,7 +561,6 @@ p { h2 { font-size: 1.5rem; font-weight: 700; - line-height: 2rem; line-height: 1.25; margin-bottom: 1.5rem; } @@ -273,75 +592,266 @@ ul { /* FOOTER */ footer { align-items: center; - background-color: var(--fg-color); - color: var(--bg-color); + background-color: var(--bg-color); + color: var(--fg-color); + border-top: 1px solid var(--header-border-color); display: flex; + flex-wrap: wrap; + gap: 0.5rem 1.5rem; flex-grow: 0; flex-shrink: 1; - font-size: 0.75rem; + font-family: var(--font-mono); + font-size: 0.82rem; justify-content: space-between; - line-height: 1rem; - padding: 0.5rem 1rem; + line-height: 1.4; + padding: 1rem 2rem; width: 100%; } +.footer-links { + display: flex; + flex-wrap: wrap; + gap: 1.25rem; +} + +.footer-links a { + color: var(--fg-color); + border-bottom: 2px solid transparent; +} + +.footer-links a:hover { + border-bottom: 2px solid var(--link-text-color); +} + +.footer-brand { + display: flex; + align-items: center; + gap: 0.5rem; +} + .github-icon { - background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); + background-image: var(--github-icon); height: 1.25rem; width: 1.25rem; } .github-icon-black { - background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='000' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E"); + background-image: var(--github-icon-black); height: 1em; width: 1em; } +@media (prefers-color-scheme: dark) { + .github-icon-black { + background-image: var(--github-icon); + } +} + +/* TERMINAL WINDOW */ +.term { + background: var(--terminal-bg-color); + border: 1px solid var(--card-border); + border-radius: 0.6em; + overflow: hidden; + box-shadow: var(--shadow-md); + margin: 2rem 0; +} + +.term-bar { + position: relative; + display: flex; + justify-content: flex-end; + align-items: center; + gap: 0.35rem; + padding: 0.4rem 0.5rem; + background: var(--term-head); + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + +.term-bar .t { + position: absolute; + left: 0; + right: 0; + text-align: center; + font-family: var(--font-mono); + font-size: 0.8rem; + color: var(--term-head-fg); + pointer-events: none; +} + +.win-btn { + width: 24px; + height: 24px; + border-radius: 50%; + display: grid; + place-items: center; + background: rgba(255, 255, 255, 0.09); + color: var(--term-head-fg); + transition: background 0.12s, color 0.12s; +} + +.win-btn svg { + width: 14px; + height: 14px; +} + +.win-btn:hover { + background: rgba(255, 255, 255, 0.18); +} + +.win-btn.close:hover { + background: #e01b24; + color: #fff; +} + +.term-body { + padding: 1.1rem 1.3rem; + font-family: var(--font-mono); + color: var(--terminal-fg-color); + overflow-x: auto; +} + +.term-prompt { + color: var(--term-comment); + margin-bottom: 0.8rem; + font-size: 0.92rem; +} + +.term-prompt .pr { + color: var(--accent-color); +} + +/* PROJECT LIST (command output) */ .projects { display: flex; flex-direction: column; - flex-wrap: wrap; - justify-content: space-between; - width: 100%; - gap: 2rem; - margin-bottom: 1.5rem; } -.project { - border-left: 0.3em solid gray; - padding: 1rem; +.prow { + display: grid; + grid-template-columns: 9.5rem 1fr auto; + gap: 1rem; + align-items: baseline; + padding: 0.7rem 0.3rem; + border-bottom: 1px dashed var(--card-border); text-decoration: none; - color: var(--fg-color); - transition: text-decoration 0.3s; - transition: border-color 0.3s; + color: var(--terminal-fg-color); + transition: background 0.12s; } -.project h2 { - margin-top: 0; - font-family: monospace; +.prow:last-child { + border-bottom: none; +} + +.prow:hover { + background: color-mix(in srgb, var(--accent-color) 12%, transparent); +} + +.prow .name { + font-family: var(--font-mono); + font-weight: 700; + color: var(--accent-color); +} + +.prow .name::before { + content: "\203A "; + color: var(--term-comment); +} + +.prow .desc { + font-family: var(--font-mono); + font-size: 0.9rem; + color: var(--term-head-fg); +} + +.prow .desc code { + color: var(--term-blue); + background: none; + padding: 0; + font-size: 0.95em; +} + +.st { + font-family: var(--font-mono); + font-weight: 700; + font-size: 0.85rem; + white-space: nowrap; +} + +.st::before { + content: "["; + color: var(--term-comment); +} + +.st::after { + content: "]"; + color: var(--term-comment); } -.project p { - margin-bottom: 0; +.st-ready { + color: var(--st-ready); } -.project>span { - color: transparent; - transition: color 0.3s; +.st-beta { + color: var(--st-beta); } -.project:hover>span { - color: black; +.st-alpha { + color: var(--st-alpha); } -.project:hover { - border-color: #c04828; +.st-wip { + color: var(--st-wip); +} + +@media (max-width: 640px) { + .prow { + grid-template-columns: 1fr auto; + } + + .prow .desc { + grid-column: 1 / -1; + } +} + +/* STATUS BADGES (project detail page titles) */ +.badge { + display: inline-block; + font-size: 0.7rem; + font-weight: 700; + padding: 0.2em 0.6em; + border-radius: 0.3em; + vertical-align: middle; + margin-left: 0.6em; + font-family: var(--font-mono); + white-space: nowrap; +} + +.badge-ready { + background: var(--badge-ready-bg); + color: var(--badge-ready-fg); +} + +.badge-beta { + background: var(--badge-beta-bg); + color: var(--badge-beta-fg); +} + +.badge-alpha { + background: var(--badge-alpha-bg); + color: var(--badge-alpha-fg); +} + +.badge-wip { + background: var(--badge-wip-bg); + color: var(--badge-wip-fg); } .title { font-size: 3em; - font-weight: 900; + font-weight: 700; margin-bottom: 0.5em; + text-transform: capitalize; } @media (min-width: 640px) { @@ -351,23 +861,130 @@ footer { } } +/* Terminal prompt line under a project-page title, echoing the hero motif. */ +.page-prompt { + font-family: var(--font-mono); + font-size: 0.95rem; + color: var(--muted-fg-color); + margin: -0.25em 0 1.4em; +} + +.page-prompt .pr { + color: var(--accent-color); +} + +/* GSOC PROJECT IDEAS (card grid) */ +.gsoc-ideas { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr)); + gap: 1rem; + margin: 2rem 0; +} + +.gsoc-card { + border: 1px solid var(--card-border); + border-radius: var(--radius); + background: var(--card-bg); + padding: 1.1rem 1.2rem; + transition: border-color 0.15s, box-shadow 0.15s, transform 0.15s; +} + +.gsoc-card:hover { + border-color: var(--accent-color); + box-shadow: var(--shadow-md); + transform: translateY(-2px); +} + +.gsoc-card-title { + font-family: var(--font-mono); + font-size: 1.05rem; + font-weight: 700; + color: var(--accent-color); + margin: 0 0 0.6rem; + line-height: 1.3; +} + +.gsoc-card-title::before { + content: "\203A "; + color: var(--muted-fg-color); +} + +.gsoc-meta { + display: flex; + flex-wrap: wrap; + gap: 0.4rem; + margin: 0 0 0.7rem; +} + +.gsoc-badge { + font-family: var(--font-mono); + font-size: 0.72rem; + font-weight: 700; + padding: 0.18em 0.6em; + border-radius: 0.3em; + white-space: nowrap; + background: var(--badge-wip-bg); + color: var(--badge-wip-fg); +} + +.gsoc-badge.diff-easy { + background: var(--badge-ready-bg); + color: var(--badge-ready-fg); +} + +.gsoc-badge.diff-medium { + background: var(--badge-beta-bg); + color: var(--badge-beta-fg); +} + +.gsoc-badge.diff-hard { + background: #fde8e8; + color: #b42318; +} + +@media (prefers-color-scheme: dark) { + .gsoc-badge.diff-hard { + background: #3a1c1c; + color: #f3a39a; + } +} + +.gsoc-card p { + margin: 0 0 0.6rem; +} + +.gsoc-card ul { + margin: 0 0 0.6rem; + padding-left: 1.1rem; +} + +.gsoc-card li { + margin: 0.15rem 0; +} + +.gsoc-skills { + font-size: 0.85rem; + color: var(--muted-fg-color); + margin: 0; +} + .details { margin-bottom: 1.5em; - color: #333; + color: var(--fg-color); } .post-thingy { display: block; padding: 1em; - color: black; - background: #e5e5e5; + color: var(--fg-color); + background: var(--post-bg-color); border-radius: 0.3em; - border: 0.2em solid #e5e5e5; + border: 0.2em solid var(--post-bg-color); margin-bottom: 0.5em; } .post-thingy:hover { - border-color: #c04828; + border-color: var(--link-text-color); background: transparent; } @@ -380,18 +997,366 @@ blockquote { padding-left: 1em; } +main img { + max-width: 100%; + height: auto; +} + .links { display: flex; + flex-wrap: wrap; justify-content: stretch; - gap: 1em; + gap: 0.8em; margin-bottom: 2em; - font-size: 1.2em; + font-family: var(--font-mono); + font-size: 1rem; } .links > a { - padding: 0.2em 1em; - color: #c04828; - border: 2px solid #c04828; + padding: 0.45em 1em; + color: var(--link-text-color); + border: 1px solid var(--accent-color); + border-radius: var(--radius); flex: 1; text-align: center; + transition: background-color 0.2s, color 0.2s, transform 0.2s, box-shadow 0.2s; +} + +.links > a:hover { + color: var(--dark-fg-color); + background-color: var(--accent-color); + border-color: var(--accent-color); + transform: translateY(-1px); + box-shadow: var(--shadow-md); +} + +/* WASM PLAYGROUND */ +.playground-toolbar { + display: flex; + align-items: center; + gap: 0.5em; + margin: 1em 0 0; + font-size: 0.9em; +} + +.playground-toolbar select { + padding: 0.3em 0.5em; + border-radius: 0.3em; + border: 1px solid var(--header-border-color); + background: var(--highlight-bg-color); + color: var(--highlight-fg-color); + font-size: 1em; + cursor: pointer; +} + +.playground-loaders { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5em; + margin: 0.6em 0 0; + font-size: 0.9em; +} + +.playground-loaders-label { + opacity: 0.8; +} + +.playground-loader { + padding: 0.3em 0.7em; + background: var(--highlight-bg-color); + color: var(--highlight-fg-color); + border: 1px solid var(--header-border-color); + border-radius: 0.3em; + font-family: "Fira Code", monospace; + font-size: 0.85em; + cursor: pointer; + transition: border-color 0.2s, background-color 0.2s, opacity 0.2s; +} + +.playground-loader:hover:not(:disabled) { + border-color: var(--accent-color); + background: var(--accent-color); + color: white; +} + +.playground-loader:disabled { + cursor: default; +} + +.playground-loader.loaded { + border-color: #2ea043; + background: #2ea043; + color: white; + opacity: 0.9; +} + +.playground-share { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.6em; + margin: -0.5em 0 1.2em; + font-size: 0.9em; +} + +#playground-share-btn { + padding: 0.3em 0.7em; + background: var(--highlight-bg-color); + color: var(--highlight-fg-color); + border: 1px solid var(--header-border-color); + border-radius: 0.3em; + font-family: "Fira Code", monospace; + font-size: 0.85em; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.2s, background-color 0.2s, opacity 0.2s; +} + +#playground-share-btn:hover:not(:disabled) { + border-color: var(--accent-color); + background: var(--accent-color); + color: white; +} + +#playground-share-btn:disabled { + cursor: default; + opacity: 0.6; +} + +#playground-share-btn.copied { + border-color: #2ea043; + background: #2ea043; + color: white; +} + +.playground-share-hint { + opacity: 0.75; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + min-width: 0; +} + +#wasm-playground { + width: 100%; + min-height: 480px; + border-radius: 0.5em; + overflow: hidden; + margin: 1.5em 0; + border: 1px solid var(--header-border-color); +} + +/* Playground wrapped in the terminal-window chrome: the term frame supplies + the border, rounding and shadow, so the inner element drops its own. */ +.term-playground { + margin: 1.5em 0; +} + +.term-playground #wasm-playground { + margin: 0; + border: none; + border-radius: 0; +} + +.playground-version { + margin: -0.5em 0 1.5em; + font-size: 0.85em; + color: var(--muted-fg-color, inherit); + opacity: 0.8; +} + +.playground-version:empty { + display: none; +} + +#wasm-playground .wasm-loading { + display: flex; + align-items: center; + justify-content: center; + height: 480px; + background: var(--terminal-bg-color); + color: var(--terminal-fg-color); + font-family: monospace; + font-size: 1.1em; +} + +/* INLINE WASM EXAMPLES */ +.wasm-example { + margin: 1em 0; +} + +.wasm-example-code { + display: flex; + align-items: stretch; + gap: 0; +} + +.wasm-example-code pre { + flex: 1; + padding: 0.8em 1em; + background: var(--highlight-bg-color); + color: var(--highlight-fg-color); + border-radius: 0.3em 0 0 0.3em; + font-family: "Fira Code", monospace; + font-size: 0.9em; + overflow-x: auto; + margin: 0; +} + +.wasm-run-btn { + padding: 0.8em 1.5em; + background: var(--accent-color); + color: white; + border: none; + border-radius: 0 0.3em 0.3em 0; + cursor: pointer; + font-size: 0.9em; + font-weight: 600; + transition: background-color 0.2s; + white-space: nowrap; +} + +.wasm-run-btn:hover { + background: var(--accent-hover); +} + +.wasm-run-btn:disabled { + opacity: 0.6; + cursor: wait; +} + +.wasm-example-output { + margin-top: 0; +} + +.wasm-example-output pre { + padding: 0.8em 1em; + background: var(--terminal-bg-color); + color: var(--terminal-fg-color); + border-radius: 0 0 0.3em 0.3em; + font-family: "Fira Code", monospace; + font-size: 0.9em; + overflow-x: auto; + max-height: 300px; + overflow-y: auto; + margin: 0; +} + +/* PLAYGROUND CLICKABLE EXAMPLES */ +.playground-examples { + display: flex; + flex-wrap: wrap; + gap: 0.5em; + margin: 1em 0 2em; +} + +.playground-example { + padding: 0.4em 0.8em; + background: var(--highlight-bg-color); + color: var(--highlight-fg-color); + border: 1px solid var(--header-border-color); + border-radius: 0.3em; + font-family: "Fira Code", monospace; + font-size: 0.85em; + cursor: pointer; + transition: border-color 0.2s, background-color 0.2s; +} + +.playground-example:hover { + border-color: var(--accent-color); + background: var(--accent-color); + color: white; +} + +/* COPY-TO-CLIPBOARD ON CODE BLOCKS */ +.code-block-wrapper { + position: relative; +} + +.copy-btn { + position: absolute; + top: 0.5em; + right: 0.5em; + padding: 0.25em 0.6em; + font-size: 0.75rem; + font-family: var(--font-face); + font-weight: 600; + color: var(--highlight-fg-color); + background: var(--highlight-bg-color); + border: 1px solid var(--header-border-color); + border-radius: var(--radius); + cursor: pointer; + opacity: 0; + transition: opacity 0.2s, background-color 0.2s, color 0.2s; +} + +.code-block-wrapper:hover .copy-btn, +.copy-btn:focus-visible { + opacity: 1; +} + +.copy-btn:hover { + color: var(--dark-fg-color); + background: var(--accent-color); + border-color: var(--accent-color); +} + +.copy-btn.copied { + opacity: 1; + color: var(--badge-ready-fg); + background: var(--badge-ready-bg); + border-color: var(--badge-ready-fg); +} + +/* REDUCED MOTION */ +@media (prefers-reduced-motion: reduce) { + html, + body { + scroll-behavior: auto; + } + + *, + ::before, + ::after { + transition: none !important; + animation: none !important; + } +} + +/* HOMEPAGE CONTENT (scoped) */ +.home-content h1 { + font-size: 1.7rem; + margin-top: 3.5rem; + margin-bottom: 1.2rem; +} + +.home-content h1::before { + content: "> "; + color: var(--accent-color); +} + +@media (min-width: 640px) { + .home-content h1 { + font-size: 2.1rem; + } +} + +.home-content ul { + list-style: none; + padding-left: 0; +} + +.home-content li { + position: relative; + padding-left: 1.5rem; +} + +.home-content li::before { + content: "+"; + position: absolute; + left: 0; + color: var(--st-ready); + font-family: var(--font-mono); + font-weight: 700; } diff --git a/templates/base.html b/templates/base.html index 61e9395ab..1bd53ff35 100644 --- a/templates/base.html +++ b/templates/base.html @@ -1,5 +1,5 @@ - + @@ -9,30 +9,69 @@ + + + + + +
- {% include "logo.html" %} + {% include "logo.html" %}uutils_
-