chore: import upstream snapshot with attribution
@@ -0,0 +1,42 @@
|
||||
version: 2.1
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
docker:
|
||||
- image: cimg/node:22.9
|
||||
resource_class: medium
|
||||
steps:
|
||||
- add_ssh_keys:
|
||||
fingerprints:
|
||||
- "86:3b:c9:a6:d1:b9:a8:dc:0e:00:db:99:8d:19:c4:3e"
|
||||
- run:
|
||||
name: Add known hosts
|
||||
command: |
|
||||
mkdir -p ~/.ssh
|
||||
echo $GH_HOST >> ~/.ssh/known_hosts
|
||||
echo $HAVANA_HOST >> ~/.ssh/known_hosts
|
||||
- run:
|
||||
name: Install NPM
|
||||
command: |
|
||||
sudo apt-get update && sudo apt-get install -y rsync npm
|
||||
- run:
|
||||
name: Clone WebVM
|
||||
command: |
|
||||
git clone --branch $CIRCLE_BRANCH --single-branch git@github.com:leaningtech/webvm.git
|
||||
- run:
|
||||
name: Build WebVM
|
||||
command: |
|
||||
cd webvm/
|
||||
npm install
|
||||
npm run build
|
||||
- run:
|
||||
name: Deploy webvm
|
||||
command: |
|
||||
rsync -avz --chown circleci:runtimes webvm/build/ circleci@havana.leaningtech.com:webvm/
|
||||
|
||||
workflows:
|
||||
deploy:
|
||||
when:
|
||||
equal: [ << pipeline.trigger_source >>, "api" ]
|
||||
jobs:
|
||||
- deploy
|
||||
@@ -0,0 +1,245 @@
|
||||
name: Deploy
|
||||
|
||||
# Define when the workflow should run
|
||||
on:
|
||||
# Allow manual triggering of the workflow from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
# Allow inputs to be passed when manually triggering the workflow from the Actions tab
|
||||
inputs:
|
||||
DOCKERFILE_PATH:
|
||||
type: string
|
||||
description: 'Path to the Dockerfile'
|
||||
required: true
|
||||
default: 'dockerfiles/debian_mini'
|
||||
|
||||
IMAGE_SIZE:
|
||||
type: string
|
||||
description: 'Image size, 950M max'
|
||||
required: true
|
||||
default: '750M'
|
||||
|
||||
DEPLOY_TO_GITHUB_PAGES:
|
||||
type: boolean
|
||||
description: 'Deploy to Github pages'
|
||||
required: true
|
||||
default: true
|
||||
|
||||
GITHUB_RELEASE:
|
||||
type: boolean
|
||||
description: 'Upload GitHub release'
|
||||
required: true
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
|
||||
guard_clause:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }} # As required by the GitHub-CLI
|
||||
|
||||
permissions:
|
||||
actions: 'write' # Required in order to terminate the workflow run.
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Guard clause that cancels the workflow in case of an invalid DOCKERFILE_PATH and/or incorrectly configured Github Pages.
|
||||
# The main reason for choosing this workaround for aborting the workflow is the fact that it does not display the workflow as successful, which can set false expectations.
|
||||
- name: DOCKERFILE_PATH.
|
||||
shell: bash
|
||||
run: |
|
||||
# We check whether the Dockerfile_path is valid.
|
||||
if [ ! -f ${{ github.event.inputs.DOCKERFILE_PATH }} ]; then
|
||||
echo "::error title=Invalid Dockerfile path::No file found at ${{ github.event.inputs.DOCKERFILE_PATH }}"
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Github Pages config guard clause
|
||||
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
|
||||
run: |
|
||||
# We use the Github Rest api to get information regarding pages for the Github Repository and store it into a temporary file named "pages_response".
|
||||
set +e
|
||||
gh api \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
/repos/${{ github.repository_owner }}/$(basename ${{ github.repository }})/pages > pages_response
|
||||
|
||||
# We make sure Github Pages has been enabled for this repository.
|
||||
if [ "$?" -ne 0 ]; then
|
||||
echo "::error title=Potential pages configuration error.::Please make sure you have enabled Github pages for the ${{ github.repository }} repository. If already enabled then Github pages might be down"
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
set -e
|
||||
|
||||
# We make sure the Github pages build & deployment source is set to "workflow" (Github Actions). Instead of a "legacy" (branch).
|
||||
if [[ "$(jq --compact-output --raw-output .build_type pages_response)" != "workflow" ]]; then
|
||||
echo "Undefined behaviour, Make sure the Github Pages source is correctly configured in the Github Pages settings."
|
||||
echo "::error title=Pages configuration error.::Please make sure you have correctly picked \"Github Actions\" as the build and deployment source for the Github Pages."
|
||||
echo "terminate=true" >> $GITHUB_ENV
|
||||
fi
|
||||
rm pages_response
|
||||
|
||||
- name: Terminate run if error occurred.
|
||||
run: |
|
||||
if [[ $terminate == "true" ]]; then
|
||||
gh run cancel ${{ github.run_id }}
|
||||
gh run watch ${{ github.run_id }}
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: guard_clause # Dependency
|
||||
runs-on: ubuntu-latest # Image to run the worker on.
|
||||
|
||||
env:
|
||||
TAG: "ext2-webvm-base-image" # Tag of docker image.
|
||||
IMAGE_SIZE: '${{ github.event.inputs.IMAGE_SIZE }}'
|
||||
DEPLOY_DIR: /webvm_deploy/ # Path to directory where we host the final image from.
|
||||
|
||||
permissions: # Permissions to grant the GITHUB_TOKEN.
|
||||
contents: write # Required permission to make a github release.
|
||||
|
||||
steps:
|
||||
# Checks-out our repository under $GITHUB_WORKSPACE, so our job can access it
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
# Setting the IMAGE_NAME variable in GITHUB_ENV to <Dockerfile name>_<date>_<run_id>.ext2.
|
||||
- name: Generate the image_name.
|
||||
id: image_name_gen
|
||||
run: |
|
||||
echo "IMAGE_NAME=$(basename ${{ github.event.inputs.DOCKERFILE_PATH }})_$(date +%Y%m%d)_${{ github.run_id }}.ext2" >> $GITHUB_ENV
|
||||
|
||||
# Create directory to host the image from.
|
||||
- run: sudo mkdir -p $DEPLOY_DIR
|
||||
|
||||
# Build the i386 Dockerfile image.
|
||||
- run: docker build . --tag $TAG --file ${{ github.event.inputs.DOCKERFILE_PATH }} --platform=i386
|
||||
|
||||
# Run the docker image so that we can export the container.
|
||||
# Run the Docker container with the Google Public DNS nameservers: 8.8.8.8, 8.8.4.4
|
||||
- run: |
|
||||
docker run --dns 8.8.8.8 --dns 8.8.4.4 -d $TAG
|
||||
echo "CONTAINER_ID=$(sudo docker ps -aq)" >> $GITHUB_ENV
|
||||
|
||||
# We extract the CMD, we first need to figure whether the Dockerfile uses CMD or an Entrypoint.
|
||||
- name: Extracting CMD / Entrypoint and args
|
||||
shell: bash
|
||||
run: |
|
||||
cmd=$(sudo docker inspect --format='{{json .Config.Cmd}}' $CONTAINER_ID)
|
||||
entrypoint=$(sudo docker inspect --format='{{json .Config.Entrypoint}}' $CONTAINER_ID)
|
||||
if [[ $entrypoint != "null" && $cmd != "null" ]]; then
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd' )" >> $GITHUB_ENV
|
||||
elif [[ $cmd != "null" ]]; then
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[:1]' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Cmd[1:]' )" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CMD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[:1]' )" >> $GITHUB_ENV
|
||||
echo "ARGS=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Entrypoint[1:]' )" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# We extract the ENV, CMD/Entrypoint and cwd from the Docker container with docker inspect.
|
||||
- name: Extracting env, args and cwd.
|
||||
shell: bash
|
||||
run: |
|
||||
echo "ENV=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.Env' )" >> $GITHUB_ENV
|
||||
echo "CWD=$( sudo docker inspect $CONTAINER_ID | jq --compact-output '.[0].Config.WorkingDir' )" >> $GITHUB_ENV
|
||||
|
||||
# We create and mount the base ext2 image to extract the Docker container's filesystem its contents into.
|
||||
- name: Create ext2 image.
|
||||
run: |
|
||||
# Preallocate space for the ext2 image
|
||||
sudo fallocate -l $IMAGE_SIZE ${IMAGE_NAME}
|
||||
# Format to ext2 linux kernel revision 0
|
||||
sudo mkfs.ext2 -r 0 ${IMAGE_NAME}
|
||||
# Mount the ext2 image to modify it
|
||||
sudo mount -o loop -t ext2 ${IMAGE_NAME} /mnt/
|
||||
|
||||
# We opt for 'docker cp --archive' over 'docker save' since our focus is solely on the end product rather than individual layers and metadata.
|
||||
# However, it's important to note that despite being specified in the documentation, the '--archive' flag does not currently preserve uid/gid information when copying files from the container to the host machine.
|
||||
# Another compelling reason to use 'docker cp' is that it preserves resolv.conf.
|
||||
- name: Export and unpack container filesystem contents into mounted ext2 image.
|
||||
run: |
|
||||
sudo docker cp -a ${CONTAINER_ID}:/ /mnt/
|
||||
sudo umount /mnt/
|
||||
# Result is an ext2 image for webvm.
|
||||
|
||||
# The .txt suffix enabled HTTP compression for free
|
||||
- name: Generate image split chunks and .meta file
|
||||
run: |
|
||||
sudo split ${{ env.IMAGE_NAME }} ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.c -a 6 -b 128k -x --additional-suffix=.txt
|
||||
sudo bash -c "stat -c%s ${{ env.IMAGE_NAME }} > ${{ env.DEPLOY_DIR }}/${{ env.IMAGE_NAME }}.meta"
|
||||
|
||||
# This step updates the default config_github_terminal.js file by performing the following actions:
|
||||
# 1. Replaces all occurrences of IMAGE_URL with the URL to the image.
|
||||
# 2. Replace CMD with the Dockerfile entry command.
|
||||
# 3. Replace args with the Dockerfile CMD / Entrypoint args.
|
||||
# 4. Replace ENV with the container's environment values.
|
||||
# 5. Replace CWD with the container's current working directory.
|
||||
- name: Adjust config_github_terminal.js
|
||||
run: |
|
||||
sed -i 's#IMAGE_URL#"${{ env.IMAGE_NAME }}"#g' config_github_terminal.js
|
||||
sed -i 's#CMD#${{ env.CMD }}#g' config_github_terminal.js
|
||||
sed -i 's#ARGS#${{ env.ARGS }}#g' config_github_terminal.js
|
||||
sed -i 's#ENV#${{ env.ENV }}#g' config_github_terminal.js
|
||||
sed -i 's#CWD#${{ env.CWD }}#g' config_github_terminal.js
|
||||
|
||||
- name: Build NPM package
|
||||
run: |
|
||||
npm install
|
||||
WEBVM_MODE=github npm run build
|
||||
|
||||
# Move required files for gh-pages deployment to the deployment directory $DEPLOY_DIR.
|
||||
- name: Copy build
|
||||
run: |
|
||||
rm build/alpine.html
|
||||
sudo mv build/* $DEPLOY_DIR/
|
||||
|
||||
# We generate index.list files for our httpfs to function properly.
|
||||
- name: make index.list
|
||||
shell: bash
|
||||
run: |
|
||||
find $DEPLOY_DIR -type d | while read -r dir;
|
||||
do
|
||||
index_list="$dir/index.list";
|
||||
sudo rm -f "$index_list";
|
||||
sudo ls "$dir" | sudo tee "$index_list" > /dev/null;
|
||||
sudo chmod +rw "$index_list";
|
||||
sudo echo "created $index_list";
|
||||
done
|
||||
|
||||
# Create a gh-pages artifact in order to deploy to gh-pages.
|
||||
- name: Upload GitHub Pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Path of the directory containing the static assets for our gh pages deployment.
|
||||
path: ${{ env.DEPLOY_DIR }} # optional, default is _site/
|
||||
|
||||
- name: github release # To upload our final ext2 image as a github release.
|
||||
if: ${{ github.event.inputs.GITHUB_RELEASE == 'true' }}
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
target_commitish: ${{ github.sha }} # Last commit on the GITHUB_REF branch or tag
|
||||
tag_name: ext2_image
|
||||
fail_on_unmatched_files: 'true' # Fail in case of no matches with the file(s) glob(s).
|
||||
files: | # Assets to upload as release.
|
||||
${{ env.IMAGE_NAME }}
|
||||
|
||||
deploy_to_github_pages: # Job that deploys the github-pages artifact to github-pages.
|
||||
if: ${{ github.event.inputs.DEPLOY_TO_GITHUB_PAGES == 'true' }}
|
||||
needs: build
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
|
||||
# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
|
||||
permissions:
|
||||
pages: write # to deploy to Pages
|
||||
id-token: write # to verify the deployment originates from an appropriate source
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Deployment to github pages
|
||||
- name: Deploy GitHub Pages site
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
@@ -0,0 +1,9 @@
|
||||
/node_modules
|
||||
/.svelte-kit
|
||||
pnpm-lock.yaml
|
||||
build/
|
||||
custom-disk-images/*
|
||||
!custom-disk-images/.gitkeep
|
||||
nginx_access.log
|
||||
nginx_error.log
|
||||
nginx_main_error.log
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,248 @@
|
||||
# WebVM
|
||||
|
||||
[](https://discord.gg/yWRr2YnD9c)
|
||||
[](https://github.com/leaningtech/webvm/issues)
|
||||
|
||||
This repository hosts the source code for [https://webvm.io](https://webvm.io), a Linux virtual machine that runs in your browser.
|
||||
|
||||
Try out the new Alpine / Xorg / i3 graphical environment: [https://webvm.io/alpine.html](https://webvm.io/alpine.html)
|
||||
|
||||
<img src="/assets/welcome_to_WebVM_alpine_2024.png" width="90%">
|
||||
|
||||
## What is WebVM?
|
||||
|
||||
WebVM is a server-less virtual environment running fully client-side in HTML5/WebAssembly. It's designed to be Linux ABI-compatible and runs an unmodified Debian distribution including many native development toolchains.
|
||||
|
||||
WebVM is powered by the **CheerpX** virtualization engine, which provides:
|
||||
|
||||
- x86-to-WebAssembly JIT compiler
|
||||
- Virtual block-based file system
|
||||
- Linux syscall emulator
|
||||
- Safe, sandboxed client-side execution
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Networking](#networking)
|
||||
- [Development & Customization](#development--customization)
|
||||
- [Deploy to GitHub Pages](#deploy-to-github-pages)
|
||||
- [Local Serving & Image Configuration](#local-serving--image-configuration)
|
||||
- [Claude AI Integration](#claude-ai-integration)
|
||||
- [Community & Support](#community--support)
|
||||
- [Learn More](#learn-more)
|
||||
- [License](#license)
|
||||
|
||||
> [!NOTE]
|
||||
> Visit [https://webvm.io](https://webvm.io) to get started immediately in your browser. No setup required.
|
||||
>
|
||||
> For local setup, custom image builds, and fork/deploy guidance, see [Development & Customization](#development--customization).
|
||||
|
||||
## Networking
|
||||
|
||||
WebVM supports **Tailscale** integration. So your browser VM can reach your private network and, with an exit node, the public internet too.
|
||||
|
||||
> [!NOTE]
|
||||
> Some low-level networking operations (especially ICMP used by `ping`) are not currently available in this environment. For connectivity checks, use `curl` or `wget`.
|
||||
|
||||
### Local network
|
||||
|
||||
1. Open the "Networking" panel from the sidebar
|
||||
2. Click "Connect to Tailscale"
|
||||
3. Log in (create a free account at [tailscale.com](https://tailscale.com) if needed)
|
||||
4. Click "Connect" when prompted
|
||||
|
||||
WebVM now has access to all machines in your Tailscale network!
|
||||
|
||||
### Internet Usage Tips
|
||||
|
||||
> [!TIP]
|
||||
> On slower connections there may be a short delay before initialisation. Connection status is shown as a colored dot on the button: orange = local network, green = global/internet. The button text shows your Tailscale IP address once connected.
|
||||
|
||||
To access the public internet from WebVM, set up an **Exit Node** on another device in your Tailscale network:
|
||||
|
||||
1. Follow the [Tailscale Exit Node quickstart](https://tailscale.com/kb/1408/quick-guide-exit-nodes?tab=linux) (sections: "Advertise a device as an exit node")
|
||||
2. WebVM automatically uses the exit node once advertised
|
||||
|
||||
### Using an Auth Key
|
||||
|
||||
As an alternative to interactive login, add your Tailscale auth key to the URL fragment:
|
||||
|
||||
```
|
||||
https://webvm.io/#authKey=<your-ephemeral-key>
|
||||
```
|
||||
|
||||
This is equivalent to Tailscale's `--login-server` option.
|
||||
|
||||
> [!TIP]
|
||||
> If you also need a custom control server, add `controlUrl` in the same URL fragment and separate values with `&`, for example: `#authKey=...&controlUrl=...`.
|
||||
|
||||
### Self-Hosting Tailscale with Headscale
|
||||
|
||||
We also support [headscale](https://headscale.net/stable/), a selfhosted open source implementation of the Tailscale control server.
|
||||
Because Headscale does not add CORS headers by default, you will need a proxy in front of it. See the [Headscale reverse proxy setup docs](https://headscale.net/stable/ref/integration/reverse-proxy/#nginx) for an example.
|
||||
|
||||
Once ready, add the following line to your `location /` block in your nginx config file.
|
||||
|
||||
```nginx
|
||||
if ($http_origin = "https://yourdomain.com") {
|
||||
add_header 'Access-Control-Allow-Origin' "$http_origin";
|
||||
add_header 'Access-Control-Allow-Credentials' 'true' always;
|
||||
}
|
||||
```
|
||||
|
||||
Then access WebVM with:
|
||||
|
||||
```
|
||||
https://yourdomain.com/#controlUrl=<your-headscale-url>
|
||||
```
|
||||
|
||||
## Development & Customization
|
||||
|
||||
> [!NOTE]
|
||||
> Users have root privileges by default. `sudo` is not installed though this can easily be added to the Dockerfile if needed.
|
||||
|
||||
### Deploy to GitHub Pages
|
||||
|
||||
This is a simple, beginner-friendly workflow. For local hosting and customization, see below [Local Serving & Image Configuration](#local-serving--image-configuration).
|
||||
|
||||
Fork the WebVM repository to deploy your own version to GitHub Pages:
|
||||
|
||||
<img src="/assets/fork_deploy_instructions.gif" alt="deploy_instructions_gif" width="90%">
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Enable GitHub Pages** _via forked repository_ in Settings → Pages using "GitHub Actions" as source
|
||||
3. **Run the `Deploy` workflow** from Actions
|
||||
4. After completion, open the URL shown under the `deploy_to_github_pages` job
|
||||
|
||||
<img src="/assets/result.png" width="70%">
|
||||
|
||||
The same `Deploy` workflow also builds custom `.ext2` disk images from a Dockerfile. You can point it at `dockerfiles/debian_mini` or another Dockerfile, then either publish the result as a GitHub Release asset or deploy the Pages build from your fork.
|
||||
|
||||
> [!NOTE]
|
||||
> `dockerfiles/debian_large` is too large of an image for GitHub pages.
|
||||
|
||||
### Local Serving & Image Configuration
|
||||
|
||||
#### 1. Clone the repository
|
||||
|
||||
```sh
|
||||
git clone https://github.com/leaningtech/webvm.git
|
||||
cd webvm
|
||||
```
|
||||
|
||||
#### 2. Put your image in `custom-disk-images/`
|
||||
|
||||
This repository includes a persistent `custom-disk-images/` directory for local `.ext2` files.
|
||||
|
||||
To use the official Debian mini image, download it from Releases:
|
||||
|
||||
[debian_mini_20230519_5022088024.ext2](https://github.com/leaningtech/webvm/releases/download/ext2_image/debian_mini_20230519_5022088024.ext2)
|
||||
|
||||
You can also copy in an image you built yourself.
|
||||
|
||||
#### 3. Point WebVM to your local image
|
||||
|
||||
Edit `config_public_terminal.js`:
|
||||
|
||||
```js
|
||||
export const diskImageUrl =
|
||||
"/custom-disk-images/debian_mini_20230519_5022088024.ext2";
|
||||
export const diskImageType = "bytes";
|
||||
```
|
||||
|
||||
#### 4. Install dependencies and build
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
#### 5. Start Nginx and open WebVM
|
||||
|
||||
```sh
|
||||
nginx -p . -c nginx.conf
|
||||
```
|
||||
|
||||
Then open http://127.0.0.1:8081 and enjoy your local WebVM!
|
||||
|
||||
For the full Alpine desktop environment, see [leaningtech/alpine-image](https://github.com/leaningtech/alpine-image).
|
||||
|
||||
For more details, see [CheerpX Custom Images documentation](https://cheerpx.io/docs/guides/custom-images).
|
||||
|
||||
> [!TIP]
|
||||
> For Python3 REPL, the `Deploy` workflow takes into account the `CMD` specified in the Dockerfile.
|
||||
>
|
||||
> To build a REPL you can simply change `CMD [ "/bin/bash" ]` to `CMD [ "/usr/bin/python3" ]` and deploy.
|
||||
|
||||
## Claude AI Integration
|
||||
|
||||
To access Claude AI, you need an API key. Follow these steps to get started:
|
||||
|
||||
#### 1. Create an account
|
||||
|
||||
- Visit [Anthropic Console](https://console.anthropic.com/login) and sign up with your e-mail. You'll receive a sign in link to the Anthropic Console.
|
||||
|
||||
#### 2. Get your API key
|
||||
|
||||
- Once logged in, navigate to **Get API keys**.
|
||||
- Purchase the amount of credits you need. After completing the purchase, you'll be able to generate the key through the API console.
|
||||
|
||||
#### 3. Log in with your API key
|
||||
|
||||
- Navigate to your WebVM and hover over the robot icon. This will show the Claude AI Integration tab. For added convenience, you can click the pin button in the top right corner to keep the tab in place.
|
||||
- You'll see a prompt where you can insert your Claude API key.
|
||||
|
||||
#### 4. Start using Claude AI
|
||||
|
||||
- Once your API key is entered, you can begin interacting with Claude AI by asking questions such as:
|
||||
|
||||
**"Solve the CTF challenge at `/home/user/chall1.bin.` Note that the binary reads from stdin."**
|
||||
|
||||
<img src="/assets/webvm_claude_ctf.gif" alt="deploy_instructions_gif" width="90%">
|
||||
|
||||
**Important:** Your API key is private and should never be shared. We do not have access to your key, it is only stored locally in your browser.
|
||||
|
||||
## Community & Support
|
||||
|
||||
**Report issues:** Use [GitHub Issues](https://github.com/leaningtech/webvm/issues) to report bugs or request features.
|
||||
|
||||
**Chat with us:** Join our [Discord community](https://discord.gg/yTNZgySKGa) to discuss WebVM, share ideas, and get help.
|
||||
|
||||
## Learn More
|
||||
|
||||
**Articles & Resources:**
|
||||
|
||||
- [WebVM: Server-less x86 virtual machines in the browser](https://leaningtech.com/webvm-server-less-x86-virtual-machines-in-the-browser/)
|
||||
- [WebVM: Linux Virtualization in WebAssembly with Full Networking via Tailscale](https://leaningtech.com/webvm-virtual-machine-with-networking-via-tailscale/)
|
||||
- [Mini.WebVM: Your own Linux box from Dockerfile, virtualized in the browser via WebAssembly](https://leaningtech.com/mini-webvm-your-linux-box-from-dockerfile-via-wasm/)
|
||||
- [Crafting the Impossible: X86 Virtualization in the Browser with WebAssembly](https://www.youtube.com/watch?v=VqrbVycTXmw) — Talk at JsNation 2022
|
||||
|
||||
**Example Deployment:**
|
||||
|
||||
- [Mini.WebVM Reference](https://mini.webvm.io) — A running example deployed to GitHub Pages
|
||||
|
||||
**Technology Behind WebVM:**
|
||||
|
||||
This project is powered by:
|
||||
|
||||
- **[CheerpX](https://cheerpx.io/)** — x86-to-WebAssembly JIT compiler | by [Leaning Technologies](https://leaningtech.com/)
|
||||
- **[xterm.js](https://xtermjs.org/)** — Web-based terminal emulator
|
||||
- **[Tailscale](https://tailscale.com/)** — VPN networking layer
|
||||
- **[lwIP](https://savannah.nongnu.org/projects/lwip/)** — TCP/IP stack, compiled for the Web via [Cheerp](https://github.com/leaningtech/cheerp-meta/)
|
||||
|
||||
**Versioning:**
|
||||
|
||||
WebVM uses the [CheerpX](https://www.npmjs.com/package/@leaningtech/cheerpx) NPM package, which is updated on every release.
|
||||
|
||||
Every build is immutable. If a specific version works well for you today, it will keep working forever.
|
||||
|
||||
## License
|
||||
|
||||
WebVM is released under the Apache License, Version 2.0.
|
||||
|
||||
You are welcome to use, modify, and redistribute the contents of this repository.
|
||||
|
||||
The public CheerpX deployment is provided **as-is** and is **free to use** for technological exploration, testing and use by individuals. Any other use by organizations, including non-profit, academia and the public sector, requires a license. Downloading a CheerpX build for the purpose of hosting it elsewhere is not permitted without a commercial license.
|
||||
|
||||
Read more [here](https://cheerpx.io/docs/licensing) about our licensing practices.
|
||||
|
||||
If you want to build a product on top of CheerpX/WebVM, please see our other licensing options: [CheerpX licensing](https://cheerpx.io/licensing) or get in touch: sales@leaningtech.com
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`leaningtech/webvm`
|
||||
- 原始仓库:https://github.com/leaningtech/webvm
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 92 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 79.37 89.83"><defs><style>.cls-1{fill:#4b647f;}.cls-2{fill:#6386a5;}.cls-3{fill:#e2e2e2;}.cls-4{fill:#fff;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><g id="code_html5"><polygon class="cls-1" points="79.37 0 39.69 0 39.69 0 0 0 7.44 80.69 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 39.69 89.83 71.92 80.69 79.37 0"/><polygon class="cls-2" points="39.69 6.57 39.69 82.99 39.69 82.99 65.8 75.59 72.16 6.57 39.69 6.57"/><path class="cls-3" d="M39.52,50.42a7.7,7.7,0,1,1,0-15.4h.17V19.66c-1.16,0-2.31,0-3.46,0a1,1,0,0,0-1.15.95c-.29,1.68-.55,3.36-.79,5.05a.61.61,0,0,1-.45.57c-.72.27-1.42.6-2.13.89a.48.48,0,0,1-.39,0q-1.84-1.41-3.66-2.83c-.88-.7-1.28-.71-2.2.05a34.62,34.62,0,0,0-4.54,4.6c-.57.7-.55,1.05,0,1.76l2.92,3.75a.42.42,0,0,1,0,.52c-.33.73-.65,1.45-.94,2.2a.45.45,0,0,1-.4.33c-1.57.23-3.13.49-4.7.69a1.59,1.59,0,0,0-1.4.89v7.22a1.45,1.45,0,0,0,1.35.9c1.56.2,3.11.44,4.66.67a.53.53,0,0,1,.49.4c.28.77.6,1.53.94,2.27a.56.56,0,0,1-.05.66c-1,1.23-1.93,2.48-2.88,3.73A1.26,1.26,0,0,0,21,56.57,45.34,45.34,0,0,0,25.7,61.3a1.08,1.08,0,0,0,1.67.1c1.34-1,2.67-2,4-3a.48.48,0,0,1,.37,0c.76.3,1.5.63,2.25.92a.39.39,0,0,1,.29.36c.24,1.61.51,3.22.73,4.84a1.42,1.42,0,0,0,.88,1.32h3.79V50.41Z"/><path class="cls-4" d="M61.39,38.31l-4.88-.73a.5.5,0,0,1-.43-.37q-.45-1.17-1-2.31a.48.48,0,0,1,.05-.59c1-1.24,1.94-2.5,2.9-3.76A1.23,1.23,0,0,0,58,28.9a49.11,49.11,0,0,0-4.68-4.78A1.1,1.1,0,0,0,51.67,24l-4,3a.54.54,0,0,1-.41.06c-.75-.29-1.47-.62-2.22-.91a.4.4,0,0,1-.31-.38c-.25-1.7-.52-3.4-.78-5.09a1.08,1.08,0,0,0-1.19-1.05c-1,0-2,0-3.06,0V35a7.69,7.69,0,0,1,0,15.38V65.79h3.5A1.45,1.45,0,0,0,44,64.54c.24-1.62.5-3.23.73-4.84a.5.5,0,0,1,.35-.45c.74-.29,1.46-.62,2.2-.92a.51.51,0,0,1,.42.05c1.2.92,2.4,1.86,3.6,2.8.9.72,1.32.74,2.23,0a35.07,35.07,0,0,0,4.57-4.63c.56-.69.54-1.05,0-1.76l-2.9-3.72a.49.49,0,0,1-.06-.59c.34-.72.63-1.46,1-2.18a.5.5,0,0,1,.32-.27c1.68-.27,3.37-.53,5.06-.78A1.13,1.13,0,0,0,62.57,46c0-2.11,0-4.23,0-6.34A1.18,1.18,0,0,0,61.39,38.31Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 127.14 96.36"><defs><style>.cls-1{fill:#5865f2;}</style></defs><g id="图层_2" data-name="图层 2"><g id="Discord_Logos" data-name="Discord Logos"><g id="Discord_Logo_-_Large_-_White" data-name="Discord Logo - Large - White"><path class="cls-1" d="M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z"/></g></g></g></svg>
|
||||
|
After Width: | Height: | Size: 988 B |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -0,0 +1 @@
|
||||
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 960 B |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 185 KiB |
@@ -0,0 +1,11 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<ellipse cx="2.89214" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="11.5685" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="2.89214" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="20.245" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="11.5685" cy="20.3703" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse cx="20.245" cy="11.7148" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="2.89214" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="11.5685" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
<ellipse opacity="0.2" cx="20.245" cy="3.0594" rx="2.89214" ry="2.88514" fill="white"></ellipse>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1007 B |
|
After Width: | Height: | Size: 5.3 MiB |
|
After Width: | Height: | Size: 514 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
After Width: | Height: | Size: 473 KiB |
@@ -0,0 +1,23 @@
|
||||
// The root filesystem location
|
||||
export const diskImageUrl = IMAGE_URL;
|
||||
// The root filesystem backend type
|
||||
export const diskImageType = "github";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = true;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = false;
|
||||
// Executable full path (Required)
|
||||
export const cmd = CMD; // Default: "/bin/bash";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = ARGS; // Default: ["--login"];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// Environment variables
|
||||
env: ENV, // Default: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
|
||||
// Current working directory
|
||||
cwd: CWD, // Default: "/home/user",
|
||||
// User id
|
||||
uid: 1000,
|
||||
// Group id
|
||||
gid: 1000
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
// The root filesystem location
|
||||
export const diskImageUrl = "wss://disks.webvm.io/alpine_20251007.ext2";
|
||||
// The root filesystem backend type
|
||||
export const diskImageType = "cloud";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = false;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = true;
|
||||
// Executable full path (Required)
|
||||
export const cmd = "/sbin/init";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = [];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// User id
|
||||
uid: 0,
|
||||
// Group id
|
||||
gid: 0
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
// The root OS image location, change to local filepath if serving locally
|
||||
export const diskImageUrl = "wss://disks.webvm.io/debian_buster_large_permis_fixed_01-06-2026.ext2";
|
||||
// The root filesystem backend type use "cloud" for serving remotely or "bytes" for serving locally
|
||||
export const diskImageType = "cloud";
|
||||
// Print an introduction message about the technology
|
||||
export const printIntro = true;
|
||||
// Is a graphical display needed
|
||||
export const needsDisplay = false;
|
||||
// Executable full path (Required)
|
||||
export const cmd = "/bin/bash";
|
||||
// Arguments, as an array (Required)
|
||||
export const args = ["--login"];
|
||||
// Optional extra parameters
|
||||
export const opts = {
|
||||
// Environment variables
|
||||
env: ["HOME=/home/user", "TERM=xterm", "USER=user", "SHELL=/bin/bash", "EDITOR=vim", "LANG=en_US.UTF-8", "LC_ALL=C"],
|
||||
// Current working directory
|
||||
cwd: "/home/user",
|
||||
// User id
|
||||
uid: 1000,
|
||||
// Group id
|
||||
gid: 1000
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
.dockerignore
|
||||
@@ -0,0 +1,29 @@
|
||||
FROM --platform=linux/386 docker.io/i386/debian:buster
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Point APT to the archived mirrors (Buster is EOL)
|
||||
RUN echo "deb [trusted=yes] http://archive.debian.org/debian buster main contrib non-free" \
|
||||
> /etc/apt/sources.list && \
|
||||
echo "deb [trusted=yes] http://archive.debian.org/debian-security buster/updates main" \
|
||||
>> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get -y upgrade && \
|
||||
apt-get install -y apt-utils beef bsdgames bsdmainutils ca-certificates \
|
||||
cowsay cpio cron curl dmidecode dmsetup g++ gcc gdbm-l10n git \
|
||||
hexedit ifupdown init logrotate lsb-base lshw lua5.3 luajit lynx make \
|
||||
nano netbase nodejs openssl procps python3 python3-cryptography \
|
||||
python3-jinja2 python3-numpy python3-pandas python3-pip python3-scipy \
|
||||
python3-six python3-yaml readline-common rsyslog ruby sensible-utils \
|
||||
ssh systemd systemd-sysv tasksel tasksel-data udev vim wget whiptail \
|
||||
xxd iptables isc-dhcp-client isc-dhcp-common kmod less netcat-openbsd
|
||||
|
||||
# Make a user, then copy over the /example directory
|
||||
RUN useradd -m user && echo "user:password" | chpasswd
|
||||
COPY --chown=user:user ./examples /home/user/examples
|
||||
RUN chmod -R +x /home/user/examples/lua
|
||||
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
|
||||
WORKDIR /home/user/
|
||||
# We set env, as this gets extracted by Webvm. This is optional.
|
||||
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="C.UTF-8"
|
||||
RUN echo 'root:password' | chpasswd
|
||||
CMD [ "/bin/bash" ]
|
||||
@@ -0,0 +1,25 @@
|
||||
FROM --platform=linux/386 docker.io/i386/debian:buster
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Point APT to the archived mirrors (Buster is EOL)
|
||||
RUN echo "deb [trusted=yes] http://archive.debian.org/debian buster main contrib non-free" \
|
||||
> /etc/apt/sources.list && \
|
||||
echo "deb [trusted=yes] http://archive.debian.org/debian-security buster/updates main" \
|
||||
>> /etc/apt/sources.list
|
||||
|
||||
RUN apt-get clean && apt-get update && apt-get -y upgrade
|
||||
RUN apt-get -y install apt-utils gcc \
|
||||
python3 vim unzip ruby nodejs \
|
||||
fakeroot dbus base whiptail hexedit \
|
||||
patch wamerican ucf manpages \
|
||||
file luajit make lua5.3 dialog curl \
|
||||
less cowsay netcat-openbsd
|
||||
RUN useradd -m user && echo "user:password" | chpasswd
|
||||
COPY --chown=user:user ./examples /home/user/examples
|
||||
RUN chmod -R +x /home/user/examples/lua
|
||||
# We set WORKDIR, as this gets extracted by Webvm to be used as the cwd. This is optional.
|
||||
WORKDIR /home/user/
|
||||
# We set env, as this gets extracted by Webvm. This is optional.
|
||||
ENV HOME="/home/user" TERM="xterm" USER="user" SHELL="/bin/bash" EDITOR="vim" LANG="C.UTF-8"
|
||||
RUN echo 'root:password' | chpasswd
|
||||
CMD [ "/bin/bash" ]
|
||||
|
After Width: | Height: | Size: 359 KiB |
@@ -0,0 +1,5 @@
|
||||
Welcome to WebVM: A complete desktop environment running in the browser
|
||||
|
||||
WebVM is powered by CheerpX: a x86-to-WebAssembly virtualization engine and Just-in-Time compiler
|
||||
|
||||
For more info: https://cheerpx.io
|
||||
@@ -0,0 +1,3 @@
|
||||
ArchitectureOverview.png
|
||||
WebAssemblyTools.pdf
|
||||
Welcome.txt
|
||||
@@ -0,0 +1,13 @@
|
||||
SRCS = $(wildcard *.c)
|
||||
|
||||
PROGS = $(patsubst %.c,%,$(SRCS))
|
||||
|
||||
all: $(PROGS)
|
||||
|
||||
%: %.c
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
clean:
|
||||
rm -f $(PROGS)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,12 @@
|
||||
#include <stdio.h>
|
||||
|
||||
// Most of the C compilers support a third parameter to main which
|
||||
// store all envorinment variables
|
||||
int main(int argc, char *argv[], char * envp[])
|
||||
{
|
||||
int i;
|
||||
for (i = 0; envp[i] != NULL; i++)
|
||||
printf("\n%s", envp[i]);
|
||||
getchar();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
#include <stdio.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
printf("Hello, World!\n");
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
link("env", "env3");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <errno.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
int ret = openat(AT_FDCWD, "/dev/tty", 0x88102, 0);
|
||||
printf("return value is %d and errno is %d\n", ret, errno);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
int status;
|
||||
|
||||
pid_t p = getpid();
|
||||
// waitpid takes a children's pid, not the current process one
|
||||
// if the pid is not a children of the current process, it returns -ECHILD
|
||||
pid_t res = waitpid(1001, &status, WNOHANG);
|
||||
|
||||
printf("res is %d, p is %d and errno is %d\n", res, p, errno);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env luajit
|
||||
cfizz,cbuzz=0,0
|
||||
for i=1,20 do
|
||||
cfizz=cfizz+1
|
||||
cbuzz=cbuzz+1
|
||||
io.write(i .. ": ")
|
||||
if cfizz~=3 and cbuzz~=5 then
|
||||
io.write(i)
|
||||
else
|
||||
if cfizz==3 then
|
||||
io.write("Fizz")
|
||||
cfizz=0
|
||||
end
|
||||
if cbuzz==5 then
|
||||
io.write("Buzz")
|
||||
cbuzz=0
|
||||
end
|
||||
end
|
||||
io.write("\n")
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env luajit
|
||||
fruits = {"banana","orange","apple","grapes"}
|
||||
|
||||
for k,v in ipairs(fruits) do
|
||||
print(k,v)
|
||||
end
|
||||
|
||||
table.sort(fruits)
|
||||
print("sorted table")
|
||||
|
||||
for k,v in ipairs(fruits) do
|
||||
print(k,v)
|
||||
end
|
||||
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env luajit
|
||||
A = { ["John"] = true, ["Bob"] = true, ["Mary"] = true, ["Elena"] = true }
|
||||
B = { ["Jim"] = true, ["Mary"] = true, ["John"] = true, ["Bob"] = true }
|
||||
|
||||
A_B = {}
|
||||
for a in pairs(A) do
|
||||
if not B[a] then A_B[a] = true end
|
||||
end
|
||||
|
||||
B_A = {}
|
||||
for b in pairs(B) do
|
||||
if not A[b] then B_A[b] = true end
|
||||
end
|
||||
|
||||
for a_b in pairs(A_B) do
|
||||
print( a_b )
|
||||
end
|
||||
for b_a in pairs(B_A) do
|
||||
print( b_a )
|
||||
end
|
||||
@@ -0,0 +1,6 @@
|
||||
console.log("process.uptime = ", global.process.uptime());
|
||||
console.log("process.title = ", global.process.title);
|
||||
console.log("process version = ", global.process.version);
|
||||
console.log("process.platform = ", global.process.platform);
|
||||
console.log("process.cwd = ", global.process.cwd());
|
||||
console.log("process.uptime = ", global.process.uptime());
|
||||
@@ -0,0 +1,161 @@
|
||||
const PI = Math.PI;
|
||||
const SOLAR_MASS = 4 * PI * PI;
|
||||
const DAYS_PER_YEAR = 365.24;
|
||||
|
||||
function Body(x, y, z, vx, vy, vz, mass) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.vx = vx;
|
||||
this.vy = vy;
|
||||
this.vz = vz;
|
||||
this.mass = mass;
|
||||
}
|
||||
|
||||
function Jupiter() {
|
||||
return new Body(
|
||||
4.84143144246472090e+00,
|
||||
-1.16032004402742839e+00,
|
||||
-1.03622044471123109e-01,
|
||||
1.66007664274403694e-03 * DAYS_PER_YEAR,
|
||||
7.69901118419740425e-03 * DAYS_PER_YEAR,
|
||||
-6.90460016972063023e-05 * DAYS_PER_YEAR,
|
||||
9.54791938424326609e-04 * SOLAR_MASS
|
||||
);
|
||||
}
|
||||
|
||||
function Saturn() {
|
||||
return new Body(
|
||||
8.34336671824457987e+00,
|
||||
4.12479856412430479e+00,
|
||||
-4.03523417114321381e-01,
|
||||
-2.76742510726862411e-03 * DAYS_PER_YEAR,
|
||||
4.99852801234917238e-03 * DAYS_PER_YEAR,
|
||||
2.30417297573763929e-05 * DAYS_PER_YEAR,
|
||||
2.85885980666130812e-04 * SOLAR_MASS
|
||||
);
|
||||
}
|
||||
|
||||
function Uranus() {
|
||||
return new Body(
|
||||
1.28943695621391310e+01,
|
||||
-1.51111514016986312e+01,
|
||||
-2.23307578892655734e-01,
|
||||
2.96460137564761618e-03 * DAYS_PER_YEAR,
|
||||
2.37847173959480950e-03 * DAYS_PER_YEAR,
|
||||
-2.96589568540237556e-05 * DAYS_PER_YEAR,
|
||||
4.36624404335156298e-05 * SOLAR_MASS
|
||||
);
|
||||
}
|
||||
|
||||
function Neptune() {
|
||||
return new Body(
|
||||
1.53796971148509165e+01,
|
||||
-2.59193146099879641e+01,
|
||||
1.79258772950371181e-01,
|
||||
2.68067772490389322e-03 * DAYS_PER_YEAR,
|
||||
1.62824170038242295e-03 * DAYS_PER_YEAR,
|
||||
-9.51592254519715870e-05 * DAYS_PER_YEAR,
|
||||
5.15138902046611451e-05 * SOLAR_MASS
|
||||
);
|
||||
}
|
||||
|
||||
function Sun() {
|
||||
return new Body(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, SOLAR_MASS);
|
||||
}
|
||||
|
||||
const bodies = Array(Sun(), Jupiter(), Saturn(), Uranus(), Neptune());
|
||||
|
||||
function offsetMomentum() {
|
||||
let px = 0;
|
||||
let py = 0;
|
||||
let pz = 0;
|
||||
const size = bodies.length;
|
||||
for (let i = 0; i < size; i++) {
|
||||
const body = bodies[i];
|
||||
const mass = body.mass;
|
||||
px += body.vx * mass;
|
||||
py += body.vy * mass;
|
||||
pz += body.vz * mass;
|
||||
}
|
||||
|
||||
const body = bodies[0];
|
||||
body.vx = -px / SOLAR_MASS;
|
||||
body.vy = -py / SOLAR_MASS;
|
||||
body.vz = -pz / SOLAR_MASS;
|
||||
}
|
||||
|
||||
function advance(dt) {
|
||||
const size = bodies.length;
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const bodyi = bodies[i];
|
||||
let vxi = bodyi.vx;
|
||||
let vyi = bodyi.vy;
|
||||
let vzi = bodyi.vz;
|
||||
for (let j = i + 1; j < size; j++) {
|
||||
const bodyj = bodies[j];
|
||||
const dx = bodyi.x - bodyj.x;
|
||||
const dy = bodyi.y - bodyj.y;
|
||||
const dz = bodyi.z - bodyj.z;
|
||||
|
||||
const d2 = dx * dx + dy * dy + dz * dz;
|
||||
const mag = dt / (d2 * Math.sqrt(d2));
|
||||
|
||||
const massj = bodyj.mass;
|
||||
vxi -= dx * massj * mag;
|
||||
vyi -= dy * massj * mag;
|
||||
vzi -= dz * massj * mag;
|
||||
|
||||
const massi = bodyi.mass;
|
||||
bodyj.vx += dx * massi * mag;
|
||||
bodyj.vy += dy * massi * mag;
|
||||
bodyj.vz += dz * massi * mag;
|
||||
}
|
||||
bodyi.vx = vxi;
|
||||
bodyi.vy = vyi;
|
||||
bodyi.vz = vzi;
|
||||
}
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const body = bodies[i];
|
||||
body.x += dt * body.vx;
|
||||
body.y += dt * body.vy;
|
||||
body.z += dt * body.vz;
|
||||
}
|
||||
}
|
||||
|
||||
function energy() {
|
||||
let e = 0;
|
||||
const size = bodies.length;
|
||||
|
||||
for (let i = 0; i < size; i++) {
|
||||
const bodyi = bodies[i];
|
||||
|
||||
e += 0.5 * bodyi.mass * ( bodyi.vx * bodyi.vx + bodyi.vy * bodyi.vy + bodyi.vz * bodyi.vz );
|
||||
|
||||
for (let j = i + 1; j < size; j++) {
|
||||
const bodyj = bodies[j];
|
||||
const dx = bodyi.x - bodyj.x;
|
||||
const dy = bodyi.y - bodyj.y;
|
||||
const dz = bodyi.z - bodyj.z;
|
||||
|
||||
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
e -= (bodyi.mass * bodyj.mass) / distance;
|
||||
}
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
const n = +50000000;
|
||||
|
||||
offsetMomentum();
|
||||
|
||||
console.log(energy().toFixed(9));
|
||||
const start = Date.now();
|
||||
for (let i = 0; i < n; i++) {
|
||||
advance(0.01);
|
||||
}
|
||||
const end = Date.now();
|
||||
console.log(energy().toFixed(9));
|
||||
console.log("elapsed:",end-start);
|
||||
@@ -0,0 +1,31 @@
|
||||
(function () {
|
||||
|
||||
function isPrime(p) {
|
||||
const upper = Math.sqrt(p);
|
||||
for(let i = 2; i <= upper; i++) {
|
||||
if (p % i === 0 ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Return n-th prime
|
||||
function prime(n) {
|
||||
if (n < 1) {
|
||||
throw Error("n too small: " + n);
|
||||
}
|
||||
let count = 0;
|
||||
let result = 1;
|
||||
while(count < n) {
|
||||
result++;
|
||||
if (isPrime(result)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
console.log("your prime is ", prime(100000));
|
||||
|
||||
}());
|
||||
@@ -0,0 +1,16 @@
|
||||
(function (){
|
||||
let bytes = new Uint8Array([
|
||||
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00,
|
||||
0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01,
|
||||
0x7f, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01,
|
||||
0x03, 0x73, 0x75, 0x6d, 0x00, 0x00, 0x0a, 0x0a,
|
||||
0x01, 0x08, 0x00, 0x20, 0x00, 0x20, 0x01, 0x6a,
|
||||
0x0f, 0x0b
|
||||
]);
|
||||
|
||||
console.log(bytes);
|
||||
let mod = new WebAssembly.Module(bytes);
|
||||
let instance = new WebAssembly.Instance(mod, {});
|
||||
console.log(instance.exports);
|
||||
return instance.exports.sum(2020, 1);
|
||||
}());
|
||||
@@ -0,0 +1,9 @@
|
||||
def factorial():
|
||||
f, n = 1, 1
|
||||
while True: # First iteration:
|
||||
yield f # yield 1 to start with and then
|
||||
f, n = f * n, n+1 # f will now be 1, and n will be 2, ...
|
||||
|
||||
for index, factorial_number in zip(range(51), factorial()):
|
||||
print('{i:3}!= {f:65}'.format(i=index, f=factorial_number))
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
def fib():
|
||||
a, b = 0, 1
|
||||
while True: # First iteration:
|
||||
yield a # yield 0 to start with and then
|
||||
a, b = b, a + b # a will now be 1, and b will also be 1, (0 + 1)
|
||||
|
||||
for index, fibonacci_number in zip(range(100), fib()):
|
||||
print('{i:3}: {f:3}'.format(i=index, f=fibonacci_number))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
from decimal import Decimal, getcontext
|
||||
getcontext().prec=60
|
||||
summation = 0
|
||||
for k in range(50):
|
||||
summation = summation + 1/Decimal(16)**k * (
|
||||
Decimal(4)/(8*k+1)
|
||||
- Decimal(2)/(8*k+4)
|
||||
- Decimal(1)/(8*k+5)
|
||||
- Decimal(1)/(8*k+6)
|
||||
)
|
||||
print(summation)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
=begin
|
||||
# The famous Hello World
|
||||
# Program is trivial in
|
||||
# Ruby. Superfluous:
|
||||
#
|
||||
# * A "main" method
|
||||
# * Newline
|
||||
# * Semicolons
|
||||
#
|
||||
# Here is the Code:
|
||||
=end
|
||||
|
||||
puts "Hello World!"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Output "I love Ruby"
|
||||
say = "I love Ruby"; puts say
|
||||
|
||||
# Output "I *LOVE* RUBY"
|
||||
say['love'] = "*love*"; puts say.upcase
|
||||
|
||||
# Output "I *love* Ruby", 5 times
|
||||
5.times { puts say }
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
puts(2 ** 1)
|
||||
puts(2 ** 2)
|
||||
puts(2 ** 3)
|
||||
puts(2 ** 10)
|
||||
puts(2 ** 100)
|
||||
puts(2 ** 1000)
|
||||
|
After Width: | Height: | Size: 73 KiB |
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Tailscale login</title>
|
||||
</head>
|
||||
<body>
|
||||
Loading network code...
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
worker_processes 1;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
error_log nginx_main_error.log info;
|
||||
pid nginx_user.pid;
|
||||
daemon off;
|
||||
|
||||
http {
|
||||
access_log nginx_access.log;
|
||||
error_log nginx_error.log info;
|
||||
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
application/javascript js;
|
||||
application/wasm wasm;
|
||||
image/png png;
|
||||
image/jpeg jpg jpeg;
|
||||
image/svg+xml svg;
|
||||
}
|
||||
|
||||
default_type application/octet-stream;
|
||||
|
||||
sendfile on;
|
||||
|
||||
server {
|
||||
# listen 8080 ssl;
|
||||
listen 8081;
|
||||
server_name localhost;
|
||||
|
||||
gzip on;
|
||||
gzip_types application/javascript application/wasm text/plain;
|
||||
|
||||
charset utf-8;
|
||||
|
||||
# ssl_certificate nginx.crt;
|
||||
# ssl_certificate_key nginx.key;
|
||||
|
||||
location / {
|
||||
root build;
|
||||
autoindex on;
|
||||
index index.html index.htm;
|
||||
add_header 'Cross-Origin-Opener-Policy' 'same-origin' always;
|
||||
add_header 'Cross-Origin-Embedder-Policy' 'require-corp' always;
|
||||
add_header 'Cross-Origin-Resource-Policy' 'cross-origin' always;
|
||||
}
|
||||
location /custom-disk-images/ {
|
||||
root .;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "webvm",
|
||||
"version": "2.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "^0.33.0",
|
||||
"@fortawesome/fontawesome-free": "^6.6.0",
|
||||
"@leaningtech/cheerpx": "latest",
|
||||
"@oddbird/popover-polyfill": "^0.4.4",
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.5",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/addon-web-links": "^0.11.0",
|
||||
"@xterm/xterm": "^5.5.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"labs": "git@github.com:leaningtech/labs.git",
|
||||
"node-html-parser": "^6.1.13",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-discard": "^2.0.0",
|
||||
"svelte": "^4.2.7",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"vite": "^5.0.3",
|
||||
"vite-plugin-static-copy": "^1.0.6",
|
||||
"html2canvas-pro": "^1.5.8"
|
||||
},
|
||||
"type": "module"
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
'postcss-discard': {rule: function(node, value)
|
||||
{
|
||||
if(!value.startsWith('.fa-') || !value.endsWith(":before"))
|
||||
return false;
|
||||
switch(value)
|
||||
{
|
||||
case '.fa-info-circle:before':
|
||||
case '.fa-wifi:before':
|
||||
case '.fa-microchip:before':
|
||||
case '.fa-compact-disc:before':
|
||||
case '.fa-discord:before':
|
||||
case '.fa-github:before':
|
||||
case '.fa-star:before':
|
||||
case '.fa-circle:before':
|
||||
case '.fa-trash-can:before':
|
||||
case '.fa-book-open:before':
|
||||
case '.fa-user:before':
|
||||
case '.fa-screwdriver-wrench:before':
|
||||
case '.fa-desktop:before':
|
||||
case '.fa-mouse-pointer:before':
|
||||
case '.fa-hourglass-half:before':
|
||||
case '.fa-hand:before':
|
||||
case '.fa-brain:before':
|
||||
case '.fa-download:before':
|
||||
case '.fa-keyboard:before':
|
||||
case '.fa-thumbtack:before':
|
||||
case '.fa-brands:before':
|
||||
case '.fa-solid:before':
|
||||
case '.fa-regular:before':
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
.scrollbar {
|
||||
scrollbar-color: #777 #0000;
|
||||
}
|
||||
|
||||
.scrollbar *::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background-color: #0000;
|
||||
}
|
||||
|
||||
/* Add a thumb */
|
||||
.scrollbar *::-webkit-scrollbar-thumb {
|
||||
border-radius: 3px;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
background: #777;
|
||||
}
|
||||
|
||||
.scrollbar *::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
async function handleFetch(request) {
|
||||
// Perform the original fetch request and store the result in order to modify the response.
|
||||
try {
|
||||
var r = await fetch(request);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
if (r.status === 0) {
|
||||
return r;
|
||||
}
|
||||
// We add headers to the original response its headers, in order to enable cross-origin-isolation. And make it independent of the server config.
|
||||
const newHeaders = new Headers(r.headers);
|
||||
// COEP & COOP for cross-origin-isolation.
|
||||
newHeaders.set("Cross-Origin-Embedder-Policy", "require-corp");
|
||||
newHeaders.set("Cross-Origin-Opener-Policy", "same-origin");
|
||||
newHeaders.set("Cross-Origin-Resource-Policy", "cross-origin");
|
||||
/**
|
||||
* This workaround is necessary due to a limitation of CheerpOS, which relies on the response URL being set to the resolved URL.
|
||||
* When constructing a new response object, the URL is not set by the Response() constructor and the serviceworker respondwith() method will set the url to event.request.url in case of an empty string.
|
||||
* To address this, we set the location URL to the resolved response URL and set the status code to 301 in the new Response object.
|
||||
* This causes the request to bounce back to the serviceworker from Cheerpos, with the event.request.url now set to the resolved URL, which allows the respondWith method to properly set the response URL in our new response.
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/respondWith.
|
||||
*/
|
||||
if (r.redirected === true)
|
||||
newHeaders.set("location", r.url);
|
||||
// In case of a redirection, we set the status to 301, and body to null, in order to not transfer too much data needlessly
|
||||
const moddedResponse = new Response(r.redirected === true ? null : r.body, {
|
||||
headers: newHeaders,
|
||||
status: r.redirected === true ? 301 : r.status,
|
||||
statusText: r.statusText,
|
||||
});
|
||||
return moddedResponse;
|
||||
}
|
||||
|
||||
function serviceWorkerInit() {
|
||||
// Init the service worker.
|
||||
self.addEventListener("install", () => self.skipWaiting());
|
||||
self.addEventListener("activate", e => e.waitUntil(self.clients.claim()));
|
||||
// Listen for fetch requests and call handleFetch function.
|
||||
self.addEventListener("fetch", function (e) {
|
||||
try {
|
||||
e.respondWith(handleFetch(e.request));
|
||||
} catch (err) {
|
||||
console.log("Serviceworker NetworkError:" + err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function doRegister() {
|
||||
try {
|
||||
const registration = await navigator.serviceWorker.register(window.document.currentScript.src);
|
||||
console.log("Service Worker registered", registration.scope);
|
||||
// EventListener to make sure that the page gets reloaded when a new serviceworker gets installed.
|
||||
// f.e on first access.
|
||||
registration.addEventListener("updatefound", () => {
|
||||
console.log("Reloading the page to transfer control to the Service Worker.");
|
||||
try {
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.log("Service Worker failed reloading the page. ERROR:" + err);
|
||||
};
|
||||
});
|
||||
// When the registration is active, but it's not controlling the page, we reload the page to have it take control.
|
||||
// This f.e occurs when you hard-reload (shift + refresh). https://www.w3.org/TR/service-workers/#navigator-service-worker-controller
|
||||
if (registration.active && !navigator.serviceWorker.controller) {
|
||||
console.log("Reloading the page to transfer control to the Service Worker.");
|
||||
try {
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
console.log("Service Worker failed reloading the page. ERROR:" + err);
|
||||
};
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.error("Service Worker failed to register:", e)
|
||||
}
|
||||
}
|
||||
|
||||
async function serviceWorkerRegister() {
|
||||
if (window.crossOriginIsolated) return;
|
||||
if (!window.isSecureContext) {
|
||||
console.log("Service Worker not registered, a secure context is required.");
|
||||
return;
|
||||
}
|
||||
// Register the service worker and reload the page to transfer control to the serviceworker.
|
||||
if ("serviceWorker" in navigator)
|
||||
await doRegister();
|
||||
else
|
||||
console.log("Service worker is not supported in this browser");
|
||||
}
|
||||
|
||||
if (typeof window === 'undefined') // If the script is running in a Service Worker context
|
||||
serviceWorkerInit()
|
||||
else // If the script is running in the browser context
|
||||
serviceWorkerRegister();
|
||||
@@ -0,0 +1,38 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>WebVM - Linux virtualization in WebAssembly</title>
|
||||
|
||||
<meta name="description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
|
||||
<meta name="keywords" content="WebVM, Virtual Machine, CheerpX, x86 virtualization, WebAssembly, Tailscale, JIT">
|
||||
<meta property="og:title" content="WebVM - Linux virtualization in WebAssembly" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:site_name" content="WebVM"/>
|
||||
<meta property="og:image" content="https://webvm.io/assets/social_2024.png" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:site" content="@leaningtech" />
|
||||
<meta name="twitter:title" content="WebVM - Linux virtualization in WebAssembly" />
|
||||
<meta name="twitter:description" content="Linux virtual machine, running in the browser via HTML5/WebAssembly. Networking and graphics supported.">
|
||||
<meta name="twitter:image" content="https://webvm.io/assets/social_2024.png" />
|
||||
|
||||
<!-- Apple iOS web clip compatibility tags -->
|
||||
<meta name="application-name" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-title" content="WebVM" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
<link rel="shortcut icon" href="tower.ico">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel='stylesheet' href='scrollbar.css'>
|
||||
<!-- Serviceworker script that adds the COI headers to the response headers in cases where the server does not support it. -->
|
||||
<script src="serviceWorker.js"></script>
|
||||
<script data-domain="webvm.io" src="https://plausible.leaningtech.com/js/script.js"></script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,116 @@
|
||||
<script>
|
||||
import { apiState, setApiKey, addMessage, clearMessageHistory, forceStop, messageList, currentMessage, enableThinking, getMessageDetails } from '$lib/anthropic.js';
|
||||
import { tick } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import SmallButton from './SmallButton.svelte';
|
||||
import { aiActivity } from './activities.js';
|
||||
import html2canvas from 'html2canvas-pro';
|
||||
export let handleTool;
|
||||
let stopRequested = false;
|
||||
function handleKeyEnter(e)
|
||||
{
|
||||
if(e.key != "Enter")
|
||||
return;
|
||||
var value = e.target.value;
|
||||
if(value == "")
|
||||
return;
|
||||
setApiKey(value);
|
||||
}
|
||||
function handleMessage(e)
|
||||
{
|
||||
if(e.key != "Enter")
|
||||
return;
|
||||
e.preventDefault();
|
||||
var textArea = e.target;
|
||||
var value = textArea.value;
|
||||
if(value == "")
|
||||
return;
|
||||
textArea.style.height = "unset";
|
||||
// Reset the textarea
|
||||
currentMessage.set("");
|
||||
addMessage(value, handleTool);
|
||||
}
|
||||
function handleResize(e)
|
||||
{
|
||||
var textArea = e.target;
|
||||
textArea.style.height = textArea.scrollHeight + "px";
|
||||
}
|
||||
async function scrollToBottom(node)
|
||||
{
|
||||
await tick();
|
||||
node.scrollTop = node.scrollHeight;
|
||||
if (!get(aiActivity)) {
|
||||
document.getElementById("ai-input").focus();
|
||||
}
|
||||
}
|
||||
function scrollMessage(node, messageList)
|
||||
{
|
||||
// Make sure the messages are always scrolled to the bottom
|
||||
scrollToBottom(node);
|
||||
return {
|
||||
update(messageList) {
|
||||
scrollToBottom(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
async function handleStop() {
|
||||
stopRequested = true;
|
||||
await forceStop();
|
||||
stopRequested = false;
|
||||
}
|
||||
|
||||
async function handleDownload() {
|
||||
const messageListElement = document.getElementById('message-list');
|
||||
// Temporarily add padding and background for the list
|
||||
messageListElement.classList.add("p-1");
|
||||
const canvas = await html2canvas(messageListElement);
|
||||
messageListElement.classList.remove("p-1");
|
||||
const link = document.createElement('a');
|
||||
link.href = canvas.toDataURL('image/png');
|
||||
link.download = 'WebVM_Claude.png';
|
||||
link.click();
|
||||
}
|
||||
|
||||
function toggleThinkingMode() {
|
||||
enableThinking.set(!get(enableThinking));
|
||||
}
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Claude AI Integration</h1>
|
||||
<p>WebVM is integrated with Claude by Anthropic AI. You can prompt the AI to control the system.</p>
|
||||
<p>You need to provide your API key. The key is only saved locally to your browser.</p>
|
||||
<div class="flex grow flex-col overflow-y-hidden gap-2">
|
||||
<p class="flex flex-row gap-2">
|
||||
<span class="mr-auto flex items-center">Conversation history</span>
|
||||
<SmallButton buttonIcon="fa-solid fa-download" clickHandler={handleDownload} buttonTooltip="Save conversation as image"></SmallButton>
|
||||
<SmallButton buttonIcon="fa-solid fa-brain" clickHandler={toggleThinkingMode} buttonTooltip="{$enableThinking ? "Disable" : "Enable"} thinking mode" bgColor={$enableThinking ? "bg-neutral-500" : "bg-neutral-700"}></SmallButton>
|
||||
<SmallButton buttonIcon="fa-solid fa-trash-can" clickHandler={clearMessageHistory} buttonTooltip="Clear conversation history"></SmallButton>
|
||||
</p>
|
||||
<div class="flex grow overflow-y-scroll scrollbar" use:scrollMessage={$messageList}>
|
||||
<div class="h-full w-full">
|
||||
<div class="w-full min-h-full flex flex-col gap-2 justify-end bg-neutral-600" id="message-list">
|
||||
{#each $messageList as msg}
|
||||
{@const details = getMessageDetails(msg)}
|
||||
{#if details.isToolUse}
|
||||
<p class="bg-neutral-700 p-2 rounded-md italic"><i class='fas {details.icon} w-6 mr-2 text-center'></i>{details.messageContent}</p>
|
||||
{:else if !details.isToolResult}
|
||||
<p class="{msg.role == 'error' ? 'bg-red-900' : 'bg-neutral-700'} p-2 rounded-md whitespace-pre-wrap"><i class='fas {details.icon} w-6 mr-2 text-center'></i>{details.messageContent}</p>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if $apiState == "KEY_REQUIRED"}
|
||||
<textarea class="bg-neutral-700 p-2 rounded-md placeholder-gray-400 resize-none shrink-0" placeholder="Insert your Claude API Key" rows="1" on:keydown={handleKeyEnter} on:input={handleResize} id="ai-input"/>
|
||||
{:else if $aiActivity}
|
||||
{#if stopRequested }
|
||||
<PanelButton buttonIcon="fa-solid fa-hand" buttonText="Stopping...">
|
||||
</PanelButton>
|
||||
{:else}
|
||||
<PanelButton buttonIcon="fa-solid fa-hand" clickHandler={handleStop} buttonText="Stop">
|
||||
</PanelButton>
|
||||
{/if}
|
||||
{:else}
|
||||
<textarea class="bg-neutral-700 p-2 rounded-md placeholder-gray-400 resize-none shrink-0" placeholder={handleTool === null ? "Waiting for system initialization..." : "Prompt..."} rows="1" on:keydown={handleMessage} on:input={handleResize} bind:value={$currentMessage} id="ai-input" disabled={handleTool === null}/>
|
||||
{/if}
|
||||
@@ -0,0 +1,9 @@
|
||||
<script>
|
||||
export let title;
|
||||
export let image;
|
||||
export let url;
|
||||
</script>
|
||||
<a href={url} target="_blank"><div class="bg-neutral-700 hover:bg-neutral-500 p-2 rounded-md">
|
||||
<img class="w-full aspect-[40/21]" src={image}>
|
||||
<h2 class="text-sm font-bold">{title}</h2>
|
||||
</div></a>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import { cpuPercentage } from './activities.js'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">Engine</h1>
|
||||
<PanelButton buttonImage="assets/cheerpx.svg" clickUrl="https://cheerpx.io/docs" buttonText="Explore CheerpX">
|
||||
</PanelButton>
|
||||
<p><span class="font-bold">Virtual CPU: </span>{$cpuPercentage}%</p>
|
||||
<p>CheerpX is a x86 virtualization engine in WebAssembly</p>
|
||||
<p>It can securely run unmodified x86 binaries and libraries in the browser</p>
|
||||
<p>Excited about our technology? <a class="underline" href="https://cheerpx.io/docs/getting-started" target="_blank">Start building</a> your projects using <a class="underline" href="https://cheerpx.io/" target="_blank">CheerpX</a> today!</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import DiscordPresenceCount from 'labs/packages/astro-theme/components/nav/DiscordPresenceCount.svelte'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">Discord</h1>
|
||||
<PanelButton buttonImage="assets/discord-mark-blue.svg" clickUrl="https://discord.gg/yTNZgySKGa" buttonText="Join our Discord">
|
||||
<i class='fas fa-circle fa-xs ml-auto text-green-500'></i>
|
||||
<span class="ml-1"><DiscordPresenceCount /></span>
|
||||
</PanelButton>
|
||||
<p>Do you have any question about WebVM or CheerpX?</p>
|
||||
<p>Join our community, we are happy to help!</p>
|
||||
@@ -0,0 +1,62 @@
|
||||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { diskLatency } from './activities.js'
|
||||
var dispatch = createEventDispatcher();
|
||||
let state = "START";
|
||||
function handleReset()
|
||||
{
|
||||
if(state == "START")
|
||||
state = "CONFIRM";
|
||||
else if (state == "CONFIRM") {
|
||||
state = "RESETTING";
|
||||
dispatch('reset');
|
||||
}
|
||||
}
|
||||
function getButtonText(state)
|
||||
{
|
||||
if(state == "START")
|
||||
return "Reset disk";
|
||||
else if (state == "RESETTING")
|
||||
return "Resetting...";
|
||||
else
|
||||
return "Reset disk. Confirm?";
|
||||
}
|
||||
function getBgColor(state)
|
||||
{
|
||||
if(state == "START")
|
||||
{
|
||||
// Use default
|
||||
return undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "bg-red-900";
|
||||
}
|
||||
}
|
||||
function getHoverColor(state)
|
||||
{
|
||||
if(state == "START")
|
||||
{
|
||||
// Use default
|
||||
return undefined;
|
||||
}
|
||||
else
|
||||
{
|
||||
return "hover:bg-red-700";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Disk</h1>
|
||||
<PanelButton buttonIcon="fa-solid fa-trash-can" clickHandler={handleReset} buttonText={getButtonText(state)} bgColor={getBgColor(state)} hoverColor={getHoverColor(state)}>
|
||||
</PanelButton>
|
||||
{#if state == "CONFIRM"}
|
||||
<p><span class="font-bold">Warning: </span>WebVM will reload</p>
|
||||
{:else if state == "RESETTING"}
|
||||
<p><span class="font-bold">Reset in progress: </span>Please wait...</p>
|
||||
{:else}
|
||||
<p><span class="font-bold">Backend latency: </span>{$diskLatency}ms</p>
|
||||
{/if}
|
||||
<p>WebVM runs on top of a complete Linux distribution</p>
|
||||
<p>Filesystems up to 2GB are supported and data is downloaded completely on-demand</p>
|
||||
<p>The WebVM cloud backend uses WebSockets and it's distributed via a global CDN to minimize download latency</p>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
import GitHubStarCount from 'labs/packages/astro-theme/components/nav/GitHubStarCount.svelte'
|
||||
</script>
|
||||
|
||||
<h1 class="text-lg font-bold">GitHub</h1>
|
||||
<PanelButton buttonImage="assets/github-mark-white.svg" clickUrl="https://github.com/leaningtech/webvm" buttonText="GitHub repo">
|
||||
<i class='fas fa-star fa-xs ml-auto'></i>
|
||||
<span class="ml-1"><GitHubStarCount repo="leaningtech/webvm"/></span>
|
||||
</PanelButton>
|
||||
<p>Like WebVM? <a class="underline" href="https://github.com/leaningtech/webvm" target="_blank">Give us a star!</a></p>
|
||||
<p>WebVM is FOSS, you can fork it to build your own version and begin working on your CheerpX-based project</p>
|
||||
<p>Found a bug? Please open a <a class="underline" href="https://github.com/leaningtech/webvm/issues" target="_blank">GitHub issue</a></p>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script>
|
||||
export let icon;
|
||||
export let info;
|
||||
export let activity;
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
function handleMouseover() {
|
||||
dispatch('mouseover', info);
|
||||
}
|
||||
function handleClick() {
|
||||
dispatch('click', { icon, info });
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="p-3 cursor-pointer text-center hover:bg-neutral-600 {$activity ? "text-amber-500 animate-pulse" : "hover:text-gray-100"}"
|
||||
style="animation-duration: 0.5s"
|
||||
on:mouseenter={handleMouseover}
|
||||
on:click={handleClick}
|
||||
>
|
||||
<i class='{icon} fa-xl'></i>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<h1 class="text-lg font-bold">Information</h1>
|
||||
<img src="assets/webvm_hero.png" alt="WebVM Logo" class="w-56 h-56 object-contain self-center">
|
||||
<p>WebVM is a virtual Linux environment running in the browser via WebAssembly</p>
|
||||
<p>It is based on:</p>
|
||||
<ul class="list-disc list-inside">
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/">CheerpX</a>: x86 JIT in Wasm</li>
|
||||
<li><a class="underline" target="_blank" href="https://xtermjs.org/">Xterm.js</a>: interactive terminal</li>
|
||||
<li>Local/private <a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/File-System-support">file storage</a></li>
|
||||
<li><a class="underline" target="_blank" href="https://cheerpx.io/docs/guides/Networking">Networking</a> via <a class="underline" target="_blank" href="https://tailscale.com/">Tailscale</a></li>
|
||||
</ul>
|
||||
<slot></slot>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script>
|
||||
import { networkData, startLogin, updateButtonData } from '$lib/network.js'
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import PanelButton from './PanelButton.svelte';
|
||||
var dispatch = createEventDispatcher();
|
||||
var connectionState = networkData.connectionState;
|
||||
var exitNode = networkData.exitNode;
|
||||
|
||||
function handleConnect() {
|
||||
connectionState.set("DOWNLOADING");
|
||||
dispatch('connect');
|
||||
}
|
||||
|
||||
let buttonData = null;
|
||||
$: buttonData = updateButtonData($connectionState, handleConnect);
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Networking</h1>
|
||||
<PanelButton buttonImage="assets/tailscale.svg" clickUrl={buttonData.clickUrl} clickHandler={buttonData.clickHandler} rightClickHandler={buttonData.rightClickHandler} buttonTooltip={buttonData.buttonTooltip} buttonText={buttonData.buttonText}>
|
||||
{#if $connectionState == "CONNECTED"}
|
||||
<i class='fas fa-circle fa-xs ml-auto {$exitNode ? 'text-green-500' : 'text-amber-500'}' title={$exitNode ? 'Ready' : 'No exit node'}></i>
|
||||
{/if}
|
||||
</PanelButton>
|
||||
<p>WebVM can connect to the Internet via Tailscale</p>
|
||||
<p>Using Tailscale is required since browser do not support TCP/UDP sockets (yet!)</p>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
export let clickUrl = null;
|
||||
export let clickHandler = null;
|
||||
export let rightClickHandler = null;
|
||||
export let buttonTooltip = null;
|
||||
export let bgColor = "bg-neutral-700";
|
||||
export let hoverColor = "hover:bg-neutral-500"
|
||||
export let buttonImage = null;
|
||||
export let buttonIcon = null;
|
||||
export let buttonText;
|
||||
</script>
|
||||
|
||||
<a href={clickUrl} target="_blank" on:click={clickHandler} on:contextmenu={rightClickHandler}><p class="flex flex-row items-center {bgColor} p-2 rounded-md shadow-md shadow-neutral-900 {(clickUrl != null || clickHandler != null) ? `${hoverColor} cursor-pointer` : ""}" title={buttonTooltip}>
|
||||
{#if buttonImage}
|
||||
<img src={buttonImage} class="inline w-8 h-8"/>
|
||||
{:else if buttonIcon}
|
||||
<i class="w-8 {buttonIcon} text-center" style="font-size: 2em;"></i>
|
||||
{/if}
|
||||
<span class="ml-1">{buttonText}</span><slot></slot></p></a>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script>
|
||||
import BlogPost from './BlogPost.svelte';
|
||||
import { page } from '$app/stores';
|
||||
</script>
|
||||
<h1 class="text-lg font-bold">Blog posts</h1>
|
||||
<div class="overflow-y-scroll scrollbar flex flex-col gap-2">
|
||||
{#each $page.data.posts as post}
|
||||
<BlogPost
|
||||
title={post.title}
|
||||
image={post.image}
|
||||
url={post.url}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,135 @@
|
||||
<script>
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import InformationTab from './InformationTab.svelte';
|
||||
import NetworkingTab from './NetworkingTab.svelte';
|
||||
import CpuTab from './CpuTab.svelte';
|
||||
import DiskTab from './DiskTab.svelte';
|
||||
import AnthropicTab from './AnthropicTab.svelte';
|
||||
import PostsTab from './PostsTab.svelte';
|
||||
import DiscordTab from './DiscordTab.svelte';
|
||||
import GitHubTab from './GitHubTab.svelte';
|
||||
import SmallButton from './SmallButton.svelte';
|
||||
import { cpuActivity, diskActivity, aiActivity } from './activities.js';
|
||||
const icons = [
|
||||
{ icon: 'fas fa-info-circle', info: 'Information', activity: null },
|
||||
{ icon: 'fas fa-wifi', info: 'Networking', activity: null },
|
||||
{ icon: 'fas fa-microchip', info: 'CPU', activity: cpuActivity },
|
||||
{ icon: 'fas fa-compact-disc', info: 'Disk', activity: diskActivity },
|
||||
{ icon: 'fas fa-robot', info: 'ClaudeAI', activity: aiActivity },
|
||||
null,
|
||||
{ icon: 'fas fa-book-open', info: 'Posts', activity: null },
|
||||
{ icon: 'fab fa-discord', info: 'Discord', activity: null },
|
||||
{ icon: 'fab fa-github', info: 'GitHub', activity: null },
|
||||
];
|
||||
let dispatch = createEventDispatcher();
|
||||
let activeInfo = null; // Tracks currently visible info.
|
||||
let hideTimeout = 0; // Timeout for hiding info panel.
|
||||
export let sideBarPinned;
|
||||
|
||||
function showInfo(info) {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = 0;
|
||||
activeInfo = info;
|
||||
}
|
||||
function hideInfo() {
|
||||
// Never remove the sidebar if pinning is enabled
|
||||
if(sideBarPinned)
|
||||
return;
|
||||
// Prevents multiple timers and hides the info panel after 400ms unless interrupted.
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = setTimeout(() => {
|
||||
activeInfo = null;
|
||||
hideTimeout = 0;
|
||||
}, 400);
|
||||
}
|
||||
function handleMouseEnterPanel() {
|
||||
clearTimeout(hideTimeout);
|
||||
hideTimeout = 0;
|
||||
}
|
||||
// Toggles the info panel for the clicked icon.
|
||||
function handleClick(icon) {
|
||||
if(sideBarPinned)
|
||||
return;
|
||||
// Hides the panel if the icon is active. Otherwise, shows the panel with info.
|
||||
if (activeInfo === icon.info) {
|
||||
activeInfo = null;
|
||||
} else {
|
||||
activeInfo = icon.info;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSidebarPin() {
|
||||
sideBarPinned = !sideBarPinned;
|
||||
dispatch('sidebarPinChange', sideBarPinned);
|
||||
}
|
||||
|
||||
export let handleTool;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-row w-14 h-full bg-neutral-700" >
|
||||
<div class="flex flex-col shrink-0 w-14 text-gray-300">
|
||||
{#each icons as i}
|
||||
{#if i}
|
||||
<Icon
|
||||
icon={i.icon}
|
||||
info={i.info}
|
||||
activity={i.activity}
|
||||
on:mouseover={(e) => showInfo(e.detail)}
|
||||
on:click={() => handleClick(i)}
|
||||
/>
|
||||
{:else}
|
||||
<div class="grow" on:mouseenter={handleMouseEnterPanel}></div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
<div
|
||||
class="relative flex flex-col gap-5 shrink-0 w-80 h-full z-10 p-2 bg-neutral-600 text-gray-100 opacity-95"
|
||||
class:hidden={!activeInfo}
|
||||
on:mouseenter={handleMouseEnterPanel}
|
||||
on:mouseleave={hideInfo}
|
||||
>
|
||||
<div class="absolute right-2 top-2">
|
||||
<SmallButton
|
||||
buttonIcon="fa-solid fa-thumbtack"
|
||||
clickHandler={toggleSidebarPin}
|
||||
buttonTooltip={sideBarPinned ? "Unpin Sidebar" : "Pin Sidebar"}
|
||||
bgColor={sideBarPinned ? "bg-neutral-500" : "bg-neutral-700"}
|
||||
/>
|
||||
</div>
|
||||
{#if activeInfo === 'Information'}
|
||||
<InformationTab>
|
||||
<slot></slot>
|
||||
</InformationTab>
|
||||
{:else if activeInfo === 'Networking'}
|
||||
<NetworkingTab on:connect/>
|
||||
{:else if activeInfo === 'CPU'}
|
||||
<CpuTab/>
|
||||
{:else if activeInfo === 'Disk'}
|
||||
<DiskTab on:reset/>
|
||||
{:else if activeInfo === 'ClaudeAI'}
|
||||
<AnthropicTab handleTool={handleTool} />
|
||||
{:else if activeInfo === 'Posts'}
|
||||
<PostsTab/>
|
||||
{:else if activeInfo === 'Discord'}
|
||||
<DiscordTab/>
|
||||
{:else if activeInfo === 'GitHub'}
|
||||
<GitHubTab/>
|
||||
{:else}
|
||||
<p>TODO: {activeInfo}</p>
|
||||
{/if}
|
||||
|
||||
<div class="mt-auto text-sm text-gray-300">
|
||||
<div class="pt-1 pb-1">
|
||||
<a href="https://cheerpx.io/" target="_blank">
|
||||
<span>Powered by CheerpX</span>
|
||||
<img src="assets/cheerpx.svg" alt="CheerpX Logo" class="w-6 h-6 inline-block">
|
||||
</a>
|
||||
</div>
|
||||
<hr class="border-t border-solid border-gray-300">
|
||||
<div class="pt-1 pb-1">
|
||||
<a href="https://leaningtech.com/" target="”_blank”">© 2022-2025 Leaning Technologies</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script>
|
||||
export let clickHandler = null;
|
||||
export let buttonTooltip = null;
|
||||
export let bgColor = "bg-neutral-700";
|
||||
export let hoverColor = "hover:bg-neutral-500"
|
||||
export let buttonIcon = null;
|
||||
</script>
|
||||
|
||||
<span class="inline-block h-7 {bgColor} text-sm p-1 rounded-md shadow-md shadow-neutral-900 {(clickHandler != null) ? `${hoverColor} cursor-pointer` : ""}" title={buttonTooltip} on:click={clickHandler}>
|
||||
<i class="w-5 {buttonIcon} text-center"></i>
|
||||
</span>
|
||||
@@ -0,0 +1,385 @@
|
||||
<script>
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { get } from 'svelte/store';
|
||||
import Nav from 'labs/packages/global-navbar/src/Nav.svelte';
|
||||
import SideBar from '$lib/SideBar.svelte';
|
||||
import '$lib/global.css';
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css'
|
||||
import { networkInterface, startLogin } from '$lib/network.js'
|
||||
import { cpuActivity, diskActivity, cpuPercentage, diskLatency } from '$lib/activities.js'
|
||||
import { introMessage, errorMessage, unexpectedErrorMessage } from '$lib/messages.js'
|
||||
import { displayConfig, handleToolImpl } from '$lib/anthropic.js'
|
||||
import { tryPlausible } from '$lib/plausible.js'
|
||||
|
||||
export let configObj = null;
|
||||
export let processCallback = null;
|
||||
export let cacheId = null;
|
||||
export let cpuActivityEvents = [];
|
||||
export let diskLatencies = [];
|
||||
export let activityEventsInterval = 0;
|
||||
|
||||
var term = null;
|
||||
var cx = null;
|
||||
var fitAddon = null;
|
||||
var cxReadFunc = null;
|
||||
var blockCache = null;
|
||||
var processCount = 0;
|
||||
var curVT = 0;
|
||||
var sideBarPinned = false;
|
||||
function writeData(buf, vt)
|
||||
{
|
||||
if(vt != 1)
|
||||
return;
|
||||
term.write(new Uint8Array(buf));
|
||||
}
|
||||
function readData(str)
|
||||
{
|
||||
if(cxReadFunc == null)
|
||||
return;
|
||||
for(var i=0;i<str.length;i++)
|
||||
cxReadFunc(str.charCodeAt(i));
|
||||
}
|
||||
function printMessage(msg)
|
||||
{
|
||||
for(var i=0;i<msg.length;i++)
|
||||
term.write(msg[i] + "\n");
|
||||
}
|
||||
function expireEvents(list, curTime, limitTime)
|
||||
{
|
||||
while(list.length > 1)
|
||||
{
|
||||
if(list[1].t < limitTime)
|
||||
{
|
||||
list.shift();
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function cleanupEvents()
|
||||
{
|
||||
var curTime = Date.now();
|
||||
var limitTime = curTime - 10000;
|
||||
expireEvents(cpuActivityEvents, curTime, limitTime);
|
||||
computeCpuActivity(curTime, limitTime);
|
||||
if(cpuActivityEvents.length == 0)
|
||||
{
|
||||
clearInterval(activityEventsInterval);
|
||||
activityEventsInterval = 0;
|
||||
}
|
||||
}
|
||||
function computeCpuActivity(curTime, limitTime)
|
||||
{
|
||||
var totalActiveTime = 0;
|
||||
var lastActiveTime = limitTime;
|
||||
var lastWasActive = false;
|
||||
for(var i=0;i<cpuActivityEvents.length;i++)
|
||||
{
|
||||
var e = cpuActivityEvents[i];
|
||||
// NOTE: The first event could be before the limit,
|
||||
// we need at least one event to correctly mark
|
||||
// active time when there is long time under load
|
||||
var eTime = e.t;
|
||||
if(eTime < limitTime)
|
||||
eTime = limitTime;
|
||||
if(e.state == "ready")
|
||||
{
|
||||
// Inactive state, add the time from lastActiveTime
|
||||
totalActiveTime += (eTime - lastActiveTime);
|
||||
lastWasActive = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Active state
|
||||
lastActiveTime = eTime;
|
||||
lastWasActive = true;
|
||||
}
|
||||
}
|
||||
// Add the last interval if needed
|
||||
if(lastWasActive)
|
||||
{
|
||||
totalActiveTime += (curTime - lastActiveTime);
|
||||
}
|
||||
cpuPercentage.set(Math.ceil((totalActiveTime / 10000) * 100));
|
||||
}
|
||||
function hddCallback(state)
|
||||
{
|
||||
diskActivity.set(state != "ready");
|
||||
}
|
||||
function latencyCallback(latency)
|
||||
{
|
||||
diskLatencies.push(latency);
|
||||
if(diskLatencies.length > 30)
|
||||
diskLatencies.shift();
|
||||
// Average the latency over at most 30 blocks
|
||||
var total = 0;
|
||||
for(var i=0;i<diskLatencies.length;i++)
|
||||
total += diskLatencies[i];
|
||||
var avg = total / diskLatencies.length;
|
||||
diskLatency.set(Math.ceil(avg));
|
||||
}
|
||||
function cpuCallback(state)
|
||||
{
|
||||
cpuActivity.set(state != "ready");
|
||||
var curTime = Date.now();
|
||||
var limitTime = curTime - 10000;
|
||||
expireEvents(cpuActivityEvents, curTime, limitTime);
|
||||
cpuActivityEvents.push({t: curTime, state: state});
|
||||
computeCpuActivity(curTime, limitTime);
|
||||
// Start an interval timer to cleanup old samples when no further activity is received
|
||||
if(activityEventsInterval != 0)
|
||||
clearInterval(activityEventsInterval);
|
||||
activityEventsInterval = setInterval(cleanupEvents, 2000);
|
||||
}
|
||||
function computeXTermFontSize()
|
||||
{
|
||||
return parseInt(getComputedStyle(document.body).fontSize);
|
||||
}
|
||||
function setScreenSize(display)
|
||||
{
|
||||
var internalMult = 1.0;
|
||||
var displayWidth = display.offsetWidth;
|
||||
var displayHeight = display.offsetHeight;
|
||||
var minWidth = 1024;
|
||||
var minHeight = 768;
|
||||
if(displayWidth < minWidth)
|
||||
internalMult = minWidth / displayWidth;
|
||||
if(displayHeight < minHeight)
|
||||
internalMult = Math.max(internalMult, minHeight / displayHeight);
|
||||
var internalWidth = Math.floor(displayWidth * internalMult);
|
||||
var internalHeight = Math.floor(displayHeight * internalMult);
|
||||
cx.setKmsCanvas(display, internalWidth, internalHeight);
|
||||
// Compute the size to be used for AI screenshots
|
||||
var screenshotMult = 1.0;
|
||||
var maxWidth = 1024;
|
||||
var maxHeight = 768;
|
||||
if(internalWidth > maxWidth)
|
||||
screenshotMult = maxWidth / internalWidth;
|
||||
if(internalHeight > maxHeight)
|
||||
screenshotMult = Math.min(screenshotMult, maxHeight / internalHeight);
|
||||
var screenshotWidth = Math.floor(internalWidth * screenshotMult);
|
||||
var screenshotHeight = Math.floor(internalHeight * screenshotMult);
|
||||
// Track the state of the mouse as requested by the AI, to avoid losing the position due to user movement
|
||||
displayConfig.set({width: screenshotWidth, height: screenshotHeight, mouseMult: internalMult * screenshotMult});
|
||||
}
|
||||
var curInnerWidth = 0;
|
||||
var curInnerHeight = 0;
|
||||
function handleResize()
|
||||
{
|
||||
// Avoid spurious resize events caused by the soft keyboard
|
||||
if(curInnerWidth == window.innerWidth && curInnerHeight == window.innerHeight)
|
||||
return;
|
||||
curInnerWidth = window.innerWidth;
|
||||
curInnerHeight = window.innerHeight;
|
||||
triggerResize();
|
||||
}
|
||||
function triggerResize()
|
||||
{
|
||||
term.options.fontSize = computeXTermFontSize();
|
||||
fitAddon.fit();
|
||||
const display = document.getElementById("display");
|
||||
if(display)
|
||||
setScreenSize(display);
|
||||
}
|
||||
async function initTerminal()
|
||||
{
|
||||
const { Terminal } = await import('@xterm/xterm');
|
||||
const { FitAddon } = await import('@xterm/addon-fit');
|
||||
const { WebLinksAddon } = await import('@xterm/addon-web-links');
|
||||
term = new Terminal({cursorBlink:true, convertEol:true, fontFamily:"monospace", fontWeight: 400, fontWeightBold: 700, fontSize: computeXTermFontSize()});
|
||||
fitAddon = new FitAddon();
|
||||
term.loadAddon(fitAddon);
|
||||
var linkAddon = new WebLinksAddon();
|
||||
term.loadAddon(linkAddon);
|
||||
const consoleDiv = document.getElementById("console");
|
||||
term.open(consoleDiv);
|
||||
term.scrollToTop();
|
||||
fitAddon.fit();
|
||||
window.addEventListener("resize", handleResize);
|
||||
term.focus();
|
||||
term.onData(readData);
|
||||
// Avoid undesired default DnD handling
|
||||
function preventDefaults (e) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
consoleDiv.addEventListener("dragover", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragenter", preventDefaults, false);
|
||||
consoleDiv.addEventListener("dragleave", preventDefaults, false);
|
||||
consoleDiv.addEventListener("drop", preventDefaults, false);
|
||||
curInnerWidth = window.innerWidth;
|
||||
curInnerHeight = window.innerHeight;
|
||||
if(configObj.printIntro)
|
||||
printMessage(introMessage);
|
||||
try
|
||||
{
|
||||
await initCheerpX();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
printMessage(unexpectedErrorMessage);
|
||||
printMessage([e.toString()]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
function handleActivateConsole(vt)
|
||||
{
|
||||
if(curVT == vt)
|
||||
return;
|
||||
curVT = vt;
|
||||
if(vt != 7)
|
||||
return;
|
||||
// Raise the display to the foreground
|
||||
const display = document.getElementById("display");
|
||||
display.parentElement.style.zIndex = 5;
|
||||
tryPlausible("Display activated");
|
||||
}
|
||||
function handleProcessCreated()
|
||||
{
|
||||
processCount++;
|
||||
if(processCallback)
|
||||
processCallback(processCount);
|
||||
}
|
||||
async function initCheerpX()
|
||||
{
|
||||
const CheerpX = await import('@leaningtech/cheerpx');
|
||||
var blockDevice = null;
|
||||
switch(configObj.diskImageType)
|
||||
{
|
||||
case "cloud":
|
||||
try
|
||||
{
|
||||
blockDevice = await CheerpX.CloudDevice.create(configObj.diskImageUrl);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
// Report the failure and try again with plain HTTP
|
||||
var wssProtocol = "wss:";
|
||||
if(configObj.diskImageUrl.startsWith(wssProtocol))
|
||||
{
|
||||
// WebSocket protocol failed, try agin using plain HTTP
|
||||
tryPlausible("WS Disk failure");
|
||||
blockDevice = await CheerpX.CloudDevice.create("https:" + configObj.diskImageUrl.substr(wssProtocol.length));
|
||||
}
|
||||
else
|
||||
{
|
||||
// No other recovery option
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "bytes":
|
||||
blockDevice = await CheerpX.HttpBytesDevice.create(configObj.diskImageUrl);
|
||||
break;
|
||||
case "github":
|
||||
blockDevice = await CheerpX.GitHubDevice.create(configObj.diskImageUrl);
|
||||
break;
|
||||
default:
|
||||
throw new Error("Unrecognized device type");
|
||||
}
|
||||
blockCache = await CheerpX.IDBDevice.create(cacheId);
|
||||
var overlayDevice = await CheerpX.OverlayDevice.create(blockDevice, blockCache);
|
||||
var webDevice = await CheerpX.WebDevice.create("");
|
||||
var documentsDevice = await CheerpX.WebDevice.create("documents");
|
||||
var dataDevice = await CheerpX.DataDevice.create();
|
||||
var mountPoints = [
|
||||
// The root filesystem, as an Ext2 image
|
||||
{type:"ext2", dev:overlayDevice, path:"/"},
|
||||
// Access to files on the Web server, relative to the current page
|
||||
{type:"dir", dev:webDevice, path:"/web"},
|
||||
// Access to read-only data coming from JavaScript
|
||||
{type:"dir", dev:dataDevice, path:"/data"},
|
||||
// Automatically created device files
|
||||
{type:"devs", path:"/dev"},
|
||||
// Pseudo-terminals
|
||||
{type:"devpts", path:"/dev/pts"},
|
||||
// The Linux 'proc' filesystem which provides information about running processes
|
||||
{type:"proc", path:"/proc"},
|
||||
// The Linux 'sysfs' filesystem which is used to enumerate emulated devices
|
||||
{type:"sys", path:"/sys"},
|
||||
// Convenient access to sample documents in the user directory
|
||||
{type:"dir", dev:documentsDevice, path:"/home/user/documents"}
|
||||
];
|
||||
try
|
||||
{
|
||||
cx = await CheerpX.Linux.create({mounts: mountPoints, networkInterface: networkInterface});
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
printMessage(errorMessage);
|
||||
printMessage([e.toString()]);
|
||||
return;
|
||||
}
|
||||
cx.registerCallback("cpuActivity", cpuCallback);
|
||||
cx.registerCallback("diskActivity", hddCallback);
|
||||
cx.registerCallback("diskLatency", latencyCallback);
|
||||
cx.registerCallback("processCreated", handleProcessCreated);
|
||||
term.scrollToBottom();
|
||||
cxReadFunc = cx.setCustomConsole(writeData, term.cols, term.rows);
|
||||
const display = document.getElementById("display");
|
||||
if(display)
|
||||
{
|
||||
setScreenSize(display);
|
||||
cx.setActivateConsole(handleActivateConsole);
|
||||
}
|
||||
// Run the command in a loop, in case the user exits
|
||||
while (true)
|
||||
{
|
||||
await cx.run(configObj.cmd, configObj.args, configObj.opts);
|
||||
}
|
||||
}
|
||||
onMount(initTerminal);
|
||||
async function handleConnect()
|
||||
{
|
||||
const w = window.open("login.html", "_blank");
|
||||
cx.networkLogin();
|
||||
try
|
||||
{
|
||||
w.location.href = await startLogin();
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
w.close();
|
||||
console.warn(e);
|
||||
}
|
||||
}
|
||||
async function handleReset()
|
||||
{
|
||||
// Be robust before initialization
|
||||
if(blockCache == null)
|
||||
return;
|
||||
await blockCache.reset();
|
||||
location.reload();
|
||||
}
|
||||
async function handleTool(tool)
|
||||
{
|
||||
return await handleToolImpl(tool, term);
|
||||
}
|
||||
async function handleSidebarPinChange(event)
|
||||
{
|
||||
sideBarPinned = event.detail;
|
||||
// Make sure the pinning state of reflected in the layout
|
||||
await tick();
|
||||
// Adjust the layout based on the new sidebar state
|
||||
triggerResize();
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="relative w-full h-full">
|
||||
<Nav />
|
||||
<div class="absolute top-10 bottom-0 left-0 right-0">
|
||||
<SideBar on:connect={handleConnect} on:reset={handleReset} handleTool={!configObj.needsDisplay || curVT == 7 ? handleTool : null} on:sidebarPinChange={handleSidebarPinChange}>
|
||||
<slot></slot>
|
||||
</SideBar>
|
||||
{#if configObj.needsDisplay}
|
||||
<div class="absolute top-0 bottom-0 {sideBarPinned ? 'left-[23.5rem]' : 'left-14'} right-0">
|
||||
<canvas class="w-full h-full cursor-none" id="display"></canvas>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="absolute top-0 bottom-0 {sideBarPinned ? 'left-[23.5rem]' : 'left-14'} right-0 p-1 scrollbar" id="console">
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,7 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const cpuActivity = writable(false);
|
||||
export const diskActivity = writable(false);
|
||||
export const aiActivity = writable(false);
|
||||
export const cpuPercentage = writable(0);
|
||||
export const diskLatency = writable(0);
|
||||
@@ -0,0 +1,507 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment'
|
||||
import { aiActivity } from '$lib/activities.js'
|
||||
import { tryPlausible } from '$lib/plausible.js';
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
|
||||
var client = null;
|
||||
var messages = [];
|
||||
var stopFlag = false;
|
||||
var lastScreenshot = null;
|
||||
var screenshotCanvas = null;
|
||||
var screenshotCtx = null;
|
||||
|
||||
export function setApiKey(key)
|
||||
{
|
||||
client = new Anthropic({apiKey: key, dangerouslyAllowBrowser: true});
|
||||
// Reset messages
|
||||
messages = []
|
||||
messageList.set(messages);
|
||||
localStorage.setItem("anthropic-api-key", key);
|
||||
apiState.set("READY");
|
||||
tryPlausible("ClaudeAI Key");
|
||||
}
|
||||
|
||||
function clearApiKey()
|
||||
{
|
||||
localStorage.removeItem("anthropic-api-key");
|
||||
apiState.set("KEY_REQUIRED");
|
||||
}
|
||||
|
||||
function addMessageInternal(role, content)
|
||||
{
|
||||
messages.push({role: role, content: content});
|
||||
messageList.set(messages);
|
||||
}
|
||||
|
||||
async function sendMessages(handleTool)
|
||||
{
|
||||
aiActivity.set(true);
|
||||
try
|
||||
{
|
||||
var dc = get(displayConfig);
|
||||
var tool = dc ? { type: "computer_20250124", name: "computer", display_width_px: dc.width, display_height_px: dc.height, display_number: 1 } : { type: "bash_20250124", name: "bash" }
|
||||
const config = {max_tokens: 2048,
|
||||
messages: messages,
|
||||
system: "You are running on a virtualized machine. Wait some extra time after all operations to compensate for slowdown.",
|
||||
model: 'claude-3-7-sonnet-20250219',
|
||||
tools: [tool],
|
||||
tool_choice: {type: "auto", disable_parallel_tool_use: true},
|
||||
betas: ["computer-use-2025-01-24"]
|
||||
};
|
||||
if(get(enableThinking))
|
||||
config.thinking = { type: "enabled", budget_tokens: 1024 };
|
||||
const response = await client.beta.messages.create(config);
|
||||
if(stopFlag)
|
||||
{
|
||||
aiActivity.set(false);
|
||||
return;
|
||||
}
|
||||
// Remove all the image payloads, we don't want to send them over and over again
|
||||
for(var i=0;i<messages.length;i++)
|
||||
{
|
||||
var c = messages[i].content;
|
||||
if(Array.isArray(c))
|
||||
{
|
||||
if(c[0].type == "tool_result" && c[0].content && c[0].content[0].type == "image")
|
||||
delete c[0].content;
|
||||
}
|
||||
}
|
||||
var content = response.content;
|
||||
// Be robust to multiple response
|
||||
for(var i=0;i<content.length;i++)
|
||||
{
|
||||
var c = content[i];
|
||||
if(c.type == "text")
|
||||
{
|
||||
addMessageInternal(response.role, c.text);
|
||||
}
|
||||
else if(c.type == "tool_use")
|
||||
{
|
||||
addMessageInternal(response.role, [c]);
|
||||
var commandResponse = await handleTool(c.input);
|
||||
var responseObj = {type: "tool_result", tool_use_id: c.id };
|
||||
if(commandResponse != null)
|
||||
{
|
||||
if(commandResponse instanceof Error)
|
||||
{
|
||||
console.warn(`Tool error: ${commandResponse.message}`);
|
||||
responseObj.content = commandResponse.message;
|
||||
responseObj.is_error = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
responseObj.content = commandResponse;
|
||||
}
|
||||
}
|
||||
addMessageInternal("user", [responseObj]);
|
||||
if(stopFlag)
|
||||
{
|
||||
// Maintain state consitency by stopping after adding a valid response
|
||||
aiActivity.set(false);
|
||||
return;
|
||||
}
|
||||
sendMessages(handleTool);
|
||||
}
|
||||
else if(c.type == "thinking")
|
||||
{
|
||||
addMessageInternal(response.role, [c]);
|
||||
}
|
||||
else
|
||||
{
|
||||
console.warn(`Invalid response type: ${c.type}`);
|
||||
}
|
||||
}
|
||||
if(response.stop_reason == "end_turn")
|
||||
{
|
||||
tryPlausible("ClaudeAI Success");
|
||||
aiActivity.set(false);
|
||||
}
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
tryPlausible("ClaudeAI Error");
|
||||
if(e.status == 401)
|
||||
{
|
||||
addMessageInternal('error', 'Invalid API key');
|
||||
clearApiKey();
|
||||
}
|
||||
else
|
||||
{
|
||||
addMessageInternal('error', e.error.error.message);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function addMessage(text, handleTool)
|
||||
{
|
||||
addMessageInternal('user', text);
|
||||
sendMessages(handleTool);
|
||||
tryPlausible("ClaudeAI Use");
|
||||
}
|
||||
|
||||
export function clearMessageHistory() {
|
||||
messages.length = 0;
|
||||
messageList.set(messages);
|
||||
}
|
||||
|
||||
export function forceStop() {
|
||||
stopFlag = true;
|
||||
return new Promise((resolve) => {
|
||||
const unsubscribe = aiActivity.subscribe((value) => {
|
||||
if (!value) {
|
||||
unsubscribe();
|
||||
stopFlag = false;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getMessageDetails(msg) {
|
||||
const isToolUse = Array.isArray(msg.content) && msg.content[0].type === "tool_use";
|
||||
const isToolResult = Array.isArray(msg.content) && msg.content[0].type === "tool_result";
|
||||
const isThinking = Array.isArray(msg.content) && msg.content[0].type === "thinking";
|
||||
let icon = "";
|
||||
let messageContent = "";
|
||||
|
||||
if (isToolUse) {
|
||||
let tool = msg.content[0].input;
|
||||
if (tool.action === "screenshot") {
|
||||
icon = "fa-desktop";
|
||||
messageContent = "Screenshot";
|
||||
} else if (tool.action === "mouse_move") {
|
||||
icon = "fa-mouse-pointer";
|
||||
var coords = tool.coordinate;
|
||||
messageContent = `Mouse at (${coords[0]}, ${coords[1]})`;
|
||||
} else if (tool.action === "left_click") {
|
||||
icon = "fa-mouse-pointer";
|
||||
var coords = tool.coordinate;
|
||||
messageContent = `Left click at (${coords[0]}, ${coords[1]})`;
|
||||
} else if (tool.action === "right_click") {
|
||||
icon = "fa-mouse-pointer";
|
||||
var coords = tool.coordinate;
|
||||
messageContent = `Right click at (${coords[0]}, ${coords[1]})`;
|
||||
} else if (tool.action === "wait") {
|
||||
icon = "fa-hourglass-half";
|
||||
messageContent = "Waiting";
|
||||
} else if (tool.action === "key") {
|
||||
icon = "fa-keyboard";
|
||||
messageContent = `Key press: ${tool.text}`;
|
||||
} else if (tool.action === "type") {
|
||||
icon = "fa-keyboard";
|
||||
messageContent = "Type text";
|
||||
} else {
|
||||
icon = "fa-screwdriver-wrench";
|
||||
messageContent = "Use the system";
|
||||
}
|
||||
} else if (isThinking) {
|
||||
icon = "fa-brain";
|
||||
messageContent = "Thinking...";
|
||||
} else {
|
||||
icon = msg.role === "user" ? "fa-user" : "fa-robot";
|
||||
messageContent = msg.content;
|
||||
}
|
||||
|
||||
return {
|
||||
isToolUse,
|
||||
isToolResult,
|
||||
icon,
|
||||
messageContent,
|
||||
role: msg.role
|
||||
};
|
||||
}
|
||||
|
||||
async function yieldHelper(timeout)
|
||||
{
|
||||
return new Promise(function(f2, r2)
|
||||
{
|
||||
setTimeout(f2, timeout);
|
||||
});
|
||||
}
|
||||
|
||||
async function kmsSendChar(textArea, charStr)
|
||||
{
|
||||
textArea.value = "_" + charStr;
|
||||
var ke = new KeyboardEvent("keydown");
|
||||
textArea.dispatchEvent(ke);
|
||||
var ke = new KeyboardEvent("keyup");
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
}
|
||||
|
||||
function getKmsInputElement()
|
||||
{
|
||||
// Find the CheerpX textare, it's attached to the body element
|
||||
for(const node of document.body.children)
|
||||
{
|
||||
if(node.tagName == "TEXTAREA")
|
||||
return node;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function handleToolImpl(tool, term)
|
||||
{
|
||||
if(tool.command)
|
||||
{
|
||||
var sentinel = "# End of AI command";
|
||||
var buffer = term.buffer.active;
|
||||
// Get the current cursor position
|
||||
var marker = term.registerMarker();
|
||||
var startLine = marker.line;
|
||||
marker.dispose();
|
||||
var ret = new Promise(function(f, r)
|
||||
{
|
||||
var callbackDisposer = term.onWriteParsed(function()
|
||||
{
|
||||
var curLength = buffer.length;
|
||||
// Accumulate the output and see if the sentinel has been printed
|
||||
var output = "";
|
||||
for(var i=startLine + 1;i<curLength;i++)
|
||||
{
|
||||
var curLine = buffer.getLine(i).translateToString(true, 0, term.cols);;
|
||||
if(curLine.indexOf(sentinel) >= 0)
|
||||
{
|
||||
// We are done, cleanup and return
|
||||
callbackDisposer.dispose();
|
||||
return f(output);
|
||||
}
|
||||
output += curLine + "\n";
|
||||
}
|
||||
});
|
||||
});
|
||||
term.input(tool.command);
|
||||
term.input("\n");
|
||||
term.input(sentinel);
|
||||
term.input("\n");
|
||||
return ret;
|
||||
}
|
||||
else if(tool.action)
|
||||
{
|
||||
// Desktop control
|
||||
// TODO: We should have an explicit API to interact with CheerpX display
|
||||
switch(tool.action)
|
||||
{
|
||||
case "screenshot":
|
||||
{
|
||||
// Insert a 3 seconds delay unconditionally, the reference implementation uses 2
|
||||
await yieldHelper(3000);
|
||||
var delayCount = 0;
|
||||
var display = document.getElementById("display");
|
||||
var dc = get(displayConfig);
|
||||
if(screenshotCanvas == null)
|
||||
{
|
||||
screenshotCanvas = document.createElement("canvas");
|
||||
screenshotCtx = screenshotCanvas.getContext("2d");
|
||||
}
|
||||
if(screenshotCanvas.width != dc.width || screenshotCanvas.height != dc.height)
|
||||
{
|
||||
screenshotCanvas.width = dc.width;
|
||||
screenshotCanvas.height = dc.height;
|
||||
}
|
||||
while(1)
|
||||
{
|
||||
// Resize the canvas to a Claude compatible size
|
||||
screenshotCtx.drawImage(display, 0, 0, display.width, display.height, 0, 0, dc.width, dc.height);
|
||||
var dataUrl = screenshotCanvas.toDataURL("image/png");
|
||||
if(dataUrl == lastScreenshot)
|
||||
{
|
||||
// Delay at most 3 times
|
||||
if(delayCount < 3)
|
||||
{
|
||||
// TODO: Defensive message, validate and remove
|
||||
console.warn("Identical screenshot, rate limiting");
|
||||
delayCount++;
|
||||
// Wait some time and retry
|
||||
await yieldHelper(5000);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
lastScreenshot = dataUrl;
|
||||
// Remove prefix from the encoded data
|
||||
dataUrl = dataUrl.substring("data:image/png;base64,".length);
|
||||
var imageSrc = { type: "base64", media_type: "image/png", data: dataUrl };
|
||||
var contentObj = { type: "image", source: imageSrc };
|
||||
return [ contentObj ];
|
||||
}
|
||||
}
|
||||
case "mouse_move":
|
||||
{
|
||||
var coords = tool.coordinate;
|
||||
var dc = get(displayConfig);
|
||||
var mouseX = coords[0] / dc.mouseMult;
|
||||
var mouseY = coords[1] / dc.mouseMult;
|
||||
var display = document.getElementById("display");
|
||||
var clientRect = display.getBoundingClientRect();
|
||||
var me = new MouseEvent('mousemove', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top });
|
||||
display.dispatchEvent(me);
|
||||
return null;
|
||||
}
|
||||
case "left_click":
|
||||
{
|
||||
var coords = tool.coordinate;
|
||||
var dc = get(displayConfig);
|
||||
var mouseX = coords[0] / dc.mouseMult;
|
||||
var mouseY = coords[1] / dc.mouseMult;
|
||||
var display = document.getElementById("display");
|
||||
var clientRect = display.getBoundingClientRect();
|
||||
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
|
||||
display.dispatchEvent(me);
|
||||
// This delay prevent X11 logic from debouncing the mouseup
|
||||
await yieldHelper(60)
|
||||
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 0 });
|
||||
display.dispatchEvent(me);
|
||||
return null;
|
||||
}
|
||||
case "right_click":
|
||||
{
|
||||
var coords = tool.coordinate;
|
||||
var dc = get(displayConfig);
|
||||
var mouseX = coords[0] / dc.mouseMult;
|
||||
var mouseY = coords[1] / dc.mouseMult;
|
||||
var display = document.getElementById("display");
|
||||
var clientRect = display.getBoundingClientRect();
|
||||
var me = new MouseEvent('mousedown', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
|
||||
display.dispatchEvent(me);
|
||||
// This delay prevent X11 logic from debouncing the mouseup
|
||||
await yieldHelper(60)
|
||||
me = new MouseEvent('mouseup', { clientX: mouseX + clientRect.left, clientY: mouseY + clientRect.top, button: 2 });
|
||||
display.dispatchEvent(me);
|
||||
return null;
|
||||
}
|
||||
case "type":
|
||||
{
|
||||
var str = tool.text;
|
||||
return new Promise(async function(f, r)
|
||||
{
|
||||
var textArea = getKmsInputElement();
|
||||
for(var i=0;i<str.length;i++)
|
||||
{
|
||||
await kmsSendChar(textArea, str[i]);
|
||||
}
|
||||
f(null);
|
||||
});
|
||||
}
|
||||
case "key":
|
||||
{
|
||||
var textArea = getKmsInputElement();
|
||||
var key = tool.text;
|
||||
// Support arbitrary order of modifiers
|
||||
var isCtrl = false;
|
||||
var isAlt = false;
|
||||
var isShift = false;
|
||||
while(1)
|
||||
{
|
||||
if(key.startsWith("shift+"))
|
||||
{
|
||||
isShift = true;
|
||||
key = key.substr("shift+".length);
|
||||
var ke = new KeyboardEvent("keydown", {keyCode: 0x10});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
continue;
|
||||
}
|
||||
else if(key.startsWith("ctrl+"))
|
||||
{
|
||||
isCtrl = true;
|
||||
key = key.substr("ctrl+".length);
|
||||
var ke = new KeyboardEvent("keydown", {keyCode: 0x11});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
continue;
|
||||
}
|
||||
else if(key.startsWith("alt+"))
|
||||
{
|
||||
isAlt = true;
|
||||
key = key.substr("alt+".length);
|
||||
var ke = new KeyboardEvent("keydown", {keyCode: 0x12});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
var ret = null;
|
||||
// Dispatch single chars directly and parse the rest
|
||||
if(key.length == 1)
|
||||
{
|
||||
await kmsSendChar(textArea, key);
|
||||
}
|
||||
else
|
||||
{
|
||||
switch(tool.text)
|
||||
{
|
||||
case "Return":
|
||||
await kmsSendChar(textArea, "\n");
|
||||
break;
|
||||
case "Escape":
|
||||
var ke = new KeyboardEvent("keydown", {keyCode: 0x1b});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
ke = new KeyboardEvent("keyup", {keyCode: 0x1b});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
break;
|
||||
default:
|
||||
// TODO: Support more key combinations
|
||||
ret = new Error(`Error: Invalid key '${tool.text}'`);
|
||||
}
|
||||
}
|
||||
if(isShift)
|
||||
{
|
||||
var ke = new KeyboardEvent("keyup", {keyCode: 0x10});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
}
|
||||
if(isCtrl)
|
||||
{
|
||||
var ke = new KeyboardEvent("keyup", {keyCode: 0x11});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
}
|
||||
if(isAlt)
|
||||
{
|
||||
var ke = new KeyboardEvent("keyup", {keyCode: 0x12});
|
||||
textArea.dispatchEvent(ke);
|
||||
await yieldHelper(0);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
case "wait":
|
||||
{
|
||||
// Wait 2x what the machine expects to compensate for virtualization slowdown
|
||||
await yieldHelper(tool.duration * 2 * 1000);
|
||||
return null;
|
||||
}
|
||||
default:
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new Error("Error: Invalid action");
|
||||
}
|
||||
else
|
||||
{
|
||||
// We can get there due to model hallucinations
|
||||
return new Error("Error: Invalid tool syntax");
|
||||
}
|
||||
}
|
||||
|
||||
function initialize()
|
||||
{
|
||||
var savedApiKey = localStorage.getItem("anthropic-api-key");
|
||||
if(savedApiKey)
|
||||
setApiKey(savedApiKey);
|
||||
}
|
||||
|
||||
export const apiState = writable("KEY_REQUIRED");
|
||||
export const messageList = writable(messages);
|
||||
export const currentMessage = writable("");
|
||||
export const displayConfig = writable(null);
|
||||
export const enableThinking = writable(false);
|
||||
|
||||
if(browser)
|
||||
initialize();
|
||||
@@ -0,0 +1,26 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,100..900;1,100..900&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind utilities;
|
||||
|
||||
body
|
||||
{
|
||||
font-family: Archivo, sans-serif;
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: black;
|
||||
}
|
||||
|
||||
html
|
||||
{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@media (width <= 850px)
|
||||
{
|
||||
html
|
||||
{
|
||||
font-size: calc(100vw / 55);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
const color = "\x1b[1;35m";
|
||||
const underline = "\x1b[94;4m";
|
||||
const normal = "\x1b[0m";
|
||||
export const introMessage = [
|
||||
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"| |",
|
||||
"| WebVM is a virtual Linux environment running in the browser via WebAssembly.|",
|
||||
"| |",
|
||||
"| WebVM is powered by the CheerpX virtualization engine, which enables safe, |",
|
||||
"| sandboxed execution of x86 binaries, fully client-side. |",
|
||||
"| |",
|
||||
"| CheerpX includes an x86-to-WebAssembly JIT compiler, a virtual block-based |",
|
||||
"| file system, and a Linux syscall emulator. |",
|
||||
"| |",
|
||||
"| Try out the new Alpine / Xorg / i3 WebVM: " + underline + "https://webvm.io/alpine.html" + normal + " |",
|
||||
"| |",
|
||||
"| [News] BrowserCode: run Claude Code in the browser via WebAssembly |",
|
||||
"| |",
|
||||
"| " + underline + "https://browsercode.io/claude" + normal + " |",
|
||||
"| |",
|
||||
"+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+",
|
||||
"",
|
||||
" Welcome to WebVM. If unsure, try these examples:",
|
||||
"",
|
||||
" python3 examples/python3/fibonacci.py ",
|
||||
" gcc -o helloworld examples/c/helloworld.c && ./helloworld",
|
||||
" objdump -d ./helloworld | less -M",
|
||||
" vim examples/c/helloworld.c",
|
||||
" curl --max-time 15 parrot.live # requires networking",
|
||||
"",
|
||||
];
|
||||
export const errorMessage = [
|
||||
color + "CheerpX could not start" + normal,
|
||||
"",
|
||||
"Check the DevTools console for more information",
|
||||
"",
|
||||
"CheerpX is expected to work with recent desktop versions of Chrome, Edge, Firefox and Safari",
|
||||
"",
|
||||
"Give it a try from a desktop version / another browser!",
|
||||
"",
|
||||
"CheerpX internal error message is:",
|
||||
"",
|
||||
];
|
||||
export const unexpectedErrorMessage = [
|
||||
color + "WebVM encountered an unexpected error" + normal,
|
||||
"",
|
||||
"Check the DevTools console for further information",
|
||||
"",
|
||||
"Please consider reporting a bug!",
|
||||
"",
|
||||
"CheerpX internal error message is:",
|
||||
"",
|
||||
];
|
||||
@@ -0,0 +1,190 @@
|
||||
import { writable } from 'svelte/store';
|
||||
import { browser } from '$app/environment'
|
||||
|
||||
let authKey = undefined;
|
||||
let controlUrl = undefined;
|
||||
if(browser)
|
||||
{
|
||||
let params = new URLSearchParams("?"+window.location.hash.substr(1));
|
||||
authKey = params.get("authKey") || undefined;
|
||||
controlUrl = params.get("controlUrl") || undefined;
|
||||
}
|
||||
let dashboardUrl = controlUrl ? null : "https://login.tailscale.com/admin/machines";
|
||||
let resolveLogin = null;
|
||||
let rejectLogin = null;
|
||||
let loginPromise = null;
|
||||
let connectionState = writable("DISCONNECTED");
|
||||
let exitNode = writable(false);
|
||||
|
||||
function resetLoginPromise()
|
||||
{
|
||||
loginPromise = new Promise((f,r) => {
|
||||
resolveLogin = f;
|
||||
rejectLogin = r;
|
||||
});
|
||||
}
|
||||
|
||||
function validateLoginUrl(url)
|
||||
{
|
||||
const parsedUrl = new URL(url);
|
||||
if(parsedUrl.protocol != "https:" && parsedUrl.protocol != "http:")
|
||||
throw new Error("Invalid Tailscale login URL scheme");
|
||||
return parsedUrl.href;
|
||||
}
|
||||
|
||||
function loginUrlCb(url)
|
||||
{
|
||||
try
|
||||
{
|
||||
url = validateLoginUrl(url);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
connectionState.set("LOGINFAILED");
|
||||
rejectLogin(e);
|
||||
resetLoginPromise();
|
||||
return;
|
||||
}
|
||||
connectionState.set("LOGINREADY");
|
||||
resolveLogin(url);
|
||||
}
|
||||
|
||||
function stateUpdateCb(state)
|
||||
{
|
||||
switch(state)
|
||||
{
|
||||
case 6 /*Running*/:
|
||||
{
|
||||
connectionState.set("CONNECTED");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function netmapUpdateCb(map)
|
||||
{
|
||||
networkData.currentIp = map.self.addresses[0];
|
||||
var exitNodeFound = false;
|
||||
for(var i=0; i < map.peers.length;i++)
|
||||
{
|
||||
if(map.peers[i].exitNode)
|
||||
{
|
||||
exitNodeFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(exitNodeFound)
|
||||
{
|
||||
exitNode.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
export async function startLogin()
|
||||
{
|
||||
connectionState.set("LOGINSTARTING");
|
||||
const url = await loginPromise;
|
||||
networkData.loginUrl = url;
|
||||
return url;
|
||||
}
|
||||
|
||||
async function handleCopyIP(event)
|
||||
{
|
||||
// To prevent the default contexmenu from showing up when right-clicking..
|
||||
event.preventDefault();
|
||||
// Copy the IP to the clipboard.
|
||||
try
|
||||
{
|
||||
await window.navigator.clipboard.writeText(networkData.currentIp)
|
||||
connectionState.set("IPCOPIED");
|
||||
setTimeout(() => {
|
||||
connectionState.set("CONNECTED");
|
||||
}, 2000);
|
||||
}
|
||||
catch(msg)
|
||||
{
|
||||
console.log("Copy ip to clipboard: Error: " + msg);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateButtonData(state, handleConnect) {
|
||||
switch(state) {
|
||||
case "DISCONNECTED":
|
||||
return {
|
||||
buttonText: "Connect to Tailscale",
|
||||
isClickable: true,
|
||||
clickHandler: handleConnect,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
case "DOWNLOADING":
|
||||
return {
|
||||
buttonText: "Loading IP stack...",
|
||||
isClickable: false,
|
||||
clickHandler: null,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
case "LOGINSTARTING":
|
||||
return {
|
||||
buttonText: "Starting Login...",
|
||||
isClickable: false,
|
||||
clickHandler: null,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
case "LOGINREADY":
|
||||
return {
|
||||
buttonText: "Login to Tailscale",
|
||||
isClickable: true,
|
||||
clickHandler: null,
|
||||
clickUrl: networkData.loginUrl,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
case "LOGINFAILED":
|
||||
return {
|
||||
buttonText: "Invalid login URL",
|
||||
isClickable: false,
|
||||
clickHandler: null,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
case "CONNECTED":
|
||||
return {
|
||||
buttonText: `IP: ${networkData.currentIp}`,
|
||||
isClickable: true,
|
||||
clickHandler: null,
|
||||
clickUrl: networkData.dashboardUrl,
|
||||
buttonTooltip: "Right-click to copy",
|
||||
rightClickHandler: handleCopyIP
|
||||
};
|
||||
case "IPCOPIED":
|
||||
return {
|
||||
buttonText: "Copied!",
|
||||
isClickable: false,
|
||||
clickHandler: null,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
default:
|
||||
return {
|
||||
buttonText: `Text for state: ${state}`,
|
||||
isClickable: false,
|
||||
clickHandler: null,
|
||||
clickUrl: null,
|
||||
buttonTooltip: null,
|
||||
rightClickHandler: null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const networkInterface = { authKey: authKey, controlUrl: controlUrl, loginUrlCb: loginUrlCb, stateUpdateCb: stateUpdateCb, netmapUpdateCb: netmapUpdateCb };
|
||||
|
||||
export const networkData = { currentIp: null, connectionState: connectionState, exitNode: exitNode, loginUrl: null, dashboardUrl: dashboardUrl }
|
||||
|
||||
resetLoginPromise();
|
||||
@@ -0,0 +1,6 @@
|
||||
// Some ad-blockers block the plausible script from loading. Check if `plausible`
|
||||
// is defined before calling it.
|
||||
export function tryPlausible(msg) {
|
||||
if (self.plausible)
|
||||
plausible(msg)
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { parse } from 'node-html-parser';
|
||||
import { read } from '$app/server';
|
||||
|
||||
var posts = [
|
||||
"https://labs.leaningtech.com/blog/webvm-claude",
|
||||
"https://labs.leaningtech.com/blog/cx-10",
|
||||
"https://labs.leaningtech.com/blog/webvm-20",
|
||||
"https://labs.leaningtech.com/blog/join-the-webvm-hackathon",
|
||||
"https://labs.leaningtech.com/blog/mini-webvm-your-linux-box-from-dockerfile-via-wasm",
|
||||
"https://labs.leaningtech.com/blog/webvm-virtual-machine-with-networking-via-tailscale",
|
||||
"https://labs.leaningtech.com/blog/webvm-server-less-x86-virtual-machines-in-the-browser",
|
||||
];
|
||||
|
||||
async function getPostData(u)
|
||||
{
|
||||
var ret = { title: null, image: null, url: u };
|
||||
var response = await fetch(u);
|
||||
var str = await response.text();
|
||||
var root = parse(str);
|
||||
var tags = root.getElementsByTagName("meta");
|
||||
for(var i=0;i<tags.length;i++)
|
||||
{
|
||||
var metaName = tags[i].getAttribute("property");
|
||||
var metaContent = tags[i].getAttribute("content");
|
||||
switch(metaName)
|
||||
{
|
||||
case "og:title":
|
||||
ret.title = metaContent;
|
||||
break;
|
||||
case "og:image":
|
||||
ret.image = metaContent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export async function load()
|
||||
{
|
||||
var ret = [];
|
||||
for(var i=0;i<posts.length;i++)
|
||||
{
|
||||
ret.push(await getPostData(posts[i]));
|
||||
}
|
||||
return { posts: ret };
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import WebVM from '$lib/WebVM.svelte';
|
||||
import * as configObj from '/config_terminal';
|
||||
import { tryPlausible } from '$lib/plausible.js';
|
||||
function handleProcessCreated(processCount)
|
||||
{
|
||||
// Log the first 5 processes, to get an idea of the level of interaction from the public
|
||||
if(processCount <= 5)
|
||||
{
|
||||
tryPlausible(`Process started: ${processCount}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_terminal">
|
||||
<p>Looking for a complete desktop experience? Try the new <a class="underline" href="/alpine.html" target="_blank">Alpine Linux</a> graphical WebVM</p>
|
||||
</WebVM>
|
||||
@@ -0,0 +1 @@
|
||||
export const prerender = true;
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
import WebVM from '$lib/WebVM.svelte';
|
||||
import * as configObj from '/config_public_alpine';
|
||||
import { tryPlausible } from '$lib/plausible.js';
|
||||
function handleProcessCreated(processCount)
|
||||
{
|
||||
// Log only the first process, as a proxy for successful startup
|
||||
if(processCount == 1)
|
||||
{
|
||||
tryPlausible("Alpine init");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<WebVM configObj={configObj} processCallback={handleProcessCreated} cacheId="blocks_alpine">
|
||||
<p>Looking for something different? Try the classic <a class="underline" href="/" target="_blank">Debian Linux</a> terminal-based WebVM</p>
|
||||
</WebVM>
|
||||
@@ -0,0 +1,12 @@
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
},
|
||||
preprocess: vitePreprocess()
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { viteStaticCopy } from 'vite-plugin-static-copy';
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'/config_terminal': process.env.WEBVM_MODE == "github" ? 'config_github_terminal.js' : 'config_public_terminal.js',
|
||||
"@leaningtech/cheerpx": process.env.CX_URL ? process.env.CX_URL : "@leaningtech/cheerpx"
|
||||
}
|
||||
},
|
||||
build: {
|
||||
target: "es2022"
|
||||
},
|
||||
plugins: [
|
||||
sveltekit(),
|
||||
viteStaticCopy({
|
||||
targets: [
|
||||
{ src: 'tower.ico', dest: '' },
|
||||
{ src: 'scrollbar.css', dest: '' },
|
||||
{ src: 'serviceWorker.js', dest: '' },
|
||||
{ src: 'login.html', dest: '' },
|
||||
{ src: 'assets/', dest: '' },
|
||||
{ src: 'documents/', dest: '' }
|
||||
]
|
||||
})
|
||||
]
|
||||
});
|
||||
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=addon-fit.js.map
|
||||
@@ -0,0 +1,2 @@
|
||||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.WebLinksAddon=t():e.WebLinksAddon=t()}(self,(()=>(()=>{"use strict";var e={6:(e,t)=>{function n(e){try{const t=new URL(e),n=t.password&&t.username?`${t.protocol}//${t.username}:${t.password}@${t.host}`:t.username?`${t.protocol}//${t.username}@${t.host}`:`${t.protocol}//${t.host}`;return e.toLocaleLowerCase().startsWith(n.toLocaleLowerCase())}catch(e){return!1}}Object.defineProperty(t,"__esModule",{value:!0}),t.LinkComputer=t.WebLinkProvider=void 0,t.WebLinkProvider=class{constructor(e,t,n,o={}){this._terminal=e,this._regex=t,this._handler=n,this._options=o}provideLinks(e,t){const n=o.computeLink(e,this._regex,this._terminal,this._handler);t(this._addCallbacks(n))}_addCallbacks(e){return e.map((e=>(e.leave=this._options.leave,e.hover=(t,n)=>{if(this._options.hover){const{range:o}=e;this._options.hover(t,n,o)}},e)))}};class o{static computeLink(e,t,r,i){const s=new RegExp(t.source,(t.flags||"")+"g"),[a,c]=o._getWindowedLineStrings(e-1,r),l=a.join("");let d;const p=[];for(;d=s.exec(l);){const e=d[0];if(!n(e))continue;const[t,s]=o._mapStrIdx(r,c,0,d.index),[a,l]=o._mapStrIdx(r,t,s,e.length);if(-1===t||-1===s||-1===a||-1===l)continue;const h={start:{x:s+1,y:t+1},end:{x:l,y:a+1}};p.push({range:h,text:e,activate:i})}return p}static _getWindowedLineStrings(e,t){let n,o=e,r=e,i=0,s="";const a=[];if(n=t.buffer.active.getLine(e)){const e=n.translateToString(!0);if(n.isWrapped&&" "!==e[0]){for(i=0;(n=t.buffer.active.getLine(--o))&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),n.isWrapped&&-1===s.indexOf(" ")););a.reverse()}for(a.push(e),i=0;(n=t.buffer.active.getLine(++r))&&n.isWrapped&&i<2048&&(s=n.translateToString(!0),i+=s.length,a.push(s),-1===s.indexOf(" ")););}return[a,o]}static _mapStrIdx(e,t,n,o){const r=e.buffer.active,i=r.getNullCell();let s=n;for(;o;){const e=r.getLine(t);if(!e)return[-1,-1];for(let n=s;n<e.length;++n){e.getCell(n,i);const s=i.getChars();if(i.getWidth()&&(o-=s.length||1,n===e.length-1&&""===s)){const e=r.getLine(t+1);e&&e.isWrapped&&(e.getCell(0,i),2===i.getWidth()&&(o+=1))}if(o<0)return[t,n]}t++,s=0}return[t,s]}}t.LinkComputer=o}},t={};function n(o){var r=t[o];if(void 0!==r)return r.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,n),i.exports}var o={};return(()=>{var e=o;Object.defineProperty(e,"__esModule",{value:!0}),e.WebLinksAddon=void 0;const t=n(6),r=/(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;function i(e,t){const n=window.open();if(n){try{n.opener=null}catch{}n.location.href=t}else console.warn("Opening link blocked as opener could not be cleared")}e.WebLinksAddon=class{constructor(e=i,t={}){this._handler=e,this._options=t}activate(e){this._terminal=e;const n=this._options,o=n.urlRegex||r;this._linkProvider=this._terminal.registerLinkProvider(new t.WebLinkProvider(this._terminal,o,this._handler,n))}dispose(){this._linkProvider?.dispose()}}})(),o})()));
|
||||
//# sourceMappingURL=addon-web-links.js.map
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility:not(.debug),
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree:not(.debug) *::selection {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility-tree {
|
||||
user-select: text;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||