Compare commits
14 Commits
reuse-exam
...
kurt-speed
Author | SHA1 | Date | |
---|---|---|---|
182865014e | |||
2452eede0b | |||
98442b9ec2 | |||
fb1c8036f6 | |||
2918612d4b | |||
abbd065c2c | |||
23e29b024f | |||
807adac371 | |||
03eb8dca32 | |||
e3358f8251 | |||
49ea3991b2 | |||
f32f0e2717 | |||
0363e4f4e0 | |||
5e60dbd5e8 |
@ -1,5 +1,6 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.dev.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
VITE_KC_API_BASE_URL=https://api.dev.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
VITE_KC_SITE_BASE_URL=https://dev.zoo.dev
|
||||||
|
VITE_KC_WASM_OVERRIDE_URL=""
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
VITE_KC_CONNECTION_TIMEOUT_MS=5000
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
VITE_KC_API_WS_MODELING_URL=wss://api.zoo.dev/ws/modeling/commands
|
||||||
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
VITE_KC_API_BASE_URL=https://api.zoo.dev
|
||||||
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
VITE_KC_SITE_BASE_URL=https://zoo.dev
|
||||||
|
VITE_KC_WASM_OVERRIDE_URL=""
|
||||||
VITE_KC_SKIP_AUTH=false
|
VITE_KC_SKIP_AUTH=false
|
||||||
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
VITE_KC_CONNECTION_TIMEOUT_MS=15000
|
||||||
|
33
.github/workflows/build-and-store-wasm.yml
vendored
@ -1,33 +0,0 @@
|
|||||||
name: Build and Store WASM
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-upload:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version-file: '.nvmrc'
|
|
||||||
cache: 'yarn'
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn
|
|
||||||
- name: Setup Rust
|
|
||||||
uses: dtolnay/rust-toolchain@stable
|
|
||||||
- name: Cache wasm
|
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
- name: build wasm
|
|
||||||
run: yarn build:wasm
|
|
||||||
|
|
||||||
|
|
||||||
# Upload the WASM bundle as an artifact
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: wasm-bundle
|
|
||||||
path: src/wasm-lib/pkg
|
|
17
.github/workflows/cargo-clippy.yml
vendored
@ -19,7 +19,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
dir: ['src/wasm-lib', 'src-tauri']
|
dir: ['src/wasm-lib']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install latest rust
|
- name: Install latest rust
|
||||||
@ -31,22 +31,9 @@ jobs:
|
|||||||
|
|
||||||
- name: install dependencies
|
- name: install dependencies
|
||||||
if: matrix.dir == 'src-tauri'
|
if: matrix.dir == 'src-tauri'
|
||||||
shell: bash
|
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
libgtk-3-dev \
|
|
||||||
libayatana-appindicator3-dev \
|
|
||||||
webkit2gtk-driver \
|
|
||||||
libsoup-3.0-dev \
|
|
||||||
libjavascriptcoregtk-4.1-dev \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
at-spi2-core \
|
|
||||||
xvfb
|
|
||||||
yarn install
|
|
||||||
yarn build:wasm
|
|
||||||
yarn build:local
|
|
||||||
|
|
||||||
- name: Rust Cache
|
- name: Rust Cache
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
uses: Swatinem/rust-cache@v2.6.1
|
||||||
|
|
||||||
|
57
.github/workflows/cargo-test-tauri.yml
vendored
@ -1,57 +0,0 @@
|
|||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'src-tauri/**.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- .github/workflows/cargo-test-tauri.yml
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- 'src-tauri/**.rs'
|
|
||||||
- '**/Cargo.toml'
|
|
||||||
- '**/Cargo.lock'
|
|
||||||
- '**/rust-toolchain.toml'
|
|
||||||
- .github/workflows/cargo-test-tauri.yml
|
|
||||||
workflow_dispatch:
|
|
||||||
permissions: read-all
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
name: cargo test of tauri
|
|
||||||
jobs:
|
|
||||||
cargotest:
|
|
||||||
name: cargo test
|
|
||||||
runs-on: ubuntu-latest-8-cores
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
dir: ['src-tauri']
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install latest rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- name: install dependencies
|
|
||||||
if: matrix.dir == 'src-tauri'
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y \
|
|
||||||
libgtk-3-dev \
|
|
||||||
libayatana-appindicator3-dev \
|
|
||||||
webkit2gtk-driver \
|
|
||||||
libsoup-3.0-dev \
|
|
||||||
libjavascriptcoregtk-4.1-dev \
|
|
||||||
libwebkit2gtk-4.1-dev \
|
|
||||||
at-spi2-core \
|
|
||||||
xvfb
|
|
||||||
- name: Rust Cache
|
|
||||||
uses: Swatinem/rust-cache@v2.6.1
|
|
||||||
- name: cargo test
|
|
||||||
shell: bash
|
|
||||||
run: |-
|
|
||||||
cd "${{ matrix.dir }}"
|
|
||||||
cargo test --all
|
|
115
.github/workflows/ci.yml
vendored
@ -13,7 +13,7 @@ on:
|
|||||||
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
# Will checkout the last commit from the default branch (main as of 2023-10-04)
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && (contains(github.event.pull_request.title, 'Cut release v')) }}
|
BUILD_RELEASE: ${{ github.event_name == 'release' || github.event_name == 'schedule' || github.event_name == 'pull_request' && contains(github.event.pull_request.title, 'Cut release v') }}
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||||
@ -50,7 +50,7 @@ jobs:
|
|||||||
- run: yarn tsc
|
- run: yarn tsc
|
||||||
|
|
||||||
|
|
||||||
check-typos:
|
check-typos:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@ -98,7 +98,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version-file: '.nvmrc'
|
node-version-file: '.nvmrc'
|
||||||
cache: 'yarn'
|
cache: 'yarn'
|
||||||
|
|
||||||
- name: Set nightly version
|
- name: Set nightly version
|
||||||
if: github.event_name == 'schedule'
|
if: github.event_name == 'schedule'
|
||||||
run: |
|
run: |
|
||||||
@ -143,21 +143,21 @@ jobs:
|
|||||||
ls -l artifact
|
ls -l artifact
|
||||||
cp artifact/package.json package.json
|
cp artifact/package.json package.json
|
||||||
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
cp artifact/src-tauri/tauri.conf.json src-tauri/tauri.conf.json
|
||||||
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
cp artifact/src-tauri/tauri.release.conf.json src-tauri/tauri.release.conf.json
|
||||||
|
|
||||||
- name: Install ubuntu system dependencies
|
- name: Install ubuntu system dependencies
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: |
|
run: >
|
||||||
sudo apt-get update
|
sudo apt-get update &&
|
||||||
sudo apt-get install -y \
|
sudo apt-get install -y
|
||||||
libgtk-3-dev \
|
libgtk-3-dev
|
||||||
libayatana-appindicator3-dev \
|
libayatana-appindicator3-dev
|
||||||
webkit2gtk-driver \
|
webkit2gtk-driver
|
||||||
libsoup-3.0-dev \
|
libsoup-3.0-dev
|
||||||
libjavascriptcoregtk-4.1-dev \
|
libjavascriptcoregtk-4.1-dev
|
||||||
libwebkit2gtk-4.1-dev \
|
libwebkit2gtk-4.1-dev
|
||||||
at-spi2-core \
|
at-spi2-core
|
||||||
xvfb
|
xvfb
|
||||||
|
|
||||||
- name: Sync node version and setup cache
|
- name: Sync node version and setup cache
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
@ -237,83 +237,6 @@ jobs:
|
|||||||
includeDebug: true
|
includeDebug: true
|
||||||
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
args: "${{ env.TAURI_ARGS_MACOS }} ${{ env.TAURI_ARGS_UBUNTU }}"
|
||||||
|
|
||||||
|
|
||||||
- name: Mac App Store
|
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
|
|
||||||
run: |
|
|
||||||
unset APPLE_SIGNING_IDENTITY
|
|
||||||
unset APPLE_CERTIFICATE
|
|
||||||
sign_app="3rd Party Mac Developer Application: KittyCAD Inc (${APPLE_TEAM_ID})"
|
|
||||||
sign_install="3rd Party Mac Developer Installer: KittyCAD Inc (${APPLE_TEAM_ID})"
|
|
||||||
profile="src-tauri/entitlements/Mac_App_Distribution.provisionprofile"
|
|
||||||
|
|
||||||
mkdir -p src-tauri/entitlements
|
|
||||||
echo "${APPLE_STORE_PROVISIONING_PROFILE}" | base64 --decode > "${profile}"
|
|
||||||
|
|
||||||
echo "${APPLE_STORE_DISTRIBUTION_CERT}" | base64 --decode > "dist.cer"
|
|
||||||
echo "${APPLE_STORE_INSTALLER_CERT}" | base64 --decode > "installer.cer"
|
|
||||||
|
|
||||||
# load the certificates into the keychain
|
|
||||||
# Create a custom keychain
|
|
||||||
security create-keychain -p gh_actions refine-build.keychain
|
|
||||||
|
|
||||||
# Make the custom keychain default, so xcodebuild will use it for signing
|
|
||||||
security default-keychain -s refine-build.keychain
|
|
||||||
|
|
||||||
# Unlock the keychain
|
|
||||||
security unlock-keychain -p gh_actions refine-build.keychain
|
|
||||||
|
|
||||||
# Set keychain timeout to 1 hour for long builds
|
|
||||||
security set-keychain-settings -t 3600 -l ~/Library/Keychains/refine-build.keychain
|
|
||||||
|
|
||||||
# Add certificates to keychain and allow codesign to access them
|
|
||||||
security import "dist.cer" -k ~/Library/Keychains/refine-build.keychain -T /usr/bin/codesign
|
|
||||||
security import "installer.cer" -k ~/Library/Keychains/refine-build.keychain -T /usr/bin/codesign
|
|
||||||
|
|
||||||
security set-key-partition-list -S apple-tool:,apple: -s -k gh_actions refine-build.keychain
|
|
||||||
|
|
||||||
target="universal-apple-darwin"
|
|
||||||
|
|
||||||
# Turn off the default target
|
|
||||||
sed -i "s/default =/# default =/" src-tauri/Cargo.toml
|
|
||||||
yarn tauri build --target "${target}" --verbose
|
|
||||||
|
|
||||||
ls -l src-tauri/target/${target}
|
|
||||||
ls -l src-tauri/target
|
|
||||||
ls -l src-tauri/target/${target}/release/bundle/macos
|
|
||||||
ls -l src-tauri/entitlements
|
|
||||||
|
|
||||||
app_path="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app"
|
|
||||||
build_name="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.pkg"
|
|
||||||
cp_dir="src-tauri/target/${target}/release/bundle/macos/Zoo Modeling App.app/Contents/embedded.provisionprofile"
|
|
||||||
entitlements="src-tauri/entitlements/Zoo Modeling App.entitlements"
|
|
||||||
|
|
||||||
cp "${profile}" "${cp_dir}"
|
|
||||||
|
|
||||||
codesign --deep --force -s "${sign_app}" --entitlements "${entitlements}" "${app_path}"
|
|
||||||
|
|
||||||
productbuild --component "${app_path}" /Applications/ --sign "${sign_install}" "${build_name}"
|
|
||||||
|
|
||||||
# Undo the changes to the Cargo.toml
|
|
||||||
git checkout src-tauri/Cargo.toml
|
|
||||||
env:
|
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
|
||||||
APPLE_STORE_PROVISIONING_PROFILE: ${{ secrets.APPLE_STORE_PROVISIONING_PROFILE }}
|
|
||||||
APPLE_STORE_DISTRIBUTION_CERT: ${{ secrets.APPLE_STORE_DISTRIBUTION_CERT }}
|
|
||||||
APPLE_STORE_INSTALLER_CERT: ${{ secrets.APPLE_STORE_INSTALLER_CERT }}
|
|
||||||
|
|
||||||
- name: 'Upload app to TestFlight'
|
|
||||||
uses: apple-actions/upload-testflight-build@v1
|
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' && matrix.os == 'macos-14' }}
|
|
||||||
with:
|
|
||||||
app-path: 'src-tauri/target/universal-apple-darwin/release/bundle/macos/Zoo Modeling App.pkg'
|
|
||||||
issuer-id: ${{ secrets.APPLE_STORE_ISSUER_ID }}
|
|
||||||
api-key-id: ${{ secrets.APPLE_STORE_API_KEY_ID }}
|
|
||||||
api-private-key: ${{ secrets.APPLE_STORE_API_PRIVATE_KEY }}
|
|
||||||
|
|
||||||
# We do this after the apple store because the apple store build is
|
|
||||||
# specific and we want to overwrite it with the this new build after and
|
|
||||||
# not upload the apple store build to the public bucket
|
|
||||||
- name: Build the app (release) and sign
|
- name: Build the app (release) and sign
|
||||||
uses: tauri-apps/tauri-action@v0
|
uses: tauri-apps/tauri-action@v0
|
||||||
if: ${{ env.BUILD_RELEASE == 'true' }}
|
if: ${{ env.BUILD_RELEASE == 'true' }}
|
||||||
@ -460,7 +383,7 @@ jobs:
|
|||||||
uses: softprops/action-gh-release@v2
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
files: 'artifact/*/Zoo*'
|
files: 'artifact/*/Zoo*'
|
||||||
|
|
||||||
announce_release:
|
announce_release:
|
||||||
needs: [publish-apps-release]
|
needs: [publish-apps-release]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -468,17 +391,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: '3.x'
|
python-version: '3.x'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install requests
|
pip install requests
|
||||||
|
|
||||||
- name: Announce Release
|
- name: Announce Release
|
||||||
env:
|
env:
|
||||||
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
|
||||||
|
37
.github/workflows/create-release.yml
vendored
@ -1,37 +0,0 @@
|
|||||||
name: Create Release
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create-release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
pull-requests: read
|
|
||||||
if: contains(github.event.head_commit.message, 'Cut release v')
|
|
||||||
steps:
|
|
||||||
- uses: actions/github-script@v7
|
|
||||||
name: Read Cut release PR info and create release
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const { owner, repo, sha } = context.repo
|
|
||||||
const pulls = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
commit_sha: sha,
|
|
||||||
})
|
|
||||||
const { title, body } = pulls.data[0]
|
|
||||||
const version = title.split('Cut release ')[1]
|
|
||||||
|
|
||||||
const result = await github.rest.repos.createRelease({
|
|
||||||
owner,
|
|
||||||
repo,
|
|
||||||
body,
|
|
||||||
tag_name: version,
|
|
||||||
name: version,
|
|
||||||
draft: true,
|
|
||||||
})
|
|
||||||
console.log(result)
|
|
104
.github/workflows/playwright.yml
vendored
@ -12,31 +12,33 @@ concurrency:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
actions: read
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
check-wasm-lib-changes:
|
||||||
check-rust-changes:
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
rust-changed: ${{ steps.filter.outputs.rust }}
|
url: ${{ steps.set-output.outputs.url }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- id: filter
|
|
||||||
name: Check for Rust changes
|
|
||||||
uses: dorny/paths-filter@v3
|
|
||||||
with:
|
with:
|
||||||
filters: |
|
fetch-depth: 0 # Fetches all history for all branches and tags
|
||||||
rust:
|
|
||||||
- 'src/wasm-lib/**'
|
- name: Check for changes in src/wasm-lib
|
||||||
|
id: set-output
|
||||||
|
run: |
|
||||||
|
if git diff --quiet origin/main...HEAD -- src/wasm-lib; then
|
||||||
|
echo "url=https://app.zoo.dev" >> $GITHUB_OUTPUT
|
||||||
|
echo "No changes detected in src/wasm-lib"
|
||||||
|
else
|
||||||
|
echo "Changes detected in src/wasm-lib"
|
||||||
|
echo "url=" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
playwright-ubuntu:
|
playwright-ubuntu:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: ubuntu-latest-8-cores
|
runs-on: ubuntu-latest-8-cores
|
||||||
needs: check-rust-changes
|
needs: check-wasm-lib-changes
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@ -48,38 +50,19 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Download Wasm Cache
|
- name: Print WASM Lib Changes URL
|
||||||
id: download-wasm
|
run: |
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
echo "WASM Lib Changes URL: ${{ needs.check-wasm-lib-changes.outputs.url }}"
|
||||||
uses: dawidd6/action-download-artifact@v3
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
name: wasm-bundle
|
|
||||||
workflow: build-and-store-wasm.yml
|
|
||||||
branch: main
|
|
||||||
path: src/wasm-lib/pkg
|
|
||||||
- name: copy wasm blob
|
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
|
||||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
|
if: ${{ needs.check-wasm-lib-changes.outputs.url }} == ''
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache Wasm (because rust diff)
|
- name: Cache wasm
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
if: ${{ needs.check-wasm-lib-changes.outputs.url }} == ''
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
- name: OR Cache Wasm (because wasm cache failed)
|
- name: build wasm
|
||||||
if: steps.download-wasm.outcome == 'failure'
|
if: ${{ needs.check-wasm-lib-changes.outputs.url }} == ''
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
- name: Build Wasm (because rust diff)
|
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
|
||||||
run: yarn build:wasm
|
|
||||||
- name: OR Build Wasm (because wasm cache failed)
|
|
||||||
if: steps.download-wasm.outcome == 'failure'
|
|
||||||
run: yarn build:wasm
|
run: yarn build:wasm
|
||||||
- name: build web
|
- name: build web
|
||||||
run: yarn build:local
|
run: yarn build:local
|
||||||
@ -89,6 +72,7 @@ jobs:
|
|||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
snapshottoken: ${{ secrets.KITTYCAD_API_TOKEN }}
|
||||||
|
WASM_OVERRIDE: ${{ steps.check-wasm-lib-changes.outputs.url }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
@ -124,6 +108,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
WASM_OVERRIDE: ${{ steps.check-wasm-lib-changes.outputs.url }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
@ -134,7 +119,7 @@ jobs:
|
|||||||
playwright-macos:
|
playwright-macos:
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
needs: check-rust-changes
|
needs: check-wasm-lib-changes
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
@ -145,38 +130,16 @@ jobs:
|
|||||||
run: yarn
|
run: yarn
|
||||||
- name: Install Playwright Browsers
|
- name: Install Playwright Browsers
|
||||||
run: yarn playwright install --with-deps
|
run: yarn playwright install --with-deps
|
||||||
- name: Download Wasm Cache
|
|
||||||
id: download-wasm
|
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
|
||||||
uses: dawidd6/action-download-artifact@v3
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
github_token: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
name: wasm-bundle
|
|
||||||
workflow: build-and-store-wasm.yml
|
|
||||||
branch: main
|
|
||||||
path: src/wasm-lib/pkg
|
|
||||||
- name: copy wasm blob
|
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'false'
|
|
||||||
run: cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
|
||||||
continue-on-error: true
|
|
||||||
- name: Setup Rust
|
- name: Setup Rust
|
||||||
|
if: needs.check-wasm-lib-changes.outputs.url == ''
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Cache Wasm (because rust diff)
|
- name: Cache wasm
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
if: needs.check-wasm-lib-changes.outputs.url == ''
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src/wasm-lib'
|
workspaces: './src/wasm-lib'
|
||||||
- name: OR Cache Wasm (because wasm cache failed)
|
- name: build wasm
|
||||||
if: steps.download-wasm.outcome == 'failure'
|
if: needs.check-wasm-lib-changes.outputs.url == ''
|
||||||
uses: Swatinem/rust-cache@v2
|
|
||||||
with:
|
|
||||||
workspaces: './src/wasm-lib'
|
|
||||||
- name: Build Wasm (because rust diff)
|
|
||||||
if: needs.check-rust-changes.outputs.rust-changed == 'true'
|
|
||||||
run: yarn build:wasm
|
|
||||||
- name: OR Build Wasm (because wasm cache failed)
|
|
||||||
if: steps.download-wasm.outcome == 'failure'
|
|
||||||
run: yarn build:wasm
|
run: yarn build:wasm
|
||||||
- name: build web
|
- name: build web
|
||||||
run: yarn build:local
|
run: yarn build:local
|
||||||
@ -187,6 +150,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
|
||||||
|
WASM_OVERRIDE: ${{ steps.check-wasm-lib-changes.outputs.url }}
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
|
1
.gitignore
vendored
@ -54,4 +54,3 @@ src/**/*.typegen.ts
|
|||||||
src-tauri/gen
|
src-tauri/gen
|
||||||
|
|
||||||
src/wasm-lib/grackle/stdlib_cube_partial.json
|
src/wasm-lib/grackle/stdlib_cube_partial.json
|
||||||
Mac_App_Distribution.provisionprofile
|
|
||||||
|
@ -59,10 +59,6 @@ followed by:
|
|||||||
```
|
```
|
||||||
yarn build:wasm-dev
|
yarn build:wasm-dev
|
||||||
```
|
```
|
||||||
or if you have the gh cli installed
|
|
||||||
```
|
|
||||||
./get-latest-wasm-bundle.sh # this will download the latest main wasm bundle
|
|
||||||
```
|
|
||||||
|
|
||||||
That will build the WASM binary and put in the `public` dir (though gitignored)
|
That will build the WASM binary and put in the `public` dir (though gitignored)
|
||||||
|
|
||||||
|
@ -1068,7 +1068,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthX('seg01', 10, %),\n 5\n ], %)\n |> close(%)\n |> extrude(5, %)"
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthX('seg01', 10, %),\n 5\n ], %)\n |> close(%)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -2074,7 +2074,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthY('seg01', 10, %),\n 5\n ], %)\n |> close(%)\n |> extrude(5, %)"
|
"const part001 = startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([1, 3.82], %, 'seg01')\n |> angledLineToX([\n -angleToMatchLengthY('seg01', 10, %),\n 5\n ], %)\n |> close(%)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -22380,8 +22380,8 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)",
|
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)",
|
||||||
"startSketchOn('YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%, \"edge1\")\n |> extrude(10, %)"
|
"startSketchOn('YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%, \"edge1\")"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -43695,7 +43695,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)\n |> extrude(10, %)"
|
"fn rectShape = (pos, w, l) => {\n const rr = startSketchOn('YZ')\n |> startProfileAt([pos[0] - (w / 2), pos[1] - (l / 2)], %)\n |> lineTo([pos[0] + w / 2, pos[1] - (l / 2)], %, \"edge1\")\n |> lineTo([pos[0] + w / 2, pos[1] + l / 2], %, \"edge2\")\n |> lineTo([pos[0] - (w / 2), pos[1] + l / 2], %, \"edge3\")\n |> close(%, \"edge4\")\n return rr\n}\n\n// Create the mounting plate extrusion, holes, and fillets\nconst part = rectShape([0, 0], 20, 20)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -45900,7 +45900,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternCircular2d({\n center: [20, 20],\n repetitions: 12,\n arcDegrees: 210,\n rotateDuplicates: true\n }, %)\n |> extrude(1, %)"
|
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternCircular2d({\n center: [20, 20],\n repetitions: 12,\n arcDegrees: 210,\n rotateDuplicates: true\n }, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -50459,7 +50459,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternLinear2d({\n axis: [0, 1],\n repetitions: 12,\n distance: 2\n }, %)\n |> extrude(1, %)"
|
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternLinear2d({\n axis: [0, 1],\n repetitions: 12,\n distance: 2\n }, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -59318,7 +59318,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
|
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -60312,7 +60312,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchAt([0, 0])\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)"
|
"startSketchAt([0, 0])\n |> line([10, 10], %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -61585,7 +61585,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)",
|
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")",
|
||||||
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
"fn cube = (pos, scale) => {\n const sg = startSketchOn('XY')\n |> startProfileAt(pos, %)\n |> line([0, scale], %)\n |> line([scale, 0], %)\n |> line([0, -scale], %)\n |> close(%)\n |> extrude(scale, %)\n\n return sg\n}\n\nconst box = cube([0, 0], 20)\n\nconst part001 = startSketchOn(box, \"start\")\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%)\n |> extrude(20, %)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@ -65584,7 +65584,7 @@
|
|||||||
"unpublished": false,
|
"unpublished": false,
|
||||||
"deprecated": false,
|
"deprecated": false,
|
||||||
"examples": [
|
"examples": [
|
||||||
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
|
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -596,12 +596,13 @@ test('Auto complete works', async ({ page }) => {
|
|||||||
|
|
||||||
test('Stored settings are validated and fall back to defaults', async ({
|
test('Stored settings are validated and fall back to defaults', async ({
|
||||||
page,
|
page,
|
||||||
|
context,
|
||||||
}) => {
|
}) => {
|
||||||
const u = getUtils(page)
|
const u = getUtils(page)
|
||||||
|
|
||||||
// Override beforeEach test setup
|
// Override beforeEach test setup
|
||||||
// with corrupted settings
|
// with corrupted settings
|
||||||
await page.addInitScript(
|
await context.addInitScript(
|
||||||
async ({ settingsKey, settings }) => {
|
async ({ settingsKey, settings }) => {
|
||||||
localStorage.setItem(settingsKey, settings)
|
localStorage.setItem(settingsKey, settings)
|
||||||
},
|
},
|
||||||
@ -618,18 +619,18 @@ test('Stored settings are validated and fall back to defaults', async ({
|
|||||||
// Check the settings were reset
|
// Check the settings were reset
|
||||||
const storedSettings = TOML.parse(
|
const storedSettings = TOML.parse(
|
||||||
await page.evaluate(
|
await page.evaluate(
|
||||||
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
|
||||||
{ settingsKey: TEST_SETTINGS_KEY }
|
{ settingsKey: TEST_SETTINGS_KEY }
|
||||||
)
|
)
|
||||||
) as { settings: SaveSettingsPayload }
|
) as { settings: SaveSettingsPayload }
|
||||||
|
|
||||||
expect(storedSettings.settings?.app?.theme).toBe(undefined)
|
expect(storedSettings.settings.app?.theme).toBe('dark')
|
||||||
|
|
||||||
// Check that the invalid settings were removed
|
// Check that the invalid settings were removed
|
||||||
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
|
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
|
||||||
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
|
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
|
||||||
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
|
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
|
||||||
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
|
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Project settings can be set and override user settings', async ({
|
test('Project settings can be set and override user settings', async ({
|
||||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 39 KiB |
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 71 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 51 KiB |
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
@ -1,7 +1,7 @@
|
|||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
import { Themes } from 'lib/theme'
|
import { Themes } from 'lib/theme'
|
||||||
|
|
||||||
export const TEST_SETTINGS_KEY = '/settings.toml'
|
export const TEST_SETTINGS_KEY = '/user.toml'
|
||||||
export const TEST_SETTINGS = {
|
export const TEST_SETTINGS = {
|
||||||
app: {
|
app: {
|
||||||
theme: Themes.Dark,
|
theme: Themes.Dark,
|
||||||
@ -24,7 +24,7 @@ export const TEST_SETTINGS = {
|
|||||||
|
|
||||||
export const TEST_SETTINGS_ONBOARDING = {
|
export const TEST_SETTINGS_ONBOARDING = {
|
||||||
...TEST_SETTINGS,
|
...TEST_SETTINGS,
|
||||||
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
|
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
|
||||||
} satisfies Partial<SaveSettingsPayload>
|
} satisfies Partial<SaveSettingsPayload>
|
||||||
|
|
||||||
export const TEST_SETTINGS_CORRUPTED = {
|
export const TEST_SETTINGS_CORRUPTED = {
|
||||||
|
@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
|
|||||||
|
|
||||||
// Now should be signed in
|
// Now should be signed in
|
||||||
const newFileButton = await $('[data-testid="home-new-file"]')
|
const newFileButton = await $('[data-testid="home-new-file"]')
|
||||||
expect(await newFileButton.getText()).toEqual('New project')
|
expect(await newFileButton.getText()).toEqual('New file')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Set the repository owner and name
|
|
||||||
REPO_OWNER="KittyCAD"
|
|
||||||
REPO_NAME="modeling-app"
|
|
||||||
WORKFLOW_NAME="build-and-store-wasm.yml"
|
|
||||||
ARTIFACT_NAME="wasm-bundle"
|
|
||||||
|
|
||||||
# Fetch the latest completed workflow run ID for the specified workflow
|
|
||||||
# RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed") | .id' | head -n 1)
|
|
||||||
RUN_ID=$(gh api repos/$REPO_OWNER/$REPO_NAME/actions/workflows/$WORKFLOW_NAME/runs --paginate --jq '.workflow_runs[] | select(.status=="completed" and .conclusion=="success") | .id' | head -n 1)
|
|
||||||
|
|
||||||
echo $RUN_ID
|
|
||||||
|
|
||||||
# Check if a valid RUN_ID was found
|
|
||||||
if [ -z "$RUN_ID" ]; then
|
|
||||||
echo "Failed to find a workflow run for $WORKFLOW_NAME."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
gh run download $RUN_ID --repo $REPO_OWNER/$REPO_NAME --name $ARTIFACT_NAME --dir ./src/wasm-lib/pkg
|
|
||||||
|
|
||||||
cp src/wasm-lib/pkg/wasm_lib_bg.wasm public
|
|
||||||
echo "latest wasm copied to public folder"
|
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "untitled-app",
|
"name": "untitled-app",
|
||||||
"version": "0.19.4",
|
"version": "0.18.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/autocomplete": "^6.16.0",
|
"@codemirror/autocomplete": "^6.16.0",
|
||||||
@ -86,7 +86,6 @@
|
|||||||
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
|
||||||
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
"fmt": "prettier --write ./src *.ts *.json *.js ./e2e",
|
||||||
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
"fmt-check": "prettier --check ./src *.ts *.json *.js ./e2e",
|
||||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
|
||||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
"build:wasm": "(cd src/wasm-lib && wasm-pack build --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && cp src/wasm-lib/pkg/wasm_lib_bg.wasm public && yarn fmt",
|
||||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||||
|
@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 1 : 1,
|
workers: process.env.CI ? 2 : 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
@ -72,7 +72,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
/* Run your local dev server before starting the tests */
|
/* Run your local dev server before starting the tests */
|
||||||
webServer: {
|
webServer: {
|
||||||
command: 'yarn serve',
|
command: 'VITE_KC_WASM_OVERRIDE_URL=$WASM_OVERRIDE yarn serve',
|
||||||
// url: 'http://127.0.0.1:3000',
|
// url: 'http://127.0.0.1:3000',
|
||||||
reuseExistingServer: !process.env.CI,
|
reuseExistingServer: !process.env.CI,
|
||||||
},
|
},
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"applinks": {
|
|
||||||
"details": [
|
|
||||||
{
|
|
||||||
"appIDs": ["92H8YB3B95.dev.zoo.modeling-app"],
|
|
||||||
"components": [
|
|
||||||
{
|
|
||||||
"/": "/file/*",
|
|
||||||
"comment": "Matches any URL whose path starts with /file/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
2418
src-tauri/Cargo.lock
generated
@ -1,13 +1,14 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "The Zoo Modeling App"
|
description = "A Tauri App"
|
||||||
authors = ["Zoo Engineers <eng@zoo.dev>"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
default-run = "app"
|
default-run = "app"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.70"
|
rust-version = "1.70"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
@ -15,13 +16,11 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
|
|
||||||
kittycad = "0.3.0"
|
kittycad = "0.3.0"
|
||||||
oauth2 = "4.4.2"
|
oauth2 = "4.4.2"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
|
||||||
tauri-plugin-cli = { version = "2.0.0-beta.3" }
|
|
||||||
tauri-plugin-deep-link = { version = "2.0.0-beta.3" }
|
|
||||||
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
tauri-plugin-fs = { version = "2.0.0-beta.6" }
|
||||||
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
tauri-plugin-http = { version = "2.0.0-beta.6" }
|
||||||
@ -29,13 +28,11 @@ tauri-plugin-os = { version = "2.0.0-beta.2" }
|
|||||||
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
tauri-plugin-process = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
tauri-plugin-shell = { version = "2.0.0-beta.2" }
|
||||||
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
tauri-plugin-updater = { version = "2.0.0-beta.4" }
|
||||||
tokio = { version = "1.37.0", features = ["time", "fs", "process"] }
|
tokio = { version = "1.37.0", features = ["time"] }
|
||||||
toml = "0.8.2"
|
toml = "0.8.2"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["updater"]
|
|
||||||
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
# this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled.
|
||||||
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
# If you use cargo directly instead of tauri's cli you can use this feature flag to switch between tauri's `dev` and `build` modes.
|
||||||
# DO NOT REMOVE!!
|
# DO NOT REMOVE!!
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
custom-protocol = ["tauri/custom-protocol"]
|
||||||
updater = []
|
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>dev.zoo.modeling-app</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>zoo-modeling-app</string>
|
|
||||||
<string>zoo</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>UTExportedTypeDeclarations</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>UTTypeIdentifier</key>
|
|
||||||
<string>dev.zoo.kcl</string>
|
|
||||||
<key>UTTypeConformsTo</key>
|
|
||||||
<array>
|
|
||||||
<string>public.source-code</string>
|
|
||||||
</array>
|
|
||||||
<key>UTTypeDescription</key>
|
|
||||||
<string>KCL (KittyCAD Language) document</string>
|
|
||||||
<key>UTTypeTagSpecification</key>
|
|
||||||
<dict>
|
|
||||||
<key>public.filename-extension</key>
|
|
||||||
<array>
|
|
||||||
<string>kcl</string>
|
|
||||||
</array>
|
|
||||||
<key>public.mime-type</key>
|
|
||||||
<array>
|
|
||||||
<string>text/vnd.zoo.kcl</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -7,8 +7,6 @@
|
|||||||
"main"
|
"main"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
"cli:default",
|
|
||||||
"deep-link:default",
|
|
||||||
"path:default",
|
"path:default",
|
||||||
"event:default",
|
"event:default",
|
||||||
"window:default",
|
"window:default",
|
||||||
@ -25,6 +23,7 @@
|
|||||||
"fs:allow-copy-file",
|
"fs:allow-copy-file",
|
||||||
"fs:allow-mkdir",
|
"fs:allow-mkdir",
|
||||||
"fs:allow-remove",
|
"fs:allow-remove",
|
||||||
|
"fs:allow-remove",
|
||||||
"fs:allow-rename",
|
"fs:allow-rename",
|
||||||
"fs:allow-exists",
|
"fs:allow-exists",
|
||||||
"fs:allow-stat",
|
"fs:allow-stat",
|
||||||
|
@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.app-sandbox</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.client</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.network.server</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.security.files.downloads.read-write</key>
|
|
||||||
<true/>
|
|
||||||
<key>com.apple.application-identifier</key>
|
|
||||||
<string>92H8YB3B95.dev.zoo.modeling-app</string>
|
|
||||||
<key>com.apple.developer.team-identifier</key>
|
|
||||||
<string>92H8YB3B95</string>
|
|
||||||
<key>com.apple.developer.associated-domains</key>
|
|
||||||
<array>
|
|
||||||
<string>applinks:app.zoo.dev</string>
|
|
||||||
</array>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
@ -1,6 +0,0 @@
|
|||||||
max_width = 120
|
|
||||||
edition = "2018"
|
|
||||||
format_code_in_doc_comments = true
|
|
||||||
format_strings = false
|
|
||||||
imports_granularity = "Crate"
|
|
||||||
group_imports = "StdExternalCrate"
|
|
@ -1,225 +1,91 @@
|
|||||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
pub(crate) mod state;
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
use std::{
|
use std::io::Read;
|
||||||
env,
|
use std::path::Path;
|
||||||
path::{Path, PathBuf},
|
use std::path::PathBuf;
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use kcl_lib::settings::types::{
|
|
||||||
file::{FileEntry, Project, ProjectRoute, ProjectState},
|
|
||||||
project::ProjectConfiguration,
|
|
||||||
Configuration, DEFAULT_PROJECT_KCL_FILE,
|
|
||||||
};
|
|
||||||
use oauth2::TokenResponse;
|
use oauth2::TokenResponse;
|
||||||
use tauri::{ipc::InvokeError, Manager};
|
use serde::Serialize;
|
||||||
use tauri_plugin_cli::CliExt;
|
use std::process::Command;
|
||||||
|
use tauri::ipc::InvokeError;
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
use tokio::process::Command;
|
const DEFAULT_HOST: &str = "https://api.kittycad.io";
|
||||||
|
|
||||||
const DEFAULT_HOST: &str = "https://api.zoo.dev";
|
|
||||||
const SETTINGS_FILE_NAME: &str = "settings.toml";
|
|
||||||
const PROJECT_SETTINGS_FILE_NAME: &str = "project.toml";
|
|
||||||
const PROJECT_FOLDER: &str = "zoo-modeling-app-projects";
|
|
||||||
|
|
||||||
|
/// This command returns the a json string parse from a toml file at the path.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
fn read_toml(path: &str) -> Result<String, InvokeError> {
|
||||||
let dir = match app.path().document_dir() {
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
Ok(dir) => dir,
|
let mut contents = String::new();
|
||||||
Err(_) => {
|
file.read_to_string(&mut contents)
|
||||||
// for headless Linux (eg. Github Actions)
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
let home_dir = app.path().home_dir()?;
|
let value =
|
||||||
home_dir.join("Documents")
|
toml::from_str::<toml::Value>(&contents).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
}
|
let value = serde_json::to_string(&value).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
};
|
Ok(value)
|
||||||
|
|
||||||
Ok(dir.join(PROJECT_FOLDER))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||||
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
|
/// Removed from tauri v2
|
||||||
let store = app.state::<state::Store>();
|
#[derive(Debug, Serialize)]
|
||||||
Ok(store.get().await)
|
pub struct DiskEntry {
|
||||||
|
/// The path to the entry.
|
||||||
|
pub path: PathBuf,
|
||||||
|
/// The name of the entry (file name with extension or directory name).
|
||||||
|
pub name: Option<String>,
|
||||||
|
/// The children of this entry if it's a directory.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub children: Option<Vec<DiskEntry>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||||
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
|
/// Removed from tauri v2
|
||||||
let store = app.state::<state::Store>();
|
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
|
||||||
store.set(state).await;
|
std::fs::metadata(path)
|
||||||
Ok(())
|
.map(|md| md.is_dir())
|
||||||
}
|
.map_err(Into::into)
|
||||||
|
|
||||||
async fn get_app_settings_file_path(app: &tauri::AppHandle) -> Result<PathBuf, InvokeError> {
|
|
||||||
let app_config_dir = app.path().app_config_dir()?;
|
|
||||||
|
|
||||||
// Ensure this directory exists.
|
|
||||||
if !app_config_dir.exists() {
|
|
||||||
tokio::fs::create_dir_all(&app_config_dir)
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(app_config_dir.join(SETTINGS_FILE_NAME))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
|
||||||
|
/// Removed from tauri v2
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
|
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
|
||||||
let mut settings_path = get_app_settings_file_path(&app).await?;
|
let mut files_and_dirs: Vec<DiskEntry> = vec![];
|
||||||
let mut needs_migration = false;
|
// let path = path.as_ref();
|
||||||
|
for entry in fs::read_dir(path).map_err(|e| InvokeError::from_anyhow(e.into()))? {
|
||||||
|
let path = entry
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?
|
||||||
|
.path();
|
||||||
|
|
||||||
// Check if this file exists.
|
if let Ok(flag) = is_dir(&path) {
|
||||||
if !settings_path.exists() {
|
files_and_dirs.push(DiskEntry {
|
||||||
// Try the backwards compatible path.
|
path: path.clone(),
|
||||||
// TODO: Remove this after a few releases.
|
children: if flag {
|
||||||
let app_config_dir = app.path().app_config_dir()?;
|
Some(read_dir_recursive(path.to_str().expect("No path"))?)
|
||||||
settings_path = format!(
|
} else {
|
||||||
"{}user.toml",
|
None
|
||||||
app_config_dir.display().to_string().trim_end_matches('/')
|
},
|
||||||
)
|
name: path
|
||||||
.into();
|
.file_name()
|
||||||
needs_migration = true;
|
.map(|name| name.to_string_lossy())
|
||||||
// Check if this path exists.
|
.map(|name| name.to_string()),
|
||||||
if !settings_path.exists() {
|
});
|
||||||
let mut default = Configuration::default();
|
|
||||||
default.settings.project.directory = get_initial_default_dir(app.clone())?;
|
|
||||||
// Return the default configuration.
|
|
||||||
return Ok(default);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(files_and_dirs)
|
||||||
|
}
|
||||||
|
|
||||||
let contents = tokio::fs::read_to_string(&settings_path)
|
/// This command returns a string that is the contents of a file at the path.
|
||||||
.await
|
#[tauri::command]
|
||||||
|
fn read_txt_file(path: &str) -> Result<String, InvokeError> {
|
||||||
|
let mut file = std::fs::File::open(path).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
|
let mut contents = String::new();
|
||||||
|
file.read_to_string(&mut contents)
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
||||||
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
Ok(contents)
|
||||||
if parsed.settings.project.directory == PathBuf::new() {
|
|
||||||
parsed.settings.project.directory = get_initial_default_dir(app.clone())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Remove this after a few releases.
|
|
||||||
if needs_migration {
|
|
||||||
write_app_settings_file(app, parsed.clone()).await?;
|
|
||||||
// Delete the old file.
|
|
||||||
tokio::fs::remove_file(settings_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn write_app_settings_file(app: tauri::AppHandle, configuration: Configuration) -> Result<(), InvokeError> {
|
|
||||||
let settings_path = get_app_settings_file_path(&app).await?;
|
|
||||||
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
tokio::fs::write(settings_path, contents.as_bytes())
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_project_settings_file_path(
|
|
||||||
app_settings: Configuration,
|
|
||||||
project_name: &str,
|
|
||||||
) -> Result<PathBuf, InvokeError> {
|
|
||||||
let project_dir = app_settings.settings.project.directory.join(project_name);
|
|
||||||
|
|
||||||
if !project_dir.exists() {
|
|
||||||
tokio::fs::create_dir_all(&project_dir)
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(project_dir.join(PROJECT_SETTINGS_FILE_NAME))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn read_project_settings_file(
|
|
||||||
app_settings: Configuration,
|
|
||||||
project_name: &str,
|
|
||||||
) -> Result<ProjectConfiguration, InvokeError> {
|
|
||||||
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
|
|
||||||
|
|
||||||
// Check if this file exists.
|
|
||||||
if !settings_path.exists() {
|
|
||||||
// Return the default configuration.
|
|
||||||
return Ok(ProjectConfiguration::default());
|
|
||||||
}
|
|
||||||
|
|
||||||
let contents = tokio::fs::read_to_string(&settings_path)
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
let parsed = ProjectConfiguration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
|
|
||||||
|
|
||||||
Ok(parsed)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn write_project_settings_file(
|
|
||||||
app_settings: Configuration,
|
|
||||||
project_name: &str,
|
|
||||||
configuration: ProjectConfiguration,
|
|
||||||
) -> Result<(), InvokeError> {
|
|
||||||
let settings_path = get_project_settings_file_path(app_settings, project_name).await?;
|
|
||||||
let contents = toml::to_string_pretty(&configuration).map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
tokio::fs::write(settings_path, contents.as_bytes())
|
|
||||||
.await
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize the directory that holds all the projects.
|
|
||||||
#[tauri::command]
|
|
||||||
async fn initialize_project_directory(configuration: Configuration) -> Result<PathBuf, InvokeError> {
|
|
||||||
configuration
|
|
||||||
.ensure_project_directory_exists()
|
|
||||||
.await
|
|
||||||
.map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new project directory.
|
|
||||||
#[tauri::command]
|
|
||||||
async fn create_new_project_directory(
|
|
||||||
configuration: Configuration,
|
|
||||||
project_name: &str,
|
|
||||||
initial_code: Option<&str>,
|
|
||||||
) -> Result<Project, InvokeError> {
|
|
||||||
configuration
|
|
||||||
.create_new_project_directory(project_name, initial_code)
|
|
||||||
.await
|
|
||||||
.map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List all the projects in the project directory.
|
|
||||||
#[tauri::command]
|
|
||||||
async fn list_projects(configuration: Configuration) -> Result<Vec<Project>, InvokeError> {
|
|
||||||
configuration.list_projects().await.map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get information about a project.
|
|
||||||
#[tauri::command]
|
|
||||||
async fn get_project_info(configuration: Configuration, project_path: &str) -> Result<Project, InvokeError> {
|
|
||||||
configuration
|
|
||||||
.get_project_info(project_path)
|
|
||||||
.await
|
|
||||||
.map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse the project route.
|
|
||||||
#[tauri::command]
|
|
||||||
async fn parse_project_route(configuration: Configuration, route: &str) -> Result<ProjectRoute, InvokeError> {
|
|
||||||
ProjectRoute::from_route(&configuration, route).map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
async fn read_dir_recursive(path: &str) -> Result<FileEntry, InvokeError> {
|
|
||||||
kcl_lib::settings::utils::walk_dir(&Path::new(path).to_path_buf())
|
|
||||||
.await
|
|
||||||
.map_err(InvokeError::from_anyhow)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This command instantiates a new window with auth.
|
/// This command instantiates a new window with auth.
|
||||||
@ -237,7 +103,8 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
let auth_client = oauth2::basic::BasicClient::new(
|
let auth_client = oauth2::basic::BasicClient::new(
|
||||||
oauth2::ClientId::new(client_id),
|
oauth2::ClientId::new(client_id),
|
||||||
None,
|
None,
|
||||||
oauth2::AuthUrl::new(format!("{host}/authorize")).map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
oauth2::AuthUrl::new(format!("{host}/authorize"))
|
||||||
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
Some(
|
Some(
|
||||||
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
|
||||||
@ -265,10 +132,12 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
// and bypass the shell::open call as it fails on GitHub Actions.
|
// and bypass the shell::open call as it fails on GitHub Actions.
|
||||||
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
|
||||||
if e2e_tauri_enabled {
|
if e2e_tauri_enabled {
|
||||||
println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
|
println!(
|
||||||
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
"E2E_TAURI_ENABLED is set, won't open {} externally",
|
||||||
.await
|
auth_uri.secret()
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
);
|
||||||
|
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
|
||||||
|
.expect("Unable to write /tmp/kittycad_user_code file");
|
||||||
} else {
|
} else {
|
||||||
app.shell()
|
app.shell()
|
||||||
.open(auth_uri.secret(), None)
|
.open(auth_uri.secret(), None)
|
||||||
@ -291,7 +160,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
|
|||||||
///This command returns the KittyCAD user info given a token.
|
///This command returns the KittyCAD user info given a token.
|
||||||
/// The string returned from this method is the user info as a json string.
|
/// The string returned from this method is the user info as a json string.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
|
async fn get_user(
|
||||||
|
token: Option<String>,
|
||||||
|
hostname: &str,
|
||||||
|
) -> Result<kittycad::types::User, InvokeError> {
|
||||||
// Use the host passed in if it's set.
|
// Use the host passed in if it's set.
|
||||||
// Otherwise, use the default host.
|
// Otherwise, use the default host.
|
||||||
let host = if hostname.is_empty() {
|
let host = if hostname.is_empty() {
|
||||||
@ -311,7 +183,7 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User,
|
|||||||
println!("Getting user info...");
|
println!("Getting user info...");
|
||||||
|
|
||||||
// use kittycad library to fetch the user info from /user/me
|
// use kittycad library to fetch the user info from /user/me
|
||||||
let mut client = kittycad::Client::new(token);
|
let mut client = kittycad::Client::new(token.unwrap());
|
||||||
|
|
||||||
if baseurl != DEFAULT_HOST {
|
if baseurl != DEFAULT_HOST {
|
||||||
client.set_base_url(&baseurl);
|
client.set_base_url(&baseurl);
|
||||||
@ -330,195 +202,50 @@ async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User,
|
|||||||
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
/// From this GitHub comment: https://github.com/tauri-apps/tauri/issues/4062#issuecomment-1338048169
|
||||||
/// But with the Linux support removed since we don't need it for now.
|
/// But with the Linux support removed since we don't need it for now.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
|
fn show_in_folder(path: String) {
|
||||||
#[cfg(not(unix))]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
Command::new("explorer")
|
Command::new("explorer")
|
||||||
.args(["/select,", &path]) // The comma after select is not a typo
|
.args(["/select,", &path]) // The comma after select is not a typo
|
||||||
.spawn()
|
.spawn()
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
Command::new("open")
|
Command::new("open").args(["-R", &path]).spawn().unwrap();
|
||||||
.args(["-R", &path])
|
|
||||||
.spawn()
|
|
||||||
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
|
.setup(|_app| {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
use tauri::Manager;
|
||||||
|
_app.get_webview("main").unwrap().open_devtools();
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
_app.handle()
|
||||||
|
.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_state,
|
|
||||||
set_state,
|
|
||||||
get_initial_default_dir,
|
|
||||||
initialize_project_directory,
|
|
||||||
create_new_project_directory,
|
|
||||||
list_projects,
|
|
||||||
get_project_info,
|
|
||||||
parse_project_route,
|
|
||||||
get_user,
|
get_user,
|
||||||
login,
|
login,
|
||||||
|
read_toml,
|
||||||
|
read_txt_file,
|
||||||
read_dir_recursive,
|
read_dir_recursive,
|
||||||
show_in_folder,
|
show_in_folder,
|
||||||
read_app_settings_file,
|
|
||||||
write_app_settings_file,
|
|
||||||
read_project_settings_file,
|
|
||||||
write_project_settings_file,
|
|
||||||
])
|
])
|
||||||
.plugin(tauri_plugin_cli::init())
|
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.setup(|app| {
|
.run(tauri::generate_context!())
|
||||||
// Do update things.
|
.expect("error while running tauri application");
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
{
|
|
||||||
app.get_webview("main").unwrap().open_devtools();
|
|
||||||
}
|
|
||||||
#[cfg(not(debug_assertions))]
|
|
||||||
#[cfg(feature = "updater")]
|
|
||||||
{
|
|
||||||
app.handle().plugin(tauri_plugin_updater::Builder::new().build())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut verbose = false;
|
|
||||||
let mut source_path: Option<PathBuf> = None;
|
|
||||||
match app.cli().matches() {
|
|
||||||
// `matches` here is a Struct with { args, subcommand }.
|
|
||||||
// `args` is `HashMap<String, ArgData>` where `ArgData` is a struct with { value, occurrences }.
|
|
||||||
// `subcommand` is `Option<Box<SubcommandMatches>>` where `SubcommandMatches` is a struct with { name, matches }.
|
|
||||||
Ok(matches) => {
|
|
||||||
if let Some(verbose_flag) = matches.args.get("verbose") {
|
|
||||||
let Some(value) = verbose_flag.value.as_bool() else {
|
|
||||||
return Err(
|
|
||||||
anyhow::anyhow!("Error parsing CLI arguments: verbose flag is not a boolean").into(),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
verbose = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the path we are trying to open.
|
|
||||||
if let Some(source_arg) = matches.args.get("source") {
|
|
||||||
// We don't do an else here because this can be null.
|
|
||||||
if let Some(value) = source_arg.value.as_str() {
|
|
||||||
source_path = Some(Path::new(value).to_path_buf());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
return Err(anyhow::anyhow!("Error parsing CLI arguments: {:?}", err).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have a source path to open, make sure it exists.
|
|
||||||
let Some(source_path) = source_path else {
|
|
||||||
// The user didn't provide a source path to open.
|
|
||||||
// Run the app as normal.
|
|
||||||
app.manage(state::Store::default());
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
|
|
||||||
if !source_path.exists() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Error: the path `{}` you are trying to open does not exist",
|
|
||||||
source_path.display()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let runner: tauri::async_runtime::JoinHandle<Result<ProjectState>> =
|
|
||||||
tauri::async_runtime::spawn(async move {
|
|
||||||
// Fix for "." path, which is the current directory.
|
|
||||||
let source_path = if source_path == Path::new(".") {
|
|
||||||
std::env::current_dir()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
|
|
||||||
} else {
|
|
||||||
source_path
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the path does not start with a slash, it is a relative path.
|
|
||||||
// We need to convert it to an absolute path.
|
|
||||||
let source_path = if source_path.is_relative() {
|
|
||||||
std::env::current_dir()
|
|
||||||
.map_err(|e| anyhow::anyhow!("Error getting the current directory: {:?}", e))?
|
|
||||||
.join(source_path)
|
|
||||||
} else {
|
|
||||||
source_path
|
|
||||||
};
|
|
||||||
|
|
||||||
// If the path is a directory, let's assume it is a project directory.
|
|
||||||
if source_path.is_dir() {
|
|
||||||
// Load the details about the project from the path.
|
|
||||||
let project = Project::from_path(&source_path).await.map_err(|e| {
|
|
||||||
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!("Project loaded from path: {}", source_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the default file in the project.
|
|
||||||
// Write the initial project file.
|
|
||||||
let project_file = source_path.join(DEFAULT_PROJECT_KCL_FILE);
|
|
||||||
tokio::fs::write(&project_file, vec![]).await?;
|
|
||||||
|
|
||||||
return Ok(ProjectState {
|
|
||||||
project,
|
|
||||||
current_file: Some(project_file.display().to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// We were given a file path, not a directory.
|
|
||||||
// Let's get the parent directory of the file.
|
|
||||||
let parent = source_path.parent().ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"Error getting the parent directory of the file: {}",
|
|
||||||
source_path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
// Load the details about the project from the parent directory.
|
|
||||||
let project = Project::from_path(&parent).await.map_err(|e| {
|
|
||||||
anyhow::anyhow!("Error loading project from path {}: {:?}", source_path.display(), e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if verbose {
|
|
||||||
println!(
|
|
||||||
"Project loaded from path: {}, current file: {}",
|
|
||||||
parent.display(),
|
|
||||||
source_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(ProjectState {
|
|
||||||
project,
|
|
||||||
current_file: Some(source_path.display().to_string()),
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
// Block on the handle.
|
|
||||||
let store = tauri::async_runtime::block_on(runner)??;
|
|
||||||
|
|
||||||
// Create a state object to hold the project.
|
|
||||||
app.manage(state::Store::new(store));
|
|
||||||
|
|
||||||
// Listen on the deep links.
|
|
||||||
app.listen("deep-link://new-url", |url| {
|
|
||||||
dbg!(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.run(tauri::generate_context!())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
@ -1,21 +0,0 @@
|
|||||||
//! State management for the application.
|
|
||||||
|
|
||||||
use kcl_lib::settings::types::file::ProjectState;
|
|
||||||
use tokio::sync::Mutex;
|
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct Store(Mutex<Option<ProjectState>>);
|
|
||||||
|
|
||||||
impl Store {
|
|
||||||
pub fn new(p: ProjectState) -> Self {
|
|
||||||
Self(Mutex::new(Some(p)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get(&self) -> Option<ProjectState> {
|
|
||||||
self.0.lock().await.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn set(&self, p: Option<ProjectState>) {
|
|
||||||
*self.0.lock().await = p;
|
|
||||||
}
|
|
||||||
}
|
|
@ -38,7 +38,11 @@
|
|||||||
},
|
},
|
||||||
"longDescription": "",
|
"longDescription": "",
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"entitlements": "entitlements/Zoo Modeling App.entitlements"
|
"entitlements": null,
|
||||||
|
"exceptionDomain": "",
|
||||||
|
"frameworks": [],
|
||||||
|
"providerShortName": null,
|
||||||
|
"signingIdentity": null
|
||||||
},
|
},
|
||||||
"resources": [],
|
"resources": [],
|
||||||
"shortDescription": "",
|
"shortDescription": "",
|
||||||
@ -46,31 +50,10 @@
|
|||||||
},
|
},
|
||||||
"identifier": "dev.zoo.modeling-app",
|
"identifier": "dev.zoo.modeling-app",
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"cli": {
|
|
||||||
"description": "Zoo Modeling App CLI",
|
|
||||||
"args": [
|
|
||||||
{
|
|
||||||
"short": "v",
|
|
||||||
"name": "verbose",
|
|
||||||
"description": "Verbosity level"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "source",
|
|
||||||
"index": 1,
|
|
||||||
"takesValue": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"subcommands": {}
|
|
||||||
},
|
|
||||||
"deep-link": {
|
|
||||||
"domains": [
|
|
||||||
{ "host": "app.zoo.dev" }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"productName": "Zoo Modeling App",
|
"productName": "Zoo Modeling App",
|
||||||
"version": "0.19.4"
|
"version": "0.18.1"
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,6 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
|
|||||||
import LspProvider from 'components/LspProvider'
|
import LspProvider from 'components/LspProvider'
|
||||||
import { KclContextProvider } from 'lang/KclProvider'
|
import { KclContextProvider } from 'lang/KclProvider'
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
||||||
import { getState, setState } from 'lib/tauri'
|
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@ -53,29 +52,10 @@ const router = createBrowserRouter([
|
|||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: paths.INDEX,
|
path: paths.INDEX,
|
||||||
loader: async () => {
|
loader: () =>
|
||||||
const inTauri = isTauri()
|
isTauri()
|
||||||
if (inTauri) {
|
|
||||||
const appState = await getState()
|
|
||||||
|
|
||||||
if (appState) {
|
|
||||||
// Reset the state.
|
|
||||||
// We do this so that we load the initial state from the cli but everything
|
|
||||||
// else we can ignore.
|
|
||||||
await setState(undefined)
|
|
||||||
// Redirect to the file if we have a file path.
|
|
||||||
if (appState.current_file) {
|
|
||||||
return redirect(
|
|
||||||
paths.FILE + '/' + encodeURIComponent(appState.current_file)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return inTauri
|
|
||||||
? redirect(paths.HOME)
|
? redirect(paths.HOME)
|
||||||
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
loader: fileLoader,
|
loader: fileLoader,
|
||||||
|
@ -15,10 +15,10 @@ import {
|
|||||||
import { useCommandsContext } from 'hooks/useCommandsContext'
|
import { useCommandsContext } from 'hooks/useCommandsContext'
|
||||||
import { fileMachine } from 'machines/fileMachine'
|
import { fileMachine } from 'machines/fileMachine'
|
||||||
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
|
||||||
|
import { readProject } from 'lib/tauriFS'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join, sep } from '@tauri-apps/api/path'
|
import { join, sep } from '@tauri-apps/api/path'
|
||||||
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
|
||||||
import { getProjectInfo } from 'lib/tauri'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
|
|||||||
services: {
|
services: {
|
||||||
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
|
||||||
const newFiles = isTauri()
|
const newFiles = isTauri()
|
||||||
? (await getProjectInfo(context.project.path)).children
|
? await readProject(context.project.path)
|
||||||
: []
|
: []
|
||||||
return {
|
return {
|
||||||
...context.project,
|
...context.project,
|
||||||
|
@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
|
|||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { Dispatch, useEffect, useRef, useState } from 'react'
|
import { Dispatch, useEffect, useRef, useState } from 'react'
|
||||||
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { Dialog, Disclosure } from '@headlessui/react'
|
import { Dialog, Disclosure } from '@headlessui/react'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -133,13 +133,18 @@ const FileTreeItem = ({
|
|||||||
project,
|
project,
|
||||||
currentFile,
|
currentFile,
|
||||||
fileOrDir,
|
fileOrDir,
|
||||||
onDoubleClick,
|
closePanel,
|
||||||
level = 0,
|
level = 0,
|
||||||
}: {
|
}: {
|
||||||
project?: IndexLoaderData['project']
|
project?: IndexLoaderData['project']
|
||||||
currentFile?: IndexLoaderData['file']
|
currentFile?: IndexLoaderData['file']
|
||||||
fileOrDir: FileEntry
|
fileOrDir: FileEntry
|
||||||
onDoubleClick?: () => void
|
closePanel: (
|
||||||
|
focusableElement?:
|
||||||
|
| HTMLElement
|
||||||
|
| React.MutableRefObject<HTMLElement | null>
|
||||||
|
| undefined
|
||||||
|
) => void
|
||||||
level?: number
|
level?: number
|
||||||
}) => {
|
}) => {
|
||||||
const { send, context } = useFileContext()
|
const { send, context } = useFileContext()
|
||||||
@ -181,7 +186,7 @@ const FileTreeItem = ({
|
|||||||
// Open kcl files
|
// Open kcl files
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
|
||||||
}
|
}
|
||||||
onDoubleClick?.()
|
closePanel()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -189,10 +194,8 @@ const FileTreeItem = ({
|
|||||||
{fileOrDir.children === undefined ? (
|
{fileOrDir.children === undefined ? (
|
||||||
<li
|
<li
|
||||||
className={
|
className={
|
||||||
'group m-0 p-0 border-solid border-0 hover:bg-primary/5 focus-within:bg-primary/5 dark:hover:bg-primary/20 dark:focus-within:bg-primary/20 ' +
|
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
|
||||||
(isCurrentFile
|
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
|
||||||
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
|
|
||||||
: '')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
@ -224,9 +227,9 @@ const FileTreeItem = ({
|
|||||||
{!isRenaming ? (
|
{!isRenaming ? (
|
||||||
<Disclosure.Button
|
<Disclosure.Button
|
||||||
className={
|
className={
|
||||||
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5 dark:hover:text-inherit dark:hover:bg-primary/10' +
|
' group border-none text-sm rounded-none p-0 m-0 flex items-center justify-start w-full py-0.5 hover:text-primary hover:bg-primary/5' +
|
||||||
(context.selectedDirectory.path.includes(fileOrDir.path)
|
(context.selectedDirectory.path.includes(fileOrDir.path)
|
||||||
? ' ui-open:bg-primary/10'
|
? ' ui-open:text-primary'
|
||||||
: '')
|
: '')
|
||||||
}
|
}
|
||||||
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
style={{ paddingInlineStart: getIndentationCSS(level) }}
|
||||||
@ -290,7 +293,7 @@ const FileTreeItem = ({
|
|||||||
fileOrDir={child}
|
fileOrDir={child}
|
||||||
project={project}
|
project={project}
|
||||||
currentFile={currentFile}
|
currentFile={currentFile}
|
||||||
onDoubleClick={onDoubleClick}
|
closePanel={closePanel}
|
||||||
level={level + 1}
|
level={level + 1}
|
||||||
key={level + '-' + child.path}
|
key={level + '-' + child.path}
|
||||||
/>
|
/>
|
||||||
@ -322,8 +325,20 @@ interface FileTreeProps {
|
|||||||
) => void
|
) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FileTreeMenu = () => {
|
export const FileTree = ({
|
||||||
const { send } = useFileContext()
|
className = '',
|
||||||
|
file,
|
||||||
|
closePanel,
|
||||||
|
}: FileTreeProps) => {
|
||||||
|
const { send, context } = useFileContext()
|
||||||
|
const docuemntHasFocus = useDocumentHasFocus()
|
||||||
|
useHotkeys('meta + n', createFile)
|
||||||
|
useHotkeys('meta + shift + n', createFolder)
|
||||||
|
|
||||||
|
// Refresh the file tree when the document gets focus
|
||||||
|
useEffect(() => {
|
||||||
|
send({ type: 'Refresh' })
|
||||||
|
}, [docuemntHasFocus])
|
||||||
|
|
||||||
async function createFile() {
|
async function createFile() {
|
||||||
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
send({ type: 'Create file', data: { name: '', makeDir: false } })
|
||||||
@ -333,88 +348,58 @@ export const FileTreeMenu = () => {
|
|||||||
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
send({ type: 'Create file', data: { name: '', makeDir: true } })
|
||||||
}
|
}
|
||||||
|
|
||||||
useHotkeys('meta + n', createFile)
|
|
||||||
useHotkeys('meta + shift + n', createFolder)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'filePlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFile}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create file
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
|
|
||||||
<ActionButton
|
|
||||||
Element="button"
|
|
||||||
icon={{
|
|
||||||
icon: 'folderPlus',
|
|
||||||
iconClassName: '!text-current',
|
|
||||||
bgClassName: 'bg-transparent',
|
|
||||||
}}
|
|
||||||
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
|
||||||
onClick={createFolder}
|
|
||||||
>
|
|
||||||
<Tooltip position="bottom-right" delay={750}>
|
|
||||||
Create folder
|
|
||||||
</Tooltip>
|
|
||||||
</ActionButton>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTree = ({ className = '', closePanel }: FileTreeProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
<div className="flex items-center gap-1 px-4 py-1 bg-chalkboard-20/40 dark:bg-chalkboard-80/50 border-b border-b-chalkboard-30 dark:border-b-chalkboard-80">
|
||||||
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
|
||||||
<FileTreeMenu />
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'filePlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFile}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create file
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
|
||||||
|
<ActionButton
|
||||||
|
Element="button"
|
||||||
|
icon={{
|
||||||
|
icon: 'folderPlus',
|
||||||
|
iconClassName: '!text-current',
|
||||||
|
bgClassName: 'bg-transparent',
|
||||||
|
}}
|
||||||
|
className="!p-0 !bg-transparent hover:text-primary border-transparent hover:border-primary !outline-none"
|
||||||
|
onClick={createFolder}
|
||||||
|
>
|
||||||
|
<Tooltip position="bottom-right" delay={750}>
|
||||||
|
Create folder
|
||||||
|
</Tooltip>
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-auto max-h-full pb-12">
|
||||||
|
<ul
|
||||||
|
className="m-0 p-0 text-sm"
|
||||||
|
onClickCapture={(e) => {
|
||||||
|
send({ type: 'Set selected directory', data: context.project })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{sortProject(context.project.children || []).map((fileOrDir) => (
|
||||||
|
<FileTreeItem
|
||||||
|
project={context.project}
|
||||||
|
currentFile={file}
|
||||||
|
fileOrDir={fileOrDir}
|
||||||
|
closePanel={closePanel}
|
||||||
|
key={fileOrDir.path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<FileTreeInner onDoubleClick={closePanel} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileTreeInner = ({
|
|
||||||
onDoubleClick,
|
|
||||||
}: {
|
|
||||||
onDoubleClick?: () => void
|
|
||||||
}) => {
|
|
||||||
const loaderData = useRouteLoaderData(paths.FILE) as IndexLoaderData
|
|
||||||
const { send, context } = useFileContext()
|
|
||||||
const documentHasFocus = useDocumentHasFocus()
|
|
||||||
|
|
||||||
// Refresh the file tree when the document gets focus
|
|
||||||
useEffect(() => {
|
|
||||||
send({ type: 'Refresh' })
|
|
||||||
}, [documentHasFocus])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="overflow-auto max-h-full pb-12">
|
|
||||||
<ul
|
|
||||||
className="m-0 p-0 text-sm"
|
|
||||||
onClickCapture={(e) => {
|
|
||||||
send({ type: 'Set selected directory', data: context.project })
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{sortProject(context.project.children || []).map((fileOrDir) => (
|
|
||||||
<FileTreeItem
|
|
||||||
project={context.project}
|
|
||||||
currentFile={loaderData?.file}
|
|
||||||
fileOrDir={fileOrDir}
|
|
||||||
onDoubleClick={onDoubleClick}
|
|
||||||
key={fileOrDir.path}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,10 @@ export function HelpMenu(props: React.PropsWithChildren) {
|
|||||||
if (isInProject) {
|
if (isInProject) {
|
||||||
navigate('onboarding')
|
navigate('onboarding')
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject(navigate)
|
createAndOpenNewProject(
|
||||||
|
settings.context.app.projectDirectory.current,
|
||||||
|
navigate
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
|
|||||||
import React, { createContext, useMemo, useEffect, useContext } from 'react'
|
import React, { createContext, useMemo, useEffect, useContext } from 'react'
|
||||||
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
|
||||||
import Client from '../editor/plugins/lsp/client'
|
import Client from '../editor/plugins/lsp/client'
|
||||||
import { TEST, VITE_KC_API_BASE_URL } from 'env'
|
import { DEV, TEST } from 'env'
|
||||||
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
import kclLanguage from 'editor/plugins/lsp/kcl/language'
|
||||||
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
|
||||||
import { useStore } from 'useStore'
|
import { useStore } from 'useStore'
|
||||||
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
baseUnit: defaultUnit.current,
|
baseUnit: defaultUnit.current,
|
||||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
devMode: DEV,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
worker: LspWorker.Kcl,
|
worker: LspWorker.Kcl,
|
||||||
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const initEvent: CopilotWorkerOptions = {
|
const initEvent: CopilotWorkerOptions = {
|
||||||
wasmUrl: wasmUrl(),
|
wasmUrl: wasmUrl(),
|
||||||
token: token,
|
token: token,
|
||||||
apiBaseUrl: VITE_KC_API_BASE_URL,
|
devMode: DEV,
|
||||||
}
|
}
|
||||||
lspWorker.postMessage({
|
lspWorker.postMessage({
|
||||||
worker: LspWorker.Copilot,
|
worker: LspWorker.Copilot,
|
||||||
|
@ -56,7 +56,6 @@ import toast from 'react-hot-toast'
|
|||||||
import { EditorSelection } from '@uiw/react-codemirror'
|
import { EditorSelection } from '@uiw/react-codemirror'
|
||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
|
||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
@ -85,12 +84,7 @@ export const ModelingMachineProvider = ({
|
|||||||
} = useSettingsAuthContext()
|
} = useSettingsAuthContext()
|
||||||
const token = auth?.context?.token
|
const token = auth?.context?.token
|
||||||
const streamRef = useRef<HTMLDivElement>(null)
|
const streamRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
let [searchParams] = useSearchParams()
|
|
||||||
const pool = searchParams.get('pool')
|
|
||||||
|
|
||||||
useSetupEngineManager(streamRef, token, {
|
useSetupEngineManager(streamRef, token, {
|
||||||
pool: pool,
|
|
||||||
theme: theme.current,
|
theme: theme.current,
|
||||||
highlightEdges: highlightEdges.current,
|
highlightEdges: highlightEdges.current,
|
||||||
enableSSAO: enableSSAO.current,
|
enableSSAO: enableSSAO.current,
|
||||||
|
@ -10,32 +10,21 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
|
|||||||
import { CustomIconName } from 'components/CustomIcon'
|
import { CustomIconName } from 'components/CustomIcon'
|
||||||
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
|
||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
|
import type { PaneType } from 'useStore'
|
||||||
import { MemoryPane } from './MemoryPane'
|
import { MemoryPane } from './MemoryPane'
|
||||||
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
import { KclErrorsPane, LogsPane } from './LoggingPanes'
|
||||||
import { DebugPane } from './DebugPane'
|
import { DebugPane } from './DebugPane'
|
||||||
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
|
|
||||||
|
|
||||||
export type SidebarType =
|
export type Pane = {
|
||||||
| 'code'
|
id: PaneType
|
||||||
| 'debug'
|
|
||||||
| 'export'
|
|
||||||
| 'files'
|
|
||||||
| 'kclErrors'
|
|
||||||
| 'logs'
|
|
||||||
| 'lspMessages'
|
|
||||||
| 'variables'
|
|
||||||
|
|
||||||
export type SidebarPane = {
|
|
||||||
id: SidebarType
|
|
||||||
title: string
|
title: string
|
||||||
icon: CustomIconName | IconDefinition
|
icon: CustomIconName | IconDefinition
|
||||||
keybinding: string
|
|
||||||
Content: ReactNode | React.FC
|
Content: ReactNode | React.FC
|
||||||
Menu?: ReactNode | React.FC
|
Menu?: ReactNode | React.FC
|
||||||
hideOnPlatform?: 'desktop' | 'web'
|
keybinding: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const topPanes: SidebarPane[] = [
|
export const topPanes: Pane[] = [
|
||||||
{
|
{
|
||||||
id: 'code',
|
id: 'code',
|
||||||
title: 'KCL Code',
|
title: 'KCL Code',
|
||||||
@ -44,18 +33,9 @@ export const topPanes: SidebarPane[] = [
|
|||||||
keybinding: 'shift + c',
|
keybinding: 'shift + c',
|
||||||
Menu: KclEditorMenu,
|
Menu: KclEditorMenu,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'files',
|
|
||||||
title: 'Project Files',
|
|
||||||
icon: 'folder',
|
|
||||||
Content: FileTreeInner,
|
|
||||||
keybinding: 'shift + f',
|
|
||||||
Menu: FileTreeMenu,
|
|
||||||
hideOnPlatform: 'web',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export const bottomPanes: SidebarPane[] = [
|
export const bottomPanes: Pane[] = [
|
||||||
{
|
{
|
||||||
id: 'variables',
|
id: 'variables',
|
||||||
title: 'Variables',
|
title: 'Variables',
|
||||||
|
@ -2,19 +2,13 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import { Resizable } from 're-resizable'
|
import { Resizable } from 're-resizable'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import { useStore } from 'useStore'
|
import { PaneType, useStore } from 'useStore'
|
||||||
import { Tab } from '@headlessui/react'
|
import { Tab } from '@headlessui/react'
|
||||||
import {
|
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
|
||||||
SidebarPane,
|
|
||||||
SidebarType,
|
|
||||||
bottomPanes,
|
|
||||||
topPanes,
|
|
||||||
} from './ModelingPanes'
|
|
||||||
import Tooltip from 'components/Tooltip'
|
import Tooltip from 'components/Tooltip'
|
||||||
import { ActionIcon } from 'components/ActionIcon'
|
import { ActionIcon } from 'components/ActionIcon'
|
||||||
import styles from './ModelingSidebar.module.css'
|
import styles from './ModelingSidebar.module.css'
|
||||||
import { ModelingPane } from './ModelingPane'
|
import { ModelingPane } from './ModelingPane'
|
||||||
import { isTauri } from 'lib/isTauri'
|
|
||||||
|
|
||||||
interface ModelingSidebarProps {
|
interface ModelingSidebarProps {
|
||||||
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
paneOpacity: '' | 'opacity-20' | 'opacity-40'
|
||||||
@ -58,7 +52,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingSidebarSectionProps {
|
interface ModelingSidebarSectionProps {
|
||||||
panes: SidebarPane[]
|
panes: Pane[]
|
||||||
alignButtons?: 'start' | 'end'
|
alignButtons?: 'start' | 'end'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,11 +69,11 @@ function ModelingSidebarSection({
|
|||||||
}))
|
}))
|
||||||
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
|
||||||
const [currentPane, setCurrentPane] = useState(
|
const [currentPane, setCurrentPane] = useState(
|
||||||
foundOpenPane || ('none' as SidebarType | 'none')
|
foundOpenPane || ('none' as PaneType | 'none')
|
||||||
)
|
)
|
||||||
|
|
||||||
const togglePane = useCallback(
|
const togglePane = useCallback(
|
||||||
(newPane: SidebarType | 'none') => {
|
(newPane: PaneType | 'none') => {
|
||||||
if (newPane === 'none') {
|
if (newPane === 'none') {
|
||||||
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
setOpenPanes(openPanes.filter((p) => p !== currentPane))
|
||||||
setCurrentPane('none')
|
setCurrentPane('none')
|
||||||
@ -96,15 +90,9 @@ function ModelingSidebarSection({
|
|||||||
|
|
||||||
// Filter out the debug panel if it's not supposed to be shown
|
// Filter out the debug panel if it's not supposed to be shown
|
||||||
// TODO: abstract out for allowing user to configure which panes to show
|
// TODO: abstract out for allowing user to configure which panes to show
|
||||||
const filteredPanes = (
|
const filteredPanes = showDebugPanel.current
|
||||||
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
|
? panes
|
||||||
).filter(
|
: panes.filter((pane) => pane.id !== 'debug')
|
||||||
(pane) =>
|
|
||||||
!pane.hideOnPlatform ||
|
|
||||||
(isTauri()
|
|
||||||
? pane.hideOnPlatform === 'web'
|
|
||||||
: pane.hideOnPlatform === 'desktop')
|
|
||||||
)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!showDebugPanel.current &&
|
!showDebugPanel.current &&
|
||||||
@ -180,8 +168,8 @@ function ModelingSidebarSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ModelingPaneButtonProps {
|
interface ModelingPaneButtonProps {
|
||||||
paneConfig: SidebarPane
|
paneConfig: Pane
|
||||||
currentPane: SidebarType | 'none'
|
currentPane: PaneType | 'none'
|
||||||
togglePane: () => void
|
togglePane: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { FormEvent, useEffect, useRef, useState } from 'react'
|
import { FormEvent, useEffect, useRef, useState } from 'react'
|
||||||
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { ActionButton } from './ActionButton'
|
import { ActionButton } from './ActionButton'
|
||||||
@ -8,11 +9,11 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
faX,
|
faX,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { getPartsCount, readProject } from '../lib/tauriFS'
|
||||||
import { FILE_EXT } from 'lib/constants'
|
import { FILE_EXT } from 'lib/constants'
|
||||||
import { Dialog } from '@headlessui/react'
|
import { Dialog } from '@headlessui/react'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import Tooltip from './Tooltip'
|
import Tooltip from './Tooltip'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
function ProjectCard({
|
function ProjectCard({
|
||||||
project,
|
project,
|
||||||
@ -20,17 +21,17 @@ function ProjectCard({
|
|||||||
handleDeleteProject,
|
handleDeleteProject,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
project: Project
|
project: ProjectWithEntryPointMetadata
|
||||||
handleRenameProject: (
|
handleRenameProject: (
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
f: Project
|
f: ProjectWithEntryPointMetadata
|
||||||
) => Promise<void>
|
) => Promise<void>
|
||||||
handleDeleteProject: (f: Project) => Promise<void>
|
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
|
||||||
}) {
|
}) {
|
||||||
useHotkeys('esc', () => setIsEditing(false))
|
useHotkeys('esc', () => setIsEditing(false))
|
||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||||
const [numberOfFiles, setNumberOfFiles] = useState(1)
|
const [numberOfParts, setNumberOfParts] = useState(1)
|
||||||
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
const [numberOfFolders, setNumberOfFolders] = useState(0)
|
||||||
|
|
||||||
let inputRef = useRef<HTMLInputElement>(null)
|
let inputRef = useRef<HTMLInputElement>(null)
|
||||||
@ -40,8 +41,7 @@ function ProjectCard({
|
|||||||
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
void handleRenameProject(e, project).then(() => setIsEditing(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDisplayedTime(dateStr: string) {
|
function getDisplayedTime(date: Date) {
|
||||||
const date = new Date(dateStr)
|
|
||||||
const startOfToday = new Date()
|
const startOfToday = new Date()
|
||||||
startOfToday.setHours(0, 0, 0, 0)
|
startOfToday.setHours(0, 0, 0, 0)
|
||||||
return date.getTime() < startOfToday.getTime()
|
return date.getTime() < startOfToday.getTime()
|
||||||
@ -50,12 +50,15 @@ function ProjectCard({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getNumberOfFiles() {
|
async function getNumberOfParts() {
|
||||||
setNumberOfFiles(project.kcl_file_count)
|
const { kclFileCount, kclDirCount } = getPartsCount(
|
||||||
setNumberOfFolders(project.directory_count)
|
await readProject(project.path)
|
||||||
|
)
|
||||||
|
setNumberOfParts(kclFileCount)
|
||||||
|
setNumberOfFolders(kclDirCount)
|
||||||
}
|
}
|
||||||
void getNumberOfFiles()
|
void getNumberOfParts()
|
||||||
}, [project.kcl_file_count, project.directory_count])
|
}, [project.path])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (inputRef.current) {
|
if (inputRef.current) {
|
||||||
@ -126,7 +129,7 @@ function ProjectCard({
|
|||||||
{project.name?.replace(FILE_EXT, '')}
|
{project.name?.replace(FILE_EXT, '')}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
|
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
|
||||||
{numberOfFolders > 0 &&
|
{numberOfFolders > 0 &&
|
||||||
`/ ${numberOfFolders} folder${
|
`/ ${numberOfFolders} folder${
|
||||||
numberOfFolders === 1 ? '' : 's'
|
numberOfFolders === 1 ? '' : 's'
|
||||||
@ -134,8 +137,8 @@ function ProjectCard({
|
|||||||
</span>
|
</span>
|
||||||
<span className="text-chalkboard-60 text-xs">
|
<span className="text-chalkboard-60 text-xs">
|
||||||
Edited{' '}
|
Edited{' '}
|
||||||
{project.metadata && project.metadata?.modified
|
{project.entrypointMetadata.mtime
|
||||||
? getDisplayedTime(project.metadata.modified)
|
? getDisplayedTime(project.entrypointMetadata.mtime)
|
||||||
: 'never'}
|
: 'never'}
|
||||||
</span>
|
</span>
|
||||||
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
<div className="absolute z-10 bottom-2 right-2 flex gap-1 items-center opacity-0 group-hover:opacity-100 group-focus-within:opacity-100">
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { fireEvent, render, screen } from '@testing-library/react'
|
import { fireEvent, render, screen } from '@testing-library/react'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
import ProjectSidebarMenu from './ProjectSidebarMenu'
|
||||||
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const projectWellFormed = {
|
const projectWellFormed = {
|
||||||
@ -14,17 +14,29 @@ const projectWellFormed = {
|
|||||||
{
|
{
|
||||||
name: 'main.kcl',
|
name: 'main.kcl',
|
||||||
path: '/some/path/Simple Box/main.kcl',
|
path: '/some/path/Simple Box/main.kcl',
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
metadata: {
|
entrypointMetadata: {
|
||||||
created: now.toISOString(),
|
atime: now,
|
||||||
modified: now.toISOString(),
|
blksize: 32,
|
||||||
|
blocks: 32,
|
||||||
|
birthtime: now,
|
||||||
|
dev: 1,
|
||||||
|
gid: 1,
|
||||||
|
ino: 1,
|
||||||
|
isDirectory: false,
|
||||||
|
isFile: true,
|
||||||
|
isSymlink: false,
|
||||||
|
mode: 1,
|
||||||
|
mtime: now,
|
||||||
|
nlink: 1,
|
||||||
|
readonly: false,
|
||||||
|
rdev: 1,
|
||||||
size: 32,
|
size: 32,
|
||||||
|
uid: 1,
|
||||||
|
fileAttributes: null,
|
||||||
},
|
},
|
||||||
kcl_file_count: 1,
|
} satisfies ProjectWithEntryPointMetadata
|
||||||
directory_count: 0,
|
|
||||||
} satisfies Project
|
|
||||||
|
|
||||||
describe('ProjectSidebarMenu tests', () => {
|
describe('ProjectSidebarMenu tests', () => {
|
||||||
test('Renders the project name', () => {
|
test('Renders the project name', () => {
|
||||||
|
@ -133,13 +133,13 @@ function ProjectMenuPopover({
|
|||||||
<p className="m-0 text-mono" data-testid="projectName">
|
<p className="m-0 text-mono" data-testid="projectName">
|
||||||
{project?.name ? project.name : APP_NAME}
|
{project?.name ? project.name : APP_NAME}
|
||||||
</p>
|
</p>
|
||||||
{project?.metadata && project.metadata.created && (
|
{project?.entrypointMetadata && (
|
||||||
<p
|
<p
|
||||||
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
|
||||||
data-testid="createdAt"
|
data-testid="createdAt"
|
||||||
>
|
>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{new Date(project.metadata.created).toLocaleDateString()}
|
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -168,7 +168,7 @@ export const SettingsAuthProviderBase = ({
|
|||||||
},
|
},
|
||||||
'Execute AST': () => kclManager.executeCode(true),
|
'Execute AST': () => kclManager.executeCode(true),
|
||||||
persistSettings: (context) =>
|
persistSettings: (context) =>
|
||||||
saveSettings(context, loadedProject?.project?.name),
|
saveSettings(context, loadedProject?.project?.path),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -8,13 +8,13 @@ export interface KclWorkerOptions {
|
|||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
baseUnit: UnitLength
|
baseUnit: UnitLength
|
||||||
apiBaseUrl: string
|
devMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CopilotWorkerOptions {
|
export interface CopilotWorkerOptions {
|
||||||
wasmUrl: string
|
wasmUrl: string
|
||||||
token: string
|
token: string
|
||||||
apiBaseUrl: string
|
devMode: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LspWorkerEventType {
|
export enum LspWorkerEventType {
|
||||||
|
@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
|
|||||||
export async function copilotLspRun(
|
export async function copilotLspRun(
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
token: string,
|
token: string,
|
||||||
baseUrl: string
|
devMode: boolean = false
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('starting copilot lsp')
|
console.log('starting copilot lsp')
|
||||||
await copilot_lsp_run(config, token, baseUrl)
|
await copilot_lsp_run(config, token, devMode)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('copilot lsp failed', e)
|
console.log('copilot lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -44,11 +44,11 @@ export async function kclLspRun(
|
|||||||
engineCommandManager: EngineCommandManager | null,
|
engineCommandManager: EngineCommandManager | null,
|
||||||
token: string,
|
token: string,
|
||||||
baseUnit: string,
|
baseUnit: string,
|
||||||
baseUrl: string
|
devMode: boolean = false
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
console.log('start kcl lsp')
|
console.log('start kcl lsp')
|
||||||
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
|
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode)
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('kcl lsp failed', e)
|
console.log('kcl lsp failed', e)
|
||||||
// We can't restart here because a moved value, we should do this another way.
|
// We can't restart here because a moved value, we should do this another way.
|
||||||
@ -80,12 +80,12 @@ onmessage = function (event) {
|
|||||||
null,
|
null,
|
||||||
kclData.token,
|
kclData.token,
|
||||||
kclData.baseUnit,
|
kclData.baseUnit,
|
||||||
kclData.apiBaseUrl
|
kclData.devMode
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
case LspWorker.Copilot:
|
case LspWorker.Copilot:
|
||||||
let copilotData = eventData as CopilotWorkerOptions
|
let copilotData = eventData as CopilotWorkerOptions
|
||||||
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
|
copilotLspRun(config, copilotData.token, copilotData.devMode)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -7,5 +7,7 @@ export const VITE_KC_API_BASE_URL = import.meta.env.VITE_KC_API_BASE_URL
|
|||||||
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
export const VITE_KC_SITE_BASE_URL = import.meta.env.VITE_KC_SITE_BASE_URL
|
||||||
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
export const VITE_KC_CONNECTION_TIMEOUT_MS = import.meta.env
|
||||||
.VITE_KC_CONNECTION_TIMEOUT_MS
|
.VITE_KC_CONNECTION_TIMEOUT_MS
|
||||||
|
export const VITE_KC_WASM_OVERRIDE_URL = import.meta.env
|
||||||
|
.VITE_KC_WASM_OVERRIDE_URL
|
||||||
export const TEST = import.meta.env.TEST
|
export const TEST = import.meta.env.TEST
|
||||||
export const DEV = import.meta.env.DEV
|
export const DEV = import.meta.env.DEV
|
||||||
|
@ -9,12 +9,10 @@ export function useSetupEngineManager(
|
|||||||
streamRef: React.RefObject<HTMLDivElement>,
|
streamRef: React.RefObject<HTMLDivElement>,
|
||||||
token?: string,
|
token?: string,
|
||||||
settings = {
|
settings = {
|
||||||
pool: null,
|
|
||||||
theme: Themes.System,
|
theme: Themes.System,
|
||||||
highlightEdges: true,
|
highlightEdges: true,
|
||||||
enableSSAO: true,
|
enableSSAO: true,
|
||||||
} as {
|
} as {
|
||||||
pool: string | null
|
|
||||||
theme: Themes
|
theme: Themes
|
||||||
highlightEdges: boolean
|
highlightEdges: boolean
|
||||||
enableSSAO: boolean
|
enableSSAO: boolean
|
||||||
@ -37,12 +35,6 @@ export function useSetupEngineManager(
|
|||||||
|
|
||||||
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
const hasSetNonZeroDimensions = useRef<boolean>(false)
|
||||||
|
|
||||||
if (settings.pool) {
|
|
||||||
// override the pool param (?pool=) to request a specific engine instance
|
|
||||||
// from a particular pool.
|
|
||||||
engineCommandManager.pool = settings.pool
|
|
||||||
}
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// Load the engine command manager once with the initial width and height,
|
// Load the engine command manager once with the initial width and height,
|
||||||
// then we do not want to reload it.
|
// then we do not want to reload it.
|
||||||
|
@ -888,7 +888,6 @@ export class EngineCommandManager {
|
|||||||
sceneCommandArtifacts: ArtifactMap = {}
|
sceneCommandArtifacts: ArtifactMap = {}
|
||||||
outSequence = 1
|
outSequence = 1
|
||||||
inSequence = 1
|
inSequence = 1
|
||||||
pool?: string
|
|
||||||
engineConnection?: EngineConnection
|
engineConnection?: EngineConnection
|
||||||
defaultPlanes: DefaultPlanes | null = null
|
defaultPlanes: DefaultPlanes | null = null
|
||||||
commandLogs: CommandLog[] = []
|
commandLogs: CommandLog[] = []
|
||||||
@ -915,9 +914,8 @@ export class EngineCommandManager {
|
|||||||
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
|
||||||
[]
|
[]
|
||||||
|
|
||||||
constructor(pool?: string) {
|
constructor() {
|
||||||
this.engineConnection = undefined
|
this.engineConnection = undefined
|
||||||
this.pool = pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private _camControlsCameraChange = () => {}
|
private _camControlsCameraChange = () => {}
|
||||||
@ -974,8 +972,7 @@ export class EngineCommandManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
|
||||||
const pool = this.pool === undefined ? '' : `&pool=${this.pool}`
|
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
|
||||||
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}${pool}`
|
|
||||||
this.engineConnection = new EngineConnection({
|
this.engineConnection = new EngineConnection({
|
||||||
engineCommandManager: this,
|
engineCommandManager: this,
|
||||||
url,
|
url,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
|
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import { join } from '@tauri-apps/api/path'
|
import { join } from '@tauri-apps/api/path'
|
||||||
import { readDirRecursive } from 'lib/tauri'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { FileEntry } from 'lib/types'
|
||||||
|
|
||||||
/// FileSystemManager is a class that provides a way to read files from the local file system.
|
/// FileSystemManager is a class that provides a way to read files from the local file system.
|
||||||
/// It assumes that you are in a project since it is solely used by the std lib
|
/// It assumes that you are in a project since it is solely used by the std lib
|
||||||
@ -68,7 +69,9 @@ class FileSystemManager {
|
|||||||
throw new Error(`Error joining dir: ${error}`)
|
throw new Error(`Error joining dir: ${error}`)
|
||||||
})
|
})
|
||||||
.then((p) => {
|
.then((p) => {
|
||||||
readDirRecursive(p)
|
invoke<FileEntry[]>('read_dir_recursive', {
|
||||||
|
path: p,
|
||||||
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw new Error(`Error reading dir: ${error}`)
|
throw new Error(`Error reading dir: ${error}`)
|
||||||
})
|
})
|
||||||
|
@ -10,11 +10,7 @@ import init, {
|
|||||||
make_default_planes,
|
make_default_planes,
|
||||||
coredump,
|
coredump,
|
||||||
toml_stringify,
|
toml_stringify,
|
||||||
default_app_settings,
|
toml_parse,
|
||||||
parse_app_settings,
|
|
||||||
parse_project_settings,
|
|
||||||
default_project_settings,
|
|
||||||
parse_project_route,
|
|
||||||
} from '../wasm-lib/pkg/wasm_lib'
|
} from '../wasm-lib/pkg/wasm_lib'
|
||||||
import { KCLError } from './errors'
|
import { KCLError } from './errors'
|
||||||
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
|
||||||
@ -29,10 +25,7 @@ import { AppInfo } from 'wasm-lib/kcl/bindings/AppInfo'
|
|||||||
import { CoreDumpManager } from 'lib/coredump'
|
import { CoreDumpManager } from 'lib/coredump'
|
||||||
import openWindow from 'lib/openWindow'
|
import openWindow from 'lib/openWindow'
|
||||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||||
import { TEST } from 'env'
|
import { TEST, VITE_KC_WASM_OVERRIDE_URL } from 'env'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
|
||||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
|
||||||
|
|
||||||
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
export type { Program } from '../wasm-lib/kcl/bindings/Program'
|
||||||
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
export type { Value } from '../wasm-lib/kcl/bindings/Value'
|
||||||
@ -83,18 +76,19 @@ export type { MemoryItem } from '../wasm-lib/kcl/bindings/MemoryItem'
|
|||||||
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
export type { ExtrudeSurface } from '../wasm-lib/kcl/bindings/ExtrudeSurface'
|
||||||
|
|
||||||
export const wasmUrl = () => {
|
export const wasmUrl = () => {
|
||||||
const baseUrl =
|
const baseUrl = VITE_KC_WASM_OVERRIDE_URL
|
||||||
typeof window === 'undefined'
|
? VITE_KC_WASM_OVERRIDE_URL
|
||||||
? 'http://127.0.0.1:3000'
|
: typeof window === 'undefined'
|
||||||
: window.location.origin.includes('tauri://localhost')
|
? 'http://127.0.0.1:3000'
|
||||||
? 'tauri://localhost' // custom protocol for macOS
|
: window.location.origin.includes('tauri://localhost')
|
||||||
: window.location.origin.includes('tauri.localhost')
|
? 'tauri://localhost' // custom protocol for macOS
|
||||||
? 'http://tauri.localhost' // fallback for Windows
|
: window.location.origin.includes('tauri.localhost')
|
||||||
: window.location.origin.includes('localhost')
|
? 'http://tauri.localhost' // fallback for Windows
|
||||||
? 'http://localhost:3000'
|
: window.location.origin.includes('localhost')
|
||||||
: window.location.origin && window.location.origin !== 'null'
|
? 'http://localhost:3000'
|
||||||
? window.location.origin
|
: window.location.origin && window.location.origin !== 'null'
|
||||||
: 'http://localhost:3000'
|
? window.location.origin
|
||||||
|
: 'http://localhost:3000'
|
||||||
const fullUrl = baseUrl + '/wasm_lib_bg.wasm'
|
const fullUrl = baseUrl + '/wasm_lib_bg.wasm'
|
||||||
console.log(`Full URL for WASM: ${fullUrl}`)
|
console.log(`Full URL for WASM: ${fullUrl}`)
|
||||||
|
|
||||||
@ -356,53 +350,11 @@ export function tomlStringify(toml: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultAppSettings(): Configuration {
|
export function tomlParse(toml: string): any {
|
||||||
try {
|
try {
|
||||||
const settings: Configuration = default_app_settings()
|
const parsed: any = toml_parse(toml)
|
||||||
return settings
|
return parsed
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
throw new Error(`Error getting default app settings: ${e}`)
|
throw new Error(`Error parsing toml: ${e}`)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseAppSettings(toml: string): Configuration {
|
|
||||||
try {
|
|
||||||
const settings: Configuration = parse_app_settings(toml)
|
|
||||||
return settings
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Error parsing app settings: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function defaultProjectSettings(): ProjectConfiguration {
|
|
||||||
try {
|
|
||||||
const settings: ProjectConfiguration = default_project_settings()
|
|
||||||
return settings
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Error getting default project settings: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseProjectSettings(toml: string): ProjectConfiguration {
|
|
||||||
try {
|
|
||||||
const settings: ProjectConfiguration = parse_project_settings(toml)
|
|
||||||
return settings
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Error parsing project settings: ${e}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseProjectRoute(
|
|
||||||
configuration: Configuration,
|
|
||||||
route_str: string
|
|
||||||
): ProjectRoute {
|
|
||||||
try {
|
|
||||||
const route: ProjectRoute = parse_project_route(
|
|
||||||
JSON.stringify(configuration),
|
|
||||||
route_str
|
|
||||||
)
|
|
||||||
return route
|
|
||||||
} catch (e: any) {
|
|
||||||
throw new Error(`Error parsing project route: ${e}`)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
|
|
||||||
|
|
||||||
const noModifiersPressed = (e: React.MouseEvent) =>
|
const noModifiersPressed = (e: React.MouseEvent) =>
|
||||||
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
|
||||||
|
|
||||||
@ -22,29 +20,6 @@ export const cameraSystems: CameraSystem[] = [
|
|||||||
'AutoCAD',
|
'AutoCAD',
|
||||||
]
|
]
|
||||||
|
|
||||||
export function mouseControlsToCameraSystem(
|
|
||||||
mouseControl: MouseControlType | undefined
|
|
||||||
): CameraSystem | undefined {
|
|
||||||
switch (mouseControl) {
|
|
||||||
case 'kitty_cad':
|
|
||||||
return 'KittyCAD'
|
|
||||||
case 'on_shape':
|
|
||||||
return 'OnShape'
|
|
||||||
case 'trackpad_friendly':
|
|
||||||
return 'Trackpad Friendly'
|
|
||||||
case 'solidworks':
|
|
||||||
return 'Solidworks'
|
|
||||||
case 'nx':
|
|
||||||
return 'NX'
|
|
||||||
case 'creo':
|
|
||||||
return 'Creo'
|
|
||||||
case 'auto_cad':
|
|
||||||
return 'AutoCAD'
|
|
||||||
default:
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MouseGuardHandler {
|
interface MouseGuardHandler {
|
||||||
description: string
|
description: string
|
||||||
callback: (e: React.MouseEvent) => boolean
|
callback: (e: React.MouseEvent) => boolean
|
||||||
|
@ -8,6 +8,8 @@ export const MAX_PADDING = 7
|
|||||||
* This is available for users to edit as a setting.
|
* This is available for users to edit as a setting.
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
export const DEFAULT_PROJECT_NAME = 'project-$nnn'
|
||||||
|
/** The file name for settings files, both at the user and project level */
|
||||||
|
export const SETTINGS_FILE_EXT = '.toml'
|
||||||
/** Name given the temporary "project" in the browser version of the app */
|
/** Name given the temporary "project" in the browser version of the app */
|
||||||
export const BROWSER_PROJECT_NAME = 'browser'
|
export const BROWSER_PROJECT_NAME = 'browser'
|
||||||
/** Name given the temporary file in the browser version of the app */
|
/** Name given the temporary file in the browser version of the app */
|
||||||
|
@ -49,11 +49,6 @@ export class CoreDumpManager {
|
|||||||
return APP_VERSION
|
return APP_VERSION
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the backend pool we've requested.
|
|
||||||
pool(): string {
|
|
||||||
return this.engineCommandManager.pool || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the os information.
|
// Get the os information.
|
||||||
getOsInfo(): Promise<string> {
|
getOsInfo(): Promise<string> {
|
||||||
if (this.isTauri()) {
|
if (this.isTauri()) {
|
||||||
|
@ -5,7 +5,7 @@ const sigmaAllow = 35000 // psi
|
|||||||
const width = 6 // inch
|
const width = 6 // inch
|
||||||
const p = 300 // Force on shelf - lbs
|
const p = 300 // Force on shelf - lbs
|
||||||
const distance = 12 // inches
|
const distance = 12 // inches
|
||||||
const M = distance * p / 2 // Moment experienced at fixed end of bracket
|
const M = 12 * 300 / 2 // Moment experienced at fixed end of bracket
|
||||||
const FOS = 2 // Factor of safety of 2
|
const FOS = 2 // Factor of safety of 2
|
||||||
const shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
const shelfMountL = 8 // The length of the bracket holding up the shelf is 6 inches
|
||||||
const wallMountL = 8 // the length of the bracket
|
const wallMountL = 8 // the length of the bracket
|
||||||
|
@ -1,11 +1,7 @@
|
|||||||
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||||
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
|
||||||
import { parseProjectRoute, readAppSettingsFile } from './tauri'
|
|
||||||
import { parseProjectRoute as parseProjectRouteWasm } from 'lang/wasm'
|
|
||||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
|
||||||
|
|
||||||
const prependRoutes =
|
const prependRoutes =
|
||||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||||
@ -29,23 +25,28 @@ export const paths = {
|
|||||||
} as const
|
} as const
|
||||||
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
|
||||||
|
|
||||||
export async function getProjectMetaByRouteId(
|
export function getProjectMetaByRouteId(id?: string, defaultDir = '') {
|
||||||
id?: string,
|
|
||||||
configuration?: Configuration
|
|
||||||
): Promise<ProjectRoute | undefined> {
|
|
||||||
if (!id) return undefined
|
if (!id) return undefined
|
||||||
|
const s = isTauri() ? sep() : '/'
|
||||||
|
|
||||||
const inTauri = isTauri()
|
const decodedId = decodeURIComponent(id).replace(/\/$/, '') // remove trailing slash
|
||||||
|
const projectAndFile =
|
||||||
|
defaultDir === '/'
|
||||||
|
? decodedId.replace(defaultDir, '')
|
||||||
|
: decodedId.replace(defaultDir + s, '')
|
||||||
|
const filePathParts = projectAndFile.split(s)
|
||||||
|
const projectName = filePathParts[0]
|
||||||
|
const projectPath =
|
||||||
|
(defaultDir === '/' ? defaultDir : defaultDir + s) + projectName
|
||||||
|
const lastPathPart = filePathParts[filePathParts.length - 1]
|
||||||
|
const currentFileName =
|
||||||
|
lastPathPart === projectName ? undefined : lastPathPart
|
||||||
|
const currentFilePath = lastPathPart === projectName ? undefined : decodedId
|
||||||
|
|
||||||
if (!configuration) {
|
return {
|
||||||
configuration = inTauri
|
projectName,
|
||||||
? await readAppSettingsFile()
|
projectPath,
|
||||||
: readLocalStorageAppSettingsFile()
|
currentFileName,
|
||||||
|
currentFilePath,
|
||||||
}
|
}
|
||||||
|
|
||||||
const route = inTauri
|
|
||||||
? await parseProjectRoute(configuration, id)
|
|
||||||
: parseProjectRouteWasm(configuration, id)
|
|
||||||
|
|
||||||
return route
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
|
||||||
import { FileLoaderData, HomeLoaderData, IndexLoaderData } from './types'
|
import {
|
||||||
|
FileEntry,
|
||||||
|
FileLoaderData,
|
||||||
|
HomeLoaderData,
|
||||||
|
IndexLoaderData,
|
||||||
|
} from './types'
|
||||||
import { isTauri } from './isTauri'
|
import { isTauri } from './isTauri'
|
||||||
import { getProjectMetaByRouteId, paths } from './paths'
|
import { getProjectMetaByRouteId, paths } from './paths'
|
||||||
import { BROWSER_PATH } from 'lib/paths'
|
import { BROWSER_PATH } from 'lib/paths'
|
||||||
@ -9,38 +14,33 @@ import {
|
|||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
import { loadAndValidateSettings } from './settings/settingsUtils'
|
import { loadAndValidateSettings } from './settings/settingsUtils'
|
||||||
|
import {
|
||||||
|
getInitialDefaultDir,
|
||||||
|
getProjectsInDir,
|
||||||
|
initializeProjectDirectory,
|
||||||
|
} from './tauriFS'
|
||||||
import makeUrlPathRelative from './makeUrlPathRelative'
|
import makeUrlPathRelative from './makeUrlPathRelative'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { join, sep } from '@tauri-apps/api/path'
|
||||||
import { readTextFile } from '@tauri-apps/plugin-fs'
|
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
|
||||||
import { codeManager, kclManager } from 'lib/singletons'
|
import { codeManager, kclManager } from 'lib/singletons'
|
||||||
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
import { fileSystemManager } from 'lang/std/fileSystemManager'
|
||||||
import {
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
getProjectInfo,
|
|
||||||
initializeProjectDirectory,
|
|
||||||
listProjects,
|
|
||||||
} from './tauri'
|
|
||||||
import { createSettings } from './settings/initialSettings'
|
|
||||||
|
|
||||||
// The root loader simply resolves the settings and any errors that
|
// The root loader simply resolves the settings and any errors that
|
||||||
// occurred during the settings load
|
// occurred during the settings load
|
||||||
export const settingsLoader: LoaderFunction = async ({
|
export const settingsLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
}): Promise<
|
}): ReturnType<typeof loadAndValidateSettings> => {
|
||||||
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
|
let settings = await loadAndValidateSettings()
|
||||||
> => {
|
|
||||||
let { settings, configuration } = await loadAndValidateSettings()
|
|
||||||
|
|
||||||
// I don't love that we have to read the settings again here,
|
// I don't love that we have to read the settings again here,
|
||||||
// but we need to get the project path to load the project settings
|
// but we need to get the project path to load the project settings
|
||||||
if (params.id) {
|
if (params.id) {
|
||||||
const projectPathData = await getProjectMetaByRouteId(
|
const defaultDir = settings.app.projectDirectory.current || ''
|
||||||
params.id,
|
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||||
configuration
|
|
||||||
)
|
|
||||||
if (projectPathData) {
|
if (projectPathData) {
|
||||||
const { project_name } = projectPathData
|
const { projectPath } = projectPathData
|
||||||
const { settings: s } = await loadAndValidateSettings(project_name)
|
settings = await loadAndValidateSettings(projectPath)
|
||||||
settings = s
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,7 +49,7 @@ export const settingsLoader: LoaderFunction = async ({
|
|||||||
|
|
||||||
// Redirect users to the appropriate onboarding page if they haven't completed it
|
// Redirect users to the appropriate onboarding page if they haven't completed it
|
||||||
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
||||||
const { settings } = await loadAndValidateSettings()
|
const settings = await loadAndValidateSettings()
|
||||||
const onboardingStatus = settings.app.onboardingStatus.current || ''
|
const onboardingStatus = settings.app.onboardingStatus.current || ''
|
||||||
const notEnRouteToOnboarding = !args.request.url.includes(
|
const notEnRouteToOnboarding = !args.request.url.includes(
|
||||||
paths.ONBOARDING.INDEX
|
paths.ONBOARDING.INDEX
|
||||||
@ -73,19 +73,17 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
|
|||||||
export const fileLoader: LoaderFunction = async ({
|
export const fileLoader: LoaderFunction = async ({
|
||||||
params,
|
params,
|
||||||
}): Promise<FileLoaderData | Response> => {
|
}): Promise<FileLoaderData | Response> => {
|
||||||
let { configuration } = await loadAndValidateSettings()
|
let settings = await loadAndValidateSettings()
|
||||||
|
|
||||||
const projectPathData = await getProjectMetaByRouteId(
|
const defaultDir = settings.app.projectDirectory.current || '/'
|
||||||
params.id,
|
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
|
||||||
configuration
|
|
||||||
)
|
|
||||||
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
||||||
|
|
||||||
if (!isBrowserProject && projectPathData) {
|
if (!isBrowserProject && projectPathData) {
|
||||||
const { project_name, project_path, current_file_name, current_file_path } =
|
const { projectName, projectPath, currentFileName, currentFilePath } =
|
||||||
projectPathData
|
projectPathData
|
||||||
|
|
||||||
if (!current_file_name || !current_file_path || !project_name) {
|
if (!currentFileName || !currentFilePath) {
|
||||||
return redirect(
|
return redirect(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
|
||||||
@ -95,34 +93,35 @@ export const fileLoader: LoaderFunction = async ({
|
|||||||
|
|
||||||
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
// TODO: PROJECT_ENTRYPOINT is hardcoded
|
||||||
// until we support setting a project's entrypoint file
|
// until we support setting a project's entrypoint file
|
||||||
const code = await readTextFile(current_file_path)
|
const code = await readTextFile(currentFilePath)
|
||||||
|
const entrypointMetadata = await stat(
|
||||||
|
await join(projectPath, PROJECT_ENTRYPOINT)
|
||||||
|
)
|
||||||
|
const children = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||||
|
path: projectPath,
|
||||||
|
})
|
||||||
// Update both the state and the editor's code.
|
// Update both the state and the editor's code.
|
||||||
// We explicitly do not write to the file here since we are loading from
|
// We explicitly do not write to the file here since we are loading from
|
||||||
// the file system and not the editor.
|
// the file system and not the editor.
|
||||||
codeManager.updateCurrentFilePath(current_file_path)
|
codeManager.updateCurrentFilePath(currentFilePath)
|
||||||
codeManager.updateCodeStateEditor(code)
|
codeManager.updateCodeStateEditor(code)
|
||||||
kclManager.executeCode(true)
|
kclManager.executeCode(true)
|
||||||
|
|
||||||
// Set the file system manager to the project path
|
// Set the file system manager to the project path
|
||||||
// So that WASM gets an updated path for operations
|
// So that WASM gets an updated path for operations
|
||||||
fileSystemManager.dir = project_path
|
fileSystemManager.dir = projectPath
|
||||||
|
|
||||||
const projectData: IndexLoaderData = {
|
const projectData: IndexLoaderData = {
|
||||||
code,
|
code,
|
||||||
project: isTauri()
|
project: {
|
||||||
? await getProjectInfo(project_path, configuration)
|
name: projectName,
|
||||||
: {
|
path: projectPath,
|
||||||
name: project_name,
|
children,
|
||||||
path: project_path,
|
entrypointMetadata,
|
||||||
children: [],
|
},
|
||||||
kcl_file_count: 0,
|
|
||||||
directory_count: 0,
|
|
||||||
},
|
|
||||||
file: {
|
file: {
|
||||||
name: current_file_name,
|
name: currentFileName,
|
||||||
path: current_file_path,
|
path: currentFilePath,
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,7 +140,6 @@ export const fileLoader: LoaderFunction = async ({
|
|||||||
file: {
|
file: {
|
||||||
name: BROWSER_FILE_NAME,
|
name: BROWSER_FILE_NAME,
|
||||||
path: decodeURIComponent(BROWSER_PATH),
|
path: decodeURIComponent(BROWSER_PATH),
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,12 +152,14 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
|||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
|
||||||
}
|
}
|
||||||
const { configuration } = await loadAndValidateSettings()
|
const settings = await loadAndValidateSettings()
|
||||||
|
|
||||||
const projectDir = await initializeProjectDirectory(configuration)
|
const projectDir = await initializeProjectDirectory(
|
||||||
|
settings.app.projectDirectory.current || (await getInitialDefaultDir())
|
||||||
|
)
|
||||||
|
|
||||||
if (projectDir) {
|
if (projectDir.path) {
|
||||||
const projects = await listProjects(configuration)
|
const projects = await getProjectsInDir(projectDir.path)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projects,
|
projects,
|
||||||
|
@ -1,228 +1,100 @@
|
|||||||
|
import {
|
||||||
|
getInitialDefaultDir,
|
||||||
|
getSettingsFilePaths,
|
||||||
|
readSettingsFile,
|
||||||
|
} from '../tauriFS'
|
||||||
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
|
||||||
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import {
|
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
|
||||||
defaultAppSettings,
|
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
|
||||||
defaultProjectSettings,
|
|
||||||
initPromise,
|
|
||||||
parseAppSettings,
|
|
||||||
parseProjectSettings,
|
|
||||||
tomlStringify,
|
|
||||||
} from 'lang/wasm'
|
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { mouseControlsToCameraSystem } from 'lib/cameraControls'
|
|
||||||
import { appThemeToTheme } from 'lib/theme'
|
|
||||||
import {
|
|
||||||
readAppSettingsFile,
|
|
||||||
readProjectSettingsFile,
|
|
||||||
writeAppSettingsFile,
|
|
||||||
writeProjectSettingsFile,
|
|
||||||
} from 'lib/tauri'
|
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
|
||||||
import { BROWSER_PROJECT_NAME } from 'lib/constants'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert from a rust settings struct into the JS settings struct.
|
* We expect the settings to be stored in a TOML file
|
||||||
* We do this because the JS settings type has all the fancy shit
|
* or TOML-formatted string in localStorage
|
||||||
* for hiding and showing settings.
|
* under a top-level [settings] key.
|
||||||
**/
|
* @param path
|
||||||
function configurationToSettingsPayload(
|
* @returns
|
||||||
configuration: Configuration
|
*/
|
||||||
): Partial<SaveSettingsPayload> {
|
function getSettingsFromStorage(path: string) {
|
||||||
return {
|
return isTauri()
|
||||||
app: {
|
? readSettingsFile(path)
|
||||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
: (tomlParse(localStorage.getItem(path) ?? '')
|
||||||
themeColor: configuration?.settings?.app?.appearance?.color
|
.settings as Partial<SaveSettingsPayload>)
|
||||||
? configuration?.settings?.app?.appearance?.color.toString()
|
|
||||||
: undefined,
|
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
|
||||||
projectDirectory: configuration?.settings?.project?.directory,
|
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
|
||||||
},
|
|
||||||
modeling: {
|
|
||||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
|
||||||
mouseControls: mouseControlsToCameraSystem(
|
|
||||||
configuration?.settings?.modeling?.mouse_controls
|
|
||||||
),
|
|
||||||
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
|
||||||
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
|
||||||
},
|
|
||||||
textEditor: {
|
|
||||||
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
|
||||||
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
|
||||||
},
|
|
||||||
projects: {
|
|
||||||
defaultProjectName:
|
|
||||||
configuration?.settings?.project?.default_project_name,
|
|
||||||
},
|
|
||||||
commandBar: {
|
|
||||||
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function projectConfigurationToSettingsPayload(
|
export async function loadAndValidateSettings(projectPath?: string) {
|
||||||
configuration: ProjectConfiguration
|
|
||||||
): Partial<SaveSettingsPayload> {
|
|
||||||
return {
|
|
||||||
app: {
|
|
||||||
theme: appThemeToTheme(configuration?.settings?.app?.appearance?.theme),
|
|
||||||
themeColor: configuration?.settings?.app?.appearance?.color
|
|
||||||
? configuration?.settings?.app?.appearance?.color.toString()
|
|
||||||
: undefined,
|
|
||||||
onboardingStatus: configuration?.settings?.app?.onboarding_status,
|
|
||||||
dismissWebBanner: configuration?.settings?.app?.dismiss_web_banner,
|
|
||||||
enableSSAO: configuration?.settings?.modeling?.enable_ssao,
|
|
||||||
},
|
|
||||||
modeling: {
|
|
||||||
defaultUnit: configuration?.settings?.modeling?.base_unit,
|
|
||||||
mouseControls: mouseControlsToCameraSystem(
|
|
||||||
configuration?.settings?.modeling?.mouse_controls
|
|
||||||
),
|
|
||||||
highlightEdges: configuration?.settings?.modeling?.highlight_edges,
|
|
||||||
showDebugPanel: configuration?.settings?.modeling?.show_debug_panel,
|
|
||||||
},
|
|
||||||
textEditor: {
|
|
||||||
textWrapping: configuration?.settings?.text_editor?.text_wrapping,
|
|
||||||
blinkingCursor: configuration?.settings?.text_editor?.blinking_cursor,
|
|
||||||
},
|
|
||||||
commandBar: {
|
|
||||||
includeSettings: configuration?.settings?.command_bar?.include_settings,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function localStorageAppSettingsPath() {
|
|
||||||
return '/settings.toml'
|
|
||||||
}
|
|
||||||
|
|
||||||
function localStorageProjectSettingsPath() {
|
|
||||||
return '/' + BROWSER_PROJECT_NAME + '/project.toml'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function readLocalStorageAppSettingsFile(): Configuration {
|
|
||||||
// TODO: Remove backwards compatibility after a few releases.
|
|
||||||
let stored =
|
|
||||||
localStorage.getItem(localStorageAppSettingsPath()) ??
|
|
||||||
localStorage.getItem('/user.toml') ??
|
|
||||||
''
|
|
||||||
|
|
||||||
if (stored === '') {
|
|
||||||
return defaultAppSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return parseAppSettings(stored)
|
|
||||||
} catch (e) {
|
|
||||||
const settings = defaultAppSettings()
|
|
||||||
localStorage.setItem(localStorageAppSettingsPath(), tomlStringify(settings))
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function readLocalStorageProjectSettingsFile(): ProjectConfiguration {
|
|
||||||
// TODO: Remove backwards compatibility after a few releases.
|
|
||||||
let stored = localStorage.getItem(localStorageProjectSettingsPath()) ?? ''
|
|
||||||
|
|
||||||
if (stored === '') {
|
|
||||||
return defaultProjectSettings()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
return parseProjectSettings(stored)
|
|
||||||
} catch (e) {
|
|
||||||
const settings = defaultProjectSettings()
|
|
||||||
localStorage.setItem(
|
|
||||||
localStorageProjectSettingsPath(),
|
|
||||||
tomlStringify(settings)
|
|
||||||
)
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppSettings {
|
|
||||||
settings: ReturnType<typeof createSettings>
|
|
||||||
configuration: Configuration
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadAndValidateSettings(
|
|
||||||
projectName?: string
|
|
||||||
): Promise<AppSettings> {
|
|
||||||
const settings = createSettings()
|
const settings = createSettings()
|
||||||
const inTauri = isTauri()
|
settings.app.projectDirectory.default = await getInitialDefaultDir()
|
||||||
|
// First, get the settings data at the user and project level
|
||||||
|
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
||||||
|
|
||||||
if (!inTauri) {
|
// Load the settings from the files
|
||||||
// Make sure we have wasm initialized.
|
if (settingsFilePaths.user) {
|
||||||
await initPromise
|
await initPromise
|
||||||
|
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
|
||||||
|
if (userSettings) {
|
||||||
|
setSettingsAtLevel(settings, 'user', userSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the app settings from the file system or localStorage.
|
|
||||||
const appSettings = inTauri
|
|
||||||
? await readAppSettingsFile()
|
|
||||||
: readLocalStorageAppSettingsFile()
|
|
||||||
// Convert the app settings to the JS settings format.
|
|
||||||
const appSettingsPayload = configurationToSettingsPayload(appSettings)
|
|
||||||
setSettingsAtLevel(settings, 'user', appSettingsPayload)
|
|
||||||
|
|
||||||
// Load the project settings if they exist
|
// Load the project settings if they exist
|
||||||
if (projectName) {
|
if (settingsFilePaths.project) {
|
||||||
const projectSettings = inTauri
|
const projectSettings = await getSettingsFromStorage(
|
||||||
? await readProjectSettingsFile(appSettings, projectName)
|
settingsFilePaths.project
|
||||||
: readLocalStorageProjectSettingsFile()
|
)
|
||||||
|
if (projectSettings) {
|
||||||
const projectSettingsPayload =
|
setSettingsAtLevel(settings, 'project', projectSettings)
|
||||||
projectConfigurationToSettingsPayload(projectSettings)
|
}
|
||||||
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the settings object
|
// Return the settings object
|
||||||
return { settings, configuration: appSettings }
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSettings(
|
export async function saveSettings(
|
||||||
allSettings: typeof settings,
|
allSettings: typeof settings,
|
||||||
projectName?: string
|
projectPath?: string
|
||||||
|
) {
|
||||||
|
const settingsFilePaths = await getSettingsFilePaths(projectPath)
|
||||||
|
|
||||||
|
if (settingsFilePaths.user) {
|
||||||
|
const changedSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
||||||
|
|
||||||
|
await writeOrClearPersistedSettings(settingsFilePaths.user, changedSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settingsFilePaths.project) {
|
||||||
|
const changedSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
||||||
|
|
||||||
|
await writeOrClearPersistedSettings(
|
||||||
|
settingsFilePaths.project,
|
||||||
|
changedSettings
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeOrClearPersistedSettings(
|
||||||
|
settingsFilePath: string,
|
||||||
|
changedSettings: Partial<SaveSettingsPayload>
|
||||||
) {
|
) {
|
||||||
// Make sure we have wasm initialized.
|
|
||||||
await initPromise
|
await initPromise
|
||||||
const inTauri = isTauri()
|
if (changedSettings && Object.keys(changedSettings).length) {
|
||||||
|
if (isTauri()) {
|
||||||
// Get the user settings.
|
await writeTextFile(
|
||||||
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
|
settingsFilePath,
|
||||||
const tomlString = tomlStringify({ settings: jsAppSettings })
|
tomlStringify({ settings: changedSettings })
|
||||||
// Parse this as a Configuration.
|
)
|
||||||
const appSettings = parseAppSettings(tomlString)
|
}
|
||||||
|
|
||||||
// Write the app settings.
|
|
||||||
if (inTauri) {
|
|
||||||
await writeAppSettingsFile(appSettings)
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
localStorageAppSettingsPath(),
|
settingsFilePath,
|
||||||
tomlStringify(appSettings)
|
tomlStringify({ settings: changedSettings })
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if (!projectName) {
|
|
||||||
// If we're not saving project settings, we're done.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the project settings.
|
|
||||||
const jsProjectSettings = getChangedSettingsAtLevel(allSettings, 'project')
|
|
||||||
const projectTomlString = tomlStringify({ settings: jsProjectSettings })
|
|
||||||
// Parse this as a Configuration.
|
|
||||||
const projectSettings = parseProjectSettings(projectTomlString)
|
|
||||||
|
|
||||||
// Write the project settings.
|
|
||||||
if (inTauri) {
|
|
||||||
await writeProjectSettingsFile(appSettings, projectName, projectSettings)
|
|
||||||
} else {
|
} else {
|
||||||
localStorage.setItem(
|
if (isTauri() && (await exists(settingsFilePath))) {
|
||||||
localStorageProjectSettingsPath(),
|
await remove(settingsFilePath)
|
||||||
tomlStringify(projectSettings)
|
}
|
||||||
)
|
localStorage.removeItem(settingsFilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
faArrowUp,
|
faArrowUp,
|
||||||
faCircle,
|
faCircle,
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
|
|
||||||
const DESC = ':desc'
|
const DESC = ':desc'
|
||||||
|
|
||||||
@ -27,7 +27,10 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSortFunction(sortBy: string) {
|
export function getSortFunction(sortBy: string) {
|
||||||
const sortByName = (a: Project, b: Project) => {
|
const sortByName = (
|
||||||
|
a: ProjectWithEntryPointMetadata,
|
||||||
|
b: ProjectWithEntryPointMetadata
|
||||||
|
) => {
|
||||||
if (a.name && b.name) {
|
if (a.name && b.name) {
|
||||||
return sortBy.includes('desc')
|
return sortBy.includes('desc')
|
||||||
? a.name.localeCompare(b.name)
|
? a.name.localeCompare(b.name)
|
||||||
@ -36,13 +39,16 @@ export function getSortFunction(sortBy: string) {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortByModified = (a: Project, b: Project) => {
|
const sortByModified = (
|
||||||
if (a.metadata?.modified && b.metadata?.modified) {
|
a: ProjectWithEntryPointMetadata,
|
||||||
const aDate = new Date(a.metadata.modified)
|
b: ProjectWithEntryPointMetadata
|
||||||
const bDate = new Date(b.metadata.modified)
|
) => {
|
||||||
|
if (a.entrypointMetadata?.mtime && b.entrypointMetadata?.mtime) {
|
||||||
return !sortBy || sortBy.includes('desc')
|
return !sortBy || sortBy.includes('desc')
|
||||||
? bDate.getTime() - aDate.getTime()
|
? b.entrypointMetadata.mtime.getTime() -
|
||||||
: aDate.getTime() - bDate.getTime()
|
a.entrypointMetadata.mtime.getTime()
|
||||||
|
: a.entrypointMetadata.mtime.getTime() -
|
||||||
|
b.entrypointMetadata.mtime.getTime()
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
150
src/lib/tauri.ts
@ -1,150 +0,0 @@
|
|||||||
// This file contains wrappers around the tauri commands we define in rust code.
|
|
||||||
|
|
||||||
import { Models } from '@kittycad/lib/dist/types/src'
|
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
|
||||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
|
||||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
|
||||||
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'
|
|
||||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
|
||||||
|
|
||||||
// Get the app state from tauri.
|
|
||||||
export async function getState(): Promise<ProjectState | undefined> {
|
|
||||||
return await invoke<ProjectState | undefined>('get_state')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the app state in tauri.
|
|
||||||
export async function setState(state: ProjectState | undefined): Promise<void> {
|
|
||||||
return await invoke('set_state', { state })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the initial default dir for holding all projects.
|
|
||||||
export async function getInitialDefaultDir(): Promise<string> {
|
|
||||||
return invoke<string>('get_initial_default_dir')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showInFolder(path: string | undefined): Promise<void> {
|
|
||||||
if (!path) {
|
|
||||||
console.error('path is undefined cannot call tauri showInFolder')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
return await invoke('show_in_folder', { path })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initializeProjectDirectory(
|
|
||||||
settings: Configuration
|
|
||||||
): Promise<string> {
|
|
||||||
return await invoke<string>('initialize_project_directory', {
|
|
||||||
configuration: settings,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createNewProjectDirectory(
|
|
||||||
projectName: string,
|
|
||||||
initialCode?: string,
|
|
||||||
configuration?: Configuration
|
|
||||||
): Promise<Project> {
|
|
||||||
if (!configuration) {
|
|
||||||
configuration = await readAppSettingsFile()
|
|
||||||
}
|
|
||||||
return await invoke<Project>('create_new_project_directory', {
|
|
||||||
configuration,
|
|
||||||
projectName,
|
|
||||||
initialCode,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listProjects(
|
|
||||||
configuration?: Configuration
|
|
||||||
): Promise<Project[]> {
|
|
||||||
if (!configuration) {
|
|
||||||
configuration = await readAppSettingsFile()
|
|
||||||
}
|
|
||||||
return await invoke<Project[]>('list_projects', { configuration })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProjectInfo(
|
|
||||||
projectPath: string,
|
|
||||||
configuration?: Configuration
|
|
||||||
): Promise<Project> {
|
|
||||||
if (!configuration) {
|
|
||||||
configuration = await readAppSettingsFile()
|
|
||||||
}
|
|
||||||
return await invoke<Project>('get_project_info', {
|
|
||||||
configuration,
|
|
||||||
projectPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function login(host: string): Promise<string> {
|
|
||||||
return await invoke('login', { host })
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseProjectRoute(
|
|
||||||
configuration: Configuration,
|
|
||||||
route: string
|
|
||||||
): Promise<ProjectRoute> {
|
|
||||||
return await invoke<ProjectRoute>('parse_project_route', {
|
|
||||||
configuration,
|
|
||||||
route,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getUser(
|
|
||||||
token: string | undefined,
|
|
||||||
host: string
|
|
||||||
): Promise<Models['User_type'] | Record<'error_code', unknown> | void> {
|
|
||||||
if (!token) {
|
|
||||||
console.error('token is undefined cannot call tauri getUser')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
return await invoke<Models['User_type'] | Record<'error_code', unknown>>(
|
|
||||||
'get_user',
|
|
||||||
{
|
|
||||||
token: token,
|
|
||||||
hostname: host,
|
|
||||||
}
|
|
||||||
).catch((err) => console.error('error from Tauri getUser', err))
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readDirRecursive(path: string): Promise<FileEntry[]> {
|
|
||||||
return await invoke<FileEntry[]>('read_dir_recursive', { path })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the contents of the app settings.
|
|
||||||
export async function readAppSettingsFile(): Promise<Configuration> {
|
|
||||||
return await invoke<Configuration>('read_app_settings_file')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the contents of the app settings.
|
|
||||||
export async function writeAppSettingsFile(
|
|
||||||
settings: Configuration
|
|
||||||
): Promise<void> {
|
|
||||||
return await invoke('write_app_settings_file', { configuration: settings })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read project settings file.
|
|
||||||
export async function readProjectSettingsFile(
|
|
||||||
appSettings: Configuration,
|
|
||||||
projectName: string
|
|
||||||
): Promise<ProjectConfiguration> {
|
|
||||||
return await invoke<ProjectConfiguration>('read_project_settings_file', {
|
|
||||||
appSettings,
|
|
||||||
projectName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write project settings file.
|
|
||||||
export async function writeProjectSettingsFile(
|
|
||||||
appSettings: Configuration,
|
|
||||||
projectName: string,
|
|
||||||
settings: ProjectConfiguration
|
|
||||||
): Promise<void> {
|
|
||||||
return await invoke('write_project_settings_file', {
|
|
||||||
appSettings,
|
|
||||||
projectName,
|
|
||||||
configuration: settings,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,4 +1,11 @@
|
|||||||
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
|
import {
|
||||||
|
deepFileFilter,
|
||||||
|
getNextProjectIndex,
|
||||||
|
getPartsCount,
|
||||||
|
interpolateProjectNameWithIndex,
|
||||||
|
isRelevantFileOrDir,
|
||||||
|
} from './tauriFS'
|
||||||
|
import type { FileEntry } from './types'
|
||||||
import { MAX_PADDING } from './constants'
|
import { MAX_PADDING } from './constants'
|
||||||
|
|
||||||
describe('Test project name utility functions', () => {
|
describe('Test project name utility functions', () => {
|
||||||
@ -24,22 +31,18 @@ describe('Test project name utility functions', () => {
|
|||||||
{
|
{
|
||||||
name: 'new-project-04.kcl',
|
name: 'new-project-04.kcl',
|
||||||
path: '/projects/new-project-04.kcl',
|
path: '/projects/new-project-04.kcl',
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-007.kcl',
|
name: 'new-project-007.kcl',
|
||||||
path: '/projects/new-project-007.kcl',
|
path: '/projects/new-project-007.kcl',
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-05.kcl',
|
name: 'new-project-05.kcl',
|
||||||
path: '/projects/new-project-05.kcl',
|
path: '/projects/new-project-05.kcl',
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'new-project-0.kcl',
|
name: 'new-project-0.kcl',
|
||||||
path: '/projects/new-project-0.kcl',
|
path: '/projects/new-project-0.kcl',
|
||||||
children: [],
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -47,3 +50,101 @@ describe('Test project name utility functions', () => {
|
|||||||
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
|
expect(getNextProjectIndex('new-project-$n', testFiles)).toBe(8)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('Test file tree utility functions', () => {
|
||||||
|
const baseFiles: FileEntry[] = [
|
||||||
|
{
|
||||||
|
name: 'show-me.kcl',
|
||||||
|
path: '/projects/show-me.kcl',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hide-me.jpg',
|
||||||
|
path: '/projects/hide-me.jpg',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '.gitignore',
|
||||||
|
path: '/projects/.gitignore',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredBaseFiles: FileEntry[] = [
|
||||||
|
{
|
||||||
|
name: 'show-me.kcl',
|
||||||
|
path: '/projects/show-me.kcl',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it('Only includes files relevant to the project in a flat directory', () => {
|
||||||
|
expect(deepFileFilter(baseFiles, isRelevantFileOrDir)).toEqual(
|
||||||
|
filteredBaseFiles
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const nestedFiles: FileEntry[] = [
|
||||||
|
...baseFiles,
|
||||||
|
{
|
||||||
|
name: 'show-me',
|
||||||
|
path: '/projects/show-me',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'show-me-nested',
|
||||||
|
path: '/projects/show-me/show-me-nested',
|
||||||
|
children: baseFiles,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hide-me',
|
||||||
|
path: '/projects/show-me/hide-me',
|
||||||
|
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hide-me',
|
||||||
|
path: '/projects/hide-me',
|
||||||
|
children: baseFiles.filter((file) => file.name !== 'show-me.kcl'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const filteredNestedFiles: FileEntry[] = [
|
||||||
|
...filteredBaseFiles,
|
||||||
|
{
|
||||||
|
name: 'show-me',
|
||||||
|
path: '/projects/show-me',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'show-me-nested',
|
||||||
|
path: '/projects/show-me/show-me-nested',
|
||||||
|
children: filteredBaseFiles,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it('Only includes directories that include files relevant to the project in a nested directory', () => {
|
||||||
|
expect(deepFileFilter(nestedFiles, isRelevantFileOrDir)).toEqual(
|
||||||
|
filteredNestedFiles
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const withHiddenDir: FileEntry[] = [
|
||||||
|
...baseFiles,
|
||||||
|
{
|
||||||
|
name: '.hide-me',
|
||||||
|
path: '/projects/.hide-me',
|
||||||
|
children: baseFiles,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
it(`Hides folders that begin with a ".", even if they contain relevant files`, () => {
|
||||||
|
expect(deepFileFilter(withHiddenDir, isRelevantFileOrDir)).toEqual(
|
||||||
|
filteredBaseFiles
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it(`Properly counts the number of relevant files and directories in a project`, () => {
|
||||||
|
expect(getPartsCount(nestedFiles)).toEqual({
|
||||||
|
kclFileCount: 2,
|
||||||
|
kclDirCount: 2,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -1,19 +1,154 @@
|
|||||||
import { appConfigDir } from '@tauri-apps/api/path'
|
|
||||||
import { isTauri } from './isTauri'
|
|
||||||
import type { FileEntry } from 'lib/types'
|
|
||||||
import {
|
import {
|
||||||
|
mkdir,
|
||||||
|
exists,
|
||||||
|
readTextFile,
|
||||||
|
writeTextFile,
|
||||||
|
stat,
|
||||||
|
} from '@tauri-apps/plugin-fs'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import {
|
||||||
|
appConfigDir,
|
||||||
|
documentDir,
|
||||||
|
homeDir,
|
||||||
|
join,
|
||||||
|
sep,
|
||||||
|
} from '@tauri-apps/api/path'
|
||||||
|
import { isTauri } from './isTauri'
|
||||||
|
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
|
import {
|
||||||
|
FILE_EXT,
|
||||||
INDEX_IDENTIFIER,
|
INDEX_IDENTIFIER,
|
||||||
MAX_PADDING,
|
MAX_PADDING,
|
||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
|
PROJECT_FOLDER,
|
||||||
|
RELEVANT_FILE_TYPES,
|
||||||
|
SETTINGS_FILE_EXT,
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
|
import { SaveSettingsPayload, SettingsLevel } from './settings/settingsTypes'
|
||||||
|
import { initPromise, tomlParse } from 'lang/wasm'
|
||||||
import { bracket } from './exampleKcl'
|
import { bracket } from './exampleKcl'
|
||||||
import { paths } from './paths'
|
import { paths } from './paths'
|
||||||
import {
|
|
||||||
createNewProjectDirectory,
|
type PathWithPossibleError = {
|
||||||
listProjects,
|
path: string | null
|
||||||
readAppSettingsFile,
|
error: Error | null
|
||||||
} from './tauri'
|
}
|
||||||
|
|
||||||
|
export async function getInitialDefaultDir() {
|
||||||
|
if (!isTauri()) return ''
|
||||||
|
let dir
|
||||||
|
try {
|
||||||
|
dir = await documentDir()
|
||||||
|
} catch (e) {
|
||||||
|
dir = await join(await homeDir(), 'Documents') // for headless Linux (eg. Github Actions)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await join(dir, PROJECT_FOLDER)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes the project directory and returns the path
|
||||||
|
// with any Errors that occurred
|
||||||
|
export async function initializeProjectDirectory(
|
||||||
|
directory: string
|
||||||
|
): Promise<PathWithPossibleError> {
|
||||||
|
let returnValue: PathWithPossibleError = {
|
||||||
|
path: null,
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isTauri()) return returnValue
|
||||||
|
|
||||||
|
if (directory) {
|
||||||
|
returnValue = await testAndCreateDir(directory, returnValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the directory from settings does not exist or could not be created,
|
||||||
|
// use the default directory
|
||||||
|
if (returnValue.path === null) {
|
||||||
|
const INITIAL_DEFAULT_DIR = await getInitialDefaultDir()
|
||||||
|
const defaultReturnValue = await testAndCreateDir(
|
||||||
|
INITIAL_DEFAULT_DIR,
|
||||||
|
returnValue,
|
||||||
|
{
|
||||||
|
exists: 'Error checking default directory.',
|
||||||
|
create: 'Error creating default directory.',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
returnValue.path = defaultReturnValue.path
|
||||||
|
returnValue.error =
|
||||||
|
returnValue.error === null ? defaultReturnValue.error : returnValue.error
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testAndCreateDir(
|
||||||
|
directory: string,
|
||||||
|
returnValue = {
|
||||||
|
path: null,
|
||||||
|
error: null,
|
||||||
|
} as PathWithPossibleError,
|
||||||
|
errorMessages = {
|
||||||
|
exists:
|
||||||
|
'Error checking directory at path from saved settings. Using default.',
|
||||||
|
create:
|
||||||
|
'Error creating directory at path from saved settings. Using default.',
|
||||||
|
}
|
||||||
|
): Promise<PathWithPossibleError> {
|
||||||
|
const dirExists = await exists(directory).catch((e) => {
|
||||||
|
console.error(`Error checking directory ${directory}. Original error:`, e)
|
||||||
|
return new Error(errorMessages.exists)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (dirExists instanceof Error) {
|
||||||
|
returnValue.error = dirExists
|
||||||
|
} else if (dirExists === false) {
|
||||||
|
const newDirCreated = await mkdir(directory, { recursive: true }).catch(
|
||||||
|
(e) => {
|
||||||
|
console.error(
|
||||||
|
`Error creating directory ${directory}. Original error:`,
|
||||||
|
e
|
||||||
|
)
|
||||||
|
return new Error(errorMessages.create)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newDirCreated instanceof Error) {
|
||||||
|
returnValue.error = newDirCreated
|
||||||
|
} else {
|
||||||
|
returnValue.path = directory
|
||||||
|
}
|
||||||
|
} else if (dirExists === true) {
|
||||||
|
returnValue.path = directory
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProjectDirectory(fileOrDir: Partial<FileEntry>) {
|
||||||
|
return (
|
||||||
|
fileOrDir.children?.length &&
|
||||||
|
fileOrDir.children.some((child) => child.name === PROJECT_ENTRYPOINT)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the contents of a directory
|
||||||
|
// and return the valid projects
|
||||||
|
export async function getProjectsInDir(projectDir: string) {
|
||||||
|
const readProjects = (
|
||||||
|
await invoke<FileEntry[]>('read_dir_recursive', { path: projectDir })
|
||||||
|
).filter(isProjectDirectory)
|
||||||
|
|
||||||
|
const projectsWithMetadata = await Promise.all(
|
||||||
|
readProjects.map(async (p) => ({
|
||||||
|
entrypointMetadata: await stat(await join(p.path, PROJECT_ENTRYPOINT)),
|
||||||
|
...p,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
return projectsWithMetadata
|
||||||
|
}
|
||||||
|
|
||||||
export const isHidden = (fileOrDir: FileEntry) =>
|
export const isHidden = (fileOrDir: FileEntry) =>
|
||||||
!!fileOrDir.name?.startsWith('.')
|
!!fileOrDir.name?.startsWith('.')
|
||||||
@ -21,6 +156,97 @@ export const isHidden = (fileOrDir: FileEntry) =>
|
|||||||
export const isDir = (fileOrDir: FileEntry) =>
|
export const isDir = (fileOrDir: FileEntry) =>
|
||||||
'children' in fileOrDir && fileOrDir.children !== undefined
|
'children' in fileOrDir && fileOrDir.children !== undefined
|
||||||
|
|
||||||
|
export function deepFileFilter(
|
||||||
|
entries: FileEntry[],
|
||||||
|
filterFn: (f: FileEntry) => boolean
|
||||||
|
): FileEntry[] {
|
||||||
|
const filteredEntries: FileEntry[] = []
|
||||||
|
for (const fileOrDir of entries) {
|
||||||
|
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||||
|
const filteredChildren = deepFileFilter(fileOrDir.children, filterFn)
|
||||||
|
if (filterFn(fileOrDir)) {
|
||||||
|
filteredEntries.push({
|
||||||
|
...fileOrDir,
|
||||||
|
children: filteredChildren,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else if (filterFn(fileOrDir)) {
|
||||||
|
filteredEntries.push(fileOrDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepFileFilterFlat(
|
||||||
|
entries: FileEntry[],
|
||||||
|
filterFn: (f: FileEntry) => boolean
|
||||||
|
): FileEntry[] {
|
||||||
|
const filteredEntries: FileEntry[] = []
|
||||||
|
for (const fileOrDir of entries) {
|
||||||
|
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||||
|
const filteredChildren = deepFileFilterFlat(fileOrDir.children, filterFn)
|
||||||
|
if (filterFn(fileOrDir)) {
|
||||||
|
filteredEntries.push({
|
||||||
|
...fileOrDir,
|
||||||
|
children: filteredChildren,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filteredEntries.push(...filteredChildren)
|
||||||
|
} else if (filterFn(fileOrDir)) {
|
||||||
|
filteredEntries.push(fileOrDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filteredEntries
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the contents of a project directory
|
||||||
|
// and return all relevant files and sub-directories recursively
|
||||||
|
export async function readProject(projectDir: string) {
|
||||||
|
const readFiles = await invoke<FileEntry[]>('read_dir_recursive', {
|
||||||
|
path: projectDir,
|
||||||
|
})
|
||||||
|
|
||||||
|
return deepFileFilter(readFiles, isRelevantFileOrDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a read project, return the number of .kcl files,
|
||||||
|
// both in the root directory and in sub-directories,
|
||||||
|
// and folders that contain at least one .kcl file
|
||||||
|
export function getPartsCount(project: FileEntry[]) {
|
||||||
|
const flatProject = deepFileFilterFlat(project, isRelevantFileOrDir)
|
||||||
|
|
||||||
|
const kclFileCount = flatProject.filter((f) =>
|
||||||
|
f.name?.endsWith(FILE_EXT)
|
||||||
|
).length
|
||||||
|
const kclDirCount = flatProject.filter((f) => f.children !== undefined).length
|
||||||
|
|
||||||
|
return {
|
||||||
|
kclFileCount,
|
||||||
|
kclDirCount,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines if a file or directory is relevant to the project
|
||||||
|
// i.e. not a hidden file or directory, and is a relevant file type
|
||||||
|
// or contains at least one relevant file (even if it's nested)
|
||||||
|
// or is a completely empty directory
|
||||||
|
export function isRelevantFileOrDir(fileOrDir: FileEntry) {
|
||||||
|
let isRelevantDir = false
|
||||||
|
if ('children' in fileOrDir && fileOrDir.children !== undefined) {
|
||||||
|
isRelevantDir =
|
||||||
|
!isHidden(fileOrDir) &&
|
||||||
|
(fileOrDir.children.some(isRelevantFileOrDir) ||
|
||||||
|
fileOrDir.children.length === 0)
|
||||||
|
}
|
||||||
|
const isRelevantFile =
|
||||||
|
!isHidden(fileOrDir) &&
|
||||||
|
RELEVANT_FILE_TYPES.some((ext) => fileOrDir.name?.endsWith(ext))
|
||||||
|
|
||||||
|
return (
|
||||||
|
(isDir(fileOrDir) && isRelevantDir) || (!isDir(fileOrDir) && isRelevantFile)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Deeply sort the files and directories in a project like VS Code does:
|
// Deeply sort the files and directories in a project like VS Code does:
|
||||||
// The main.kcl file is always first, then files, then directories
|
// The main.kcl file is always first, then files, then directories
|
||||||
// Files and directories are sorted alphabetically
|
// Files and directories are sorted alphabetically
|
||||||
@ -53,6 +279,47 @@ export function sortProject(project: FileEntry[]): FileEntry[] {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Creates a new file in the default directory with the default project name
|
||||||
|
// Returns the path to the new file
|
||||||
|
export async function createNewProject(
|
||||||
|
path: string,
|
||||||
|
initCode = ''
|
||||||
|
): Promise<ProjectWithEntryPointMetadata> {
|
||||||
|
if (!isTauri) {
|
||||||
|
throw new Error('createNewProject() can only be called from a Tauri app')
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirExists = await exists(path)
|
||||||
|
if (!dirExists) {
|
||||||
|
await mkdir(path, { recursive: true }).catch((err) => {
|
||||||
|
console.error('Error creating new directory:', err)
|
||||||
|
throw err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeTextFile(await join(path, PROJECT_ENTRYPOINT), initCode).catch(
|
||||||
|
(err) => {
|
||||||
|
console.error('Error creating new file:', err)
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const m = await stat(path)
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: path.slice(path.lastIndexOf(sep()) + 1),
|
||||||
|
path: path,
|
||||||
|
entrypointMetadata: m,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: PROJECT_ENTRYPOINT,
|
||||||
|
path: await join(path, PROJECT_ENTRYPOINT),
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// create a regex to match the project name
|
// create a regex to match the project name
|
||||||
// replacing any instances of "$n" with a regex to match any number
|
// replacing any instances of "$n" with a regex to match any number
|
||||||
function interpolateProjectName(projectName: string) {
|
function interpolateProjectName(projectName: string) {
|
||||||
@ -106,6 +373,55 @@ function getPaddedIdentifierRegExp() {
|
|||||||
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
return new RegExp(`${escapedIdentifier}(${escapedIdentifier.slice(-1)}*)`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserSettingsFilePath(
|
||||||
|
filename: string = SETTINGS_FILE_EXT
|
||||||
|
) {
|
||||||
|
const dir = await appConfigDir()
|
||||||
|
return await join(dir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readSettingsFile(
|
||||||
|
path: string
|
||||||
|
): Promise<Partial<SaveSettingsPayload>> {
|
||||||
|
const dir = path.slice(0, path.lastIndexOf(sep()))
|
||||||
|
|
||||||
|
const dirExists = await exists(dir)
|
||||||
|
if (!dirExists) {
|
||||||
|
await mkdir(dir, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsExist = dirExists ? await exists(path) : false
|
||||||
|
|
||||||
|
if (!settingsExist) {
|
||||||
|
console.log(`Settings file does not exist at ${path}`)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await initPromise
|
||||||
|
const settings = await readTextFile(path)
|
||||||
|
// We expect the settings to be under a top-level [settings] key
|
||||||
|
return tomlParse(settings).settings as Partial<SaveSettingsPayload>
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error reading settings file:', e)
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getSettingsFilePaths(
|
||||||
|
projectPath?: string
|
||||||
|
): Promise<Partial<Record<SettingsLevel, string>>> {
|
||||||
|
const { user, project } = await getSettingsFolderPaths(projectPath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
user: user + 'user' + SETTINGS_FILE_EXT,
|
||||||
|
project:
|
||||||
|
project !== undefined
|
||||||
|
? project + (isTauri() ? sep() : '/') + 'project' + SETTINGS_FILE_EXT
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getSettingsFolderPaths(projectPath?: string) {
|
export async function getSettingsFolderPaths(projectPath?: string) {
|
||||||
const user = isTauri() ? await appConfigDir() : '/'
|
const user = isTauri() ? await appConfigDir() : '/'
|
||||||
const project = projectPath !== undefined ? projectPath : undefined
|
const project = projectPath !== undefined ? projectPath : undefined
|
||||||
@ -117,15 +433,18 @@ export async function getSettingsFolderPaths(projectPath?: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function createAndOpenNewProject(
|
export async function createAndOpenNewProject(
|
||||||
|
projectDirectory: string,
|
||||||
navigate: (path: string) => void
|
navigate: (path: string) => void
|
||||||
) {
|
) {
|
||||||
const configuration = await readAppSettingsFile()
|
const projects = await getProjectsInDir(projectDirectory)
|
||||||
const projects = await listProjects(configuration)
|
const nextIndex = await getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
||||||
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
|
||||||
const name = interpolateProjectNameWithIndex(
|
const name = interpolateProjectNameWithIndex(
|
||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
nextIndex
|
nextIndex
|
||||||
)
|
)
|
||||||
const newFile = await createNewProjectDirectory(name, bracket, configuration)
|
const newFile = await createNewProject(
|
||||||
|
await join(projectDirectory, name),
|
||||||
|
bracket
|
||||||
|
)
|
||||||
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
|
||||||
}
|
}
|
||||||
|
@ -1,26 +1,9 @@
|
|||||||
import { AppTheme } from 'wasm-lib/kcl/bindings/AppTheme'
|
|
||||||
|
|
||||||
export enum Themes {
|
export enum Themes {
|
||||||
Light = 'light',
|
Light = 'light',
|
||||||
Dark = 'dark',
|
Dark = 'dark',
|
||||||
System = 'system',
|
System = 'system',
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appThemeToTheme(
|
|
||||||
theme: AppTheme | undefined
|
|
||||||
): Themes | undefined {
|
|
||||||
switch (theme) {
|
|
||||||
case 'light':
|
|
||||||
return Themes.Light
|
|
||||||
case 'dark':
|
|
||||||
return Themes.Dark
|
|
||||||
case 'system':
|
|
||||||
return Themes.System
|
|
||||||
default:
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the theme from the system settings manually
|
// Get the theme from the system settings manually
|
||||||
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
export function getSystemTheme(): Exclude<Themes, 'system'> {
|
||||||
return typeof globalThis.window !== 'undefined' &&
|
return typeof globalThis.window !== 'undefined' &&
|
||||||
|
@ -1,22 +1,35 @@
|
|||||||
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
import { type FileInfo } from '@tauri-apps/plugin-fs'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
|
|
||||||
|
|
||||||
export type IndexLoaderData = {
|
export type IndexLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
project?: Project
|
project?: ProjectWithEntryPointMetadata
|
||||||
file?: FileEntry
|
file?: FileEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FileLoaderData = {
|
export type FileLoaderData = {
|
||||||
code: string | null
|
code: string | null
|
||||||
project?: FileEntry | Project
|
project?: FileEntry | ProjectWithEntryPointMetadata
|
||||||
file?: FileEntry
|
file?: FileEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ProjectWithEntryPointMetadata = FileEntry & {
|
||||||
|
entrypointMetadata: FileInfo
|
||||||
|
}
|
||||||
export type HomeLoaderData = {
|
export type HomeLoaderData = {
|
||||||
projects: Project[]
|
projects: ProjectWithEntryPointMetadata[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// From https://github.com/tauri-apps/tauri/blob/1.x/tooling/api/src/fs.ts#L159
|
||||||
|
// Removed from tauri v2
|
||||||
|
export interface FileEntry {
|
||||||
|
path: string
|
||||||
|
/**
|
||||||
|
* Name of the directory/file
|
||||||
|
* can be null if the path terminates with `..`
|
||||||
|
*/
|
||||||
|
name?: string
|
||||||
|
/** Children of this entry if it's a directory; null otherwise */
|
||||||
|
children?: FileEntry[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272
|
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272
|
||||||
|
@ -2,8 +2,8 @@ import { createMachine, assign } from 'xstate'
|
|||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import withBaseURL from '../lib/withBaseURL'
|
import withBaseURL from '../lib/withBaseURL'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { VITE_KC_API_BASE_URL } from 'env'
|
import { VITE_KC_API_BASE_URL } from 'env'
|
||||||
import { getUser as getUserTauri } from 'lib/tauri'
|
|
||||||
|
|
||||||
const SKIP_AUTH =
|
const SKIP_AUTH =
|
||||||
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
|
||||||
@ -129,7 +129,10 @@ async function getUser(context: UserContext) {
|
|||||||
})
|
})
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.catch((err) => console.error('error from Browser getUser', err))
|
.catch((err) => console.error('error from Browser getUser', err))
|
||||||
: getUserTauri(context.token, VITE_KC_API_BASE_URL)
|
: invoke<Models['User_type'] | Record<'error_code', unknown>>('get_user', {
|
||||||
|
token: context.token,
|
||||||
|
hostname: VITE_KC_API_BASE_URL,
|
||||||
|
}).catch((err) => console.error('error from Tauri getUser', err))
|
||||||
|
|
||||||
const user = await userPromise
|
const user = await userPromise
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
import type { FileEntry } from 'lib/types'
|
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
export const fileMachine = createMachine(
|
export const fileMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -10,7 +9,7 @@ export const fileMachine = createMachine(
|
|||||||
initial: 'Reading files',
|
initial: 'Reading files',
|
||||||
|
|
||||||
context: {
|
context: {
|
||||||
project: {} as Project,
|
project: {} as ProjectWithEntryPointMetadata,
|
||||||
selectedDirectory: {} as FileEntry,
|
selectedDirectory: {} as FileEntry,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -155,7 +154,7 @@ export const fileMachine = createMachine(
|
|||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'done.invoke.read-files'
|
type: 'done.invoke.read-files'
|
||||||
data: Project
|
data: ProjectWithEntryPointMetadata
|
||||||
}
|
}
|
||||||
| { type: 'assign'; data: { [key: string]: any } }
|
| { type: 'assign'; data: { [key: string]: any } }
|
||||||
| { type: 'Refresh' },
|
| { type: 'Refresh' },
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { assign, createMachine } from 'xstate'
|
import { assign, createMachine } from 'xstate'
|
||||||
|
import { type ProjectWithEntryPointMetadata } from 'lib/types'
|
||||||
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
|
|
||||||
export const homeMachine = createMachine(
|
export const homeMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -10,7 +10,7 @@ export const homeMachine = createMachine(
|
|||||||
initial: 'Reading projects',
|
initial: 'Reading projects',
|
||||||
|
|
||||||
context: {
|
context: {
|
||||||
projects: [] as Project[],
|
projects: [] as ProjectWithEntryPointMetadata[],
|
||||||
defaultProjectName: '',
|
defaultProjectName: '',
|
||||||
defaultDirectory: '',
|
defaultDirectory: '',
|
||||||
},
|
},
|
||||||
@ -145,7 +145,7 @@ export const homeMachine = createMachine(
|
|||||||
| { type: 'navigate'; data: { name: string } }
|
| { type: 'navigate'; data: { name: string } }
|
||||||
| {
|
| {
|
||||||
type: 'done.invoke.read-projects'
|
type: 'done.invoke.read-projects'
|
||||||
data: Project[]
|
data: ProjectWithEntryPointMetadata[]
|
||||||
}
|
}
|
||||||
| { type: 'assign'; data: { [key: string]: any } },
|
| { type: 'assign'; data: { [key: string]: any } },
|
||||||
},
|
},
|
||||||
@ -157,7 +157,7 @@ export const homeMachine = createMachine(
|
|||||||
{
|
{
|
||||||
actions: {
|
actions: {
|
||||||
setProjects: assign((_, event) => {
|
setProjects: assign((_, event) => {
|
||||||
return { projects: event.data as Project[] }
|
return { projects: event.data as ProjectWithEntryPointMetadata[] }
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import { FormEvent, useEffect } from 'react'
|
import { FormEvent, useEffect } from 'react'
|
||||||
import { remove, rename } from '@tauri-apps/plugin-fs'
|
import { remove, rename } from '@tauri-apps/plugin-fs'
|
||||||
import {
|
import {
|
||||||
|
createNewProject,
|
||||||
getNextProjectIndex,
|
getNextProjectIndex,
|
||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
doesProjectNameNeedInterpolated,
|
doesProjectNameNeedInterpolated,
|
||||||
|
getProjectsInDir,
|
||||||
} from '../lib/tauriFS'
|
} from '../lib/tauriFS'
|
||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
|
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
|
||||||
@ -12,7 +14,10 @@ import { AppHeader } from '../components/AppHeader'
|
|||||||
import ProjectCard from '../components/ProjectCard'
|
import ProjectCard from '../components/ProjectCard'
|
||||||
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { type HomeLoaderData } from 'lib/types'
|
import {
|
||||||
|
type ProjectWithEntryPointMetadata,
|
||||||
|
type HomeLoaderData,
|
||||||
|
} from 'lib/types'
|
||||||
import Loading from '../components/Loading'
|
import Loading from '../components/Loading'
|
||||||
import { useMachine } from '@xstate/react'
|
import { useMachine } from '@xstate/react'
|
||||||
import { homeMachine } from '../machines/homeMachine'
|
import { homeMachine } from '../machines/homeMachine'
|
||||||
@ -34,8 +39,6 @@ import { kclManager } from 'lib/singletons'
|
|||||||
import { useLspContext } from 'components/LspProvider'
|
import { useLspContext } from 'components/LspProvider'
|
||||||
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
import { useRefreshSettings } from 'hooks/useRefreshSettings'
|
||||||
import { LowerRightControls } from 'components/LowerRightControls'
|
import { LowerRightControls } from 'components/LowerRightControls'
|
||||||
import { Project } from 'wasm-lib/kcl/bindings/Project'
|
|
||||||
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
|
|
||||||
|
|
||||||
// This route only opens in the Tauri desktop context for now,
|
// This route only opens in the Tauri desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
// as defined in Router.tsx, so we can use the Tauri APIs and types.
|
||||||
@ -91,7 +94,7 @@ const Home = () => {
|
|||||||
},
|
},
|
||||||
services: {
|
services: {
|
||||||
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
|
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
|
||||||
listProjects(),
|
getProjectsInDir(context.defaultDirectory),
|
||||||
createProject: async (
|
createProject: async (
|
||||||
context: ContextFrom<typeof homeMachine>,
|
context: ContextFrom<typeof homeMachine>,
|
||||||
event: EventFrom<typeof homeMachine, 'Create project'>
|
event: EventFrom<typeof homeMachine, 'Create project'>
|
||||||
@ -107,7 +110,7 @@ const Home = () => {
|
|||||||
name = interpolateProjectNameWithIndex(name, nextIndex)
|
name = interpolateProjectNameWithIndex(name, nextIndex)
|
||||||
}
|
}
|
||||||
|
|
||||||
await createNewProjectDirectory(name)
|
await createNewProject(await join(context.defaultDirectory, name))
|
||||||
|
|
||||||
return `Successfully created "${name}"`
|
return `Successfully created "${name}"`
|
||||||
},
|
},
|
||||||
@ -178,7 +181,7 @@ const Home = () => {
|
|||||||
|
|
||||||
async function handleRenameProject(
|
async function handleRenameProject(
|
||||||
e: FormEvent<HTMLFormElement>,
|
e: FormEvent<HTMLFormElement>,
|
||||||
project: Project
|
project: ProjectWithEntryPointMetadata
|
||||||
) {
|
) {
|
||||||
const { newProjectName } = Object.fromEntries(
|
const { newProjectName } = Object.fromEntries(
|
||||||
new FormData(e.target as HTMLFormElement)
|
new FormData(e.target as HTMLFormElement)
|
||||||
@ -189,7 +192,7 @@ const Home = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteProject(project: Project) {
|
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
|
||||||
send('Delete project', { data: { name: project.name || '' } })
|
send('Delete project', { data: { name: project.name || '' } })
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,7 +284,7 @@ const Home = () => {
|
|||||||
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
|
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
|
||||||
data-testid="home-new-file"
|
data-testid="home-new-file"
|
||||||
>
|
>
|
||||||
New project
|
New file
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -14,11 +14,7 @@ export default function CodeEditor() {
|
|||||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black opacity-50 dark:opacity-80 pointer-events-none"
|
className="fixed inset-0 bg-black opacity-50 dark:opacity-80 pointer-events-none"
|
||||||
style={
|
style={{ clipPath: useBackdropHighlight('code-pane') }}
|
||||||
{
|
|
||||||
/*clipPath: useBackdropHighlight('code-pane')*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
@ -15,11 +15,7 @@ export default function InteractiveNumbers() {
|
|||||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black opacity-50 pointer-events-none"
|
className="fixed inset-0 bg-black opacity-50 pointer-events-none"
|
||||||
style={
|
style={{ clipPath: useBackdropHighlight('code-pane') }}
|
||||||
{
|
|
||||||
/*clipPath: useBackdropHighlight('code-pane')*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
@ -4,7 +4,9 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
|||||||
import { Themes, getSystemTheme } from 'lib/theme'
|
import { Themes, getSystemTheme } from 'lib/theme'
|
||||||
import { bracket } from 'lib/exampleKcl'
|
import { bracket } from 'lib/exampleKcl'
|
||||||
import {
|
import {
|
||||||
|
createNewProject,
|
||||||
getNextProjectIndex,
|
getNextProjectIndex,
|
||||||
|
getProjectsInDir,
|
||||||
interpolateProjectNameWithIndex,
|
interpolateProjectNameWithIndex,
|
||||||
} from 'lib/tauriFS'
|
} from 'lib/tauriFS'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
@ -18,21 +20,33 @@ import {
|
|||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
PROJECT_ENTRYPOINT,
|
PROJECT_ENTRYPOINT,
|
||||||
} from 'lib/constants'
|
} from 'lib/constants'
|
||||||
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
|
|
||||||
|
|
||||||
function OnboardingWithNewFile() {
|
function OnboardingWithNewFile() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dismiss = useDismiss()
|
const dismiss = useDismiss()
|
||||||
const next = useNextClick(onboardingPaths.INDEX)
|
const next = useNextClick(onboardingPaths.INDEX)
|
||||||
|
const {
|
||||||
|
settings: {
|
||||||
|
context: {
|
||||||
|
app: { projectDirectory },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} = useSettingsAuthContext()
|
||||||
|
|
||||||
async function createAndOpenNewProject() {
|
async function createAndOpenNewProject() {
|
||||||
const projects = await listProjects()
|
const projects = await getProjectsInDir(projectDirectory.current)
|
||||||
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
|
const nextIndex = await getNextProjectIndex(
|
||||||
|
ONBOARDING_PROJECT_NAME,
|
||||||
|
projects
|
||||||
|
)
|
||||||
const name = interpolateProjectNameWithIndex(
|
const name = interpolateProjectNameWithIndex(
|
||||||
ONBOARDING_PROJECT_NAME,
|
ONBOARDING_PROJECT_NAME,
|
||||||
nextIndex
|
nextIndex
|
||||||
)
|
)
|
||||||
const newFile = await createNewProjectDirectory(name, bracket)
|
const newFile = await createNewProject(
|
||||||
|
await join(projectDirectory.current, name),
|
||||||
|
bracket
|
||||||
|
)
|
||||||
navigate(
|
navigate(
|
||||||
`${paths.FILE}/${encodeURIComponent(
|
`${paths.FILE}/${encodeURIComponent(
|
||||||
await join(newFile.path, PROJECT_ENTRYPOINT)
|
await join(newFile.path, PROJECT_ENTRYPOINT)
|
||||||
|
@ -31,11 +31,7 @@ export default function ParametricModeling() {
|
|||||||
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 bg-black dark:bg-black-80 opacity-50 pointer-events-none"
|
className="fixed inset-0 bg-black dark:bg-black-80 opacity-50 pointer-events-none"
|
||||||
style={
|
style={{ clipPath: useBackdropHighlight('code-pane') }}
|
||||||
{
|
|
||||||
/*clipPath: useBackdropHighlight('code-pane')*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
|
@ -10,10 +10,15 @@ import { useHotkeys } from 'react-hotkeys-hook'
|
|||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
import { useDotDotSlash } from 'hooks/useDotDotSlash'
|
||||||
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
|
import {
|
||||||
|
createAndOpenNewProject,
|
||||||
|
getInitialDefaultDir,
|
||||||
|
getSettingsFolderPaths,
|
||||||
|
} from 'lib/tauriFS'
|
||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
import { isTauri } from 'lib/isTauri'
|
import { isTauri } from 'lib/isTauri'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import React, { Fragment, useMemo, useRef, useState } from 'react'
|
import React, { Fragment, useMemo, useRef, useState } from 'react'
|
||||||
import { Setting } from 'lib/settings/initialSettings'
|
import { Setting } from 'lib/settings/initialSettings'
|
||||||
import decamelize from 'decamelize'
|
import decamelize from 'decamelize'
|
||||||
@ -26,7 +31,6 @@ import {
|
|||||||
shouldHideSetting,
|
shouldHideSetting,
|
||||||
shouldShowSettingInput,
|
shouldShowSettingInput,
|
||||||
} from 'lib/settings/settingsUtils'
|
} from 'lib/settings/settingsUtils'
|
||||||
import { getInitialDefaultDir, showInFolder } from 'lib/tauri'
|
|
||||||
|
|
||||||
export const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
|
export const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
|
||||||
|
|
||||||
@ -66,7 +70,7 @@ export const Settings = () => {
|
|||||||
if (isFileSettings) {
|
if (isFileSettings) {
|
||||||
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
|
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
|
||||||
} else {
|
} else {
|
||||||
createAndOpenNewProject(navigate)
|
createAndOpenNewProject(context.app.projectDirectory.current, navigate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -298,7 +302,9 @@ export const Settings = () => {
|
|||||||
? decodeURIComponent(projectPath)
|
? decodeURIComponent(projectPath)
|
||||||
: undefined
|
: undefined
|
||||||
)
|
)
|
||||||
showInFolder(paths[settingsLevel])
|
void invoke('show_in_folder', {
|
||||||
|
path: paths[settingsLevel],
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
icon={{
|
icon={{
|
||||||
icon: 'folder',
|
icon: 'folder',
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ActionButton } from '../components/ActionButton'
|
import { ActionButton } from '../components/ActionButton'
|
||||||
import { isTauri } from '../lib/isTauri'
|
import { isTauri } from '../lib/isTauri'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
import { VITE_KC_SITE_BASE_URL, VITE_KC_API_BASE_URL } from '../env'
|
||||||
import { Themes, getSystemTheme } from '../lib/theme'
|
import { Themes, getSystemTheme } from '../lib/theme'
|
||||||
import { paths } from 'lib/paths'
|
import { paths } from 'lib/paths'
|
||||||
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
|
||||||
import { APP_NAME } from 'lib/constants'
|
import { APP_NAME } from 'lib/constants'
|
||||||
import { login } from 'lib/tauri'
|
|
||||||
|
|
||||||
const SignIn = () => {
|
const SignIn = () => {
|
||||||
const {
|
const {
|
||||||
@ -28,7 +28,9 @@ const SignIn = () => {
|
|||||||
const signInTauri = async () => {
|
const signInTauri = async () => {
|
||||||
// We want to invoke our command to login via device auth.
|
// We want to invoke our command to login via device auth.
|
||||||
try {
|
try {
|
||||||
const token: string = await login(VITE_KC_API_BASE_URL)
|
const token: string = await invoke('login', {
|
||||||
|
host: VITE_KC_API_BASE_URL,
|
||||||
|
})
|
||||||
send({ type: 'Log in', token })
|
send({ type: 'Log in', token })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error with login button', error)
|
console.error('Error with login button', error)
|
||||||
|
@ -9,7 +9,6 @@ import {
|
|||||||
import { enginelessExecutor } from './lib/testHelpers'
|
import { enginelessExecutor } from './lib/testHelpers'
|
||||||
import { EngineCommandManager } from './lang/std/engineConnection'
|
import { EngineCommandManager } from './lang/std/engineConnection'
|
||||||
import { KCLError } from './lang/errors'
|
import { KCLError } from './lang/errors'
|
||||||
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
|
|
||||||
|
|
||||||
export type ToolTip =
|
export type ToolTip =
|
||||||
| 'lineTo'
|
| 'lineTo'
|
||||||
@ -45,6 +44,14 @@ export const toolTips = [
|
|||||||
'tangentialArcTo',
|
'tangentialArcTo',
|
||||||
] as any as ToolTip[]
|
] as any as ToolTip[]
|
||||||
|
|
||||||
|
export type PaneType =
|
||||||
|
| 'code'
|
||||||
|
| 'variables'
|
||||||
|
| 'debug'
|
||||||
|
| 'kclErrors'
|
||||||
|
| 'logs'
|
||||||
|
| 'lspMessages'
|
||||||
|
|
||||||
export interface StoreState {
|
export interface StoreState {
|
||||||
mediaStream?: MediaStream
|
mediaStream?: MediaStream
|
||||||
setMediaStream: (mediaStream: MediaStream) => void
|
setMediaStream: (mediaStream: MediaStream) => void
|
||||||
@ -70,8 +77,8 @@ export interface StoreState {
|
|||||||
|
|
||||||
showHomeMenu: boolean
|
showHomeMenu: boolean
|
||||||
setHomeShowMenu: (showMenu: boolean) => void
|
setHomeShowMenu: (showMenu: boolean) => void
|
||||||
openPanes: SidebarType[]
|
openPanes: PaneType[]
|
||||||
setOpenPanes: (panes: SidebarType[]) => void
|
setOpenPanes: (panes: PaneType[]) => void
|
||||||
homeMenuItems: {
|
homeMenuItems: {
|
||||||
name: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
|
92
src/wasm-lib/Cargo.lock
generated
@ -240,9 +240,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-recursion"
|
name = "async-recursion"
|
||||||
version = "1.1.1"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
checksum = "30c5ef0ede93efbf733c1a727f3b6b5a1060bbedd5600183e66f6e4be4af0ec5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@ -582,7 +582,7 @@ dependencies = [
|
|||||||
"anstream",
|
"anstream",
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"clap_lex",
|
"clap_lex",
|
||||||
"strsim 0.11.0",
|
"strsim",
|
||||||
"unicase",
|
"unicase",
|
||||||
"unicode-width",
|
"unicode-width",
|
||||||
]
|
]
|
||||||
@ -849,41 +849,6 @@ dependencies = [
|
|||||||
"syn 2.0.60",
|
"syn 2.0.60",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling"
|
|
||||||
version = "0.20.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"darling_macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_core"
|
|
||||||
version = "0.20.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f"
|
|
||||||
dependencies = [
|
|
||||||
"fnv",
|
|
||||||
"ident_case",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"strsim 0.10.0",
|
|
||||||
"syn 2.0.60",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "darling_macro"
|
|
||||||
version = "0.20.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f"
|
|
||||||
dependencies = [
|
|
||||||
"darling_core",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.60",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@ -962,7 +927,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
version = "0.1.17"
|
version = "0.1.16"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Inflector",
|
"Inflector",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
@ -1699,12 +1664,6 @@ dependencies = [
|
|||||||
"cc",
|
"cc",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ident_case"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1895,7 +1854,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kcl-lib"
|
name = "kcl-lib"
|
||||||
version = "0.1.53"
|
version = "0.1.50"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"approx 0.5.1",
|
"approx 0.5.1",
|
||||||
@ -1936,13 +1895,11 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite",
|
"tokio-tungstenite",
|
||||||
"toml",
|
|
||||||
"tower-lsp",
|
"tower-lsp",
|
||||||
"ts-rs",
|
"ts-rs",
|
||||||
"twenty-twenty",
|
"twenty-twenty",
|
||||||
"url",
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
"validator",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
@ -1974,7 +1931,6 @@ dependencies = [
|
|||||||
"bigdecimal",
|
"bigdecimal",
|
||||||
"bytes",
|
"bytes",
|
||||||
"chrono",
|
"chrono",
|
||||||
"clap",
|
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"format_serde_error",
|
"format_serde_error",
|
||||||
"futures",
|
"futures",
|
||||||
@ -3765,12 +3721,6 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c"
|
checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@ -4387,7 +4337,6 @@ version = "7.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
|
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"ts-rs-macros",
|
"ts-rs-macros",
|
||||||
"url",
|
"url",
|
||||||
@ -4572,36 +4521,6 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "validator"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db79c75af171630a3148bd3e6d7c4f42b6a9a014c2945bc5ed0020cbb8d9478e"
|
|
||||||
dependencies = [
|
|
||||||
"idna",
|
|
||||||
"once_cell",
|
|
||||||
"regex",
|
|
||||||
"serde",
|
|
||||||
"serde_derive",
|
|
||||||
"serde_json",
|
|
||||||
"url",
|
|
||||||
"validator_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "validator_derive"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "55591299b7007f551ed1eb79a684af7672c19c3193fb9e0a31936987bb2438ec"
|
|
||||||
dependencies = [
|
|
||||||
"darling",
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro-error",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.60",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@ -4727,7 +4646,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bson",
|
"bson",
|
||||||
"clap",
|
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"futures",
|
"futures",
|
||||||
"gloo-utils",
|
"gloo-utils",
|
||||||
|
@ -11,7 +11,6 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
|
||||||
clap = "4.5.4"
|
|
||||||
gloo-utils = "0.2.0"
|
gloo-utils = "0.2.0"
|
||||||
kcl-lib = { path = "kcl" }
|
kcl-lib = { path = "kcl" }
|
||||||
kittycad = { workspace = true }
|
kittycad = { workspace = true }
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "derive-docs"
|
name = "derive-docs"
|
||||||
description = "A tool for generating documentation from Rust derive macros"
|
description = "A tool for generating documentation from Rust derive macros"
|
||||||
version = "0.1.17"
|
version = "0.1.16"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://github.com/KittyCAD/modeling-app"
|
repository = "https://github.com/KittyCAD/modeling-app"
|
||||||
|
@ -775,11 +775,16 @@ fn generate_code_block_test(
|
|||||||
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
if let Ok(addr) = std::env::var("LOCAL_ENGINE_ADDR") {
|
||||||
client.set_base_url(addr);
|
client.set_base_url(addr);
|
||||||
}
|
}
|
||||||
|
let ws = client
|
||||||
|
.modeling()
|
||||||
|
.commands_ws(None, None, None, None, None,None, Some(false))
|
||||||
|
.await.unwrap();
|
||||||
|
|
||||||
let tokens = crate::token::lexer(#code_block).unwrap();
|
let tokens = crate::token::lexer(#code_block).unwrap();
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default()).await.unwrap();
|
let units = kittycad::types::UnitLength::Mm;
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(ws, units.clone()).await.unwrap();
|
||||||
|
|
||||||
ctx.run(program, None).await.unwrap();
|
ctx.run(program, None).await.unwrap();
|
||||||
|
|
||||||
|
@ -20,10 +20,16 @@ mod test_examples_show {
|
|||||||
client.set_base_url(addr);
|
client.set_base_url(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ws = client
|
||||||
|
.modeling()
|
||||||
|
.commands_ws(None, None, None, None, None, None, Some(false))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap();
|
let tokens = crate::token::lexer("This is another code block.\nyes sirrr.\nshow").unwrap();
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
let units = kittycad::types::UnitLength::Mm;
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ctx.run(program, None).await.unwrap();
|
ctx.run(program, None).await.unwrap();
|
||||||
@ -91,10 +97,16 @@ mod test_examples_show {
|
|||||||
client.set_base_url(addr);
|
client.set_base_url(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ws = client
|
||||||
|
.modeling()
|
||||||
|
.commands_ws(None, None, None, None, None, None, Some(false))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
let units = kittycad::types::UnitLength::Mm;
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ctx.run(program, None).await.unwrap();
|
ctx.run(program, None).await.unwrap();
|
||||||
|
@ -20,10 +20,16 @@ mod test_examples_show {
|
|||||||
client.set_base_url(addr);
|
client.set_base_url(addr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let ws = client
|
||||||
|
.modeling()
|
||||||
|
.commands_ws(None, None, None, None, None, None, Some(false))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
let tokens = crate::token::lexer("This is code.\nIt does other shit.\nshow").unwrap();
|
||||||
let parser = crate::parser::Parser::new(tokens);
|
let parser = crate::parser::Parser::new(tokens);
|
||||||
let program = parser.ast().unwrap();
|
let program = parser.ast().unwrap();
|
||||||
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
|
let units = kittycad::types::UnitLength::Mm;
|
||||||
|
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
ctx.run(program, None).await.unwrap();
|
ctx.run(program, None).await.unwrap();
|
||||||
|