Compare commits

...

24 Commits

Author SHA1 Message Date
cd68f80b71 bump version to 0.19.4 (#2262)
bump version

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 21:33:05 -07:00
d341681c0d get rid of code pane shit (#2259)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 21:09:08 -07:00
0578e9d2a1 Bump actions/upload-artifact from 2 to 3 (#2260)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2 to 3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2...v3)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 21:00:59 -07:00
b413538e9e fetch wasm bundle locally (#2257) 2024-04-25 23:59:10 +00:00
c4e7754fc5 fix for relative path (#2252)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 22:53:44 +00:00
94515b5490 more speed up wasm build (#2254)
* more spped up wasm build

* try download again

* clean up

* rando change to rust to check it builds wasm

* make workflow more grokable/commented

* Revert "rando change to rust to check it builds wasm"

This reverts commit d2d9926b4b.
2024-04-25 22:27:28 +00:00
aa52407fda Cut release v0.19.3 (#2251) 2024-04-25 13:28:42 -07:00
e45be831d0 Pass the ?pool query param through to the backend. (#2246)
Pass the ?pool query param through to the backend.

This will slice off the ?pool= param and pass it to the WebSocket
request, which requests that the Zoo API use a particular pool of
engines. This isn't something any users of the zoo api require; but it's
needed for the internal engine Zoo development workflow. This may be
used in the future, but for now this'll be always enabled. Passing any
value in the production servers will result in a "no backend" error for
now.
2024-04-25 19:51:33 +00:00
005944f3a3 fix the updater (#2250)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 12:41:22 -07:00
755ef8ce7f download-wasm if there's no rust changes (#2234)
* download-wasm if there's no rust changes

* typo

* typo

* artifact stuff

* add needs

* permissions

* hmm

* more logic

* same for ubuntu
2024-04-26 05:37:32 +10:00
005d1f0ca7 Filter files and folders that start with a . (#2249) 2024-04-25 19:01:50 +00:00
e158f6f513 Better rust parsing of route uris for files (#2248)
* refactors

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fiex;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 18:55:11 +00:00
879d7ec4f4 Cut release v0.19.2 (#2247) 2024-04-25 14:38:25 -04:00
f6838b9b14 always ensure the dirs exist (#2245)
Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 17:07:24 +00:00
cb75c47631 fix env vars for lsp server to match other .env vars (#2243)
fix env vars for lsp

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 16:41:39 +00:00
9b95ec1083 fix relevant extensions (#2241)
* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 08:36:45 -07:00
a3eeff65c8 Cut release v0.19.1 (#2240) 2024-04-25 10:40:31 -04:00
fab3d2b130 Make FileTree a pane (desktop only) (#2232) 2024-04-25 09:56:55 -04:00
0a96dc6fd2 Project state improvements (#2239) 2024-04-25 12:52:08 +00:00
e123a00d4b Cut release v0.19.0 (#2206)
Co-authored-by: Kurt Hutten <k.hutten@protonmail.ch>
2024-04-25 07:30:49 -04:00
b950cc0583 able to go to and from app settings to executor settings for cli (#2236)
* settings from and into exefcutor settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* ipdates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* ensure all have extrudes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 09:31:18 +00:00
c89780a489 upload artifact to same action (#2235) 2024-04-25 17:43:48 +10:00
1afed68dd7 Settings move to rust (for read/write from files) (#2220)
* start of settings types

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add validator

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* start of settings in rust

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix wasm

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix wasm

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more tests

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* derive docs

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* configuration

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* read and write functions with migration

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* make more dry

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more parsing of app settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* more things

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* trim end

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* project settings

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixes

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup tauri commands

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* refactor

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* refactor

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* change to files

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* better

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup more

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* get rid of dead code

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fixed

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* cleanup some more shit

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* updates

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* add validation

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validation

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validate

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* validate

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippuy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* clippuy

Signed-off-by: Jess Frazelle <github@jessfraz.com>

* fix;

Signed-off-by: Jess Frazelle <github@jessfraz.com>

---------

Signed-off-by: Jess Frazelle <github@jessfraz.com>
2024-04-25 07:13:09 +00:00
dcbed4f06f build and store wasm (#2233) 2024-04-25 07:01:42 +00:00
142 changed files with 5213 additions and 2225 deletions

View File

@ -0,0 +1,33 @@
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

View File

@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
dir: ['src/wasm-lib']
dir: ['src/wasm-lib', 'src-tauri']
steps:
- uses: actions/checkout@v4
- name: Install latest rust
@ -31,9 +31,22 @@ jobs:
- name: install dependencies
if: matrix.dir == 'src-tauri'
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
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
yarn install
yarn build:wasm
yarn build:local
- name: Rust Cache
uses: Swatinem/rust-cache@v2.6.1

57
.github/workflows/cargo-test-tauri.yml vendored Normal file
View File

@ -0,0 +1,57 @@
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

View File

@ -50,7 +50,7 @@ jobs:
- run: yarn tsc
check-typos:
check-typos:
runs-on: ubuntu-latest
steps:
- name: Checkout
@ -98,7 +98,7 @@ jobs:
with:
node-version-file: '.nvmrc'
cache: 'yarn'
- name: Set nightly version
if: github.event_name == 'schedule'
run: |
@ -143,21 +143,21 @@ jobs:
ls -l artifact
cp artifact/package.json package.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
if: matrix.os == 'ubuntu-latest'
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
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: Sync node version and setup cache
uses: actions/setup-node@v4
@ -383,7 +383,7 @@ jobs:
uses: softprops/action-gh-release@v2
with:
files: 'artifact/*/Zoo*'
announce_release:
needs: [publish-apps-release]
runs-on: ubuntu-latest
@ -391,17 +391,17 @@ jobs:
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install requests
- name: Announce Release
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}

View File

@ -12,11 +12,31 @@ concurrency:
permissions:
contents: write
pull-requests: write
actions: read
jobs:
check-rust-changes:
runs-on: ubuntu-latest
outputs:
rust-changed: ${{ steps.filter.outputs.rust }}
steps:
- uses: actions/checkout@v4
- id: filter
name: Check for Rust changes
uses: dorny/paths-filter@v3
with:
filters: |
rust:
- 'src/wasm-lib/**'
playwright-ubuntu:
timeout-minutes: 60
runs-on: ubuntu-latest-8-cores
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -28,13 +48,38 @@ jobs:
run: yarn
- name: Install Playwright Browsers
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
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
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
- name: build web
run: yarn build:local
@ -89,6 +134,7 @@ jobs:
playwright-macos:
timeout-minutes: 60
runs-on: macos-14
needs: check-rust-changes
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -99,13 +145,38 @@ jobs:
run: yarn
- name: Install Playwright Browsers
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
uses: dtolnay/rust-toolchain@stable
- name: Cache wasm
- name: Cache Wasm (because rust diff)
if: needs.check-rust-changes.outputs.rust-changed == 'true'
uses: Swatinem/rust-cache@v2
with:
workspaces: './src/wasm-lib'
- name: build wasm
- name: OR Cache Wasm (because wasm cache failed)
if: steps.download-wasm.outcome == 'failure'
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
- name: build web
run: yarn build:local

View File

@ -59,6 +59,10 @@ followed by:
```
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)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1068,7 +1068,7 @@
"unpublished": false,
"deprecated": false,
"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(%)"
"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, %)"
]
},
{
@ -2074,7 +2074,7 @@
"unpublished": false,
"deprecated": false,
"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(%)"
"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, %)"
]
},
{
@ -22380,8 +22380,8 @@
"unpublished": false,
"deprecated": false,
"examples": [
"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\")"
"startSketchOn('XZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)",
"startSketchOn('YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%, \"edge1\")\n |> extrude(10, %)"
]
},
{
@ -43695,7 +43695,7 @@
"unpublished": false,
"deprecated": false,
"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)"
"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, %)"
]
},
{
@ -45900,7 +45900,7 @@
"unpublished": false,
"deprecated": false,
"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 }, %)"
"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, %)"
]
},
{
@ -50459,7 +50459,7 @@
"unpublished": false,
"deprecated": false,
"examples": [
"const part = startSketchOn('XY')\n |> circle([0, 0], 2, %)\n |> patternLinear2d({\n axis: [0, 1],\n repetitions: 12,\n distance: 2\n }, %)"
"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, %)"
]
},
{
@ -59318,7 +59318,7 @@
"unpublished": false,
"deprecated": false,
"examples": [
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)"
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
]
},
{
@ -60312,7 +60312,7 @@
"unpublished": false,
"deprecated": false,
"examples": [
"startSketchAt([0, 0])\n |> line([10, 10], %)"
"startSketchAt([0, 0])\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)"
]
},
{
@ -61585,7 +61585,7 @@
"unpublished": false,
"deprecated": false,
"examples": [
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")",
"startSketchOn('XY')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %)\n |> line([20, 10], %, \"edge1\")\n |> close(%, \"edge2\")\n |> extrude(10, %)",
"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,
"deprecated": false,
"examples": [
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)"
"startSketchOn('-YZ')\n |> startProfileAt([0, 0], %)\n |> line([10, 10], %, \"edge0\")\n |> tangentialArcTo([10, 0], %)\n |> close(%)\n |> extrude(10, %)"
]
},
{

File diff suppressed because one or more lines are too long

View File

@ -596,13 +596,12 @@ test('Auto complete works', async ({ page }) => {
test('Stored settings are validated and fall back to defaults', async ({
page,
context,
}) => {
const u = getUtils(page)
// Override beforeEach test setup
// with corrupted settings
await context.addInitScript(
await page.addInitScript(
async ({ settingsKey, settings }) => {
localStorage.setItem(settingsKey, settings)
},
@ -619,18 +618,18 @@ test('Stored settings are validated and fall back to defaults', async ({
// Check the settings were reset
const storedSettings = TOML.parse(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '{}',
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
) as { settings: SaveSettingsPayload }
expect(storedSettings.settings.app?.theme).toBe('dark')
expect(storedSettings.settings?.app?.theme).toBe(undefined)
// Check that the invalid settings were removed
expect(storedSettings.settings.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings.projects?.defaultProjectName).toBe(undefined)
expect(storedSettings.settings?.modeling?.defaultUnit).toBe(undefined)
expect(storedSettings.settings?.modeling?.mouseControls).toBe(undefined)
expect(storedSettings.settings?.app?.projectDirectory).toBe(undefined)
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(undefined)
})
test('Project settings can be set and override user settings', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,7 +1,7 @@
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { Themes } from 'lib/theme'
export const TEST_SETTINGS_KEY = '/user.toml'
export const TEST_SETTINGS_KEY = '/settings.toml'
export const TEST_SETTINGS = {
app: {
theme: Themes.Dark,
@ -24,7 +24,7 @@ export const TEST_SETTINGS = {
export const TEST_SETTINGS_ONBOARDING = {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export ' },
app: { ...TEST_SETTINGS.app, onboardingStatus: '/export' },
} satisfies Partial<SaveSettingsPayload>
export const TEST_SETTINGS_CORRUPTED = {

View File

@ -71,7 +71,7 @@ describe('ZMA (Tauri, Linux)', () => {
// Now should be signed in
const newFileButton = await $('[data-testid="home-new-file"]')
expect(await newFileButton.getText()).toEqual('New file')
expect(await newFileButton.getText()).toEqual('New project')
})
it('opens the settings page, checks filesystem settings, and closes the settings page', async () => {

24
get-latest-wasm-bundle.sh Executable file
View File

@ -0,0 +1,24 @@
#!/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"

View File

@ -1,6 +1,6 @@
{
"name": "untitled-app",
"version": "0.18.1",
"version": "0.19.4",
"private": true,
"dependencies": {
"@codemirror/autocomplete": "^6.16.0",
@ -86,6 +86,7 @@
"simpleserver": "yarn pretest && http-server ./public --cors -p 3000",
"fmt": "prettier --write ./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": "(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",

View File

@ -18,7 +18,7 @@ export default defineConfig({
/* Retry on CI only */
retries: process.env.CI ? 3 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 2 : 1,
workers: process.env.CI ? 1 : 1,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */

2420
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,6 @@ repository = "https://github.com/KittyCAD/modeling-app"
default-run = "app"
edition = "2021"
rust-version = "1.70"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies]
@ -16,11 +15,12 @@ tauri-build = { version = "2.0.0-beta.13", features = [] }
[dependencies]
anyhow = "1"
kcl-lib = { version = "0.1.53", path = "../src/wasm-lib/kcl" }
kittycad = "0.3.0"
oauth2 = "4.4.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tauri = { version = "2.0.0-beta.15", features = [ "devtools", "unstable"] }
tauri-plugin-cli = { version = "2.0.0-beta.3" }
tauri-plugin-dialog = { version = "2.0.0-beta.6" }
tauri-plugin-fs = { version = "2.0.0-beta.6" }
tauri-plugin-http = { version = "2.0.0-beta.6" }
@ -28,7 +28,7 @@ tauri-plugin-os = { 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-updater = { version = "2.0.0-beta.4" }
tokio = { version = "1.37.0", features = ["time"] }
tokio = { version = "1.37.0", features = ["time", "fs"] }
toml = "0.8.2"
[features]

View File

@ -7,6 +7,7 @@
"main"
],
"permissions": [
"cli:default",
"path:default",
"event:default",
"window:default",
@ -23,7 +24,6 @@
"fs:allow-copy-file",
"fs:allow-mkdir",
"fs:allow-remove",
"fs:allow-remove",
"fs:allow-rename",
"fs:allow-exists",
"fs:allow-stat",

6
src-tauri/rustfmt.toml Normal file
View File

@ -0,0 +1,6 @@
max_width = 120
edition = "2018"
format_code_in_doc_comments = true
format_strings = false
imports_granularity = "Crate"
group_imports = "StdExternalCrate"

View File

@ -1,91 +1,225 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use std::env;
use std::fs;
use std::io::Read;
use std::path::Path;
use std::path::PathBuf;
pub(crate) mod state;
use std::{
env,
path::{Path, PathBuf},
process::Command,
};
use anyhow::Result;
use kcl_lib::settings::types::{
file::{FileEntry, Project, ProjectRoute, ProjectState},
project::ProjectConfiguration,
Configuration, DEFAULT_PROJECT_KCL_FILE,
};
use oauth2::TokenResponse;
use serde::Serialize;
use std::process::Command;
use tauri::ipc::InvokeError;
use tauri::{ipc::InvokeError, Manager};
use tauri_plugin_cli::CliExt;
use tauri_plugin_shell::ShellExt;
const DEFAULT_HOST: &str = "https://api.kittycad.io";
/// This command returns the a json string parse from a toml file at the path.
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";
#[tauri::command]
fn read_toml(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()))?;
let value =
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)
fn get_initial_default_dir(app: tauri::AppHandle) -> Result<PathBuf, InvokeError> {
let dir = match app.path().document_dir() {
Ok(dir) => dir,
Err(_) => {
// for headless Linux (eg. Github Actions)
let home_dir = app.path().home_dir()?;
home_dir.join("Documents")
}
};
Ok(dir.join(PROJECT_FOLDER))
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[derive(Debug, Serialize)]
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>>,
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
fn is_dir<P: AsRef<Path>>(path: P) -> Result<bool> {
std::fs::metadata(path)
.map(|md| md.is_dir())
.map_err(Into::into)
}
/// From https://github.com/tauri-apps/tauri/blob/1.x/core/tauri/src/api/dir.rs#L51
/// Removed from tauri v2
#[tauri::command]
fn read_dir_recursive(path: &str) -> Result<Vec<DiskEntry>, InvokeError> {
let mut files_and_dirs: Vec<DiskEntry> = vec![];
// 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();
async fn get_state(app: tauri::AppHandle) -> Result<Option<ProjectState>, InvokeError> {
let store = app.state::<state::Store>();
Ok(store.get().await)
}
if let Ok(flag) = is_dir(&path) {
files_and_dirs.push(DiskEntry {
path: path.clone(),
children: if flag {
Some(read_dir_recursive(path.to_str().expect("No path"))?)
} else {
None
},
name: path
.file_name()
.map(|name| name.to_string_lossy())
.map(|name| name.to_string()),
});
#[tauri::command]
async fn set_state(app: tauri::AppHandle, state: Option<ProjectState>) -> Result<(), InvokeError> {
let store = app.state::<state::Store>();
store.set(state).await;
Ok(())
}
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))
}
#[tauri::command]
async fn read_app_settings_file(app: tauri::AppHandle) -> Result<Configuration, InvokeError> {
let mut settings_path = get_app_settings_file_path(&app).await?;
let mut needs_migration = false;
// Check if this file exists.
if !settings_path.exists() {
// Try the backwards compatible path.
// TODO: Remove this after a few releases.
let app_config_dir = app.path().app_config_dir()?;
settings_path = format!(
"{}user.toml",
app_config_dir.display().to_string().trim_end_matches('/')
)
.into();
needs_migration = true;
// Check if this path exists.
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)
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
let mut parsed = Configuration::backwards_compatible_toml_parse(&contents).map_err(InvokeError::from_anyhow)?;
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)
}
/// This command returns a string that is the contents of a file at the path.
#[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)
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(contents)
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.
@ -103,8 +237,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
let auth_client = oauth2::basic::BasicClient::new(
oauth2::ClientId::new(client_id),
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(
oauth2::TokenUrl::new(format!("{host}/oauth2/device/token"))
.map_err(|e| InvokeError::from_anyhow(e.into()))?,
@ -132,12 +265,10 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
// and bypass the shell::open call as it fails on GitHub Actions.
let e2e_tauri_enabled = env::var("E2E_TAURI_ENABLED").is_ok();
if e2e_tauri_enabled {
println!(
"E2E_TAURI_ENABLED is set, won't open {} externally",
auth_uri.secret()
);
fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.expect("Unable to write /tmp/kittycad_user_code file");
println!("E2E_TAURI_ENABLED is set, won't open {} externally", auth_uri.secret());
tokio::fs::write("/tmp/kittycad_user_code", details.user_code().secret())
.await
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
} else {
app.shell()
.open(auth_uri.secret(), None)
@ -160,10 +291,7 @@ async fn login(app: tauri::AppHandle, host: &str) -> Result<String, InvokeError>
///This command returns the KittyCAD user info given a token.
/// The string returned from this method is the user info as a json string.
#[tauri::command]
async fn get_user(
token: Option<String>,
hostname: &str,
) -> Result<kittycad::types::User, InvokeError> {
async fn get_user(token: &str, hostname: &str) -> Result<kittycad::types::User, InvokeError> {
// Use the host passed in if it's set.
// Otherwise, use the default host.
let host = if hostname.is_empty() {
@ -183,7 +311,7 @@ async fn get_user(
println!("Getting user info...");
// use kittycad library to fetch the user info from /user/me
let mut client = kittycad::Client::new(token.unwrap());
let mut client = kittycad::Client::new(token);
if baseurl != DEFAULT_HOST {
client.set_base_url(&baseurl);
@ -202,50 +330,188 @@ async fn get_user(
/// 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.
#[tauri::command]
fn show_in_folder(path: String) {
#[cfg(target_os = "windows")]
fn show_in_folder(path: &str) -> Result<(), InvokeError> {
#[cfg(not(unix))]
{
Command::new("explorer")
.args(["/select,", &path]) // The comma after select is not a typo
.spawn()
.unwrap();
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
#[cfg(target_os = "macos")]
#[cfg(unix)]
{
Command::new("open").args(["-R", &path]).spawn().unwrap();
Command::new("open")
.args(["-R", &path])
.spawn()
.map_err(|e| InvokeError::from_anyhow(e.into()))?;
}
Ok(())
}
fn main() {
fn main() -> Result<()> {
tauri::Builder::default()
.setup(|_app| {
.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,
login,
read_dir_recursive,
show_in_folder,
read_app_settings_file,
write_app_settings_file,
read_project_settings_file,
write_project_settings_file,
])
.plugin(tauri_plugin_cli::init())
.setup(|app| {
// Do update things.
#[cfg(debug_assertions)]
{
use tauri::Manager;
_app.get_webview("main").unwrap().open_devtools();
app.get_webview("main").unwrap().open_devtools();
}
#[cfg(not(debug_assertions))]
{
_app.handle()
.plugin(tauri_plugin_updater::Builder::new().build())?;
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));
Ok(())
})
.invoke_handler(tauri::generate_handler![
get_user,
login,
read_toml,
read_txt_file,
read_dir_recursive,
show_in_folder,
])
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_shell::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");
.run(tauri::generate_context!())?;
Ok(())
}

21
src-tauri/src/state.rs Normal file
View File

@ -0,0 +1,21 @@
//! 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;
}
}

View File

@ -50,10 +50,26 @@
},
"identifier": "dev.zoo.modeling-app",
"plugins": {
"cli": {
"description": "Zoo Modeling App CLI",
"args": [
{
"short": "v",
"name": "verbose",
"description": "Verbosity level"
},
{
"name": "source",
"index": 1,
"takesValue": true
}
],
"subcommands": {}
},
"shell": {
"open": true
}
},
"productName": "Zoo Modeling App",
"version": "0.18.1"
"version": "0.19.4"
}

View File

@ -30,6 +30,7 @@ import SettingsAuthProvider from 'components/SettingsAuthProvider'
import LspProvider from 'components/LspProvider'
import { KclContextProvider } from 'lang/KclProvider'
import { BROWSER_PROJECT_NAME } from 'lib/constants'
import { getState, setState } from 'lib/tauri'
const router = createBrowserRouter([
{
@ -52,10 +53,29 @@ const router = createBrowserRouter([
children: [
{
path: paths.INDEX,
loader: () =>
isTauri()
loader: async () => {
const inTauri = 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.FILE + '/%2F' + BROWSER_PROJECT_NAME),
: redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
},
},
{
loader: fileLoader,

View File

@ -15,10 +15,10 @@ import {
import { useCommandsContext } from 'hooks/useCommandsContext'
import { fileMachine } from 'machines/fileMachine'
import { mkdir, remove, rename, create } from '@tauri-apps/plugin-fs'
import { readProject } from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
import { join, sep } from '@tauri-apps/api/path'
import { DEFAULT_FILE_NAME, FILE_EXT } from 'lib/constants'
import { getProjectInfo } from 'lib/tauri'
type MachineContext<T extends AnyStateMachine> = {
state: StateFrom<T>
@ -62,7 +62,7 @@ export const FileMachineProvider = ({
services: {
readFiles: async (context: ContextFrom<typeof fileMachine>) => {
const newFiles = isTauri()
? await readProject(context.project.path)
? (await getProjectInfo(context.project.path)).children
: []
return {
...context.project,

View File

@ -3,7 +3,7 @@ import { paths } from 'lib/paths'
import { ActionButton } from './ActionButton'
import Tooltip from './Tooltip'
import { Dispatch, useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useNavigate, useRouteLoaderData } from 'react-router-dom'
import { Dialog, Disclosure } from '@headlessui/react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronRight, faTrashAlt } from '@fortawesome/free-solid-svg-icons'
@ -133,18 +133,13 @@ const FileTreeItem = ({
project,
currentFile,
fileOrDir,
closePanel,
onDoubleClick,
level = 0,
}: {
project?: IndexLoaderData['project']
currentFile?: IndexLoaderData['file']
fileOrDir: FileEntry
closePanel: (
focusableElement?:
| HTMLElement
| React.MutableRefObject<HTMLElement | null>
| undefined
) => void
onDoubleClick?: () => void
level?: number
}) => {
const { send, context } = useFileContext()
@ -186,7 +181,7 @@ const FileTreeItem = ({
// Open kcl files
navigate(`${paths.FILE}/${encodeURIComponent(fileOrDir.path)}`)
}
closePanel()
onDoubleClick?.()
}
return (
@ -194,8 +189,10 @@ const FileTreeItem = ({
{fileOrDir.children === undefined ? (
<li
className={
'group m-0 p-0 border-solid border-0 hover:text-primary hover:bg-primary/5 focus-within:bg-primary/5 ' +
(isCurrentFile ? '!bg-primary/10 !text-primary' : '')
'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 ' +
(isCurrentFile
? '!bg-primary/10 !text-primary dark:!bg-primary/20 dark:!text-inherit'
: '')
}
>
{!isRenaming ? (
@ -227,9 +224,9 @@ const FileTreeItem = ({
{!isRenaming ? (
<Disclosure.Button
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' +
' 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' +
(context.selectedDirectory.path.includes(fileOrDir.path)
? ' ui-open:text-primary'
? ' ui-open:bg-primary/10'
: '')
}
style={{ paddingInlineStart: getIndentationCSS(level) }}
@ -293,7 +290,7 @@ const FileTreeItem = ({
fileOrDir={child}
project={project}
currentFile={currentFile}
closePanel={closePanel}
onDoubleClick={onDoubleClick}
level={level + 1}
key={level + '-' + child.path}
/>
@ -325,20 +322,8 @@ interface FileTreeProps {
) => void
}
export const FileTree = ({
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])
export const FileTreeMenu = () => {
const { send } = useFileContext()
async function createFile() {
send({ type: 'Create file', data: { name: '', makeDir: false } })
@ -348,58 +333,88 @@ export const FileTree = ({
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 (
<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">
<h2 className="flex-1 m-0 p-0 text-sm mono">Files</h2>
<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>
<FileTreeMenu />
</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>
)
}

View File

@ -94,10 +94,7 @@ export function HelpMenu(props: React.PropsWithChildren) {
if (isInProject) {
navigate('onboarding')
} else {
createAndOpenNewProject(
settings.context.app.projectDirectory.current,
navigate
)
createAndOpenNewProject(navigate)
}
}}
>

View File

@ -3,7 +3,7 @@ import type * as LSP from 'vscode-languageserver-protocol'
import React, { createContext, useMemo, useEffect, useContext } from 'react'
import { FromServer, IntoServer } from 'editor/plugins/lsp/codec'
import Client from '../editor/plugins/lsp/client'
import { DEV, TEST } from 'env'
import { TEST, VITE_KC_API_BASE_URL } from 'env'
import kclLanguage from 'editor/plugins/lsp/kcl/language'
import { copilotPlugin } from 'editor/plugins/lsp/copilot'
import { useStore } from 'useStore'
@ -103,7 +103,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
wasmUrl: wasmUrl(),
token: token,
baseUnit: defaultUnit.current,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Kcl,
@ -177,7 +177,7 @@ export const LspProvider = ({ children }: { children: React.ReactNode }) => {
const initEvent: CopilotWorkerOptions = {
wasmUrl: wasmUrl(),
token: token,
devMode: DEV,
apiBaseUrl: VITE_KC_API_BASE_URL,
}
lspWorker.postMessage({
worker: LspWorker.Copilot,

View File

@ -56,6 +56,7 @@ import toast from 'react-hot-toast'
import { EditorSelection } from '@uiw/react-codemirror'
import { CoreDumpManager } from 'lib/coredump'
import { useHotkeys } from 'react-hotkeys-hook'
import { useSearchParams } from 'react-router-dom'
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
type MachineContext<T extends AnyStateMachine> = {
@ -84,7 +85,12 @@ export const ModelingMachineProvider = ({
} = useSettingsAuthContext()
const token = auth?.context?.token
const streamRef = useRef<HTMLDivElement>(null)
let [searchParams] = useSearchParams()
const pool = searchParams.get('pool')
useSetupEngineManager(streamRef, token, {
pool: pool,
theme: theme.current,
highlightEdges: highlightEdges.current,
enableSSAO: enableSSAO.current,

View File

@ -10,21 +10,32 @@ import { KclEditorMenu } from 'components/ModelingSidebar/ModelingPanes/KclEdito
import { CustomIconName } from 'components/CustomIcon'
import { KclEditorPane } from 'components/ModelingSidebar/ModelingPanes/KclEditorPane'
import { ReactNode } from 'react'
import type { PaneType } from 'useStore'
import { MemoryPane } from './MemoryPane'
import { KclErrorsPane, LogsPane } from './LoggingPanes'
import { DebugPane } from './DebugPane'
import { FileTreeInner, FileTreeMenu } from 'components/FileTree'
export type Pane = {
id: PaneType
export type SidebarType =
| 'code'
| 'debug'
| 'export'
| 'files'
| 'kclErrors'
| 'logs'
| 'lspMessages'
| 'variables'
export type SidebarPane = {
id: SidebarType
title: string
icon: CustomIconName | IconDefinition
keybinding: string
Content: ReactNode | React.FC
Menu?: ReactNode | React.FC
keybinding: string
hideOnPlatform?: 'desktop' | 'web'
}
export const topPanes: Pane[] = [
export const topPanes: SidebarPane[] = [
{
id: 'code',
title: 'KCL Code',
@ -33,9 +44,18 @@ export const topPanes: Pane[] = [
keybinding: 'shift + c',
Menu: KclEditorMenu,
},
{
id: 'files',
title: 'Project Files',
icon: 'folder',
Content: FileTreeInner,
keybinding: 'shift + f',
Menu: FileTreeMenu,
hideOnPlatform: 'web',
},
]
export const bottomPanes: Pane[] = [
export const bottomPanes: SidebarPane[] = [
{
id: 'variables',
title: 'Variables',

View File

@ -2,13 +2,19 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Resizable } from 're-resizable'
import { useCallback, useEffect, useState } from 'react'
import { useHotkeys } from 'react-hotkeys-hook'
import { PaneType, useStore } from 'useStore'
import { useStore } from 'useStore'
import { Tab } from '@headlessui/react'
import { Pane, bottomPanes, topPanes } from './ModelingPanes'
import {
SidebarPane,
SidebarType,
bottomPanes,
topPanes,
} from './ModelingPanes'
import Tooltip from 'components/Tooltip'
import { ActionIcon } from 'components/ActionIcon'
import styles from './ModelingSidebar.module.css'
import { ModelingPane } from './ModelingPane'
import { isTauri } from 'lib/isTauri'
interface ModelingSidebarProps {
paneOpacity: '' | 'opacity-20' | 'opacity-40'
@ -52,7 +58,7 @@ export function ModelingSidebar({ paneOpacity }: ModelingSidebarProps) {
}
interface ModelingSidebarSectionProps {
panes: Pane[]
panes: SidebarPane[]
alignButtons?: 'start' | 'end'
}
@ -69,11 +75,11 @@ function ModelingSidebarSection({
}))
const foundOpenPane = openPanes.find((pane) => paneIds.includes(pane))
const [currentPane, setCurrentPane] = useState(
foundOpenPane || ('none' as PaneType | 'none')
foundOpenPane || ('none' as SidebarType | 'none')
)
const togglePane = useCallback(
(newPane: PaneType | 'none') => {
(newPane: SidebarType | 'none') => {
if (newPane === 'none') {
setOpenPanes(openPanes.filter((p) => p !== currentPane))
setCurrentPane('none')
@ -90,9 +96,15 @@ function ModelingSidebarSection({
// 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
const filteredPanes = showDebugPanel.current
? panes
: panes.filter((pane) => pane.id !== 'debug')
const filteredPanes = (
showDebugPanel.current ? panes : panes.filter((pane) => pane.id !== 'debug')
).filter(
(pane) =>
!pane.hideOnPlatform ||
(isTauri()
? pane.hideOnPlatform === 'web'
: pane.hideOnPlatform === 'desktop')
)
useEffect(() => {
if (
!showDebugPanel.current &&
@ -168,8 +180,8 @@ function ModelingSidebarSection({
}
interface ModelingPaneButtonProps {
paneConfig: Pane
currentPane: PaneType | 'none'
paneConfig: SidebarPane
currentPane: SidebarType | 'none'
togglePane: () => void
}

View File

@ -1,5 +1,4 @@
import { FormEvent, useEffect, useRef, useState } from 'react'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { paths } from 'lib/paths'
import { Link } from 'react-router-dom'
import { ActionButton } from './ActionButton'
@ -9,11 +8,11 @@ import {
faTrashAlt,
faX,
} from '@fortawesome/free-solid-svg-icons'
import { getPartsCount, readProject } from '../lib/tauriFS'
import { FILE_EXT } from 'lib/constants'
import { Dialog } from '@headlessui/react'
import { useHotkeys } from 'react-hotkeys-hook'
import Tooltip from './Tooltip'
import { Project } from 'wasm-lib/kcl/bindings/Project'
function ProjectCard({
project,
@ -21,17 +20,17 @@ function ProjectCard({
handleDeleteProject,
...props
}: {
project: ProjectWithEntryPointMetadata
project: Project
handleRenameProject: (
e: FormEvent<HTMLFormElement>,
f: ProjectWithEntryPointMetadata
f: Project
) => Promise<void>
handleDeleteProject: (f: ProjectWithEntryPointMetadata) => Promise<void>
handleDeleteProject: (f: Project) => Promise<void>
}) {
useHotkeys('esc', () => setIsEditing(false))
const [isEditing, setIsEditing] = useState(false)
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
const [numberOfParts, setNumberOfParts] = useState(1)
const [numberOfFiles, setNumberOfFiles] = useState(1)
const [numberOfFolders, setNumberOfFolders] = useState(0)
let inputRef = useRef<HTMLInputElement>(null)
@ -41,7 +40,8 @@ function ProjectCard({
void handleRenameProject(e, project).then(() => setIsEditing(false))
}
function getDisplayedTime(date: Date) {
function getDisplayedTime(dateStr: string) {
const date = new Date(dateStr)
const startOfToday = new Date()
startOfToday.setHours(0, 0, 0, 0)
return date.getTime() < startOfToday.getTime()
@ -50,15 +50,12 @@ function ProjectCard({
}
useEffect(() => {
async function getNumberOfParts() {
const { kclFileCount, kclDirCount } = getPartsCount(
await readProject(project.path)
)
setNumberOfParts(kclFileCount)
setNumberOfFolders(kclDirCount)
async function getNumberOfFiles() {
setNumberOfFiles(project.kcl_file_count)
setNumberOfFolders(project.directory_count)
}
void getNumberOfParts()
}, [project.path])
void getNumberOfFiles()
}, [project.kcl_file_count, project.directory_count])
useEffect(() => {
if (inputRef.current) {
@ -129,7 +126,7 @@ function ProjectCard({
{project.name?.replace(FILE_EXT, '')}
</Link>
<span className="text-chalkboard-60 text-xs">
{numberOfParts} part{numberOfParts === 1 ? '' : 's'}{' '}
{numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
{numberOfFolders > 0 &&
`/ ${numberOfFolders} folder${
numberOfFolders === 1 ? '' : 's'
@ -137,8 +134,8 @@ function ProjectCard({
</span>
<span className="text-chalkboard-60 text-xs">
Edited{' '}
{project.entrypointMetadata.mtime
? getDisplayedTime(project.entrypointMetadata.mtime)
{project.metadata && project.metadata?.modified
? getDisplayedTime(project.metadata.modified)
: 'never'}
</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">

View File

@ -1,10 +1,10 @@
import { fireEvent, render, screen } from '@testing-library/react'
import { BrowserRouter } from 'react-router-dom'
import ProjectSidebarMenu from './ProjectSidebarMenu'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { SettingsAuthProviderJest } from './SettingsAuthProvider'
import { APP_NAME } from 'lib/constants'
import { CommandBarProvider } from './CommandBar/CommandBarProvider'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const now = new Date()
const projectWellFormed = {
@ -14,29 +14,17 @@ const projectWellFormed = {
{
name: 'main.kcl',
path: '/some/path/Simple Box/main.kcl',
children: [],
},
],
entrypointMetadata: {
atime: now,
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,
metadata: {
created: now.toISOString(),
modified: now.toISOString(),
size: 32,
uid: 1,
fileAttributes: null,
},
} satisfies ProjectWithEntryPointMetadata
kcl_file_count: 1,
directory_count: 0,
} satisfies Project
describe('ProjectSidebarMenu tests', () => {
test('Renders the project name', () => {

View File

@ -133,13 +133,13 @@ function ProjectMenuPopover({
<p className="m-0 text-mono" data-testid="projectName">
{project?.name ? project.name : APP_NAME}
</p>
{project?.entrypointMetadata && (
{project?.metadata && project.metadata.created && (
<p
className="m-0 text-xs text-chalkboard-80 dark:text-chalkboard-40"
data-testid="createdAt"
>
Created{' '}
{project.entrypointMetadata.birthtime?.toLocaleDateString()}
{new Date(project.metadata.created).toLocaleDateString()}
</p>
)}
</div>

View File

@ -168,7 +168,7 @@ export const SettingsAuthProviderBase = ({
},
'Execute AST': () => kclManager.executeCode(true),
persistSettings: (context) =>
saveSettings(context, loadedProject?.project?.path),
saveSettings(context, loadedProject?.project?.name),
},
}
)

View File

@ -8,13 +8,13 @@ export interface KclWorkerOptions {
wasmUrl: string
token: string
baseUnit: UnitLength
devMode: boolean
apiBaseUrl: string
}
export interface CopilotWorkerOptions {
wasmUrl: string
token: string
devMode: boolean
apiBaseUrl: string
}
export enum LspWorkerEventType {

View File

@ -28,11 +28,11 @@ const initialise = async (wasmUrl: string) => {
export async function copilotLspRun(
config: ServerConfig,
token: string,
devMode: boolean = false
baseUrl: string
) {
try {
console.log('starting copilot lsp')
await copilot_lsp_run(config, token, devMode)
await copilot_lsp_run(config, token, baseUrl)
} catch (e: any) {
console.log('copilot lsp failed', e)
// 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,
token: string,
baseUnit: string,
devMode: boolean = false
baseUrl: string
) {
try {
console.log('start kcl lsp')
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, devMode)
await kcl_lsp_run(config, engineCommandManager, baseUnit, token, baseUrl)
} catch (e: any) {
console.log('kcl lsp failed', e)
// We can't restart here because a moved value, we should do this another way.
@ -80,12 +80,12 @@ onmessage = function (event) {
null,
kclData.token,
kclData.baseUnit,
kclData.devMode
kclData.apiBaseUrl
)
break
case LspWorker.Copilot:
let copilotData = eventData as CopilotWorkerOptions
copilotLspRun(config, copilotData.token, copilotData.devMode)
copilotLspRun(config, copilotData.token, copilotData.apiBaseUrl)
break
}
})

View File

@ -9,10 +9,12 @@ export function useSetupEngineManager(
streamRef: React.RefObject<HTMLDivElement>,
token?: string,
settings = {
pool: null,
theme: Themes.System,
highlightEdges: true,
enableSSAO: true,
} as {
pool: string | null
theme: Themes
highlightEdges: boolean
enableSSAO: boolean
@ -35,6 +37,12 @@ export function useSetupEngineManager(
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(() => {
// Load the engine command manager once with the initial width and height,
// then we do not want to reload it.

View File

@ -888,6 +888,7 @@ export class EngineCommandManager {
sceneCommandArtifacts: ArtifactMap = {}
outSequence = 1
inSequence = 1
pool?: string
engineConnection?: EngineConnection
defaultPlanes: DefaultPlanes | null = null
commandLogs: CommandLog[] = []
@ -914,8 +915,9 @@ export class EngineCommandManager {
callbacksEngineStateConnection: ((state: EngineConnectionState) => void)[] =
[]
constructor() {
constructor(pool?: string) {
this.engineConnection = undefined
this.pool = pool
}
private _camControlsCameraChange = () => {}
@ -972,7 +974,8 @@ export class EngineCommandManager {
}
const additionalSettings = settings.enableSSAO ? '&post_effect=ssao' : ''
const url = `${VITE_KC_API_WS_MODELING_URL}?video_res_width=${width}&video_res_height=${height}${additionalSettings}`
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}${pool}`
this.engineConnection = new EngineConnection({
engineCommandManager: this,
url,

View File

@ -1,8 +1,7 @@
import { readFile, exists as tauriExists } from '@tauri-apps/plugin-fs'
import { isTauri } from 'lib/isTauri'
import { join } from '@tauri-apps/api/path'
import { invoke } from '@tauri-apps/api/core'
import { FileEntry } from 'lib/types'
import { readDirRecursive } from 'lib/tauri'
/// 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
@ -69,9 +68,7 @@ class FileSystemManager {
throw new Error(`Error joining dir: ${error}`)
})
.then((p) => {
invoke<FileEntry[]>('read_dir_recursive', {
path: p,
})
readDirRecursive(p)
.catch((error) => {
throw new Error(`Error reading dir: ${error}`)
})

View File

@ -10,7 +10,11 @@ import init, {
make_default_planes,
coredump,
toml_stringify,
toml_parse,
default_app_settings,
parse_app_settings,
parse_project_settings,
default_project_settings,
parse_project_route,
} from '../wasm-lib/pkg/wasm_lib'
import { KCLError } from './errors'
import { KclError as RustKclError } from '../wasm-lib/kcl/bindings/KclError'
@ -26,6 +30,9 @@ import { CoreDumpManager } from 'lib/coredump'
import openWindow from 'lib/openWindow'
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
import { TEST } 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 { Value } from '../wasm-lib/kcl/bindings/Value'
@ -349,11 +356,53 @@ export function tomlStringify(toml: any): string {
}
}
export function tomlParse(toml: string): any {
export function defaultAppSettings(): Configuration {
try {
const parsed: any = toml_parse(toml)
return parsed
const settings: Configuration = default_app_settings()
return settings
} catch (e: any) {
throw new Error(`Error parsing toml: ${e}`)
throw new Error(`Error getting default app settings: ${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}`)
}
}

View File

@ -1,3 +1,5 @@
import { MouseControlType } from 'wasm-lib/kcl/bindings/MouseControlType'
const noModifiersPressed = (e: React.MouseEvent) =>
!e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey
@ -20,6 +22,29 @@ export const cameraSystems: CameraSystem[] = [
'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 {
description: string
callback: (e: React.MouseEvent) => boolean

View File

@ -8,8 +8,6 @@ export const MAX_PADDING = 7
* This is available for users to edit as a setting.
*/
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 */
export const BROWSER_PROJECT_NAME = 'browser'
/** Name given the temporary file in the browser version of the app */

View File

@ -49,6 +49,11 @@ export class CoreDumpManager {
return APP_VERSION
}
// Get the backend pool we've requested.
pool(): string {
return this.engineCommandManager.pool || ''
}
// Get the os information.
getOsInfo(): Promise<string> {
if (this.isTauri()) {

View File

@ -1,7 +1,11 @@
import { sep } from '@tauri-apps/api/path'
import { onboardingPaths } from 'routes/Onboarding/paths'
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
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 =
(routesObject: Record<string, string>) => (prepend: string) => {
@ -25,28 +29,23 @@ export const paths = {
} as const
export const BROWSER_PATH = `%2F${BROWSER_PROJECT_NAME}%2F${BROWSER_FILE_NAME}${FILE_EXT}`
export function getProjectMetaByRouteId(id?: string, defaultDir = '') {
export async function getProjectMetaByRouteId(
id?: string,
configuration?: Configuration
): Promise<ProjectRoute | undefined> {
if (!id) return undefined
const s = isTauri() ? sep() : '/'
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
const inTauri = isTauri()
return {
projectName,
projectPath,
currentFileName,
currentFilePath,
if (!configuration) {
configuration = inTauri
? await readAppSettingsFile()
: readLocalStorageAppSettingsFile()
}
const route = inTauri
? await parseProjectRoute(configuration, id)
: parseProjectRouteWasm(configuration, id)
return route
}

View File

@ -1,10 +1,5 @@
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom'
import {
FileEntry,
FileLoaderData,
HomeLoaderData,
IndexLoaderData,
} from './types'
import { FileLoaderData, HomeLoaderData, IndexLoaderData } from './types'
import { isTauri } from './isTauri'
import { getProjectMetaByRouteId, paths } from './paths'
import { BROWSER_PATH } from 'lib/paths'
@ -14,33 +9,38 @@ import {
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { loadAndValidateSettings } from './settings/settingsUtils'
import {
getInitialDefaultDir,
getProjectsInDir,
initializeProjectDirectory,
} from './tauriFS'
import makeUrlPathRelative from './makeUrlPathRelative'
import { join, sep } from '@tauri-apps/api/path'
import { readTextFile, stat } from '@tauri-apps/plugin-fs'
import { sep } from '@tauri-apps/api/path'
import { readTextFile } from '@tauri-apps/plugin-fs'
import { codeManager, kclManager } from 'lib/singletons'
import { fileSystemManager } from 'lang/std/fileSystemManager'
import { invoke } from '@tauri-apps/api/core'
import {
getProjectInfo,
initializeProjectDirectory,
listProjects,
} from './tauri'
import { createSettings } from './settings/initialSettings'
// The root loader simply resolves the settings and any errors that
// occurred during the settings load
export const settingsLoader: LoaderFunction = async ({
params,
}): ReturnType<typeof loadAndValidateSettings> => {
let settings = await loadAndValidateSettings()
}): Promise<
ReturnType<typeof createSettings> | ReturnType<typeof redirect>
> => {
let { settings, configuration } = await loadAndValidateSettings()
// 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
if (params.id) {
const defaultDir = settings.app.projectDirectory.current || ''
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
const projectPathData = await getProjectMetaByRouteId(
params.id,
configuration
)
if (projectPathData) {
const { projectPath } = projectPathData
settings = await loadAndValidateSettings(projectPath)
const { project_name } = projectPathData
const { settings: s } = await loadAndValidateSettings(project_name)
settings = s
}
}
@ -49,7 +49,7 @@ export const settingsLoader: LoaderFunction = async ({
// Redirect users to the appropriate onboarding page if they haven't completed it
export const onboardingRedirectLoader: ActionFunction = async (args) => {
const settings = await loadAndValidateSettings()
const { settings } = await loadAndValidateSettings()
const onboardingStatus = settings.app.onboardingStatus.current || ''
const notEnRouteToOnboarding = !args.request.url.includes(
paths.ONBOARDING.INDEX
@ -73,17 +73,19 @@ export const onboardingRedirectLoader: ActionFunction = async (args) => {
export const fileLoader: LoaderFunction = async ({
params,
}): Promise<FileLoaderData | Response> => {
let settings = await loadAndValidateSettings()
let { configuration } = await loadAndValidateSettings()
const defaultDir = settings.app.projectDirectory.current || '/'
const projectPathData = getProjectMetaByRouteId(params.id, defaultDir)
const projectPathData = await getProjectMetaByRouteId(
params.id,
configuration
)
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
if (!isBrowserProject && projectPathData) {
const { projectName, projectPath, currentFileName, currentFilePath } =
const { project_name, project_path, current_file_name, current_file_path } =
projectPathData
if (!currentFileName || !currentFilePath) {
if (!current_file_name || !current_file_path || !project_name) {
return redirect(
`${paths.FILE}/${encodeURIComponent(
`${params.id}${isTauri() ? sep() : '/'}${PROJECT_ENTRYPOINT}`
@ -93,35 +95,34 @@ export const fileLoader: LoaderFunction = async ({
// TODO: PROJECT_ENTRYPOINT is hardcoded
// until we support setting a project's entrypoint file
const code = await readTextFile(currentFilePath)
const entrypointMetadata = await stat(
await join(projectPath, PROJECT_ENTRYPOINT)
)
const children = await invoke<FileEntry[]>('read_dir_recursive', {
path: projectPath,
})
const code = await readTextFile(current_file_path)
// Update both the state and the editor's code.
// We explicitly do not write to the file here since we are loading from
// the file system and not the editor.
codeManager.updateCurrentFilePath(currentFilePath)
codeManager.updateCurrentFilePath(current_file_path)
codeManager.updateCodeStateEditor(code)
kclManager.executeCode(true)
// Set the file system manager to the project path
// So that WASM gets an updated path for operations
fileSystemManager.dir = projectPath
fileSystemManager.dir = project_path
const projectData: IndexLoaderData = {
code,
project: {
name: projectName,
path: projectPath,
children,
entrypointMetadata,
},
project: isTauri()
? await getProjectInfo(project_path, configuration)
: {
name: project_name,
path: project_path,
children: [],
kcl_file_count: 0,
directory_count: 0,
},
file: {
name: currentFileName,
path: currentFilePath,
name: current_file_name,
path: current_file_path,
children: [],
},
}
@ -140,6 +141,7 @@ export const fileLoader: LoaderFunction = async ({
file: {
name: BROWSER_FILE_NAME,
path: decodeURIComponent(BROWSER_PATH),
children: [],
},
}
}
@ -152,14 +154,12 @@ export const homeLoader: LoaderFunction = async (): Promise<
if (!isTauri()) {
return redirect(paths.FILE + '/%2F' + BROWSER_PROJECT_NAME)
}
const settings = await loadAndValidateSettings()
const { configuration } = await loadAndValidateSettings()
const projectDir = await initializeProjectDirectory(
settings.app.projectDirectory.current || (await getInitialDefaultDir())
)
const projectDir = await initializeProjectDirectory(configuration)
if (projectDir.path) {
const projects = await getProjectsInDir(projectDir.path)
if (projectDir) {
const projects = await listProjects(configuration)
return {
projects,

View File

@ -1,100 +1,228 @@
import {
getInitialDefaultDir,
getSettingsFilePaths,
readSettingsFile,
} from '../tauriFS'
import { Setting, createSettings, settings } from 'lib/settings/initialSettings'
import { SaveSettingsPayload, SettingsLevel } from './settingsTypes'
import { isTauri } from 'lib/isTauri'
import { remove, writeTextFile, exists } from '@tauri-apps/plugin-fs'
import { initPromise, tomlParse, tomlStringify } from 'lang/wasm'
import {
defaultAppSettings,
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'
/**
* We expect the settings to be stored in a TOML file
* or TOML-formatted string in localStorage
* under a top-level [settings] key.
* @param path
* @returns
*/
function getSettingsFromStorage(path: string) {
return isTauri()
? readSettingsFile(path)
: (tomlParse(localStorage.getItem(path) ?? '')
.settings as Partial<SaveSettingsPayload>)
* Convert from a rust settings struct into the JS settings struct.
* We do this because the JS settings type has all the fancy shit
* for hiding and showing settings.
**/
function configurationToSettingsPayload(
configuration: Configuration
): 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,
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,
},
}
}
export async function loadAndValidateSettings(projectPath?: string) {
const settings = createSettings()
settings.app.projectDirectory.default = await getInitialDefaultDir()
// First, get the settings data at the user and project level
const settingsFilePaths = await getSettingsFilePaths(projectPath)
function projectConfigurationToSettingsPayload(
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,
},
}
}
// Load the settings from the files
if (settingsFilePaths.user) {
await initPromise
const userSettings = await getSettingsFromStorage(settingsFilePaths.user)
if (userSettings) {
setSettingsAtLevel(settings, 'user', userSettings)
}
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()
}
// Load the project settings if they exist
if (settingsFilePaths.project) {
const projectSettings = await getSettingsFromStorage(
settingsFilePaths.project
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)
)
if (projectSettings) {
setSettingsAtLevel(settings, 'project', projectSettings)
}
return settings
}
}
export interface AppSettings {
settings: ReturnType<typeof createSettings>
configuration: Configuration
}
export async function loadAndValidateSettings(
projectName?: string
): Promise<AppSettings> {
const settings = createSettings()
const inTauri = isTauri()
if (!inTauri) {
// Make sure we have wasm initialized.
await initPromise
}
// 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
if (projectName) {
const projectSettings = inTauri
? await readProjectSettingsFile(appSettings, projectName)
: readLocalStorageProjectSettingsFile()
const projectSettingsPayload =
projectConfigurationToSettingsPayload(projectSettings)
setSettingsAtLevel(settings, 'project', projectSettingsPayload)
}
// Return the settings object
return settings
return { settings, configuration: appSettings }
}
export async function saveSettings(
allSettings: typeof settings,
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>
projectName?: string
) {
// Make sure we have wasm initialized.
await initPromise
if (changedSettings && Object.keys(changedSettings).length) {
if (isTauri()) {
await writeTextFile(
settingsFilePath,
tomlStringify({ settings: changedSettings })
)
}
localStorage.setItem(
settingsFilePath,
tomlStringify({ settings: changedSettings })
)
const inTauri = isTauri()
// Get the user settings.
const jsAppSettings = getChangedSettingsAtLevel(allSettings, 'user')
const tomlString = tomlStringify({ settings: jsAppSettings })
// Parse this as a Configuration.
const appSettings = parseAppSettings(tomlString)
// Write the app settings.
if (inTauri) {
await writeAppSettingsFile(appSettings)
} else {
if (isTauri() && (await exists(settingsFilePath))) {
await remove(settingsFilePath)
}
localStorage.removeItem(settingsFilePath)
localStorage.setItem(
localStorageAppSettingsPath(),
tomlStringify(appSettings)
)
}
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 {
localStorage.setItem(
localStorageProjectSettingsPath(),
tomlStringify(projectSettings)
)
}
}

View File

@ -3,7 +3,7 @@ import {
faArrowUp,
faCircle,
} from '@fortawesome/free-solid-svg-icons'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { Project } from 'wasm-lib/kcl/bindings/Project'
const DESC = ':desc'
@ -27,10 +27,7 @@ export function getNextSearchParams(currentSort: string, newSort: string) {
}
export function getSortFunction(sortBy: string) {
const sortByName = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
const sortByName = (a: Project, b: Project) => {
if (a.name && b.name) {
return sortBy.includes('desc')
? a.name.localeCompare(b.name)
@ -39,16 +36,13 @@ export function getSortFunction(sortBy: string) {
return 0
}
const sortByModified = (
a: ProjectWithEntryPointMetadata,
b: ProjectWithEntryPointMetadata
) => {
if (a.entrypointMetadata?.mtime && b.entrypointMetadata?.mtime) {
const sortByModified = (a: Project, b: Project) => {
if (a.metadata?.modified && b.metadata?.modified) {
const aDate = new Date(a.metadata.modified)
const bDate = new Date(b.metadata.modified)
return !sortBy || sortBy.includes('desc')
? b.entrypointMetadata.mtime.getTime() -
a.entrypointMetadata.mtime.getTime()
: a.entrypointMetadata.mtime.getTime() -
b.entrypointMetadata.mtime.getTime()
? bDate.getTime() - aDate.getTime()
: aDate.getTime() - bDate.getTime()
}
return 0
}

150
src/lib/tauri.ts Normal file
View File

@ -0,0 +1,150 @@
// 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,
})
}

View File

@ -1,11 +1,4 @@
import {
deepFileFilter,
getNextProjectIndex,
getPartsCount,
interpolateProjectNameWithIndex,
isRelevantFileOrDir,
} from './tauriFS'
import type { FileEntry } from './types'
import { getNextProjectIndex, interpolateProjectNameWithIndex } from './tauriFS'
import { MAX_PADDING } from './constants'
describe('Test project name utility functions', () => {
@ -31,18 +24,22 @@ describe('Test project name utility functions', () => {
{
name: 'new-project-04.kcl',
path: '/projects/new-project-04.kcl',
children: [],
},
{
name: 'new-project-007.kcl',
path: '/projects/new-project-007.kcl',
children: [],
},
{
name: 'new-project-05.kcl',
path: '/projects/new-project-05.kcl',
children: [],
},
{
name: 'new-project-0.kcl',
path: '/projects/new-project-0.kcl',
children: [],
},
]
@ -50,101 +47,3 @@ describe('Test project name utility functions', () => {
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,
})
})
})

View File

@ -1,154 +1,19 @@
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 { appConfigDir } from '@tauri-apps/api/path'
import { isTauri } from './isTauri'
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
import type { FileEntry } from 'lib/types'
import {
FILE_EXT,
INDEX_IDENTIFIER,
MAX_PADDING,
ONBOARDING_PROJECT_NAME,
PROJECT_ENTRYPOINT,
PROJECT_FOLDER,
RELEVANT_FILE_TYPES,
SETTINGS_FILE_EXT,
} from 'lib/constants'
import { SaveSettingsPayload, SettingsLevel } from './settings/settingsTypes'
import { initPromise, tomlParse } from 'lang/wasm'
import { bracket } from './exampleKcl'
import { paths } from './paths'
type PathWithPossibleError = {
path: string | null
error: Error | null
}
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
}
import {
createNewProjectDirectory,
listProjects,
readAppSettingsFile,
} from './tauri'
export const isHidden = (fileOrDir: FileEntry) =>
!!fileOrDir.name?.startsWith('.')
@ -156,97 +21,6 @@ export const isHidden = (fileOrDir: FileEntry) =>
export const isDir = (fileOrDir: FileEntry) =>
'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:
// The main.kcl file is always first, then files, then directories
// Files and directories are sorted alphabetically
@ -279,47 +53,6 @@ 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
// replacing any instances of "$n" with a regex to match any number
function interpolateProjectName(projectName: string) {
@ -373,55 +106,6 @@ function getPaddedIdentifierRegExp() {
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) {
const user = isTauri() ? await appConfigDir() : '/'
const project = projectPath !== undefined ? projectPath : undefined
@ -433,18 +117,15 @@ export async function getSettingsFolderPaths(projectPath?: string) {
}
export async function createAndOpenNewProject(
projectDirectory: string,
navigate: (path: string) => void
) {
const projects = await getProjectsInDir(projectDirectory)
const nextIndex = await getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
const configuration = await readAppSettingsFile()
const projects = await listProjects(configuration)
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
const name = interpolateProjectNameWithIndex(
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProject(
await join(projectDirectory, name),
bracket
)
const newFile = await createNewProjectDirectory(name, bracket, configuration)
navigate(`${paths.FILE}/${encodeURIComponent(newFile.path)}`)
}

View File

@ -1,9 +1,26 @@
import { AppTheme } from 'wasm-lib/kcl/bindings/AppTheme'
export enum Themes {
Light = 'light',
Dark = 'dark',
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
export function getSystemTheme(): Exclude<Themes, 'system'> {
return typeof globalThis.window !== 'undefined' &&

View File

@ -1,35 +1,22 @@
import { type FileInfo } from '@tauri-apps/plugin-fs'
import { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export type { FileEntry } from 'wasm-lib/kcl/bindings/FileEntry'
export type IndexLoaderData = {
code: string | null
project?: ProjectWithEntryPointMetadata
project?: Project
file?: FileEntry
}
export type FileLoaderData = {
code: string | null
project?: FileEntry | ProjectWithEntryPointMetadata
project?: FileEntry | Project
file?: FileEntry
}
export type ProjectWithEntryPointMetadata = FileEntry & {
entrypointMetadata: FileInfo
}
export type HomeLoaderData = {
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[]
projects: Project[]
}
// From the very helpful @jcalz on StackOverflow: https://stackoverflow.com/a/58436959/22753272

View File

@ -2,8 +2,8 @@ import { createMachine, assign } from 'xstate'
import { Models } from '@kittycad/lib'
import withBaseURL from '../lib/withBaseURL'
import { isTauri } from 'lib/isTauri'
import { invoke } from '@tauri-apps/api/core'
import { VITE_KC_API_BASE_URL } from 'env'
import { getUser as getUserTauri } from 'lib/tauri'
const SKIP_AUTH =
import.meta.env.VITE_KC_SKIP_AUTH === 'true' && import.meta.env.DEV
@ -129,10 +129,7 @@ async function getUser(context: UserContext) {
})
.then((res) => res.json())
.catch((err) => console.error('error from Browser getUser', err))
: 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))
: getUserTauri(context.token, VITE_KC_API_BASE_URL)
const user = await userPromise

View File

@ -1,5 +1,6 @@
import { assign, createMachine } from 'xstate'
import type { FileEntry, ProjectWithEntryPointMetadata } from 'lib/types'
import type { FileEntry } from 'lib/types'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export const fileMachine = createMachine(
{
@ -9,7 +10,7 @@ export const fileMachine = createMachine(
initial: 'Reading files',
context: {
project: {} as ProjectWithEntryPointMetadata,
project: {} as Project,
selectedDirectory: {} as FileEntry,
},
@ -154,7 +155,7 @@ export const fileMachine = createMachine(
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-files'
data: ProjectWithEntryPointMetadata
data: Project
}
| { type: 'assign'; data: { [key: string]: any } }
| { type: 'Refresh' },

View File

@ -1,6 +1,6 @@
import { assign, createMachine } from 'xstate'
import { type ProjectWithEntryPointMetadata } from 'lib/types'
import { HomeCommandSchema } from 'lib/commandBarConfigs/homeCommandConfig'
import { Project } from 'wasm-lib/kcl/bindings/Project'
export const homeMachine = createMachine(
{
@ -10,7 +10,7 @@ export const homeMachine = createMachine(
initial: 'Reading projects',
context: {
projects: [] as ProjectWithEntryPointMetadata[],
projects: [] as Project[],
defaultProjectName: '',
defaultDirectory: '',
},
@ -145,7 +145,7 @@ export const homeMachine = createMachine(
| { type: 'navigate'; data: { name: string } }
| {
type: 'done.invoke.read-projects'
data: ProjectWithEntryPointMetadata[]
data: Project[]
}
| { type: 'assign'; data: { [key: string]: any } },
},
@ -157,7 +157,7 @@ export const homeMachine = createMachine(
{
actions: {
setProjects: assign((_, event) => {
return { projects: event.data as ProjectWithEntryPointMetadata[] }
return { projects: event.data as Project[] }
}),
},
}

View File

@ -1,11 +1,9 @@
import { FormEvent, useEffect } from 'react'
import { remove, rename } from '@tauri-apps/plugin-fs'
import {
createNewProject,
getNextProjectIndex,
interpolateProjectNameWithIndex,
doesProjectNameNeedInterpolated,
getProjectsInDir,
} from '../lib/tauriFS'
import { ActionButton } from '../components/ActionButton'
import { faArrowDown, faPlus } from '@fortawesome/free-solid-svg-icons'
@ -14,10 +12,7 @@ import { AppHeader } from '../components/AppHeader'
import ProjectCard from '../components/ProjectCard'
import { useLoaderData, useNavigate, useSearchParams } from 'react-router-dom'
import { Link } from 'react-router-dom'
import {
type ProjectWithEntryPointMetadata,
type HomeLoaderData,
} from 'lib/types'
import { type HomeLoaderData } from 'lib/types'
import Loading from '../components/Loading'
import { useMachine } from '@xstate/react'
import { homeMachine } from '../machines/homeMachine'
@ -39,6 +34,8 @@ import { kclManager } from 'lib/singletons'
import { useLspContext } from 'components/LspProvider'
import { useRefreshSettings } from 'hooks/useRefreshSettings'
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,
// as defined in Router.tsx, so we can use the Tauri APIs and types.
@ -94,7 +91,7 @@ const Home = () => {
},
services: {
readProjects: async (context: ContextFrom<typeof homeMachine>) =>
getProjectsInDir(context.defaultDirectory),
listProjects(),
createProject: async (
context: ContextFrom<typeof homeMachine>,
event: EventFrom<typeof homeMachine, 'Create project'>
@ -110,7 +107,7 @@ const Home = () => {
name = interpolateProjectNameWithIndex(name, nextIndex)
}
await createNewProject(await join(context.defaultDirectory, name))
await createNewProjectDirectory(name)
return `Successfully created "${name}"`
},
@ -181,7 +178,7 @@ const Home = () => {
async function handleRenameProject(
e: FormEvent<HTMLFormElement>,
project: ProjectWithEntryPointMetadata
project: Project
) {
const { newProjectName } = Object.fromEntries(
new FormData(e.target as HTMLFormElement)
@ -192,7 +189,7 @@ const Home = () => {
})
}
async function handleDeleteProject(project: ProjectWithEntryPointMetadata) {
async function handleDeleteProject(project: Project) {
send('Delete project', { data: { name: project.name || '' } })
}
@ -284,7 +281,7 @@ const Home = () => {
icon={{ icon: faPlus, iconClassName: 'p-1 w-4' }}
data-testid="home-new-file"
>
New file
New project
</ActionButton>
</>
)}

View File

@ -14,7 +14,11 @@ export default function CodeEditor() {
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className="fixed inset-0 bg-black opacity-50 dark:opacity-80 pointer-events-none"
style={{ clipPath: useBackdropHighlight('code-pane') }}
style={
{
/*clipPath: useBackdropHighlight('code-pane')*/
}
}
></div>
<div
className={

View File

@ -15,7 +15,11 @@ export default function InteractiveNumbers() {
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className="fixed inset-0 bg-black opacity-50 pointer-events-none"
style={{ clipPath: useBackdropHighlight('code-pane') }}
style={
{
/*clipPath: useBackdropHighlight('code-pane')*/
}
}
></div>
<div
className={

View File

@ -4,9 +4,7 @@ import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { Themes, getSystemTheme } from 'lib/theme'
import { bracket } from 'lib/exampleKcl'
import {
createNewProject,
getNextProjectIndex,
getProjectsInDir,
interpolateProjectNameWithIndex,
} from 'lib/tauriFS'
import { isTauri } from 'lib/isTauri'
@ -20,33 +18,21 @@ import {
ONBOARDING_PROJECT_NAME,
PROJECT_ENTRYPOINT,
} from 'lib/constants'
import { createNewProjectDirectory, listProjects } from 'lib/tauri'
function OnboardingWithNewFile() {
const navigate = useNavigate()
const dismiss = useDismiss()
const next = useNextClick(onboardingPaths.INDEX)
const {
settings: {
context: {
app: { projectDirectory },
},
},
} = useSettingsAuthContext()
async function createAndOpenNewProject() {
const projects = await getProjectsInDir(projectDirectory.current)
const nextIndex = await getNextProjectIndex(
ONBOARDING_PROJECT_NAME,
projects
)
const projects = await listProjects()
const nextIndex = getNextProjectIndex(ONBOARDING_PROJECT_NAME, projects)
const name = interpolateProjectNameWithIndex(
ONBOARDING_PROJECT_NAME,
nextIndex
)
const newFile = await createNewProject(
await join(projectDirectory.current, name),
bracket
)
const newFile = await createNewProjectDirectory(name, bracket)
navigate(
`${paths.FILE}/${encodeURIComponent(
await join(newFile.path, PROJECT_ENTRYPOINT)

View File

@ -31,7 +31,11 @@ export default function ParametricModeling() {
<div className="fixed grid justify-end items-center inset-0 z-50 pointer-events-none">
<div
className="fixed inset-0 bg-black dark:bg-black-80 opacity-50 pointer-events-none"
style={{ clipPath: useBackdropHighlight('code-pane') }}
style={
{
/*clipPath: useBackdropHighlight('code-pane')*/
}
}
></div>
<div
className={

View File

@ -10,15 +10,10 @@ import { useHotkeys } from 'react-hotkeys-hook'
import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { useDotDotSlash } from 'hooks/useDotDotSlash'
import {
createAndOpenNewProject,
getInitialDefaultDir,
getSettingsFolderPaths,
} from 'lib/tauriFS'
import { createAndOpenNewProject, getSettingsFolderPaths } from 'lib/tauriFS'
import { sep } from '@tauri-apps/api/path'
import { isTauri } from 'lib/isTauri'
import toast from 'react-hot-toast'
import { invoke } from '@tauri-apps/api/core'
import React, { Fragment, useMemo, useRef, useState } from 'react'
import { Setting } from 'lib/settings/initialSettings'
import decamelize from 'decamelize'
@ -31,6 +26,7 @@ import {
shouldHideSetting,
shouldShowSettingInput,
} from 'lib/settings/settingsUtils'
import { getInitialDefaultDir, showInFolder } from 'lib/tauri'
export const APP_VERSION = import.meta.env.PACKAGE_VERSION || 'unknown'
@ -70,7 +66,7 @@ export const Settings = () => {
if (isFileSettings) {
navigate(dotDotSlash(1) + paths.ONBOARDING.INDEX)
} else {
createAndOpenNewProject(context.app.projectDirectory.current, navigate)
createAndOpenNewProject(navigate)
}
}
@ -302,9 +298,7 @@ export const Settings = () => {
? decodeURIComponent(projectPath)
: undefined
)
void invoke('show_in_folder', {
path: paths[settingsLevel],
})
showInFolder(paths[settingsLevel])
}}
icon={{
icon: 'folder',

View File

@ -1,11 +1,11 @@
import { ActionButton } from '../components/ActionButton'
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 { Themes, getSystemTheme } from '../lib/theme'
import { paths } from 'lib/paths'
import { useSettingsAuthContext } from 'hooks/useSettingsAuthContext'
import { APP_NAME } from 'lib/constants'
import { login } from 'lib/tauri'
const SignIn = () => {
const {
@ -28,9 +28,7 @@ const SignIn = () => {
const signInTauri = async () => {
// We want to invoke our command to login via device auth.
try {
const token: string = await invoke('login', {
host: VITE_KC_API_BASE_URL,
})
const token: string = await login(VITE_KC_API_BASE_URL)
send({ type: 'Log in', token })
} catch (error) {
console.error('Error with login button', error)

View File

@ -9,6 +9,7 @@ import {
import { enginelessExecutor } from './lib/testHelpers'
import { EngineCommandManager } from './lang/std/engineConnection'
import { KCLError } from './lang/errors'
import { SidebarType } from 'components/ModelingSidebar/ModelingPanes'
export type ToolTip =
| 'lineTo'
@ -44,14 +45,6 @@ export const toolTips = [
'tangentialArcTo',
] as any as ToolTip[]
export type PaneType =
| 'code'
| 'variables'
| 'debug'
| 'kclErrors'
| 'logs'
| 'lspMessages'
export interface StoreState {
mediaStream?: MediaStream
setMediaStream: (mediaStream: MediaStream) => void
@ -77,8 +70,8 @@ export interface StoreState {
showHomeMenu: boolean
setHomeShowMenu: (showMenu: boolean) => void
openPanes: PaneType[]
setOpenPanes: (panes: PaneType[]) => void
openPanes: SidebarType[]
setOpenPanes: (panes: SidebarType[]) => void
homeMenuItems: {
name: string
path: string

View File

@ -582,7 +582,7 @@ dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
"strsim 0.11.0",
"unicase",
"unicode-width",
]
@ -849,6 +849,41 @@ dependencies = [
"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]]
name = "dashmap"
version = "5.5.3"
@ -927,7 +962,7 @@ dependencies = [
[[package]]
name = "derive-docs"
version = "0.1.16"
version = "0.1.17"
dependencies = [
"Inflector",
"anyhow",
@ -1664,6 +1699,12 @@ dependencies = [
"cc",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "idna"
version = "0.5.0"
@ -1854,7 +1895,7 @@ dependencies = [
[[package]]
name = "kcl-lib"
version = "0.1.50"
version = "0.1.53"
dependencies = [
"anyhow",
"approx 0.5.1",
@ -1895,11 +1936,13 @@ dependencies = [
"thiserror",
"tokio",
"tokio-tungstenite",
"toml",
"tower-lsp",
"ts-rs",
"twenty-twenty",
"url",
"uuid",
"validator",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
@ -1931,6 +1974,7 @@ dependencies = [
"bigdecimal",
"bytes",
"chrono",
"clap",
"data-encoding",
"format_serde_error",
"futures",
@ -3721,6 +3765,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9557cb6521e8d009c51a8666f09356f4b817ba9ba0981a305bd86aee47bd35c"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strsim"
version = "0.11.0"
@ -4337,6 +4387,7 @@ version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6"
dependencies = [
"chrono",
"thiserror",
"ts-rs-macros",
"url",
@ -4521,6 +4572,36 @@ dependencies = [
"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]]
name = "valuable"
version = "0.1.0"
@ -4646,6 +4727,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bson",
"clap",
"console_error_panic_hook",
"futures",
"gloo-utils",

View File

@ -11,6 +11,7 @@ crate-type = ["cdylib"]
[dependencies]
bson = { version = "2.10.0", features = ["uuid-1", "chrono"] }
clap = "4.5.4"
gloo-utils = "0.2.0"
kcl-lib = { path = "kcl" }
kittycad = { workspace = true }

View File

@ -1,7 +1,7 @@
[package]
name = "derive-docs"
description = "A tool for generating documentation from Rust derive macros"
version = "0.1.16"
version = "0.1.17"
edition = "2021"
license = "MIT"
repository = "https://github.com/KittyCAD/modeling-app"

View File

@ -775,16 +775,11 @@ fn generate_code_block_test(
if let Ok(addr) = std::env::var("LOCAL_ENGINE_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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone()).await.unwrap();
let ctx = crate::executor::ExecutorContext::new(&client, Default::default()).await.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_show {
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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -97,16 +91,10 @@ mod test_examples_show {
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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_show {
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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,17 +20,11 @@ mod test_examples_my_func {
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.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -98,16 +92,10 @@ mod test_examples_my_func {
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.\nmyFunc").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -21,17 +21,11 @@ mod test_examples_import {
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.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -100,16 +94,10 @@ mod test_examples_import {
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.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,17 +20,11 @@ mod test_examples_line_to {
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.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -98,16 +92,10 @@ mod test_examples_line_to {
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.\nlineTo").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_min {
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.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();
@ -97,16 +91,10 @@ mod test_examples_min {
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.\nmin").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_show {
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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_import {
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.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_import {
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.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_import {
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.\nimport").unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

View File

@ -20,16 +20,10 @@ mod test_examples_show {
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 parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let units = kittycad::types::UnitLength::Mm;
let ctx = crate::executor::ExecutorContext::new(ws, units.clone())
let ctx = crate::executor::ExecutorContext::new(&client, Default::default())
.await
.unwrap();
ctx.run(program, None).await.unwrap();

Some files were not shown because too many files have changed in this diff Show More